Compare commits
No commits in common. 'master' and 'v5stable' have entirely different histories.
@ -1,23 +0,0 @@ |
|||||||
Copyright (c) 2014, Ondřej Hruška (MightyPork), <ondra@ondrovo.com> |
|
||||||
All rights reserved. |
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without |
|
||||||
modification, are permitted provided that the following conditions are met: |
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright notice, this |
|
||||||
list of conditions and the following disclaimer. |
|
||||||
|
|
||||||
2. Redistributions in binary form must reproduce the above copyright notice, |
|
||||||
this list of conditions and the following disclaimer in the documentation |
|
||||||
and/or other materials provided with the distribution. |
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND |
|
||||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
||||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
@ -1,20 +0,0 @@ |
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> |
|
||||||
<jardesc> |
|
||||||
<jar path="Rogue/build/in/build.jar"/> |
|
||||||
<options buildIfNeeded="true" compress="false" descriptionLocation="/Rogue/build/export_new.jardesc" exportErrors="true" exportWarnings="true" includeDirectoryEntries="true" overwrite="true" saveDescription="true" storeRefactorings="false" useSourceFolders="false"/> |
|
||||||
<storedRefactorings deprecationInfo="true" structuralOnly="false"/> |
|
||||||
<selectedProjects/> |
|
||||||
<manifest generateManifest="true" manifestLocation="" manifestVersion="1.0" reuseManifest="false" saveManifest="false" usesManifest="true"> |
|
||||||
<sealing sealJar="false"> |
|
||||||
<packagesToSeal/> |
|
||||||
<packagesToUnSeal/> |
|
||||||
</sealing> |
|
||||||
</manifest> |
|
||||||
<selectedElements exportClassFiles="true" exportJavaFiles="false" exportOutputFolder="false"> |
|
||||||
<javaElement handleIdentifier="=DynMath/src"/> |
|
||||||
<javaElement handleIdentifier="=Rogue/src"/> |
|
||||||
<javaElement handleIdentifier="=Ion/src"/> |
|
||||||
<folder path="/Rogue/res"/> |
|
||||||
<file path="/Rogue/LICENSE.txt"/> |
|
||||||
</selectedElements> |
|
||||||
</jardesc> |
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,246 @@ |
|||||||
|
package mightypork.gamecore.core; |
||||||
|
|
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import mightypork.gamecore.input.KeyStroke; |
||||||
|
import mightypork.gamecore.input.Keys; |
||||||
|
import mightypork.gamecore.logging.Log; |
||||||
|
import mightypork.gamecore.util.files.config.Property; |
||||||
|
import mightypork.gamecore.util.files.config.PropertyManager; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Static application configuration |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public class Config { |
||||||
|
|
||||||
|
/** |
||||||
|
* Config setup. Used to populate the config file. |
||||||
|
*/ |
||||||
|
public static interface ConfigSetup { |
||||||
|
|
||||||
|
void addOptions(PropertyManager prop); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Key configurator access |
||||||
|
*/ |
||||||
|
public static class KeyOpts { |
||||||
|
|
||||||
|
private KeyOpts() |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public void add(String cfgKey, String dataString) |
||||||
|
{ |
||||||
|
add(cfgKey, dataString, null); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public void add(String cfgKey, String dataString, String comment) |
||||||
|
{ |
||||||
|
final KeyProperty kprop = new KeyProperty(prefixKey(cfgKey), KeyStroke.createFromDataString(dataString), comment); |
||||||
|
strokes.put(prefixKey(cfgKey), kprop); |
||||||
|
cfg.putProperty(kprop); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Key configurator |
||||||
|
*/ |
||||||
|
public static interface KeySetup { |
||||||
|
|
||||||
|
public void addKeys(KeyOpts keys); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Key property.<br> |
||||||
|
* The stored value must be invariant ({@link KeyStroke} is mutable). |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public static class KeyProperty extends Property<KeyStroke> { |
||||||
|
|
||||||
|
public KeyProperty(String key, KeyStroke defaultValue, String comment) |
||||||
|
{ |
||||||
|
super(key, defaultValue, comment); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public KeyStroke decode(String string, KeyStroke defval) |
||||||
|
{ |
||||||
|
if (string != null) { |
||||||
|
// keep it invariant
|
||||||
|
|
||||||
|
final int backup_key = getValue().getKey(); |
||||||
|
final int backup_mod = getValue().getMod(); |
||||||
|
|
||||||
|
getValue().fromDataString(string); |
||||||
|
if (getValue().getKey() == Keys.NONE) { |
||||||
|
getValue().setTo(backup_key, backup_mod); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return getValue(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public String encode(KeyStroke value) |
||||||
|
{ |
||||||
|
return value.toDataString(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public void setValue(Object value) |
||||||
|
{ |
||||||
|
// keep it invariant
|
||||||
|
getValue().setTo(((KeyStroke) value).getKey(), ((KeyStroke) value).getMod()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public static Config.KeyOpts keyOpts = new Config.KeyOpts(); |
||||||
|
public static Map<String, KeyProperty> strokes = new HashMap<>(); |
||||||
|
|
||||||
|
private static PropertyManager cfg; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Initialize property manger for a file |
||||||
|
* |
||||||
|
* @param file config file |
||||||
|
* @param comment file comment |
||||||
|
*/ |
||||||
|
public static void init(File file, String comment) |
||||||
|
{ |
||||||
|
cfg = new PropertyManager(file, comment); |
||||||
|
cfg.cfgNewlineBeforeComments(true); |
||||||
|
cfg.cfgSeparateSections(true); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* populate config with keys from a key setup |
||||||
|
* |
||||||
|
* @param keys key setup to add |
||||||
|
*/ |
||||||
|
public static void registerKeys(KeySetup keys) |
||||||
|
{ |
||||||
|
keys.addKeys(Config.keyOpts); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Populate config with options from a config setup |
||||||
|
* |
||||||
|
* @param conf config setup to add |
||||||
|
*/ |
||||||
|
public static void registerOptions(ConfigSetup conf) |
||||||
|
{ |
||||||
|
conf.addOptions(Config.cfg); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Load config from file |
||||||
|
*/ |
||||||
|
public static void load() |
||||||
|
{ |
||||||
|
cfg.load(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Save config to file |
||||||
|
*/ |
||||||
|
public static void save() |
||||||
|
{ |
||||||
|
Log.f3("Saving config."); |
||||||
|
cfg.save(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Get an option for key |
||||||
|
* |
||||||
|
* @param key |
||||||
|
* @return option value |
||||||
|
*/ |
||||||
|
public static <T> T getValue(String key) |
||||||
|
{ |
||||||
|
try { |
||||||
|
if (cfg.getProperty(key) == null) { |
||||||
|
throw new IllegalArgumentException("No such property: " + key); |
||||||
|
} |
||||||
|
|
||||||
|
return cfg.getValue(key); |
||||||
|
} catch (final ClassCastException cce) { |
||||||
|
throw new RuntimeException("Property of incompatible type: " + key); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Set option to a value |
||||||
|
* |
||||||
|
* @param key option key |
||||||
|
* @param value value to set |
||||||
|
*/ |
||||||
|
public static <T> void setValue(String key, T value) |
||||||
|
{ |
||||||
|
if (cfg.getProperty(key) == null) { |
||||||
|
throw new IllegalArgumentException("No such property: " + key); |
||||||
|
} |
||||||
|
|
||||||
|
cfg.setValue(key, value); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private static String prefixKey(String cfgKey) |
||||||
|
{ |
||||||
|
return "key." + cfgKey; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Get keystroke for name |
||||||
|
* |
||||||
|
* @param cfgKey stroke identifier in config file |
||||||
|
* @return the stroke |
||||||
|
*/ |
||||||
|
public static KeyStroke getKey(String cfgKey) |
||||||
|
{ |
||||||
|
final KeyProperty kp = strokes.get(prefixKey(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 static void setKey(String cfgKey, int key, int mod) |
||||||
|
{ |
||||||
|
final Config.KeyProperty kp = strokes.get(prefixKey(cfgKey)); |
||||||
|
if (kp == null) { |
||||||
|
throw new IllegalArgumentException("No such stroke: " + cfgKey); |
||||||
|
} |
||||||
|
|
||||||
|
kp.getValue().setTo(key, mod); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,117 @@ |
|||||||
|
package mightypork.gamecore.core; |
||||||
|
|
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import mightypork.gamecore.logging.Log; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Static application workdir accessor. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public class WorkDir { |
||||||
|
|
||||||
|
/** |
||||||
|
* Route configurator. |
||||||
|
*/ |
||||||
|
public static interface RouteSetup { |
||||||
|
|
||||||
|
public void addRoutes(RouteOpts routeOpts); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Route configurator access |
||||||
|
*/ |
||||||
|
public static class RouteOpts { |
||||||
|
|
||||||
|
public void addPath(String alias, String path) |
||||||
|
{ |
||||||
|
WorkDir.addPath(alias, path); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public static RouteOpts routeOpts = new RouteOpts(); |
||||||
|
private static File workdir; |
||||||
|
private static Map<String, String> namedPaths = new HashMap<>(); |
||||||
|
|
||||||
|
|
||||||
|
public static void init(File workdir) |
||||||
|
{ |
||||||
|
WorkDir.workdir = workdir; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Add a path alias (dir or file) |
||||||
|
* |
||||||
|
* @param alias path alias |
||||||
|
* @param path path relative to workdir |
||||||
|
*/ |
||||||
|
public static void addPath(String alias, String path) |
||||||
|
{ |
||||||
|
namedPaths.put(alias, path); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public static void registerRoutes(RouteSetup rs) |
||||||
|
{ |
||||||
|
rs.addRoutes(routeOpts); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* 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; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
package mightypork.gamecore.core.events; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.core.modules.MainLoop; |
||||||
|
import mightypork.gamecore.eventbus.BusEvent; |
||||||
|
import mightypork.gamecore.eventbus.event_flags.SingleReceiverEvent; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Request to execute given {@link Runnable} in main loop. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
@SingleReceiverEvent |
||||||
|
public class MainLoopRequest extends BusEvent<MainLoop> { |
||||||
|
|
||||||
|
private final Runnable task; |
||||||
|
private final boolean priority; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @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); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
package mightypork.gamecore.core.events; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.core.modules.MainLoop; |
||||||
|
import mightypork.gamecore.eventbus.BusEvent; |
||||||
|
import mightypork.gamecore.eventbus.event_flags.NonConsumableEvent; |
||||||
|
import mightypork.gamecore.eventbus.event_flags.SingleReceiverEvent; |
||||||
|
import mightypork.gamecore.resources.audio.SoundSystem; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Shutdown request, non-interactive. Shutdown needs to execute on GL thread for |
||||||
|
* display to deinit properly. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
@SingleReceiverEvent |
||||||
|
@NonConsumableEvent |
||||||
|
public class ShudownRequest extends BusEvent<MainLoop> { |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleBy(final MainLoop handler) |
||||||
|
{ |
||||||
|
handler.queueTask(new Runnable() { |
||||||
|
|
||||||
|
@Override |
||||||
|
public void run() |
||||||
|
{ |
||||||
|
handler.shutdown(); |
||||||
|
} |
||||||
|
}, true); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
package mightypork.gamecore.core.events; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.eventbus.BusEvent; |
||||||
|
import mightypork.gamecore.eventbus.EventBus; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* User quit request. This event is triggered when user clicks the "close" |
||||||
|
* titlebar button, and if no client consumes it, the application will be shut |
||||||
|
* down. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public class UserQuitRequest extends BusEvent<UserQuitRequestListener> { |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void handleBy(UserQuitRequestListener handler) |
||||||
|
{ |
||||||
|
handler.onQuitRequest(this); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public void onDispatchComplete(EventBus bus) |
||||||
|
{ |
||||||
|
if (!isConsumed()) { |
||||||
|
bus.send(new ShudownRequest()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
package mightypork.gamecore.core.events; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Quit request listener; implementing client can abort shutdown. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public interface UserQuitRequestListener { |
||||||
|
|
||||||
|
/** |
||||||
|
* Intercept quit request.<br> |
||||||
|
* Consume the event to abort shutdown (ie. ask user to save) |
||||||
|
* |
||||||
|
* @param event quit request event. |
||||||
|
*/ |
||||||
|
void onQuitRequest(UserQuitRequest event); |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
package mightypork.gamecore.core.modules; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.eventbus.BusAccess; |
||||||
|
import mightypork.gamecore.input.InputSystem; |
||||||
|
import mightypork.gamecore.render.DisplaySystem; |
||||||
|
import mightypork.gamecore.resources.audio.SoundSystem; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* App interface visible to subsystems |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public interface AppAccess extends BusAccess { |
||||||
|
|
||||||
|
/** |
||||||
|
* @return sound system |
||||||
|
*/ |
||||||
|
abstract SoundSystem getSoundSystem(); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @return input system |
||||||
|
*/ |
||||||
|
abstract InputSystem getInput(); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @return display system |
||||||
|
*/ |
||||||
|
abstract DisplaySystem getDisplay(); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Quit to OS<br> |
||||||
|
* Destroy app & exit VM |
||||||
|
*/ |
||||||
|
abstract void shutdown(); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,65 @@ |
|||||||
|
package mightypork.gamecore.core.modules; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.eventbus.EventBus; |
||||||
|
import mightypork.gamecore.input.InputSystem; |
||||||
|
import mightypork.gamecore.render.DisplaySystem; |
||||||
|
import mightypork.gamecore.resources.audio.SoundSystem; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* App access adapter (defualt {@link AppAccess} implementation) |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public class AppAccessAdapter implements AppAccess { |
||||||
|
|
||||||
|
private final AppAccess app; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @param app app access |
||||||
|
*/ |
||||||
|
public AppAccessAdapter(AppAccess app) |
||||||
|
{ |
||||||
|
if (app == null) throw new NullPointerException("AppAccess instance cannot be null."); |
||||||
|
|
||||||
|
this.app = app; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final SoundSystem getSoundSystem() |
||||||
|
{ |
||||||
|
return app.getSoundSystem(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final InputSystem getInput() |
||||||
|
{ |
||||||
|
return app.getInput(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final DisplaySystem getDisplay() |
||||||
|
{ |
||||||
|
return app.getDisplay(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final EventBus getEventBus() |
||||||
|
{ |
||||||
|
return app.getEventBus(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final void shutdown() |
||||||
|
{ |
||||||
|
app.shutdown(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
package mightypork.gamecore.core.modules; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.eventbus.clients.RootBusNode; |
||||||
|
import mightypork.gamecore.input.InputSystem; |
||||||
|
import mightypork.gamecore.render.DisplaySystem; |
||||||
|
import mightypork.gamecore.resources.audio.SoundSystem; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* App event bus client, to be used for subsystems, screens and anything that |
||||||
|
* needs access to the eventbus and other systems; Attached directly to bus. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public abstract class AppModule extends RootBusNode implements AppAccess { |
||||||
|
|
||||||
|
private final AppAccess app; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Create a module |
||||||
|
* |
||||||
|
* @param app access to app systems |
||||||
|
*/ |
||||||
|
public AppModule(AppAccess app) |
||||||
|
{ |
||||||
|
super(app); |
||||||
|
|
||||||
|
this.app = app; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final SoundSystem getSoundSystem() |
||||||
|
{ |
||||||
|
return app.getSoundSystem(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final InputSystem getInput() |
||||||
|
{ |
||||||
|
return app.getInput(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final DisplaySystem getDisplay() |
||||||
|
{ |
||||||
|
return app.getDisplay(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final void shutdown() |
||||||
|
{ |
||||||
|
app.shutdown(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,63 @@ |
|||||||
|
package mightypork.gamecore.core.modules; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.eventbus.clients.BusNode; |
||||||
|
import mightypork.gamecore.eventbus.clients.DelegatingClient; |
||||||
|
import mightypork.gamecore.eventbus.clients.RootBusNode; |
||||||
|
import mightypork.gamecore.input.InputSystem; |
||||||
|
import mightypork.gamecore.render.DisplaySystem; |
||||||
|
import mightypork.gamecore.resources.audio.SoundSystem; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Delegating bus client, to be attached to any {@link DelegatingClient}, such |
||||||
|
* as a {@link RootBusNode}. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public class AppSubModule extends BusNode implements AppAccess { |
||||||
|
|
||||||
|
private final AppAccess app; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Create submodule |
||||||
|
* |
||||||
|
* @param app access to app systems |
||||||
|
*/ |
||||||
|
public AppSubModule(AppAccess app) |
||||||
|
{ |
||||||
|
super(app); |
||||||
|
|
||||||
|
this.app = app; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final SoundSystem getSoundSystem() |
||||||
|
{ |
||||||
|
return app.getSoundSystem(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final InputSystem getInput() |
||||||
|
{ |
||||||
|
return app.getInput(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final DisplaySystem getDisplay() |
||||||
|
{ |
||||||
|
return app.getDisplay(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final void shutdown() |
||||||
|
{ |
||||||
|
app.shutdown(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,525 @@ |
|||||||
|
package mightypork.gamecore.core.modules; |
||||||
|
|
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.io.IOException; |
||||||
|
import java.lang.Thread.UncaughtExceptionHandler; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.logging.Level; |
||||||
|
|
||||||
|
import javax.swing.JOptionPane; |
||||||
|
|
||||||
|
import mightypork.gamecore.core.Config; |
||||||
|
import mightypork.gamecore.core.WorkDir; |
||||||
|
import mightypork.gamecore.core.Config.ConfigSetup; |
||||||
|
import mightypork.gamecore.core.Config.KeySetup; |
||||||
|
import mightypork.gamecore.core.WorkDir.RouteSetup; |
||||||
|
import mightypork.gamecore.eventbus.EventBus; |
||||||
|
import mightypork.gamecore.eventbus.events.DestroyEvent; |
||||||
|
import mightypork.gamecore.gui.screens.ScreenRegistry; |
||||||
|
import mightypork.gamecore.gui.screens.impl.CrossfadeOverlay; |
||||||
|
import mightypork.gamecore.input.InputSystem; |
||||||
|
import mightypork.gamecore.logging.Log; |
||||||
|
import mightypork.gamecore.logging.SlickLogRedirector; |
||||||
|
import mightypork.gamecore.logging.writers.LogWriter; |
||||||
|
import mightypork.gamecore.render.DisplaySystem; |
||||||
|
import mightypork.gamecore.resources.AsyncResourceLoader; |
||||||
|
import mightypork.gamecore.resources.Res; |
||||||
|
import mightypork.gamecore.resources.ResourceLoader; |
||||||
|
import mightypork.gamecore.resources.ResourceSetup; |
||||||
|
import mightypork.gamecore.resources.audio.SoundSystem; |
||||||
|
import mightypork.gamecore.util.annot.DefaultImpl; |
||||||
|
import mightypork.gamecore.util.files.InstanceLock; |
||||||
|
import mightypork.gamecore.util.ion.Ion; |
||||||
|
import mightypork.gamecore.util.math.algo.Coord; |
||||||
|
import mightypork.gamecore.util.math.algo.Move; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Basic screen-based game with subsystems.<br> |
||||||
|
* This class takes care of the initialization sequence. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public abstract class BaseApp implements AppAccess, UncaughtExceptionHandler { |
||||||
|
|
||||||
|
/** |
||||||
|
* Init options holder class
|
||||||
|
*/ |
||||||
|
public class AppInitOptions { |
||||||
|
|
||||||
|
private String logDir = "log"; |
||||||
|
private String logFilePrefix = "runtime"; |
||||||
|
|
||||||
|
private String screenshotDir = "screenshots"; |
||||||
|
|
||||||
|
private int logArchiveCount = 0; |
||||||
|
private boolean busLogging = false; |
||||||
|
|
||||||
|
private String configFile = "settings.cfg"; |
||||||
|
private String configComment = "Main config file"; |
||||||
|
|
||||||
|
public String lockFile = ".lock"; |
||||||
|
|
||||||
|
private final List<ResourceSetup> resourceLists = new ArrayList<>(); |
||||||
|
private final List<Config.KeySetup> keyLists = new ArrayList<>(); |
||||||
|
private final List<ConfigSetup> configLists = new ArrayList<>(); |
||||||
|
private final List<RouteSetup> routeLists = new ArrayList<>(); |
||||||
|
|
||||||
|
private ResourceLoader resourceLoader = new AsyncResourceLoader(); |
||||||
|
private Level logLevel = Level.ALL; |
||||||
|
public boolean sigleInstance; |
||||||
|
private Level logSoutLevel; |
||||||
|
|
||||||
|
|
||||||
|
public void setConfigFile(String filename, String comment) |
||||||
|
{ |
||||||
|
configFile = filename; |
||||||
|
configComment = comment; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public void addConfig(ConfigSetup cfg) |
||||||
|
{ |
||||||
|
configLists.add(cfg); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public void addKeys(Config.KeySetup keys) |
||||||
|
{ |
||||||
|
keyLists.add(keys); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public void addRoutes(RouteSetup keys) |
||||||
|
{ |
||||||
|
routeLists.add(keys); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public void addResources(ResourceSetup res) |
||||||
|
{ |
||||||
|
resourceLists.add(res); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// modules
|
||||||
|
private InputSystem inputSystem; |
||||||
|
private DisplaySystem displaySystem; |
||||||
|
private SoundSystem soundSystem; |
||||||
|
private EventBus eventBus; |
||||||
|
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 opt() |
||||||
|
{ |
||||||
|
if (started) { |
||||||
|
throw new IllegalStateException("Cannot alter init options after starting the App."); |
||||||
|
} |
||||||
|
|
||||||
|
return opt; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public BaseApp(File workdir, boolean singleInstance) |
||||||
|
{ |
||||||
|
WorkDir.init(workdir); |
||||||
|
|
||||||
|
opt.sigleInstance = singleInstance; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Start the application |
||||||
|
*/ |
||||||
|
public final void start() |
||||||
|
{ |
||||||
|
Thread.setDefaultUncaughtExceptionHandler(this); |
||||||
|
|
||||||
|
initialize(); |
||||||
|
|
||||||
|
Log.i("Starting main loop..."); |
||||||
|
|
||||||
|
// open first screen
|
||||||
|
started = true; |
||||||
|
gameLoop.start(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Init the app |
||||||
|
*/ |
||||||
|
protected void initialize() |
||||||
|
{ |
||||||
|
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(); |
||||||
|
|
||||||
|
/* |
||||||
|
* Setup logging |
||||||
|
*/ |
||||||
|
final LogWriter log = Log.create(opt.logFilePrefix, new File(WorkDir.getDir(opt.logDir), opt.logFilePrefix + ".log"), opt.logArchiveCount); |
||||||
|
Log.setMainLogger(log); |
||||||
|
Log.setLevel(opt.logLevel); |
||||||
|
Log.setSysoutLevel(opt.logSoutLevel); |
||||||
|
|
||||||
|
// connect slickutil to the logger
|
||||||
|
org.newdawn.slick.util.Log.setLogSystem(new SlickLogRedirector(log)); |
||||||
|
writeLogHeader(); |
||||||
|
|
||||||
|
|
||||||
|
Log.i("=== Starting initialization sequence ==="); |
||||||
|
|
||||||
|
// pre-init hook
|
||||||
|
Log.f2("Calling pre-init hook..."); |
||||||
|
preInit(); |
||||||
|
|
||||||
|
/* |
||||||
|
* Event bus |
||||||
|
*/ |
||||||
|
Log.f2("Starting Event Bus..."); |
||||||
|
eventBus = new EventBus(); |
||||||
|
eventBus.subscribe(this); |
||||||
|
eventBus.detailedLogging = opt.busLogging; |
||||||
|
|
||||||
|
/* |
||||||
|
* Ionizables |
||||||
|
*/ |
||||||
|
Log.f3("Initializing ION save system..."); |
||||||
|
registerIonizables(); |
||||||
|
|
||||||
|
/* |
||||||
|
* Display |
||||||
|
*/ |
||||||
|
Log.f2("Initializing Display System..."); |
||||||
|
displaySystem = new DisplaySystem(this); |
||||||
|
initDisplay(displaySystem); |
||||||
|
|
||||||
|
/* |
||||||
|
* Audio |
||||||
|
*/ |
||||||
|
Log.f2("Initializing Sound System..."); |
||||||
|
soundSystem = new SoundSystem(this); |
||||||
|
initSoundSystem(soundSystem); |
||||||
|
|
||||||
|
/* |
||||||
|
* Input |
||||||
|
*/ |
||||||
|
Log.f2("Initializing Input System..."); |
||||||
|
inputSystem = new InputSystem(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); |
||||||
|
|
||||||
|
postInit(); |
||||||
|
Log.i("=== Initialization sequence completed ==="); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
protected void writeLogHeader() |
||||||
|
{ |
||||||
|
logSystemInfo(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
protected void logSystemInfo() |
||||||
|
{ |
||||||
|
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); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
protected void registerIonizables() |
||||||
|
{ |
||||||
|
Ion.registerType(Coord.ION_MARK, Coord.class); |
||||||
|
Ion.registerType(Move.ION_MARK, Move.class); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Called at the beginning of the initialization sequence, right after lock |
||||||
|
* was obtained. |
||||||
|
*/ |
||||||
|
@DefaultImpl |
||||||
|
protected void preInit() |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Called at the end of init sequence, before main loop starts. |
||||||
|
*/ |
||||||
|
@DefaultImpl |
||||||
|
protected void postInit() |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Create window and configure display system |
||||||
|
* |
||||||
|
* @param display |
||||||
|
*/ |
||||||
|
@DefaultImpl |
||||||
|
protected void initDisplay(DisplaySystem display) |
||||||
|
{ |
||||||
|
display.createMainWindow(800, 600, true, false, "LWJGL game"); |
||||||
|
display.setTargetFps(60); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Configure sound system (ie. adjust volume) |
||||||
|
* |
||||||
|
* @param audio |
||||||
|
*/ |
||||||
|
@DefaultImpl |
||||||
|
protected void initSoundSystem(SoundSystem audio) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* 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); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/* |
||||||
|
* Try to obtain lock. |
||||||
|
*/ |
||||||
|
private void initLock() |
||||||
|
{ |
||||||
|
final File lock = WorkDir.getFile(opt.lockFile); |
||||||
|
if (!InstanceLock.onFile(lock)) { |
||||||
|
onLockError(); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@DefaultImpl |
||||||
|
protected void initInputSystem(InputSystem input) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Triggered when lock cannot be obtained.<br> |
||||||
|
* App should terminate gracefully. |
||||||
|
*/ |
||||||
|
|
||||||
|
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 "+opt.lockFile +" file in the working directory to override)", |
||||||
|
"Lock Error", |
||||||
|
JOptionPane.ERROR_MESSAGE |
||||||
|
); |
||||||
|
//@formatter:on
|
||||||
|
|
||||||
|
shutdown(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final SoundSystem getSoundSystem() |
||||||
|
{ |
||||||
|
return soundSystem; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final InputSystem getInput() |
||||||
|
{ |
||||||
|
return inputSystem; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final DisplaySystem getDisplay() |
||||||
|
{ |
||||||
|
return displaySystem; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final EventBus getEventBus() |
||||||
|
{ |
||||||
|
return eventBus; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
protected void beforeShutdown() |
||||||
|
{ |
||||||
|
if (lockObtained) Config.save(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final void uncaughtException(Thread t, Throwable e) |
||||||
|
{ |
||||||
|
onCrash(e); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
protected void onCrash(Throwable e) |
||||||
|
{ |
||||||
|
Log.e("The game has crashed.", e); |
||||||
|
shutdown(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final void shutdown() |
||||||
|
{ |
||||||
|
beforeShutdown(); |
||||||
|
|
||||||
|
Log.i("Shutting down subsystems..."); |
||||||
|
|
||||||
|
try { |
||||||
|
if (getEventBus() != null) { |
||||||
|
getEventBus().send(new DestroyEvent()); |
||||||
|
getEventBus().destroy(); |
||||||
|
} |
||||||
|
} catch (final Throwable e) { |
||||||
|
Log.e(e); |
||||||
|
} |
||||||
|
|
||||||
|
Log.i("Terminating..."); |
||||||
|
System.exit(0); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,157 @@ |
|||||||
|
package mightypork.gamecore.core.modules; |
||||||
|
|
||||||
|
|
||||||
|
import java.util.Deque; |
||||||
|
import java.util.Queue; |
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque; |
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue; |
||||||
|
|
||||||
|
import mightypork.gamecore.eventbus.events.UpdateEvent; |
||||||
|
import mightypork.gamecore.gui.screens.ScreenRegistry; |
||||||
|
import mightypork.gamecore.logging.Log; |
||||||
|
import mightypork.gamecore.render.Renderable; |
||||||
|
import mightypork.gamecore.render.TaskTakeScreenshot; |
||||||
|
import mightypork.gamecore.render.events.ScreenshotRequestListener; |
||||||
|
import mightypork.gamecore.resources.Profiler; |
||||||
|
import mightypork.gamecore.util.Utils; |
||||||
|
import mightypork.gamecore.util.annot.DefaultImpl; |
||||||
|
import mightypork.gamecore.util.math.timing.TimerDelta; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Delta-timed game loop with task queue etc. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public class MainLoop extends AppModule implements ScreenshotRequestListener { |
||||||
|
|
||||||
|
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<Runnable> tasks = new ConcurrentLinkedDeque<>(); |
||||||
|
private TimerDelta timer; |
||||||
|
private Renderable rootRenderable; |
||||||
|
private volatile boolean running = true; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @param app {@link AppAccess} instance |
||||||
|
*/ |
||||||
|
public MainLoop(AppAccess app) |
||||||
|
{ |
||||||
|
super(app); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* 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) { |
||||||
|
getDisplay().beginFrame(); |
||||||
|
|
||||||
|
double delta = timer.getDelta(); |
||||||
|
if (delta > MAX_DELTA) { |
||||||
|
Log.f3("(timing) Cropping delta: was " + delta + " , limit " + MAX_DELTA); |
||||||
|
delta = MAX_DELTA; |
||||||
|
} |
||||||
|
|
||||||
|
getEventBus().sendDirect(new UpdateEvent(delta)); |
||||||
|
|
||||||
|
Runnable r; |
||||||
|
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(); |
||||||
|
|
||||||
|
getDisplay().endFrame(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Called before render |
||||||
|
*/ |
||||||
|
@DefaultImpl |
||||||
|
protected void beforeRender() |
||||||
|
{ |
||||||
|
//
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Called after render |
||||||
|
*/ |
||||||
|
@DefaultImpl |
||||||
|
protected void afterRender() |
||||||
|
{ |
||||||
|
//
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
protected void deinit() |
||||||
|
{ |
||||||
|
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); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public void onScreenshotRequest() |
||||||
|
{ |
||||||
|
// ensure it's started in main thread
|
||||||
|
queueTask(new Runnable() { |
||||||
|
|
||||||
|
@Override |
||||||
|
public void run() |
||||||
|
{ |
||||||
|
Utils.runAsThread(new TaskTakeScreenshot()); |
||||||
|
} |
||||||
|
}, false); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package mightypork.gamecore.eventbus; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Access to an {@link EventBus} instance |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public interface BusAccess { |
||||||
|
|
||||||
|
/** |
||||||
|
* @return event bus |
||||||
|
*/ |
||||||
|
EventBus getEventBus(); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,121 @@ |
|||||||
|
package mightypork.gamecore.eventbus; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.eventbus.event_flags.DelayedEvent; |
||||||
|
import mightypork.gamecore.eventbus.event_flags.DirectEvent; |
||||||
|
import mightypork.gamecore.eventbus.event_flags.NonConsumableEvent; |
||||||
|
import mightypork.gamecore.eventbus.event_flags.NotLoggedEvent; |
||||||
|
import mightypork.gamecore.eventbus.event_flags.SingleReceiverEvent; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* <p> |
||||||
|
* Event that can be handled by HANDLER, subscribing to the event bus. |
||||||
|
* </p> |
||||||
|
* <p> |
||||||
|
* Can be annotated as {@link SingleReceiverEvent} to be delivered once only, |
||||||
|
* and {@link DelayedEvent} or {@link DirectEvent} to specify default sending |
||||||
|
* mode. When marked as {@link NotLoggedEvent}, it will not appear in detailed |
||||||
|
* bus logging (useful for very frequent events, such as UpdateEvent). |
||||||
|
* </p> |
||||||
|
* <p> |
||||||
|
* Events annotated as {@link NonConsumableEvent} will throw an exception upon |
||||||
|
* an attempt to consume them. |
||||||
|
* </p> |
||||||
|
* <p> |
||||||
|
* Default sending mode (if not changed by annotations) is <i>queued</i> with |
||||||
|
* zero delay. |
||||||
|
* </p> |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
* @param <HANDLER> handler type |
||||||
|
*/ |
||||||
|
public abstract class BusEvent<HANDLER> { |
||||||
|
|
||||||
|
private boolean consumed; |
||||||
|
private boolean served; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Ask handler to handle this message. |
||||||
|
* |
||||||
|
* @param handler handler instance |
||||||
|
*/ |
||||||
|
protected abstract void handleBy(HANDLER handler); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Consume the event, so no other clients will receive it. |
||||||
|
* |
||||||
|
* @throws UnsupportedOperationException if the {@link NonConsumableEvent} |
||||||
|
* annotation is present. |
||||||
|
*/ |
||||||
|
public final void consume() |
||||||
|
{ |
||||||
|
if (consumed) throw new IllegalStateException("Already consumed."); |
||||||
|
|
||||||
|
if (getClass().isAnnotationPresent(NonConsumableEvent.class)) { |
||||||
|
throw new UnsupportedOperationException("Not consumable."); |
||||||
|
} |
||||||
|
|
||||||
|
consumed = true; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Deliver to a handler using the handleBy method. |
||||||
|
* |
||||||
|
* @param handler handler instance |
||||||
|
*/ |
||||||
|
final void deliverTo(HANDLER handler) |
||||||
|
{ |
||||||
|
handleBy(handler); |
||||||
|
|
||||||
|
if (!served) { |
||||||
|
if (getClass().isAnnotationPresent(SingleReceiverEvent.class)) { |
||||||
|
consumed = true; |
||||||
|
} |
||||||
|
|
||||||
|
served = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Check if the event is consumed. Consumed event is not served to other |
||||||
|
* clients. |
||||||
|
* |
||||||
|
* @return true if consumed |
||||||
|
*/ |
||||||
|
public final boolean isConsumed() |
||||||
|
{ |
||||||
|
return consumed; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @return true if the event was served to at least 1 client |
||||||
|
*/ |
||||||
|
final boolean wasServed() |
||||||
|
{ |
||||||
|
return served; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Clear "served" and "consumed" flags before dispatching. |
||||||
|
*/ |
||||||
|
final void clearFlags() |
||||||
|
{ |
||||||
|
served = false; |
||||||
|
consumed = false; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Called after all clients have received the event. |
||||||
|
*/ |
||||||
|
public void onDispatchComplete(EventBus bus) |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,400 @@ |
|||||||
|
package mightypork.gamecore.eventbus; |
||||||
|
|
||||||
|
|
||||||
|
import java.lang.reflect.ParameterizedType; |
||||||
|
import java.lang.reflect.Type; |
||||||
|
import java.util.Collection; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque; |
||||||
|
import java.util.concurrent.DelayQueue; |
||||||
|
import java.util.concurrent.Delayed; |
||||||
|
import java.util.concurrent.TimeUnit; |
||||||
|
|
||||||
|
import mightypork.gamecore.eventbus.clients.DelegatingClient; |
||||||
|
import mightypork.gamecore.eventbus.event_flags.DelayedEvent; |
||||||
|
import mightypork.gamecore.eventbus.event_flags.DirectEvent; |
||||||
|
import mightypork.gamecore.eventbus.event_flags.NotLoggedEvent; |
||||||
|
import mightypork.gamecore.eventbus.events.Destroyable; |
||||||
|
import mightypork.gamecore.logging.Log; |
||||||
|
import mightypork.gamecore.util.Utils; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* An event bus, accommodating multiple EventChannels.<br> |
||||||
|
* Channel will be created when an event of type is first encountered. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
final public class EventBus implements Destroyable, BusAccess { |
||||||
|
|
||||||
|
/** |
||||||
|
* Queued event holder |
||||||
|
*/ |
||||||
|
private class DelayQueueEntry implements Delayed { |
||||||
|
|
||||||
|
private final long due; |
||||||
|
private final BusEvent<?> evt; |
||||||
|
|
||||||
|
|
||||||
|
public DelayQueueEntry(double seconds, BusEvent<?> event) |
||||||
|
{ |
||||||
|
super(); |
||||||
|
this.due = System.currentTimeMillis() + (long) (seconds * 1000); |
||||||
|
this.evt = event; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public int compareTo(Delayed o) |
||||||
|
{ |
||||||
|
return Long.valueOf(getDelay(TimeUnit.MILLISECONDS)).compareTo(o.getDelay(TimeUnit.MILLISECONDS)); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public long getDelay(TimeUnit unit) |
||||||
|
{ |
||||||
|
return unit.convert(due - System.currentTimeMillis(), TimeUnit.MILLISECONDS); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public BusEvent<?> getEvent() |
||||||
|
{ |
||||||
|
return evt; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Thread handling queued events |
||||||
|
*/ |
||||||
|
private class QueuePollingThread extends Thread { |
||||||
|
|
||||||
|
public volatile boolean stopped = false; |
||||||
|
|
||||||
|
|
||||||
|
public QueuePollingThread() |
||||||
|
{ |
||||||
|
super("Queue Polling Thread"); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public void run() |
||||||
|
{ |
||||||
|
DelayQueueEntry evt; |
||||||
|
|
||||||
|
while (!stopped) { |
||||||
|
evt = null; |
||||||
|
|
||||||
|
try { |
||||||
|
evt = sendQueue.take(); |
||||||
|
} catch (final InterruptedException ignored) { |
||||||
|
//
|
||||||
|
} |
||||||
|
|
||||||
|
if (evt != null) { |
||||||
|
try { |
||||||
|
dispatch(evt.getEvent()); |
||||||
|
} catch (final Throwable t) { |
||||||
|
Log.w(logMark + "Error while dispatching event: ", t); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static final String logMark = "(bus) "; |
||||||
|
|
||||||
|
|
||||||
|
private static Class<?> getEventListenerClass(BusEvent<?> event) |
||||||
|
{ |
||||||
|
// BEHOLD, MAGIC!
|
||||||
|
|
||||||
|
final Type evtc = event.getClass().getGenericSuperclass(); |
||||||
|
|
||||||
|
if (evtc instanceof ParameterizedType) { |
||||||
|
if (((ParameterizedType) evtc).getRawType() == BusEvent.class) { |
||||||
|
final Type[] types = ((ParameterizedType) evtc).getActualTypeArguments(); |
||||||
|
for (final Type genericType : types) { |
||||||
|
return (Class<?>) genericType; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
throw new RuntimeException("Could not detect event listener type."); |
||||||
|
} |
||||||
|
|
||||||
|
/** Log detailed messages (debug) */ |
||||||
|
public boolean detailedLogging = false; |
||||||
|
|
||||||
|
/** Queue polling thread */ |
||||||
|
private final QueuePollingThread busThread; |
||||||
|
|
||||||
|
/** Registered clients */ |
||||||
|
private final Set<Object> clients = Collections.newSetFromMap(new ConcurrentHashMap<Object,Boolean>()); |
||||||
|
|
||||||
|
/** Whether the bus was destroyed */ |
||||||
|
private boolean dead = false; |
||||||
|
|
||||||
|
/** Message channels */ |
||||||
|
private final Set<EventChannel<?, ?>> channels = Collections.newSetFromMap(new ConcurrentHashMap<EventChannel<?, ?>,Boolean>()); |
||||||
|
|
||||||
|
/** Messages queued for delivery */ |
||||||
|
private final DelayQueue<DelayQueueEntry> sendQueue = new DelayQueue<>(); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Make a new bus and start it's queue thread. |
||||||
|
*/ |
||||||
|
public EventBus() |
||||||
|
{ |
||||||
|
busThread = new QueuePollingThread(); |
||||||
|
busThread.setDaemon(true); |
||||||
|
busThread.start(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Halt bus thread and reject any future events. |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public void destroy() |
||||||
|
{ |
||||||
|
assertLive(); |
||||||
|
|
||||||
|
busThread.stopped = true; |
||||||
|
dead = true; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Send based on annotation |
||||||
|
* |
||||||
|
* @param event event |
||||||
|
*/ |
||||||
|
public void send(BusEvent<?> event) |
||||||
|
{ |
||||||
|
assertLive(); |
||||||
|
|
||||||
|
final DelayedEvent adelay = Utils.getAnnotation(event, DelayedEvent.class); |
||||||
|
if (adelay != null) { |
||||||
|
sendDelayed(event, adelay.delay()); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (Utils.hasAnnotation(event, DirectEvent.class)) { |
||||||
|
sendDirect(event); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
sendQueued(event); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Add event to a queue |
||||||
|
* |
||||||
|
* @param event event |
||||||
|
*/ |
||||||
|
public void sendQueued(BusEvent<?> event) |
||||||
|
{ |
||||||
|
assertLive(); |
||||||
|
|
||||||
|
sendDelayed(event, 0); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Add event to a queue, scheduled for given time. |
||||||
|
* |
||||||
|
* @param event event |
||||||
|
* @param delay delay before event is dispatched |
||||||
|
*/ |
||||||
|
public void sendDelayed(BusEvent<?> event, double delay) |
||||||
|
{ |
||||||
|
assertLive(); |
||||||
|
|
||||||
|
final DelayQueueEntry dm = new DelayQueueEntry(delay, event); |
||||||
|
|
||||||
|
if (shallLog(event)) { |
||||||
|
Log.f3(logMark + "Qu [" + Log.str(event) + "]" + (delay == 0 ? "" : (", delay: " + delay + "s"))); |
||||||
|
} |
||||||
|
|
||||||
|
sendQueue.add(dm); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Send immediately.<br> |
||||||
|
* Should be used for real-time events that require immediate response, such |
||||||
|
* as timing events. |
||||||
|
* |
||||||
|
* @param event event |
||||||
|
*/ |
||||||
|
public void sendDirect(BusEvent<?> event) |
||||||
|
{ |
||||||
|
assertLive(); |
||||||
|
|
||||||
|
if (shallLog(event)) Log.f3(logMark + "Di [" + Log.str(event) + "]"); |
||||||
|
|
||||||
|
dispatch(event); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public void sendDirectToChildren(DelegatingClient delegatingClient, BusEvent<?> event) |
||||||
|
{ |
||||||
|
assertLive(); |
||||||
|
|
||||||
|
if (shallLog(event)) Log.f3(logMark + "Di->sub [" + Log.str(event) + "]"); |
||||||
|
|
||||||
|
doDispatch(delegatingClient.getChildClients(), event); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Connect a client to the bus. The client will be connected to all current |
||||||
|
* and future channels, until removed from the bus. |
||||||
|
* |
||||||
|
* @param client the client |
||||||
|
*/ |
||||||
|
public void subscribe(Object client) |
||||||
|
{ |
||||||
|
assertLive(); |
||||||
|
|
||||||
|
if (client == null) return; |
||||||
|
|
||||||
|
clients.add(client); |
||||||
|
|
||||||
|
if (detailedLogging) Log.f3(logMark + "Client joined: " + Log.str(client)); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Disconnect a client from the bus. |
||||||
|
* |
||||||
|
* @param client the client |
||||||
|
*/ |
||||||
|
public void unsubscribe(Object client) |
||||||
|
{ |
||||||
|
assertLive(); |
||||||
|
|
||||||
|
clients.remove(client); |
||||||
|
|
||||||
|
if (detailedLogging) Log.f3(logMark + "Client left: " + Log.str(client)); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
private boolean addChannelForEvent(BusEvent<?> event) |
||||||
|
{ |
||||||
|
try { |
||||||
|
if (detailedLogging) { |
||||||
|
Log.f3(logMark + "Setting up channel for new event type: " + Log.str(event.getClass())); |
||||||
|
} |
||||||
|
|
||||||
|
final Class<?> listener = getEventListenerClass(event); |
||||||
|
final EventChannel<?, ?> ch = EventChannel.create(event.getClass(), listener); |
||||||
|
|
||||||
|
if (ch.canBroadcast(event)) { |
||||||
|
|
||||||
|
channels.add(ch); |
||||||
|
//channels.flush();
|
||||||
|
|
||||||
|
if (detailedLogging) { |
||||||
|
Log.f3(logMark + "Created new channel: " + Log.str(event.getClass()) + " -> " + Log.str(listener)); |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
|
||||||
|
} else { |
||||||
|
Log.w(logMark + "Could not create channel for event " + Log.str(event.getClass())); |
||||||
|
} |
||||||
|
|
||||||
|
} catch (final Throwable t) { |
||||||
|
Log.w(logMark + "Error while trying to add channel for event.", t); |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Make sure the bus is not destroyed. |
||||||
|
* |
||||||
|
* @throws IllegalStateException if the bus is dead. |
||||||
|
*/ |
||||||
|
private void assertLive() throws IllegalStateException |
||||||
|
{ |
||||||
|
if (dead) throw new IllegalStateException("EventBus is dead."); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Send immediately.<br> |
||||||
|
* Should be used for real-time events that require immediate response, such |
||||||
|
* as timing events. |
||||||
|
* |
||||||
|
* @param event event |
||||||
|
*/ |
||||||
|
private synchronized void dispatch(BusEvent<?> event) |
||||||
|
{ |
||||||
|
assertLive(); |
||||||
|
|
||||||
|
doDispatch(clients, event); |
||||||
|
event.onDispatchComplete(this); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Send to a set of clients |
||||||
|
* |
||||||
|
* @param clients clients |
||||||
|
* @param event event |
||||||
|
*/ |
||||||
|
private synchronized void doDispatch(Collection<?> clients, BusEvent<?> event) |
||||||
|
{ |
||||||
|
boolean accepted = false; |
||||||
|
|
||||||
|
event.clearFlags(); |
||||||
|
|
||||||
|
for (int i = 0; i < 2; i++) { // two tries.
|
||||||
|
|
||||||
|
for (final EventChannel<?, ?> b : channels) { |
||||||
|
if (b.canBroadcast(event)) { |
||||||
|
accepted = true; |
||||||
|
b.broadcast(event, clients); |
||||||
|
} |
||||||
|
|
||||||
|
if (event.isConsumed()) break; |
||||||
|
} |
||||||
|
|
||||||
|
if (!accepted) if (addChannelForEvent(event)) continue; |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
if (!accepted) Log.e(logMark + "Not accepted by any channel: " + Log.str(event)); |
||||||
|
if (!event.wasServed() && shallLog(event)) Log.w(logMark + "Not delivered: " + Log.str(event)); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private boolean shallLog(BusEvent<?> event) |
||||||
|
{ |
||||||
|
if (!detailedLogging) return false; |
||||||
|
if (Utils.hasAnnotation(event, NotLoggedEvent.class)) return false; |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public EventBus getEventBus() |
||||||
|
{ |
||||||
|
return this; // just for compatibility use-case
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,207 @@ |
|||||||
|
package mightypork.gamecore.eventbus; |
||||||
|
|
||||||
|
|
||||||
|
import java.util.Collection; |
||||||
|
import java.util.HashSet; |
||||||
|
|
||||||
|
import mightypork.gamecore.eventbus.clients.DelegatingClient; |
||||||
|
import mightypork.gamecore.eventbus.clients.ToggleableClient; |
||||||
|
import mightypork.gamecore.eventbus.event_flags.NonRejectableEvent; |
||||||
|
import mightypork.gamecore.logging.Log; |
||||||
|
import mightypork.gamecore.util.Utils; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Event delivery channel, module of {@link EventBus} |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
* @param <EVENT> event type |
||||||
|
* @param <CLIENT> client (subscriber) type |
||||||
|
*/ |
||||||
|
class EventChannel<EVENT extends BusEvent<CLIENT>, CLIENT> { |
||||||
|
|
||||||
|
private final Class<CLIENT> clientClass; |
||||||
|
private final Class<EVENT> eventClass; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Create a channel |
||||||
|
* |
||||||
|
* @param eventClass event class
|
||||||
|
* @param clientClass client class
|
||||||
|
*/ |
||||||
|
public EventChannel(Class<EVENT> eventClass, Class<CLIENT> clientClass) |
||||||
|
{ |
||||||
|
|
||||||
|
if (eventClass == null || clientClass == null) { |
||||||
|
throw new NullPointerException("Null Event or Client class."); |
||||||
|
} |
||||||
|
|
||||||
|
this.clientClass = clientClass; |
||||||
|
this.eventClass = eventClass; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Try to broadcast a event.<br> |
||||||
|
* If event is of wrong type, <code>false</code> is returned. |
||||||
|
* |
||||||
|
* @param event a event to be sent |
||||||
|
* @param clients collection of clients |
||||||
|
*/ |
||||||
|
public void broadcast(BusEvent<?> event, Collection<?> clients) |
||||||
|
{ |
||||||
|
if (!canBroadcast(event)) return; |
||||||
|
|
||||||
|
doBroadcast(eventClass.cast(event), clients, new HashSet<>()); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Send the event |
||||||
|
* |
||||||
|
* @param event sent event |
||||||
|
* @param clients subscribing clients |
||||||
|
* @param processed clients already processed |
||||||
|
*/ |
||||||
|
private void doBroadcast(final EVENT event, final Collection<?> clients, final Collection<Object> processed) |
||||||
|
{ |
||||||
|
for (final Object client : clients) { |
||||||
|
|
||||||
|
// exclude obvious non-clients
|
||||||
|
if (!isClientValid(client)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
// avoid executing more times
|
||||||
|
if (processed.contains(client)) { |
||||||
|
Log.w(EventBus.logMark + "Client already served: " + Log.str(client)); |
||||||
|
continue; |
||||||
|
} |
||||||
|
processed.add(client); |
||||||
|
|
||||||
|
final boolean must_deliver = Utils.hasAnnotation(event, NonRejectableEvent.class); |
||||||
|
|
||||||
|
// opt-out
|
||||||
|
if (client instanceof ToggleableClient) { |
||||||
|
if (!must_deliver && !((ToggleableClient) client).isListening()) continue; |
||||||
|
} |
||||||
|
|
||||||
|
sendTo(client, event); |
||||||
|
|
||||||
|
if (event.isConsumed()) return; |
||||||
|
|
||||||
|
// pass on to delegated clients
|
||||||
|
if (client instanceof DelegatingClient) { |
||||||
|
if (must_deliver || ((DelegatingClient) client).doesDelegate()) { |
||||||
|
|
||||||
|
final Collection<?> children = ((DelegatingClient) client).getChildClients(); |
||||||
|
|
||||||
|
if (children != null && children.size() > 0) { |
||||||
|
doBroadcast(event, children, processed); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Send an event to a client. |
||||||
|
* |
||||||
|
* @param client target client |
||||||
|
* @param event event to send |
||||||
|
*/ |
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
private void sendTo(Object client, EVENT event) |
||||||
|
{ |
||||||
|
if (isClientOfChannelType(client)) { |
||||||
|
((BusEvent<CLIENT>) event).deliverTo((CLIENT) client); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Check if the given event can be broadcasted by this channel |
||||||
|
* |
||||||
|
* @param event event object |
||||||
|
* @return can be broadcasted |
||||||
|
*/ |
||||||
|
public boolean canBroadcast(BusEvent<?> event) |
||||||
|
{ |
||||||
|
return event != null && eventClass.isInstance(event); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Create an instance for given types |
||||||
|
* |
||||||
|
* @param eventClass event class
|
||||||
|
* @param clientClass client class
|
||||||
|
* @return the broadcaster |
||||||
|
*/ |
||||||
|
public static <F_EVENT extends BusEvent<F_CLIENT>, F_CLIENT> EventChannel<F_EVENT, F_CLIENT> create(Class<F_EVENT> eventClass, Class<F_CLIENT> clientClass) |
||||||
|
{ |
||||||
|
return new EventChannel<>(eventClass, clientClass); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Check if client is of channel type |
||||||
|
* |
||||||
|
* @param client client |
||||||
|
* @return is of type |
||||||
|
*/ |
||||||
|
private boolean isClientOfChannelType(Object client) |
||||||
|
{ |
||||||
|
return clientClass.isInstance(client); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Check if the channel is compatible with given |
||||||
|
* |
||||||
|
* @param client client |
||||||
|
* @return is supported |
||||||
|
*/ |
||||||
|
public boolean isClientValid(Object client) |
||||||
|
{ |
||||||
|
return isClientOfChannelType(client) || (client instanceof DelegatingClient); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public int hashCode() |
||||||
|
{ |
||||||
|
final int prime = 13; |
||||||
|
int result = 1; |
||||||
|
result = prime * result + ((clientClass == null) ? 0 : clientClass.hashCode()); |
||||||
|
result = prime * result + ((eventClass == null) ? 0 : eventClass.hashCode()); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean equals(Object obj) |
||||||
|
{ |
||||||
|
if (this == obj) return true; |
||||||
|
if (obj == null) return false; |
||||||
|
if (!(obj instanceof EventChannel)) return false; |
||||||
|
final EventChannel<?, ?> other = (EventChannel<?, ?>) obj; |
||||||
|
if (clientClass == null) { |
||||||
|
if (other.clientClass != null) return false; |
||||||
|
} else if (!clientClass.equals(other.clientClass)) return false; |
||||||
|
if (eventClass == null) { |
||||||
|
if (other.eventClass != null) return false; |
||||||
|
} else if (!eventClass.equals(other.eventClass)) return false; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() |
||||||
|
{ |
||||||
|
return "{ " + Log.str(eventClass) + " => " + Log.str(clientClass) + " }"; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,115 @@ |
|||||||
|
package mightypork.gamecore.eventbus.clients; |
||||||
|
|
||||||
|
|
||||||
|
import java.util.Collection; |
||||||
|
import java.util.LinkedHashSet; |
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
import mightypork.gamecore.eventbus.BusAccess; |
||||||
|
import mightypork.gamecore.eventbus.EventBus; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Client that can be attached to the {@link EventBus}, or added as a child |
||||||
|
* client to another {@link DelegatingClient} |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public abstract class BusNode implements BusAccess, ClientHub { |
||||||
|
|
||||||
|
private final BusAccess busAccess; |
||||||
|
|
||||||
|
private final Set<Object> clients = new LinkedHashSet<>(); |
||||||
|
private boolean listening = true; |
||||||
|
private boolean delegating = true; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @param busAccess access to bus |
||||||
|
*/ |
||||||
|
public BusNode(BusAccess busAccess) |
||||||
|
{ |
||||||
|
this.busAccess = busAccess; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public Collection<Object> getChildClients() |
||||||
|
{ |
||||||
|
return clients; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean doesDelegate() |
||||||
|
{ |
||||||
|
return delegating; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isListening() |
||||||
|
{ |
||||||
|
return listening; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Add a child subscriber to the {@link EventBus}.<br> |
||||||
|
* |
||||||
|
* @param client |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public void addChildClient(Object client) |
||||||
|
{ |
||||||
|
if (client instanceof RootBusNode) { |
||||||
|
throw new IllegalArgumentException("Cannot nest RootBusNode."); |
||||||
|
} |
||||||
|
|
||||||
|
clients.add(client); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Remove a child subscriber |
||||||
|
* |
||||||
|
* @param client subscriber to remove |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public void removeChildClient(Object client) |
||||||
|
{ |
||||||
|
if (client != null) { |
||||||
|
clients.remove(client); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Set whether events should be received. |
||||||
|
* |
||||||
|
* @param listening receive events |
||||||
|
*/ |
||||||
|
public void setListening(boolean listening) |
||||||
|
{ |
||||||
|
this.listening = listening; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Set whether events should be passed on to child nodes |
||||||
|
* |
||||||
|
* @param delegating |
||||||
|
*/ |
||||||
|
public void setDelegating(boolean delegating) |
||||||
|
{ |
||||||
|
this.delegating = delegating; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public EventBus getEventBus() |
||||||
|
{ |
||||||
|
return busAccess.getEventBus(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
package mightypork.gamecore.eventbus.clients; |
||||||
|
|
||||||
|
|
||||||
|
import java.util.Collection; |
||||||
|
|
||||||
|
import mightypork.gamecore.eventbus.EventBus; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Common methods for client hubs (ie delegating vlient implementations) |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public interface ClientHub extends DelegatingClient, ToggleableClient { |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean doesDelegate(); |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public Collection<Object> getChildClients(); |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isListening(); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Add a child subscriber to the {@link EventBus}.<br> |
||||||
|
* |
||||||
|
* @param client |
||||||
|
*/ |
||||||
|
public void addChildClient(Object client); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Remove a child subscriber |
||||||
|
* |
||||||
|
* @param client subscriber to remove |
||||||
|
*/ |
||||||
|
void removeChildClient(Object client); |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
package mightypork.gamecore.eventbus.clients; |
||||||
|
|
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Array-list with varargs constructor |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public class ClientList extends ArrayList<Object> { |
||||||
|
|
||||||
|
public ClientList(Object... clients) |
||||||
|
{ |
||||||
|
for (final Object c : clients) { |
||||||
|
super.add(c); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
package mightypork.gamecore.eventbus.clients; |
||||||
|
|
||||||
|
|
||||||
|
import java.util.Collection; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Client containing child clients. According to the contract, if the collection |
||||||
|
* of clients is ordered, the clients will be served in that order. In any case, |
||||||
|
* the {@link DelegatingClient} itself will be served beforehand. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public interface DelegatingClient { |
||||||
|
|
||||||
|
/** |
||||||
|
* @return collection of child clients. Can not be null. |
||||||
|
*/ |
||||||
|
public Collection<?> getChildClients(); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @return true if delegating is active |
||||||
|
*/ |
||||||
|
public boolean doesDelegate(); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
package mightypork.gamecore.eventbus.clients; |
||||||
|
|
||||||
|
|
||||||
|
import java.util.Collection; |
||||||
|
|
||||||
|
import mightypork.gamecore.gui.Enableable; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Basic delegating client |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public class DelegatingList extends ClientList implements DelegatingClient, Enableable { |
||||||
|
|
||||||
|
private boolean enabled = true; |
||||||
|
|
||||||
|
|
||||||
|
public DelegatingList(Object... clients) |
||||||
|
{ |
||||||
|
super(clients); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public Collection<?> getChildClients() |
||||||
|
{ |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean doesDelegate() |
||||||
|
{ |
||||||
|
return isEnabled(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public void setEnabled(boolean yes) |
||||||
|
{ |
||||||
|
enabled = yes; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isEnabled() |
||||||
|
{ |
||||||
|
return enabled; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,45 @@ |
|||||||
|
package mightypork.gamecore.eventbus.clients; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.eventbus.BusAccess; |
||||||
|
import mightypork.gamecore.eventbus.events.Destroyable; |
||||||
|
import mightypork.gamecore.util.annot.DefaultImpl; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Bus node that should be directly attached to the bus. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public abstract class RootBusNode extends BusNode implements Destroyable { |
||||||
|
|
||||||
|
/** |
||||||
|
* @param busAccess access to bus |
||||||
|
*/ |
||||||
|
public RootBusNode(BusAccess busAccess) |
||||||
|
{ |
||||||
|
super(busAccess); |
||||||
|
|
||||||
|
getEventBus().subscribe(this); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final void destroy() |
||||||
|
{ |
||||||
|
deinit(); |
||||||
|
|
||||||
|
getEventBus().unsubscribe(this); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Deinitialize the subsystem<br> |
||||||
|
* (called during destruction) |
||||||
|
*/ |
||||||
|
@DefaultImpl |
||||||
|
protected void deinit() |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package mightypork.gamecore.eventbus.clients; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Client that can toggle receiving messages. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public interface ToggleableClient { |
||||||
|
|
||||||
|
/** |
||||||
|
* @return true if the client wants to receive messages |
||||||
|
*/ |
||||||
|
public boolean isListening(); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
package mightypork.gamecore.eventbus.event_flags; |
||||||
|
|
||||||
|
|
||||||
|
import java.lang.annotation.*; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Event that should be queued with given delay (default: 0); |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Target(ElementType.TYPE) |
||||||
|
@Inherited |
||||||
|
@Documented |
||||||
|
public @interface DelayedEvent { |
||||||
|
|
||||||
|
/** |
||||||
|
* @return event dispatch delay [seconds] |
||||||
|
*/ |
||||||
|
double delay() default 0; |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package mightypork.gamecore.eventbus.event_flags; |
||||||
|
|
||||||
|
|
||||||
|
import java.lang.annotation.*; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Event that should not be queued. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Target(ElementType.TYPE) |
||||||
|
@Inherited |
||||||
|
@Documented |
||||||
|
public @interface DirectEvent {} |
@ -0,0 +1,21 @@ |
|||||||
|
package mightypork.gamecore.eventbus.event_flags; |
||||||
|
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented; |
||||||
|
import java.lang.annotation.ElementType; |
||||||
|
import java.lang.annotation.Retention; |
||||||
|
import java.lang.annotation.RetentionPolicy; |
||||||
|
import java.lang.annotation.Target; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Event that cannot be consumed |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Documented |
||||||
|
@Target(ElementType.TYPE) |
||||||
|
public @interface NonConsumableEvent { |
||||||
|
|
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
package mightypork.gamecore.eventbus.event_flags; |
||||||
|
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented; |
||||||
|
import java.lang.annotation.ElementType; |
||||||
|
import java.lang.annotation.Retention; |
||||||
|
import java.lang.annotation.RetentionPolicy; |
||||||
|
import java.lang.annotation.Target; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Event that is forcibly delivered to all clients (bypass Toggleable etc) |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Documented |
||||||
|
@Target(ElementType.TYPE) |
||||||
|
public @interface NonRejectableEvent { |
||||||
|
|
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
package mightypork.gamecore.eventbus.event_flags; |
||||||
|
|
||||||
|
|
||||||
|
import java.lang.annotation.*; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Event that's not worth logging, unless there was an error with it.<br> |
||||||
|
* Useful for common events that would otherwise clutter the log. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Target(ElementType.TYPE) |
||||||
|
@Inherited |
||||||
|
@Documented |
||||||
|
public @interface NotLoggedEvent {} |
@ -0,0 +1,16 @@ |
|||||||
|
package mightypork.gamecore.eventbus.event_flags; |
||||||
|
|
||||||
|
|
||||||
|
import java.lang.annotation.*; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Handled only by the first client, then discarded. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Target(ElementType.TYPE) |
||||||
|
@Inherited |
||||||
|
@Documented |
||||||
|
public @interface SingleReceiverEvent {} |
@ -0,0 +1,24 @@ |
|||||||
|
package mightypork.gamecore.eventbus.events; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.eventbus.BusEvent; |
||||||
|
import mightypork.gamecore.eventbus.event_flags.DirectEvent; |
||||||
|
import mightypork.gamecore.eventbus.event_flags.NonConsumableEvent; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Invoke destroy() method of all subscribers. Used to deinit a system. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
@DirectEvent |
||||||
|
@NonConsumableEvent |
||||||
|
public class DestroyEvent extends BusEvent<Destroyable> { |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleBy(Destroyable handler) |
||||||
|
{ |
||||||
|
handler.destroy(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
package mightypork.gamecore.eventbus.events; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Object that can be destroyed (free resources etc) |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public interface Destroyable { |
||||||
|
|
||||||
|
/** |
||||||
|
* Destroy this object |
||||||
|
*/ |
||||||
|
public void destroy(); |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
package mightypork.gamecore.eventbus.events; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.eventbus.BusEvent; |
||||||
|
import mightypork.gamecore.eventbus.event_flags.DirectEvent; |
||||||
|
import mightypork.gamecore.eventbus.event_flags.NonConsumableEvent; |
||||||
|
import mightypork.gamecore.eventbus.event_flags.NotLoggedEvent; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Delta timing update event. Not logged. |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
@NotLoggedEvent |
||||||
|
@DirectEvent |
||||||
|
@NonConsumableEvent |
||||||
|
public class UpdateEvent extends BusEvent<Updateable> { |
||||||
|
|
||||||
|
private final double deltaTime; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @param deltaTime time since last update (sec) |
||||||
|
*/ |
||||||
|
public UpdateEvent(double deltaTime) |
||||||
|
{ |
||||||
|
this.deltaTime = deltaTime; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleBy(Updateable handler) |
||||||
|
{ |
||||||
|
handler.update(deltaTime); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
package mightypork.gamecore.eventbus.events; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Uses delta timing |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public interface Updateable { |
||||||
|
|
||||||
|
/** |
||||||
|
* Update item state based on elapsed time |
||||||
|
* |
||||||
|
* @param delta time elapsed since last update, in seconds |
||||||
|
*/ |
||||||
|
public void update(double delta); |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
package mightypork.gamecore.gui; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Triggered action |
||||||
|
* |
||||||
|
* @author 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(); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
package mightypork.gamecore.gui; |
||||||
|
|
||||||
|
|
||||||
|
import java.util.HashSet; |
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
|
||||||
|
public class ActionGroup implements Enableable { |
||||||
|
|
||||||
|
private boolean enabled = true; |
||||||
|
|
||||||
|
private final Set<Enableable> 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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
package mightypork.gamecore.gui; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Element that can be assigned an action (ie. button); |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public interface ActionTrigger { |
||||||
|
|
||||||
|
/** |
||||||
|
* Assign an action |
||||||
|
* |
||||||
|
* @param action action |
||||||
|
*/ |
||||||
|
void setAction(Action action); |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
package mightypork.gamecore.gui; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Horizontal align sides |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public enum AlignX |
||||||
|
{ |
||||||
|
LEFT, CENTER, RIGHT; |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
package mightypork.gamecore.gui; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Vertical align sides |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public enum AlignY |
||||||
|
{ |
||||||
|
TOP, CENTER, BOTTOM; |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
package mightypork.gamecore.gui; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Can be enabled or disabled.<br> |
||||||
|
* Implementations should take appropriate action (ie. stop listening to events, |
||||||
|
* updating etc.) |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public interface Enableable { |
||||||
|
|
||||||
|
/** |
||||||
|
* Change enabled state |
||||||
|
* |
||||||
|
* @param yes enabled |
||||||
|
*/ |
||||||
|
public void setEnabled(boolean yes); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @return true if enabled |
||||||
|
*/ |
||||||
|
public boolean isEnabled(); |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
package mightypork.gamecore.gui; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Element that can be hidden or visible |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public interface Hideable { |
||||||
|
|
||||||
|
void setVisible(boolean yes); |
||||||
|
|
||||||
|
|
||||||
|
boolean isVisible(); |
||||||
|
} |
@ -0,0 +1,169 @@ |
|||||||
|
package mightypork.gamecore.gui.components; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.gui.Enableable; |
||||||
|
import mightypork.gamecore.gui.events.LayoutChangeEvent; |
||||||
|
import mightypork.gamecore.gui.events.LayoutChangeListener; |
||||||
|
import mightypork.gamecore.input.InputSystem; |
||||||
|
import mightypork.gamecore.logging.Log; |
||||||
|
import mightypork.gamecore.render.Renderable; |
||||||
|
import mightypork.gamecore.util.annot.DefaultImpl; |
||||||
|
import mightypork.gamecore.util.math.color.Color; |
||||||
|
import mightypork.gamecore.util.math.constraints.num.Num; |
||||||
|
import mightypork.gamecore.util.math.constraints.rect.Rect; |
||||||
|
import mightypork.gamecore.util.math.constraints.rect.caching.AbstractRectCache; |
||||||
|
import mightypork.gamecore.util.math.constraints.rect.proxy.RectBound; |
||||||
|
import mightypork.gamecore.util.math.constraints.rect.proxy.RectBoundAdapter; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* {@link Renderable} with pluggable context. When caching is enabled, the |
||||||
|
* layout update can be triggered by firing the {@link LayoutChangeEvent}. |
||||||
|
* |
||||||
|
* @author 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 RectBoundAdapter(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: " + Log.str(getClass())); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final void onConstraintChanged() |
||||||
|
{ |
||||||
|
updateLayout(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public final boolean isMouseOver() |
||||||
|
{ |
||||||
|
return InputSystem.getMousePos().isInside(this); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Draw the component (it's visible) |
||||||
|
*/ |
||||||
|
protected abstract void renderComponent(); |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
@DefaultImpl |
||||||
|
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; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,95 @@ |
|||||||
|
package mightypork.gamecore.gui.components; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.gui.Enableable; |
||||||
|
import mightypork.gamecore.gui.Hideable; |
||||||
|
import mightypork.gamecore.util.math.constraints.num.Num; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Basic UI component interface
|
||||||
|
* |
||||||
|
* @author 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.<br> |
||||||
|
* 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(); |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
package mightypork.gamecore.gui.components; |
||||||
|
|
||||||
|
|
||||||
|
public interface DynamicWidthComponent extends Component { |
||||||
|
|
||||||
|
double computeWidth(double height); |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
package mightypork.gamecore.gui.components; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.eventbus.clients.ToggleableClient; |
||||||
|
import mightypork.gamecore.gui.Enableable; |
||||||
|
|
||||||
|
|
||||||
|
public abstract class InputComponent extends BaseComponent implements Enableable, ToggleableClient { |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isListening() |
||||||
|
{ |
||||||
|
return isEnabled(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,162 @@ |
|||||||
|
package mightypork.gamecore.gui.components; |
||||||
|
|
||||||
|
|
||||||
|
import java.util.Collection; |
||||||
|
import java.util.LinkedList; |
||||||
|
|
||||||
|
import mightypork.gamecore.core.modules.AppAccess; |
||||||
|
import mightypork.gamecore.core.modules.AppSubModule; |
||||||
|
import mightypork.gamecore.eventbus.EventBus; |
||||||
|
import mightypork.gamecore.eventbus.clients.ClientHub; |
||||||
|
import mightypork.gamecore.input.InputSystem; |
||||||
|
import mightypork.gamecore.render.DisplaySystem; |
||||||
|
import mightypork.gamecore.resources.audio.SoundSystem; |
||||||
|
import mightypork.gamecore.util.math.constraints.rect.proxy.RectBound; |
||||||
|
|
||||||
|
|
||||||
|
public abstract class LayoutComponent extends BaseComponent implements ClientHub, AppAccess { |
||||||
|
|
||||||
|
private final AppSubModule subModule; |
||||||
|
final LinkedList<Component> components = new LinkedList<>(); |
||||||
|
|
||||||
|
|
||||||
|
public LayoutComponent(AppAccess app, RectBound context) |
||||||
|
{ |
||||||
|
this.subModule = new AppSubModule(app); |
||||||
|
setRect(context); |
||||||
|
enableCaching(true); // layout is typically updated only when screen resizes.
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public LayoutComponent(AppAccess app) |
||||||
|
{ |
||||||
|
this(app, null); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public EventBus getEventBus() |
||||||
|
{ |
||||||
|
return subModule.getEventBus(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public Collection<Object> getChildClients() |
||||||
|
{ |
||||||
|
return subModule.getChildClients(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean doesDelegate() |
||||||
|
{ |
||||||
|
return subModule.doesDelegate(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isListening() |
||||||
|
{ |
||||||
|
return subModule.isListening(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public SoundSystem getSoundSystem() |
||||||
|
{ |
||||||
|
return subModule.getSoundSystem(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public InputSystem getInput() |
||||||
|
{ |
||||||
|
return subModule.getInput(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public DisplaySystem getDisplay() |
||||||
|
{ |
||||||
|
return subModule.getDisplay(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public void shutdown() |
||||||
|
{ |
||||||
|
subModule.shutdown(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public void addChildClient(Object client) |
||||||
|
{ |
||||||
|
subModule.addChildClient(client); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public void removeChildClient(Object client) |
||||||
|
{ |
||||||
|
subModule.removeChildClient(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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,78 @@ |
|||||||
|
package mightypork.gamecore.gui.components; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.util.math.constraints.num.Num; |
||||||
|
import mightypork.gamecore.util.math.constraints.rect.Rect; |
||||||
|
import mightypork.gamecore.util.math.constraints.rect.proxy.RectBound; |
||||||
|
import mightypork.gamecore.util.math.constraints.vect.Vect; |
||||||
|
import mightypork.gamecore.util.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; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
package mightypork.gamecore.gui.components; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.render.Renderable; |
||||||
|
import mightypork.gamecore.util.math.constraints.rect.Rect; |
||||||
|
import mightypork.gamecore.util.math.constraints.rect.proxy.PluggableRectBound; |
||||||
|
import mightypork.gamecore.util.math.constraints.rect.proxy.RectBound; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Renderable that can be assigned different context |
||||||
|
* |
||||||
|
* @author MightyPork |
||||||
|
*/ |
||||||
|
public interface PluggableRenderable extends Renderable, PluggableRectBound { |
||||||
|
|
||||||
|
@Override |
||||||
|
void render(); |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
Rect getRect(); |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
void setRect(RectBound rect); |
||||||
|
|
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,64 @@ |
|||||||
|
package mightypork.gamecore.gui.components.input; |
||||||
|
|
||||||
|
|
||||||
|
import java.util.Collection; |
||||||
|
|
||||||
|
import mightypork.gamecore.eventbus.clients.ClientList; |
||||||
|
import mightypork.gamecore.eventbus.clients.DelegatingClient; |
||||||
|
import mightypork.gamecore.gui.components.Component; |
||||||
|
|
||||||
|
|
||||||
|
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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,78 @@ |
|||||||
|
package mightypork.gamecore.gui.components.input; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.gui.AlignX; |
||||||
|
import mightypork.gamecore.gui.components.DynamicWidthComponent; |
||||||
|
import mightypork.gamecore.gui.components.painters.TextPainter; |
||||||
|
import mightypork.gamecore.input.InputSystem; |
||||||
|
import mightypork.gamecore.resources.fonts.GLFont; |
||||||
|
import mightypork.gamecore.util.math.color.Color; |
||||||
|
import mightypork.gamecore.util.math.color.pal.RGB; |
||||||
|
import mightypork.gamecore.util.math.constraints.vect.Vect; |
||||||
|
import mightypork.gamecore.util.math.constraints.vect.mutable.VectVar; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Menu-like button with shadow and push state |
||||||
|
* |
||||||
|
* @author 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(GLFont 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 (InputSystem.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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,45 @@ |
|||||||
|
package mightypork.gamecore.gui.components.layout; |
||||||
|
|
||||||
|
|
||||||
|
import mightypork.gamecore.core.modules.AppAccess; |
||||||
|
import mightypork.gamecore.gui.components.Component; |
||||||
|
import mightypork.gamecore.util.math.constraints.rect.proxy.RectBound; |
||||||
|
|
||||||
|
|
||||||
|
public class ColumnLayout extends GridLayout { |
||||||
|
|
||||||
|
private int col = 0; |
||||||
|
|
||||||
|
|
||||||
|
public ColumnLayout(AppAccess app, int rows) |
||||||
|
{ |
||||||
|
this(app, null, rows); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public ColumnLayout(AppAccess app, RectBound context, int cols) |
||||||
|
{ |
||||||
|
super(app, 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; |
||||||
|
} |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue