.
diff --git a/LICENCE.txt b/LICENCE.txt
new file mode 100644
index 0000000..d767c5a
--- /dev/null
+++ b/LICENCE.txt
@@ -0,0 +1,6 @@
+RetroWar Copyright 2016-2018 Richard Smith
+
+The code to the RetroWar-Common library is distributed under the GPL V3.
+
+(Note this does not apply to the other RetroWar packages, which are proprietary
+and all rights reserved.)
diff --git a/THIRD-PARTY.txt b/THIRD-PARTY.txt
new file mode 100644
index 0000000..971aee7
--- /dev/null
+++ b/THIRD-PARTY.txt
@@ -0,0 +1,21 @@
+Third Party Software may impose additional restrictions and it is the
+user's responsibility to ensure that they have met the licensing
+requirements of PhantomJS and the relevant license of the Third Party
+Software they are using.
+
+Files with additional authors/copyrights/licenses:
+
+Asset: cgwg's CRT shader
+File: android/assets/shaders/crt-cgwg-fast.glsl
+Copyright (C) 2010-2011 cgwg, Themaister
+License: GPL V2
+
+Asset: IBXM music player
+Files:
+Copyright: (c)2017 mumart@gmail.com https://github.com/martincameron/micromod
+License: BSD 3-clause license
+
+Asset: Blargg audio library
+Files:
+Copyright: (C) 2003-2007 Shay Green http://www.slack.net/~ant/libs/audio.html
+License: Lesser GPL V2.1
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..5363f3e
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,95 @@
+apply plugin: "kotlin"
+
+sourceCompatibility = 1.6
+[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
+
+sourceSets.main.java.srcDirs = ["src/"]
+
+
+repositories {
+ flatDir {
+ dirs '../libs'
+ }
+}
+
+configurations {
+ ktlint
+}
+
+task ktlint(type: JavaExec) {
+ main = "com.github.shyiko.ktlint.Main"
+ classpath = configurations.ktlint
+ args "src/**/*.kt"
+}
+
+check.dependsOn ktlint
+
+task ktlintFormat(type: JavaExec) {
+ main = "com.github.shyiko.ktlint.Main"
+ classpath = configurations.ktlint
+ args "-F", "src/**/*.kt"
+}
+
+dependencies {
+ compile 'io.sentry:sentry:1.7.3'
+ compile 'org.slf4j:slf4j-simple:1.7.21'
+ compile "com.code-disaster.steamworks4j:steamworks4j:1.6.2"
+ compile "com.badlogicgames.gdx:gdx-backend-lwjgl:$gdxVersion"
+ compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
+ compile "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
+ compile "net.dermetfan.libgdx-utils:libgdx-utils:$gdxUtilsVersion"
+ compile "net.mostlyoriginal.artemis-odb:contrib-core:2.2.0"
+ compile "net.mostlyoriginal.artemis-odb:contrib-eventbus:2.2.0"
+ compile "net.mostlyoriginal.artemis-odb:contrib-plugin-profiler:2.2.0"
+ ktlint 'com.github.shyiko:ktlint:0.27.0'
+ compile "net.onedaybeard.artemis:artemis-odb:2.1.0"
+ compile "net.onedaybeard.artemis:artemis-odb-serializer-kryo:2.1.0"
+ compile("com.esotericsoftware:kryo:4.0.2")
+ compile("com.esotericsoftware:kryonet:2.22.0-RC1") {
+ exclude module: 'kryo'
+ }
+ compile "com.badlogicgames.gdx:gdx:$gdxVersion"
+ compile "com.badlogicgames.gdx:gdx-box2d:$gdxVersion"
+ compile "com.badlogicgames.gdx:gdx-controllers:$gdxVersion"
+ compile "com.badlogicgames.ashley:ashley:$ashleyVersion"
+ compile "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
+ compile 'com.beust:klaxon:0.30'
+ // https://mvnrepository.com/artifact/org.codehaus.groovy/groovy-all
+ compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.5.0-rc-3'
+ compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.0'
+
+}
+
+apply plugin: 'org.jetbrains.dokka'
+
+dokka {
+ outputFormat = 'javadoc'
+ outputDirectory = "$buildDir/javadoc"
+ includeNonPublic = false
+ skipEmptyPackages = true
+ jdkVersion = 8
+
+ packageOptions {
+ prefix = "uk.me.fantastic.retro.music"
+ suppress = true
+ }
+ packageOptions {
+ prefix = "uk.me.fantastic.retro.menu"
+ suppress = true
+ }
+
+ packageOptions {
+ prefix = "uk.me.fantastic.retro.network"
+ suppress = true
+ }
+ packageOptions {
+ prefix = "de.golfgl.gdxgameanalytics"
+ suppress = true
+ }
+}
+
+jar {
+ manifest {
+ attributes 'Implementation-Version': version
+ }
+}
\ No newline at end of file
diff --git a/src/de/golfgl/gdxgameanalytics/GameAnalytics.java b/src/de/golfgl/gdxgameanalytics/GameAnalytics.java
new file mode 100755
index 0000000..1972afa
--- /dev/null
+++ b/src/de/golfgl/gdxgameanalytics/GameAnalytics.java
@@ -0,0 +1,688 @@
+package de.golfgl.gdxgameanalytics;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.Net;
+import com.badlogic.gdx.Preferences;
+import com.badlogic.gdx.utils.*;
+
+import java.lang.StringBuilder;
+import java.util.HashMap;
+import java.util.Map;
+
+import static uk.me.fantastic.retro.GlobalsKt.log;
+
+/**
+ * Gameanalytics.com client for libGDX
+ *
+ * Created by Benjamin Schulte on 05.05.2018 based up on example implementation
+ * https://s3.amazonaws.com/download.gameanalytics.com/examples/GameAnalytics+REST+API+example.java
+ *
+ * (That's the reason why this code looks like made by a server dev - it acutally is. Improvements welcome)
+ */
+
+public class GameAnalytics {
+ protected static final String URL_SANDBOX = "http://sandbox-api.gameanalytics.com/v2/";
+ protected static final String TAG = "Gameanalytics";
+ private final static String sdk_version = "rest api v2";
+ private static final int FLUSH_QUEUE_INTERVAL = 20;
+ private static final String URL_GAMEANALYTICS = "https://api.gameanalytics.com/v2/";
+ private static final int MAX_EVENTS_SENT = 100;
+ private static final int MAX_EVENTS_CACHED = 1000;
+
+ // possible TODO: Timer fires on foreground thread, building and compressing content should be done in background
+ protected Timer.Task pingTask;
+
+ protected String url = URL_GAMEANALYTICS;
+ protected boolean flushingQueue;
+ private String game_key = null;
+ private String secret_key = null;
+ //dimension information
+ private String platform = null;
+ private String os_version = null;
+ private String device = "unknown";
+ private String manufacturer = "unkown";
+ //game information
+ private String build;
+ //user information
+ private String user_id = null;
+ private String session_id;
+ private int session_num = 0;
+ private String custom1;
+ private String custom2;
+ private String custom3;
+ //SDK status - this is false when not initialized or initializing failed
+ private boolean connectionInitialized = false;
+ private int nextQueueFlushInSeconds = 0;
+ private Queue waitingQueue = new Queue();
+ private Queue sendingQueue = new Queue();
+ private int failedFlushAttempts;
+ private long timeStampDiscrepancy;
+ private long sessionStartTimestamp;
+ private Preferences prefs;
+
+ /**
+ * initializes and starts the session. Make sure you have set all neccessary parameters before calling this
+ * This can be called but twice, but if it is called when a session is still ongoing, it just resets the session
+ * start time.
+ *
+ * Call this on game start and on resume.
+ */
+ public void startSession() {
+ if (sessionStartTimestamp > 0 && connectionInitialized) {
+ log(TAG, "No new session started. Session still ongoing");
+ sessionStartTimestamp = TimeUtils.millis();
+ return;
+ }
+
+ if (game_key == null || secret_key == null)
+ throw new IllegalStateException("You must set your game key and secret key");
+
+ if (platform == null)
+ setPlatform(GwtIncompatibleStuff.getDefaultPlatform(Gdx.app.getType()));
+
+ if (os_version == null)
+ throw new IllegalStateException("You need to set a os version");
+
+ if (prefs == null)
+ log(TAG, "You did not set up preferences. Session and user tracking will not work without it");
+
+ loadOrInitUserStringAndSessionNum();
+
+ session_id = GwtIncompatibleStuff.generateUuid();
+
+ submitInitRequest();
+ // start session is called if request is successful
+ }
+
+ private void loadOrInitUserStringAndSessionNum() {
+ if (prefs != null) {
+ user_id = prefs.getString("ga_userid", null);
+ session_num = prefs.getInteger("ga_sessionnum", 0);
+ }
+
+ if (user_id == null || user_id.isEmpty()) {
+ log(TAG, "No user id found. Generating a new one.");
+ user_id = GwtIncompatibleStuff.generateUuid();
+
+ if (prefs != null)
+ prefs.putString("ga_userid", user_id);
+ }
+
+ session_num++;
+
+ if (prefs != null) {
+ prefs.putInteger("ga_sessionnum", session_num);
+ prefs.flush();
+ }
+ }
+
+ private int loadAndIncrementTransactionNum() {
+ if (prefs == null)
+ return 0;
+
+ int transactionNum = prefs.getInteger("ga_transactionnum", 0);
+ transactionNum++;
+ prefs.putInteger("ga_transactionnum", transactionNum);
+ prefs.flush();
+ return transactionNum;
+ }
+
+ /**
+ * gets called every second by pingtask
+ */
+ protected void flushQueue() {
+ log("flushQueue");
+ if (!connectionInitialized || flushingQueue)
+ return;
+
+ // countdown to flush
+ if (nextQueueFlushInSeconds > 0) {
+ nextQueueFlushInSeconds -= 1;
+ return;
+ }
+
+ if (waitingQueue.size == 0 && sendingQueue.size == 0)
+ return;
+
+ flushingQueue = true;
+ nextQueueFlushInSeconds = FLUSH_QUEUE_INTERVAL;
+
+ StringBuilder payload = new StringBuilder();
+ Json json = new Json();
+ json.setOutputType(JsonWriter.OutputType.json);
+
+ synchronized (waitingQueue) {
+ while (sendingQueue.size < MAX_EVENTS_SENT && waitingQueue.size > 0)
+ sendingQueue.addLast(waitingQueue.removeFirst());
+
+ log(TAG, "Sending queue with " + sendingQueue.size + " events");
+ payload.append("[");
+ for (int i = 0; i < sendingQueue.size; i++) {
+ payload.append(json.toJson(sendingQueue.get(i)));
+ if (i != sendingQueue.size - 1)
+ payload.append(",");
+ }
+ }
+ payload.append("]");
+
+ final Net.HttpRequest request = createHttpRequest(this.url + game_key + "/events", payload.toString());
+ //Execute and read response
+ Gdx.net.sendHttpRequest(request, new Net.HttpResponseListener() {
+ @Override
+ public void handleHttpResponse(Net.HttpResponse httpResponse) {
+ synchronized (waitingQueue) {
+ sendingQueue.clear();
+ }
+
+ int statusCode = httpResponse.getStatus().getStatusCode();
+ String resultAsString = httpResponse.getResultAsString();
+
+ if (statusCode == 200)
+ log(TAG, statusCode + " " + resultAsString);
+ else
+ log(TAG, statusCode + " " + resultAsString);
+
+ failedFlushAttempts = 0;
+ flushingQueue = false;
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ failed();
+ }
+
+ @Override
+ public void cancelled() {
+ failed();
+ }
+
+ private void failed() {
+ log(TAG, "Could not send events in queue - probably offline");
+ // lengthen the time to the next waitingQueue flush after a fail, but not more than 180 seconds
+ failedFlushAttempts = Math.min(failedFlushAttempts + 1, 180 / FLUSH_QUEUE_INTERVAL);
+ nextQueueFlushInSeconds = FLUSH_QUEUE_INTERVAL * (failedFlushAttempts + 1);
+ log(TAG, "Next flush attempt in " + nextQueueFlushInSeconds + " seconds");
+ flushingQueue = false;
+ }
+ });
+ }
+
+ private Net.HttpRequest createHttpRequest(String url, String payload) {
+ final Net.HttpRequest request = new Net.HttpRequest("POST");
+ request.setUrl(url);
+ String hash = GwtIncompatibleStuff.setHttpRequestContent(request, payload, secret_key);
+ request.setHeader("Accept", "application/json");
+ request.setHeader("Content-type", "application/json");
+ request.setHeader("Authorization", hash);
+ return request;
+ }
+
+ private void addToWaitingQueue(AnnotatedEvent event) {
+ while (waitingQueue.size > MAX_EVENTS_CACHED)
+ waitingQueue.removeFirst();
+
+ waitingQueue.addLast(event);
+ }
+
+ private void submitStartSessionRequest() {
+ AnnotatedEvent event = new AnnotatedEvent();
+ event.put("category", "user");
+ synchronized (waitingQueue) {
+ addToWaitingQueue(event);
+ }
+ }
+
+ public void submitDesignEvent(String event_id) {
+ if (!isInitialized())
+ return;
+
+ AnnotatedEvent event = new AnnotatedEvent();
+ event.put("category", "design");
+ event.put("event_id", event_id);
+ synchronized (waitingQueue) {
+ log(TAG, "Queuing design event");
+ addToWaitingQueue(event);
+ }
+ }
+
+ public void submitDesignEvent(String event_id, float value) {
+ if (!isInitialized())
+ return;
+
+ AnnotatedEvent event = new AnnotatedEvent();
+ event.put("category", "design");
+ event.put("event_id", event_id);
+ event.putFloat("value", value);
+ synchronized (waitingQueue) {
+ log(TAG, "Queuing design event");
+ addToWaitingQueue(event);
+ }
+ }
+
+ /**
+ * Submits a payment transaction to GameAnalytics
+ *
+ * @param itemType category for items
+ * @param itemId identifier for what has been purchased
+ * @param amount in cents
+ * @param currency see http://openexchangerates.org/currencies.json
+ */
+ public void submitBusinessEvent(String itemType, String itemId, int amount, String currency) {
+ if (!isInitialized())
+ return;
+
+ AnnotatedEvent event = new AnnotatedEvent();
+ event.put("category", "business");
+ event.put("event_id", itemType + ":" + itemId);
+ event.putInt("amount", amount);
+ event.put("currency", currency);
+ event.putInt("transaction_num", loadAndIncrementTransactionNum());
+ synchronized (waitingQueue) {
+ log(TAG, "Queuing business event");
+ addToWaitingQueue(event);
+ }
+ }
+
+ public void submitProgressionEvent(ProgressionStatus status, String progression01, String progression02,
+ String progression03) {
+ submitProgressionEvent(status, progression01, progression02, progression03, 0, 0);
+ }
+
+ public void submitProgressionEvent(ProgressionStatus status, String progression01, String progression02,
+ String progression03, int score, int attemptNum) {
+ if (!isInitialized())
+ return;
+
+ AnnotatedEvent event = new AnnotatedEvent();
+ event.put("category", "progression");
+
+ String event_id = getStatusString(status) + ":" + progression01;
+ if (progression02.length() > 0) {
+ event_id += ":" + progression02;
+ }
+ if (progression03.length() > 0) {
+ event_id += ":" + progression03;
+ }
+ event.put("event_id", event_id);
+
+ if (status == ProgressionStatus.Complete || status == ProgressionStatus.Fail) {
+ if (attemptNum > 0)
+ event.putInt("attempt_num", attemptNum);
+ if (score > 0)
+ event.putInt("score", score);
+ }
+ synchronized (waitingQueue) {
+ log(TAG, "Queuing progression event");
+ addToWaitingQueue(event);
+ }
+ }
+
+ private String getStatusString(ProgressionStatus status) {
+ switch (status) {
+ case Start:
+ return "Start";
+ case Fail:
+ return "Fail";
+ default:
+ return "Complete";
+ }
+ }
+
+ private void createResourceEvent(ResourceFlowType flowType, String virtualCurrency, String itemType,
+ String itemId, float amount) {
+ if (!isInitialized())
+ return;
+
+ AnnotatedEvent event = new AnnotatedEvent();
+ event.put("category", "resource");
+
+ String event_id = getFlowTypeString(flowType) + ":" + virtualCurrency + ":" + itemType + ":" + itemId;
+ event.put("event_id", event_id);
+ event.putFloat("amount", amount);
+ synchronized (waitingQueue) {
+ log(TAG, "Queuing resource event");
+ addToWaitingQueue(event);
+ }
+ }
+
+ private String getFlowTypeString(ResourceFlowType flowType) {
+ switch (flowType) {
+ case Sink:
+ return "Skink";
+ default:
+ return "Source";
+ }
+ }
+
+ /**
+ * submits an error event
+ *
+ * @param severity
+ * @param message
+ */
+ public void submitErrorEvent(ErrorType severity, String message) {
+ if (!isInitialized())
+ return;
+
+ if (message.length() > 8000)
+ message = message.substring(0, 8000);
+
+ AnnotatedEvent event = new AnnotatedEvent();
+ event.put("category", "error");
+ event.put("severity", getSeverityString(severity));
+ event.put("message", message);
+ synchronized (waitingQueue) {
+ log(TAG, "Queuing error event (" + message + ")");
+ addToWaitingQueue(event);
+ }
+ }
+
+ private String getSeverityString(ErrorType severity) {
+ switch (severity) {
+ case info:
+ return "info";
+ case debug:
+ return "debug";
+ case error:
+ return "error";
+ case critical:
+ return "critical";
+ default:
+ return "warning";
+ }
+ }
+
+ /**
+ * closes the ongoing session. Call this on your game's pause() method
+ *
+ * This is failsafe - if no session is open, nothing is done
+ */
+ public void closeSession() {
+ //TODO should get saved for next time, but works well on Android (not on Desktop though)
+ if (sessionStartTimestamp > 0 && connectionInitialized) {
+ AnnotatedEvent session_end_event = new AnnotatedEvent();
+ session_end_event.put("category", "session_end");
+ session_end_event.putInt("length", (int) ((TimeUtils.millis() - sessionStartTimestamp) / 1000L));
+
+ //this will not work if queue is full. But in that case, the message will probably never get sent
+ addToWaitingQueue(session_end_event);
+ flushQueueImmediately();
+ }
+ sessionStartTimestamp = 0;
+ }
+
+ public void flushQueueImmediately() {
+ nextQueueFlushInSeconds = 0;
+ flushQueue();
+ }
+
+ /**
+ * send init request
+ */
+ protected void submitInitRequest() {
+ Json json = new Json();
+ json.setOutputType(JsonWriter.OutputType.json);
+
+ String event = "[" + json.toJson(new InitEvent()) + "]";
+
+ final Net.HttpRequest request = createHttpRequest(url + game_key + "/init", event);
+
+ connectionInitialized = false;
+ timeStampDiscrepancy = 0;
+
+ //Execute and read response
+ Gdx.net.sendHttpRequest(request, new Net.HttpResponseListener() {
+ @Override
+ public void handleHttpResponse(Net.HttpResponse httpResponse) {
+ connectionInitialized = httpResponse.getStatus().getStatusCode() == 200;
+ String resultAsString = httpResponse.getResultAsString();
+
+ if (connectionInitialized) {
+ log(TAG, httpResponse.getStatus().getStatusCode() + " " + resultAsString);
+ // calculate the client's time stamp discrepancy
+
+ sessionStartTimestamp = TimeUtils.millis();
+ try {
+ JsonValue response = new JsonReader().parse(resultAsString);
+ long serverTimestamp = response.getLong("server_ts") * 1000L;
+ timeStampDiscrepancy = serverTimestamp - TimeUtils.millis();
+ log(TAG, "Session open. Time stamp discrepancy in ms: " +
+ timeStampDiscrepancy);
+ } catch (Exception e) {
+ // do nothing
+ }
+
+ submitStartSessionRequest();
+ flushQueueImmediately();
+
+ // add automated task to flush the qeue every 20 seconds
+ // FIXME if this is called while lockscreen is on, task is not working. Mostly a problem when
+ // testing with adb
+ if (pingTask == null)
+ pingTask = Timer.schedule(new Timer.Task() {
+ @Override
+ public void run() {
+ flushQueue();
+ }
+ }, 1, 1);
+ } else
+ log(TAG, "Connection attempt failed: " + httpResponse.getStatus().getStatusCode() + " "
+ + resultAsString);
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ cancelled();
+ }
+
+ @Override
+ public void cancelled() {
+ connectionInitialized = false;
+ log(TAG, "Could not connect to GameAnalytics - suspended");
+ }
+ });
+ }
+
+ /**
+ * @return if events are sent to gameanalytics after a successful login
+ */
+ public boolean isInitialized() {
+ return connectionInitialized;
+ }
+
+ /**
+ * @return current time on server. Only valid after successful initialization, so check {@link #isInitialized()}
+ * before trusting this value
+ */
+ public long getCurrentServerTime() {
+ return TimeUtils.millis() + timeStampDiscrepancy;
+ }
+
+ public void setGameKey(String gamekey) {
+ this.game_key = gamekey;
+ }
+
+ public void setGameSecretKey(String secretkey) {
+ this.secret_key = secretkey;
+ }
+
+ public String getPlatform() {
+ return platform;
+ }
+
+ public void setPlatform(Platform platform) {
+ switch (platform) {
+ case Windows:
+ this.platform = "windows";
+ return;
+ case WebGL:
+ this.platform = "webgl";
+ return;
+ case iOS:
+ this.platform = "ios";
+ return;
+ case MacOS:
+ this.platform = "mac_osx";
+ return;
+ case Android:
+ this.platform = "android";
+ return;
+ case Linux:
+ this.platform = "linux";
+ return;
+ }
+ }
+
+ public String getPlatformVersionString() {
+ return os_version;
+ }
+
+ /**
+ * @param os_version must match [0-9]{0,5}(\.[0-9]{0,5}){0,2}$. Unique value limit is 255
+ * @return if your version String matched the expected regex.
+ */
+ public boolean setPlatformVersionString(String os_version) {
+ this.os_version = os_version;
+ boolean matches = os_version.matches("[0-9]{0,5}(\\.[0-9]{0,5}){0,2}");
+ return matches;
+ }
+
+ public String getGameBuildNumber() {
+ return build;
+ }
+
+ /**
+ * @param build buildnumber of your game. This is a string, so you can also add build type information
+ * (e.g. "1818_debug", "1205_amazon", "1.5_tv") - but be aware, limit of unit strings is 100
+ */
+ public void setGameBuildNumber(String build) {
+ this.build = build;
+ }
+
+ /**
+ * @param device device information. Unqiue value limit is 500
+ */
+ public void setDevice(String device) {
+ if (device.length() > 30)
+ device = device.substring(0, 30);
+
+ this.device = device;
+ }
+
+ public void setManufacturer(String manufacturer) {
+ this.manufacturer = manufacturer;
+ }
+
+ /**
+ * @param prefs your game's preferences. Needed to save user id and session information. All settings will be
+ * saved with "ga_" prefix to not interphere with your other settings
+ */
+ public void setPrefs(Preferences prefs) {
+ this.prefs = prefs;
+ }
+
+ /**
+ * @param custom1 value for custom dimension. 50 different values supported at max, max length 32
+ */
+ public void setCustom1(String custom1) {
+ this.custom1 = custom1;
+ }
+
+ /**
+ * @param custom2 value for custom dimension. 50 different values supported at max, max length 32
+ */
+ public void setCustom2(String custom2) {
+ this.custom2 = custom2;
+ }
+
+ /**
+ * @param custom3 value for custom dimension. 50 different values supported at max, max length 32
+ */
+ public void setCustom3(String custom3) {
+ this.custom3 = custom3;
+ }
+
+ public enum ProgressionStatus {Start, Fail, Complete}
+
+ public enum ResourceFlowType {Sink, Source}
+
+ public enum ErrorType {debug, info, warning, error, critical}
+
+ /**
+ * Gameanalytics does not allow free definition of platforms. I did not find a documented list of supported
+ * platforms, but these ones work
+ */
+ public enum Platform {
+ Windows, Linux, Android, iOS, WebGL, MacOS
+ }
+
+ private class InitEvent implements Json.Serializable {
+ @Override
+ public void write(Json json) {
+ json.writeValue("platform", platform);
+ json.writeValue("os_version", platform + " " + os_version);
+ json.writeValue("sdk_version", sdk_version);
+ }
+
+ @Override
+ public void read(Json json, JsonValue jsonData) {
+ // not implemented
+ }
+ }
+
+ private class AnnotatedEvent implements Json.Serializable {
+ private Map keyValues = new HashMap();
+ private String sessionId;
+ private int sessionNum;
+
+ public AnnotatedEvent() {
+ //this is stored
+ keyValues.put("client_ts", (Long) getCurrentServerTime() / 1000L);
+ this.sessionId = session_id;
+ this.sessionNum = session_num;
+ }
+
+ @Override
+ public void write(Json event) {
+ event.writeValue("platform", platform);
+ event.writeValue("os_version", platform + " " + os_version);
+ event.writeValue("sdk_version", sdk_version);
+ event.writeValue("device", device);
+ event.writeValue("manufacturer", manufacturer);
+ if (build != null)
+ event.writeValue("build", build);
+ event.writeValue("user_id", user_id);
+ event.writeValue("v", 2);
+ if (custom1 != null)
+ event.writeValue("custom_01", custom1);
+ if (custom2 != null)
+ event.writeValue("custom_02", custom2);
+ if (custom3 != null)
+ event.writeValue("custom_03", custom3);
+
+ event.writeValue("session_id", sessionId);
+ event.writeValue("session_num", sessionNum);
+
+ for (String key : keyValues.keySet()) {
+ event.writeValue(key, keyValues.get(key));
+ }
+ }
+
+ @Override
+ public void read(Json json, JsonValue jsonData) {
+ // not supported
+ }
+
+ public void put(String name, String value) {
+ keyValues.put(name, value);
+ }
+
+ public void putInt(String name, int value) {
+ keyValues.put(name, value);
+ }
+
+ public void putFloat(String name, float value) {
+ keyValues.put(name, value);
+ }
+ }
+}
diff --git a/src/de/golfgl/gdxgameanalytics/GwtIncompatibleStuff.java b/src/de/golfgl/gdxgameanalytics/GwtIncompatibleStuff.java
new file mode 100755
index 0000000..c6f286c
--- /dev/null
+++ b/src/de/golfgl/gdxgameanalytics/GwtIncompatibleStuff.java
@@ -0,0 +1,104 @@
+package de.golfgl.gdxgameanalytics;
+
+import com.badlogic.gdx.Application;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.Net;
+import com.badlogic.gdx.utils.Base64Coder;
+import com.badlogic.gdx.utils.SharedLibraryLoader;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.UUID;
+import java.util.zip.GZIPOutputStream;
+
+/**
+ * Created by Benjamin Schulte on 05.05.2018.
+ */
+
+public class GwtIncompatibleStuff {
+
+ private static String generateHash(byte[] json, String secretKey) {
+ try {
+ Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
+ byte[] encoded = secretKey.getBytes();
+ SecretKeySpec secretKeySpec = new SecretKeySpec(encoded, "HmacSHA256");
+ sha256_HMAC.init(secretKeySpec);
+ return new String(Base64Coder.encode(sha256_HMAC.doFinal(json)));
+ } catch (Exception ex) {
+ Gdx.app.error(GameAnalytics.TAG, "Error generating Hmac: " + ex.toString());
+ return "";
+ }
+ }
+
+ /**
+ * @return UUID on Java, a nearly-UUID on GWT
+ */
+ public static String generateUuid() {
+ UUID sid = UUID.randomUUID();
+ return sid.toString();
+ }
+
+ protected static GameAnalytics.Platform getDefaultPlatform(Application.ApplicationType type) {
+ switch (type) {
+ case Android:
+ return GameAnalytics.Platform.Android;
+ case WebGL:
+ return GameAnalytics.Platform.WebGL;
+ default:
+ if (SharedLibraryLoader.isWindows)
+ return GameAnalytics.Platform.Windows;
+ else if (SharedLibraryLoader.isLinux)
+ return GameAnalytics.Platform.Linux;
+ else if (SharedLibraryLoader.isIos)
+ return GameAnalytics.Platform.iOS;
+ else if (SharedLibraryLoader.isMac)
+ return GameAnalytics.Platform.MacOS;
+ else
+ throw new IllegalStateException("You need to set a platform");
+ }
+ }
+
+ /**
+ * sets the http request content, zipped if possible.
+ *
+ * @return header for authentication
+ */
+ protected static String setHttpRequestContent(Net.HttpRequest request, String content, String secretKey) {
+ byte[] compressedContent = null;
+ String hash;
+
+ try {
+ compressedContent = compress(content);
+ } catch (Throwable t) {
+ // do nothing
+ }
+
+ Gdx.app.debug(GameAnalytics.TAG, content);
+
+ if (compressedContent != null) {
+ Gdx.app.debug(GameAnalytics.TAG, "(Compressed from " + content.length() +
+ " to " + compressedContent.length + " bytes)");
+
+ request.setContent(new ByteArrayInputStream(compressedContent), compressedContent.length);
+ hash = GwtIncompatibleStuff.generateHash(compressedContent, secretKey);
+ request.setHeader("Content-Encoding", "gzip");
+ } else {
+ hash = GwtIncompatibleStuff.generateHash(content.getBytes(), secretKey);
+ request.setContent(content);
+ }
+ return hash;
+ }
+
+ public static byte[] compress(String paramString) throws IOException {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(paramString.length());
+ GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);
+ gzipOutputStream.write(paramString.getBytes());
+ gzipOutputStream.close();
+ byte[] bytes = byteArrayOutputStream.toByteArray();
+ byteArrayOutputStream.close();
+ return bytes;
+ }
+}
diff --git a/src/uk/me/fantastic/retro/AbstractGameFactory.kt b/src/uk/me/fantastic/retro/AbstractGameFactory.kt
new file mode 100644
index 0000000..e491a5d
--- /dev/null
+++ b/src/uk/me/fantastic/retro/AbstractGameFactory.kt
@@ -0,0 +1,47 @@
+package uk.me.fantastic.retro
+
+import com.badlogic.gdx.graphics.Texture
+import uk.me.fantastic.retro.menu.MultiChoiceMenuItem
+import uk.me.fantastic.retro.screens.GameSession
+
+/**
+ * GameFactories provide RetroWar with info about a game and generate an
+ * instance of the game on demand, applying any configuration that has been
+ * stored in the factory.
+ *
+ * If you are making a stand-alone SimpleGame you don't need this, just create
+ * a SimpleGameFactory. But if you are making a plugin for RetroWar then you need to write
+ * your own subclass of AbstractGameFactory.
+ *
+ * @property name Game name
+ * @property levels a List of level names, only for games that have multiple levels. May be null.
+ */
+abstract class AbstractGameFactory(val name: String, val levels: List? = null) {
+
+ /** Currently selected level number */
+ var level = 0
+
+ /** Whether the game should be shown on the main menu or relegated to the 'mods' menu */
+ var showOnGamesMenu = true
+
+ /** Texture screenshot or logo to display on menu */
+ abstract val image: Texture
+
+ /** Description displayed on menu */
+ abstract val description: String
+
+ abstract fun create(session: GameSession): Game
+
+ /** For mult-game tournaments ignore the settings in this factory and create a game with some
+ * defaults appropriate for a tournament */
+ open fun createWithDefaultSettings(session: GameSession): Game {
+ return create(session)
+ }
+
+ /**
+ * Any MenuItems in this List will be displayed by RetroWar on an option screen
+ * It's a way to configure the Factory via a GUI
+ * If there are none, just leave List empty
+ */
+ open val options: List = ArrayList()
+}
\ No newline at end of file
diff --git a/src/uk/me/fantastic/retro/App.kt b/src/uk/me/fantastic/retro/App.kt
new file mode 100644
index 0000000..5de93ab
--- /dev/null
+++ b/src/uk/me/fantastic/retro/App.kt
@@ -0,0 +1,283 @@
+//
+/*
+ Copyright 2018 Richard Smith.
+
+ RetroWar is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ RetroWar is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with RetroWar. If not, see .
+*/
+//
+package uk.me.fantastic.retro
+
+import com.badlogic.gdx.Application
+import com.badlogic.gdx.Game
+import com.badlogic.gdx.Gdx
+import com.badlogic.gdx.Input
+import com.badlogic.gdx.InputAdapter
+import com.badlogic.gdx.Screen
+import com.badlogic.gdx.controllers.Controllers
+import com.codedisaster.steamworks.SteamAPI
+import de.golfgl.gdxgameanalytics.GameAnalytics
+import uk.me.fantastic.retro.Prefs.BinPref
+import uk.me.fantastic.retro.input.GamepadInput
+import uk.me.fantastic.retro.input.KeyboardMouseInput
+import uk.me.fantastic.retro.input.MappedController
+import uk.me.fantastic.retro.input.SimpleTouchscreenInput
+import uk.me.fantastic.retro.input.StatefulController
+import uk.me.fantastic.retro.music.ibxm.IBXMPlayer
+import uk.me.fantastic.retro.network.Client
+import uk.me.fantastic.retro.network.Server
+import uk.me.fantastic.retro.screens.GameSession
+import uk.me.fantastic.retro.utils.RetroShader
+import java.io.BufferedReader
+import java.io.File
+import java.io.InputStreamReader
+import java.net.URL
+import kotlin.concurrent.thread
+
+/**
+ * Main libgdx common application class. Created by platform specific launchers.
+ * Delegates actual rendering loop to Screens
+ *
+ * For most games you can use SimpleApp rather than making your own subclass of App.
+ *
+ * @param callback For setting maximum FPS, platform specific
+ * @param logger If debug is on then logs are sent here, which may send them to screen, file or
+ * cloud.
+ * @param manualGC GDX on iOS has very poor garbage collection. Supply one of these to disable
+ * it and do your own GC. Otherwise null.
+ */
+abstract class App(val callback: Callback, val logger: Logger, val manualGC: ManualGC? = null) :
+ Game() {
+
+ /** Title screen */
+ var title: Screen? = null
+
+ /** If you are using GameAnalytics service set this, otherwise null */
+ var gameAnalytics: GameAnalytics? = null
+
+ companion object {
+ /** A static reference to the singleton Application */
+ @JvmStatic
+ lateinit var app: App
+
+ val LOG_FILE_PATH: String = System.getProperty("user.home") + File.separator + "retrowar-log.txt"
+ val PREF_DIR: String = System.getProperty("user.home") + File.separator + ".prefs"
+ }
+
+ init {
+ app = this
+ findIPaddress()
+ }
+
+ protected fun findIPaddress() {
+ thread {
+ try {
+ val whatismyip = URL("http://checkip.amazonaws.com")
+ val i = BufferedReader(InputStreamReader(whatismyip.openStream()))
+ ip = i.readLine()
+ } catch (e: Exception) {
+ }
+ }
+ }
+
+ /** Uses the Callback to set max FPS, if the platform supports it */
+ fun setFPS(f: Int) {
+ callback.setForegroundFPS(f)
+ callback.setBackgroundFPS(f)
+ }
+
+ /** Setup network stuff, not currently working */
+ protected fun initialiseNetwork() {
+ server = Server()
+ server?.initialise()
+ client = Client()
+ client?.initialise()
+ }
+
+ internal val mappedControllers = ArrayList()
+
+ internal val statefulControllers = ArrayList()
+
+ /** Current IP address, if known. Else "unknown" */
+ var ip: String = "unknown"
+
+ /** May be null if no Server */
+ var server: Server? = null
+
+ /** May be null if no Client */
+ var client: Client? = null
+
+ lateinit var controllerTest: GameFactory
+ lateinit var screenTest1: GameFactory
+ lateinit var screenTest2: GameFactory
+
+ val ibxmPlayer = IBXMPlayer()
+
+ internal var mouseClicked = false
+
+ lateinit var shader: RetroShader
+
+ fun anyKeyHit(): Boolean {
+ return statefulControllers.any { it.isButtonAJustPressed } ||
+ app.mouseJustClicked ||
+ Gdx.input.isKeyJustPressed(Input.Keys.SPACE) ||
+ Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)
+ }
+
+ fun testSandbox(): String {
+ return (App::class.java.name + ": I shouldnt be able to do this: " + System.getProperty("os" +
+ ".name"))
+ }
+
+ val mouseJustClicked: Boolean
+ get() {
+ val t = mouseClicked
+ mouseClicked = false
+ return t
+ }
+
+ val versionString = (App::class.java.`package`.implementationVersion ?: "devel")
+
+ fun swapScreenAndDispose(screen: Screen) {
+ log("swapscreen")
+ val s = app.screen
+ app.setScreen(screen)
+ s.dispose()
+ }
+
+ fun showTitleScreen() {
+ val title = title
+ if (title != null) {
+ swapScreenAndDispose(title)
+ } else {
+ log("There is no titlescreen so exiting")
+ quit()
+ }
+ }
+
+ abstract fun quit()
+
+ fun clearEvents() {
+ statefulControllers.forEach { it.clearEvents() }
+ mouseJustClicked // eat the event if there is a click already waiting
+ }
+
+ protected fun initialisePrefs() {
+ BinPref.values().forEach(BinPref::apply)
+ Prefs.MultiChoicePref.LIMIT_FPS.apply()
+ }
+
+ protected fun initialiseControllers() {
+ println("Detected ${Controllers.getControllers().size} controllers")
+
+ Controllers.getControllers().mapTo(mappedControllers, ::MappedController)
+
+ mappedControllers.mapTo(statefulControllers, ::StatefulController)
+ }
+
+ protected fun initializeInput() {
+ Gdx.input.inputProcessor = object : InputAdapter() {
+ override fun touchUp(x: Int, y: Int, pointer: Int, button: Int): Boolean {
+ log("app touchdown")
+ mouseClicked = true
+ return true
+ }
+ }
+ }
+
+ protected fun initialiseDesktop() {
+ }
+
+ protected fun initialiseAndroid() {
+ if (Gdx.app.type == Application.ApplicationType.Android) {
+ Gdx.input.isCatchBackKey = true
+ }
+ }
+
+ fun initialiseShader() {
+ shader = RetroShader("shaders/" + Prefs.MultiChoicePref.SHADER.getString())
+ }
+
+ protected fun initialiseSteam() {
+ System.out.println("Initialise Steam client API ...")
+
+ if (!SteamAPI.init()) {
+ log("steam error")
+ SteamAPI.printDebugInfo(System.err)
+ }
+
+ SteamAPI.printDebugInfo(System.out)
+ }
+
+ fun setScreenMode() {
+ if (BinPref.FULLSCREEN.isEnabled()) {
+ Gdx.graphics.setFullscreenMode(Gdx.graphics.displayMode)
+ } else {
+ Gdx.graphics.setWindowedMode(832, 512)
+ }
+ Gdx.graphics.setVSync(BinPref.VSYNC.isEnabled())
+ }
+
+ open fun submitAnalytics(s: String) {
+ gameAnalytics?.submitDesignEvent(s)
+ }
+
+ fun configureSessionWithPreSelectedInputDevice(session: GameSession){
+ if (Gdx.app.type == Application.ApplicationType.Desktop) {
+ val controller1 = App.app.mappedControllers.firstOrNull()
+ if (controller1 != null) {
+ session.preSelectedInputDevice = GamepadInput(controller1)
+ } else {
+ session.preSelectedInputDevice = KeyboardMouseInput(session)
+ session.KBinUse = true
+ }
+ } else if (isMobile) {
+ session.preSelectedInputDevice = SimpleTouchscreenInput()
+ }
+ }
+}
+
+// class SingleGameAppFromClass(callback: Callback, val name: String, val gameClazz: Class, val screenClazz: Class, val t: Screen? = null) : App(callback) {
+//
+// val factory: AbstractGameFactory =
+// GameFactory(name = name,
+// createGame =
+// { session: GameSession -> (gameClazz.getConstructor(GameSession::class.java).newInstance(session)) }
+// )
+//
+//
+// override fun create() {
+// log("SingleGameAppFromClass create")
+//
+// app = this
+// games = listOf(factory)
+//
+// initialiseAndroid()
+// initialiseDesktop()
+// setPrefsToDefaultsForSingleGames()
+// initialisePrefs()
+// initializeInput()
+// initialiseControllers()
+//
+//
+// val game = GameSession(factory)
+//
+// if (screenClazz != null) {
+// title = screenClazz.newInstance()
+// setScreen(title)
+// } else {
+// setScreen(game)
+// }
+//
+// }
+// }
diff --git a/src/uk/me/fantastic/retro/ControllerTester.kt b/src/uk/me/fantastic/retro/ControllerTester.kt
new file mode 100644
index 0000000..21aa85f
--- /dev/null
+++ b/src/uk/me/fantastic/retro/ControllerTester.kt
@@ -0,0 +1,125 @@
+package uk.me.fantastic.retro
+
+import com.badlogic.gdx.Gdx
+import com.badlogic.gdx.controllers.Controllers
+import com.badlogic.gdx.controllers.PovDirection
+import com.badlogic.gdx.graphics.GL20
+import com.badlogic.gdx.graphics.g2d.Batch
+import com.badlogic.gdx.utils.Align
+import com.badlogic.gdx.utils.GdxRuntimeException
+import uk.me.fantastic.retro.App.Companion.app
+import uk.me.fantastic.retro.screens.GameSession
+
+/**
+ * A simple RetroWar game
+ */
+class ControllerTester(session: GameSession) // Constructor (required)
+// width and height of screen in pixels
+ : SimpleGame(session, 640f, 480f) {
+
+ override fun doLogic(deltaTime: Float) { // Called automatically every frame
+ }
+
+ override fun doDrawing(batch: Batch) { // called automatically every frame
+
+ Gdx.gl.glClearColor(0f, 0f, 0f, 1f) // clear the screen
+ Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)
+
+ font.draw(batch, "Controllers connected: ${Controllers.getControllers().size}", 0f, 20f)
+ // batch.begin()
+
+ var x = 0f
+ for (i in 0..app.mappedControllers.lastIndex) {
+ var y = 472f
+ val m = app.mappedControllers[i]
+ val c = m.controller
+ font.draw(batch, c.name, x, y, 256f, Align.left, false)
+ y -= 8
+ font.draw(batch, m.mapping, x, y, 256f, Align.left, false)
+ for (j in 0..31) {
+ if (c.getButton(j)) {
+ y -= 8f
+ val mapping: String = when (j) {
+ m.A -> "A"
+ m.B -> "B"
+ m.X -> "X"
+ m.Y -> "Y"
+ m.L_BUMPER -> "L_BUMPER"
+ m.R_BUMPER -> "R_BUMPER"
+ m.GUIDE -> "GUIDE"
+ m.BACK -> "BACK"
+ m.START -> "START"
+ m.DPAD_DOWN -> "DPAD_DOWN"
+ m.DPAD_UP -> "DPAD_UP"
+ m.DPAD_LEFT -> "DPAD_LEFT"
+ m.DPAD_RIGHT -> "DPAD_RIGHT"
+ m.L_TRIGGER -> "L_TRIGGER"
+ m.R_TRIGGER -> "R_TRIGGER"
+ m.R_STICK_PUSH -> "R_STICK_PUSH"
+ m.L_STICK_PUSH -> "L_STICK_PUSH"
+ else -> "UNMAPPED BUTTON $j"
+ }
+ font.draw(batch, mapping, x, y, 256f, Align.left, false)
+ }
+ }
+
+ for (j in 0..31) {
+ if (c.getAxis(j) != 0f) {
+ y -= 8f
+ val mapping: String = when (j) {
+ m.L_STICK_HORIZONTAL_AXIS -> "L_STICK_HORIZONTAL_AXIS"
+ m.R_STICK_HORIZONTAL_AXIS -> "R_STICK_HORIZONTAL_AXIS"
+ m.L_STICK_VERTICAL_AXIS -> "L_STICK_VERTICAL_AXIS"
+ m.R_STICK_VERTICAL_AXIS -> "R_STICK_VERTICAL_AXIS"
+ m.L_TRIGGER_AXIS -> "L_TRIGGER_AXIS"
+ m.R_TRIGGER_AXIS -> "R_TRIGGER_AXIS"
+ else -> "UNMAPPED AXIS $j"
+ }
+ font.draw(batch, "$mapping: ${c.getAxis(j)} ", x, y, 256f, Align.left, false)
+ }
+ }
+ for (j in 0..31) {
+ if (c.getPov(j) != PovDirection.center) {
+ y -= 8f
+ val mapping: String = when (j) {
+ m.DPAD -> "DPAD"
+ else -> "UNKNOWN DPAD $j"
+ }
+ font.draw(batch, "$mapping ${c.getPov(j)}", x, y, 256f, Align.left, false)
+ }
+ }
+ try {
+
+ for (j in 0..31) {
+ y -= 8f
+ font.draw(batch, "Accel$j: ${c.getAccelerometer(j)}", x, y, 256f, Align.left, false)
+ }
+ } catch (e: GdxRuntimeException) {
+ }
+
+ for (j in 0..31) {
+ if (c.getSliderX(j)) {
+ y -= 8f
+ font.draw(batch, "XSlider$j: ${c.getSliderX(j)}", x, y, 256f, Align.left, false)
+ }
+ }
+ for (j in 0..31) {
+ if (c.getSliderY(j)) {
+ y -= 8f
+ font.draw(batch, "YSlider$j: ${c.getSliderY(j)}", x, y, 256f, Align.left, false)
+ }
+ }
+ x += 256f
+ }
+
+ // batch.end()
+ }
+
+ // These methods must be implemented but don't have to do anything
+ override fun show() {
+ }
+
+ override fun hide() {}
+
+ override fun dispose() {}
+}
diff --git a/src/uk/me/fantastic/retro/FBORenderer.kt b/src/uk/me/fantastic/retro/FBORenderer.kt
new file mode 100644
index 0000000..187237d
--- /dev/null
+++ b/src/uk/me/fantastic/retro/FBORenderer.kt
@@ -0,0 +1,203 @@
+package uk.me.fantastic.retro
+
+import com.badlogic.gdx.Gdx
+import com.badlogic.gdx.graphics.Camera
+import com.badlogic.gdx.graphics.Color
+import com.badlogic.gdx.graphics.GL20
+import com.badlogic.gdx.graphics.OrthographicCamera
+import com.badlogic.gdx.graphics.g2d.Batch
+import com.badlogic.gdx.graphics.g2d.GlyphLayout
+import com.badlogic.gdx.graphics.g2d.SpriteBatch
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType.Filled
+import com.badlogic.gdx.math.MathUtils
+import com.badlogic.gdx.math.Vector3
+import uk.me.fantastic.retro.Prefs.BinPref.FPS
+
+/**
+ * Renders sprites to a FrameBufferObject and thence to the screen
+ * This is the version where I attempt to add new features and probably fuck up how it works
+ * Does not support bilinear filtering when smooth motion is enabled
+ * Creates new objects every frame, not sure how heavy they are or if they could better be pooled and reused
+ */
+class FBORenderer(val WIDTH: Float, val HEIGHT: Float, val fadeInEffect: Boolean) {
+
+ var cam: OrthographicCamera = setupCam(Gdx.graphics.width.toFloat(), Gdx.graphics.height.toFloat())
+ internal var shape = ShapeRenderer(5000, createDefaultShapeShader())
+ internal var batch = SpriteBatch(1000, createDefaultShader())
+
+ internal var scaleFactor = 1f
+
+ var timer = 0.0f
+
+ internal var fboBatch = SpriteBatch(100, createDefaultShader())
+ internal var glyphLayout = GlyphLayout()
+
+ private val mFBO = ManagedFBO()
+
+ fun renderFBOtoScreen() {
+ endFBO()
+
+ cam.update()
+ fboBatch.projectionMatrix = cam.combined
+
+ Gdx.gl.glClearColor(0f, 0f, 0.2f, 1f)
+ Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)
+
+ Prefs.BinPref.BILINEAR.filter(mFBO.texture)
+
+ fboBatch.begin()
+
+ mFBO.updateShader(fboBatch)
+
+ fboBatch.draw(
+ mFBO.texture,
+ 0f, 0f,
+ 0f, 0f,
+ // mFBO.width, mFBO.height,
+ WIDTH, HEIGHT,
+ 1f, 1f,
+ 0f,
+ 0, 0,
+ mFBO.width.toInt(), mFBO.height.toInt(),
+ // WIDTH.toInt(), HEIGHT.toInt(),
+ false, true
+ )
+
+ fboBatch.end()
+
+ drawScanlines(shape, cam)
+ }
+
+ fun beginFBO(): Batch {
+ timer += Gdx.graphics.deltaTime
+
+ if (fadeInEffect && timer < 3f) {
+ scaleFactor = 4f * (-MathUtils.log2(timer) + 1.5f)
+ if (scaleFactor < 1f) scaleFactor = 1f
+ } else {
+ scaleFactor = 1f
+ }
+
+ if (Prefs.BinPref.SMOOTH.isEnabled()) {
+ mFBO.resizeToScreenSize(WIDTH, HEIGHT)
+ } else {
+ mFBO.resize(WIDTH, HEIGHT, scaleFactor)
+ }
+ mFBO.begin()
+
+ batch.projectionMatrix = mFBO.projectionMatrix
+
+ return batch
+ }
+
+ fun getShape(): ShapeRenderer {
+ shape.projectionMatrix = mFBO.projectionMatrix
+ return shape
+ }
+
+ private fun endFBO() {
+ batch.begin()
+ if (FPS.isEnabled()) {
+ Resources.FONT_ENGLISH.setColor(Color.WHITE)
+ glyphLayout.setText(Resources.FONT_ENGLISH, "FPS ${Gdx.graphics.framesPerSecond}")
+ Resources.FONT_ENGLISH.draw(batch, glyphLayout, WIDTH / 2 - glyphLayout.width / 2, HEIGHT + 1)
+ }
+
+ batch.end()
+
+ mFBO.end()
+ }
+
+ fun resize(width: Int, height: Int) {
+ log("FBOrenderer resize")
+ // mFBO.resizeToScreenSize(WIDTH, HEIGHT, scaleFactor, m)
+
+ cam = setupCam(width.toFloat(), height.toFloat())
+ }
+
+ // fixme messes up batch somehow
+ fun darkenScreen(c: Color) {
+ // batch.end()
+
+ shape.projectionMatrix = mFBO.projectionMatrix
+
+ Gdx.gl.glEnable(GL20.GL_BLEND)
+ Gdx.gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA)
+
+ shape.begin(Filled)
+ shape.color = c
+
+ shape.rect(0f, 0f, WIDTH, HEIGHT)
+
+ shape.end()
+
+ Gdx.gl.glDisable(GL20.GL_BLEND)
+
+ // batch.begin()
+ }
+
+ var scaledWidth = 0f
+ var scaledHeight = 0f
+ var m = 0f
+
+ fun setupCam(x: Float, h: Float): OrthographicCamera {
+ val w = x
+ // val m: Float
+ m = findAppropriateScaleFactor(w, h)
+ scaledWidth = WIDTH * m
+ scaledHeight = HEIGHT * m
+ log("setupcam $scaledWidth $scaledHeight $m")
+ val cam = OrthographicCamera((w) / m, h / m)
+ cam.translate((WIDTH / 2), (HEIGHT / 2))
+ cam.update()
+ return cam
+ }
+
+ fun findAppropriateScaleFactor(w: Float, h: Float): Float =
+ if (Prefs.BinPref.STRETCH.isEnabled()) findHighestScaleFactor(w, h)
+ else findHighestIntegerScaleFactor(w, h)
+
+ fun findHighestIntegerScaleFactor(width: Float, height: Float): Float {
+ val w = width / WIDTH
+ val h = height / HEIGHT
+ return if (w < h) w.roundDown() else h.roundDown()
+ }
+
+ fun findHighestScaleFactor(width: Float, height: Float): Float {
+ val w = width / WIDTH
+ val h = height / HEIGHT
+ return if (w < h) w else h
+ }
+
+ fun drawScanlines(shape: ShapeRenderer, cam: Camera) {
+ if (Prefs.BinPref.SCANLINES.isEnabled()) {
+ shape.projectionMatrix = cam.combined
+
+ Gdx.gl.glEnable(GL20.GL_BLEND)
+ Gdx.gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA)
+
+ shape.begin(ShapeRenderer.ShapeType.Line)
+ shape.setColor(0.0f, 0.0f, 0.0f, 0.5f)
+
+ for (i in 0..HEIGHT.toInt()) {
+ val y = i.toFloat()
+ shape.line(0f, y, WIDTH, y)
+ }
+ shape.end()
+
+ Gdx.gl.glDisable(GL20.GL_BLEND)
+ }
+ }
+
+ fun convertGameCoordsToScreenCoords(x: Float, y: Float): Vector3 {
+ return cam.project(Vector3(x, y, 0f))
+ }
+
+ fun convertScreenToGameCoords(x: Int, y: Int): Vector3 {
+ val g = cam.unproject(Vector3(x.toFloat(), y.toFloat(), 0f))
+ // log("convertcoords","${g.x} ${g.y}")
+ // g.y = Renderer.HEIGHT-g.y
+ return g
+ }
+}
diff --git a/src/uk/me/fantastic/retro/Game.kt b/src/uk/me/fantastic/retro/Game.kt
new file mode 100644
index 0000000..8432e5c
--- /dev/null
+++ b/src/uk/me/fantastic/retro/Game.kt
@@ -0,0 +1,41 @@
+package uk.me.fantastic.retro
+
+import com.badlogic.gdx.Gdx
+import com.badlogic.gdx.math.MathUtils
+import uk.me.fantastic.retro.screens.GameSession
+import uk.me.fantastic.retro.utils.Vec
+
+/**
+ * top the hierarchy, most abstract kind of Game we support. if you implement this you will do most everything youself.
+ * most games probably want to implement SimpleGame subclass instead to get menus.
+ */
+abstract class Game(val session: GameSession) {
+
+ interface UsesMouseAsInputDevice {
+ fun getMouse(): Vec
+ }
+
+ open val MAX_FPS = 1000f
+ open val MIN_FPS = 10f
+
+ val players: ArrayList
+ get() = session.players
+
+ open fun gameover() {
+ session.quit()
+ }
+
+ abstract fun show()
+ abstract fun hide()
+
+ abstract fun resize(width: Int, height: Int)
+ abstract fun render(deltaTime: Float)
+ abstract fun postMessage(s: String)
+
+ abstract val renderer: FBORenderer
+ abstract fun dispose()
+ fun renderAndClampFramerate() {
+ val delta = MathUtils.clamp(Gdx.graphics.rawDeltaTime, 1f / MAX_FPS, 1f / MIN_FPS)
+ render(delta)
+ }
+}
\ No newline at end of file
diff --git a/src/uk/me/fantastic/retro/GameFactory.kt b/src/uk/me/fantastic/retro/GameFactory.kt
new file mode 100644
index 0000000..3c0269c
--- /dev/null
+++ b/src/uk/me/fantastic/retro/GameFactory.kt
@@ -0,0 +1,29 @@
+package uk.me.fantastic.retro
+
+import com.badlogic.gdx.Gdx
+import com.badlogic.gdx.graphics.Texture
+import uk.me.fantastic.retro.screens.GameSession
+
+/**
+ * produces Games. you may want to subclass this for your own Game, but you may be able
+ * use it as-is by passing in your own constructor method
+ */
+open class GameFactory(name: String, val createGame: (GameSession) -> Game, val i: Texture? = null) : AbstractGameFactory(name) {
+
+ val default = Texture(Gdx.files.internal("badlogic.jpg"))
+
+ override val description: String = name
+
+ override val image: Texture
+ get() {
+ if (i != null) {
+ return i
+ } else {
+ return default
+ }
+ }
+
+ override fun create(session: GameSession): Game {
+ return createGame(session)
+ }
+}
diff --git a/src/uk/me/fantastic/retro/ManagedFBO.kt b/src/uk/me/fantastic/retro/ManagedFBO.kt
new file mode 100644
index 0000000..e24460e
--- /dev/null
+++ b/src/uk/me/fantastic/retro/ManagedFBO.kt
@@ -0,0 +1,64 @@
+package uk.me.fantastic.retro
+
+import com.badlogic.gdx.Gdx
+import com.badlogic.gdx.graphics.OrthographicCamera
+import com.badlogic.gdx.graphics.Pixmap
+import com.badlogic.gdx.graphics.g2d.SpriteBatch
+import com.badlogic.gdx.graphics.glutils.FrameBuffer
+import com.badlogic.gdx.math.Vector2
+import uk.me.fantastic.retro.App.Companion.app
+
+class ManagedFBO {
+ val MAX_WIDTH = Gdx.graphics.displayMode.width
+ val MAX_HEIGHT = Gdx.graphics.displayMode.height
+ private var fbo: FrameBuffer = FrameBuffer(Pixmap.Format.RGB888, MAX_WIDTH, MAX_HEIGHT, false)
+ internal var fboCam: OrthographicCamera = OrthographicCamera()
+
+ val texture
+ get() = fbo.colorBufferTexture
+
+ var width: Float = 0f
+ var height: Float = 0f
+
+ val projectionMatrix
+ get() = fboCam.combined
+
+ fun resize(w: Float, h: Float, scale: Float) {
+ width = Math.max(w / scale, 1f)
+ height = Math.max(h / scale, 1f)
+
+ val camWidth = MAX_WIDTH * scale // renderer.WIDTH
+ val camHeight = MAX_HEIGHT * scale // renderer.HEIGHT
+ fboCam.setToOrtho(false, camWidth, camHeight)
+
+ fboCam.position.set(camWidth / 2f, camHeight / 2f, 0f)
+ fboCam.update()
+ }
+
+ fun resizeToScreenSize(w: Float, h: Float) {
+ width = Gdx.graphics.width.toFloat()
+ height = Gdx.graphics.height.toFloat()
+
+ val camWidth = MAX_WIDTH.toFloat() / (width / w)
+ val camHeight = MAX_HEIGHT.toFloat() / (height / h)
+
+ fboCam.setToOrtho(false, camWidth, camHeight)
+ // fboCam.position.set((camWidth / 2f).roundToInt().toFloat(), camHeight / 2f, 0f)
+ fboCam.update()
+ }
+
+ fun begin() {
+ fbo.begin()
+ }
+
+ fun end() {
+ fbo.end()
+ }
+
+ fun updateShader(fboBatch: SpriteBatch) {
+ val outVec = Vector2(Gdx.graphics.width.toFloat(), Gdx.graphics.height.toFloat())
+ val inVec = Vector2(width, height)
+ val textureSize = Vector2(Gdx.graphics.displayMode.width.toFloat(), Gdx.graphics.displayMode.height.toFloat())
+ app.shader.process(fboBatch, textureSize, inVec, outVec)
+ }
+}
\ No newline at end of file
diff --git a/src/uk/me/fantastic/retro/Player.kt b/src/uk/me/fantastic/retro/Player.kt
new file mode 100644
index 0000000..4d87d89
--- /dev/null
+++ b/src/uk/me/fantastic/retro/Player.kt
@@ -0,0 +1,63 @@
+package uk.me.fantastic.retro
+
+import com.badlogic.gdx.graphics.Color
+import com.badlogic.gdx.math.MathUtils
+import uk.me.fantastic.retro.input.InputDevice
+
+/**
+ *
+ */
+open class Player(
+ @Transient val input: InputDevice,
+ val name: String,
+ val color: Color,
+ val color2:
+ Color
+) :
+ Comparable {
+
+ override fun compareTo(other: Player): Int {
+ return score.compareTo(other.score)
+ }
+
+ var entityId: Int = -1 // might not be used by most games but convenient for those that use it to have it here
+
+ var score: Int = 0
+ var deaths: Int = 0
+ var healthLost: Float = 0f
+
+ var metaScore: Int = 0
+ private set(value) {field = value}
+
+ fun incMetaScore(i: Int=1){
+ metaScore+=i
+ }
+
+ var startingHealth: Float = 0f
+
+ fun healthDisplayString(startingHealth: Float): String {
+ val healthLeft = MathUtils.clamp((startingHealth - healthLost).toInt(), 0, 10)
+ val health = "#".repeat(healthLeft)
+ return health
+ }
+
+ fun livesDisplayString(startingLives: Int): String {
+ val livesLeft = MathUtils.clamp(startingLives - deaths, 0, 10)
+
+ val lives = "*".repeat(livesLeft)
+
+ return lives
+ }
+
+ fun isOutOfLives(lives: Int): Boolean {
+ return deaths >= lives
+ }
+
+ fun reset() {
+ score = 0
+ deaths = 0
+ healthLost = 0f
+ }
+
+ // constructor() : this(null, "", -1, Color.WHITE)
+}
diff --git a/src/uk/me/fantastic/retro/Prefs.kt b/src/uk/me/fantastic/retro/Prefs.kt
new file mode 100644
index 0000000..3878fa0
--- /dev/null
+++ b/src/uk/me/fantastic/retro/Prefs.kt
@@ -0,0 +1,357 @@
+//
+/*
+ Copyright 2018 Richard Smith.
+
+ RetroWar is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ RetroWar is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with RetroWar. If not, see .
+*/
+//
+package uk.me.fantastic.retro
+
+import com.badlogic.gdx.Gdx
+import com.badlogic.gdx.Preferences
+import com.badlogic.gdx.graphics.Color
+import com.badlogic.gdx.graphics.Texture
+import com.badlogic.gdx.graphics.Texture.TextureFilter.Linear
+import com.badlogic.gdx.graphics.Texture.TextureFilter.Nearest
+import com.badlogic.gdx.graphics.g2d.TextureRegion
+import com.badlogic.gdx.math.MathUtils
+import uk.me.fantastic.retro.App.Companion.app
+
+import uk.me.fantastic.retro.Resources.Companion.TEXT
+
+/**
+ * Stores stuff in GDX preference files, but also provides singleton/enums for use in creating option Menus
+ */
+object Prefs {
+
+ private var name = System.getProperty("sun.java.command")?.substringBefore(' ')
+ private var fileName = name ?: "uk.me.fantastic.retro"
+
+ var prefs: Preferences = Gdx.app.getPreferences(fileName)
+
+ val shaders = makeShaderList()
+
+ val colors = listOf(
+ Color(0, 0, 0), // 0: black
+ Color(157, 157, 157), // 1: grey
+ Color(255, 255, 255), // 2: white
+ Color(190, 38, 51), // 3: red
+ Color(224, 111, 139), // 4: pink
+ Color(73, 60, 43), // 5: dbrown
+ Color(164, 100, 34), // 6: lbrown
+ Color(235, 137, 49), // 7: orange
+ Color(247, 226, 107), // 8: yellow
+ Color(47, 72, 78), // 9: unknown
+ Color(68, 137, 26), // 10: dgreen
+ Color(163, 206, 39), // 11: lgreen
+ Color(27, 38, 50), // 12: ddblue
+ Color(0, 87, 132), // 13: dblue
+ Color(49, 162, 242), // 14: blue
+ Color(178, 220, 239) // 15: lblue
+ ).map { it.toString() }
+
+ private fun makeShaderList(): List {
+ val shaderFiles = Gdx.files.internal("shaders").list("glsl").map { it.name() }
+ return listOf("NONE") + shaderFiles
+ }
+
+ /** @suppress */
+ enum class MultiChoicePref(val pref: String, vararg val choices: String, val default: Int = 0) {
+ GRAPHICS("graphics", "retro", "modern", "CRT") {
+ override fun apply() {
+ when (getNum()) {
+ 0 -> {
+ BinPref.SMOOTH.disable()
+ BinPref.BILINEAR.enable()
+ BinPref.SCANLINES.enable()
+ SHADER.set(0)
+ app.initialiseShader()
+ }
+ 1 -> {
+ BinPref.SMOOTH.enable()
+ BinPref.BILINEAR.disable()
+ BinPref.SCANLINES.disable()
+ SHADER.set(0)
+ app.initialiseShader()
+ }
+ else -> {
+ BinPref.SMOOTH.disable()
+ BinPref.BILINEAR.disable()
+ BinPref.SCANLINES.disable()
+ SHADER.set(1)
+ app.initialiseShader()
+ }
+ }
+ }
+ },
+
+ SHADER("shader", *shaders.toTypedArray()) {
+ override fun apply() {
+ app.initialiseShader()
+ BinPref.BILINEAR.disable()
+ BinPref.SMOOTH.disable()
+ BinPref.SCANLINES.disable()
+ BinPref.STRETCH.enable()
+ }
+ },
+ PLAYER1_COLOR("player1_color", *(colors.map { it.toString() }.toTypedArray()), default = 9),
+ PLAYER1_COLOR2("player1_color2", *(colors.map { it.toString() }.toTypedArray()), default = 15),
+ PLAYER2_COLOR("player2_color", *(colors.map { it.toString() }.toTypedArray()), default = 3),
+ PLAYER2_COLOR2("player2_color2", *(colors.map { it.toString() }.toTypedArray()), default = 4),
+ PLAYER3_COLOR("player3_color", *(colors.map { it.toString() }.toTypedArray()), default = 13),
+ PLAYER3_COLOR2("player3_color2", *(colors.map { it.toString() }.toTypedArray()), default = 14),
+ PLAYER4_COLOR("player4_color", *(colors.map { it.toString() }.toTypedArray()), default = 10),
+ PLAYER4_COLOR2("player4_color2", *(colors.map { it.toString() }.toTypedArray()), default = 11),
+ PLAYERGUEST_COLOR("playerguest_color", *(colors.map { it.toString() }.toTypedArray()), default = 5),
+ PLAYERGUEST_COLOR2("playerguest_color2", *(colors.map { it.toString() }.toTypedArray()), default = 7),
+ LIMIT_FPS("limitfps", "0", "30", "60") {
+ override fun apply() {
+ App.app.setFPS(getString().toInt())
+ }
+ };
+
+ fun next() {
+ val n = getNum() + 1
+
+ if (n > choices.lastIndex) {
+ set(0)
+ } else {
+ set(n)
+ }
+ }
+
+ fun prev() {
+ val n = getNum() - 1
+ if (n < 0) {
+ set(choices.lastIndex)
+ } else {
+ set(n)
+ }
+ }
+
+ fun set(n: Int) {
+ prefs.putInteger(pref, MathUtils.clamp(n, 0, choices.lastIndex))
+ prefs.flush()
+ apply()
+ }
+
+ fun displayText(): String {
+ return choices[prefs.getInteger(pref, default)]
+ }
+
+ fun getString(): String {
+ return choices[prefs.getInteger(pref, default)]
+ }
+
+ fun getNum(): Int {
+ return prefs.getInteger(pref, default)
+ }
+
+ fun reset() {
+ set(default)
+ }
+
+ open fun apply() {}
+ }
+
+ /** @suppress */
+ enum class BinPref(
+ val pref: String,
+ val text: String = pref,
+ val tText: String = TEXT["on"],
+ val fText:
+ String = TEXT["off"],
+ val default: Boolean = true
+ ) {
+ VSYNC("vsync") {
+ override fun apply() {
+ Gdx.graphics.setVSync(VSYNC.isEnabled())
+ }
+ },
+ STRETCH("stretch", tText = TEXT["stretched"], fText = TEXT["pixelPerfect"], default = true) {
+ override fun apply() {
+ App.app.resize(Gdx.graphics.width, Gdx.graphics.height)
+ }
+ },
+ SCANLINES("scanlines", default = true),
+ SMOOTH("smooth", tText = "FAKE but SMOOTH", fText = "GENUINE", default = false),
+ SPLASH("splash", default = true),
+ // PROFANITY("profanity", default = false) {
+// override fun apply() {
+// var locale = Locale(Resources.defaultLocale.language, "", if (PROFANITY.isEnabled()) "profane" else "")
+// Resources.TEXT = I18NBundle.createBundle(Resources.baseFileHandle, locale)
+// }
+// },
+ BILINEAR("bilinear", tText = TEXT["on"], fText = TEXT["off"], default = true) {
+ override fun apply() {}
+ },
+ DEBUG("debug", tText = TEXT["on"], fText = TEXT["off"], default = false) {
+ override fun apply() {}
+ },
+ CRASH_REPORTS("crashreports", tText = TEXT["on"], fText = TEXT["off"], default = true) {
+ override fun apply() {}
+ },
+ AUTOFIRE("autofire", tText = TEXT["on"], fText = TEXT["off"], default = false) {
+ override fun apply() {}
+ },
+ MUSIC("music", tText = TEXT["on"], fText = TEXT["off"]) {
+ override fun apply() {
+ }
+ },
+ FPS("fps", default = false) {
+ override fun apply() {}
+ },
+ FULLSCREEN("fullscreen", tText = TEXT["fullscreen"], fText = TEXT["windowed"]) {
+ override fun apply() {
+ if (!isWindows) {
+ app.setScreenMode()
+ }
+ }
+ };
+
+ fun displayText(): String {
+ if (isEnabled()) return tText
+ else return fText
+ }
+
+ fun enable() {
+ log("enabled " + this)
+ prefs.putBoolean(pref, true)
+ prefs.flush()
+ apply()
+ }
+
+ fun disable() {
+ log("disabled " + this)
+ prefs.putBoolean(pref, false)
+ prefs.flush()
+ apply()
+ }
+
+ fun toggle() {
+ log("toggled " + this)
+ prefs.putBoolean(pref, !isEnabled())
+ prefs.flush()
+ apply()
+ }
+
+ fun isEnabled(): Boolean {
+ return prefs.getBoolean(pref, default)
+ }
+
+ open fun apply() {}
+ fun filter(img: TextureRegion) {
+ filter(img.texture)
+ }
+
+ fun filter(tex: Texture) {
+ if (BILINEAR.isEnabled())
+ tex.setFilter(Linear, Linear)
+ else tex.setFilter(Nearest, Nearest)
+ }
+ }
+
+ /** @suppress */
+ enum class NumPref(val pref: String, val text: String = pref, val min: Int = 0, val max: Int = 0, val default: Int = 50, val step: Int = 1) {
+ SCREEN_SHAKE("screenshake", min = 0, max = 100, default = 30, step = 10),
+ SHIP_SPEED("shipspeed", min = 100, max = 500, default = 180, step = 10),
+ SHIP_ACC("shipacc", min = 100, max = 1000, default = 300, step = 10),
+ BULLET_SPEED("bulletspeed", min = 100, max = 1000, default = 300, step = 10),
+ BULLET_RATE("bulletrate", min = 1, max = 100, default = 20),
+ SHIP_HEALTH("shiphealth", min = 1, max = 20, default = 10, step = 1),
+ SHIP_KNOCKBACK("shipgnockback", min = 0, max = 40, default = 10, step = 1),
+ FX_VOLUME("fxvolume", min = 0, max = 10, default = 10, step = 1),
+ MUSIC_VOLUME("musicvolume", min = 0, max = 10, default = 10, step = 1),
+
+ BUFFER("buffer", min = 0, max = 20, default = 6, step = 1);
+
+ fun displayText(): String {
+ return "${prefs.getInteger(pref, default)}"
+ }
+
+ fun asVolume(): Float {
+ val a = getNum().toFloat() / 10f
+ return a * a * a
+ }
+
+ fun asPercent(): Float {
+ return getNum().toFloat() / 100f
+ }
+
+ fun getNum(): Int {
+ return prefs.getInteger(pref, default)
+ }
+
+ fun increase() {
+ var i = prefs.getInteger(pref, default)
+ if (i < max) {
+ i += step
+
+ prefs.putInteger(pref, i)
+ prefs.flush()
+ log("pref $name set to $i")
+ }
+ apply()
+ }
+
+ fun decreass() {
+ var i = prefs.getInteger(pref, default)
+ if (i > min) {
+ i -= step
+ prefs.putInteger(pref, i)
+ prefs.flush()
+ log("pref $name set to $i")
+ }
+ apply()
+ }
+
+ open fun apply() {}
+ }
+
+ /** @suppress */
+ enum class StringPref(val pref: String, val text: String = pref, val default: String = "") {
+ PLAYER1("player1", default = "PLAYER1"),
+ PLAYER2("player2", default = "PLAYER2"),
+ PLAYER3("player3", default = "PLAYER3"),
+ PLAYER4("player4", default = "PLAYER4"),
+ SERVER("server", default = "1.1.1.1"),
+ GAME("game", default = "RETDROID"),
+ PLAYER_GUEST("playerguest", default = "GUEST");
+
+ fun displayText(): String {
+ return prefs.getString(pref, default)
+ }
+
+ fun getString(): String {
+ return prefs.getString(pref, default)
+ }
+
+ fun setString(s: String) {
+ prefs.putString(pref, s)
+ prefs.flush()
+ }
+
+ fun appendChar(c: Char) {
+ setString(getString() + c)
+ }
+
+ fun deleteChar() {
+ setString(getString().dropLast(1))
+ }
+
+ fun reset() {
+ setString(default)
+ }
+ }
+}
diff --git a/src/uk/me/fantastic/retro/Resources.kt b/src/uk/me/fantastic/retro/Resources.kt
new file mode 100644
index 0000000..8a05fad
--- /dev/null
+++ b/src/uk/me/fantastic/retro/Resources.kt
@@ -0,0 +1,53 @@
+//
+/*
+ Copyright 2018 Richard Smith.
+
+ RetroWar is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ RetroWar is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with RetroWar. If not, see .
+*/
+//
+package uk.me.fantastic.retro
+
+import com.badlogic.gdx.Gdx
+import com.badlogic.gdx.graphics.Texture
+import com.badlogic.gdx.graphics.g2d.BitmapFont
+import com.badlogic.gdx.graphics.g2d.TextureRegion
+import com.badlogic.gdx.utils.I18NBundle
+
+class Resources {
+
+ companion object {
+
+ var baseFileHandle = Gdx.files.internal("i18n/RetroWar")
+ var defaultLocale = java.util.Locale.getDefault()
+
+ // var locale = Locale.Builder().setLocale(defaultLocale).setVariant("PROFANE").build()
+
+ var TEXT = I18NBundle.createBundle(baseFileHandle)
+
+ val MISSING_TEXTURE = Texture("badlogic.jpg")
+ val MISSING_TEXTUREREGION = TextureRegion(MISSING_TEXTURE)
+
+ private fun TextureRegion(s: String): TextureRegion = TextureRegion(Texture(s))
+
+// val FONT = BitmapFont(Gdx.files.internal("c64_low3_black.fnt"))
+// val FONT_CLEAR = BitmapFont(Gdx.files.internal("c64_low3.fnt"))
+
+ val FONT = BitmapFont(Gdx.files.internal(TEXT["fontBlack"]))
+ val FONT_CLEAR = BitmapFont(Gdx.files.internal(TEXT["font"]))
+
+ val FONT_ENGLISH = BitmapFont(Gdx.files.internal("english.fnt"))
+
+ val BLING = Gdx.audio.newSound(Gdx.files.internal("powerup.wav"))!!
+ }
+}
diff --git a/src/uk/me/fantastic/retro/SimpleApp.kt b/src/uk/me/fantastic/retro/SimpleApp.kt
new file mode 100644
index 0000000..08f44c7
--- /dev/null
+++ b/src/uk/me/fantastic/retro/SimpleApp.kt
@@ -0,0 +1,47 @@
+package uk.me.fantastic.retro
+
+import com.badlogic.gdx.Gdx
+import uk.me.fantastic.retro.screens.SimpleTitleScreen
+
+class SimpleApp(callback: Callback, val name: String, val factory: AbstractGameFactory, logger: Logger, manualGC:
+ManualGC? = null, val advertise: Boolean = false) : App
+(callback, logger, manualGC) {
+
+ override fun quit() {
+ log("App", "Quit")
+ Gdx.app.exit()
+ }
+
+ override fun create() {
+
+ log("SimpleApp from $factory create")
+ setScreenMode()
+
+ initialiseAndroid()
+ initialiseDesktop()
+ setPrefsToDefaultsForSingleGames()
+ initialisePrefs()
+ initializeInput()
+ initialiseControllers()
+ initialiseShader()
+
+ if(advertise){
+ title = SimpleTitleScreen(title = name, factory = factory, quitText = "More RetroWar", quitURL =
+ "https://store.steampowered.com/app/664240/)")
+ }else {
+ title = SimpleTitleScreen(title = name, factory = factory)
+ }
+ setScreen(title)
+ }
+
+ fun setPrefsToDefaultsForSingleGames() {
+ Prefs.BinPref.FULLSCREEN.enable()
+ Prefs.BinPref.VSYNC.enable()
+ Prefs.MultiChoicePref.LIMIT_FPS.set(0)
+// BinPrefMenuItem("motion ", BinPref.SMOOTH),
+// BinPrefMenuItem("pixels ", BinPref.BILINEAR),
+// BinPrefMenuItem("scaling ", BinPref.STRETCH),
+// BinPrefMenuItem("scanlines ", BinPref.SCANLINES),
+ Prefs.BinPref.FPS.disable()
+ }
+}
diff --git a/src/uk/me/fantastic/retro/SimpleGame.kt b/src/uk/me/fantastic/retro/SimpleGame.kt
new file mode 100644
index 0000000..ab7b71f
--- /dev/null
+++ b/src/uk/me/fantastic/retro/SimpleGame.kt
@@ -0,0 +1,78 @@
+package uk.me.fantastic.retro
+
+import com.badlogic.gdx.Gdx
+import com.badlogic.gdx.graphics.GL20
+import com.badlogic.gdx.graphics.g2d.Batch
+import com.badlogic.gdx.graphics.g2d.BitmapFont
+import uk.me.fantastic.retro.menu.MenuController
+import uk.me.fantastic.retro.screens.GameSession
+
+/**
+ * Most games probably want to extend this. It does menus and rendering loop, but it's not a Unigame so you're still
+ * free to do any sort of game you want really.
+ */
+abstract class SimpleGame(
+ session: GameSession,
+ val width: Float,
+ val height: Float,
+ val fontClear: BitmapFont =
+ Resources.FONT_CLEAR,
+ val font: BitmapFont = Resources.FONT,
+ val fadeInEffect: Boolean = true
+) : Game(session) {
+
+ override val renderer = FBORenderer(WIDTH = width, HEIGHT = height, fadeInEffect = fadeInEffect)
+
+ private val controller = MenuController(session.standardMenu(), width, height, x = 0f, y = height -
+ 4)
+
+ init {
+ font.data.markupEnabled = true
+ fontClear.data.markupEnabled = true
+ }
+
+ // render is called by libgdx once every frame (required)
+ override fun render(deltaTime: Float) {
+
+ doLogic(deltaTime)
+
+ val batch = renderer.beginFBO()
+ Gdx.gl.glClearColor(0f, 0f, 0f, 1f)
+ Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)
+ batch.begin()
+ doDrawing(batch)
+
+ batch.end()
+
+ if (session.state == GameSession.GameState.MENU) {
+ batch.begin()
+ drawMenu(batch)
+ batch.end()
+ }
+
+ renderer.renderFBOtoScreen()
+ }
+
+ fun simpleHighScoreTable(): String = players.sortedDescending().joinToString("") {
+ "\n\n${it.name} ${it.score}"
+ }
+
+ abstract fun doDrawing(batch: Batch)
+
+ abstract fun doLogic(deltaTime: Float)
+
+ private fun drawMenu(batch: Batch) {
+ controller.doInput()
+ val mouse = renderer.convertScreenToGameCoords(Gdx.input.x, Gdx.input.y)
+ controller.doMouseInput(mouse.x, mouse.y)
+ controller.draw(batch)
+ }
+
+ override fun resize(width: Int, height: Int) {
+ log("retrogame resize " + toString())
+ renderer.resize(width, height)
+ }
+
+ override fun postMessage(s: String) {
+ }
+}
\ No newline at end of file
diff --git a/src/uk/me/fantastic/retro/SimpleGameFactory.kt b/src/uk/me/fantastic/retro/SimpleGameFactory.kt
new file mode 100644
index 0000000..f394168
--- /dev/null
+++ b/src/uk/me/fantastic/retro/SimpleGameFactory.kt
@@ -0,0 +1,14 @@
+package uk.me.fantastic.retro
+
+import com.badlogic.gdx.Gdx
+import com.badlogic.gdx.graphics.Texture
+import uk.me.fantastic.retro.screens.GameSession
+
+class SimpleGameFactory(name: String, val gameClazz: Class) : AbstractGameFactory(name = name) {
+
+ override val description = name
+ override val image: Texture by lazy { Texture(Gdx.files.internal("badlogic.jpg")) }
+ override fun create(session: GameSession): Game {
+ return gameClazz.getConstructor(GameSession::class.java).newInstance(session)
+ }
+}
\ No newline at end of file
diff --git a/src/uk/me/fantastic/retro/globals.kt b/src/uk/me/fantastic/retro/globals.kt
new file mode 100644
index 0000000..6d399fa
--- /dev/null
+++ b/src/uk/me/fantastic/retro/globals.kt
@@ -0,0 +1,349 @@
+//
+/*
+ Copyright 2018 Richard Smith.
+
+ RetroWar is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ RetroWar is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with RetroWar. If not, see .
+*/
+//
+package uk.me.fantastic.retro
+
+import com.badlogic.gdx.Application
+import com.badlogic.gdx.Gdx
+import com.badlogic.gdx.graphics.Color
+import com.badlogic.gdx.graphics.GL20
+import com.badlogic.gdx.graphics.OrthographicCamera
+import com.badlogic.gdx.graphics.Pixmap
+import com.badlogic.gdx.graphics.Texture
+import com.badlogic.gdx.graphics.g2d.SpriteBatch
+import com.badlogic.gdx.graphics.g2d.TextureRegion
+import com.badlogic.gdx.graphics.glutils.FrameBuffer
+import com.badlogic.gdx.graphics.glutils.ShaderProgram
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer
+import com.badlogic.gdx.maps.tiled.TiledMap
+import com.badlogic.gdx.maps.tiled.TiledMapTileLayer
+import com.badlogic.gdx.maps.tiled.renderers.OrthogonalTiledMapRenderer
+import com.badlogic.gdx.math.MathUtils
+import com.badlogic.gdx.math.Rectangle
+import uk.me.fantastic.retro.input.InputDevice
+import uk.me.fantastic.retro.input.NetworkInput
+import uk.me.fantastic.retro.network.ClientPlayer
+import uk.me.fantastic.retro.utils.sqrt
+import java.util.ArrayList
+import kotlin.math.roundToInt
+
+/*
+ * Global scope stuff that IntelliJ doesn't like being in the actual files where it is used
+ */
+
+interface Logger {
+ fun log(message: String)
+ fun log(caller: String, message: String)
+ fun error(message: String)
+ fun initialize()
+}
+
+val osName = System.getProperty("os.name")
+val isOSX: Boolean = osName.contains("OS X")
+val isMobile = Gdx.app.type == Application.ApplicationType.Android || Gdx.app.type == Application.ApplicationType.iOS
+val isLinux: Boolean = osName.contains("Linux") && !isMobile
+val isWindows: Boolean = !isLinux && !isOSX && !isMobile
+
+interface ManualGC {
+ fun enable()
+ fun disable()
+ fun doGC()
+}
+
+
+@Suppress("NOTHING_TO_INLINE")
+inline fun log(log: String) {
+ if (Prefs.BinPref.DEBUG.isEnabled()) {
+ App.app.logger.log(log)
+ }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+inline fun log(c: String, log: String) {
+ if (Prefs.BinPref.DEBUG.isEnabled()) {
+ App.app.logger.log(c, log)
+ }
+}
+
+fun error(message: String) {
+ App.app.logger.error(message)
+}
+
+fun drawBox(
+ MARGIN: Int,
+ SHADOW_OFFSET: Int,
+ shape: ShapeRenderer,
+ width: Float,
+ height: Float,
+ y: Float,
+ SCREEN_WIDTH: Float
+) {
+ val box = Rectangle(0f, 0f, width + MARGIN, height + MARGIN)
+ shape.begin(ShapeRenderer.ShapeType.Filled)
+ shape.color = Color.BLACK
+ shape.rect(SCREEN_WIDTH / 2 - box.width / 2 + SHADOW_OFFSET, y - SHADOW_OFFSET - box.height + MARGIN / 2, box.width, box
+ .height)
+ shape.color = Color(0, 87, 132)
+ shape.rect(SCREEN_WIDTH / 2 - box.width / 2, y - box.height + MARGIN / 2, box.width, box.height)
+ shape.end()
+ shape.begin(ShapeRenderer.ShapeType.Line)
+ shape.color = Color.WHITE
+ shape.rect(SCREEN_WIDTH / 2 - box.width / 2, y - box.height + MARGIN / 2, box.width, box.height)
+ shape.end()
+}
+
+fun listAllLevels() = Gdx.files.internal("levels").list().filter { it.extension() == "tmx" }.map { it.name().dropLast(4) }
+
+fun Float.roundDown(): Float {
+ return MathUtils.floor(this).toFloat()
+}
+
+/** @suppress */
+enum class Size {
+ SMALL, MEDIUM, LARGE
+}
+
+/** @suppress */
+class JoinRequest
+
+/** @suppress */
+class WorldUpdate(val buffer: ByteArray?, val id: Int) {
+ constructor() : this(null, 0)
+}
+
+/** @suppress */
+class PlayersUpdate(val players: ArrayList?) {
+ constructor() : this(null)
+}
+
+/** @suppress */
+class CreatePlayerRequest(val player: ClientPlayer?) {
+ constructor() : this(null)
+}
+
+/** @suppress */
+class InputUpdate(input: InputDevice?, val playerId: Int) {
+
+ constructor() : this(null, -1)
+
+ var networkInput: NetworkInput? = null
+
+ init {
+ if (input != null) {
+ networkInput = NetworkInput(input.leftStick, input.rightStick, input.leftTrigger, input.rightTrigger,
+ input.A)
+ }
+ }
+}
+
+/** @suppress */
+class CreatePlayerResponse(val serverPlayerId: Int, val clientPlayerId: Int) {
+ constructor() : this(-1, -1)
+}
+
+fun Pair.normVector(): Pair {
+
+ val vMagnitude = (first * first + second * second).sqrt()
+ return Pair(first / vMagnitude, second / vMagnitude)
+}
+
+interface Callback {
+ fun setForegroundFPS(foregroundFPS: Int)
+ fun setBackgroundFPS(backgroundFPS: Int)
+}
+
+class EmptyCallback : Callback {
+
+ override fun setForegroundFPS(foregroundFPS: Int) {
+ }
+
+ override fun setBackgroundFPS(backgroundFPS: Int) {
+ }
+}
+
+typealias Square = ArrayList
+
+fun Color(r: Int, g: Int, b: Int): Color {
+ return Color(r.toFloat() / 255f, g.toFloat() / 255f, b.toFloat() / 255f, 1.0f)
+}
+
+/*
+FIXME in Java this would be a performance improvement over ArrayList; in Kotlin I don't know how to avoid
+autoboxing when I pull the ints out of the array so this might be what causes lots of Integers to be allocated!
+It might be better just to use Array. There's also IntArray but that lacks a clear()
+*/
+// class MyIntArray : com.badlogic.gdx.utils.IntArray(false, 256), Iterable {
+// override fun iterator(): Iterator {
+// return object : Iterator {
+// var i = 0
+// override fun next(): Int {
+// return items[i++]
+// }
+//
+// override fun hasNext(): Boolean {
+// return i < size
+// }
+//
+// }
+// }
+//
+// }
+
+inline fun matrix2d(height: Int, width: Int, init: (Int, Int) -> Array) = Array(height, { row -> init(row, width) })
+
+fun Float.round(): Float = roundToInt().toFloat()
+
+fun createDefaultShader(): ShaderProgram {
+ val vertexShader = ("in vec4 " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" + //
+ "in vec4 " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" + //
+ "in vec2 " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" + //
+ "uniform mat4 u_projTrans;\n" + //
+ "out vec4 v_color;\n" + //
+ "out vec2 v_texCoords;\n" + //
+ "\n" + //
+ "void main()\n" + //
+ "{\n" + //
+ " v_color = " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" + //
+ " v_color.a = v_color.a * (255.0/254.0);\n" + //
+ " v_texCoords = " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" + //
+ " gl_Position = u_projTrans * " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" + //
+ "}\n")
+ val fragmentShader = ("#ifdef GL_ES\n" + //
+ "#define LOWP lowp\n" + //
+ "precision mediump float;\n" + //
+ "#else\n" + //
+ "#define LOWP \n" + //
+ "#endif\n" + //
+ "in LOWP vec4 v_color;\n" + //
+ "in vec2 v_texCoords;\n" + //
+ "out vec4 fragColor;\n" + //
+ "uniform sampler2D u_texture;\n" + //
+ "void main()\n" + //
+ "{\n" + //
+ " fragColor = v_color * texture(u_texture, v_texCoords);\n" + //
+ "}")
+
+ ShaderProgram.prependFragmentCode = "#version 330\n"
+ ShaderProgram.prependVertexCode = "#version 330\n"
+ val shader = ShaderProgram(vertexShader, fragmentShader)
+ if (shader.isCompiled == false) throw IllegalArgumentException("Error compiling shader: " + shader.log)
+ return shader
+}
+
+private fun createVertexShader(hasNormals: Boolean, hasColors: Boolean, numTexCoords: Int): String {
+ var shader = ("in vec4 " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" +
+ (if (hasNormals) "in vec3 " + ShaderProgram.NORMAL_ATTRIBUTE + ";\n" else "") +
+ if (hasColors) "in vec4 " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" else "")
+
+ for (i in 0 until numTexCoords) {
+ shader += "in vec2 " + ShaderProgram.TEXCOORD_ATTRIBUTE + i + ";\n"
+ }
+
+ shader += "uniform mat4 u_projModelView;\n"
+ shader += if (hasColors) "out vec4 v_col;\n" else ""
+
+ for (i in 0 until numTexCoords) {
+ shader += "out vec2 v_tex$i;\n"
+ }
+
+ shader += ("void main() {\n" + " gl_Position = u_projModelView * " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" +
+ if (hasColors) " v_col = " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" else "")
+
+ for (i in 0 until numTexCoords) {
+ shader += " v_tex" + i + " = " + ShaderProgram.TEXCOORD_ATTRIBUTE + i + ";\n"
+ }
+ shader += " gl_PointSize = 1.0;\n"
+ shader += "}\n"
+ return shader
+}
+
+private fun createFragmentShader(hasColors: Boolean, numTexCoords: Int): String {
+ var shader = "#ifdef GL_ES\n" + "precision mediump float;\n" + "#endif\n"
+
+ if (hasColors) shader += "in vec4 v_col;\n"
+ for (i in 0 until numTexCoords) {
+ shader += "in vec2 v_tex$i;\n"
+ shader += "uniform sampler2D u_sampler$i;\n"
+ }
+ shader += "out vec4 fragColor;\n"
+
+ shader += "void main() {\n" + " fragColor = " + if (hasColors) "v_col" else "vec4(1, 1, 1, 1)"
+
+ if (numTexCoords > 0) shader += " * "
+
+ for (i in 0 until numTexCoords) {
+ if (i == numTexCoords - 1) {
+ shader += " texture(u_sampler$i, v_tex$i)"
+ } else {
+ shader += " texture(u_sampler$i, v_tex$i) *"
+ }
+ }
+
+ shader += ";\n}"
+ return shader
+}
+
+/** Returns a new instance of the default shader used by SpriteBatch for GL2 when no shader is specified. */
+fun createDefaultShapeShader(hasNormals: Boolean = false, hasColors: Boolean = true, numTexCoords: Int = 0):
+ ShaderProgram {
+ val vertexShader = createVertexShader(hasNormals, hasColors, numTexCoords)
+ val fragmentShader = createFragmentShader(hasColors, numTexCoords)
+ ShaderProgram.prependFragmentCode = "#version 330\n"
+ ShaderProgram.prependVertexCode = "#version 330\n"
+ val shader = ShaderProgram(vertexShader, fragmentShader)
+ if (shader.isCompiled == false) throw IllegalArgumentException("Error compiling shader: " + shader.log)
+ return shader
+}
+
+fun renderTileMapToTexture(map: TiledMap): TextureRegion {
+ val tiles = map.layers[0] as TiledMapTileLayer
+ val width = tiles.width * tiles.tileWidth
+ val height = tiles.height * tiles.tileHeight
+ val batch = SpriteBatch(1000, createDefaultShader())
+ val mapRenderer = OrthogonalTiledMapRenderer(map, 1f, SpriteBatch(1000, createDefaultShader()))
+
+ val fbo = FrameBuffer(Pixmap.Format.RGB888, width.toInt(), height.toInt(), false)
+
+ val fboCam = OrthographicCamera(width, height)
+ val fboTexture = TextureRegion(fbo.colorBufferTexture)
+
+ fboTexture.texture.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest)
+
+ fboTexture.flip(false, true) // for some reason y-axis is inverted in framebuffer
+
+ fboCam.position.set(width / 2f, height / 2f, 0f)
+ fboCam.update()
+
+ fbo.begin()
+
+ Gdx.gl.glClearColor(0f, 0f, 0f, 1f)
+ Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)
+
+ batch.projectionMatrix = fboCam.combined
+
+ batch.begin()
+
+ mapRenderer.setView(fboCam)
+ mapRenderer.render()
+
+ batch.end()
+
+ fbo.end()
+
+ return fboTexture
+}
\ No newline at end of file
diff --git a/src/uk/me/fantastic/retro/input/GamepadInput.kt b/src/uk/me/fantastic/retro/input/GamepadInput.kt
new file mode 100644
index 0000000..10593c8
--- /dev/null
+++ b/src/uk/me/fantastic/retro/input/GamepadInput.kt
@@ -0,0 +1,66 @@
+package uk.me.fantastic.retro.input
+
+import uk.me.fantastic.retro.utils.Vec
+
+/**
+ * Created by Richard on 13/08/2016.
+ * Maps a controller to an input
+ */
+internal class GamepadInput(val controller: MappedController) : InputDevice() {
+
+ override val leftStick: Vec
+ get() {
+ val x = controller.LStickHorizontalAxis()
+ val y = controller.LStickVerticalAxis()
+ val (a, b) = filterDeadzone(0.05f, x, y)
+ return Vec(a, b)
+ }
+
+ override val rightStick: Vec
+ get() {
+ val x = controller.RStickHorizontalAxis()
+ val y = controller.RStickVerticalAxis()
+ val (a, b) = filterDeadzone(0.6f, x, y)
+ return Vec(a, b)
+ }
+
+ override val leftTrigger: Float
+ get() {
+ // log("GamepadInput ${controller.leftTrigger()}")
+ return controller.leftTrigger()
+ }
+ override val rightTrigger: Float
+ get() {
+ return controller.rightTrigger()
+ }
+
+ override val A: Boolean
+ get() {
+ return controller.a()
+ }
+
+ override val B: Boolean
+ get() {
+ return controller.b()
+ }
+
+ override val X: Boolean
+ get() {
+ return controller.x()
+ }
+
+ override val Y: Boolean
+ get() {
+ return controller.y()
+ }
+
+ override val leftBumper: Boolean
+ get() {
+ return controller.lBumper()
+ }
+
+ override val rightBumper: Boolean
+ get() {
+ return controller.rBumper()
+ }
+}
diff --git a/src/uk/me/fantastic/retro/input/InputDevice.kt b/src/uk/me/fantastic/retro/input/InputDevice.kt
new file mode 100644
index 0000000..def135a
--- /dev/null
+++ b/src/uk/me/fantastic/retro/input/InputDevice.kt
@@ -0,0 +1,36 @@
+package uk.me.fantastic.retro.input
+
+import uk.me.fantastic.retro.utils.Vec
+
+/**
+ * All input devices are abstracted to look something like the ubiquitious xbox controller
+ */
+abstract class InputDevice {
+
+ abstract val leftStick: Vec
+ abstract val rightStick: Vec
+ abstract val leftTrigger: Float
+ abstract val rightTrigger: Float
+
+ abstract val A: Boolean
+ abstract val B: Boolean
+ abstract val X: Boolean
+ abstract val Y: Boolean
+
+ abstract val leftBumper: Boolean
+ abstract val rightBumper: Boolean
+
+ val fire: Boolean
+ get() {
+ return (A || B || X || Y) || rightBumper
+ }
+
+ var entity: Int = -1
+
+ internal fun filterDeadzone(deadzone: Float, axisX: Float, axisY: Float): Pair {
+ if (axisX < deadzone && axisX > -deadzone && axisY < deadzone && axisY > -deadzone) {
+ return Pair(0f, 0f)
+ }
+ return Pair(axisX, axisY)
+ }
+}
diff --git a/src/uk/me/fantastic/retro/input/KeyboardMouseInput.kt b/src/uk/me/fantastic/retro/input/KeyboardMouseInput.kt
new file mode 100644
index 0000000..94688fc
--- /dev/null
+++ b/src/uk/me/fantastic/retro/input/KeyboardMouseInput.kt
@@ -0,0 +1,115 @@
+package uk.me.fantastic.retro.input
+
+import com.badlogic.gdx.Gdx
+import com.badlogic.gdx.Input
+import uk.me.fantastic.retro.Game
+import uk.me.fantastic.retro.screens.GameSession
+
+import uk.me.fantastic.retro.utils.Vec
+
+/**
+ * Maps Keyboard and mouse to input
+ */
+internal class KeyboardMouseInput(val session: GameSession) : InputDevice() {
+
+ override val leftTrigger: Float
+ get() = 0f
+ override val rightTrigger: Float
+ get() = 0f
+
+ override val leftStick: Vec
+ get() {
+ var x = 0f
+ var y = 0f
+ if (Gdx.input.isKeyPressed(Input.Keys.A) || Gdx.input.isKeyPressed(Input.Keys.NUMPAD_4)) {
+ x = -1f
+ }
+ if (Gdx.input.isKeyPressed(Input.Keys.D) || Gdx.input.isKeyPressed(Input.Keys.NUMPAD_6)) {
+ x = 1f
+ }
+ if (Gdx.input.isKeyPressed(Input.Keys.W) || Gdx.input.isKeyPressed(Input.Keys.NUMPAD_8)) {
+ y = -1f
+ }
+ if (Gdx.input.isKeyPressed(Input.Keys.S) || Gdx.input.isKeyPressed(Input.Keys.NUMPAD_2)) {
+ y = 1f
+ }
+ return Vec(x, y)
+ }
+
+ override val A: Boolean
+ get() {
+ return Gdx.input.isKeyPressed(Input.Keys.SPACE) || Gdx.input.isButtonPressed(0)
+ }
+ override val B: Boolean
+ get() {
+ return Gdx.input.isKeyPressed(Input.Keys.ENTER)
+ }
+ override val X: Boolean
+ get() {
+ return Gdx.input.isKeyPressed(Input.Keys.CONTROL_LEFT)
+ }
+ override val Y: Boolean
+ get() {
+ return Gdx.input.isKeyPressed(Input.Keys.CONTROL_RIGHT)
+ }
+
+ override val leftBumper: Boolean
+ get() {
+ return Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT)
+ }
+ override val rightBumper: Boolean
+ get() {
+ return Gdx.input.isKeyPressed(Input.Keys.SHIFT_RIGHT)
+ }
+ // val pointers = Aspect.all(IsPointer::class.java)
+
+ override val rightStick: Vec
+ get() {
+ if (pressed(Input.Keys.UP) || pressed(Input.Keys.DOWN) || pressed(Input.Keys.LEFT) || pressed(Input.Keys.RIGHT)) {
+ return keyboardAsRightStick()
+ }
+
+ // if (!Gdx.input.isButtonPressed(0)) return Vec(0f, 0f)
+ // if (Gdx.input.isButtonPressed(Input.Buttons.LEFT)) {
+ // val target = GameMappers.positionMapper.get(pointer)
+
+ // val playerV = (playerVelocity.x*playerVelocity.x+playerVelocity.y*playerVelocity.y).sqrt()
+
+ if (!Gdx.input.isButtonPressed(Input.Buttons.LEFT)) {
+ return Vec(0f, 0f)
+ }
+
+ val game = session.game
+ if (game != null && game is Game.UsesMouseAsInputDevice) {
+ val m = game.getMouse()
+ return m
+ }
+ return Vec(0f, 0f)
+ }
+
+ private fun keyboardAsRightStick(): Vec {
+ var x = 0f
+ var y = 0f
+ if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) {
+ x = -1f
+ }
+ if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) {
+ x = 1f
+ }
+ if (Gdx.input.isKeyPressed(Input.Keys.UP)) {
+ y = -1f
+ }
+ if (Gdx.input.isKeyPressed(Input.Keys.DOWN)) {
+ y = 1f
+ }
+ return Vec(x, y)
+ }
+
+ fun pressed(x: Int): Boolean = Gdx.input.isKeyPressed(x)
+
+ val Pair.x: A
+ get() = first
+
+ val Pair.y: B
+ get() = second
+}
diff --git a/src/uk/me/fantastic/retro/input/MappedController.kt b/src/uk/me/fantastic/retro/input/MappedController.kt
new file mode 100644
index 0000000..db96c83
--- /dev/null
+++ b/src/uk/me/fantastic/retro/input/MappedController.kt
@@ -0,0 +1,397 @@
+package uk.me.fantastic.retro.input
+
+import com.badlogic.gdx.controllers.Controller
+import com.badlogic.gdx.controllers.ControllerAdapter
+import com.badlogic.gdx.controllers.PovDirection
+
+import uk.me.fantastic.retro.isLinux
+import uk.me.fantastic.retro.isOSX
+
+/**
+ * Wraps a GDX controller, provides mapping for buttons/axises because GDX seems to lack this
+ */
+internal class MappedController(val controller: Controller) {
+ var A: Int = 1
+ var B: Int = 2
+ var X: Int = 0
+ var Y: Int = 3
+ var GUIDE: Int = 12
+ var L_BUMPER: Int = 4
+ var R_BUMPER: Int = 5
+ var BACK: Int = 8
+ var START: Int = 9
+ var DPAD_UP: Int = -1
+ var DPAD_DOWN: Int = -1
+ var DPAD_LEFT: Int = -1
+ var DPAD_RIGHT: Int = -1
+ var L_STICK_PUSH: Int = 10
+ var R_STICK_PUSH: Int = 11
+
+ var DPAD = 0
+ var combinedDpad = true
+
+ // Axes
+
+ var L_TRIGGER: Int = 6
+ var R_TRIGGER: Int = 7
+ var L_TRIGGER_AXIS: Int = -1
+ var R_TRIGGER_AXIS: Int = -1
+
+ /** left stick vertical axis, -1 if up, 1 if down */
+ var L_STICK_VERTICAL_AXIS: Int = 2
+ /** left stick horizontal axis, -1 if left, 1 if right */
+ var L_STICK_HORIZONTAL_AXIS: Int = 3
+ /** right stick vertical axis, -1 if up, 1 if down */
+ var R_STICK_VERTICAL_AXIS: Int = 0
+ /** right stick horizontal axis, -1 if left, 1 if right */
+ var R_STICK_HORIZONTAL_AXIS: Int = 1
+
+ var mapping = "Unknown"
+
+ var crippledTrigger = false
+
+ fun setToMacDefault() {
+ mapping = "MAC Default"
+ A = 1
+ B = 2
+ X = 0
+ Y = 3
+ GUIDE = 12
+ L_BUMPER = 4
+ R_BUMPER = 5
+ BACK = 8
+ START = 9
+ DPAD_UP = -1
+ DPAD_DOWN = -1
+ DPAD_LEFT = -1
+ DPAD_RIGHT = -1
+ L_STICK_PUSH = 10
+ R_STICK_PUSH = 11
+
+ DPAD = -1
+
+ L_TRIGGER_AXIS = 4
+ R_TRIGGER_AXIS = 5
+
+ R_TRIGGER = 7
+ L_TRIGGER = 6
+
+ L_STICK_VERTICAL_AXIS = 1
+
+ L_STICK_HORIZONTAL_AXIS = 0
+
+ R_STICK_VERTICAL_AXIS = 3
+
+ R_STICK_HORIZONTAL_AXIS = 2
+ }
+
+ fun setToLinuxDefault() {
+ mapping = "Linux Default"
+ A = 0
+ B = 1
+ X = 3
+ Y = 2
+ GUIDE = 10 // was 12
+ L_BUMPER = 4
+ R_BUMPER = 5
+ BACK = 8
+ START = 9
+ DPAD_UP = -1
+ DPAD_DOWN = -1
+ DPAD_LEFT = -1
+ DPAD_RIGHT = -1
+ L_STICK_PUSH = 11 // was 10
+ R_STICK_PUSH = 12 // was 11
+ DPAD = 0
+ L_TRIGGER_AXIS = 2
+ R_TRIGGER_AXIS = 5
+ R_TRIGGER = 7
+ L_TRIGGER = 6
+ L_STICK_VERTICAL_AXIS = 1
+ L_STICK_HORIZONTAL_AXIS = 0
+ R_STICK_VERTICAL_AXIS = 4
+ R_STICK_HORIZONTAL_AXIS = 3
+ }
+
+ fun setToLinuxXbox() {
+ mapping = "Linux Xbox"
+ A = 0
+ B = 1
+ X = 2
+ Y = 3
+ GUIDE = 8 // was 12
+ L_BUMPER = 4
+ R_BUMPER = 5
+ BACK = 6
+ START = 7
+ DPAD_UP = -1
+ DPAD_DOWN = -1
+ DPAD_LEFT = -1
+ DPAD_RIGHT = -1
+ L_STICK_PUSH = 9 // was 10
+ R_STICK_PUSH = 10 // 12
+ DPAD = 0
+ L_TRIGGER_AXIS = 2
+ R_TRIGGER_AXIS = 5
+ R_TRIGGER = 11
+ L_TRIGGER = 12
+ L_STICK_VERTICAL_AXIS = 1
+ L_STICK_HORIZONTAL_AXIS = 0
+ R_STICK_VERTICAL_AXIS = 4
+ R_STICK_HORIZONTAL_AXIS = 3
+ }
+
+ fun setToDS4() {
+ mapping = "DS4"
+ A = 1
+ B = 2
+ X = 0
+ Y = 3
+ GUIDE = 12
+ L_BUMPER = 4
+ R_BUMPER = 5
+ BACK = 8
+ START = 9
+ DPAD_UP = -1
+ DPAD_DOWN = -1
+ DPAD_LEFT = -1
+ DPAD_RIGHT = -1
+ L_STICK_PUSH = 10
+ R_STICK_PUSH = 11
+
+ DPAD = 0
+
+ L_TRIGGER_AXIS = 5
+ R_TRIGGER_AXIS = 4
+
+ R_TRIGGER = 7
+ L_TRIGGER = 6
+
+ L_STICK_VERTICAL_AXIS = 2
+
+ L_STICK_HORIZONTAL_AXIS = 3
+
+ R_STICK_VERTICAL_AXIS = 0
+
+ R_STICK_HORIZONTAL_AXIS = 1
+ }
+
+ fun setToX360() {
+ mapping = "Xbox 360"
+ A = 0
+ B = 1
+ X = 2
+ Y = 3
+ GUIDE = 12
+ L_BUMPER = 4
+ R_BUMPER = 5
+ BACK = 6
+ START = 7
+ DPAD_UP = -1
+ DPAD_DOWN = -1
+ DPAD_LEFT = -1
+ DPAD_RIGHT = -1
+ L_STICK_PUSH = 8 // was 10, changed for wii u pad
+ R_STICK_PUSH = 9 // was 11
+
+ DPAD = 0
+
+ L_TRIGGER_AXIS = 4
+ R_TRIGGER_AXIS = 5 // doesnt work on 360 pad, both are same axis
+ crippledTrigger = true
+
+ R_TRIGGER = 7 // doesnt work on 360 pad
+ L_TRIGGER = 6 // doesnt work on 360 pad
+
+ L_STICK_VERTICAL_AXIS = 0
+
+ L_STICK_HORIZONTAL_AXIS = 1
+
+ R_STICK_VERTICAL_AXIS = 2
+
+ R_STICK_HORIZONTAL_AXIS = 3
+ }
+
+ fun setToF310() {
+ mapping = "Logitech F310"
+ A = 0
+ B = 1
+ X = 2
+ Y = 3
+ GUIDE = -1
+ L_BUMPER = 4
+ R_BUMPER = 5
+ BACK = 6
+ START = 7
+ DPAD_UP = -1
+ DPAD_DOWN = -1
+ DPAD_LEFT = -1
+ DPAD_RIGHT = -1
+ L_STICK_PUSH = 8
+ R_STICK_PUSH = 9
+
+ DPAD = 0
+
+ // TRIGGER_AXIS = 4
+
+ R_TRIGGER = -1
+ L_TRIGGER = -1
+
+ L_STICK_VERTICAL_AXIS = 0
+
+ L_STICK_HORIZONTAL_AXIS = 1
+
+ R_STICK_VERTICAL_AXIS = 2
+
+ R_STICK_HORIZONTAL_AXIS = 3
+ }
+
+ fun setToDirectX() {
+ mapping = "DirectX controller"
+ A = 1
+ B = 2
+ X = 0
+ Y = 3
+ GUIDE = -1
+ L_BUMPER = 4
+ R_BUMPER = 5
+ BACK = 8
+ START = 9
+ DPAD_UP = -1
+ DPAD_DOWN = -1
+ DPAD_LEFT = -1
+ DPAD_RIGHT = -1
+ L_STICK_PUSH = 10 // f310, was 8
+ R_STICK_PUSH = 11 // f310, was9
+
+ DPAD = 0
+
+ // TRIGGER_AXIS = 4
+
+ R_TRIGGER = 7
+ L_TRIGGER = 6
+
+ L_STICK_VERTICAL_AXIS = 2
+
+ L_STICK_HORIZONTAL_AXIS = 3
+
+ R_STICK_VERTICAL_AXIS = 0
+
+ R_STICK_HORIZONTAL_AXIS = 1
+ }
+
+ init {
+ if (isOSX) {
+ setToMacDefault()
+ } else if (isLinux) {
+ if (controller.name.contains("Sony")) {
+ setToLinuxDefault()
+ } else if (controller.name.contains("X-Box")) {
+ setToLinuxXbox()
+ } else {
+ setToLinuxDefault()
+ }
+ } else { // isWindows
+ if (controller.name.contains("F310")) {
+ setToF310()
+ } else if (controller.name.contains("Logitech Dual Action")) {
+ setToDirectX()
+ } else if (controller.name.contains("Wireless Controller")) {
+ setToDS4()
+ } else {
+ setToX360()
+ }
+ }
+ }
+
+ fun b(): Boolean {
+ return controller.getButton(B)
+ }
+
+ fun a(): Boolean {
+ return controller.getButton(A)
+ }
+
+ fun x(): Boolean {
+ return controller.getButton(X)
+ }
+
+ fun y(): Boolean {
+ return controller.getButton(Y)
+ }
+
+ fun lBumper(): Boolean {
+ return controller.getButton(L_BUMPER)
+ }
+
+ fun rBumper(): Boolean {
+ return controller.getButton(R_BUMPER)
+ }
+
+ fun lTrigger(): Boolean {
+ return controller.getButton(L_TRIGGER)
+ }
+
+ fun rTrigger(): Boolean {
+ return controller.getButton(R_TRIGGER)
+ }
+
+ fun LStickHorizontalAxis(): Float {
+ if (combinedDpad && DPAD != -1) {
+ val d = controller.getPov(DPAD)
+ if (d == PovDirection.east || d == PovDirection.northEast || d == PovDirection.southEast) {
+ return 1f
+ } else if (d == PovDirection.west || d == PovDirection.northWest || d == PovDirection.southWest) {
+ return -1f
+ } else if (d == PovDirection.north || d == PovDirection.south) {
+ return 0f
+ }
+ }
+ return controller.getAxis(L_STICK_HORIZONTAL_AXIS)
+ }
+
+ fun LStickVerticalAxis(): Float {
+ if (combinedDpad && DPAD != -1) {
+ val d = controller.getPov(DPAD)
+ if (d == PovDirection.north || d == PovDirection.northEast || d == PovDirection.northWest) {
+ return -1f
+ } else if (d == PovDirection.south || d == PovDirection.southEast || d == PovDirection.southWest) {
+ return 1f
+ } else if (d == PovDirection.east || d == PovDirection.west) {
+ return 0f
+ }
+ }
+ return controller.getAxis(L_STICK_VERTICAL_AXIS)
+ }
+
+ fun RStickHorizontalAxis(): Float {
+ return controller.getAxis(R_STICK_HORIZONTAL_AXIS)
+ }
+
+ fun RStickVerticalAxis(): Float {
+ return controller.getAxis(R_STICK_VERTICAL_AXIS)
+ }
+
+ fun leftTrigger(): Float {
+ // log("mappedcontroller","$L_TRIGGER_AXIS $L_TRIGGER ${controller.getAxis(L_TRIGGER_AXIS)}")
+ if (crippledTrigger) {
+ return (controller.getAxis(L_TRIGGER_AXIS) * 2) - 1
+ } else {
+ return controller.getAxis(L_TRIGGER_AXIS)
+ }
+ }
+
+ fun rightTrigger(): Float {
+ if (crippledTrigger) {
+ return (controller.getAxis(L_TRIGGER_AXIS) * -2) - 1
+ } else {
+ return controller.getAxis((R_TRIGGER_AXIS))
+ }
+ }
+
+ fun start(): Boolean {
+ return controller.getButton(START)
+ }
+
+ var listener: ControllerAdapter? = null
+}
\ No newline at end of file
diff --git a/src/uk/me/fantastic/retro/input/NetworkInput.kt b/src/uk/me/fantastic/retro/input/NetworkInput.kt
new file mode 100644
index 0000000..e281098
--- /dev/null
+++ b/src/uk/me/fantastic/retro/input/NetworkInput.kt
@@ -0,0 +1,40 @@
+package uk.me.fantastic.retro.input
+
+import uk.me.fantastic.retro.utils.Vec
+
+/**
+ * stores state of a device (FIXME REMOVE CLIENTID?)
+ * to be sent over network
+ */
+open class NetworkInput(
+ override var leftStick: Vec = Vec(0f, 0f),
+ override var rightStick: Vec = Vec(0f, 0f),
+ override var leftTrigger: Float = 0f,
+ override var rightTrigger: Float = 0f,
+ override var A: Boolean = false,
+ val clientId: Int = -1
+) :
+ InputDevice() {
+ override val B: Boolean
+ get() = TODO("not implemented") // To change initializer of created properties use File | Settings | File Templates.
+ override val X: Boolean
+ get() = TODO("not implemented") // To change initializer of created properties use File | Settings | File Templates.
+ override val Y: Boolean
+ get() = TODO("not implemented") // To change initializer of created properties use File | Settings | File Templates.
+ override val leftBumper: Boolean
+ get() = TODO("not implemented") // To change initializer of created properties use File | Settings | File Templates.
+ override val rightBumper: Boolean
+ get() = TODO("not implemented") // To change initializer of created properties use File | Settings | File Templates.
+
+ fun copyTo(other: NetworkInput) {
+ other.leftStick = leftStick
+ other.rightStick = rightStick
+ other.A = A
+ }
+
+ fun copyFrom(other: InputDevice) {
+ leftStick = other.leftStick
+ rightStick = other.rightStick
+ A = other.A
+ }
+}
diff --git a/src/uk/me/fantastic/retro/input/RobotInput.kt b/src/uk/me/fantastic/retro/input/RobotInput.kt
new file mode 100644
index 0000000..689e0b8
--- /dev/null
+++ b/src/uk/me/fantastic/retro/input/RobotInput.kt
@@ -0,0 +1,66 @@
+package uk.me.fantastic.retro.input
+
+/**
+ * AI controlled, not very smart, more like drunk input!
+ */
+// class RobotInput(val game: GameMappers) : InputDevice() {
+// override val B: Boolean
+// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
+// override val X: Boolean
+// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
+// override val Y: Boolean
+// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
+// override val leftBumper: Boolean
+// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
+// override val rightBumper: Boolean
+// get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates.
+// override val leftTrigger: Float
+// get() = 0f
+// override val rightTrigger: Float
+// get() = 0f
+//
+//
+// // FIXME array is not the best for a circular list I know
+// val waypoints = arrayOf(Pair(350f, 200f), Pair(100f, 200f), Pair(100f, 100f), Pair(350f, 100f))
+//
+// var waypoint = 0
+//
+// override val leftStick: Vec
+// get() {
+//
+// val destination = waypoints[waypoint]
+// val (x, y) = destination
+//
+// val position = game.positionMapper[entity]
+//
+// if (position!!.isCloseTo(destination)) {
+// waypoint++
+// if (waypoint >= waypoints.size) {
+// waypoint = 0
+// }
+// }
+//
+// val dx = x - position.x
+// val dy = y - position.y
+//
+// val nx = clamp(dx, -1f, 1f)
+// val ny = clamp(dy, -1f, 1f)
+//
+// return Vec(nx, -ny)
+// }
+//
+// override val rightStick: Vec
+// get() {
+//
+// return Vec()
+// }
+//
+// override val A: Boolean
+// get() {
+// return false
+// }
+//
+// fun clamp(x: Float, min: Float, max: Float): Float {
+// return Math.max(min, Math.min(max, x))
+// }
+// }
diff --git a/src/uk/me/fantastic/retro/input/SimpleTouchscreenInput.kt b/src/uk/me/fantastic/retro/input/SimpleTouchscreenInput.kt
new file mode 100644
index 0000000..06710e6
--- /dev/null
+++ b/src/uk/me/fantastic/retro/input/SimpleTouchscreenInput.kt
@@ -0,0 +1,84 @@
+package uk.me.fantastic.retro.input
+
+import com.badlogic.gdx.Gdx
+import uk.me.fantastic.retro.utils.Vec
+
+/**
+ * has one joystick on the left and one button on the right of the screen
+ */
+internal class SimpleTouchscreenInput : InputDevice() {
+
+ var joyStickOrigin = Vec(0f, 0f)
+ var joyStickPosition = Vec(0f, 0f)
+ var joyStickFinger = -1
+
+ override val leftTrigger: Float
+ get() = 0f
+ override val rightTrigger: Float
+ get() = 0f
+
+ override val A: Boolean
+ get() {
+// if (TouchscreenJoystick.touchReleased) {
+// TouchscreenJoystick.touchReleased = false
+// return true
+// }
+ for (i in 0..10) {
+ if (Gdx.input.isTouched(i)) {
+ val x = Gdx.input.getX(i).toFloat()
+ // val y = Gdx.input.getY(i).toFloat()
+ if (x > Gdx.graphics.displayMode.width / 2) {
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ override val leftStick: Vec
+ get() {
+ for (i in 0..10) {
+ if (Gdx.input.isTouched(i)) {
+ val x = Gdx.input.getX(i).toFloat()
+ val y = Gdx.input.getY(i).toFloat()
+ if (x < Gdx.graphics.displayMode.width / 2) {
+ if (joyStickFinger == -1) {
+ joyStickOrigin = Vec(x, y)
+ }
+ joyStickFinger = i
+ joyStickPosition = Vec(x, y)
+ val (a, b) = filterDeadzone(0f, (joyStickPosition.x - joyStickOrigin.x), (joyStickPosition.y - joyStickOrigin.y))
+ return Vec(a, b).normVector()
+ }
+ }
+ }
+ joyStickFinger = -1
+ return Vec(0f, 0f)
+ }
+
+ override val rightStick: Vec
+ get() {
+
+ return Vec(0f, 0f)
+ }
+ override val B: Boolean
+ get() {
+ return false
+ }
+ override val X: Boolean
+ get() {
+ return false
+ }
+ override val Y: Boolean
+ get() {
+ return false
+ }
+ override val leftBumper: Boolean
+ get() {
+ return false
+ }
+ override val rightBumper: Boolean
+ get() {
+ return false
+ }
+}
diff --git a/src/uk/me/fantastic/retro/input/StatefulController.kt b/src/uk/me/fantastic/retro/input/StatefulController.kt
new file mode 100644
index 0000000..5c8c493
--- /dev/null
+++ b/src/uk/me/fantastic/retro/input/StatefulController.kt
@@ -0,0 +1,186 @@
+package uk.me.fantastic.retro.input
+
+import com.badlogic.gdx.controllers.Controller
+import com.badlogic.gdx.controllers.ControllerAdapter
+import com.badlogic.gdx.controllers.PovDirection
+
+/**
+ * Gets state of wrapped controller using events, then provides this state through an
+ * api that can be polled
+ * Useful for menus (and simple games where you don't want to miss an input ?)
+ */
+internal class StatefulController(val c: MappedController) : ControllerAdapter() {
+
+ var startPressed = false
+ var upPressed = false
+ var downPressed = false
+ var leftPressed = false
+ var rightPressed = false
+ var APressed = false
+ var BPressed = false
+
+ var horCentered = true
+ var vertCentered = true
+
+ val THRESHOLD = 0.3
+ val THRESHOLD_H = 0.6
+
+ init {
+ c.controller.addListener(this)
+ }
+
+ fun clearEvents() {
+ startPressed = false
+ upPressed = false
+ downPressed = false
+ leftPressed = false
+ rightPressed = false
+ APressed = false
+ BPressed = false
+ horCentered = true
+ vertCentered = true
+ }
+
+ val isUpButtonJustPressed: Boolean
+ get() {
+ val t = upPressed
+ upPressed = false
+ return t
+ }
+
+ val isDownButtonJustPressed: Boolean
+ get() {
+ val t = downPressed
+ downPressed = false
+ return t
+ }
+
+ val isLeftButtonJustPressed: Boolean
+ get() {
+ val t = leftPressed
+ leftPressed = false
+ return t
+ }
+
+ val isRightButtonJustPressed: Boolean
+ get() {
+ val t = rightPressed
+ rightPressed = false
+ return t
+ }
+
+ val isButtonAJustPressed: Boolean
+ get() {
+ // log("stateful $this pressed button")
+ val t = APressed
+ APressed = false
+ return t
+ }
+ val isButtonBJustPressed: Boolean
+ get() {
+ val t = BPressed
+ BPressed = false
+ return t
+ }
+
+ val isStartButtonJustPressed: Boolean
+ get() {
+ val t = startPressed
+ startPressed = false
+ return t
+ }
+
+ override fun buttonDown(controller: Controller?, buttonIndex: Int): Boolean {
+
+ when (buttonIndex) {
+ c.START -> startPressed = true
+ c.A -> APressed = true
+ c.B -> BPressed = true
+ c.DPAD_UP -> upPressed = true
+ c.DPAD_DOWN -> downPressed = true
+ c.DPAD_LEFT -> leftPressed = true
+ c.DPAD_RIGHT -> rightPressed = true
+ }
+
+ return true
+ }
+
+ override fun povMoved(controller: Controller?, povIndex: Int, value: PovDirection?): Boolean {
+
+ when (value) {
+ PovDirection.north -> upPressed = true
+ PovDirection.south -> downPressed = true
+ PovDirection.west -> leftPressed = true
+ PovDirection.east -> rightPressed = true
+ PovDirection.center -> {
+ }
+ PovDirection.northEast -> {
+ }
+ PovDirection.northWest -> {
+ }
+ PovDirection.southEast -> {
+ }
+ PovDirection.southWest -> {
+ }
+ }
+
+ return true
+ }
+
+ override fun axisMoved(controller: Controller?, axisIndex: Int, value: Float): Boolean {
+ if (axisIndex == c.L_STICK_HORIZONTAL_AXIS) {
+ if (value > THRESHOLD_H && horCentered) {
+ rightPressed = true
+ horCentered = false
+ } else if (value < -THRESHOLD_H && horCentered) {
+ leftPressed = true
+ horCentered = false
+ } else if (value < 0.1 && value > -0.1) {
+ horCentered = true
+ }
+ } else if (axisIndex == c.L_STICK_VERTICAL_AXIS) {
+ if (value > THRESHOLD && vertCentered) {
+ downPressed = true
+ vertCentered = false
+ } else if (value < -THRESHOLD && vertCentered) {
+ upPressed = true
+ vertCentered = false
+ } else if (value < 0.1 && value > -0.1) {
+ vertCentered = true
+ }
+ }
+
+ return true
+ }
+}
+
+/*
+ * move this to MAppedController. possibly make a named controlleradapter class, or use mappedcontroller to implement
+ it, whatever. DONE
+ * make isJustPressed behave like the keyboard one, only store the data for one frame TODO NOT SURE WONT MISS STUFF
+ so anyone who wants to use these events will have to call a poll method first anyway
+ so we could do it all by polling, the only reason for events is that polling the mouse was found to miss clicks
+ no??? we can assume each bool is read once per frame, then no need for poll method??? but then we can't know
+ the state on the previous frame to know if its a press or a hold
+ also if it doesnt get read by a screen but does get set then event stayed until another screen reads it
+
+ for controllers, using the gdx event api still doesnt get us press/depress events on sticks
+ the old way of having a delay in the menu after an input received treats keyboard and controller the same
+ when actually we may want key-repeat behaviour to be different. could scale repeats with how far stick is
+ pushed for instance.
+
+ * can we do poll in app render so its always done? i think so. TODO NOT SURE
+
+ polling vs events
+ if you want controller stuff to be entirely
+ poll based we could do it here too, but since you seem to be able to stack as many listeners as you want
+ on a controller i dont see a downside of using them. (with key/mouse listener you only get one, unless you
+ deliberately using a multiplex one, but then if you forgot on any other screen and overwrite it you lose your listeners)
+ listeners are kind of like 'comefrom' though. if you forget you created them you have code executing that you cant discover
+ just from reading the main loop. if they are created when app starts and they dont affect code in other files that
+ seems ok. but game session adds and removes listeners during app lifetime.
+
+ * maybe we could get rid of listeners in gamesession though and just query an isAnyButtonJustPressed for each controller?
+
+ * remember also to test splashscreens DONE
+ */
diff --git a/src/uk/me/fantastic/retro/input/TouchscreenInput.kt b/src/uk/me/fantastic/retro/input/TouchscreenInput.kt
new file mode 100644
index 0000000..6bab954
--- /dev/null
+++ b/src/uk/me/fantastic/retro/input/TouchscreenInput.kt
@@ -0,0 +1,59 @@
+package uk.me.fantastic.retro.input
+
+/**
+ * Created by Richard on 13/08/2016.
+ * Maps a the touchscreen controller system controller to an inputdevice so it can be read
+ */
+// class TouchscreenInput : InputDevice() {
+//
+// override val leftTrigger: Float
+// get() = 0f
+// override val rightTrigger: Float
+// get() = 0f
+//
+// override val A: Boolean
+// get() {
+// // if (TouchscreenJoystick.touchReleased) {
+// // TouchscreenJoystick.touchReleased = false
+// // return true
+// // }
+// return (Gdx.input.isTouched(1))
+// // return false
+// }
+//
+// override val leftStick: Vec
+// get() {
+// val x = TouchscreenJoystick.LStickHorizontalAxis
+// val y = TouchscreenJoystick.LStickVerticalAxis
+// val (a, b) = filterDeadzone(0.15f, x, y)
+// return Vec(a, b)
+// }
+//
+// override val rightStick: Vec
+// get() {
+// val x = TouchscreenJoystick.RStickHorizontalAxis
+// val y = TouchscreenJoystick.RStickVerticalAxis
+// val (a, b) = filterDeadzone(0.6f, x, y)
+// return Vec(a, b)
+// }
+// override val B: Boolean
+// get() {
+// return false
+// }
+// override val X: Boolean
+// get() {
+// return false
+// }
+// override val Y: Boolean
+// get() {
+// return false
+// }
+// override val leftBumper: Boolean
+// get() {
+// return false
+// }
+// override val rightBumper: Boolean
+// get() {
+// return false
+// }
+// }
diff --git a/src/uk/me/fantastic/retro/menu/Menu.kt b/src/uk/me/fantastic/retro/menu/Menu.kt
new file mode 100644
index 0000000..500da72
--- /dev/null
+++ b/src/uk/me/fantastic/retro/menu/Menu.kt
@@ -0,0 +1,74 @@
+package uk.me.fantastic.retro.menu
+
+import uk.me.fantastic.retro.Resources
+import uk.me.fantastic.retro.log
+import java.util.ArrayList
+
+/**
+ * Created by richard on 13/07/2016.
+ * Essentially an ArrayList of MenuItems that tracks which of them is selected
+ */
+
+class Menu(
+ val title: String,
+ val bottomText: () -> String = { "" },
+ val quitAction: () -> Unit = {},
+
+ val doubleSpaced: Boolean = Resources.FONT.data.down > -10
+) : ArrayList