diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..67df35c
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,18 @@
+# https://editorconfig.org
+root = true
+
+[*]
+indent_style = space
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.{java,scala,groovy,kt,kts}]
+indent_size = 4
+
+[*.gradle]
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6c84be0
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+* text=auto eol=lf
+*.bat text=auto eol=crlf
diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
new file mode 100644
index 0000000..867d3e3
--- /dev/null
+++ b/android/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 0000000..f08ef67
--- /dev/null
+++ b/android/build.gradle
@@ -0,0 +1,118 @@
+apply plugin: 'com.android.application'
+
+
+android {
+ compileSdk 32
+ sourceSets {
+ main {
+ manifest.srcFile 'AndroidManifest.xml'
+ java.srcDirs('src/main/java')
+ aidl.srcDirs('src/main/java')
+ renderscript.srcDirs('src/main/java')
+ res.srcDirs('res')
+ assets.srcDirs('../assets')
+ jniLibs.srcDirs('libs')
+ }
+ }
+ packagingOptions {
+ resources.with {
+ excludes += ['META-INF/robovm/ios/robovm.xml',
+ 'META-INF/DEPENDENCIES.txt', 'META-INF/DEPENDENCIES', 'META-INF/dependencies.txt', '**/*.gwt.xml']
+ pickFirsts += ['META-INF/LICENSE.txt', 'META-INF/LICENSE', 'META-INF/license.txt', 'META-INF/LGPL2.1',
+ 'META-INF/NOTICE.txt', 'META-INF/NOTICE', 'META-INF/notice.txt']
+ }
+ }
+ defaultConfig {
+ applicationId 'com.agifans.agile'
+ minSdkVersion 19
+ targetSdkVersion 32
+ versionCode 1
+ versionName "1.0"
+ multiDexEnabled true
+ }
+ namespace "com.agifans.agile"
+ compileOptions {
+ sourceCompatibility "11"
+ targetCompatibility "11"
+ coreLibraryDesugaringEnabled true
+ }
+ buildTypes {
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+repositories {
+ // needed for AAPT2, may be needed for other tools
+ google()
+}
+
+configurations { natives }
+
+dependencies {
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
+ implementation "com.badlogicgames.gdx:gdx-backend-android:$gdxVersion"
+ implementation project(':core')
+
+ natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-arm64-v8a"
+ natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi-v7a"
+ natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86"
+ natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86_64"
+
+}
+
+// Called every time gradle gets executed, takes the native dependencies of
+// the natives configuration, and extracts them to the proper libs/ folders
+// so they get packed with the APK.
+tasks.register('copyAndroidNatives') {
+ doFirst {
+ file("libs/armeabi-v7a/").mkdirs()
+ file("libs/arm64-v8a/").mkdirs()
+ file("libs/x86_64/").mkdirs()
+ file("libs/x86/").mkdirs()
+
+ configurations.named("natives").orNull.copy().files.each { jar ->
+ def outputDir = null
+ if(jar.name.endsWith("natives-armeabi-v7a.jar")) outputDir = file("libs/armeabi-v7a")
+ if(jar.name.endsWith("natives-arm64-v8a.jar")) outputDir = file("libs/arm64-v8a")
+ if(jar.name.endsWith("natives-x86_64.jar")) outputDir = file("libs/x86_64")
+ if(jar.name.endsWith("natives-x86.jar")) outputDir = file("libs/x86")
+ if(outputDir != null) {
+ copy {
+ from zipTree(jar)
+ into outputDir
+ include "*.so"
+ }
+ }
+ }
+ }
+}
+tasks.matching { it.name.contains("merge") && it.name.contains("JniLibFolders") }.configureEach { packageTask ->
+ packageTask.dependsOn 'copyAndroidNatives'
+}
+
+tasks.register('run', Exec) {
+ def path
+ def localProperties = project.file("../local.properties")
+ if (localProperties.exists()) {
+ Properties properties = new Properties()
+ localProperties.withInputStream { instr ->
+ properties.load(instr)
+ }
+ def sdkDir = properties.getProperty('sdk.dir')
+ if (sdkDir) {
+ path = sdkDir
+ } else {
+ path = "$System.env.ANDROID_SDK_ROOT"
+ }
+ } else {
+ path = "$System.env.ANDROID_SDK_ROOT"
+ }
+
+ def adb = path + "/platform-tools/adb"
+ commandLine "$adb", 'shell', 'am', 'start', '-n', 'com.agifans.agile/com.agifans.agile.android.AndroidLauncher'
+}
+
+eclipse.project.name = appName + "-android"
diff --git a/android/ic_launcher-web.png b/android/ic_launcher-web.png
new file mode 100644
index 0000000..0f59902
Binary files /dev/null and b/android/ic_launcher-web.png differ
diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro
new file mode 100644
index 0000000..9c00b1d
--- /dev/null
+++ b/android/proguard-rules.pro
@@ -0,0 +1,60 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+-verbose
+
+-dontwarn android.support.**
+-dontwarn com.badlogic.gdx.backends.android.AndroidFragmentApplication
+-dontwarn com.badlogic.gdx.utils.GdxBuild
+-dontwarn com.badlogic.gdx.physics.box2d.utils.Box2DBuild
+-dontwarn com.badlogic.gdx.jnigen.BuildTarget*
+-dontwarn com.badlogic.gdx.graphics.g2d.freetype.FreetypeBuild
+
+# If you're encountering ProGuard issues and use gdx-controllers, THIS MIGHT BE WHY!!!
+
+# Uncomment the following line if you use the gdx-controllers official extension.
+#-keep class com.badlogic.gdx.controllers.android.AndroidControllers
+
+-keepclassmembers class com.badlogic.gdx.backends.android.AndroidInput* {
+ (com.badlogic.gdx.Application, android.content.Context, java.lang.Object, com.badlogic.gdx.backends.android.AndroidApplicationConfiguration);
+}
+
+-keepclassmembers class com.badlogic.gdx.physics.box2d.World {
+ boolean contactFilter(long, long);
+ void beginContact(long);
+ void endContact(long);
+ void preSolve(long, long);
+ void postSolve(long, long);
+ boolean reportFixture(long);
+ float reportRayFixture(long, float, float, float, float, float);
+}
+
+# You will need the next three lines if you use scene2d for UI or gameplay
+# If you don't use scene2d at all, you can remove or comment out the next line
+-keep public class com.badlogic.gdx.scenes.scene2d.** { *; }
+# You will need the next two lines if you use BitmapFont or any scene2d.ui text
+-keep public class com.badlogic.gdx.graphics.g2d.BitmapFont { *; }
+# You will probably need this line in most cases
+-keep public class com.badlogic.gdx.graphics.Color { *; }
+
+# These two lines are used with mapping files; see https://developer.android.com/build/shrink-code#retracing
+-keepattributes LineNumberTable,SourceFile
+-renamesourcefileattribute SourceFile
diff --git a/android/project.properties b/android/project.properties
new file mode 100644
index 0000000..aab9a82
--- /dev/null
+++ b/android/project.properties
@@ -0,0 +1,14 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-rules.pro
+
+# Project target.
+target=android-19
diff --git a/android/res/drawable-hdpi/ic_launcher.png b/android/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..9fca30c
Binary files /dev/null and b/android/res/drawable-hdpi/ic_launcher.png differ
diff --git a/android/res/drawable-mdpi/ic_launcher.png b/android/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..d0befa9
Binary files /dev/null and b/android/res/drawable-mdpi/ic_launcher.png differ
diff --git a/android/res/drawable-xhdpi/ic_launcher.png b/android/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..ba83684
Binary files /dev/null and b/android/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/android/res/drawable-xxhdpi/ic_launcher.png b/android/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4225649
Binary files /dev/null and b/android/res/drawable-xxhdpi/ic_launcher.png differ
diff --git a/android/res/values/strings.xml b/android/res/values/strings.xml
new file mode 100644
index 0000000..9a1c4bf
--- /dev/null
+++ b/android/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ agile
+
diff --git a/android/res/values/styles.xml b/android/res/values/styles.xml
new file mode 100644
index 0000000..ce3571e
--- /dev/null
+++ b/android/res/values/styles.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/android/src/main/java/com/agifans/agile/android/AndroidLauncher.java b/android/src/main/java/com/agifans/agile/android/AndroidLauncher.java
new file mode 100644
index 0000000..a70844f
--- /dev/null
+++ b/android/src/main/java/com/agifans/agile/android/AndroidLauncher.java
@@ -0,0 +1,18 @@
+package com.agifans.agile.android;
+
+import android.os.Bundle;
+
+import com.badlogic.gdx.backends.android.AndroidApplication;
+import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;
+import com.agifans.agile.Agile;
+
+/** Launches the Android application. */
+public class AndroidLauncher extends AndroidApplication {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ AndroidApplicationConfiguration configuration = new AndroidApplicationConfiguration();
+ configuration.useImmersiveMode = true; // Recommended, but not required.
+ initialize(new Agile(), configuration);
+ }
+}
\ No newline at end of file
diff --git a/assets/libgdx.png b/assets/libgdx.png
new file mode 100644
index 0000000..6c7bfca
Binary files /dev/null and b/assets/libgdx.png differ
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..051911a
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,44 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ maven { url 'https://s01.oss.sonatype.org' }
+ mavenLocal()
+ google()
+ maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
+ maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' }
+ }
+ dependencies {
+ classpath "com.android.tools.build:gradle:8.1.2"
+ classpath "org.docstr:gwt-gradle-plugin:$gwtPluginVersion"
+
+ }
+}
+
+allprojects {
+ apply plugin: 'eclipse'
+ apply plugin: 'idea'
+}
+
+configure(subprojects - project(':android')) {
+ apply plugin: 'java-library'
+ sourceCompatibility = 11
+ compileJava {
+ options.incremental = true
+ }
+}
+
+subprojects {
+ version = '1.0.0'
+ ext.appName = 'agile'
+ repositories {
+ mavenCentral()
+ maven { url 'https://s01.oss.sonatype.org' }
+ // You may want to remove the following line if you have errors downloading dependencies.
+ mavenLocal()
+ maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
+ maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' }
+ maven { url 'https://jitpack.io' }
+ }
+}
+
+eclipse.project.name = 'agile' + '-parent'
diff --git a/core/build.gradle b/core/build.gradle
new file mode 100644
index 0000000..6d84ca8
--- /dev/null
+++ b/core/build.gradle
@@ -0,0 +1,6 @@
+[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
+eclipse.project.name = appName + '-core'
+
+dependencies {
+ api "com.badlogicgames.gdx:gdx:$gdxVersion"
+}
diff --git a/core/src/main/java/com/agifans/agile/Agile.gwt.xml b/core/src/main/java/com/agifans/agile/Agile.gwt.xml
new file mode 100644
index 0000000..f98a40a
--- /dev/null
+++ b/core/src/main/java/com/agifans/agile/Agile.gwt.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/java/com/agifans/agile/Agile.java b/core/src/main/java/com/agifans/agile/Agile.java
new file mode 100644
index 0000000..46ba493
--- /dev/null
+++ b/core/src/main/java/com/agifans/agile/Agile.java
@@ -0,0 +1,82 @@
+package com.agifans.agile;
+
+import com.badlogic.gdx.ApplicationAdapter;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.graphics.GL20;
+import com.badlogic.gdx.graphics.g2d.SpriteBatch;
+
+/** {@link com.badlogic.gdx.ApplicationListener} implementation shared by all platforms. */
+public class Agile extends ApplicationAdapter {
+
+ private SpriteBatch batch;
+ private GameScreen screen;
+ private AgileRunner agileRunner;
+ private WavePlayer wavePlayer;
+
+ /**
+ * Constructor for Agile.
+ *
+ * @param agileRunner
+ * @param wavePlayer
+ */
+ public Agile(AgileRunner agileRunner, WavePlayer wavePlayer) {
+ this.agileRunner = agileRunner;
+ this.wavePlayer = wavePlayer;
+ }
+
+ @Override
+ public void create() {
+ batch = new SpriteBatch();
+ screen = new GameScreen();
+ startGame(selectGame());
+ }
+
+ /**
+ * Starts the AGI game contained in the given game folder.
+ *
+ * @param gameFolder The folder from which we'll get all of the game data.
+ */
+ private void startGame(String gameFolder) {
+ // Register the key event handlers for keyUp, keyDown, and keyTyped.
+ UserInput userInput = new UserInput();
+ Gdx.input.setInputProcessor(userInput);
+
+ agileRunner.init(gameFolder, userInput, wavePlayer, screen.getPixels());
+
+ // Start up the AgileRunner to run the interpreter in the background.
+ agileRunner.start();
+ }
+
+ /**
+ * Selects am AGI game folder to run.
+ *
+ * @return The folder containing the AGI game's resources.
+ */
+ private String selectGame() {
+ // TODO: Implement selection logic. This is a placeholder for now.
+ // TODO: Space Quest 1 VIEW on title screen is mis-placed.
+ // TODO: MH2 and GR both complain that logic is null.
+ // TODO: Game clock should stop when in menus or window showing, as should animations.
+ return "C:\\dev\\agi\\winagi\\kq1";
+ }
+
+ @Override
+ public void render() {
+ // Update screen.
+ screen.render();
+
+ // Render.
+ Gdx.gl.glClearColor(0.15f, 0.15f, 0.2f, 1f);
+ Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
+ batch.begin();
+ batch.draw(screen.getDrawScreen(), 0, 0, 960, 600);
+ batch.end();
+ }
+
+ @Override
+ public void dispose() {
+ agileRunner.stop();
+ batch.dispose();
+ screen.dispose();
+ }
+}
diff --git a/core/src/main/java/com/agifans/agile/AgileRunner.java b/core/src/main/java/com/agifans/agile/AgileRunner.java
new file mode 100644
index 0000000..5e525cd
--- /dev/null
+++ b/core/src/main/java/com/agifans/agile/AgileRunner.java
@@ -0,0 +1,156 @@
+package com.agifans.agile;
+
+import java.io.File;
+import java.util.List;
+
+import com.agifans.agile.agilib.Game;
+import com.agifans.agile.agilib.Logic;
+import com.agifans.agile.agilib.Logic.Action;
+import com.agifans.agile.agilib.Logic.OperandType;
+
+import com.badlogic.gdx.Gdx;
+
+/**
+ * Performs the actual loading and then running of the AGI game. This is an abstract
+ * class since the code needs to be run in a background thread/worker, which is something
+ * that is best handled by the platform specific code. Most of the code is in this class
+ * though, but the launching of the background thread/worker, and its main timing loop,
+ * is implemented in the sub-classes.
+ */
+public abstract class AgileRunner {
+
+ protected Interpreter interpreter;
+
+ private String gameFolder;
+ private WavePlayer wavePlayer;
+ private UserInput userInput;
+ private short[] pixels;
+
+ public void init(String gameFolder, UserInput userInput, WavePlayer wavePlayer, short[] pixels) {
+ this.gameFolder = gameFolder;
+ this.userInput = userInput;
+ this.wavePlayer = wavePlayer;
+ this.pixels = pixels;
+ }
+
+ /**
+ * Attempts to load an AGI game from the game folder.
+ */
+ protected void loadGame() {
+ Game game = null;
+
+ // Use a dummy TextGraphics instance to render the "Loading" text in grand AGI fashion.
+ TextGraphics textGraphics = new TextGraphics(pixels, null, null);
+ try {
+ // TODO: Change to libgdx files??
+ File wordsFile = new File(gameFolder + "\\WORDS.TOK");
+ if (wordsFile.exists()) {
+ textGraphics.drawString(pixels, "Loading... Please wait", 72, 88, 15, 0);
+ }
+ game = new Game(gameFolder);
+ }
+ finally {
+ textGraphics.clearLines(0, 24, 0);
+ }
+
+ // Game detection logic and update windows title.
+ Detection gameDetection = new Detection(game);
+ Gdx.graphics.setTitle(String.format("AGILE v0.0.0.0 | %s", gameDetection.gameName));
+
+ // Patch game option.
+ patchGame(game, gameDetection.gameId, gameDetection.gameName);
+
+ // Create the Interpreter to run this Game.
+ this.interpreter = new Interpreter(game, userInput, wavePlayer, pixels);
+ }
+
+ /**
+ * Patches the given games's Logic scripts, so that the starting question is skipped.
+ *
+ * @param game Game to patch the Logics for.
+ * @param gameId The detected game ID.
+ * @param gameName The detected game name.
+ *
+ * @return The patched Game.
+ */
+ private Game patchGame(Game game, String gameId, String gameName) {
+ for (Logic logic : game.logics) {
+ if (logic != null) {
+ List actions = logic.actions;
+
+ switch (gameId) {
+
+ case "goldrush":
+ // Gold Rush version 3.0 doesn't have copy protection
+ if (gameName.contains("3.0")) {
+ break;
+ }
+ if (logic.index == 129) {
+ // Changes the new.room(125) to be new.room(73) instead, thus skipping the questions.
+ Action action = actions.get(27);
+ if ((action.operation.opcode == 18) && (action.operands.get(0).asInt() == 125)) {
+ action.operands.set(0, logic.new Operand(OperandType.NUM, 73));
+ }
+ }
+ break;
+
+ case "mh1":
+ if (logic.index == 159) {
+ // Modifies LOGIC.159 to jump to the code that is run when a successful answer is entered.
+ if ((actions.get(134).operation.opcode == 18) && (actions.get(134).operands.get(0).asInt() == 153)) {
+ actions.set(0, logic.new GotoAction(List.of(logic.new Operand(OperandType.ADDRESS, actions.get(132).address))));
+ actions.get(0).logic = logic;
+ }
+ }
+ break;
+
+ case "kq4":
+ if (logic.index == 0) {
+ // Changes the new.room(140) to be new.room(96) instead, thus skipping the questions.
+ Action action = actions.get(55);
+ if ((action.operation.opcode == 18) && (action.operands.get(0).asInt() == 140)) {
+ action.operands.set(0, logic.new Operand(OperandType.NUM, 96));
+ }
+ }
+ break;
+
+ case "lsl1":
+ if (logic.index == 6) {
+ // Modifies LOGIC.6 to jump to the code that is run when all of the trivia questions has been answered correctly.
+ Action action = actions.get(0);
+ // Verify that the action is the if-condition to check if the user can enter the game.
+ if (action.operation.opcode == 255 && action.operands.size() == 2) {
+ actions.set(0, logic.new GotoAction(List.of(logic.new Operand(OperandType.ADDRESS, actions.get(1).address))));
+ actions.get(0).logic = logic;
+
+ // Skips the 'Thank you. And now, slip into your leisure suit and prepare to enter the
+ // "Land of the Lounge Lizards" with "Leisure "Suit Larry!"' message
+ int printIndex = 9;
+ Action printAction = actions.get(printIndex);
+
+ // Verify it's the print function
+ if (printAction.operation.opcode == 101) {
+ // Go to next command in the logic, which is the new.room command
+ actions.set(printIndex, logic.new GotoAction(List.of(logic.new Operand(OperandType.ADDRESS, actions.get(printIndex + 1).address))));
+ actions.get(printIndex).logic = logic;
+ }
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+
+ return game;
+ }
+
+ public abstract void start();
+
+ public abstract void stop();
+
+ public abstract boolean isRunning();
+
+}
diff --git a/core/src/main/java/com/agifans/agile/AnimatedObject.java b/core/src/main/java/com/agifans/agile/AnimatedObject.java
new file mode 100644
index 0000000..eea7da2
--- /dev/null
+++ b/core/src/main/java/com/agifans/agile/AnimatedObject.java
@@ -0,0 +1,1644 @@
+package com.agifans.agile;
+
+import com.agifans.agile.agilib.Picture;
+import com.agifans.agile.agilib.View;
+import com.agifans.agile.agilib.View.Cel;
+import com.agifans.agile.agilib.View.Loop;
+
+/**
+ * The AnimatedObject class is one of the core classes in the AGI interpreter. An instance of
+ * this class holds the state of an animated object on the screen. Many of the action commands
+ * change the state within an instance of AnimatedObject, and the interpreter makes use of
+ * the instances of this class stored within the animated object table to perform an animation
+ * cycle.
+ */
+public class AnimatedObject implements Comparable {
+
+ /**
+ * Number of animate cycles between moves of the AnimatedObject. Set by step.time action command.
+ */
+ public int stepTime;
+
+ /**
+ * Count down from StepTime for determining when the AnimatedObject will move. Initially set
+ * by step.time and it then counts down from there on each animate cycle, resetting back to
+ * the StepTime value when it hits zero.
+ */
+ public int stepTimeCount;
+
+ /**
+ * The index of this AnimatedObject in the animated object table. Set to -1 for add.to.pic objects.
+ */
+ public byte objectNumber;
+
+ /**
+ * Current X position of this AnimatedObject.
+ */
+ public short x;
+
+ /**
+ * Current Y position of this AnimatedObject.
+ */
+ public short y;
+
+ /**
+ * The current view number for this AnimatedObject.
+ */
+ public int currentView;
+
+ /**
+ * The View currently being used by this AnimatedObject.
+ */
+ public View view() { return state.views[currentView]; }
+
+ /**
+ * The current loop number within the View.
+ */
+ public int currentLoop;
+
+ /**
+ * The number of loops in the View.
+ */
+ public int numberOfLoops() { return view().loops.size(); }
+
+ /**
+ * The Loop that is currently cycling for this AnimatedObject.
+ */
+ public Loop loop() { return (Loop)view().loops.get(currentLoop); }
+
+ /**
+ * The current cell number within the loop.
+ */
+ public int currentCel;
+
+ /**
+ * The number of cels in the current loop.
+ */
+ public int numberOfCels() { return loop().cels.size(); }
+
+ /**
+ * The Cel currently being displayed.
+ */
+ public Cel cel() { return (Cel)loop().cels.get(currentCel); }
+
+ /**
+ * The previous Cel that was displayed.
+ */
+ public Cel previousCel;
+
+ /**
+ * The background save area for this AnimatedObject.
+ */
+ public SaveArea saveArea;
+
+ /**
+ * Previous X position.
+ */
+ public short prevX;
+
+ /**
+ * Previous Y position.
+ */
+ public short prevY;
+
+ /**
+ * X dimension of the current cel.
+ */
+ public short xSize() { return (short)cel().getWidth(); }
+
+ /**
+ * Y dimesion of the current cel.
+ */
+ public short ySize() { return (short)cel().getHeight(); }
+
+ /**
+ * Distance that this AnimatedObject will move on each move.
+ */
+ public int stepSize;
+
+ /**
+ * The number of animate cycles between changing to the next cel in the current
+ * loop. Set by the cycle.time action command.
+ */
+ public int cycleTime;
+
+ /**
+ * Count down from CycleTime for determining when the AnimatedObject will cycle to the next
+ * cel in the loop. Initially set by cycle.time and it then counts down from there on each
+ * animate cycle, resetting back to the CycleTime value when it hits zero.
+ */
+ public int cycleTimeCount;
+
+ /**
+ * The AnimatedObject's direction.
+ */
+ public byte direction;
+
+ /**
+ * The AnimatedObject's motion type.
+ */
+ public MotionType motionType;
+
+ /**
+ * The AnimatedObject's cycling type.
+ */
+ public CycleType cycleType;
+
+ /**
+ * The priority band value for this AnimatedObject.
+ */
+ public byte priority;
+
+ /**
+ * The control colour of the box around the base of add.to.pic objects. Not application
+ * to normal AnimatedObjects.
+ */
+ public byte controlBoxColour;
+
+ /**
+ * true if AnimatedObject is drawn on the screen; otherwise false;
+ */
+ public boolean drawn;
+
+ /**
+ * true if the AnimatedObject should ignore blocks; otherwise false. Ignoring blocks
+ * means that it can pass black priority one lines and also script blocks. Set to true
+ * by the ignore.blocks action command. Set to false by the observe.blocks action
+ * command.
+ */
+ public boolean ignoreBlocks;
+
+ /**
+ * true if the AnimatedObject has fixed priority; otherwise false. Set to true by the
+ * set.priority action command. Set to false by the release.priority action command.
+ */
+ public boolean fixedPriority;
+
+ /**
+ * true if the AnimatedObject should ignore the horizon; otherwise false. Set to true
+ * by the ignore.horizon action command. Set to false by the observe.horizon action
+ * command.
+ */
+ public boolean ignoreHorizon;
+
+ /**
+ * true if the AnimatedObject should be updated; otherwise false.
+ */
+ public boolean update;
+
+ /**
+ * true if the AnimatedObject should be cycled; otherwise false.
+ */
+ public boolean cycle;
+
+ /**
+ * true if the AnimatedObject can move; otherwise false.
+ */
+ public boolean animated;
+
+ /**
+ * true if the AnimatedObject is blocked; otherwise false.
+ */
+ public boolean blocked;
+
+ /**
+ * true if the AnimatedObject must stay entirely on water; otherwise false.
+ */
+ public boolean stayOnWater;
+
+ /**
+ * true if the AnimatedObject must not be entirely on water; otherwise false.
+ */
+ public boolean stayOnLand;
+
+ /**
+ * true if the AnimatedObject is ignoring collisions with other AnimatedObjects; otherwise false.
+ */
+ public boolean ignoreObjects;
+
+ /**
+ * true if the AnimatedObject is being repositioned in this cycle; otherwise false.
+ */
+ public boolean repositioned;
+
+ /**
+ * true if the AnimatedObject should not have the cel advanced in this loop; otherwise false.
+ */
+ public boolean noAdvance;
+
+ /**
+ * true if the AnimatedObject should not have the loop fixed; otherwise false. Having
+ * the loop fixed means that it will not adjust according to the direction. Set to
+ * true by the fix.loop action command. Set to false by the release.loop action command.
+ */
+ public boolean fixedLoop;
+
+ /**
+ * true if the AnimatedObject did not move in the last animation cycle; otherwise false.
+ */
+ public boolean stopped;
+
+ /**
+ * Miscellaneous motion parameter 1. Used by Wander, MoveTo, and Follow.
+ */
+ public short motionParam1;
+
+ /**
+ * Miscellaneous motion parameter 2.
+ */
+ public short motionParam2;
+
+ /**
+ * Miscellaneous motion parameter 3.
+ */
+ public short motionParam3;
+
+ /**
+ * Miscellaneous motion parameter 4.
+ */
+ public short motionParam4;
+
+ /**
+ * The GameState class holds all of the data and state for the Game currently
+ * being run by the interpreter.
+ */
+ private GameState state;
+
+ /**
+ * Constructor for AnimatedObject.
+ *
+ * @param state
+ * @param objectNum
+ */
+ public AnimatedObject(GameState state, int objectNum) {
+ this.state = state;
+ this.objectNumber = (byte)objectNum;
+ this.saveArea = new SaveArea();
+ reset(true);
+ }
+
+ /**
+ * Resets the AnimatedObject back to its initial state.
+ */
+ public void reset() {
+ reset(false);
+ }
+
+ /**
+ * Resets the AnimatedObject back to its initial state.
+ *
+ * @param fullReset true if it should be a full reset; otherwise false.
+ */
+ public void reset(boolean fullReset) {
+ animated = false;
+ drawn = false;
+ update = true;
+
+ previousCel = null;
+ saveArea.visBackPixels = null;
+ saveArea.priBackPixels = null;
+
+ stepSize = 1;
+ cycleTime = 1;
+ cycleTimeCount = 1;
+ stepTime = 1;
+ stepTimeCount = 1;
+
+ // A full reset is to go back to the initial state, whereas a normal reset is
+ // simply for changing rooms.
+ if (fullReset) {
+ this.blocked = false;
+ this.controlBoxColour = 0;
+ this.currentCel = 0;
+ this.currentLoop = 0;
+ this.currentView = 0;
+ this.cycle = false;
+ this.cycleType = CycleType.NORMAL;
+ this.direction = 0;
+ this.fixedLoop = false;
+ this.fixedPriority = false;
+ this.ignoreBlocks = false;
+ this.ignoreHorizon = false;
+ this.ignoreObjects = false;
+ this.motionParam1 = 0;
+ this.motionParam2 = 0;
+ this.motionParam3 = 0;
+ this.motionParam4 = 0;
+ this.motionType = MotionType.NORMAL;
+ this.noAdvance = false;
+ this.prevX = this.x = 0;
+ this.prevY = this.y = 0;
+ this.priority = 0;
+ this.repositioned = false;
+ this.stayOnLand = false;
+ this.stayOnWater = false;
+ this.stopped = false;
+ }
+ }
+
+ /**
+ * Updates the AnimatedObject's Direction based on its current MotionType.
+ */
+ public void updateDirection() {
+ if (animated && update && drawn && (stepTimeCount == 1)) {
+ switch (motionType) {
+ case WANDER:
+ wander();
+ break;
+
+ case FOLLOW:
+ follow();
+ break;
+
+ case MOVE_TO:
+ moveTo();
+ break;
+
+ case NORMAL:
+ // Nothing to do.
+ break;
+ }
+
+ // If no blocks are in effect, clear the 'blocked' flag. Otherwise,
+ // if object must observe blocks, check for blocking.
+ if (!state.blocking) {
+ blocked = false;
+ }
+ else if (!ignoreBlocks && (direction != 0)) {
+ checkBlock();
+ }
+ }
+ }
+
+ /**
+ * Starts the Wander motion for this AnimatedObject.
+ */
+ public void startWander() {
+ if (this == state.ego) {
+ state.userControl = false;
+ }
+ this.motionType = MotionType.WANDER;
+ this.update = true;
+ }
+
+ /**
+ * If the AnimatedObject has stopped, but the motion type is Wander, then this
+ * method picks a random direction and distance.
+ *
+ * Note: motionParam1 is used to track the distance.
+ */
+ private void wander() {
+ // Wander uses general purpose motion parameter 1 for the distance.
+ if ((motionParam1-- == 0) || stopped) {
+ direction = (byte)state.random.nextInt(9);
+
+ // If the AnimatedObject is ego, then set the EGODIR var.
+ if (objectNumber == 0) {
+ state.vars[Defines.EGODIR] = direction;
+ }
+
+ motionParam1 = (short)((state.random.nextInt((Defines.MAXDIST - Defines.MINDIST)) + Defines.MINDIST) & 0xFF);
+ }
+ }
+
+ /**
+ * New Direction matrix to support the MoveDirection method.
+ */
+ private static final byte[][] newdir = { {8, 1, 2}, {7, 0, 3}, {6, 5, 4} };
+
+ /**
+ * Return the direction from (oldx, oldy) to (newx, newy). If the object is within
+ * 'delta' of the position in both directions, return 0
+ *
+ * @param oldx
+ * @param oldy
+ * @param newx
+ * @param newy
+ * @param delta
+ *
+ * @return
+ */
+ private byte moveDirection(short oldx, short oldy, short newx, short newy, short delta) {
+ return (newdir[directionIndex(newy - oldy, delta)][directionIndex(newx - oldx, delta)]);
+ }
+
+ /**
+ * Return 0, 1, or 2 depending on whether the difference between coords, d,
+ * indicates that the coordinate should decrease, stay the same, or increase.
+ * The return value is used as one of the indeces into 'newdir' above.
+ *
+ * @param d
+ * @param delta
+ *
+ * @return 0, 1, or 2, as described in the summary above.
+ */
+ private byte directionIndex(int d, short delta) {
+ byte index = 0;
+
+ if (d <= -delta) {
+ index = 0;
+ }
+ else if (d >= delta) {
+ index = 2;
+ }
+ else {
+ index = 1;
+ }
+
+ return index;
+ }
+
+ /**
+ * Move this AnimatedObject towards ego.
+ *
+ * motionParam1 (endDist): Distance from ego which is considered to be completion of the motion.
+ * motionParam2 (endFlag): Flag to set on completion of the motion
+ * motionParam3 (randDist): Distance to move in current direction (for random search)
+ */
+ private void follow() {
+ int maxDist = 0;
+
+ // Get coordinates of center of object's & ego's bases.
+ short ecx = (short)(state.ego.x + (state.ego.xSize() / 2));
+ short ocx = (short)(this.x + (this.xSize() / 2));
+
+ // Get direction from object's center to ego's center.
+ byte dir = moveDirection(ocx, this.y, ecx, state.ego.y, motionParam1);
+
+ // If the direction is zero, the object and ego have collided, so signal completion.
+ if (dir == 0) {
+ this.direction = 0;
+ this.motionType = MotionType.NORMAL;
+ this.state.flags[this.motionParam2] = true;
+ return;
+ }
+
+ // If the object has not moved since last time, assume it is blocked and
+ // move in a random direction for a random distance no greater than the
+ // distance between the object and ego
+
+ // NOTE: randDist = -1 indicates that this is initialization, and thus
+ // we don't care about the previous position
+ if (this.motionParam3 == -1) {
+ this.motionParam3 = 0;
+ }
+ else if (this.stopped) {
+ // Make sure that the object goes in some direction.
+ direction = (byte)(state.random.nextInt(8) + 1);
+
+ // Average the x and y distances to the object for movement limit.
+ maxDist = (Math.abs(ocx - ecx) + Math.abs(this.y - state.ego.y)) / 2 + 1;
+
+ // Make sure that the distance is at least the object stepsize.
+ if (maxDist <= this.stepSize) {
+ this.motionParam3 = (short)this.stepSize;
+ }
+ else {
+ this.motionParam3 = (short)(state.random.nextInt((maxDist - this.stepSize)) + this.stepSize);
+ }
+
+ return;
+ }
+
+ // If 'randDist' is non-zero, keep moving the object in the current direction.
+ if (this.motionParam3 != 0) {
+ if ((this.motionParam3 -= this.stepSize) < 0) {
+ // Down with the random movement.
+ this.motionParam3 = 0;
+ }
+ return;
+ }
+
+ // Otherwise, just move the object towards ego. Whew...
+ this.direction = dir;
+ }
+
+ /**
+ * Starts a Follow ego motion for this AnimatedObject.
+ *
+ * @param dist Distance from ego which is considered to be completion of the motion.
+ * @param completionFlag The number of the flag to set when the motion is completed.
+ */
+ public void startFollowEgo(int dist, int completionFlag) {
+ this.motionType = MotionType.FOLLOW;
+
+ // Distance from ego which is considered to be completion of the motion is the larger of
+ // the object's StepSize and the dist parameter.
+ this.motionParam1 = (short)(dist > this.stepSize ? dist : this.stepSize);
+ this.motionParam2 = (short)completionFlag;
+ this.motionParam3 = -1; // 'follow' routine expects this.
+ state.flags[completionFlag] = false; // Flag to set at completion.
+ this.update = true;
+ }
+
+ /**
+ * Move this AnimatedObject toward the target (xt, yt) position, as defined below:
+ *
+ * motionParam1 (xt): Target X coordinate.
+ * motionParam2 (yt): Target Y coordinate.
+ * motionParam3 (oldStep): Old stepsize for this AnimatedObject.
+ * motionParam4 (endFlag): Flag to set when this AnimatedObject reaches the target position.
+ */
+ public void moveTo() {
+ // Get the direction to move.
+ this.direction = moveDirection(this.x, this.y, this.motionParam1, this.motionParam2, (short)this.stepSize);
+
+ // If this AnimatedObject is ego, set var[EGODIR]
+ if (this.objectNumber == 0) {
+ this.state.vars[Defines.EGODIR] = this.direction;
+ }
+
+ // If 0, signal completion.
+ if (this.direction == 0) {
+ endMoveObj();
+ }
+ }
+
+ /**
+ * Starts the MoveTo motion for this AnimatedObject.
+ *
+ * @param x The x position to move to.
+ * @param y The y position to move to.
+ * @param stepSize The step size to use for the motion. If 0, then the current StepSize value for this AnimatedObject is used.
+ * @param completionFlag The flag number to set when the motion has completed.
+ */
+ public void startMoveObj(int x, int y, int stepSize, int completionFlag) {
+ this.motionType = MotionType.MOVE_TO;
+ this.motionParam1 = (short)x;
+ this.motionParam2 = (short)y;
+ this.motionParam3 = (short)this.stepSize;
+ if (stepSize != 0) {
+ this.stepSize = stepSize;
+ }
+ this.motionParam4 = (short)completionFlag;
+ state.flags[completionFlag] = false;
+ this.update = true;
+ if (this == state.ego) {
+ state.userControl = false;
+ }
+ this.moveTo();
+ }
+
+ /**
+ * Ends the MoveTo motion for this AnimatedObject.
+ */
+ private void endMoveObj() {
+ // Restore old step size.
+ this.stepSize = this.motionParam3;
+
+ // Set flag indicating completion.
+ this.state.flags[this.motionParam4] = true;
+
+ // Set it back to normal motion.
+ this.motionType = MotionType.NORMAL;
+
+ // If this AnimatedObject is ego, then give back user control.
+ if (this.objectNumber == 0) {
+ state.userControl = true;
+ state.vars[Defines.EGODIR] = 0;
+ }
+ }
+
+ /**
+ * A block is in effect and the object must observe blocks. Check to see
+ * if the object can move in its current direction.
+ */
+ private void checkBlock() {
+ boolean objInBlock;
+ short ox, oy;
+
+ // Get obj coord into temp vars and determine if the object is
+ // currently within the block.
+ ox = this.x;
+ oy = this.y;
+
+ objInBlock = inBlock(ox, oy);
+
+ // Get object coordinate after moving.
+ switch (this.direction) {
+ case 1:
+ oy -= this.stepSize;
+ break;
+
+ case 2:
+ ox += this.stepSize;
+ oy -= this.stepSize;
+ break;
+
+ case 3:
+ ox += this.stepSize;
+ break;
+
+ case 4:
+ ox += this.stepSize;
+ oy += this.stepSize;
+ break;
+
+ case 5:
+ oy += this.stepSize;
+ break;
+
+ case 6:
+ ox -= this.stepSize;
+ oy += this.stepSize;
+ break;
+
+ case 7:
+ ox -= this.stepSize;
+ break;
+
+ case 8:
+ ox -= this.stepSize;
+ oy -= this.stepSize;
+ break;
+ }
+
+ // If moving the object will not change its 'in block' status, let it move.
+ if (objInBlock == inBlock(ox, oy)) {
+ this.blocked = false;
+ }
+ else {
+ this.blocked = true;
+ this.direction = 0;
+
+ // When Ego is the blocked object also set ego's direction to zero.
+ if (this.objectNumber == 0) {
+ state.vars[Defines.EGODIR] = 0;
+ }
+ }
+ }
+
+ /**
+ * Tests if the currently active block contains the given X/Y position. Ths method should
+ * not be called unless a block has been set.
+ *
+ * @param x The X position to test.
+ * @param y The Y position to test.
+ *
+ * @return
+ */
+ private boolean inBlock(short x, short y) {
+ return (x > state.blockUpperLeftX && x < state.blockLowerRightX && y > state.blockUpperLeftY && y < state.blockLowerRightY);
+ }
+
+ private static short[] xs = { 0, 0, 1, 1, 1, 0, -1, -1, -1 };
+ private static short[] ys = { 0, -1, -1, 0, 1, 1, 1, 0, -1 };
+
+ /**
+ * Updates this AnimatedObject's position on the screen according to its current state.
+ */
+ public void updatePosition() {
+ if (animated && update && drawn) {
+ // Decrement the move clock for this object. Don't move the object unless
+ // the clock has reached 0.
+ if ((stepTimeCount != 0) && (--stepTimeCount != 0)) return;
+
+ // Reset the move clock.
+ stepTimeCount = stepTime;
+
+ // Clear border collision flag.
+ byte border = 0;
+
+ short ox = this.x;
+ short px = this.x;
+ short oy = this.y;
+ short py = this.y;
+ byte od = 0;
+ short os = 0;
+
+ // If object has not been repositioned, move it.
+ if (!this.repositioned) {
+ od = this.direction;
+ os = (short)this.stepSize;
+ ox += (short)(xs[od] * os);
+ oy += (short)(ys[od] * os);
+ }
+
+ // Check for object border collision.
+ if (ox < Defines.MINX) {
+ ox = Defines.MINX;
+ border = Defines.LEFT;
+ }
+ else if (ox + this.xSize() > Defines.MAXX + 1) {
+ ox = (short)(Defines.MAXX + 1 - this.xSize());
+ border = Defines.RIGHT;
+ }
+ if (oy - this.ySize() < Defines.MINY - 1) {
+ oy = (short)(Defines.MINY - 1 + this.ySize());
+ border = Defines.TOP;
+ }
+ else if (oy > Defines.MAXY) {
+ oy = Defines.MAXY;
+ border = Defines.BOTTOM;
+ }
+ else if (!ignoreHorizon && (oy <= state.horizon)) {
+ oy = (short)(state.horizon + 1);
+ border = Defines.TOP;
+ }
+
+ // Update X and Y to the new position.
+ this.x = ox;
+ this.y = oy;
+
+ // If object can't be in this position, then move back to previous
+ // position and clear the border collision flag
+ if (collide() || !canBeHere()) {
+ this.x = px;
+ this.y = py;
+ border = 0;
+
+ // Make sure that this position is OK
+ findPosition();
+ }
+
+ // If the object hit the border, set the appropriate flags.
+ if (border > 0) {
+ if (this.objectNumber == 0) {
+ state.vars[Defines.EGOEDGE] = border;
+ }
+ else {
+ state.vars[Defines.OBJHIT] = this.objectNumber;
+ state.vars[Defines.OBJEDGE] = border;
+ }
+
+ // If the object was on a 'moveobj', set the move as finished.
+ if (this.motionType == MotionType.MOVE_TO) {
+ endMoveObj();
+ }
+ }
+
+ // If object was not to be repositioned, it can be repositioned from now on.
+ this.repositioned = false;
+ }
+ }
+
+ /**
+ * Return true if the object's position puts it on the screen; false otherwise.
+ *
+ * @return true if the object's position puts it on the screen; false otherwise.
+ */
+ private boolean goodPosition() {
+ return ((this.x >= Defines.MINX) && ((this.x + this.xSize()) <= Defines.MAXX + 1) &&
+ ((this.y - this.ySize()) >= Defines.MINY - 1) && (this.y <= Defines.MAXY) &&
+ (this.ignoreHorizon || this.y > state.horizon));
+ }
+
+ /**
+ * Find a position for this AnimatedObject where it does not collide with any
+ * unappropriate objects or priority regions. If the object can't be in
+ * its current position, then start scanning in a spiral pattern for a position
+ * at which it can be placed.
+ */
+ public void findPosition() {
+ // Place Y below horizon if it is above it and is not ignoring the horizon.
+ if ((this.y <= state.horizon) && !this.ignoreHorizon) {
+ this.y = (short)(state.horizon + 1);
+ }
+
+ // If current position is OK, return.
+ if (goodPosition() && !collide() && canBeHere()) {
+ return;
+ }
+
+ // Start scan.
+ int legLen = 1, legDir = 0, legCnt = 1;
+
+ while (!goodPosition() || collide() || !canBeHere()) {
+ switch (legDir) {
+ case 0: // Move left.
+ --this.x;
+
+ if (--legCnt == 0)
+ {
+ legDir = 1;
+ legCnt = legLen;
+ }
+ break;
+
+ case 1: // Move down.
+ ++this.y;
+
+ if (--legCnt == 0)
+ {
+ legDir = 2;
+ legCnt = ++legLen;
+ }
+ break;
+
+ case 2: // Move right.
+ ++this.x;
+
+ if (--legCnt == 0)
+ {
+ legDir = 3;
+ legCnt = legLen;
+ }
+ break;
+
+ case 3: // Move up.
+ --this.y;
+
+ if (--legCnt == 0)
+ {
+ legDir = 0;
+ legCnt = ++legLen;
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Checks if this AnimatedObject has collided with another AnimatedObject.
+ *
+ * @return true if collided with another AnimatedObject; otherwise false.
+ */
+ private boolean collide() {
+ // If AnimatedObject is ignoring objects this return false.
+ if (this.ignoreObjects) {
+ return false;
+ }
+
+ for (AnimatedObject otherObj : state.animatedObjects) {
+ // Collision with another object if:
+ // - other object is animated and drawn
+ // - other object is not ignoring objects
+ // - other object is not this object
+ // - the two objects have overlapping baselines
+ if (otherObj.animated && otherObj.drawn &&
+ !otherObj.ignoreObjects &&
+ (this.objectNumber != otherObj.objectNumber) &&
+ (this.x + this.xSize() >= otherObj.x) &&
+ (this.x <= otherObj.x + otherObj.xSize()))
+
+ // At this point, the two objects have overlapping
+ // x coordinates. A collision has occurred if they have
+ // the same y coordinate or if the object in question has
+ // moved across the other object in the last animation cycle
+ if ((this.y == otherObj.y) ||
+ (this.y > otherObj.y && this.prevY < otherObj.prevY) ||
+ (this.y < otherObj.y && this.prevY > otherObj.prevY)) {
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * For the given y value, calculates what the priority value should be.
+ *
+ * @param y
+ *
+ * @return
+ */
+ private byte calculatePriority(int y) {
+ return (byte)(y < state.priorityBase ? Defines.BACK_MOST_PRIORITY : (byte)(((y - state.priorityBase) / ((168.0 - state.priorityBase) / 10.0f)) + 5));
+ }
+
+ /**
+ * Return the effective Y for this Animated Object, which is Y if the priority is not fixed, or if it
+ * is fixed then is the value corresponding to the start of the fixed priority band.
+ *
+ */
+ private short effectiveY() {
+ // IMPORTANT: When in fixed priority mode, it uses the "top" of the priority band, not the bottom, i.e. the "start" is the top.
+ return (fixedPriority ? (short)(state.priorityBase + Math.ceil(((168.0 - state.priorityBase) / 10.0f) * (priority - Defines.BACK_MOST_PRIORITY - 1))) : y);
+ }
+
+ /**
+ * Checks if this AnimatedObject can be in its current position according to
+ * the control lines. Normally this method would be invoked immediately after
+ * setting its position to a newly calculated position.
+ *
+ * There are a number of side effects to calling this method, and in fact
+ * it is responsible for performing these updates:
+ *
+ * - It sets the priority value for the current Y position.
+ * - It sets the on.water flag, if applicable.
+ * - It sets the hit.special flag, if applicable.
+ *
+ * @return true if it can be in the current position; otherwise false.
+ */
+ private boolean canBeHere() {
+ boolean canBeHere = true;
+ boolean entirelyOnWater = false;
+ boolean hitSpecial = false;
+
+ // If the priority is not fixed, calculate the priority based on current Y position.
+ if (!this.fixedPriority) {
+ // NOTE: The following table only applies to games that don't support the ability to change the PriorityBase.
+ // Priority Band Y range
+ // ------------------------
+ // 4 -
+ // 5 48 - 59
+ // 6 60 - 71
+ // 7 72 - 83
+ // 8 84 - 95
+ // 9 96 - 107
+ // 10 108 - 119
+ // 11 120 - 131
+ // 12 132 - 143
+ // 13 144 - 155
+ // 14 156 - 167
+ // 15 168
+ // ------------------------
+ this.priority = calculatePriority(this.y);
+ }
+
+ // Priority 15 skips the whole base line testing. None of the control lines
+ // have any affect.
+ if (this.priority != 15) {
+ // Start by assuming we're on water. Will be set false if it turns out we're not.
+ entirelyOnWater = true;
+
+ // Loop over the priority screen pixels for the area covered by this
+ // object's base line.
+ int startPixelPos = (y * 160) + x;
+ int endPixelPos = startPixelPos + xSize();
+
+ for (int pixelPos = startPixelPos; pixelPos < endPixelPos; pixelPos++) {
+ // Get the priority screen priority value for this pixel of the base line.
+ int priority = state.controlPixels[pixelPos];
+
+ if (priority != 3) {
+ // This pixel is not water (i.e. not 3), so it can't be entirely on water.
+ entirelyOnWater = false;
+
+ if (priority == 0) {
+ // Permanent block.
+ canBeHere = false;
+ break;
+ }
+ else if (priority == 1) {
+ // Blocks if the AnimatedObject isn't ignoring blocks.
+ if (!ignoreBlocks) {
+ canBeHere = false;
+ break;
+ }
+ }
+ else if (priority == 2) {
+ hitSpecial = true;
+ }
+ }
+ }
+
+ if (entirelyOnWater) {
+ if (this.stayOnLand) {
+ // Must not be entirely on water, so can't be here.
+ canBeHere = false;
+ }
+ }
+ else {
+ if (this.stayOnWater) {
+ canBeHere = false;
+ }
+ }
+ }
+
+ // If the object is ego then we need to determine the on.water and hit.special flag values.
+ if (this.objectNumber == 0) {
+ state.flags[Defines.ONWATER] = entirelyOnWater;
+ state.flags[Defines.HITSPEC] = hitSpecial;
+ }
+
+ return canBeHere;
+ }
+
+ // Object views -- Same, Right, Left, Front, Back.
+ private static final byte S = 4;
+ private static final byte R = 0;
+ private static final byte L = 1;
+ private static final byte F = 2;
+ private static final byte B = 3;
+ private static byte[] twoLoop = { S, S, R, R, R, S, L, L, L };
+ private static byte[] fourLoop = { S, B, R, R, R, F, L, L, L };
+
+ /**
+ * Updates the loop and cel numbers based on the AnimatedObjects current state.
+ */
+ public void updateLoopAndCel() {
+ byte newLoop = 0;
+
+ if (animated && update && drawn) {
+ // Get the appropriate loop based on the current direction.
+ newLoop = S;
+
+ if (!fixedLoop) {
+ if (numberOfLoops() == 2 || numberOfLoops() == 3) {
+ newLoop = twoLoop[direction];
+ }
+ else if (numberOfLoops() == 4) {
+ newLoop = fourLoop[direction];
+ }
+ else if ((numberOfLoops() > 4) && (state.gameId.equals("KQ4"))) {
+ // Main Ego View (0) in KQ4 has 5 loops, but is expected to automatically change
+ // loop in sync with the Direction, in the same way as if it had only 4 loops.
+ newLoop = fourLoop[direction];
+ }
+ }
+
+ // If the object is to move in this cycle and the loop has changed, point to the new loop.
+ if ((stepTimeCount == 1) && (newLoop != S) && (currentLoop != newLoop)) {
+ setLoop(newLoop);
+ }
+
+ // If it is time to cycle the object, advance it's cel.
+ if (cycle && (cycleTimeCount > 0) && (--cycleTimeCount == 0)) {
+ advanceCel();
+
+ cycleTimeCount = cycleTime;
+ }
+ }
+ }
+
+ /**
+ * Determine which cel of an object to display next.
+ */
+ public void advanceCel() {
+ int theCel;
+ int lastCel;
+
+ if (noAdvance) {
+ noAdvance = false;
+ return;
+ }
+
+ // Advance to the next cel in the loop.
+ theCel = currentCel;
+ lastCel = (numberOfCels() - 1);
+
+ switch (cycleType) {
+ case NORMAL:
+ // Move to the next sequential cel.
+ if (++theCel > lastCel) {
+ theCel = 0;
+ }
+ break;
+
+ case END_LOOP:
+ // Advance to the end of the loop, set flag in parms[0] when done
+ if (theCel >= lastCel || ++theCel == lastCel) {
+ state.flags[motionParam1] = true;
+ cycle = false;
+ direction = 0;
+ cycleType = CycleType.NORMAL;
+ }
+ break;
+
+ case REVERSE_LOOP:
+ // Move backwards, celwise, until beginning of loop, then set flag.
+ if (theCel == 0 || --theCel == 0) {
+ state.flags[motionParam1] = true;
+ cycle = false;
+ direction = 0;
+ cycleType = CycleType.NORMAL;
+ }
+ break;
+
+ case REVERSE:
+ // Cycle continually, but from end of loop to beginning.
+ if (theCel > 0) {
+ --theCel;
+ }
+ else {
+ theCel = lastCel;
+ }
+ break;
+ }
+
+ // Get pointer to the new cel and set cel dimensions.
+ setCel(theCel);
+ }
+
+ /**
+ * Adds this AnimatedObject as a permanent part of the current picture. If the priority parameter
+ * is 0, the object's priority is that of the priority band in which it is placed; otherwise it
+ * will be set to the specified priority value. If the controlBoxColour parameter is below 4,
+ * then a control line box is added to the control screen of the specified control colour value,
+ * which extends from the object's baseline to the bottom of the next lowest priority band. If
+ * this control box priority is set to 0, then obviously this would prevent animated objects from
+ * walking through it. The other 3 control colours have their normal behaviours as well. The
+ * add.to.pic objects ignore all control lines, all base lines of other objects, and the "block"
+ * if one is active... i.e. it can go anywhere in the picture. Once added, it is not animated
+ * and cannot be erased ecept by drawing something over it. It effectively becomes part of the
+ * picture.
+ *
+ * @param viewNum
+ * @param loopNum
+ * @param celNum
+ * @param x
+ * @param y
+ * @param priority
+ * @param controlBoxColour
+ * @param pixels
+ */
+ public void addToPicture(int viewNum, int loopNum, int celNum, int x, int y, int priority, int controlBoxColour, short[] pixels) {
+ // Add the add.to.pic details to the script event buffer.
+ state.scriptBuffer.addScript(ScriptBuffer.ScriptBufferEventType.ADD_TO_PIC, 0, new byte[] {
+ (byte)viewNum, (byte)loopNum, (byte)celNum, (byte)x, (byte)y, (byte)(priority | (controlBoxColour << 4))
+ });
+
+ // Set the view, loop, and cel to those specified.
+ setView(viewNum);
+ setLoop(loopNum);
+ setCel(celNum);
+
+ // Set PreviousCel to current Cel for Show call.
+ this.previousCel = this.cel();
+
+ // Place the add.to.pic at the specified position. This may not be fully within the
+ // screen bounds, so a call below to FindPosition is made to resolve this.
+ this.x = this.prevX = (short)x;
+ this.y = this.prevY = (short)y;
+
+ // In order to make use of FindPosition, we set these flags to disable certain parts
+ // of the FindPosition functionality that don't apply to add.to.pic objects.
+ this.ignoreHorizon = true;
+ this.fixedPriority = true;
+ this.ignoreObjects = true;
+
+ // And we set the priority temporarily to 15 so that when FindPosition is doing its thing,
+ // the control lines will be ignored, as they have no effect on add.to.pic objects.
+ this.priority = 15;
+
+ // Now we call FindPosition to adjust the object's position if it has been placed either
+ // partially or fully outside of the picture area.
+ findPosition();
+
+ // Having checked and (if appropriate) adjusted the position, we can now work out what the
+ // object priority should be.
+ if (priority == 0) {
+ // If the specified priority is 0, it means that the priority should be calculated
+ // from the object's Y position as would normally happen if its priority is not fixed.
+ this.priority = calculatePriority(this.y);
+ }
+ else {
+ // Otherwise it will be set to the specified value.
+ this.priority = (byte)priority;
+ }
+
+ this.controlBoxColour = (byte)controlBoxColour;
+
+ // Draw permanently to the CurrentPicture, including the control box.
+ draw(state.currentPicture);
+
+ // Restore backgrounds, add add.to.pic to VisualPixels, then redraw AnimatedObjects and show updated area.
+ state.restoreBackgrounds();
+ draw();
+ state.drawObjects();
+ show(pixels);
+ }
+
+ /**
+ * Set the Cel of this AnimatedObject to the given cel number.
+ *
+ * @param celNum The cel number within the current Loop to set the Cel to.
+ */
+ public void setCel(int celNum) {
+ // Set the cel number.
+ this.currentCel = celNum;
+
+ // The border collision can only be performed if a valid combination of loops and cels has been set.
+ if ((this.currentLoop < this.numberOfLoops()) && (this.currentCel < this.numberOfCels())) {
+ // Make sure that the new cel size doesn't cause a border collision.
+ if (this.x + this.xSize() > Defines.MAXX + 1) {
+ // Don't let the object move.
+ this.repositioned = true;
+ this.x = (short)(Defines.MAXX - this.xSize());
+ }
+
+ if (this.y - this.ySize() < Defines.MINY - 1) {
+ this.repositioned = true;
+ this.y = (short)(Defines.MINY - 1 + this.ySize());
+
+ if (this.y <= state.horizon && !this.ignoreHorizon) {
+ this.y = (short)(state.horizon + 1);
+ }
+ }
+ }
+ }
+
+ /**
+ * Set the loop of this AnimatedObject to the given loop number.
+ *
+ * @param loopNum The loop number within the current View to set the Loop to.
+ */
+ public void setLoop(int loopNum) {
+ this.currentLoop = loopNum;
+
+ // If the current cel # is greater than the cel count for this loop, set
+ // it to 0, otherwise leave it alone. Sometimes the loop number is set before
+ // the associated view number is set. We allow for this in the check below.
+ if ((this.currentLoop >= this.numberOfLoops()) || (this.currentCel >= this.numberOfCels())) {
+ this.currentCel = 0;
+ }
+
+ this.setCel(this.currentCel);
+ }
+
+ /**
+ * Set the number of the View for this AnimatedObject to use.
+ *
+ * @param viewNum The number of the View for this AnimatedObject to use.
+ */
+ public void setView(int viewNum) {
+ this.currentView = viewNum;
+
+ // If the current loop is greater than the number of loops for the view,
+ // set the loop number to 0. Otherwise, leave it alone.
+ setLoop(currentLoop >= numberOfLoops()? 0 : currentLoop);
+ }
+
+ /**
+ * Performs an animate.obj on this AnimatedObject.
+ */
+ public void animate() {
+ if (!animated) {
+ // Most flags are reset to false.
+ this.ignoreBlocks = false;
+ this.fixedPriority = false;
+ this.ignoreHorizon = false;
+ this.cycle = false;
+ this.blocked = false;
+ this.stayOnLand = false;
+ this.stayOnWater = false;
+ this.ignoreObjects = false;
+ this.repositioned = false;
+ this.noAdvance = false;
+ this.fixedLoop = false;
+ this.stopped = false;
+
+ // But these ones are specifying set to true.
+ this.animated = true;
+ this.update = true;
+ this.cycle = true;
+
+ this.motionType = MotionType.NORMAL;
+ this.cycleType = CycleType.NORMAL;
+ this.direction = 0;
+ }
+ }
+
+ /**
+ * Repositions the object by the deltaX and deltaY values.
+ *
+ * @param deltaX Delta for the X position (signed, where negative is to the left)
+ * @param deltaY Delta for the Y position (signed, where negative is to the top)
+ */
+ public void reposition(int deltaX, int deltaY) {
+ this.repositioned = true;
+
+ if ((deltaX < 0) && (this.x < -deltaX)) {
+ this.x = 0;
+ }
+ else {
+ this.x = (short)(this.x + deltaX);
+ }
+
+ if ((deltaY < 0) && (this.y < -deltaY)) {
+ this.y = 0;
+ }
+ else {
+ this.y = (short)(this.y + deltaY);
+ }
+
+ // Make sure that this position is OK
+ findPosition();
+ }
+
+ /**
+ * Calculates the distance between this AnimatedObject and the given AnimatedObject.
+ *
+ * @param aniObj The AnimatedObject to calculate the distance to.
+ *
+ * @return
+ */
+ public int distance(AnimatedObject aniObj) {
+ if (!this.drawn || !aniObj.drawn) {
+ return Defines.MAXVAR;
+ }
+ else {
+ int dist = Math.abs((this.x + this.xSize() / 2) - (aniObj.x + aniObj.xSize() / 2)) + Math.abs(this.y - aniObj.y);
+ return ((dist > 254) ? 254 : dist);
+ }
+ }
+
+ /**
+ * Draws this AnimatedObject to the pixel arrays of the given Picture. This is intended for use by
+ * add.to.pic objects, which is a specialist static type of AnimatedObject that becomes a permanent
+ * part of the Picture.
+ *
+ * @param picture
+ */
+ public void draw(Picture picture) {
+ Cel cel = cel();
+ int cellWidth = cel.getWidth();
+ int cellHeight = cel.getHeight();
+
+ // The cellPixels array is already in ARGB format.
+ int[] cellPixels = cel.getPixelData();
+
+ // The visualPixels array is already in ARGB format.
+ int[] visualPixels = picture.getVisualPixels();
+
+ // The priorityPixels array is in index format (i.e. 0-15)
+ int[] priorityPixels = picture.getPriorityPixels();
+
+ // Get the transparency colour. We'll use this to ignore pixels this colour.
+ int transparentPixelRGB = this.cel().getTransparentPixel();
+
+ // Calculate starting position within the pixel arrays.
+ int aniObjTop = ((this.y - cellHeight) + 1);
+ int screenPos = (aniObjTop * 160) + this.x;
+ int screenLineAdd = 160 - cellWidth;
+ int cellPos = 0;
+ int cellXAdd = 1;
+ int cellYAdd = 0;;
+
+ // Iterate over each of the pixels and decide if the priority screen allows the pixel
+ // to be drawn or not when adding them in to the VisualPixels and PriorityPixels arrays.
+ for (int y = 0; y < cellHeight; y++, screenPos += screenLineAdd, cellPos += cellYAdd) {
+ for (int x = 0; x < cellWidth; x++, screenPos++, cellPos += cellXAdd) {
+ // Check that the pixel is within the bounds of the AGI picture area.
+ if (((aniObjTop + y) >= 0) && ((aniObjTop + y) < 168) && ((this.x + x) >= 0) && ((this.x + x) < 160)) {
+ // Get the priority colour index for this position from the priority screen.
+ int priorityIndex = priorityPixels[screenPos];
+
+ // If this AnimatedObject's priority is greater or equal to the priority screen value
+ // for this pixel's position, then we'll draw it.
+ if (this.priority >= priorityIndex) {
+ // Get the colour index from the Cell bitmap pixels.
+ int cellPixelRGB = cellPixels[cellPos];
+
+ // If the colourIndex is not the transparent index, then we'll draw the pixel.
+ if (cellPixelRGB != transparentPixelRGB) {
+ visualPixels[screenPos] = cellPixelRGB;
+ // Replace the priority pixel only if the existing one is not a special priority pixel (0, 1, 2)
+ if (priorityIndex > 2) {
+ priorityPixels[screenPos] = this.priority;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Draw the control box.
+ if (controlBoxColour <= 3) {
+ // Calculate the height of the box.
+ int yy = this.y;
+ byte priorityHeight = 0;
+ byte objPriorityForY = calculatePriority(this.y);
+ do {
+ priorityHeight++;
+ if (yy <= 0) break;
+ yy--;
+ }
+ while (calculatePriority(yy) == objPriorityForY);
+ int height = (ySize() > priorityHeight ? priorityHeight : ySize());
+
+ // Draw bottom line.
+ for (int i = 0; i < xSize(); i++) {
+ priorityPixels[(this.y * 160) + this.x + i] = controlBoxColour;
+ }
+
+ if (height > 1) {
+ // Draw both sides.
+ for (int i = 1; i < height; i++) {
+ priorityPixels[((this.y - i) * 160) + this.x] = controlBoxColour;
+ priorityPixels[((this.y - i) * 160) + this.x + xSize() - 1] = controlBoxColour;
+ }
+
+ // Draw top line.
+ for (int i = 1; i < xSize() - 1; i++) {
+ priorityPixels[((this.y - (height - 1)) * 160) + this.x + i] = controlBoxColour;
+ }
+ }
+ }
+ }
+
+ /**
+ * Draws this AnimatedObject to the VisualPixels pixels array.
+ */
+ public void draw() {
+ Cel cel = cel();
+ int cellWidth = cel.getWidth();
+ int cellHeight = cel.getHeight();
+ int[] cellPixels = cel.getPixelData();
+
+ // Get the transparency colour. We'll use this to ignore pixels this colour.
+ int transparentPixelRGB = cel.getTransparentPixel();
+
+ // Calculate starting screen offset. AGI pixels are 2x1 within the picture area.
+ int aniObjTop = ((this.y - cellHeight) + 1);
+ int screenPos = (aniObjTop * 320) + (this.x * 2);
+ int screenLineAdd = 320 - (cellWidth << 1);
+
+ // Calculate starting position within the priority screen.
+ int priorityPos = (aniObjTop * 160) + this.x;
+ int priorityLineAdd = 160 - cellWidth;
+ int cellPos = 0;
+ int cellXAdd = 1;
+ int cellYAdd = 0;
+
+ // Allocate new background pixel array for the current cell size.
+ this.saveArea.visBackPixels = new short[cellWidth][cellHeight];
+ this.saveArea.priBackPixels = new int[cellWidth][cellHeight];
+ this.saveArea.x = this.x;
+ this.saveArea.y = this.y;
+ this.saveArea.width = cellWidth;
+ this.saveArea.height = cellHeight;
+
+ // Iterate over each of the pixels and decide if the priority screen allows the pixel
+ // to be drawn or not. Deliberately tried to avoid multiplication within the loops.
+ for (int y = 0; y < cellHeight; y++, screenPos += screenLineAdd, priorityPos += priorityLineAdd, cellPos += cellYAdd) {
+ for (int x = 0; x < cellWidth; x++, screenPos += 2, priorityPos++, cellPos += cellXAdd) {
+ // Check that the pixel is within the bounds of the AGI picture area.
+ if (((aniObjTop + y) >= 0) && ((aniObjTop + y) < 168) && ((this.x + x) >= 0) && ((this.x + x) < 160)) {
+ // Store the background pixel. Should be the same colour in both pixels.
+ this.saveArea.visBackPixels[x][y] = state.visualPixels[screenPos];
+ this.saveArea.priBackPixels[x][y] = state.priorityPixels[priorityPos];
+
+ // Get the priority colour index for this position from the priority screen.
+ int priorityIndex = state.priorityPixels[priorityPos];
+
+ // If this AnimatedObject's priority is greater or equal to the priority screen value
+ // for this pixel's position, then we'll draw it.
+ if (this.priority >= priorityIndex) {
+ // Get the colour index from the Cell bitmap pixels.
+ int cellPixelRGB = cellPixels[cellPos];
+
+ // If the colourIndex is not the transparent index, then we'll draw the pixel.
+ if (cellPixelRGB != transparentPixelRGB) {
+ // Get the RGB565 value from the AGI Color Palette.
+ short colorRGB565 = EgaPalette.RGB888_TO_RGB565_MAP.get(cellPixelRGB);
+
+ // Draw two pixels (due to AGI picture pixels being 2x1).
+ state.visualPixels[screenPos] = colorRGB565;
+ state.visualPixels[screenPos + 1] = colorRGB565;
+
+ // Priority screen is only stored 160x168 though.
+ state.priorityPixels[priorityPos] = this.priority;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Restores the current background pixels to the previous position of this AnimatedObject.
+ */
+ public void restoreBackPixels() {
+ if ((saveArea.visBackPixels != null) && (saveArea.priBackPixels != null)) {
+ int saveWidth = saveArea.width;
+ int saveHeight = saveArea.height;
+ int aniObjTop = ((saveArea.y - saveHeight) + 1);
+ int screenPos = (aniObjTop * 320) + (saveArea.x * 2);
+ int screenLineAdd = 320 - (saveWidth << 1);
+ int priorityPos = (aniObjTop * 160) + saveArea.x;
+ int priorityLineAdd = 160 - saveWidth;
+
+ for (int y = 0; y < saveHeight; y++, screenPos += screenLineAdd, priorityPos += priorityLineAdd) {
+ for (int x = 0; x < saveWidth; x++, screenPos += 2, priorityPos++) {
+ if (((aniObjTop + y) >= 0) && ((aniObjTop + y) < 168) && ((saveArea.x + x) >= 0) && ((saveArea.x + x) < 160)) {
+ state.visualPixels[screenPos] = saveArea.visBackPixels[x][y];
+ state.visualPixels[screenPos + 1] = saveArea.visBackPixels[x][y];
+ state.priorityPixels[priorityPos] = saveArea.priBackPixels[x][y];
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Shows the AnimatedObject by blitting the bounds of its current and previous cels to the screen
+ * pixels. The include the previous cel so that we pick up the restoration of the save area.
+ *
+ * @param pixels The screen pixels to blit the AnimatedObject to.
+ */
+ public void show(short[] pixels) {
+ // We will only render an AnimatedObject to the screen if the picture is currently visible.
+ if (state.pictureVisible) {
+ // Work out the rectangle that covers the previous and current cells.
+ int prevCelWidth = (this.previousCel != null ? this.previousCel.getWidth() : this.xSize());
+ int prevCelHeight = (this.previousCel != null? this.previousCel.getHeight() : this.ySize());
+ int prevX = (this.previousCel != null ? this.prevX : this.x);
+ int prevY = (this.previousCel != null ? this.prevY : this.y);
+ int leftmostX = Math.min(prevX, this.x);
+ int rightmostX = Math.max(prevX + prevCelWidth, this.x + this.xSize()) - 1;
+ int topmostY = Math.min(prevY - prevCelHeight, this.y - this.ySize()) + 1;
+ int bottommostY = Math.max(prevY, this.y);
+
+ // We no longer need the PreviousCel, so point it at the new one.
+ this.previousCel = this.cel();
+
+ int height = (bottommostY - topmostY) + 1;
+ int width = ((rightmostX - leftmostX) + 1) * 2;
+ int picturePos = (topmostY * 320) + (leftmostX * 2);
+ int pictureLineAdd = 320 - width;
+ int screenPos = picturePos + (state.pictureRow * 8 * 320);
+
+ for (int y = 0; y < height; y++, picturePos += pictureLineAdd, screenPos += pictureLineAdd) {
+ for (int x = 0; x < width; x++, screenPos++, picturePos++) {
+ if (((topmostY + y) >= 0) && ((topmostY + y) < 168) && ((leftmostX + x) >= 0) && ((leftmostX + x) < 320) && (screenPos >= 0) && (screenPos < pixels.length)) {
+ pixels[screenPos] = state.visualPixels[picturePos];
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Used to sort by drawing order when drawing AnimatedObjects to the screen. When
+ * invoked, it compares the other AnimatedObject with this one and says which is in
+ * front and which is behind. Since we want to draw those with lowest priority first,
+ * and if their priority is equal then lowest Y, then this is what determines whether
+ * we return a negative value, equal, or greater.
+ *
+ * @param other The other AnimatedObject to compare this one to.
+ */
+ public int compareTo(AnimatedObject other) {
+ if (this.priority < other.priority) {
+ return -1;
+ }
+ else if (this.priority > other.priority) {
+ return 1;
+ }
+ else {
+ if (this.effectiveY() < other.effectiveY()) {
+ return -1;
+ }
+ else if (this.effectiveY() > other.effectiveY()) {
+ return 1;
+ }
+ else {
+ return 0;
+ }
+ }
+ }
+
+ /**
+ * Gets the core status of the object in the status string format used by the AGI
+ * debug mode.
+ */
+ public String getStatusStr() {
+ return String.format(
+ "Object %d:\nx: %d xsize: %d\ny: %d ysize: %d\npri: %d\nstepsize: %d",
+ objectNumber, x, xSize(), y, ySize(), priority, stepSize);
+ }
+
+ /**
+ * An enum that defines the types of motion that an AnimatedObject can have.
+ */
+ public enum MotionType {
+
+ /**
+ * AnimatedObject is using the normal motion.
+ */
+ NORMAL,
+
+ /**
+ * AnimatedObject randomly moves around the screen.
+ */
+ WANDER,
+
+ /**
+ * AnimatedObject follows another AnimatedObject.
+ */
+ FOLLOW,
+
+ /**
+ * AnimatedObject is moving to a given coordinate.
+ */
+ MOVE_TO
+ }
+
+ /**
+ * An enum that defines the type of cel cycling that an AnimatedObject can have.
+ */
+ public enum CycleType {
+
+ /**
+ * Normal repetitive cycling of the AnimatedObject.
+ */
+ NORMAL,
+
+ /**
+ * Cycle to the end of the loop and then stop.
+ */
+ END_LOOP,
+
+ /**
+ * Cycle in reverse order to the start of the loop and then stop.
+ */
+ REVERSE_LOOP,
+
+ /**
+ * Cycle continually in reverse.
+ */
+ REVERSE
+ }
+}
diff --git a/core/src/main/java/com/agifans/agile/Character.java b/core/src/main/java/com/agifans/agile/Character.java
new file mode 100644
index 0000000..a72e19b
--- /dev/null
+++ b/core/src/main/java/com/agifans/agile/Character.java
@@ -0,0 +1,212 @@
+package com.agifans.agile;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.badlogic.gdx.Input.Keys;
+
+/**
+ * The AGI interpreter uses standard ASCII keycodes. This class is used to map
+ * the libgdx keystrokes to standard ASCII and then to provide constants for use
+ * within the AGILE interpreter. This includes CTRL key combinations that would
+ * result in an ASCI character, e.g. CTRL-I being the same as TAB.
+ *
+ * Many of the libgdx characters result in the keyTyped of the InputAdapter being
+ * invoked, but there are some that do not, such as the CTRL key combinations, but
+ * also ESC. We let keyTyped handle the ones that it can, but for those that it
+ * can't, we provide the mapping so that keyDown can enqueue it instead.
+ */
+public class Character {
+
+ /**
+ * The CTRL modifier key.
+ */
+ private static final int CONTROL_MODIFIER = 0x20000;
+
+ // ASCII characters
+ public static final int CTRL_A = 1;
+ public static final int CTRL_B = 2;
+ public static final int CTRL_C = 3;
+ public static final int CTRL_D = 4;
+ public static final int CTRL_E = 5;
+ public static final int CTRL_F = 6;
+ public static final int CTRL_G = 7;
+ public static final int CTRL_H = 8;
+ public static final int BACKSPACE = 8;
+ public static final int CTRL_I = 9;
+ public static final int TAB = 9;
+ public static final int CTRL_J = 10;
+ public static final int CTRL_ENTER = 10;
+ public static final int CTRL_K = 11;
+ public static final int CTRL_L = 12;
+ public static final int CTRL_M = 13;
+ public static final int ENTER = 13;
+ public static final int CTRL_N = 14;
+ public static final int CTRL_O = 15;
+ public static final int CTRL_P = 16;
+ public static final int CTRL_Q = 17;
+ public static final int CTRL_R = 18;
+ public static final int CTRL_S = 19;
+ public static final int CTRL_T = 20;
+ public static final int CTRL_U = 21;
+ public static final int CTRL_V = 22;
+ public static final int CTRL_W = 23;
+ public static final int CTRL_X = 24;
+ public static final int CTRL_Y = 25;
+ public static final int CTRL_Z = 26;
+
+ public static final int ESC = 27;
+
+ public static final int CTRL_BACK_SLASH = 28;
+ public static final int CTRL_CLOSE_SQUARE_BRACKET = 29;
+ public static final int CTRL_6 = 30;
+ public static final int CTRL_MINUS = 31;
+
+ public static final int SPACE = 32;
+ public static final int EXCLAIMATION_MARK = 33;
+ public static final int DOUBLE_QUOTE = 34;
+ public static final int HASH = 35;
+ public static final int DOLLAR_SIGN = 36;
+ public static final int PERCENTAGE_SIGN = 37;
+ public static final int AMPERSAND = 38;
+ public static final int APOSTROPHE = 39;
+ public static final int OPEN_BACKET = 40;
+ public static final int CLOSE_BRACKET = 41;
+ public static final int ASTERISK = 42;
+ public static final int PLUS_SIGN = 43;
+ public static final int COMMA = 44;
+ public static final int MINUS_SIGN = 45;
+ public static final int PERIOD = 46;
+ public static final int FORWARD_SLASH = 47;
+
+ public static final int NUM_0 = 48;
+ public static final int NUM_1 = 49;
+ public static final int NUM_2 = 50;
+ public static final int NUM_3 = 51;
+ public static final int NUM_4 = 52;
+ public static final int NUM_5 = 53;
+ public static final int NUM_6 = 54;
+ public static final int NUM_7 = 55;
+ public static final int NUM_8 = 56;
+ public static final int NUM_9 = 57;
+
+ public static final int COLON = 58;
+ public static final int SEMI_COLON = 59;
+ public static final int LESS_THAN = 60;
+ public static final int EQUALS = 61;
+ public static final int GREATER_THAN = 62;
+ public static final int QUESTION_MARK = 63;
+ public static final int AT_SIGN = 64;
+
+ public static final int UPPER_A = 65;
+ public static final int UPPER_B = 66;
+ public static final int UPPER_C = 67;
+ public static final int UPPER_D = 68;
+ public static final int UPPER_E = 69;
+ public static final int UPPER_F = 70;
+ public static final int UPPER_G = 71;
+ public static final int UPPER_H = 72;
+ public static final int UPPER_I = 73;
+ public static final int UPPER_J = 74;
+ public static final int UPPER_K = 75;
+ public static final int UPPER_L = 76;
+ public static final int UPPER_M = 77;
+ public static final int UPPER_N = 78;
+ public static final int UPPER_O = 79;
+ public static final int UPPER_P = 80;
+ public static final int UPPER_Q = 81;
+ public static final int UPPER_R = 82;
+ public static final int UPPER_S = 83;
+ public static final int UPPER_T = 84;
+ public static final int UPPER_U = 85;
+ public static final int UPPER_V = 86;
+ public static final int UPPER_W = 87;
+ public static final int UPPER_X = 88;
+ public static final int UPPER_Y = 89;
+ public static final int UPPER_Z = 90;
+
+ public static final int OPEN_SQUARE_BRACKET = 91;
+ public static final int BACK_SLASH = 92;
+ public static final int CLOSE_SQUARE_BRACKET = 93;
+ public static final int CARAT = 94;
+ public static final int UNDERSCORE = 95;
+ public static final int BACK_TICK = 96;
+
+ public static final int LOWER_A = 97;
+ public static final int LOWER_B = 98;
+ public static final int LOWER_C = 99;
+ public static final int LOWER_D = 100;
+ public static final int LOWER_E = 101;
+ public static final int LOWER_F = 102;
+ public static final int LOWER_G = 103;
+ public static final int LOWER_H = 104;
+ public static final int LOWER_I = 105;
+ public static final int LOWER_J = 106;
+ public static final int LOWER_K = 107;
+ public static final int LOWER_L = 108;
+ public static final int LOWER_M = 109;
+ public static final int LOWER_N = 110;
+ public static final int LOWER_O = 111;
+ public static final int LOWER_P = 112;
+ public static final int LOWER_Q = 113;
+ public static final int LOWER_R = 114;
+ public static final int LOWER_S = 115;
+ public static final int LOWER_T = 116;
+ public static final int LOWER_U = 117;
+ public static final int LOWER_V = 118;
+ public static final int LOWER_W = 119;
+ public static final int LOWER_X = 120;
+ public static final int LOWER_Y = 121;
+ public static final int LOWER_Z = 122;
+
+ public static final int OPEN_BRACE = 123;
+ public static final int PIPE = 124;
+ public static final int CLOSE_BRACE = 125;
+ public static final int TILDA = 126;
+
+
+ public static final Map KEYSTROKE_TO_CHAR_MAP = new HashMap<>();
+ static {
+
+ // LibGDX does not translate CTRL combinations into "keyTyped" calls.
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.A, CTRL_A);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.B, CTRL_B);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.C, CTRL_C);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.D, CTRL_D);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.E, CTRL_E);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.F, CTRL_F);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.G, CTRL_G);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.H, CTRL_H);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.I, CTRL_I);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.J, CTRL_J);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.ENTER, CTRL_ENTER);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.K, CTRL_K);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.L, CTRL_L);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.M, CTRL_M);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.N, CTRL_N);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.O, CTRL_O);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.P, CTRL_P);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.Q, CTRL_Q);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.R, CTRL_R);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.S, CTRL_S);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.T, CTRL_T);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.U, CTRL_U);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.V, CTRL_V);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.W, CTRL_W);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.X, CTRL_X);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.Y, CTRL_Y);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.Z, CTRL_Z);
+
+ // ENTER goes through to keyTyped as 0x0A, i.e. LF!! So we map this ourselves to CR.
+ KEYSTROKE_TO_CHAR_MAP.put(Keys.ENTER, ENTER);
+
+ // ESC does not pass through to keyTyped either.
+ KEYSTROKE_TO_CHAR_MAP.put(Keys.ESCAPE, ESC);
+
+
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.BACKSLASH, CTRL_BACK_SLASH);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.LEFT_BRACKET, CTRL_CLOSE_SQUARE_BRACKET);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.NUM_6, CTRL_6);
+ KEYSTROKE_TO_CHAR_MAP.put(CONTROL_MODIFIER + Keys.MINUS, CTRL_MINUS);
+ }
+}
diff --git a/core/src/main/java/com/agifans/agile/Commands.java b/core/src/main/java/com/agifans/agile/Commands.java
new file mode 100644
index 0000000..d5b8586
--- /dev/null
+++ b/core/src/main/java/com/agifans/agile/Commands.java
@@ -0,0 +1,2147 @@
+package com.agifans.agile;
+
+import com.agifans.agile.AnimatedObject.CycleType;
+import com.agifans.agile.AnimatedObject.MotionType;
+import com.agifans.agile.ScriptBuffer.ScriptBufferEvent;
+import com.agifans.agile.agilib.Logic;
+import com.agifans.agile.agilib.Logic.Action;
+import com.agifans.agile.agilib.Logic.Condition;
+import com.agifans.agile.agilib.Logic.GotoAction;
+import com.agifans.agile.agilib.Logic.IfAction;
+import com.agifans.agile.agilib.Picture;
+import com.agifans.agile.agilib.Sound;
+import com.agifans.agile.agilib.View;
+
+/**
+ * Performs the execution of an AGI Logic script.
+ */
+public class Commands {
+
+ /**
+ * The GameState class holds all of the data and state for the Game currently
+ * being run by the interpreter.
+ */
+ private GameState state;
+
+ /**
+ * Holds the data and state for the user input, i.e. keyboard and mouse input.
+ */
+ private UserInput userInput;
+
+ /**
+ * The pixels array for the AGI screen on which the background Picture and
+ * AnimatedObjects will be drawn to.
+ */
+ private short[] pixels;
+
+ /**
+ * Provides methods for drawing text on to the AGI screen.
+ */
+ private TextGraphics textGraphics;
+
+ /**
+ * Responsible for parsing the user input line to match known words
+ */
+ private Parser parser;
+
+ /**
+ * Responsible for displaying the inventory screen.
+ */
+ private Inventory inventory;
+
+ /**
+ * Responsible for displaying the menu system.
+ */
+ private Menu menu;
+
+ /**
+ * Responsible for saving and restoring saved game files.
+ */
+ private SavedGames savedGames;
+
+ /**
+ * Responsible for playing Sound resources.
+ */
+ private SoundPlayer soundPlayer;
+
+ /**
+ * Constructor for Commands.
+ *
+ * @param pixels
+ * @param state
+ * @param userInput
+ * @param textGraphics
+ * @param parser
+ * @param soundPlayer
+ * @param menu
+ */
+ public Commands(short[] pixels, GameState state, UserInput userInput, TextGraphics textGraphics, Parser parser, SoundPlayer soundPlayer, Menu menu) {
+ this.pixels = pixels;
+ this.state = state;
+ this.userInput = userInput;
+ this.textGraphics = textGraphics;
+ this.parser = parser;
+ this.menu = menu;
+ this.inventory = new Inventory(state, userInput, textGraphics, pixels);
+ this.savedGames = new SavedGames(state, userInput, textGraphics, pixels);
+ this.soundPlayer = soundPlayer;
+ }
+
+ /**
+ * Draws the AGI Picture identified by the given picture number.
+ *
+ * @param pictureNum The number of the picture to draw.
+ */
+ private void drawPicture(int pictureNum) {
+ state.scriptBuffer.addScript(ScriptBuffer.ScriptBufferEventType.DRAW_PIC, pictureNum);
+ state.restoreBackgrounds();
+
+ // We create a clone of the Picture so that is drawing state isn't persisted
+ // back to the master list of pictures in the GameState.
+ Picture picture = state.pictures[pictureNum].clone();
+ picture.drawPicture();
+
+ state.currentPicture = picture;
+
+ updatePixelArrays();
+
+ state.drawObjects();
+
+ state.pictureVisible = false;
+ }
+
+ /**
+ * Updates the Visual, Priority and Control pixel arrays with the bitmaps from the
+ * current Picture.
+ */
+ private void updatePixelArrays() {
+ Picture picture = state.currentPicture;
+
+ int[] visualPixels = picture.getVisualPixels();
+
+ // Copy the pixels to our VisualPixels array, doubling each one as we go.
+ for (int i = 0, ii = 0; i < (160 * 168); i++, ii += 2) {
+ // NOTE: Visual pixel array in JAGI is in RGB888 format
+ short rgb565Color = EgaPalette.RGB888_TO_RGB565_MAP.get(visualPixels[i]);
+ state.visualPixels[ii + 0] = rgb565Color;
+ state.visualPixels[ii + 1] = rgb565Color;
+ }
+
+ splitPriorityPixels();
+ }
+
+ /**
+ * Overlays an AGI Picture identified by the given picture number over the current picture.
+ *
+ * @param pictureNum
+ */
+ private void overlayPicture(int pictureNum) {
+ state.scriptBuffer.addScript(ScriptBuffer.ScriptBufferEventType.OVERLAY_PIC, pictureNum);
+ state.restoreBackgrounds();
+
+ // Draw the overlay picture on top of the current picture.
+ Picture overlayPicture = state.pictures[pictureNum];
+ state.currentPicture.overlayPicture(overlayPicture);
+
+ updatePixelArrays();
+
+ state.drawObjects();
+
+ showVisualPixels();
+
+ state.pictureVisible = false;
+ }
+
+ /**
+ * For the current picture, sets the relevant pixels in the PriorityPixels and
+ * ControlPixels arrays in the GameState. It determines the priority information for
+ * pixels that are overdrawn by control lines by the same method used in Sierra's
+ * interpreter. To quote the original AGI specs: "Control pixels still have a visual
+ * priority from 4 to 15. To accomplish this, AGI scans directly down the control
+ * priority until it finds some 'non-control' priority".
+ */
+ private void splitPriorityPixels() {
+ Picture picture = state.currentPicture;
+ int[] priorityPixels = picture.getPriorityPixels();
+
+ for (int x = 0; x < 160; x++) {
+ for (int y = 0; y < 168; y++) {
+ // Shift left 7 + shift level 5 is a trick to avoid multiplying by 160.
+ int index = (y << 7) + (y << 5) + x;
+ int data = priorityPixels[index];
+
+ if (data == 3) {
+ state.priorityPixels[index] = 3;
+ state.controlPixels[index] = data;
+ }
+ else if (data < 3) {
+ state.controlPixels[index] = data;
+
+ int dy = y + 1;
+ boolean priFound = false;
+
+ while (!priFound && (dy < 168)) {
+ data = priorityPixels[(dy << 7) + (dy << 5) + x];
+
+ if (data > 2) {
+ priFound = true;
+ state.priorityPixels[index] = data;
+ }
+ else {
+ dy++;
+ }
+ }
+ }
+ else {
+ state.controlPixels[index] = 4;
+ state.priorityPixels[index] = data;
+ }
+ }
+ }
+ }
+
+ /**
+ * Shows the current priority pixels and control pixels to screen.
+ */
+ public void showPriorityScreen() {
+ short[] backPixels = new short[pixels.length];
+
+ System.arraycopy(pixels, 0, backPixels, 0, pixels.length);
+
+ for (int i = 0, ii = (8 * state.pictureRow) * 320; i < (160 * 168); i++, ii += 2) {
+ int priColorIndex = state.priorityPixels[i];
+ int ctrlColorIndex = state.controlPixels[i];
+ short rgb565Color = EgaPalette.colours[ctrlColorIndex <= 3 ? ctrlColorIndex : priColorIndex];
+ pixels[ii + 0] = rgb565Color;
+ pixels[ii + 1] = rgb565Color;
+ }
+
+ userInput.waitForKey(true);
+
+ System.arraycopy(backPixels, 0, pixels, 0, pixels.length);
+ }
+
+ /**
+ * Blits the current VisualPixels array to the screen pixels array.
+ */
+ private void showVisualPixels() {
+ // Perform the copy to the pixels array of the VisualPixels. This is where the PictureRow comes in to effect.
+ System.arraycopy(state.visualPixels, 0, this.pixels, (8 * state.pictureRow) * 320, state.visualPixels.length);
+ }
+
+ /**
+ * Implements the show.pic command. Blits the current VisualPixels array to the screen pixels
+ * array. If there is an open window, it will be closed by default.
+ */
+ private void showPicture() {
+ showPicture(true);
+ }
+
+ /**
+ * Implements the show.pic command. Blits the current VisualPixels array to the screen pixels
+ * array. If there is an open window, the closeWindow parameter determines when to close the
+ * window.
+ *
+ * @param closeWindow Skips the closing of open windows if set to false.
+ */
+ private void showPicture(boolean closeWindow) {
+ if (closeWindow) {
+ // It is possible to leave the window up from the previous room, so we force a close.
+ state.flags[Defines.LEAVE_WIN] = false;
+ textGraphics.closeWindow(false);
+ }
+
+ // Perform the copy to the pixels array of the VisualPixels
+ showVisualPixels();
+
+ // Remember that the picture is now being displayed to the user.
+ state.pictureVisible = true;
+ }
+
+ /**
+ * Executes the shake.screen command. Implementation is based on the scummvm code.
+ *
+ * @param repeatCount The number of times to do the shake routine.
+ */
+ private void shakeScreen(int repeatCount) {
+ int shakeCount = (repeatCount * 8);
+ short backgroundRGB565 = EgaPalette.colours[0];
+ short[] backPixels = new short[pixels.length];
+
+ System.arraycopy(pixels, 0, backPixels, 0, pixels.length);
+
+ for (int shakeNumber = 0; shakeNumber < shakeCount; shakeNumber++) {
+ if ((shakeNumber & 1) == 1) {
+ System.arraycopy(backPixels, 0, pixels, 0, pixels.length);
+ }
+ else {
+ for (int y = 0, screenPos = 0; y < 200; y++) {
+ for (int x = 0; x < 320; x++, screenPos++) {
+ if ((x < 8) || (y < 4)) {
+ this.pixels[screenPos] = backgroundRGB565;
+ }
+ else {
+ this.pixels[screenPos] = backPixels[screenPos - 1288];
+ }
+ }
+ }
+ }
+ try {
+ Thread.sleep(66);
+ } catch (InterruptedException e) {
+ // Ignore.
+ }
+ }
+
+ System.arraycopy(backPixels, 0, pixels, 0, pixels.length);
+ }
+
+ /**
+ * Replays the events that happened in the ScriptBuffer. This would usually be called
+ * immediately after restoring a saved game file, to do things such as add the add.to.pics,
+ * draw the picture, show the picture, etc.
+ */
+ private void replayScriptEvents() {
+ // Mainly for the AddToPicture method, since that adds script events if active.
+ state.scriptBuffer.scriptOff();
+
+ for (ScriptBufferEvent scriptBufferEvent : state.scriptBuffer.events) {
+ switch (scriptBufferEvent.type) {
+ case ADD_TO_PIC:
+ {
+ AnimatedObject picObj = new AnimatedObject(state, -1);
+ picObj.addToPicture(
+ (scriptBufferEvent.data[0] & 0xFF),
+ (scriptBufferEvent.data[1] & 0xFF),
+ (scriptBufferEvent.data[2] & 0xFF),
+ (scriptBufferEvent.data[3] & 0xFF),
+ (scriptBufferEvent.data[4] & 0xFF),
+ (scriptBufferEvent.data[5] & 0x0F),
+ ((scriptBufferEvent.data[5] >> 4) & 0x0F),
+ pixels);
+ splitPriorityPixels();
+ }
+ break;
+
+ case DISCARD_PIC:
+ {
+ Picture pic = state.pictures[scriptBufferEvent.resourceNumber];
+ if (pic != null) pic.isLoaded = false;
+ }
+ break;
+
+ case DISCARD_VIEW:
+ {
+ View view = state.views[scriptBufferEvent.resourceNumber];
+ if (view != null) view.isLoaded = false;
+ }
+ break;
+
+ case DRAW_PIC:
+ {
+ drawPicture(scriptBufferEvent.resourceNumber);
+ }
+ break;
+
+ case LOAD_LOGIC:
+ {
+ Logic logic = state.logics[scriptBufferEvent.resourceNumber];
+ if (logic != null) logic.isLoaded = true;
+ }
+ break;
+
+ case LOAD_PIC:
+ {
+ Picture pic = state.pictures[scriptBufferEvent.resourceNumber];
+ if (pic != null) pic.isLoaded = true;
+ }
+ break;
+
+ case LOAD_SOUND:
+ {
+ Sound sound = state.sounds[scriptBufferEvent.resourceNumber];
+ if (sound != null)
+ {
+ soundPlayer.loadSound(sound);
+ sound.isLoaded = true;
+ }
+ }
+ break;
+
+ case LOAD_VIEW:
+ {
+ View view = state.views[scriptBufferEvent.resourceNumber];
+ if (view != null) view.isLoaded = true;
+ }
+ break;
+
+ case OVERLAY_PIC:
+ {
+ overlayPicture(scriptBufferEvent.resourceNumber);
+ }
+ break;
+ }
+ }
+
+ state.scriptBuffer.scriptOn();
+ }
+
+ /**
+ * Evaluates the given Condition.
+ *
+ * @param condition The Condition to evaluate.
+ *
+ * @return The result of evaluating the Condition; either true or false.
+ */
+ private boolean isConditionTrue(Condition condition) {
+ boolean result = false;
+
+ switch (condition.operation.opcode) {
+
+ case 1: // equaln
+ {
+ result = (state.vars[condition.operands.get(0).asByte()] == condition.operands.get(1).asByte());
+ }
+ break;
+
+ case 2: // equalv
+ {
+ result = (state.vars[condition.operands.get(0).asByte()] == state.vars[condition.operands.get(1).asByte()]);
+ }
+ break;
+
+ case 3: // lessn
+ {
+ result = (state.vars[condition.operands.get(0).asByte()] < condition.operands.get(1).asByte());
+ }
+ break;
+
+ case 4: // lessv
+ {
+ result = (state.vars[condition.operands.get(0).asByte()] < state.vars[condition.operands.get(1).asByte()]);
+ }
+ break;
+
+ case 5: // greatern
+ {
+ result = (state.vars[condition.operands.get(0).asByte()] > condition.operands.get(1).asByte());
+ }
+ break;
+
+ case 6: // greaterv
+ {
+ result = (state.vars[condition.operands.get(0).asByte()] > state.vars[condition.operands.get(1).asByte()]);
+ }
+ break;
+
+ case 7: // isset
+ {
+ result = state.flags[condition.operands.get(0).asByte()];
+ }
+ break;
+
+ case 8: // issetv
+ {
+ result = state.flags[state.vars[condition.operands.get(0).asByte()]];
+ }
+ break;
+
+ case 9: // has
+ {
+ result = (state.objects.objects.get(condition.operands.get(0).asByte()).room == Defines.CARRYING);
+ }
+ break;
+
+ case 10: // obj.in.room
+ {
+ result = (state.objects.objects.get(condition.operands.get(0).asByte()).room == state.vars[condition.operands.get(1).asByte()]);
+ }
+ break;
+
+ case 11: // posn
+ {
+ AnimatedObject aniObj = state.animatedObjects[condition.operands.get(0).asByte()];
+ int x1 = condition.operands.get(1).asByte();
+ int y1 = condition.operands.get(2).asByte();
+ int x2 = condition.operands.get(3).asByte();
+ int y2 = condition.operands.get(4).asByte();
+ result = ((aniObj.x >= x1) && (aniObj.y >= y1) && (aniObj.x <= x2) && (aniObj.y <= y2));
+ }
+ break;
+
+ case 12: // controller
+ {
+ result = state.controllers[condition.operands.get(0).asByte()];
+ }
+ break;
+
+ case 13: // have.key
+ {
+ int key = state.vars[Defines.LAST_CHAR];
+ if (key == 0) {
+ key = userInput.getKey();
+ }
+ if (key > 0) {
+ state.vars[Defines.LAST_CHAR] = (key & 0xFF);
+ }
+ result = (key != 0);
+ }
+ break;
+
+ case 14: // said
+ {
+ result = parser.said(condition.operands.get(0).asInts());
+ }
+ break;
+
+ case 15: // compare.strings
+ {
+ // Compare two strings. Ignore case, whitespace, and punctuation.
+ String str1 = state.strings[condition.operands.get(0).asByte()].toLowerCase().replaceAll("[ \t.,;:\'!-]", "");
+ String str2 = state.strings[condition.operands.get(1).asByte()].toLowerCase().replaceAll("[ \t.,;:\'!-]", "");
+ result = str1.equals(str2);
+ }
+ break;
+
+ case 16: // obj.in.box
+ {
+ AnimatedObject aniObj = state.animatedObjects[condition.operands.get(0).asByte()];
+ int x1 = condition.operands.get(1).asByte();
+ int y1 = condition.operands.get(2).asByte();
+ int x2 = condition.operands.get(3).asByte();
+ int y2 = condition.operands.get(4).asByte();
+ result = ((aniObj.x >= x1) && (aniObj.y >= y1) && ((aniObj.x + aniObj.xSize() - 1) <= x2) && (aniObj.y <= y2));
+ }
+ break;
+
+ case 17: // center.posn
+ {
+ AnimatedObject aniObj = state.animatedObjects[condition.operands.get(0).asByte()];
+ int x1 = condition.operands.get(1).asByte();
+ int y1 = condition.operands.get(2).asByte();
+ int x2 = condition.operands.get(3).asByte();
+ int y2 = condition.operands.get(4).asByte();
+ result = ((aniObj.x + (aniObj.xSize() / 2) >= x1) && (aniObj.y >= y1) && (aniObj.x + (aniObj.xSize() / 2) <= x2) && (aniObj.y <= y2));
+ }
+ break;
+
+ case 18: // right.posn
+ {
+ AnimatedObject aniObj = state.animatedObjects[condition.operands.get(0).asByte()];
+ int x1 = condition.operands.get(1).asByte();
+ int y1 = condition.operands.get(2).asByte();
+ int x2 = condition.operands.get(3).asByte();
+ int y2 = condition.operands.get(4).asByte();
+ result = (((aniObj.x + aniObj.xSize() - 1) >= x1) && (aniObj.y >= y1) && ((aniObj.x + aniObj.xSize() - 1) <= x2) && (aniObj.y <= y2));
+ }
+ break;
+
+ case 0xfc: // OR
+ {
+ result = false;
+ for (Condition orCondition : condition.operands.get(0).asConditions()) {
+ if (isConditionTrue(orCondition)) {
+ result = true;
+ break;
+ }
+ }
+ }
+ break;
+
+ case 0xfd: // NOT
+ {
+ result = !isConditionTrue(condition.operands.get(0).asCondition());
+ }
+ break;
+ }
+
+ return result;
+ }
+
+ /**
+ * Executes the given Action command.
+ *
+ * @param action The Action command to execute.
+ *
+ * @return The index of the next Action to execute, or 0 to rescan logics from top, or -1 when at end of Logic.
+ */
+ private int executeAction(Action action) {
+ // Normally the next Action will be the next one in the Actions list, but this
+ // can be overwritten by the If and Goto actions.
+ int nextActionNum = action.logic.addressToActionIndex.get(action.address) + 1;
+
+ switch (action.operation.opcode) {
+ case 0: // return
+ return -1;
+
+ case 1: // increment
+ {
+ int varNum = action.operands.get(0).asByte();
+ if (state.vars[varNum] < 255) state.vars[varNum]++;
+ }
+ break;
+
+ case 2: // decrement
+ {
+ int varNum = action.operands.get(0).asByte();
+ if (state.vars[varNum] > 0) state.vars[varNum]--;
+ }
+ break;
+
+ case 3: // assignn
+ {
+ int varNum = action.operands.get(0).asByte();
+ int value = action.operands.get(1).asByte();
+ state.vars[varNum] = value;
+ }
+ break;
+
+ case 4: // assignv
+ {
+ int varNum1 = action.operands.get(0).asByte();
+ int varNum2 = action.operands.get(1).asByte();
+ state.vars[varNum1] = state.vars[varNum2];
+ }
+ break;
+
+ case 5: // addn
+ {
+ int varNum = action.operands.get(0).asByte();
+ int value = action.operands.get(1).asByte();
+ state.vars[varNum] += value;
+ }
+ break;
+
+ case 6: // addv
+ {
+ int varNum1 = action.operands.get(0).asByte();
+ int varNum2 = action.operands.get(1).asByte();
+ state.vars[varNum1] += state.vars[varNum2];
+ }
+ break;
+
+ case 7: // subn
+ {
+ int varNum = action.operands.get(0).asByte();
+ int value = action.operands.get(1).asByte();
+ state.vars[varNum] -= value;
+ }
+ break;
+
+ case 8: // subv
+ {
+ int varNum1 = action.operands.get(0).asByte();
+ int varNum2 = action.operands.get(1).asByte();
+ state.vars[varNum1] -= state.vars[varNum2];
+ }
+ break;
+
+ case 9: // lindirectv
+ {
+ int varNum1 = action.operands.get(0).asByte();
+ int varNum2 = action.operands.get(1).asByte();
+ state.vars[state.vars[varNum1]] = state.vars[varNum2];
+ }
+ break;
+
+ case 10: // rindirect
+ {
+ int varNum1 = action.operands.get(0).asByte();
+ int varNum2 = action.operands.get(1).asByte();
+ state.vars[varNum1] = state.vars[state.vars[varNum2]];
+ }
+ break;
+
+ case 11: // lindirectn
+ {
+ int varNum = action.operands.get(0).asByte();
+ int value = action.operands.get(1).asByte();
+ state.vars[state.vars[varNum]] = value;
+ }
+ break;
+
+ case 12: // set
+ {
+ state.flags[action.operands.get(0).asByte()] = true;
+ }
+ break;
+
+ case 13: // reset
+ {
+ state.flags[action.operands.get(0).asByte()] = false;
+ }
+ break;
+
+ case 14: // toggle
+ {
+ int flagNum = action.operands.get(0).asByte();
+ state.flags[flagNum] = !state.flags[flagNum];
+ }
+ break;
+
+ case 15: // set.v
+ {
+ state.flags[state.vars[action.operands.get(0).asByte()]] = true;
+ }
+ break;
+
+ case 16: // reset.v
+ {
+ state.flags[state.vars[action.operands.get(0).asByte()]] = false;
+ }
+ break;
+
+ case 17: // toggle.v
+ {
+ int flagNum = state.vars[action.operands.get(0).asByte()];
+ state.flags[flagNum] = !state.flags[flagNum];
+ }
+ break;
+
+ case 18: // new.room
+ newRoom(action.operands.get(0).asByte());
+ return 0;
+
+ case 19: // new.room.v
+ newRoom(state.vars[action.operands.get(0).asByte()]);
+ return 0;
+
+ case 20: // load.logics
+ {
+ // All logics are already loaded in this interpreter, so nothing to do as such
+ // other than to remember it was "loaded".
+ Logic logic = state.logics[action.operands.get(0).asByte()];
+ if ((logic != null) && !logic.isLoaded) {
+ logic.isLoaded = true;
+ state.scriptBuffer.addScript(ScriptBuffer.ScriptBufferEventType.LOAD_LOGIC, logic.index);
+ }
+ }
+ break;
+
+ case 21: // load.logics.v
+ {
+ // All logics are already loaded in this interpreter, so nothing to do as such
+ // other than to remember it was "loaded".
+ Logic logic = state.logics[state.vars[action.operands.get(0).asByte()]];
+ if ((logic != null) && !logic.isLoaded) {
+ logic.isLoaded = true;
+ state.scriptBuffer.addScript(ScriptBuffer.ScriptBufferEventType.LOAD_LOGIC, logic.index);
+ }
+ }
+ break;
+
+ case 22: // call
+ {
+ if (executeLogic(action.operands.get(0).asByte())) {
+ // This means that a rescan from the top of Logic.0 should be done.
+ return 0;
+ }
+ }
+ break;
+
+ case 23: // call.v
+ {
+ if (executeLogic(state.vars[action.operands.get(0).asByte()])) {
+ // This means that a rescan from the top of Logic.0 should be done.
+ return 0;
+ }
+ }
+ break;
+
+ case 24: // load.pic
+ {
+ // All pictures are already loaded in this interpreter, so nothing to do as such
+ // other than to remember it was "loaded".
+ Picture pic = state.pictures[state.vars[action.operands.get(0).asByte()]];
+ if ((pic != null) && !pic.isLoaded) {
+ pic.isLoaded = true;
+ state.scriptBuffer.addScript(ScriptBuffer.ScriptBufferEventType.LOAD_PIC, pic.index);
+ }
+ }
+ break;
+
+ case 25: // draw.pic
+ {
+ drawPicture(state.vars[action.operands.get(0).asByte()]);
+ }
+ break;
+
+ case 26: // show.pic
+ {
+ showPicture();
+ }
+ break;
+
+ case 27: // discard.pic
+ {
+ // All pictures are kept loaded in this interpreter, so nothing to do as such
+ // other than to remember it was "unloaded".
+ Picture pic = state.pictures[state.vars[action.operands.get(0).asByte()]];
+ if ((pic != null) && pic.isLoaded) {
+ pic.isLoaded = false;
+ state.scriptBuffer.addScript(ScriptBuffer.ScriptBufferEventType.DISCARD_PIC, pic.index);
+ }
+ }
+ break;
+
+ case 28: // overlay.pic
+ {
+ overlayPicture(state.vars[action.operands.get(0).asByte()]);
+ }
+ break;
+
+ case 29: // show.pri.screen
+ {
+ showPriorityScreen();
+ }
+ break;
+
+ case 30: // load.view
+ {
+ // All views are already loaded in this interpreter, so nothing to do as such
+ // other than to remember it was "loaded".
+ View view = state.views[action.operands.get(0).asByte()];
+ if ((view != null) && !view.isLoaded) {
+ view.isLoaded = true;
+ state.scriptBuffer.addScript(ScriptBuffer.ScriptBufferEventType.LOAD_VIEW, view.index);
+ }
+ }
+ break;
+
+ case 31: // load.view.v
+ {
+ // All views are already loaded in this interpreter, so nothing to do as such
+ // other than to remember it was "loaded".
+ View view = state.views[state.vars[action.operands.get(0).asByte()]];
+ if ((view != null) && !view.isLoaded) {
+ view.isLoaded = true;
+ state.scriptBuffer.addScript(ScriptBuffer.ScriptBufferEventType.LOAD_VIEW, view.index);
+ }
+ }
+ break;
+
+ case 32: // discard.view
+ {
+ // All views are kept loaded in this interpreter, so nothing to do as such
+ // other than to remember it was "unloaded".
+ View view = state.views[action.operands.get(0).asByte()];
+ if ((view != null) && view.isLoaded) {
+ view.isLoaded = false;
+ state.scriptBuffer.addScript(ScriptBuffer.ScriptBufferEventType.DISCARD_VIEW, view.index);
+ }
+ }
+ break;
+
+ case 33: // animate.obj
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.animate();
+ }
+ break;
+
+ case 34: // unanimate.all
+ {
+ state.restoreBackgrounds();
+ for (AnimatedObject aniObj : state.animatedObjects)
+ {
+ aniObj.animated = false;
+ aniObj.drawn = false;
+ }
+ }
+ break;
+
+ case 35: // draw
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ if (!aniObj.drawn)
+ {
+ aniObj.update = true;
+ aniObj.findPosition();
+ aniObj.prevX = aniObj.x;
+ aniObj.prevY = aniObj.y;
+ aniObj.previousCel = aniObj.cel();
+ state.restoreBackgrounds(state.updateObjectList);
+ aniObj.drawn = true;
+ state.drawObjects(state.makeUpdateObjectList());
+ aniObj.show(pixels);
+ aniObj.noAdvance = false;
+ }
+ }
+ break;
+
+ case 36: // erase
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ state.restoreBackgrounds(state.updateObjectList);
+ if (!aniObj.update)
+ {
+ state.restoreBackgrounds(state.stoppedObjectList);
+ }
+ aniObj.drawn = false;
+ if (!aniObj.update)
+ {
+ state.drawObjects(state.makeStoppedObjectList());
+ }
+ state.drawObjects(state.makeUpdateObjectList());
+ aniObj.show(pixels);
+ }
+ break;
+
+ case 37: // position
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.x = aniObj.prevX = (short)action.operands.get(1).asByte();
+ aniObj.y = aniObj.prevY = (short)action.operands.get(2).asByte();
+ }
+ break;
+
+ case 38: // position.v
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.x = aniObj.prevX = (short)state.vars[action.operands.get(1).asByte()];
+ aniObj.y = aniObj.prevY = (short)state.vars[action.operands.get(2).asByte()];
+ }
+ break;
+
+ case 39: // get.posn
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ state.vars[action.operands.get(1).asByte()] = aniObj.x;
+ state.vars[action.operands.get(2).asByte()] = aniObj.y;
+ }
+ break;
+
+ case 40: // reposition
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.reposition(state.vars[action.operands.get(1).asByte()], state.vars[action.operands.get(2).asByte()]);
+ }
+ break;
+
+ case 41: // set.view
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.setView(action.operands.get(1).asByte());
+ }
+ break;
+
+ case 42: // set.view.v
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.setView(state.vars[action.operands.get(1).asByte()]);
+ }
+ break;
+
+ case 43: // set.loop
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.setLoop(action.operands.get(1).asByte());
+ }
+ break;
+
+ case 44: // set.loop.v
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.setLoop(state.vars[action.operands.get(1).asByte()]);
+ }
+ break;
+
+ case 45: // fix.loop
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.fixedLoop = true;
+ }
+ break;
+
+ case 46: // release.loop
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.fixedLoop = false;
+ }
+ break;
+
+ case 47: // set.cel
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.setCel(action.operands.get(1).asByte());
+ aniObj.noAdvance = false;
+ }
+ break;
+
+ case 48: // set.cel.v
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.setCel(state.vars[action.operands.get(1).asByte()]);
+ aniObj.noAdvance = false;
+ }
+ break;
+
+ case 49: // last.cel
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ state.vars[action.operands.get(1).asByte()] = (aniObj.numberOfCels() - 1);
+ }
+ break;
+
+ case 50: // current.cel
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ state.vars[action.operands.get(1).asByte()] = aniObj.currentCel;
+ }
+ break;
+
+ case 51: // current.loop
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ state.vars[action.operands.get(1).asByte()] = aniObj.currentLoop;
+ }
+ break;
+
+ case 52: // current.view
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ state.vars[action.operands.get(1).asByte()] = aniObj.currentView;
+ }
+ break;
+
+ case 53: // number.of.loops
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ state.vars[action.operands.get(1).asByte()] = aniObj.numberOfLoops();
+ }
+ break;
+
+ case 54: // set.priority
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.fixedPriority = true;
+ aniObj.priority = (byte)action.operands.get(1).asByte();
+ }
+ break;
+
+ case 55: // set.priority.v
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.fixedPriority = true;
+ aniObj.priority = (byte)state.vars[action.operands.get(1).asByte()];
+ }
+ break;
+
+ case 56: // release.priority
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.fixedPriority = false;
+ }
+ break;
+
+ case 57: // get.priority
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ state.vars[action.operands.get(1).asByte()] = aniObj.priority;
+ }
+ break;
+
+ case 58: // stop.update
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ if (aniObj.update)
+ {
+ state.restoreBackgrounds();
+ aniObj.update = false;
+ state.drawObjects();
+ }
+ }
+ break;
+
+ case 59: // start.update
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ if (!aniObj.update)
+ {
+ state.restoreBackgrounds();
+ aniObj.update = true;
+ state.drawObjects();
+ }
+ }
+ break;
+
+ case 60: // force.update
+ {
+ // Although this command has a parameter, it seems to get ignored. Instead
+ // every AnimatedObject is redrawn and blitted to the screen.
+ state.restoreBackgrounds();
+ state.drawObjects();
+ state.showObjects(pixels);
+ }
+ break;
+
+ case 61: // ignore.horizon
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.ignoreHorizon = true;
+ }
+ break;
+
+ case 62: // observe.horizon
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.ignoreHorizon = false;
+ }
+ break;
+
+ case 63: // set.horizon
+ {
+ state.horizon = action.operands.get(0).asByte();
+ }
+ break;
+
+ case 64: // object.on.water
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.stayOnWater = true;
+ }
+ break;
+
+ case 65: // object.on.land
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.stayOnLand = true;
+ }
+ break;
+
+ case 66: // object.on.anything
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.stayOnLand = false;
+ aniObj.stayOnWater = false;
+ }
+ break;
+
+ case 67: // ignore.objs
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.ignoreObjects = true;
+ }
+ break;
+
+ case 68: // observe.objs
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.ignoreObjects = false;
+ }
+ break;
+
+ case 69: // distance
+ {
+ AnimatedObject aniObj1 = state.animatedObjects[action.operands.get(0).asByte()];
+ AnimatedObject aniObj2 = state.animatedObjects[action.operands.get(1).asByte()];
+ state.vars[action.operands.get(2).asByte()] = aniObj1.distance(aniObj2);
+ }
+ break;
+
+ case 70: // stop.cycling
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.cycle = false;
+ }
+ break;
+
+ case 71: // start.cycling
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.cycle = true;
+ }
+ break;
+
+ case 72: // normal.cycle
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.cycleType = CycleType.NORMAL;
+ aniObj.cycle = true;
+ }
+ break;
+
+ case 73: // end.of.loop
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ int flagNum = action.operands.get(1).asByte();
+ aniObj.cycleType = CycleType.END_LOOP;
+ aniObj.update = true;
+ aniObj.cycle = true;
+ aniObj.noAdvance = true;
+ aniObj.motionParam1 = (short)flagNum;
+ state.flags[flagNum] = false;
+ }
+ break;
+
+ case 74: // reverse.cycle
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.cycleType = CycleType.REVERSE;
+ aniObj.cycle = true;
+ }
+ break;
+
+ case 75: // reverse.loop
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ int flagNum = action.operands.get(1).asByte();
+ aniObj.cycleType = CycleType.REVERSE_LOOP;
+ aniObj.update = true;
+ aniObj.cycle = true;
+ aniObj.noAdvance = true;
+ aniObj.motionParam1 = (short)flagNum;
+ state.flags[flagNum] = false;
+ }
+ break;
+
+ case 76: // cycle.time
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.cycleTimeCount = aniObj.cycleTime = state.vars[action.operands.get(1).asByte()];
+ }
+ break;
+
+ case 77: // stop.motion
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.direction = 0;
+ aniObj.motionType = MotionType.NORMAL;
+ if (aniObj == state.ego)
+ {
+ state.vars[Defines.EGODIR] = 0;
+ state.userControl = false;
+ }
+ }
+ break;
+
+ case 78: // start.motion
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.motionType = MotionType.NORMAL;
+ if (aniObj == state.ego)
+ {
+ state.vars[Defines.EGODIR] = 0;
+ state.userControl = true;
+ }
+ }
+ break;
+
+ case 79: // step.size
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.stepSize = state.vars[action.operands.get(1).asByte()];
+ }
+ break;
+
+ case 80: // step.time
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.stepTimeCount = aniObj.stepTime = state.vars[action.operands.get(1).asByte()];
+ }
+ break;
+
+ case 81: // move.obj
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.startMoveObj(
+ action.operands.get(1).asByte(), action.operands.get(2).asByte(),
+ action.operands.get(3).asByte(), action.operands.get(4).asByte());
+ }
+ break;
+
+ case 82: // move.obj.v
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.startMoveObj(
+ state.vars[action.operands.get(1).asByte()], state.vars[action.operands.get(2).asByte()],
+ state.vars[action.operands.get(3).asByte()], action.operands.get(4).asByte());
+ }
+ break;
+
+ case 83: // follow.ego
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.startFollowEgo(action.operands.get(1).asByte(), action.operands.get(2).asByte());
+ }
+ break;
+
+ case 84: // wander
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.startWander();
+ }
+ break;
+
+ case 85: // normal.motion
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.motionType = MotionType.NORMAL;
+ }
+ break;
+
+ case 86: // set.dir
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.direction = (byte)state.vars[action.operands.get(1).asByte()];
+ }
+ break;
+
+ case 87: // get.dir
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ state.vars[action.operands.get(1).asByte()] = aniObj.direction;
+ }
+ break;
+
+ case 88: // ignore.blocks
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.ignoreBlocks = true;
+ }
+ break;
+
+ case 89: // observe.blocks
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.ignoreBlocks = false;
+ }
+ break;
+
+ case 90: // block
+ {
+ state.blocking = true;
+ state.blockUpperLeftX = (short)action.operands.get(0).asByte();
+ state.blockUpperLeftY = (short)action.operands.get(1).asByte();
+ state.blockLowerRightX = (short)action.operands.get(2).asByte();
+ state.blockLowerRightY = (short)action.operands.get(3).asByte();
+ }
+ break;
+
+ case 91: // unblock
+ {
+ state.blocking = false;
+ }
+ break;
+
+ case 92: // get
+ {
+ state.objects.objects.get(action.operands.get(0).asByte()).room = Defines.CARRYING;
+ }
+ break;
+
+ case 93: // get.v
+ {
+ state.objects.objects.get(state.vars[action.operands.get(0).asByte()]).room = Defines.CARRYING;
+ }
+ break;
+
+ case 94: // drop
+ {
+ state.objects.objects.get(action.operands.get(0).asByte()).room = Defines.LIMBO;
+ }
+ break;
+
+ case 95: // put
+ {
+ state.objects.objects.get(action.operands.get(0).asByte()).room = state.vars[action.operands.get(1).asByte()];
+ }
+ break;
+
+ case 96: // put.v
+ {
+ state.objects.objects.get(state.vars[action.operands.get(0).asByte()]).room = state.vars[action.operands.get(1).asByte()];
+ }
+ break;
+
+ case 97: // get.room.v
+ {
+ state.vars[action.operands.get(1).asByte()] = state.objects.objects.get(state.vars[action.operands.get(0).asByte()]).room;
+ }
+ break;
+
+ case 98: // load.sound
+ {
+ // All sounds are already loaded in this interpreter, so nothing to do as such
+ // other than to remember it was "loaded".
+ int soundNum = action.operands.get(0).asByte();
+ Sound sound = state.sounds[soundNum];
+ if ((sound != null) && !sound.isLoaded)
+ {
+ soundPlayer.loadSound(sound);
+ sound.isLoaded = true;
+ state.scriptBuffer.addScript(ScriptBuffer.ScriptBufferEventType.LOAD_SOUND, sound.index);
+ }
+ }
+ break;
+
+ case 99: // sound
+ {
+ int soundNum = action.operands.get(0).asByte();
+ int endFlag = action.operands.get(1).asByte();
+ state.flags[endFlag] = false;
+ Sound sound = state.sounds[soundNum];
+ if ((sound != null) && (sound.isLoaded))
+ {
+ this.soundPlayer.playSound(sound, endFlag);
+ }
+ }
+ break;
+
+ case 100: // stop.sound
+ {
+ this.soundPlayer.stopSound();
+ }
+ break;
+
+ case 101: // print
+ {
+ this.textGraphics.print(action.logic.messages.get(action.operands.get(0).asByte()));
+ }
+ break;
+
+ case 102: // print.v
+ {
+ this.textGraphics.print(action.logic.messages.get(state.vars[action.operands.get(0).asByte()]));
+ }
+ break;
+
+ case 103: // display
+ {
+ int row = action.operands.get(0).asByte();
+ int col = action.operands.get(1).asByte();
+ String message = action.logic.messages.get(action.operands.get(2).asByte());
+ this.textGraphics.display(message, row, col);
+ }
+ break;
+
+ case 104: // display.v
+ {
+ int row = state.vars[action.operands.get(0).asByte()];
+ int col = state.vars[action.operands.get(1).asByte()];
+ String message = action.logic.messages.get(state.vars[action.operands.get(2).asByte()]);
+ this.textGraphics.display(message, row, col);
+ }
+ break;
+
+ case 105: // clear.lines
+ {
+ int colour = textGraphics.makeBackgroundColour(action.operands.get(2).asByte());
+ textGraphics.clearLines(action.operands.get(0).asByte(), action.operands.get(1).asByte(), colour);
+ }
+ break;
+
+ case 106: // text.screen
+ {
+ textGraphics.textScreen();
+ }
+ break;
+
+ case 107: // graphics
+ {
+ textGraphics.graphicsScreen();
+ }
+ break;
+
+ case 108: // set.cursor.char
+ {
+ String cursorStr = action.logic.messages.get(action.operands.get(0).asByte());
+ state.cursorCharacter = (cursorStr.length() > 0? cursorStr.charAt(0) : (char)0);
+ }
+ break;
+
+ case 109: // set.text.attribute
+ {
+ textGraphics.setTextAttribute(action.operands.get(0).asByte(), action.operands.get(1).asByte());
+ }
+ break;
+
+ case 110: // shake.screen
+ {
+ shakeScreen(action.operands.get(0).asByte());
+ }
+ break;
+
+ case 111: // configure.screen
+ {
+ state.pictureRow = action.operands.get(0).asByte();
+ state.inputLineRow = action.operands.get(1).asByte();
+ state.statusLineRow = action.operands.get(2).asByte();
+ }
+ break;
+
+ case 112: // status.line.on
+ {
+ state.showStatusLine = true;
+ textGraphics.clearLines(state.statusLineRow, state.statusLineRow, 15);
+ textGraphics.updateStatusLine();
+ }
+ break;
+
+ case 113: // status.line.off
+ {
+ state.showStatusLine = false;
+ textGraphics.clearLines(state.statusLineRow, state.statusLineRow, 0);
+ }
+ break;
+
+ case 114: // set.string
+ {
+ state.strings[action.operands.get(0).asByte()] = action.logic.messages.get(action.operands.get(1).asByte());
+ }
+ break;
+
+ case 115: // get.string
+ {
+ textGraphics.getString(action.operands.get(0).asByte(), action.logic.messages.get(action.operands.get(1).asByte()),
+ action.operands.get(2).asByte(), action.operands.get(3).asByte(), action.operands.get(4).asByte());
+ }
+ break;
+
+ case 116: // word.to.string
+ {
+ state.strings[action.operands.get(0).asByte()] = state.recognisedWords.get(action.operands.get(1).asByte());
+ }
+ break;
+
+ case 117: // parse
+ {
+ parser.parseString(action.operands.get(0).asByte());
+ }
+ break;
+
+ case 118: // get.num
+ {
+ state.vars[action.operands.get(1).asByte()] = textGraphics.getNum(action.logic.messages.get(action.operands.get(0).asByte()));
+ }
+ break;
+
+ case 119: // prevent.input
+ {
+ state.acceptInput = false;
+ textGraphics.updateInputLine();
+ }
+ break;
+
+ case 120: // accept.input
+ {
+ state.acceptInput = true;
+ textGraphics.updateInputLine();
+ }
+ break;
+
+ case 121: // set.key
+ {
+ int keyCode = (action.operands.get(0).asByte() + (action.operands.get(1).asByte() << 8));
+ if (userInput.keyCodeMap.containsKey(keyCode))
+ {
+ int controllerNum = action.operands.get(2).asByte();
+ int interKeyCode = userInput.keyCodeMap.get(keyCode);
+ if (state.keyToControllerMap.containsKey(interKeyCode))
+ {
+ state.keyToControllerMap.remove(interKeyCode);
+ }
+ state.keyToControllerMap.put(userInput.keyCodeMap.get(keyCode), controllerNum);
+ }
+ }
+ break;
+
+ case 122: // add.to.pic
+ {
+ AnimatedObject picObj = new AnimatedObject(state, -1);
+ picObj.addToPicture(
+ action.operands.get(0).asByte(), action.operands.get(1).asByte(), action.operands.get(2).asByte(),
+ action.operands.get(3).asByte(), action.operands.get(4).asByte(), action.operands.get(5).asByte(),
+ action.operands.get(6).asByte(), pixels);
+ splitPriorityPixels();
+ picObj.show(pixels);
+ }
+ break;
+
+ case 123: // add.to.pic.v
+ {
+ AnimatedObject picObj = new AnimatedObject(state, -1);
+ picObj.addToPicture(
+ state.vars[action.operands.get(0).asByte()], state.vars[action.operands.get(1).asByte()],
+ state.vars[action.operands.get(2).asByte()], state.vars[action.operands.get(3).asByte()],
+ state.vars[action.operands.get(4).asByte()], state.vars[action.operands.get(5).asByte()],
+ state.vars[action.operands.get(6).asByte()], pixels);
+ splitPriorityPixels();
+ }
+ break;
+
+ case 124: // status
+ {
+ inventory.showInventoryScreen();
+ }
+ break;
+
+ case 125: // save.game
+ {
+ savedGames.saveGameState();
+ }
+ break;
+
+ case 126: // restore.game
+ {
+ if (savedGames.restoreGameState())
+ {
+ soundPlayer.reset();
+ menu.enableAllMenus();
+ replayScriptEvents();
+ showPicture(false);
+ textGraphics.updateStatusLine();
+ return 0;
+ }
+ }
+ break;
+
+ case 127: // init.disk
+ {
+ // No need to implement this.
+ }
+ break;
+
+ case 128: // restart.game
+ {
+ if (state.flags[Defines.NO_PRMPT_RSTRT] || textGraphics.windowPrint("Press ENTER to restart\nthe game.\n\nPress ESC to continue\nthis game."))
+ {
+ soundPlayer.reset();
+ state.init();
+ state.flags[Defines.RESTART] = true;
+ menu.enableAllMenus();
+ textGraphics.clearLines(0, 24, 0);
+ return 0;
+ }
+ }
+ break;
+
+ case 129: // show.obj
+ {
+ inventory.showInventoryObject(action.operands.get(0).asByte());
+ }
+ break;
+
+ case 130: // random.num
+ {
+ int minVal = action.operands.get(0).asByte();
+ int maxVal = action.operands.get(1).asByte();
+ state.vars[action.operands.get(2).asByte()] = (((state.random.nextInt(255) % (maxVal - minVal + 1)) + minVal) & 0xFF);
+ }
+ break;
+
+ case 131: // program.control
+ {
+ state.userControl = false;
+ }
+ break;
+
+ case 132: // player.control
+ {
+ state.userControl = true;
+ state.ego.motionType = MotionType.NORMAL;
+ }
+ break;
+
+ case 133: // obj.status.v
+ {
+ AnimatedObject aniObj = state.animatedObjects[state.vars[action.operands.get(0).asByte()]];
+ textGraphics.windowPrint(aniObj.getStatusStr());
+ }
+ break;
+
+ case 134: // quit
+ {
+ int quitAction = (action.operands.size() == 0 ? 1 : action.operands.get(0).asByte());
+ if ((quitAction == 1) || textGraphics.windowPrint("Press ENTER to quit.\nPress ESC to keep playing."))
+ {
+ soundPlayer.shutdown();
+ QuitAction.exit();
+ }
+ }
+ break;
+
+ case 135: // show.mem
+ {
+ // No need to implement this.
+ }
+ break;
+
+ case 136: // pause
+ {
+ // Note: In the original AGI interpreter, pause stopped sound rather than pause
+ soundPlayer.stopSound();
+ this.textGraphics.print(" Game paused.\nPress Enter to continue.");
+ }
+ break;
+
+ case 137: // echo.line
+ {
+ if (state.currentInput.length() < state.lastInput.length())
+ {
+ state.currentInput.append(state.lastInput.substring(state.currentInput.length()));
+ }
+ }
+ break;
+
+ case 138: // cancel.line
+ {
+ state.currentInput.setLength(0);
+ }
+ break;
+
+ case 139: // init.joy
+ {
+ // No need to implement this.
+ }
+ break;
+
+ case 140: // toggle.monitor
+ {
+ // No need to implement this.
+ }
+ break;
+
+ case 141: // version
+ {
+ this.textGraphics.print("Adventure Game Interpreter\n Version " + state.version);
+ }
+ break;
+
+ case 142: // script.size
+ {
+ state.scriptBuffer.setScriptSize(action.operands.get(0).asByte());
+ }
+ break;
+
+ // --------------------------------------------------------------------------------------------------
+ // ---- AGI version 2.001 in effect ended here. It did have a 143 and 144 but they were different ---
+ // --------------------------------------------------------------------------------------------------
+
+ case 143: // set.game.id (was max.drawn in AGI v2.001)
+ {
+ state.gameId = action.logic.messages.get(action.operands.get(0).asByte());
+ }
+ break;
+
+ case 144: // log
+ {
+ // No need to implement this.
+ }
+ break;
+
+ case 145: // set.scan.start
+ {
+ state.scanStart[action.logic.index] = action.getActionNumber() + 1;
+ }
+ break;
+
+ case 146: // reset.scan.start
+ {
+ state.scanStart[action.logic.index] = 0;
+ }
+ break;
+
+ case 147: // reposition.to
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.x = (short)action.operands.get(1).asByte();
+ aniObj.y = (short)action.operands.get(2).asByte();
+ aniObj.repositioned = true;
+ aniObj.findPosition(); // Make sure that this position is OK.
+ }
+ break;
+
+ case 148: // reposition.to.v
+ {
+ AnimatedObject aniObj = state.animatedObjects[action.operands.get(0).asByte()];
+ aniObj.x = (short)state.vars[action.operands.get(1).asByte()];
+ aniObj.y = (short)state.vars[action.operands.get(2).asByte()];
+ aniObj.repositioned = true;
+ aniObj.findPosition(); // Make sure that this position is OK.
+ }
+ break;
+
+ case 149: // trace.on
+ {
+ // No need to implement this.
+ }
+ break;
+
+ case 150: // trace.info
+ {
+ // No need to implement this.
+ }
+ break;
+
+ case 151: // print.at
+ {
+ String message = action.logic.messages.get(action.operands.get(0).asByte());
+ int row = action.operands.get(1).asByte();
+ int col = action.operands.get(2).asByte();
+ int width = action.operands.get(3).asByte();
+ this.textGraphics.printAt(message, row, col, width);
+ }
+ break;
+
+ case 152: // print.at.v
+ {
+ String message = action.logic.messages.get(state.vars[action.operands.get(0).asByte()]);
+ int row = action.operands.get(1).asByte();
+ int col = action.operands.get(2).asByte();
+ int width = action.operands.get(3).asByte();
+ this.textGraphics.printAt(message, row, col, width);
+ }
+ break;
+
+ case 153: // discard.view.v
+ {
+ // All views are kept loaded in this interpreter, so nothing to do as such
+ // other than to remember it was "unloaded".
+ View view = state.views[state.vars[action.operands.get(0).asByte()]];
+ if ((view != null) && view.isLoaded)
+ {
+ view.isLoaded = false;
+ state.scriptBuffer.addScript(ScriptBuffer.ScriptBufferEventType.DISCARD_VIEW, view.index);
+ }
+ }
+ break;
+
+ case 154: // clear.text.rect
+ {
+ int top = action.operands.get(0).asByte();
+ int left = action.operands.get(1).asByte();
+ int bottom = action.operands.get(2).asByte();
+ int right = action.operands.get(3).asByte();
+ int colour = textGraphics.makeBackgroundColour(action.operands.get(4).asByte());
+ textGraphics.clearRect(top, left, bottom, right, colour);
+ }
+ break;
+
+ case 155: // set.upper.left
+ {
+ // Only used on the Apple. No need to implement.
+ }
+ break;
+
+ // --------------------------------------------------------------------------------------------------
+ // ---- AGI version 2.089 ends with command 155 above, i.e before the menu system was introduced ----
+ // --------------------------------------------------------------------------------------------------
+
+ case 156: // set.menu
+ {
+ menu.setMenu(action.logic.messages.get(action.operands.get(0).asByte()));
+ }
+ break;
+
+ case 157: // set.menu.item
+ {
+ String menuItemName = action.logic.messages.get(action.operands.get(0).asByte());
+ byte controllerNum = (byte)action.operands.get(1).asByte();
+ menu.setMenuItem(menuItemName, controllerNum);
+ }
+ break;
+
+ case 158: // submit.menu
+ {
+ menu.submitMenu();
+ }
+ break;
+
+ case 159: // enable.item
+ {
+ menu.enableItem(action.operands.get(0).asByte());
+ }
+ break;
+
+ case 160: // disable.item
+ {
+ menu.disableItem(action.operands.get(0).asByte());
+ }
+ break;
+
+ case 161: // menu.input
+ {
+ state.menuOpen = true;
+ }
+ break;
+
+ // -------------------------------------------------------------------------------------------------
+ // ---- AGI version 2.272 ends with command 161 above, i.e after the menu system was introduced ----
+ // -------------------------------------------------------------------------------------------------
+
+ case 162: // show.obj.v
+ {
+ inventory.showInventoryObject(state.vars[action.operands.get(0).asByte()]);
+ }
+ break;
+
+ case 163: // open.dialogue
+ {
+ // Appears to be something specific to monochrome. No need to implement.
+ }
+ break;
+
+ case 164: // close.dialogue
+ {
+ // Appears to be something specific to monochrome. No need to implement.
+ }
+ break;
+
+ case 165: // mul.n
+ {
+ int varNum = action.operands.get(0).asByte();
+ int value = action.operands.get(1).asByte();
+ state.vars[varNum] *= value;
+ }
+ break;
+
+ case 166: // mul.v
+ {
+ int varNum1 = action.operands.get(0).asByte();
+ int varNum2 = action.operands.get(1).asByte();
+ state.vars[varNum1] *= state.vars[varNum2];
+ }
+ break;
+
+ case 167: // div.n
+ {
+ int varNum = action.operands.get(0).asByte();
+ int value = action.operands.get(1).asByte();
+ state.vars[varNum] /= value;
+ }
+ break;
+
+ case 168: // div.v
+ {
+ int varNum1 = action.operands.get(0).asByte();
+ int varNum2 = action.operands.get(1).asByte();
+ state.vars[varNum1] /= state.vars[varNum2];
+ }
+ break;
+
+ case 169: // close.window
+ {
+ textGraphics.closeWindow();
+ }
+ break;
+
+ case 170: // set.simple (i.e. simpleName variable for saved games)
+ {
+ state.simpleName = action.logic.messages.get(action.operands.get(0).asByte());
+ }
+ break;
+
+ case 171: // push.script
+ {
+ state.scriptBuffer.pushScript();
+ }
+ break;
+
+ case 172: // pop.script
+ {
+ state.scriptBuffer.popScript();
+ }
+ break;
+
+ case 173: // hold.key
+ {
+ state.holdKey = true;
+ }
+ break;
+
+ // --------------------------------------------------------------------------------------------------
+ // ---- AGI version 2.915/2.917 ends with command 173 above ----
+ // --------------------------------------------------------------------------------------------------
+
+ case 174: // set.pri.base
+ {
+ state.priorityBase = action.operands.get(0).asByte();
+ }
+ break;
+
+ case 175: // discard.sound
+ {
+ // Note: Interpreter 2.936 doesn't persist discard sound to the script event buffer.
+ }
+ break;
+
+ // --------------------------------------------------------------------------------------------------
+ // ---- AGI version 2.936 ends with command 175 above ----
+ // --------------------------------------------------------------------------------------------------
+
+ case 176: // hide.mouse
+ {
+ // This command isn't supported by PC versions of original AGI Interpreter.
+ }
+ break;
+
+ case 177: // allow.menu
+ {
+ state.menuEnabled = (action.operands.get(0).asByte() != 0);
+ }
+ break;
+
+ case 178: // show.mouse
+ {
+ // This command isn't supported by PC versions of original AGI Interpreter.
+ }
+ break;
+
+ case 179: // fence.mouse
+ {
+ // This command isn't supported by PC versions of original AGI Interpreter.
+ }
+ break;
+
+ case 180: // mouse.posn
+ {
+ // This command isn't supported by PC versions of original AGI Interpreter.
+ }
+ break;
+
+ case 181: // release.key
+ {
+ state.holdKey = false;
+ }
+ break;
+
+ case 182: // adj.ego.move.to.x.y
+ {
+ // This command isn't supported by PC versions of original AGI Interpreter.
+ }
+ break;
+
+ case 0xfe: // Unconditional branch: else, goto.
+ {
+ nextActionNum = ((GotoAction)action).getDestinationActionIndex();
+ }
+ break;
+
+ case 0xff: // Conditional branch: if.
+ {
+ for (Condition condition : action.operands.get(0).asConditions()) {
+ if (!isConditionTrue(condition)) {
+ nextActionNum = ((IfAction)action).getDestinationActionIndex();
+ break;
+ }
+ }
+ }
+ break;
+
+ default: // Error has occurred
+ break;
+ }
+
+ return nextActionNum;
+ }
+
+ /**
+ * Executes the Logic identified by the given logic number.
+ *
+ * @param logicNum The number of the Logic to execute.
+ *
+ * @return true if logics should be rescanned from the top (i.e. top of Logic 0); otherwise false.
+ */
+ public boolean executeLogic(int logicNum) {
+ // Remember the previous Logic number.
+ int previousLogNum = state.currentLogNum;
+
+ // Store the new Logic number in the state so that actions will know this.
+ state.currentLogNum = logicNum;
+
+ // Prepare to start executing the Logic.
+ Logic logic = state.logics[logicNum];
+ int actionNum = state.scanStart[logicNum];
+
+ // Continually execute the Actions in the Logic until one of them tells us to exit.
+ do actionNum = executeAction(logic.actions.get(actionNum)); while (actionNum > 0);
+
+ // Restore the previous Logic number before we leave.
+ state.currentLogNum = previousLogNum;
+
+ // If ExecuteAction return 0, then it means that a newroom, restore or restart is
+ // happening. In those cases, we need to immediately rescan logics from the top of Logic.0
+ return (actionNum == 0);
+ }
+
+ /**
+ * Performs all the necessary updates to vars, flags, animated objects, controllers,
+ * and other state to prepare for entry in to the next room.
+ *
+ * @param roomNum
+ */
+ private void newRoom(int roomNum) {
+ // Simulate a slow room change if there is a text window open.
+ if (textGraphics.isWindowOpen()) {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+
+ // Turn off sound.
+ soundPlayer.reset();
+
+ // Clear the script event buffer ready for next room.
+ state.scriptBuffer.initScript();
+ state.scriptBuffer.scriptOn();
+
+ // Resets the Logics, Views, Pictures and Sounds back to new room state.
+ state.resetResources();
+
+ // Carry over ego's view number.
+ // TODO: For some reason in MH2, the ego View can be null at this point. Needs investigation to determine why.
+ if (state.ego.view() != null) {
+ state.vars[Defines.CURRENT_EGO] = (state.ego.view().index & 0xFF);
+ }
+
+ // Reset state for all animated objects.
+ for (AnimatedObject aniObj : state.animatedObjects) aniObj.reset();
+
+ // Current room logic is loaded automatically on room change and not directly by load.logic
+ Logic logic = state.logics[roomNum];
+ logic.isLoaded = true;
+ state.scriptBuffer.addScript(ScriptBuffer.ScriptBufferEventType.LOAD_LOGIC, logic.index);
+
+ // If ego collided with a border, set his position in the new room to
+ // the appropriate edge of the screen.
+ switch (state.vars[Defines.EGOEDGE]) {
+ case Defines.TOP:
+ state.ego.y = Defines.MAXY;
+ break;
+
+ case Defines.RIGHT:
+ state.ego.x = Defines.MINX;
+ break;
+
+ case Defines.BOTTOM:
+ state.ego.y = Defines.HORIZON + 1;
+ break;
+
+ case Defines.LEFT:
+ state.ego.x = (short)(Defines.MAXX + 1 - state.ego.xSize());
+ break;
+ }
+
+ // Change the room number.
+ state.vars[Defines.PREVROOM] = state.vars[Defines.CURROOM];
+ state.vars[Defines.CURROOM] = roomNum;
+
+ // Set flags and vars as appropriate for a new room.
+ state.vars[Defines.OBJHIT] = 0;
+ state.vars[Defines.OBJEDGE] = 0;
+ state.vars[Defines.UNKNOWN_WORD] = 0;
+ state.vars[Defines.EGOEDGE] = 0;
+ state.flags[Defines.INPUT] = false;
+ state.flags[Defines.INITLOGS] = true;
+ state.userControl = true;
+ state.blocking = false;
+ state.horizon = Defines.HORIZON;
+ state.clearControllers();
+
+ // Draw the status line, if applicable.
+ textGraphics.updateStatusLine();
+ }
+}
diff --git a/core/src/main/java/com/agifans/agile/Defines.java b/core/src/main/java/com/agifans/agile/Defines.java
new file mode 100644
index 0000000..0b1cc22
--- /dev/null
+++ b/core/src/main/java/com/agifans/agile/Defines.java
@@ -0,0 +1,175 @@
+package com.agifans.agile;
+
+/**
+ * The core constants and definitions within the AGI system.
+ */
+public class Defines {
+
+ /* ------------------------ System variables -------------------------- */
+
+ public final static int CURROOM = 0; /* current.room */
+
+ public final static int PREVROOM = 1; /* previous.room */
+
+ public final static int EGOEDGE = 2; /* edge.ego.hit */
+
+ public final static int SCORE = 3; /* score */
+
+ public final static int OBJHIT = 4; /* obj.hit.edge */
+
+ public final static int OBJEDGE = 5; /* edge.obj.hit */
+
+ public final static int EGODIR = 6; /* ego's direction */
+
+ public final static int MAXSCORE = 7; /* maximum possible score */
+
+ public final static int MEMLEFT = 8; /* remaining heap space in pages */
+
+ public final static int UNKNOWN_WORD = 9; /* number of unknown word */
+
+ public final static int ANIMATION_INT = 10; /* animation interval */
+
+ public final static int SECONDS = 11;
+
+ public final static int MINUTES = 12; /* time since game start */
+
+ public final static int HOURS = 13;
+
+ public final static int DAYS = 14;
+
+ public final static int DBL_CLK_DELAY = 15;
+
+ public final static int CURRENT_EGO = 16;
+
+ public final static int ERROR_NUM = 17;
+
+ public final static int ERROR_PARAM = 18;
+
+ public final static int LAST_CHAR = 19;
+
+ public final static int MACHINE_TYPE = 20;
+
+ public final static int PRINT_TIMEOUT = 21;
+
+ public final static int NUM_VOICES = 22;
+
+ public final static int ATTENUATION = 23;
+
+ public final static int INPUTLEN = 24;
+
+ public final static int SELECTED_OBJ = 25; /* selected object number */
+
+ public final static int MONITOR_TYPE = 26;
+
+
+ /* ------------------------ System flags ------------------------ */
+
+ public final static int ONWATER = 0; /* on.water */
+
+ public final static int SEE_EGO = 1; /* can we see ego? */
+
+ public final static int INPUT = 2; /* have.input */
+
+ public final static int HITSPEC = 3; /* hit.special */
+
+ public final static int HADMATCH = 4; /* had a word match */
+
+ public final static int INITLOGS = 5; /* signal to init logics */
+
+ public final static int RESTART = 6; /* is a restart in progress? */
+
+ public final static int NO_SCRIPT = 7; /* don't add to the script buffer */
+
+ public final static int DBL_CLK = 8; /* enable double click on joystick */
+
+ public final static int SOUNDON = 9; /* state of sound playing */
+
+ public final static int TRACE_ENABLE = 10; /* to enable tracing */
+
+ public final static int HAS_NOISE = 11; /* does machine have noise channel */
+
+ public final static int RESTORE = 12; /* restore game in progress */
+
+ public final static int ENABLE_SELECT = 13; /* allow selection of objects from inventory screen */
+
+ public final static int ENABLE_MENU = 14;
+
+ public final static int LEAVE_WIN = 15; /* leave windows on the screen */
+
+ public final static int NO_PRMPT_RSTRT = 16; /* don't prompt on restart */
+
+
+ /* ------------------------ Miscellaneous ------------------------ */
+
+ public final static int NUMVARS = 256; /* number of vars */
+
+ public final static int NUMFLAGS = 256; /* number of flags */
+
+ public final static int NUMCONTROL = 50; /* number of controllers */
+
+ public final static int NUMWORDS = 10; /* maximum # of words recognized in input */
+
+ public final static int NUMANIMATED = 256; /* maximum # of animated objects */
+
+ public final static int MAXVAR = 255; /* maximum value for a var */
+
+ public final static int TEXTCOLS = 40; /* number of columns of text */
+
+ public final static int TEXTLINES = 25; /* number of lines of text */
+
+ public final static int MAXINPUT = 40; /* maximum length of user input */
+
+ public final static int DIALOGUE_WIDTH = 35; /* maximum width of dialog box */
+
+ public final static int NUMSTRINGS = 24; /* number of user-definable strings */
+
+ public final static int STRLENGTH = 40; /* maximum length of user strings */
+
+ public final static int GLSIZE = 40; /* maximum length for GetLine calls, used internally for things like save dialog */
+
+ public final static int PROMPTSTR = 0; /* string number of prompt */
+
+ public final static int ID_LEN = 7; /* length of gameID string */
+
+ public final static int MAXDIST = 50; /* maximum movement distance */
+
+ public final static int MINDIST = 6; /* minimum movement distance */
+
+ public final static int BACK_MOST_PRIORITY = 4; /* priority value of back most priority */
+
+
+ /* ------------------------ Inventory item final staticants --------------------------- */
+
+ public final static int LIMBO = 0; /* room number of objects that are gone */
+
+ public final static int CARRYING = 255; /* room number of objects in ego's posession */
+
+
+ /* ------------------------ Default status and input row numbers ------------------------ */
+
+ public final static int STATUSROW = 21;
+
+ public final static int INPUTROW = 23;
+
+
+ /* ------------------------ Screen edges ------------------------ */
+
+ public final static int TOP = 1;
+
+ public final static int RIGHT = 2;
+
+ public final static int BOTTOM = 3;
+
+ public final static int LEFT = 4;
+
+ public final static int MINX = 0;
+
+ public final static int MINY = 0;
+
+ public final static int MAXX = 159;
+
+ public final static int MAXY = 167;
+
+ public final static int HORIZON = 36;
+
+}
diff --git a/core/src/main/java/com/agifans/agile/Detection.java b/core/src/main/java/com/agifans/agile/Detection.java
new file mode 100644
index 0000000..06d29e3
--- /dev/null
+++ b/core/src/main/java/com/agifans/agile/Detection.java
@@ -0,0 +1,323 @@
+package com.agifans.agile;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+
+import com.agifans.agile.agilib.Game;
+
+/**
+ * The Detection class handles detection of AGI games, demos and fan made games.
+ */
+public class Detection {
+
+ /**
+ * The short ID of the game, as known by the AGILE interpreter. For fan made games, it is always "fanmade".
+ */
+ public String gameId = "unknown";
+
+ /**
+ * The displayable name of the game.
+ */
+ public String gameName = "Unrecognised game";
+
+ /**
+ * Constructor for Detection.
+ *
+ * @param game
+ */
+ public Detection(Game game) {
+ try {
+ StringBuilder dirFilePath = new StringBuilder();
+ dirFilePath.append(game.gameFolder);
+ dirFilePath.append(File.separator);
+ if (game.v3GameSig != null) {
+ dirFilePath.append(game.v3GameSig.toUpperCase());
+ dirFilePath.append("DIR");
+ }
+ else {
+ dirFilePath.append("LOGDIR");
+ }
+
+ // Calculate MD5 hash of the game.
+ byte[] data = readBytesFromFile(dirFilePath.toString());
+ byte[] hash = MessageDigest.getInstance("MD5").digest(data);
+ String md5HashString = new BigInteger(1, hash).toString(16);
+
+ // Compare with known MD5 hash values for AGI games and demos.
+ for (int i = 0; i < gameDefinitions.length; i++) {
+ if (gameDefinitions[i][2].equals(md5HashString)) {
+ gameId = gameDefinitions[i][0];
+ gameName = gameDefinitions[i][1];
+ break;
+ }
+ }
+ }
+ catch (Exception e) {
+ // Failure in game detection code. Continue with the default unrecognised game values.
+ e.printStackTrace();
+ }
+ }
+
+ private byte[] readBytesFromFile(String filePath) throws FileNotFoundException, IOException {
+ try (FileInputStream fis = new FileInputStream(filePath)) {
+ int numOfBytesReads;
+ byte[] data = new byte[256];
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ while ((numOfBytesReads = fis.read(data, 0, data.length)) != -1) {
+ buffer.write(data, 0, numOfBytesReads);
+ }
+ return buffer.toByteArray();
+ }
+ }
+
+ /**
+ * MD5 hash values for known games and demos.
+ */
+ private static String[][] gameDefinitions = {
+ {"agidemo", "AGI Demo 1 (1987-05-20)", "9c4a5b09cc3564bc48b4766e679ea332"},
+ {"agidemo", "AGI Demo 2 (1987-11-24 3.5\")", "e8ebeb0bbe978172fe166f91f51598c7"},
+ {"agidemo", "AGI Demo 2 (1987-11-24 [version 1] 5.25\")", "852ac303a374df62571642ca1e2d1f0a"},
+ {"agidemo", "AGI Demo 2 (1987-11-25 [version 2] 5.25\")", "1503f02086ea9f388e7e041c039eaa69"},
+ {"agidemo", "AGI Demo 2 (Tandy)", "94eca021fe7da8f8572c2edcc631bbc6"},
+ {"agidemo", "AGI Demo Kings Quest III and Space Quest I", "502e6bf96827b6c4d3e67c9cdccd1033"},
+ {"bc", "The Black Cauldron (2.00 1987-06-14)", "7f598d4712319b09d7bd5b3be10a2e4a"},
+ {"ddp", "Donald Duck's Playground (1.0A 1986-08-08)", "64388812e25dbd75f7af1103bc348596"},
+ {"ddp", "Donald Duck's Playground (1.0C 1986-06-09)", "550971d196f65190a5c760d2479406ef"},
+ {"ddp", "Donald Duck's Playground (1.50 1987-06-22)", "268074cc8cb75aa2227c4398886d7acd"},
+ {"kq1", "King's Quest I (2.0F 1987-05-05 5.25\"/3.5\")", "10ad66e2ecbd66951534a50aedcd0128"},
+ {"kq2", "King's Quest II (2.1 1987-04-10)", "759e39f891a0e1d86dd29d7de485c6ac"},
+ {"kq2", "King's Quest II (2.2 1987-05-07 5.25\"/3.5\")", "b944c4ff18fb8867362dc21cc688a283"},
+ {"kq3", "King's Quest III (1.01 1986-11-08)", "9c2b34e7ffaa89c8e2ecfeb3695d444b"},
+ {"kq3", "King's Quest III (2.00 1987-05-25 5.25\")", "18aad8f7acaaff760720c5c6885b6bab"},
+ {"kq3", "King's Quest III (2.00 1987-05-25 5.25\")", "b46dc63d6272fb6ed24a004ad580a033"},
+ {"kq3", "King's Quest III (2.14 1988-03-15 3.5\")", "d3d17b77b3b3cd13246749231d9473cd"},
+ {"lsl1", "Leisure Suit Larry (1.00 1987-06-01 5.25\"/3.5\")", "1fe764e66857e7f305a5f03ca3f4971d"},
+ {"mixedup", "Mixed Up Mother Goose (1987-11-10)", "e524655abf9b96a3b179ffcd1d0f79af"},
+ {"pq1", "Police Quest (2.0E 1987-11-17)", "2fd992a92df6ab0461d5a2cd83c72139"},
+ {"pq1", "Police Quest (2.0A 1987-10-23)", "b9dbb305092851da5e34d6a9f00240b1"},
+ {"pq1", "Police Quest (2.0G 1987-12-03 5.25\"/ST)", "231f3e28170d6e982fc0ced4c98c5c1c"},
+ {"pq1", "Police Quest (2.0G 1987-12-03)", "d194e5d88363095f55d5096b8e32fbbb"},
+ {"sq1", "Space Quest I (1.1A 1986-11-13)", "8d8c20ab9f4b6e4817698637174a1cb6"},
+ {"sq1", "Space Quest I (1.1A 720kb)", "0a92b1be7daf3bb98caad3f849868aeb"},
+ {"sq1", "Space Quest I (1.0X 1986-09-24)", "af93941b6c51460790a9efa0e8cb7122"},
+ {"sq1", "Space Quest I (2.2 1987-05-07 5.25\"/3.5\")", "5d67630aba008ec5f7f9a6d0a00582f4"},
+ {"sq2", "Space Quest II (2.0D 1988-03-14 3.5\")", "85390bde8958c39830e1adbe9fff87f3"},
+ {"sq2", "Space Quest II (2.0A 1987-11-06 5.25\")", "ad7ce8f800581ecc536f3e8021d7a74d"},
+ {"sq2", "Space Quest II (2.0A 1987-11-06 3.5\")", "6c25e33d23b8bed42a5c7fa63d588e5c"},
+ {"sq2", "Space Quest II (2.0C/A 5.25\"/ST)", "bd71fe54869e86945041700f1804a651"},
+ {"sq2", "Space Quest II (2.0F 1989-01-05 3.5\")", "28add5125484302d213911df60d2aded"},
+ {"xmascard", "Christmas Card (1986-11-13 [version 1])", "3067b8d5957e2861e069c3c0011bd43d"},
+ {"agidemo", "Demo 3 1988-09-13", "289c7a2c881f1d973661e961ced77d74"},
+ {"bc", "The Black Cauldron (2.10 1988-11-10 5.25\")", "0c5a9acbcc7e51127c34818e75806df6"},
+ {"bc", "The Black Cauldron (2.10 1988-11-10 3.5\")", "0de3953c9225009dc91e5b0d1692967b"},
+ {"goldrush", "Gold Rush! (2.01 1988-12-22 5.25\")", "db733d199238d4009a9e95f11ece34e9"},
+ {"goldrush", "Gold Rush! (2.01 1988-12-22 3.5\")", "6a285235745f69b4b421403659497216"},
+ {"goldrush", "Gold Rush! (2.01 1988-12-22)", "3ae052117feb483f01a9017025fbb366"},
+ {"goldrush", "Gold Rush! (2.01 1988-12-22)", "1ef85c37fcf7224f9731f20f169c8c53"},
+ {"goldrush", "Gold Rush! (3.0 1998-12-22 3.5\")", "6882b6090473209da4cd78bb59f78dbe"},
+ {"kq4", "King's Quest IV (2.0 1988-07-27)", "f50f7f997208ca0e35b2650baec43a2d"},
+ {"kq4", "King's Quest IV (2.0 1988-07-27 3.5\")", "fe44655c42f16c6f81046fdf169b6337"},
+ {"kq4", "King's Quest IV (2.2 1988-09-27 3.5\")", "7470b3aeb49d867541fc66cc8454fb7d"},
+ {"kq4", "King's Quest IV (2.3 1988-09-27)", "6d7714b8b61466a5f5981242b993498f"},
+ {"kq4", "King's Quest IV (2.3 1988-09-27 3.5\")", "82a0d39af891042e99ac1bd6e0b29046"},
+ {"kq4", "King's Quest IV Demo (1988-12-20)", "a3332d70170a878469d870b14863d0bf"},
+ {"mh1", "Manhunter: New York (1.22 1988-08-31)", "0c7b86f05fe02c2e26cff1b07450b82a"},
+ {"mh1", "Manhunter: New York (1.22 1988-08-31)", "5b625329021ad49fd0c1d6f2d6f54bba"},
+ {"mh2", "Manhunter: San Francisco (3.02 1989-07-26 5.25\")", "bbb2c2f88d5740f7437fb7aa6f080b7b"},
+ {"mh2", "Manhunter: San Francisco (3.02 1989-07-26 3.5\")", "6fb6f0ee2437704c409cf17e081ba152"},
+ {"mh2", "Manhunter: San Francisco (3.03 1989-08-17 5.25\")", "b90e4795413c43de469a715fb3c1fa93"},
+ {"fanmade","Advanced Epic Fighting", "6454e8c82a7351c8eef5927ad306af4f"},
+ {"fanmade","AGI Combat", "0be6a8a9e19203dcca0067d280798871"},
+ {"fanmade","AGI Combat (Beta)", "341a47d07be8490a488d0c709578dd10"},
+ {"fanmade","AGI Contest 1 Template", "d879aed25da6fc655564b29567358ae2"},
+ {"fanmade","AGI Contest 2 Template", "5a2fb2894207eff36c72f5c1b08bcc07"},
+ {"fanmade","AGI Piano (v1.0)", "8778b3d89eb93c1d50a70ef06ef10310"},
+ {"fanmade","AGI Quest (v1.46-TJ0)", "1cf1a5307c1a0a405f5039354f679814"},
+ {"fanmade","AGI Tetris (1998)", "1afcbc25bfafded2d5fb82de9da0bd9a"},
+ {"fanmade","AGI Trek (Demo)", "c02882b8a8245b629c91caf7eb78eafe"},
+ {"fanmade","Acidopolis", "7017db1a4b726d0d59e65e9020f7d9f7"},
+ {"fanmade","Agent 0055 (v1.0)", "c2b34a0c77acb05482781dda32895f24"},
+ {"fanmade","Agent 06 vs. The Super Nazi", "136f89ca9f117c617e88a85119777529"},
+ {"fanmade","Agent Quest", "59e49e8f72058a33c00d60ee1097e631"},
+ {"fanmade","Al Pond - On Holiday (v1.0)", "a84975496b42d485920e886e92eed68b"},
+ {"fanmade","Al Pond - On Holiday (v1.1)", "7c95ac4689d0c3bfec61e935f3093634"},
+ {"fanmade","Al Pond - On Holiday (v1.3)", "8f30c260de9e1dd3d8b8f89cc19d2633"},
+ {"fanmade","Al Pond 1 - Al Lives Forever (v1.0)", "e8921c3043b749b056ff51f56d1b451b"},
+ {"fanmade","Al Pond 1 - Al Lives Forever (v1.3)", "fb4699474054962e0dbfb4cf12ca52f6"},
+ {"fanmade","Apocalyptic Quest (v0.03 Teaser)", "42ced528b67965d3bc3b52c635f94a57"},
+ {"fanmade","Apocalyptic Quest Demo 2003-06-24", "c68c49a37eaac73e5aa80ce7f05bbd72"},
+ {"fanmade","Apocalyptic Quest 4.00 Alpha 2", "30c74d194840abc3fb1341b567743ac3"},
+ {"fanmade","Beyond the Titanic 2", "9b8de38dc64ffb3f52b7877ea3ebcef9"},
+ {"fanmade","Biri Quest 1", "1b08f34f2c43e626c775c9d6649e2f17"},
+ {"fanmade","Bob The Farmboy", "e4b7df9d0830addee5af946d380e66d7"},
+ {"fanmade","Botz", "a8fabe4e807adfe5ec02bfec6d983695"},
+ {"fanmade","Brian's Quest (v1.0)", "0964aa79b9cdcff7f33a12b1d7e04b9c"},
+ {"fanmade","CPU-21 (v1.0)", "35b7cdb4d17e890e4c52018d96e9cbf4"},
+ {"fanmade","Car Driver (v1.1)", "2311611d2d36d20ccc9da806e6cba157"},
+ {"fanmade","Cloak of Darkness (v1.0)", "5ba6e18bf0b53be10db8f2f3831ee3e5"},
+ {"fanmade","Coco Coq (English) - Coco Coq In Grostesteing's Base (v.1.0.3)", "97631f8e710544a58bd6da9e780f9320"},
+ {"fanmade","Coco Coq (French) - Coco Coq Dans la Base de Grostesteing (v1.0.2)", "ef579ebccfe5e356f9a557eb3b2d8649"},
+ {"fanmade","Corby's Murder Mystery (v1.0)", "4ebe62ac24c5a8c7b7898c8eb070efe5"},
+ {"fanmade","DG: The Adventure Game (v1.1)", "0d6376d493fa7a21ec4da1a063e12b25"},
+ {"fanmade","DG: The Adventure Game (v1.1)", "258bdb3bb8e61c92b71f2f456cc69e23"},
+ {"fanmade","Dashiki (16 Colors)", "9b2c7b9b0283ab9f12bedc0cb6770a07"},
+ {"fanmade","Date Quest 1 (v1.0)", "ba3dcb2600645be53a13170aa1a12e69"},
+ {"fanmade","Date Quest 2 (v1.0 Demo)", "1602d6a2874856e928d9a8c8d2d166e9"},
+ {"fanmade","Date Quest 2 (v1.0)", "f13f6fc85aa3e6e02b0c20408fb63b47"},
+ {"fanmade","Dave's Quest (v0.07)", "f29c3660de37bacc1d23547a167f27c9"},
+ {"fanmade","Dave's Quest (v0.17)", "da3772624cc4a86f7137db812f6d7c39"},
+ {"fanmade","Disco Nights (Demo)", "dc5a2b21182ba38bdcd992a3a978e690"},
+ {"fanmade","Dogs Quest - The Quest for the Golden Bone (v1.0)", "f197357edaaea0ff70880602d2f09b3e"},
+ {"fanmade","Dr. Jummybummy's Space Adventure", "988bd81785f8a452440a2a8ac67f96aa"},
+ {"fanmade","Ed Ward", "98be839b9f30cbedea4c9cee5442d827"},
+ {"fanmade","Elfintard", "c3b847e9e9e978af9708df76a0751dc2"},
+ {"fanmade","Enclosure (v1.01)", "f08e66fee9ecdde77db7ee9a10c96ba2"},
+ {"fanmade","Enclosure (v1.03)", "e4a0613ed02401502e506ba3565a8c40"},
+ {"fanmade","Epic Fighting (v0.1)", "aff24a1b3bdd676187685c4d95ba4294"},
+ {"fanmade","Escape Quest (v0.0.3)", "2346b65619b1da0298b715b06d1a45a1"},
+ {"fanmade","Escape from the Desert (beta 1)", "dfdc634d340854bd6ece28024010758d"},
+ {"fanmade","Escape from the Salesman", "e723ca4fe0f6f56affe039fbb4dbeb6c"},
+ {"fanmade","Fu$k Quest 2 - Romancing the Bone (Teaser)", "d288355d71d9bb1639260ccaa3b2fbfe"},
+ {"fanmade","Fu$k Quest 2 - Romancing the Bone", "294beeb7765c7ea6b05ed7b9bf7bff4f"},
+ {"fanmade","Gennadi Tahab Autot - Mission Pack 1 - Kuressaare", "bfa5fe71978e6ccf3d4eedd430124015"},
+ {"fanmade","Go West, Young Hippie", "ff31484ea465441cb5f3a0f8e956b716"},
+ {"fanmade","Good Man (demo v3.41)", "3facd8a8f856b7b6e0f6c3200274d88c"},
+ {"fanmade","Good Man (demo v4.0)", "d36f5d98cfcfd28cf7d4103906c59a77"},
+ {"fanmade","Good Man (demo v4.0T)", "8184f70a5a33d4f407dfc8e9ddab99e9"},
+ {"fanmade","Hank's Quest (v1.0 English) - Victim of Society", "64c15b3d0483d17888129100dc5af213"},
+ {"fanmade","Hank's Quest (v1.1 English) - Victim of Society", "86d1f1dd9b0c4858d096e2a60cca8a14"},
+ {"fanmade","Hank's Quest (v1.81 Dutch) - Slachtoffer Van Het Gebeuren", "41e53972d55ff3dff9e90d15fe1b659f"},
+ {"fanmade","Hank's Quest (v1.81 English) - Victim of Society", "7a776383282f62a57c3a960dafca62d1"},
+ {"fanmade","Herbao (v0.2)", "6a5186fc8383a9060517403e85214fc2"},
+ {"fanmade","Hobbits", "4a1c1ef3a7901baf0ab45fde0cfadd89"},
+ {"fanmade","Jack & Julia - VAMPYR", "8aa0b9a26f8d5a4421067ab8cc3706f6"},
+ {"fanmade","Jeff's Quest (v.5 alpha Jun 1)", "10f1720eed40c12b02a0f32df3e72ded"},
+ {"fanmade","Jeff's Quest (v.5 alpha May 31)", "51ff71c0ed90db4e987a488ed3bf0551"},
+ {"fanmade","Jen's Quest (Demo 1)", "361afb5bdb6160213a1857245e711939"},
+ {"fanmade","Jen's Quest (Demo 2)", "3c321eee33013b289ab8775449df7df2"},
+ {"fanmade","Jiggy Jiggy Uh! Uh!", "bc331588a71e7a1c8840f6cc9b9487e4"},
+ {"fanmade","Jimmy In: The Alien Attack (v0.1)", "a4e9db0564a494728de7873684a4307c"},
+ {"fanmade","Joe McMuffin In \"What's Cooking, Doc\" (v1.0)", "8a3de7e61a99cb605fa6d233dd91c8e1"},
+ {"fanmade","Journey Of Chef", "aa0a0b5a6364801ae65fdb96d6741df5"},
+ {"fanmade","Jukebox (v1.0)", "c4b9c5528cc67f6ba777033830de7751"},
+ {"fanmade","Justin Quest (v1.0 in development)", "103050989da7e0ffdc1c5e1793a4e1ec"},
+ {"fanmade","Jõulumaa (v0.05)", "53982ecbfb907e41392b3961ad1c3475"},
+ {"fanmade","Kings Quest 2 - Breast Intentions (v2.0 Mar 26)", "a25d7379d281b1b296d4785df90a8e78"},
+ {"fanmade","Kings Quest 2 - Breast Intentions (v2.0 Aug 16)", "6b4f796d0421d2e12e501b511962e03a"},
+ {"fanmade","Lasse Holm: The Quest for Revenge (v1.0)", "f9fbcc8a4ef510bfbb92423296ff4abb"},
+ {"fanmade","Lawman for Hire", "c78b28bfd3767dd455b992cd8b7854fa"},
+ {"fanmade","Lefty Goes on Vacation (Not in The Right Place)", "ccdc49a33870310b01f2c48b8a1f3c34"},
+ {"fanmade","Les Inseparables (v1.0)", "4b780887cab0ecabc5eca319acb3acf2"},
+ {"fanmade","Little Pirate (Demo 2 v0.6)", "437068efe4ec32d436da09d6f2ea56e1"},
+ {"fanmade","Lost Eternity (v1.0)", "95f15c5632feb8a39e9ca3d9af35fcc9"},
+ {"fanmade","MD Quest - The Search for Michiel (v0.10)", "2a6fcb21d2b5e4144c38ed817fabe8ee"},
+ {"fanmade","Maale Adummin Quest", "ddfbeb33feb7cf78504fe4dba14ec63b"},
+ {"fanmade","Monkey Man", "2322d03f997e8cc235d4578efff69cfa"},
+ {"fanmade","Naturette 1 (English v1.2)", "0a75884e7f010974a230bdf269651117"},
+ {"fanmade","Naturette 1 (English v1.3)", "f15bbf999ac55ebd404aa1eb84f7c1d9"},
+ {"fanmade","Naturette 1 (French v1.2)", "d3665622cc41aeb9c7ecf4fa43f20e53"},
+ {"fanmade","New AGI Hangman Test", "d69c0e9050ccc29fd662b74d9fc73a15"},
+ {"fanmade","Nick's Quest - In Pursuit of QuakeMovie (v2.1 Gold)", "e29cbf9222551aee40397fabc83eeca0"},
+ {"fanmade","Operation: Recon", "0679ce8405411866ccffc8a6743370d0"},
+ {"fanmade","Patrick's Quest (Demo v1.0)", "f254f5b894b98fec5f92acc07fb62841"},
+ {"fanmade","Phantasmagoria", "87d20c1c11aee99a4baad3797b63146b"},
+ {"fanmade","Pharaoh Quest (v0.0)", "51c630899d076cf799e573dadaa2276d"},
+ {"fanmade","Phil's Quest - the Search for Tolbaga", "5e7ca45c360e03164b8358e49900c588"},
+ {"fanmade","Pinkun Maze Quest (v0.1)", "148ff0843af389928b3939f463bfd20d"},
+ {"fanmade","Pirate Quest", "bb612a919ed2b9ea23bbf03ce69fed42"},
+ {"fanmade","Pothead Quest (v0.1)", "d181101385d3a45082f418cd4b3c5b01"},
+ {"fanmade","President's Quest", "4937d0e8ecadb7888faeb347799b0388"},
+ {"fanmade","Prince Quest", "266248d75c3130c8ccc9c9bf2ad30a0d"},
+ {"fanmade","Professor (English) - The Professor is Missing (Mar 17)", "6232de31cc204affdf2e92dfe3dc0e4d"},
+ {"fanmade","Professor (English) - The Professor is Missing (Mar 22)", "b5fcf0ca2f0d1c073be82f01e2170961"},
+ {"fanmade","Professor (French) - Le Professeur a Disparu", "7d9f8a4d4610bb9b0b97caa17590c2d3"},
+ {"fanmade","Quest for Glory VI - Hero's Adventure", "d26765c3075064c80d284c5e06e33a7e"},
+ {"fanmade","Quest for Home", "d2895dc1cd3930f2489af0f843b144b3"},
+ {"fanmade","Quest for Ladies (demo v1.1 Apr 1)", "3f6e02f16e1154a0daf296c8895edd97"},
+ {"fanmade","Quest for Ladies (demo v1.1 Apr 6)", "f75e7b6a0769a3fa926eea0854711591"},
+ {"fanmade","Quest for Piracy 1 - Enter the Silver Pirate (v0.15)", "d23f5c2a26f6dc60c686f8a2436ea4a6"},
+ {"fanmade","Quest for a Record Deal", "f4fbd7abf056d2d3204f790da5ac89ab"},
+ {"fanmade","Ralph's Quest (v0.1)", "5cf56378aa01a26ec30f25295f0750ca"},
+ {"fanmade","Residence 44 Quest (v0.99)", "7c5cc64200660c70240053b33d379d7d"},
+ {"fanmade","Residence 44 Quest (v0.99)", "fe507851fddc863d540f2bec67cc67fd"},
+ {"fanmade","Residence 44 Quest (v1.0a)", "f99e3f69dc8c77a45399da9472ef5801"},
+ {"fanmade","SQ2Eye (v0.3)", "2be2519401d38ad9ce8f43b948d093a3"},
+ {"fanmade","SQ2Eye (v0.41)", "f0e82c55f10eb3542d7cd96c107ae113"},
+ {"fanmade","SQ2Eye (v0.42)", "d7beae55f6328ef8b2da47b1aafea40c"},
+ {"fanmade","SQ2Eye (v0.43)", "2a895f06e45de153bb4b77c982009e06"},
+ {"fanmade","SQ2Eye (v0.44)", "5174fc4b6d8a477ba0ff0575cd64e0aa"},
+ {"fanmade","SQ2Eye (v0.45)", "6e06f8bb7b90ce6f6aabf1a0e620159c"},
+ {"fanmade","SQ2Eye (v0.46)", "bf0ad7a035ff9113951d09d1efe380c4"},
+ {"fanmade","SQ2Eye (v0.47)", "85dc3be1d33ff932c292b74f9037abaa"},
+ {"fanmade","SQ2Eye (v0.48)", "587574252972a5b5c070a647973a9b4a"},
+ {"fanmade","SQ2Eye (v0.481)", "fc9234beb49804ae869696ce5af8ef30"},
+ {"fanmade","SQ2Eye (v0.482)", "3ed84b7b87fa6840f25c15f250a11ffb"},
+ {"fanmade","SQ2Eye (v0.483)", "647c31298d3f9cda641231b893e347c0"},
+ {"fanmade","SQ2Eye (v0.484)", "f2c86fae7b9046d408c62c8c49a4b882"},
+ {"fanmade","SQ2Eye (v0.485)", "af59e36bc28f44545458b68a93e91e67"},
+ {"fanmade","SQ2Eye (v0.486)", "3fd86436e93456770dbdd4593eded70a"},
+ {"fanmade","Sarien", "314e5fdef17b803226d1de3af2e997ea"},
+ {"fanmade","Save Santa (v1.0)", "4644f6beb5802081772f14be56ae196c"},
+ {"fanmade","Save Santa (v1.3)", "f8afdb6efc5af5e7c0228b44633066af"},
+ {"fanmade","Schiller (preview 1)", "ade39dea968c959cfebe1cf935d653e9"},
+ {"fanmade","Schiller (preview 2)", "62cd1f8fc758bf6b4aa334e553624cef"},
+ {"fanmade","Shifty (v1.0)", "2a07984d27b938364bf6bd243ac75080"},
+ {"fanmade","Snowboarding Demo (v1.0)", "24bb8f29f1eddb5c0a099705267c86e4"},
+ {"fanmade","Solar System Tour", "b5a3d0f392dfd76a6aa63f3d5f578403"},
+ {"fanmade","Sorceror's Appraisal", "fe62615557b3cb7b08dd60c9d35efef1"},
+ {"fanmade","Space Trek (v1.0)", "807a1aeadb2ace6968831d36ab5ea37a"},
+ {"fanmade","Special Delivery", "88764dfe61126b8e73612c851b510a33"},
+ {"fanmade","Speeder Bike Challenge (v1.0)", "2deb25bab379285ca955df398d96c1e7"},
+ {"fanmade","Star Commander 1 - The Escape (v1.0)", "a7806f01e6fa14ebc029faa58f263750"},
+ {"fanmade","Star Pilot: Bigger Fish", "8cb26f8e1c045b75c6576c839d4a0172"},
+ {"fanmade","Tales of the Tiki", "8103c9c87e3964690a14a3d0d83f7ddc"},
+ {"fanmade","Tex McPhilip 1 - Quest For The Papacy", "3c74b9a24b51aa8020ac82bee3132266"},
+ {"fanmade","Tex McPhilip 2 - Road To Divinity (v1.5)", "7387e8df854440bc26620ca0ea43af9a"},
+ {"fanmade","Tex McPhilip 3 - A Destiny of Sin (Demo v0.25)", "992d12031a486ad84e592ff5d7c9d782"},
+ {"fanmade","Tex McPhilip 3 - A Destiny of Sin (v1.02)", "587d15e1106e59c33053c01b301ffe05"},
+ {"fanmade","The 13th Disciple (v1.00)", "887719ad59afce9a41ec057dbb73ad73"},
+ {"fanmade","The 13th Disciple (v1.01)", "58e3ec1b9ac1a79901c472aaa59db832"},
+ {"fanmade","The Adventures of a Crazed Hermit", "6e3086cbb794d3299a9c5a9792295511"},
+ {"fanmade","The Gourd of the Beans", "246f4d94946afb547482d44a53616d06"},
+ {"fanmade","The Grateful Dead", "c2146631afacf8cb455ce24f3d2d46e7"},
+ {"fanmade","The Legend of Shay-Larah 1 - The Lost Prince", "04e720c8e30c9cf12db22ea14a24a3dd"},
+ {"fanmade","The Legend of Zelda: The Fungus of Time (Demo v1.00)", "dcaf8166ceb62a3d9b9aea7f3b197c09"},
+ {"fanmade","The Legendary Harry Soupsmith (Demo 1998 Apr 2)", "64c46b0d6fc135c9835afa80980d2831"},
+ {"fanmade","The Legendary Harry Soupsmith (Demo 1998 Aug 19)", "8d06d82970f2c591d880a95476efbcf0"},
+ {"fanmade","The Long Haired Dude: Encounter of the 18-th Kind", "86ea17b9fc2f3e537a7e40863d352c29"},
+ {"fanmade","The Lost Planet (v0.9)", "590dffcbd932a9fbe554be13b769cac0"},
+ {"fanmade","The Lost Planet (v1.0)", "58564df8b6394612dd4b6f5c0fd68d44"},
+ {"fanmade","The New Adventure of Roger Wilco (v1.00)", "e5f0a7cb8d49f66b89114951888ca688"},
+ {"fanmade","The Ruby Cast (v0.02)", "ed138e461bb1516e097007e017ab62df"},
+ {"fanmade","The Shadow Plan", "c02cd10267e721f4e836b1431f504a0a"},
+ {"fanmade","The Sorceror's Appraisal", "b121ba95d2beb6c16e2f762a13b8baa2"},
+ {"fanmade","Time Quest (Demo v0.1)", "12e1a6f03ea4b8c5531acd0400b4ed8d"},
+ {"fanmade","Time Quest (Demo v0.2)", "7b710608abc99e0861ac59b967bf3f6d"},
+ {"fanmade","Toby's World (Demo)", "3f8ebea0eb32303e65e2a6e8341c6741"},
+ {"fanmade","Tonight The Shrieking Corpses Bleed (Demo v0.11)", "bcc57a7c8d563fa0c333107ae1c0a6e6"},
+ {"fanmade","Tonight The Shrieking Corpses Bleed (v1.01)", "36b38f621b38e8d104aa0807302dc8c9"},
+ {"fanmade","Turks' Quest - Heir to the Planet", "3d19254b737c8b218e5bc4580542b79a"},
+ {"fanmade","Ultimate AGI Fangame (Demo)", "2d14d6fa2a2136d681e46e06821905bf"},
+ {"fanmade","URI Quest (v0.173 Feb 27)", "3986eefcf546dafc45f920ae91a697c3"},
+ {"fanmade","URI Quest (v0.173 Jan 29)", "494150940d34130605a4f2e67ee40b12"},
+ {"fanmade","V - The Graphical Adventure", "c71f5c1e008d352ae9040b77fcf79327"},
+ {"fanmade","Voodoo Girl - Queen of the Darned (v1.2 2002 Jan 1)", "ae95f0c77d9a97b61420fd192348b937"},
+ {"fanmade","Voodoo Girl - Queen of the Darned (v1.2 2002 Mar 29)", "11d0417b7b886f963d0b36789dac4c8f"},
+ {"fanmade","Wizaro (v0.1)", "abeec1eda6eaf8dbc52443ea97ff140c"},
+ {"tetris", "", "7a874e2db2162e7a4ce31c9130248d8a"},
+ {"caitlyn", "Demo", "5b8a3cdb2fc05469f8119d49f50fbe98"},
+ {"caitlyn", "", "818469c484cae6dad6f0e9a353f68bf8"},
+ {"fanmade", "Get Outta Space Quest", "aaea5b4a348acb669d13b0e6f22d4dc9"},
+ {"sq0", "v1.03", "d2fd6f7404e86182458494e64375e590"},
+ {"sq0", "v1.04", "2ad9d1a4624a98571ee77dcc83f231b6"},
+ {"sq0", "", "e1a8e4efcce86e1efcaa14633b9eb986"},
+ {"sqx", "v10.0 Feb 05", "c992ae2f8ab18360404efdf16fa9edd1"},
+ {"sqx", "v10.0 Jul 18", "812edec45cefad559d190ffde2f9c910"},
+ {"sqx", "v10.0", "f0a59044475a5fa37c055d8c3eb4d1a7"}
+ };
+}
diff --git a/core/src/main/java/com/agifans/agile/EgaPalette.java b/core/src/main/java/com/agifans/agile/EgaPalette.java
new file mode 100644
index 0000000..bc721c5
--- /dev/null
+++ b/core/src/main/java/com/agifans/agile/EgaPalette.java
@@ -0,0 +1,106 @@
+package com.agifans.agile;
+
+import java.awt.Color;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This class holds the 16 colours that make up the EGA palette.
+ *
+ * @author Lance Ewing
+ */
+public class EgaPalette {
+
+ // The Color constants for the 16 EGA colours (and also the transparent colour we use).
+ public final static Color BLACK = new Color(0x000000);
+ public final static Color BLUE = new Color(0x0000AA);
+ public final static Color GREEN = new Color(0x00AA00);
+ public final static Color CYAN = new Color(0x00AAAA);
+ public final static Color RED = new Color(0xAA0000);
+ public final static Color MAGENTA = new Color(0xAA00AA);
+ public final static Color BROWN = new Color(0xAA5500);
+ public final static Color GREY = new Color(0xAAAAAA);
+ public final static Color DARKGREY = new Color(0x555555);
+ public final static Color LIGHTBLUE = new Color(0x5555FF);
+ public final static Color LIGHTGREEN = new Color(0x55FF55);
+ public final static Color LIGHTCYAN = new Color(0x55FFFF);
+ public final static Color PINK = new Color(0xFF5555);
+ public final static Color LIGHTMAGENTA = new Color(0xFF55FF);
+ public final static Color YELLOW = new Color(0xFFFF55);
+ public final static Color WHITE = new Color(0xFFFFFF);
+
+ // JAGI RGB values
+ // 0x005454FC
+
+ // RGB values for use in colors array.
+ public final static int black = BLACK.getRGB();
+ public final static int blue = BLUE.getRGB();
+ public final static int green = GREEN.getRGB();
+ public final static int cyan = CYAN.getRGB();
+ public final static int red = RED.getRGB();
+ public final static int magenta = MAGENTA.getRGB();
+ public final static int brown = BROWN.getRGB();
+ public final static int grey = GREY.getRGB();
+ public final static int darkgrey = DARKGREY.getRGB();
+ public final static int lightblue = LIGHTBLUE.getRGB();
+ public final static int lightgreen = LIGHTGREEN.getRGB();
+ public final static int lightcyan = LIGHTCYAN.getRGB();
+ public final static int pink = PINK.getRGB();
+ public final static int lightmagenta = LIGHTMAGENTA.getRGB();
+ public final static int yellow = YELLOW.getRGB();
+ public final static int white = WHITE.getRGB();
+
+ private static short toRGB565(int argb8888) {
+ com.badlogic.gdx.graphics.Color color = new com.badlogic.gdx.graphics.Color();
+ com.badlogic.gdx.graphics.Color.argb8888ToColor(color, argb8888);
+ return (short)com.badlogic.gdx.graphics.Color.rgb565(color);
+ }
+
+ /**
+ * Holds a mapping from RGB8888 value to libgdx RGB565 value.
+ */
+ public final static Map RGB888_TO_RGB565_MAP = new HashMap<>();
+ static {
+ RGB888_TO_RGB565_MAP.put(black & 0xFFFFFF, toRGB565(black));
+ RGB888_TO_RGB565_MAP.put(blue & 0xFFFFFF, toRGB565(blue));
+ RGB888_TO_RGB565_MAP.put(green & 0xFFFFFF, toRGB565(green));
+ RGB888_TO_RGB565_MAP.put(cyan & 0xFFFFFF, toRGB565(cyan));
+ RGB888_TO_RGB565_MAP.put(red & 0xFFFFFF, toRGB565(red));
+ RGB888_TO_RGB565_MAP.put(magenta & 0xFFFFFF, toRGB565(magenta));
+ RGB888_TO_RGB565_MAP.put(brown & 0xFFFFFF, toRGB565(brown));
+ RGB888_TO_RGB565_MAP.put(grey & 0xFFFFFF, toRGB565(grey));
+ RGB888_TO_RGB565_MAP.put(darkgrey & 0xFFFFFF, toRGB565(darkgrey));
+ RGB888_TO_RGB565_MAP.put(lightblue & 0xFFFFFF, toRGB565(lightblue));
+ RGB888_TO_RGB565_MAP.put(lightgreen & 0xFFFFFF, toRGB565(lightgreen));
+ RGB888_TO_RGB565_MAP.put(lightcyan & 0xFFFFFF, toRGB565(lightcyan));
+ RGB888_TO_RGB565_MAP.put(pink & 0xFFFFFF, toRGB565(pink));
+ RGB888_TO_RGB565_MAP.put(lightmagenta & 0xFFFFFF, toRGB565(lightmagenta));
+ RGB888_TO_RGB565_MAP.put(yellow & 0xFFFFFF, toRGB565(yellow));
+ RGB888_TO_RGB565_MAP.put(white & 0xFFFFFF, toRGB565(white));
+ }
+
+ /**
+ * Holds the RGB values for the 16 EGA colours.
+ */
+ // TODO: Remove when satisfied that RGB565 is working.
+ //public final static int[] colours = { black, blue, green, cyan, red, magenta, brown, grey, darkgrey, lightblue, lightgreen, lightcyan, pink, lightmagenta, yellow, white };
+
+ public final static short[] colours = {
+ RGB888_TO_RGB565_MAP.get(black & 0xFFFFFF),
+ RGB888_TO_RGB565_MAP.get(blue & 0xFFFFFF),
+ RGB888_TO_RGB565_MAP.get(green & 0xFFFFFF),
+ RGB888_TO_RGB565_MAP.get(cyan & 0xFFFFFF),
+ RGB888_TO_RGB565_MAP.get(red & 0xFFFFFF),
+ RGB888_TO_RGB565_MAP.get(magenta & 0xFFFFFF),
+ RGB888_TO_RGB565_MAP.get(brown & 0xFFFFFF),
+ RGB888_TO_RGB565_MAP.get(grey & 0xFFFFFF),
+ RGB888_TO_RGB565_MAP.get(darkgrey & 0xFFFFFF),
+ RGB888_TO_RGB565_MAP.get(lightblue & 0xFFFFFF),
+ RGB888_TO_RGB565_MAP.get(lightgreen & 0xFFFFFF),
+ RGB888_TO_RGB565_MAP.get(lightcyan & 0xFFFFFF),
+ RGB888_TO_RGB565_MAP.get(pink & 0xFFFFFF),
+ RGB888_TO_RGB565_MAP.get(lightmagenta & 0xFFFFFF),
+ RGB888_TO_RGB565_MAP.get(yellow & 0xFFFFFF),
+ RGB888_TO_RGB565_MAP.get(white & 0xFFFFFF)
+ };
+}
diff --git a/core/src/main/java/com/agifans/agile/GameScreen.java b/core/src/main/java/com/agifans/agile/GameScreen.java
new file mode 100644
index 0000000..a48229a
--- /dev/null
+++ b/core/src/main/java/com/agifans/agile/GameScreen.java
@@ -0,0 +1,59 @@
+package com.agifans.agile;
+
+import com.badlogic.gdx.graphics.Pixmap;
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.utils.BufferUtils;
+
+public class GameScreen {
+
+ /**
+ * The pixels array for the AGI screen. Any change made to this array will be copied
+ * to the Pixmap on every frame.
+ */
+ private short[] pixels;
+
+ private Pixmap screenPixmap;
+ private Texture[] screens;
+ private int drawScreen = 1;
+ private int updateScreen = 0;
+
+ /**
+ * Constructor for GameScreen.
+ */
+ public GameScreen() {
+ // Uses an approach used successfully in my various libgdx emulators.
+ pixels = new short[320 * 200];
+ screenPixmap = new Pixmap(320, 200, Pixmap.Format.RGB565);
+ screens = new Texture[3];
+ screens[0] = new Texture(screenPixmap, Pixmap.Format.RGB565, false);
+ screens[0].setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);
+ screens[1] = new Texture(screenPixmap, Pixmap.Format.RGB565, false);
+ screens[1].setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);
+ screens[2] = new Texture(screenPixmap, Pixmap.Format.RGB565, false);
+ screens[2].setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);
+ }
+
+ public boolean render() {
+ // TODO: After implementing web worker/separate background thread, need to response to message instead.
+ BufferUtils.copy(pixels, 0, screenPixmap.getPixels(), 320 * 200);
+ screens[updateScreen].draw(screenPixmap, 0, 0);
+ updateScreen = (updateScreen + 1) % 3;
+ drawScreen = (drawScreen + 1) % 3;
+ return true;
+ }
+
+ public short[] getPixels() {
+ return pixels;
+ }
+
+ public Texture getDrawScreen() {
+ return screens[drawScreen];
+ }
+
+ public void dispose() {
+ screenPixmap.dispose();
+ screens[0].dispose();
+ screens[1].dispose();
+ screens[2].dispose();
+ }
+}
diff --git a/core/src/main/java/com/agifans/agile/GameState.java b/core/src/main/java/com/agifans/agile/GameState.java
new file mode 100644
index 0000000..18aa564
--- /dev/null
+++ b/core/src/main/java/com/agifans/agile/GameState.java
@@ -0,0 +1,463 @@
+package com.agifans.agile;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+import com.agifans.agile.agilib.Game;
+import com.agifans.agile.agilib.Logic;
+import com.agifans.agile.agilib.Objects;
+import com.agifans.agile.agilib.Picture;
+import com.agifans.agile.agilib.Sound;
+import com.agifans.agile.agilib.View;
+import com.agifans.agile.agilib.Words;
+
+/**
+ * The GameState class holds all of the data and state for the Game currently
+ * being run by the interpreter.
+ */
+public class GameState {
+
+ /**
+ * The Game whose data we are interpreting.
+ */
+ private Game game;
+
+ public Logic[] logics;
+ public Picture[] pictures;
+ public View[] views;
+ public Sound[] sounds;
+ public Objects objects;
+ public Words words;
+
+ /**
+ * Scan start values for each Logic. Index is the Logic number. We normally start
+ * scanning the Logic at position 0, but this can be set to another value via the
+ * set.scan.start AGI command. Note that only loaded logics can have their scan
+ * offset set. When they are unloaded, their scan offset is forgotten. Logic 0 is
+ * always loaded, so its scan start is never forgotten.
+ */
+ public int[] scanStart;
+
+ public boolean[] controllers;
+ public int[] vars;
+ public boolean[] flags;
+ public String[] strings;
+ public AnimatedObject[] animatedObjects;
+ public AnimatedObject ego;
+
+ /**
+ * The List of animated objects that currently have the DRAWN and UPDATE flags set.
+ */
+ public List updateObjectList;
+
+ /**
+ * The List of animated objects that have the DRAWN flag set but not the UPDATE flag.
+ */
+ public List stoppedObjectList;
+
+ /**
+ * A Map between a key event code and the matching controller number.
+ */
+ public Map keyToControllerMap;
+
+ /**
+ * For making random decisions.
+ */
+ public Random random = new Random();
+
+ /**
+ * The Picture that is currently drawn, i.e. the last one for which a draw.pic()
+ * command was executed. This will be a clone of an instance in the Pictures array,
+ * which may have subsequently had an overlay drawn on top of it.
+ */
+ public Picture currentPicture;
+
+ /**
+ * The pixel array for the visual data for the current Picture, where the values
+ * are the ARGB values. The dimensions of this are 320x168, i.e. two pixels per
+ * AGI pixel. Makes it easier to copy to the main pixels array when required.
+ */
+ public short[] visualPixels;
+
+ /**
+ * The pixel array for the priority data for the current Picture, where the values
+ * are from 4 to 15 (i.e. they are not ARGB values). The dimensions of this one
+ * are 160x168 as its usage is non-visual.
+ */
+ public int[] priorityPixels;
+
+ /**
+ * The pixel array for the control line data for the current Picture, where the
+ * values are from 0 to 4 (i.e. not ARGB values). The dimensions of this one
+ * are 160x168 as its usage is non-visual.
+ */
+ public int[] controlPixels;
+
+ /**
+ * Whether or not the picture is currently visible. This is set to true after a
+ * show.pic call. The draw.pic and overlay.pic commands both set it to false. It's
+ * value is used to determine whether to render the AnimatedObjects.
+ */
+ public boolean pictureVisible;
+
+ public boolean acceptInput;
+ public boolean userControl;
+ public boolean graphicsMode;
+ public boolean showStatusLine;
+ public int statusLineRow;
+ public int pictureRow;
+ public int inputLineRow;
+ public int horizon;
+ public int textAttribute;
+ public int foregroundColour;
+ public int backgroundColour;
+ public char cursorCharacter;
+ public long totalTicks;
+ public long animationTicks;
+ public boolean gamePaused;
+ public int currentLogNum;
+ public StringBuilder currentInput;
+ public String lastInput;
+ public String gameId;
+ public String version;
+ public int maxDrawn;
+ public int priorityBase;
+ public String simpleName;
+ public boolean menuEnabled;
+ public boolean menuOpen;
+ public boolean holdKey;
+
+ /**
+ * The List of recognised words from the current user input line.
+ */
+ public List recognisedWords;
+
+ /**
+ * Indicates that a block has been set.
+ */
+ public boolean blocking;
+
+ public short blockUpperLeftX;
+ public short blockUpperLeftY;
+ public short blockLowerRightX;
+ public short blockLowerRightY;
+
+ /**
+ * Contains a transcript of events leading to the current state in the current room.
+ */
+ public ScriptBuffer scriptBuffer;
+
+ /**
+ * Returns true if the AGI game files are V3; otherwise false.
+ */
+ public boolean isAGIV3() { return (game.v3GameSig != null); }
+
+ /**
+ * Constructor for GameState.
+ *
+ * @param game The Game from which we'll get all of the game data.
+ */
+ public GameState(Game game) {
+ this.game = game;
+ this.vars = new int[Defines.NUMVARS];
+ this.flags = new boolean[Defines.NUMFLAGS];
+ this.strings = new String[Defines.NUMSTRINGS];
+ this.controllers = new boolean[Defines.NUMCONTROL];
+ this.scanStart = new int[256];
+ this.logics = new Logic[256];
+ this.pictures = new Picture[256];
+ this.views = new View[256];
+ this.sounds = new Sound[256];
+ this.objects = new Objects(game.objects);
+ this.words = game.words;
+ this.maxDrawn = 15;
+ this.priorityBase = 48;
+ this.statusLineRow = 21;
+ this.inputLineRow = 23;
+ this.currentInput = new StringBuilder();
+ this.lastInput = "";
+ this.simpleName = "";
+ this.gameId = (game.v3GameSig != null? game.v3GameSig : "UNKNOWN");
+ this.version = (game.version.equals("Unknown")? "2.917" : game.version);
+ this.menuEnabled = true;
+ this.holdKey = false;
+ this.keyToControllerMap = new HashMap<>();
+ this.recognisedWords = new ArrayList<>();
+ this.scriptBuffer = new ScriptBuffer(this);
+
+ this.visualPixels = new short[320 * 168];
+ this.priorityPixels = new int[160 * 168];
+ this.controlPixels = new int[160 * 168];
+
+ // Create and initialise all of the AnimatedObject entries.
+ this.animatedObjects = new AnimatedObject[Defines.NUMANIMATED];
+ for (int i=0; i < Defines.NUMANIMATED; i++) {
+ this.animatedObjects[i] = new AnimatedObject(this, i);
+ }
+ this.ego = this.animatedObjects[0];
+
+ this.updateObjectList = new ArrayList();
+ this.stoppedObjectList = new ArrayList();
+
+ // Store resources in arrays for easy lookup.
+ this.logics = this.game.logics;
+ this.pictures = this.game.pictures;
+ this.views = this.game.views;
+ this.sounds = this.game.sounds;
+
+ // Logic 0 is always marked as loaded. It never gets unloaded.
+ logics[0].isLoaded = true;
+ }
+
+ /**
+ * Performs the initialisation of the state of the game being interpreted. Usually called whenever
+ * the game starts or restarts.
+ */
+ public void init() {
+ clearStrings();
+ clearVars();
+ vars[Defines.MACHINE_TYPE] = 0; // IBM PC
+ vars[Defines.MONITOR_TYPE] = 3; // EGA
+ vars[Defines.INPUTLEN] = Defines.MAXINPUT + 1;
+ vars[Defines.NUM_VOICES] = 3;
+
+ // The game would usually set this, but no harm doing it here (2 = NORMAL).
+ vars[Defines.ANIMATION_INT] = 2;
+
+ // Set to the maximum memory amount as recognised by AGI.
+ vars[Defines.MEMLEFT] = 255;
+
+ clearFlags();
+ flags[Defines.HAS_NOISE] = true;
+ flags[Defines.INITLOGS] = true;
+ flags[Defines.SOUNDON] = true;
+
+ // Set the text attribute to default (black on white), and display the input line.
+ foregroundColour = 15;
+ backgroundColour = 0;
+
+ horizon = Defines.HORIZON;
+ userControl = true;
+ blocking = false;
+
+ clearVisualPixels();
+ graphicsMode = true;
+ acceptInput = false;
+ showStatusLine = false;
+ currentLogNum = 0;
+ currentInput.setLength(0);
+ lastInput = "";
+ simpleName = "";
+ clearControllers();
+ menuEnabled = true;
+ holdKey = false;
+
+ for (AnimatedObject aniObj : animatedObjects) {
+ aniObj.reset(true);
+ }
+
+ stoppedObjectList.clear();
+ updateObjectList.clear();
+
+ this.objects = new Objects(game.objects);
+ }
+
+ /**
+ * Resets the four resources types back to their new room state. The main reason for doing
+ * this is to support the script event buffer.
+ */
+ public void resetResources() {
+ for (int i = 0; i < 256; i++) {
+ // For Logics and Views, number 0 is never unloaded.
+ if (i > 0) {
+ if (logics[i] != null) logics[i].isLoaded = false;
+ }
+ if (views[i] != null) views[i].isLoaded = false;
+ if (pictures[i] != null) pictures[i].isLoaded = false;
+ if (sounds[i] != null) sounds[i].isLoaded = false;
+ }
+ }
+
+ /**
+ * Restores all of the background save areas for the most recently drawn AnimatedObjects.
+ */
+ public void restoreBackgrounds() {
+ // If no list specified, then restore update list then stopped list.
+ restoreBackgrounds(updateObjectList);
+ restoreBackgrounds(stoppedObjectList);
+ }
+
+ /**
+ * Restores all of the background save areas for the most recently drawn AnimatedObjects.
+ *
+ * @param restoreList
+ */
+ public void restoreBackgrounds(List restoreList) {
+ // Restore the backgrounds of the previous drawn cels for each AnimatedObject.
+ for (int i = restoreList.size(); --i >= 0;) {
+ restoreList.get(i).restoreBackPixels();
+ }
+ }
+
+ /**
+ * Draws all of the drawn AnimatedObjects in their priority / Y position order. This method
+ * does not actually render the objects to the screen but rather to the "back" screen, or
+ * "off" screen version of the visual screen.
+ */
+ public void drawObjects() {
+ // If no list specified, then draw stopped list then update list.
+ drawObjects(makeStoppedObjectList());
+ drawObjects(makeUpdateObjectList());
+ }
+
+ /**
+ * Draws all of the drawn AnimatedObjects in their priority / Y position order. This method
+ * does not actually render the objects to the screen but rather to the "back" screen, or
+ * "off" screen version of the visual screen.
+ *
+ * @param objectDrawList
+ */
+ public void drawObjects(List objectDrawList) {
+ // Draw the AnimatedObjects to screen in priority order.
+ for (AnimatedObject aniObj : objectDrawList) {
+ aniObj.draw();
+ }
+ }
+
+ /**
+ * Shows all AnimatedObjects by blitting the bounds of their current cel to the screen
+ * pixels. Also updates the Stopped flag and previous position as per the original AGI
+ * interpreter behaviour.
+ *
+ * @param pixels The screen pixels to blit the AnimatedObjects to.
+ */
+ public void showObjects(short[] pixels) {
+ // If no list specified, then draw stopped list then update list.
+ showObjects(pixels, stoppedObjectList);
+ showObjects(pixels, updateObjectList);
+ }
+
+ /**
+ * Shows all AnimatedObjects by blitting the bounds of their current cel to the screen
+ * pixels. Also updates the Stopped flag and previous position as per the original AGI
+ * interpreter behaviour.
+ *
+ * @param pixels The screen pixels to blit the AnimatedObjects to.
+ * @param objectShowList
+ */
+ public void showObjects(short[] pixels, List objectShowList) {
+ for (AnimatedObject aniObj : objectShowList)
+ {
+ aniObj.show(pixels);
+
+ // Check if the AnimatedObject moved this cycle and if it did then set the flags accordingly. The
+ // position of an AnimatedObject is updated only when the StepTimeCount hits 0, at which point it
+ // reloads from StepTime. So if the values are equal, this is a step time reload cycle and therefore
+ // the AnimatedObject's position would have been updated and it is appropriate to update Stopped flag.
+ if (aniObj.stepTimeCount == aniObj.stepTime)
+ {
+ if ((aniObj.x == aniObj.prevX) && (aniObj.y == aniObj.prevY))
+ {
+ aniObj.stopped = true;
+ }
+ else
+ {
+ aniObj.prevX = aniObj.x;
+ aniObj.prevY = aniObj.y;
+ aniObj.stopped = false;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a List of the AnimatedObjects to draw, in the order in which they should be
+ * drawn. It gets the list of candidate AnimatedObjects from the given GameState and
+ * then for each object that is in a Drawn state, it adds them to the list to be draw
+ * and then sorts that list by a combination of Y position and priority state, which
+ * results in the List to be drawn in the order they should be drawn. The updating param
+ * determines what the value of the Update flag should be in order to include an object
+ * in the list.
+ *
+ * @param objsToDraw >
+ * @param updating The value of the UPDATE flag to check for when adding to list
+ */
+ public List makeObjectDrawList(List objsToDraw, boolean updating) {
+ objsToDraw.clear();
+
+ for (AnimatedObject aniObj : this.animatedObjects) {
+ if (aniObj.drawn && (aniObj.update == updating)) {
+ objsToDraw.add(aniObj);
+ }
+ }
+
+ // Sorts them by draw order.
+ objsToDraw.sort(null);
+
+ return objsToDraw;
+ }
+
+ /**
+ * Recreates and then returns the list of animated objects that are currently
+ * being updated, in draw order.
+ */
+ public List makeUpdateObjectList() {
+ return makeObjectDrawList(updateObjectList, true);
+ }
+
+ /**
+ * Recreates and the returns the list of animated objects that are currently
+ * not being updated, in draw order.
+ */
+ public List makeStoppedObjectList() {
+ return makeObjectDrawList(stoppedObjectList, false);
+ }
+
+ /**
+ * Clears the VisualPixels screen to it's initial black state.
+ */
+ public void clearVisualPixels() {
+ for (int i=0; i < this.visualPixels.length; i++) {
+ this.visualPixels[i] = EgaPalette.colours[0];
+ }
+ }
+
+ /**
+ * Clears all of the AGI variables to be zero.
+ */
+ public void clearVars() {
+ for (int i = 0; i < Defines.NUMVARS; i++) {
+ vars[i] = 0;
+ }
+ }
+
+ /**
+ * Clears all of the AGI flags to be false.
+ */
+ public void clearFlags() {
+ for (int i = 0; i < Defines.NUMFLAGS; i++) {
+ flags[i] = false;
+ }
+ }
+
+ /**
+ * Clears all of the AGI controllers to be false.
+ */
+ public void clearControllers() {
+ for (int i = 0; i < Defines.NUMCONTROL; i++) {
+ controllers[i] = false;
+ }
+ }
+
+ /**
+ * Clears all of the AGI Strings to be empty.
+ */
+ public void clearStrings() {
+ for (int i = 0; i < Defines.NUMSTRINGS; i++) {
+ strings[i] = "";
+ }
+ }
+}
diff --git a/core/src/main/java/com/agifans/agile/Interpreter.java b/core/src/main/java/com/agifans/agile/Interpreter.java
new file mode 100644
index 0000000..51757db
--- /dev/null
+++ b/core/src/main/java/com/agifans/agile/Interpreter.java
@@ -0,0 +1,360 @@
+package com.agifans.agile;
+
+import com.agifans.agile.agilib.Game;
+import com.badlogic.gdx.Input.Keys;
+
+/**
+ * Interpreter is the core class in the AGILE AGI interpreter. It controls the overall interpreter cycle.
+ */
+public class Interpreter {
+
+ /**
+ * The GameState class holds all of the data and state for the Game currently
+ * being run by the interpreter.
+ */
+ private GameState state;
+
+ /**
+ * Holds the data and state for user input events, such as keyboard and mouse input.
+ */
+ private UserInput userInput;
+
+ /**
+ * The pixels array for the AGI screen on which the background Picture and
+ * AnimatedObjects will be drawn to.
+ */
+ private short[] pixels;
+
+ /**
+ * Provides methods for drawing text on to the AGI screen.
+ */
+ private TextGraphics textGraphics;
+
+ /**
+ * Direct reference to AnimatedObject number one, i.e. ego, the main character.
+ */
+ private AnimatedObject ego;
+
+ /**
+ * Performs the execution of the LOGIC scripts.
+ */
+ private Commands commands;
+
+ /**
+ * Responsible for displaying the menu system.
+ */
+ private Menu menu;
+
+ /**
+ * Responsible for parsing the user input line to match known words.
+ */
+ private Parser parser;
+
+ /**
+ * Responsible for playing Sound resources.
+ */
+ private SoundPlayer soundPlayer;
+
+ /**
+ * Indicates that a thread is currently executing the Tick, i.e. a single interpretation
+ * cycle. This flag exists because there are some AGI commands that wait for something to
+ * happen before continuing. For example, a print window will stay up for a defined timeout
+ * period or until a key is pressed. In such cases, the thread can be in the Tick method
+ * for the duration of what would normally be many Ticks.
+ */
+ private volatile boolean inTick;
+
+ /**
+ * Constructor for Interpreter.
+ *
+ * @param game
+ * @param userInput
+ * @param wavePlayer
+ * @param pixels
+ */
+ public Interpreter(Game game, UserInput userInput, WavePlayer wavePlayer, short[] pixels) {
+ this.state = new GameState(game);
+ this.userInput = userInput;
+ this.pixels = pixels;
+ this.textGraphics = new TextGraphics(pixels, state, userInput);
+ this.parser = new Parser(state);
+ this.soundPlayer = new SoundPlayer(state, wavePlayer);
+ this.menu = new Menu(state, textGraphics, pixels, userInput);
+ this.commands = new Commands(pixels, state, userInput, textGraphics, parser, soundPlayer, menu);
+ this.ego = state.ego;
+ this.state.init();
+ this.textGraphics.updateInputLine();
+ }
+
+ /**
+ * Updates the internal AGI game clock. This method is invoked once a second.
+ */
+ private void updateGameClock() {
+ if (++state.vars[Defines.SECONDS] >= 60)
+ {
+ // One minute has passed.
+ if (++state.vars[Defines.MINUTES] >= 60)
+ {
+ // One hour has passed.
+ if (++state.vars[Defines.HOURS] >= 24)
+ {
+ // One day has passed.
+ state.vars[Defines.DAYS]++;
+ state.vars[Defines.HOURS] = 0;
+ }
+
+ state.vars[Defines.MINUTES] = 0;
+ }
+
+ state.vars[Defines.SECONDS] = 0;
+ }
+ }
+
+ /**
+ * Executes a single AGI interpreter tick, or cycle. This method is invoked 60 times a
+ * second, but the rate at which the logics are run and the animation updated is determined
+ * by the animation interval variable.
+ */
+ public void tick() {
+ // Regardless of whether we're already in a Tick, we keep counting the number of Ticks.
+ state.totalTicks++;
+
+ // Tick is called 60 times a second, so every 60th call, the second clock ticks. We
+ // deliberately do this outside of the main Tick block because some scripts wait for
+ // the clock to reach a certain clock value, which will never happen if the block isn't
+ // updated outside of the Tick block.
+ if ((state.totalTicks % 60) == 0)
+ {
+ updateGameClock();
+ }
+
+ // Only one thread can be running the core interpreter cycle at a time.
+ if (!inTick)
+ {
+ inTick = true;
+
+ // Proceed only if the animation tick count has reached the set animation interval x 3.
+ if (++state.animationTicks < (state.vars[Defines.ANIMATION_INT] * 3))
+ {
+ inTick = false;
+ return;
+ }
+
+ // Reset animation tick count.
+ state.animationTicks = 0;
+
+ // Clear controllers and get user input.
+ processUserInput();
+
+ // Update input line text on every cycle.
+ textGraphics.updateInputLine(false);
+
+ // If ego is under program control, override user input as to his direction.
+ if (!state.userControl)
+ {
+ state.vars[Defines.EGODIR] = ego.direction;
+ }
+ else
+ {
+ ego.direction = (byte)state.vars[Defines.EGODIR];
+ }
+
+ // Calculate the direction in which objects will move, based on their MotionType. We do
+ // this here, i.e. call UpdateObjectDirections() before starting the logic scan, to
+ // allow ego's direction to be known to the logics even when ego is on a move.obj().
+ updateObjectDirections();
+
+ // Store score and sound state prior to scanning LOGIC 0, so we can determine if they change.
+ int previousScore = state.vars[Defines.SCORE];
+ boolean soundStatus = state.flags[Defines.SOUNDON];
+
+ // Continue scanning LOGIC 0 while the return value is true (which is what indicates a rescan).
+ while (commands.executeLogic(0))
+ {
+ state.vars[Defines.OBJHIT] = 0;
+ state.vars[Defines.OBJEDGE] = 0;
+ state.vars[Defines.UNKNOWN_WORD] = 0;
+ state.flags[Defines.INPUT] = false;
+ previousScore = state.vars[Defines.SCORE];
+ }
+
+ // Set ego's direction from the variable.
+ ego.direction = (byte)state.vars[Defines.EGODIR];
+
+ // Update the status line, if the score or sound status have changed.
+ if ((state.vars[Defines.SCORE] != previousScore) || (soundStatus != state.flags[Defines.SOUNDON]))
+ {
+ // If the SOUND ON flag is off, then immediately stop any currently playing sound.
+ if (!state.flags[Defines.SOUNDON]) soundPlayer.stopSound();
+
+ textGraphics.updateStatusLine();
+ }
+
+ state.vars[Defines.OBJHIT] = 0;
+ state.vars[Defines.OBJEDGE] = 0;
+
+ // Clear the restart, restore, & init logics flags.
+ state.flags[Defines.INITLOGS] = false;
+ state.flags[Defines.RESTART] = false;
+ state.flags[Defines.RESTORE] = false;
+
+ // If in graphics mode, animate the AnimatedObjects.
+ if (state.graphicsMode)
+ {
+ animateObjects();
+ }
+
+ // If there is an open text window, we render it now.
+ if (textGraphics.isWindowOpen())
+ {
+ textGraphics.drawWindow();
+ }
+
+ // Store what the key states were in this cycle before leaving.
+ for (int i = 0; i < 256; i++) userInput.oldKeys[i] = userInput.keys[i];
+
+ inTick = false;
+ }
+ }
+
+ /**
+ * Fully shuts down the SoundPlayer.
+ */
+ public void shutdownSound() {
+ soundPlayer.shutdown();
+ }
+
+ /**
+ * Animates each of the AnimatedObjects that are currently on the screen. This
+ * involves the cell cycling, the movement, and the drawing to the screen.
+ */
+ private void animateObjects() {
+ // Ask each AnimatedObject to update its loop and cell number if required.
+ for (AnimatedObject aniObj : state.animatedObjects) {
+ aniObj.updateLoopAndCel();
+ }
+
+ state.vars[Defines.EGOEDGE] = 0;
+ state.vars[Defines.OBJHIT] = 0;
+ state.vars[Defines.OBJEDGE] = 0;
+
+ // Restore the backgrounds of the previous drawn cels for each AnimatedObject.
+ state.restoreBackgrounds(state.updateObjectList);
+
+ // Ask each AnimatedObject to move if it needs to.
+ for (AnimatedObject aniObj : state.animatedObjects) {
+ aniObj.updatePosition();
+ }
+
+ // Draw the AnimatedObjects to screen in priority order.
+ state.drawObjects(state.makeUpdateObjectList());
+ state.showObjects(pixels, state.updateObjectList);
+
+ // Clear the 'must be on water or land' bits for ego.
+ state.ego.stayOnLand = false;
+ state.ego.stayOnWater = false;
+ }
+
+ /**
+ * Asks every AnimatedObject to calculate their direction based on their current state.
+ */
+ private void updateObjectDirections() {
+ for (AnimatedObject aniObj : state.animatedObjects) {
+ aniObj.updateDirection();
+ }
+ }
+
+ /**
+ * Processes the user's input.
+ */
+ private void processUserInput() {
+ state.clearControllers();
+ state.flags[Defines.INPUT] = false;
+ state.flags[Defines.HADMATCH] = false;
+ state.vars[Defines.UNKNOWN_WORD] = 0;
+ state.vars[Defines.LAST_CHAR] = 0;
+
+ // If opening of the menu was "triggered" in the last cycle, we open it now before processing the rest of the input.
+ if (state.menuOpen) {
+ menu.menuInput();
+ }
+
+ // F12 shows the priority and control screens.
+ if (userInput.keys[(int)Keys.F12] && !userInput.oldKeys[(int)Keys.F12]) {
+ commands.showPriorityScreen();
+ }
+
+ // Handle arrow keys.
+ if (state.userControl) {
+ if (state.holdKey) {
+ // In "hold key" mode, the ego direction directly reflects the direction key currently being held down.
+ byte direction = 0;
+ if (userInput.keys[(int)Keys.UP]) direction = 1;
+ if (userInput.keys[(int)Keys.PAGE_UP]) direction = 2;
+ if (userInput.keys[(int)Keys.RIGHT]) direction = 3;
+ if (userInput.keys[(int)Keys.PAGE_DOWN]) direction = 4;
+ if (userInput.keys[(int)Keys.DOWN]) direction = 5;
+ if (userInput.keys[(int)Keys.END]) direction = 6;
+ if (userInput.keys[(int)Keys.LEFT]) direction = 7;
+ if (userInput.keys[(int)Keys.HOME]) direction = 8;
+ state.vars[Defines.EGODIR] = direction;
+ }
+ else {
+ // Whereas in "release key" mode, the direction key press will toggle movement in that direction.
+ byte direction = 0;
+ if (userInput.keys[(int)Keys.UP] && !userInput.oldKeys[(int)Keys.UP]) direction = 1;
+ if (userInput.keys[(int)Keys.PAGE_UP] && !userInput.oldKeys[(int)Keys.PAGE_UP]) direction = 2;
+ if (userInput.keys[(int)Keys.RIGHT] && !userInput.oldKeys[(int)Keys.RIGHT]) direction = 3;
+ if (userInput.keys[(int)Keys.PAGE_DOWN] && !userInput.oldKeys[(int)Keys.PAGE_DOWN]) direction = 4;
+ if (userInput.keys[(int)Keys.DOWN] && !userInput.oldKeys[(int)Keys.DOWN]) direction = 5;
+ if (userInput.keys[(int)Keys.END] && !userInput.oldKeys[(int)Keys.END]) direction = 6;
+ if (userInput.keys[(int)Keys.LEFT] && !userInput.oldKeys[(int)Keys.LEFT]) direction = 7;
+ if (userInput.keys[(int)Keys.HOME] && !userInput.oldKeys[(int)Keys.HOME]) direction = 8;
+ if (direction > 0) {
+ state.vars[Defines.EGODIR] = (state.vars[Defines.EGODIR] == direction ? (byte)0 : direction);
+ }
+ }
+ }
+
+ // Check all waiting characters.
+ int ch;
+ while ((ch = userInput.getKey()) > 0) {
+
+ // Check controller matches. They take precedence.
+ if (state.keyToControllerMap.containsKey(ch)) {
+ state.controllers[state.keyToControllerMap.get(ch)] = true;
+ }
+ else if ((ch & 0xF0000) == UserInput.ASCII) { // Standard char from a keypress event.
+ char character = (char)(ch & 0xFF);
+
+ state.vars[Defines.LAST_CHAR] = character;
+
+ if (state.acceptInput) {
+ // Handle enter and backspace for user input line.
+ switch (character) {
+ case Character.ENTER:
+ if (state.currentInput.length() > 0) {
+ parser.parse(state.currentInput.toString());
+ state.lastInput = state.currentInput.toString();
+ state.currentInput.setLength(0);
+ }
+ break;
+
+ case Character.BACKSPACE:
+ if (state.currentInput.length() > 0) {
+ state.currentInput.delete(state.currentInput.length() - 1, state.currentInput.length());
+ }
+ break;
+
+ default:
+ // Handle normal characters for user input line.
+ if ((state.strings[0].length() + (state.cursorCharacter > 0 ? 1 : 0) + state.currentInput.length()) < Defines.MAXINPUT) {
+ state.currentInput.append(character);
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/com/agifans/agile/Inventory.java b/core/src/main/java/com/agifans/agile/Inventory.java
new file mode 100644
index 0000000..a39201c
--- /dev/null
+++ b/core/src/main/java/com/agifans/agile/Inventory.java
@@ -0,0 +1,227 @@
+package com.agifans.agile;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.badlogic.gdx.Input.Keys;
+
+/**
+ * The Inventory class handles the viewing of the player's inventory items.
+ */
+public class Inventory {
+
+ /**
+ * The GameState class holds all of the data and state for the Game currently
+ * being run by the interpreter.
+ */
+ private GameState state;
+
+ /**
+ * Holds the data and state for the user input, i.e. keyboard and mouse input.
+ */
+ private UserInput userInput;
+
+ /**
+ * Provides methods for drawing text on to the AGI screen.
+ */
+ private TextGraphics textGraphics;
+
+ /**
+ * The pixels array for the AGI screen, in which the text will be drawn.
+ */
+ private short[] pixels;
+
+ /**
+ * Constructor for Inventory.
+ *
+ * @param state Holds all of the data and state for the Game currently running.
+ * @param userInput Holds the data and state for the user input, i.e. keyboard and mouse input.
+ * @param textGraphics Provides methods for drawing text on to the AGI screen.
+ * @param pixels The pixels array for the AGI screen, in which the text will be drawn.
+ */
+ public Inventory(GameState state, UserInput userInput, TextGraphics textGraphics, short[] pixels) {
+ this.state = state;
+ this.userInput = userInput;
+ this.textGraphics = textGraphics;
+ this.pixels = pixels;
+ }
+
+ /**
+ * Used during the drawing of the inventory screen to represent a single inventory
+ * item name displayed in a specified cell of the two column inventory table.
+ */
+ class InvItem {
+ public byte num;
+ public String name;
+ public int row;
+ public int col;
+ }
+
+ /**
+ * Shows the inventory screen. Implements the AGI "status" command.
+ */
+ public void showInventoryScreen() {
+ List invItems = new ArrayList();
+ byte selectedItemIndex = 0;
+ int howMany = 0;
+ int row = 2;
+
+ // Switch to the text screen.
+ textGraphics.textScreen(15);
+
+ // Construct the table of objects being carried, deciding where on
+ // the screen they are to be printed as we go.
+ for (byte i=0; i < state.objects.objects.size(); i++) {
+ com.agifans.agile.agilib.Objects.Object obj = state.objects.objects.get(i);
+ if (obj.room == Defines.CARRYING) {
+ InvItem invItem = new InvItem();
+ invItem.num = i;
+ invItem.name = obj.name;
+ invItem.row = row;
+
+ if ((howMany & 1) == 0) {
+ invItem.col = 1;
+ }
+ else {
+ row++;
+ invItem.col = 39 - invItem.name.length();
+ }
+
+ if (i == state.vars[Defines.SELECTED_OBJ]) selectedItemIndex = (byte)invItems.size();
+
+ invItems.add(invItem);
+ howMany++;
+ }
+ }
+
+ // If no objects in inventory, then say so.
+ if (howMany == 0) {
+ InvItem invItem = new InvItem();
+ invItem.num = 0;
+ invItem.name = "nothing";
+ invItem.row = row;
+ invItem.col = 16;
+ invItems.add(invItem);
+ }
+
+ // Display the inventory items.
+ drawInventoryItems(invItems, invItems.get(selectedItemIndex));
+
+ // If we are not allowing an item to be selected, we simply wait for a key press then return.
+ if (!state.flags[Defines.ENABLE_SELECT]) {
+ userInput.waitForKey();
+ }
+ else {
+ // Otherwise we handle movement between the items and selection of an item.
+ while (true) {
+ int key = userInput.waitForKey();
+ if (key == (UserInput.ASCII | Character.ENTER)) {
+ state.vars[Defines.SELECTED_OBJ] = invItems.get(selectedItemIndex).num;
+ break;
+ }
+ else if (key == (UserInput.ASCII | Character.ESC)) {
+ state.vars[Defines.SELECTED_OBJ] = 0xFF;
+ break;
+ }
+ else if ((key == Keys.UP) || (key == Keys.DOWN) || (key == Keys.RIGHT) || (key == Keys.LEFT)) {
+ selectedItemIndex = moveSelect(invItems, key, selectedItemIndex);
+ }
+ }
+ }
+
+ // Switch back to the graphics screen.
+ textGraphics.graphicsScreen();
+ }
+
+ /**
+ * Shows a special view of an object that has an attached description. Intended for use
+ * with the "look at object" scenario when the object looked at is an inventory item.
+ *
+ * @param viewNumber The number of the view to show the special inventory object view of.
+ */
+ public void showInventoryObject(int viewNumber) {
+ // Set up the AnimatedObject that will be used to display this view.
+ AnimatedObject aniObj = new AnimatedObject(state, -1);
+ aniObj.setView(viewNumber);
+ aniObj.x = aniObj.prevX = (short)((Defines.MAXX - aniObj.xSize()) / 2);
+ aniObj.y = aniObj.prevY = Defines.MAXY;
+ aniObj.priority = 15;
+ aniObj.fixedPriority = true;
+ aniObj.previousCel = aniObj.cel();
+
+ // Display the description in a window along with the item picture.
+ textGraphics.windowPrint(state.views[viewNumber].description, aniObj);
+
+ // Restore the pixels that were behind the item's image.
+ aniObj.restoreBackPixels();
+ aniObj.show(pixels);
+ }
+
+ /**
+ * Draws the table of inventory items.
+ *
+ * @param invItems The List of the items in the inventory table.
+ * @param selectedItem The currently selected item.
+ */
+ private void drawInventoryItems(List invItems, InvItem selectedItem) {
+ textGraphics.drawString(this.pixels, "You are carrying:", 11 * 8, 0 * 8, 0, 15);
+
+ for (InvItem invItem : invItems) {
+ if ((invItem == selectedItem) && state.flags[Defines.ENABLE_SELECT]) {
+ textGraphics.drawString(this.pixels, invItem.name, invItem.col * 8, invItem.row * 8, 15, 0);
+ }
+ else {
+ textGraphics.drawString(this.pixels, invItem.name, invItem.col * 8, invItem.row * 8, 0, 15);
+ }
+ }
+
+ if (state.flags[Defines.ENABLE_SELECT]) {
+ textGraphics.drawString(this.pixels, "Press ENTER to select, ESC to cancel", 2 * 8, 24 * 8, 0, 15);
+ }
+ else {
+ textGraphics.drawString(this.pixels, "Press a key to return to the game", 4 * 8, 24 * 8, 0, 15);
+ }
+ }
+
+ /**
+ * Processes the direction key that has been pressed. If within the bounds of the
+ * inventory List, a new selected item index will be returned and a new inventory
+ * item highlighted on the screen.
+ *
+ * @param invItems
+ * @param dirKey
+ * @param oldSelectedItemIndex
+ *
+ * @return The index of the new selected inventory item.
+ */
+ private byte moveSelect(List invItems, int dirKey, byte oldSelectedItemIndex) {
+ byte newSelectedItemIndex = oldSelectedItemIndex;
+
+ switch (dirKey) {
+ case Keys.UP:
+ newSelectedItemIndex -= 2;
+ break;
+ case Keys.RIGHT:
+ newSelectedItemIndex += 1;
+ break;
+ case Keys.DOWN:
+ newSelectedItemIndex += 2;
+ break;
+ case Keys.LEFT:
+ newSelectedItemIndex -= 1;
+ break;
+ }
+
+ if ((newSelectedItemIndex < 0) || (newSelectedItemIndex >= invItems.size())) {
+ newSelectedItemIndex = oldSelectedItemIndex;
+ }
+ else {
+ InvItem previousItem = invItems.get(oldSelectedItemIndex);
+ InvItem newItem = invItems.get(newSelectedItemIndex);
+ textGraphics.drawString(this.pixels, previousItem.name, previousItem.col * 8, previousItem.row * 8, 0, 15);
+ textGraphics.drawString(this.pixels, newItem.name, newItem.col * 8, newItem.row * 8, 15, 0);
+ }
+
+ return newSelectedItemIndex;
+ }
+}
diff --git a/core/src/main/java/com/agifans/agile/Menu.java b/core/src/main/java/com/agifans/agile/Menu.java
new file mode 100644
index 0000000..c178cc3
--- /dev/null
+++ b/core/src/main/java/com/agifans/agile/Menu.java
@@ -0,0 +1,411 @@
+package com.agifans.agile;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.agifans.agile.TextGraphics.TextWindow;
+import com.badlogic.gdx.Input.Keys;
+
+/**
+ * The Menu class is responsible for processing both the AGI commands that define the
+ * menus and their items and also for rendering the menu system when it is activated and
+ * processing the navigation and selection events while it is open.
+ */
+public class Menu {
+
+ // Various static constants for calculating menu window dimensions and position.
+ private static final int CHARWIDTH = 4;
+ private static final int CHARHEIGHT = 8;
+ private static final int VMARGIN = 8;
+ private static final int HMARGIN = CHARWIDTH;
+
+ /**
+ * The GameState class holds all of the data and state for the Game currently
+ * being run by the interpreter.
+ */
+ private GameState state;
+
+ /**
+ * The pixels array for the AGI screen, in which the text will be drawn.
+ */
+ private short[] pixels;
+
+ /**
+ * Holds the data and state for the user input, i.e. keyboard and mouse input.
+ */
+ private UserInput userInput;
+
+ /**
+ * Provides methods for drawing text on to the AGI screen.
+ */
+ private TextGraphics textGraphics;
+
+ /**
+ * The List of the top level menu headers currently defined in the menu system.
+ */
+ private List headers;
+
+ /**
+ * The currently highlighted item in the currently open menu header.
+ */
+ private MenuItem currentItem;
+
+ /**
+ * The currently open menu header, i.e. the open whose items are currently being displayed.
+ */
+ private MenuHeader currentHeader;
+
+ private int menuCol;
+ private int itemRow;
+ private int itemCol;
+
+ /**
+ * If set to true then this prevents further menu definition commands from being processed.
+ */
+ private boolean menuSubmitted;
+
+ class MenuHeader {
+ public MenuItem title;
+ public List
+ * The first four bits of this byte tell the interpreter how to handle the
+ * mirroring of this cell (explained later). The last four bits represent the
+ * transparent color. When the cell is drawn on the screen, any pixels of this
+ * color will show through to the background. All cells have a transparent color,
+ * so if you want an opaque cell then you must set the transparent color to one
+ * that is not used in the cell.
+ *
+ * Cell data
+ * The actual image data for each cell is stored using RLE (run length encoding)
+ * compression. This means that instead of having one byte for each single pixel
+ * (or 1/2 byte as you would use for 16 colors), each byte specifies how many
+ * pixels there are to be in a row and what colour they are. I will refer to
+ * these groups of pixels as 'chunks'.
+ *
+ * This method of compression is not very efficient if there is a lot of single
+ * pixels in the image (e.g. a view showing static on a TV screen), but in most
+ * cases it does save a fair amount of space.
+ *
+ * Each line (not to be confused with a chunk) in the cell consists of several
+ * bytes of pixel data, then a 0 to end the line. Each byte of pixel data
+ * represents one chunk. The first four bits determine the colour, and the last
+ * four bits determine the number of pixels in the chunk.
+ *
+ * e.g. AX BY CZ 00
+ *
+ * This line will have: X pixels of colour A [AX]
+ *
Y pixels of colour B [BY]
+ *
Z pixels of colour C [CZ]
+ *
(then that will be the end of the line) [00]
+ *
+ * If the color of the last chunk on the line is the transparent color, there is
+ * no need to store this. For example, if C was the transparent color in the
+ * above example, you could just write AX BY 00. This also saves some space.
+ *
+ * Mirroring
+ * Mirroring is when you have one loop where all the cells are a mirror image of
+ * the corresponding cells in another loop. Although you could do this manually
+ * by drawing one loop and then copying and pasting all the cells to another loop
+ * and flipping them horizontally, AGI views provide the ability to have this
+ * done automatically - you can draw one loop, and have another loop which is
+ * set as a mirror of this loop. Thus, when you change one loop you change the
+ * other. This is useful if you have an animation of a character walking left
+ * and right - you just draw the right-walking animation and have another loop a
+ * mirror of this which will have the left-walking animation. Another advantage
+ * of cell mirroring is to save space - it doesn't make much difference these
+ * days, but back when AGI was written the games were designed to run on 256k
+ * systems which meant that memory had to be used as efficiently as possible.
+ *
+ * Mirroring is done by having both loops share the same cell data - you saw
+ * above that you can have two loop references pointing to the same place. The
+ * first four bits of the 3rd byte in the header of each cell tell the interpreter
+ * what is mirrored:
+ *
Bit 1 specifies whether or not this cell is mirrored.
+ *
Bits 1, 2 and 3 specify the number of the loop (from 0-7) which is NOT mirrored.
+ *
+ * When the interpreter goes to display a loop, it looks at the bit 1 and sees
+ * if it is mirrored or not. If it is, then it checks the loop number - if this
+ * is NOT the same as the current loop, then it flips the cell before displaying it.
+ *
+ * Leaving enough room for the mirrored image:
+ * If you have a cell that is mirrored, you need to ensure that the number of
+ * bytes the cell takes up in the resource is greater than or equal to the number
+ * of bytes that the flipped version of the cell would take up.
+ *
+ * The reason for this is that the interpreter loads the view resource into
+ * memory and works with that for displaying cells, rather than decoding it and
+ * storing it in memory as a non-compressed bitmap. I assume that it doesn't
+ * even bother 'decoding' it as such - it probably just draws the chunks of
+ * color on the screen as they are. When it has to display the flipped version
+ * of a cell, instead of storing the flipped cell somewhere else in memory, it
+ * flips the version that is there. So in memory you have the view resource that
+ * was read from the file, except that some of the cells have been changed. This
+ * is why there is mirroring information stored in each cell - the interpreter
+ * has to know what cells have been changed. When it flips a cell, it changes the
+ * loop number in the 3rd byte of the cell header to the number of the loop it is
+ * currently displaying the cell for. So when it looks at this number the next
+ * time for a different loop, it will see that the cell is not the right way
+ * round for that loop and mirror it again.
+ *
+ * This process seems very inefficient to me. I don't know why it doesn't just
+ * draw the chunks of color on the screen back to front. But since it does it
+ * this way we have to make sure that there is enough room for the flipped cell.
+ *
+ * It seems that not all versions of the interpreter require this, however - I
+ * was working with version 2.917 when I was testing this out, but I noticed that
+ * versoin 2.440 did not require this. I will attempt to try this with all
+ * different interpreters and provide a list of which ones use this in the next
+ * version of this document. But it is best to put these bytes in just in case,
+ * as the views will still work regardless.
+ *
+ * Description
+ * All the Views in the game that are used as close-ups of inventory items have
+ * a description. When a player 'examines' these (in some games you can select
+ * 'see object' from the menu), they will see the first cell of the first loop of
+ * this view and the description of the object they are examining. This is
+ * brought up using the show.obj command. The Description is stored in plain
+ * text, and terminated by a null character. Lines are separated by an 0x0A.
+ *
+ *
+ * @author Dr. Z
+ * @author Lance Ewing (Documentation)
+ * @version 0.00.00.01
+ */
+public class View {
+ /**
+ * Inventory objects have descriptions.
+ */
+ protected String description;
+
+ /**
+ * Loops
+ */
+ protected Loop[] loops;
+
+ /**
+ * Creates a new View representation.
+ *
+ * @param context Game's Context (facultative)
+ * @param stream View's Data
+ * @param size View's Size (facultative if the stream.available() returns
+ * the size of the view.)
+ * @throws ViewException If error occur when loading while loading,
+ * ViewException is throwed.
+ */
+ public View(Loop[] loops, String description) throws ViewException {
+ this.loops = loops;
+ this.description = description;
+ }
+
+ /**
+ * Obtain a specific loop.
+ *
+ * @param loopNumber Loop number.
+ * @return returns the wanted loop object.
+ */
+ public Loop getLoop(short loopNumber) {
+ return loops[loopNumber];
+ }
+
+ /**
+ * Obtain the loop count.
+ *
+ * @return Returns the loop count.
+ */
+ public short getLoopCount() {
+ return (short) loops.length;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/com/sierra/agi/view/ViewException.java b/core/src/main/java/com/sierra/agi/view/ViewException.java
new file mode 100644
index 0000000..6146790
--- /dev/null
+++ b/core/src/main/java/com/sierra/agi/view/ViewException.java
@@ -0,0 +1,30 @@
+/*
+ * ViewException.java
+ * Adventure Game Interpreter View Package
+ *
+ * Created by Dr. Z.
+ * Copyright (c) 2001 Dr. Z. All rights reserved.
+ */
+
+package com.sierra.agi.view;
+
+/**
+ * @author Dr. Z
+ * @version 0.00.00.01
+ */
+public class ViewException extends Exception {
+ /**
+ * Creates new ViewException without detail message.
+ */
+ public ViewException() {
+ }
+
+ /**
+ * Constructs an ViewException with the specified detail message.
+ *
+ * @param msg the detail message.
+ */
+ public ViewException(String msg) {
+ super(msg);
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/com/sierra/agi/view/ViewProvider.java b/core/src/main/java/com/sierra/agi/view/ViewProvider.java
new file mode 100644
index 0000000..3efbbbb
--- /dev/null
+++ b/core/src/main/java/com/sierra/agi/view/ViewProvider.java
@@ -0,0 +1,16 @@
+/*
+ * ViewProvider.java
+ * Adventure Game Interpreter View Package
+ *
+ * Created by Dr. Z.
+ * Copyright (c) 2001 Dr. Z. All rights reserved.
+ */
+
+package com.sierra.agi.view;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public interface ViewProvider {
+ View loadView(InputStream inputStream, int size) throws IOException, ViewException;
+}
diff --git a/core/src/main/java/com/sierra/agi/word/Word.java b/core/src/main/java/com/sierra/agi/word/Word.java
new file mode 100644
index 0000000..f5157a5
--- /dev/null
+++ b/core/src/main/java/com/sierra/agi/word/Word.java
@@ -0,0 +1,35 @@
+/*
+ * Word.java
+ * Adventure Game Interpreter Word Package
+ *
+ * Created by Dr. Z
+ * Copyright (c) 2001 Dr. Z. All rights reserved.
+ */
+
+package com.sierra.agi.word;
+
+/**
+ * Represent a word.
+ *
+ * @author Dr. Z
+ * @version 0.00.00.01
+ */
+public class Word implements Comparable {
+ /**
+ * Word number.
+ */
+ public int number;
+
+ /**
+ * Word textual representation.
+ */
+ public String text;
+
+ public String toString() {
+ return text + " (" + number + ")";
+ }
+
+ public int compareTo(Object o) {
+ return text.compareTo(((Word) o).text);
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/com/sierra/agi/word/Words.java b/core/src/main/java/com/sierra/agi/word/Words.java
new file mode 100644
index 0000000..ce9c41b
--- /dev/null
+++ b/core/src/main/java/com/sierra/agi/word/Words.java
@@ -0,0 +1,287 @@
+/*
+ * Words.java
+ * Adventure Game Interpreter Word Package
+ *
+ * Created by Dr. Z
+ * Copyright (c) 2001 Dr. Z. All rights reserved.
+ */
+
+package com.sierra.agi.word;
+
+import com.sierra.agi.io.ByteCasterStream;
+import com.sierra.agi.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+
+/**
+ * Stores Words of the game.
+ *
+ * Word File Format
+ * The words.tok file is used to store the games vocabulary, i.e. the dictionary
+ * of words that the interpreter understands. These words are stored along with
+ * a word number which is used by the said test commands as argument values for
+ * that command. Many words can have the same word number which basically means
+ * that these words are synonyms for each other as far as the game is concerned.
+ *
+ * The file itself is both packed and encrypted. Words are stored in alphabetic
+ * order which is required for the compression method to work.
+ *
+ * The first section
+ * At the start of the file is a section that is always 26x2 bytes long. This
+ * section contains a two byte entry for every letter of the alphabet. It is
+ * essentially an index which gives the starting location of the words beginning
+ * with the corresponding letter.
+ *
+ *
+ *
Byte
Meaning
+ *
+ *
0-1
Hi and then Lo byte for 'A' offset
+ *
...
+ *
50-51
Hi and then Lo byte for 'Z' offset
+ *
52
Words section
+ *
+ *
+ * The important thing to note from the above is that the 16 bit words are
+ * big-endian (HI-LO). The little endian (LO-HI) byte order convention used
+ * everywhere else in the AGI system is not used here. For example, 0x00 and
+ * 0x24 means 0x0024, not 0x2400. Big endian words are used later on for word
+ * numbers as well.
+ *
+ * All offsets are taken from the beginning of the file. If no words start with
+ * a particular letter, then the offset in that field will be 0x0000.
+ *
+ * The words section
+ * Words are stored in a compressed way in which each word will use part of the
+ * previous word as a starting point for itself. For example, "forearm" and
+ * "forest" both have the prefix "fore". If "forest" comes immediately after
+ * "forearm", then the data for "forest" will specify that it will start with
+ * the first four characters of the previous word. Whether this method is used
+ * for further confusion for would be cheaters or whether it is to help in the
+ * searching process, I don't yet know, but it most certainly isn't purely for
+ * compression since the words.tok file is usally quite small and no attempt is
+ * made to compress any of the larger files (before AGI version 3 that is).
+ *
+ *
+ *
Byte
Meaning
+ *
+ *
0
Number of characters to include from start of prevous word
+ *
1
Char 1 (xor 0x7F gives the ASCII code for the character)
+ *
2
Char 2
+ *
...
+ *
n
Last char
+ *
n + 1
Wordnum (LO-HI) -- see below
+ *
+ *
+ * If a word does not use any part of the previous word, then the prefix field
+ * is equal to zero. This will always be the case for the first word starting
+ * with a new letter. There is nothing to indicate where the words starting with
+ * one letter finish and the next set starts, infact the words section is just
+ * one continuous chain of words conforming to the above format. The index
+ * section mentioned earlier is not needed to read the words in which suggests
+ * that the whole words.tok format is organised to find words quickly.
+ *
+ * A note about word numbers
+ * Some word numbers have special meaning. They are listed below:
+ *
+ *
+ *
Word #
Meaning
+ *
+ *
0
Words are ignored (e.g. the, at)
+ *
1
Anyword
+ *
9999
ROL (Rest Of Line) -- it does matter what the rest of the input list is
+ * public static void main(String... args) {
+ * if (StartupHelper.startNewJvmIfRequired(true)) return; // This handles macOS support and helps on Windows.
+ * // after this is the actual main method code
+ * }
+ *
+ *
+ * @param redirectOutput
+ * whether the output of the new JVM should be rerouted to the
+ * old JVM, so it can be accessed in the same place; keeps the
+ * old JVM running if enabled
+ * @return whether a new JVM was started and thus no code should be executed
+ * in this one
+ */
+ public static boolean startNewJvmIfRequired(boolean redirectOutput) {
+ String osName = System.getProperty("os.name").toLowerCase();
+ if (!osName.contains("mac")) {
+ if (osName.contains("windows")) {
+// Here, we are trying to work around an issue with how LWJGL3 loads its extracted .dll files.
+// By default, LWJGL3 extracts to the directory specified by "java.io.tmpdir", which is usually the user's home.
+// If the user's name has non-ASCII (or some non-alphanumeric) characters in it, that would fail.
+// By extracting to the relevant "ProgramData" folder, which is usually "C:\ProgramData", we avoid this.
+ System.setProperty("java.io.tmpdir", System.getenv("ProgramData") + "/libGDX-temp");
+ }
+ return false;
+ }
+
+ // There is no need for -XstartOnFirstThread on Graal native image
+ if (!System.getProperty("org.graalvm.nativeimage.imagecode", "").isEmpty()) {
+ return false;
+ }
+
+ long pid = LibC.getpid();
+
+ // check whether -XstartOnFirstThread is enabled
+ if ("1".equals(System.getenv("JAVA_STARTED_ON_FIRST_THREAD_" + pid))) {
+ return false;
+ }
+
+ // check whether the JVM was previously restarted
+ // avoids looping, but most certainly leads to a crash
+ if ("true".equals(System.getProperty(JVM_RESTARTED_ARG))) {
+ System.err.println(
+ "There was a problem evaluating whether the JVM was started with the -XstartOnFirstThread argument.");
+ return false;
+ }
+
+ // Restart the JVM with -XstartOnFirstThread
+ ArrayList jvmArgs = new ArrayList<>();
+ String separator = System.getProperty("file.separator");
+ // The following line is used assuming you target Java 8, the minimum for LWJGL3.
+ String javaExecPath = System.getProperty("java.home") + separator + "bin" + separator + "java";
+ // If targeting Java 9 or higher, you could use the following instead of the above line:
+ //String javaExecPath = ProcessHandle.current().info().command().orElseThrow();
+
+ if (!(new File(javaExecPath)).exists()) {
+ System.err.println(
+ "A Java installation could not be found. If you are distributing this app with a bundled JRE, be sure to set the -XstartOnFirstThread argument manually!");
+ return false;
+ }
+
+ jvmArgs.add(javaExecPath);
+ jvmArgs.add("-XstartOnFirstThread");
+ jvmArgs.add("-D" + JVM_RESTARTED_ARG + "=true");
+ jvmArgs.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
+ jvmArgs.add("-cp");
+ jvmArgs.add(System.getProperty("java.class.path"));
+ String mainClass = System.getenv("JAVA_MAIN_CLASS_" + pid);
+ if (mainClass == null) {
+ StackTraceElement[] trace = Thread.currentThread().getStackTrace();
+ if (trace.length > 0) {
+ mainClass = trace[trace.length - 1].getClassName();
+ } else {
+ System.err.println("The main class could not be determined.");
+ return false;
+ }
+ }
+ jvmArgs.add(mainClass);
+
+ try {
+ if (!redirectOutput) {
+ ProcessBuilder processBuilder = new ProcessBuilder(jvmArgs);
+ processBuilder.start();
+ } else {
+ Process process = (new ProcessBuilder(jvmArgs))
+ .redirectErrorStream(true).start();
+ BufferedReader processOutput = new BufferedReader(
+ new InputStreamReader(process.getInputStream()));
+ String line;
+
+ while ((line = processOutput.readLine()) != null) {
+ System.out.println(line);
+ }
+
+ process.waitFor();
+ }
+ } catch (Exception e) {
+ System.err.println("There was a problem restarting the JVM");
+ e.printStackTrace();
+ }
+
+ return true;
+ }
+
+ /**
+ * Starts a new JVM if the application was started on macOS without the
+ * {@code -XstartOnFirstThread} argument. Returns whether a new JVM was
+ * started and thus no code should be executed. Redirects the output of the
+ * new JVM to the old one.
+ *
+ * Usage:
+ *
+ *
+ * public static void main(String... args) {
+ * if (StartupHelper.startNewJvmIfRequired()) return; // This handles macOS support and helps on Windows.
+ * // the actual main method code
+ * }
+ *
+ *
+ * @return whether a new JVM was started and thus no code should be executed
+ * in this one
+ */
+ public static boolean startNewJvmIfRequired() {
+ return startNewJvmIfRequired(true);
+ }
+}
\ No newline at end of file
diff --git a/lwjgl3/src/main/resources/META-INF/native-image/com/agifans/agile/agile/resource-config.json b/lwjgl3/src/main/resources/META-INF/native-image/com/agifans/agile/agile/resource-config.json
new file mode 100644
index 0000000..ecde37b
--- /dev/null
+++ b/lwjgl3/src/main/resources/META-INF/native-image/com/agifans/agile/agile/resource-config.json
@@ -0,0 +1,9 @@
+{
+ "resources":{
+ "includes":[
+ {
+ "pattern": ".+\\.(png|jpg|jpeg|tmx|tsx|fnt|ttf|otf|json|xml|glsl)"
+ }
+ ]},
+ "bundles":[]
+}
\ No newline at end of file
diff --git a/lwjgl3/src/main/resources/libgdx128.png b/lwjgl3/src/main/resources/libgdx128.png
new file mode 100644
index 0000000..788f542
Binary files /dev/null and b/lwjgl3/src/main/resources/libgdx128.png differ
diff --git a/lwjgl3/src/main/resources/libgdx16.png b/lwjgl3/src/main/resources/libgdx16.png
new file mode 100644
index 0000000..47af189
Binary files /dev/null and b/lwjgl3/src/main/resources/libgdx16.png differ
diff --git a/lwjgl3/src/main/resources/libgdx32.png b/lwjgl3/src/main/resources/libgdx32.png
new file mode 100644
index 0000000..4cf903a
Binary files /dev/null and b/lwjgl3/src/main/resources/libgdx32.png differ
diff --git a/lwjgl3/src/main/resources/libgdx64.png b/lwjgl3/src/main/resources/libgdx64.png
new file mode 100644
index 0000000..ebcd8f1
Binary files /dev/null and b/lwjgl3/src/main/resources/libgdx64.png differ
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..3991192
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include 'core', 'android', 'lwjgl3', 'html'