From b536588e4d2be4ec213f40cb83c98b0166204e5e Mon Sep 17 00:00:00 2001 From: Lance Ewing Date: Tue, 19 Dec 2023 19:43:05 +0000 Subject: [PATCH] Initial commit of AGILE code for Desktop version. --- .editorconfig | 18 + .gitattributes | 2 + android/AndroidManifest.xml | 27 + android/build.gradle | 118 + android/ic_launcher-web.png | Bin 0 -> 6865 bytes android/proguard-rules.pro | 60 + android/project.properties | 14 + android/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 1380 bytes android/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 1015 bytes android/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 1657 bytes android/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 2302 bytes android/res/values/strings.xml | 4 + android/res/values/styles.xml | 10 + .../agile/android/AndroidLauncher.java | 18 + assets/libgdx.png | Bin 0 -> 2458 bytes build.gradle | 44 + core/build.gradle | 6 + .../main/java/com/agifans/agile/Agile.gwt.xml | 6 + .../main/java/com/agifans/agile/Agile.java | 82 + .../java/com/agifans/agile/AgileRunner.java | 156 ++ .../com/agifans/agile/AnimatedObject.java | 1644 +++++++++++++ .../java/com/agifans/agile/Character.java | 212 ++ .../main/java/com/agifans/agile/Commands.java | 2147 +++++++++++++++++ .../main/java/com/agifans/agile/Defines.java | 175 ++ .../java/com/agifans/agile/Detection.java | 323 +++ .../java/com/agifans/agile/EgaPalette.java | 106 + .../java/com/agifans/agile/GameScreen.java | 59 + .../java/com/agifans/agile/GameState.java | 463 ++++ .../java/com/agifans/agile/Interpreter.java | 360 +++ .../java/com/agifans/agile/Inventory.java | 227 ++ .../src/main/java/com/agifans/agile/Menu.java | 411 ++++ .../main/java/com/agifans/agile/Parser.java | 188 ++ .../java/com/agifans/agile/QuitAction.java | 14 + .../main/java/com/agifans/agile/SaveArea.java | 20 + .../java/com/agifans/agile/SavedGames.java | 1195 +++++++++ .../java/com/agifans/agile/ScriptBuffer.java | 213 ++ .../java/com/agifans/agile/SoundPlayer.java | 441 ++++ .../java/com/agifans/agile/TextGraphics.java | 1341 ++++++++++ .../java/com/agifans/agile/UserInput.java | 480 ++++ .../java/com/agifans/agile/WavePlayer.java | 50 + .../agile/agilib/AgileLogicProvider.java | 36 + .../agile/agilib/AgileSoundProvider.java | 46 + .../java/com/agifans/agile/agilib/Game.java | 129 + .../java/com/agifans/agile/agilib/Logic.java | 810 +++++++ .../com/agifans/agile/agilib/Objects.java | 115 + .../com/agifans/agile/agilib/Picture.java | 52 + .../com/agifans/agile/agilib/Resource.java | 28 + .../java/com/agifans/agile/agilib/Sound.java | 96 + .../java/com/agifans/agile/agilib/View.java | 54 + .../java/com/agifans/agile/agilib/Words.java | 71 + .../java/com/sierra/agi/awt/EgaUtils.java | 101 + .../com/sierra/agi/inv/InventoryObject.java | 32 + .../com/sierra/agi/inv/InventoryObjects.java | 242 ++ .../com/sierra/agi/inv/InventoryProvider.java | 16 + .../java/com/sierra/agi/io/ByteCaster.java | 49 + .../com/sierra/agi/io/ByteCasterStream.java | 76 + .../com/sierra/agi/io/CryptedInputStream.java | 114 + .../main/java/com/sierra/agi/io/IOUtils.java | 50 + .../com/sierra/agi/io/LZWInputStream.java | 232 ++ .../agi/io/LittleEndianOutputStream.java | 137 ++ .../com/sierra/agi/io/PictureInputStream.java | 117 + .../agi/io/PublicByteArrayInputStream.java | 50 + .../sierra/agi/io/SegmentedInputStream.java | 176 ++ .../main/java/com/sierra/agi/logic/Logic.java | 12 + .../com/sierra/agi/logic/LogicException.java | 20 + .../com/sierra/agi/logic/LogicProvider.java | 16 + .../agi/pic/CorruptedPictureException.java | 21 + .../main/java/com/sierra/agi/pic/Picture.java | 43 + .../com/sierra/agi/pic/PictureContext.java | 310 +++ .../java/com/sierra/agi/pic/PictureEntry.java | 13 + .../sierra/agi/pic/PictureEntryAbsLine.java | 44 + .../sierra/agi/pic/PictureEntryChangePen.java | 21 + .../agi/pic/PictureEntryChangePicColor.java | 21 + .../agi/pic/PictureEntryChangePriColor.java | 21 + .../com/sierra/agi/pic/PictureEntryDrawX.java | 60 + .../com/sierra/agi/pic/PictureEntryDrawY.java | 58 + .../com/sierra/agi/pic/PictureEntryFill.java | 141 ++ .../com/sierra/agi/pic/PictureEntryMulti.java | 28 + .../com/sierra/agi/pic/PictureEntryPlot.java | 164 ++ .../sierra/agi/pic/PictureEntryRelLine.java | 66 + .../com/sierra/agi/pic/PictureException.java | 31 + .../com/sierra/agi/pic/PictureProvider.java | 16 + .../agi/pic/StandardPictureProvider.java | 414 ++++ .../agi/res/CorruptedResourceException.java | 33 + .../res/NoDirectoryAvailableException.java | 34 + .../agi/res/NoVolumeAvailableException.java | 34 + .../com/sierra/agi/res/ResourceCache.java | 370 +++ .../com/sierra/agi/res/ResourceCacheFile.java | 44 + .../agi/res/ResourceCacheFileDebug.java | 18 + .../sierra/agi/res/ResourceConfiguration.java | 16 + .../com/sierra/agi/res/ResourceException.java | 33 + .../agi/res/ResourceNotExistingException.java | 33 + .../com/sierra/agi/res/ResourceProvider.java | 114 + .../sierra/agi/res/ResourceProviderZip.java | 61 + .../agi/res/ResourceTypeInvalidException.java | 32 + .../agi/res/VolumeNotFoundException.java | 33 + .../sierra/agi/res/dir/ResourceDirectory.java | 127 + .../sierra/agi/res/v2/ResourceProviderV2.java | 566 +++++ .../sierra/agi/res/v3/ResourceProviderV3.java | 385 +++ .../main/java/com/sierra/agi/sound/Sound.java | 12 + .../com/sierra/agi/sound/SoundProvider.java | 16 + .../main/java/com/sierra/agi/view/Cel.java | 152 ++ .../main/java/com/sierra/agi/view/Loop.java | 51 + .../sierra/agi/view/StandardViewProvider.java | 62 + .../main/java/com/sierra/agi/view/View.java | 214 ++ .../com/sierra/agi/view/ViewException.java | 30 + .../com/sierra/agi/view/ViewProvider.java | 16 + .../main/java/com/sierra/agi/word/Word.java | 35 + .../main/java/com/sierra/agi/word/Words.java | 287 +++ .../com/sierra/agi/word/WordsProvider.java | 16 + gradle.properties | 9 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 249 ++ gradlew.bat | 92 + html/build.gradle | 153 ++ .../com/agifans/agile/GdxDefinition.gwt.xml | 14 + .../agile/GdxDefinitionSuperdev.gwt.xml | 9 + .../com/agifans/agile/gwt/GwtAgileRunner.java | 31 + .../com/agifans/agile/gwt/GwtLauncher.java | 28 + .../com/agifans/agile/gwt/GwtWavePlayer.java | 37 + html/webapp/WEB-INF/web.xml | 3 + html/webapp/index.html | 31 + html/webapp/refresh.png | Bin 0 -> 232 bytes html/webapp/styles.css | 53 + lwjgl3/build.gradle | 115 + lwjgl3/icons/logo.icns | Bin 0 -> 201876 bytes lwjgl3/icons/logo.ico | Bin 0 -> 410598 bytes lwjgl3/icons/logo.png | Bin 0 -> 9754 bytes lwjgl3/nativeimage.gradle | 29 + .../agile/lwjgl3/DesktopAgileRunner.java | 65 + .../agile/lwjgl3/DesktopWavePlayer.java | 101 + .../agifans/agile/lwjgl3/Lwjgl3Launcher.java | 34 + .../agifans/agile/lwjgl3/StartupHelper.java | 179 ++ .../agifans/agile/agile/resource-config.json | 9 + lwjgl3/src/main/resources/libgdx128.png | Bin 0 -> 9754 bytes lwjgl3/src/main/resources/libgdx16.png | Bin 0 -> 879 bytes lwjgl3/src/main/resources/libgdx32.png | Bin 0 -> 2092 bytes lwjgl3/src/main/resources/libgdx64.png | Bin 0 -> 4996 bytes settings.gradle | 1 + 140 files changed, 19056 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 android/AndroidManifest.xml create mode 100644 android/build.gradle create mode 100644 android/ic_launcher-web.png create mode 100644 android/proguard-rules.pro create mode 100644 android/project.properties create mode 100644 android/res/drawable-hdpi/ic_launcher.png create mode 100644 android/res/drawable-mdpi/ic_launcher.png create mode 100644 android/res/drawable-xhdpi/ic_launcher.png create mode 100644 android/res/drawable-xxhdpi/ic_launcher.png create mode 100644 android/res/values/strings.xml create mode 100644 android/res/values/styles.xml create mode 100644 android/src/main/java/com/agifans/agile/android/AndroidLauncher.java create mode 100644 assets/libgdx.png create mode 100644 build.gradle create mode 100644 core/build.gradle create mode 100644 core/src/main/java/com/agifans/agile/Agile.gwt.xml create mode 100644 core/src/main/java/com/agifans/agile/Agile.java create mode 100644 core/src/main/java/com/agifans/agile/AgileRunner.java create mode 100644 core/src/main/java/com/agifans/agile/AnimatedObject.java create mode 100644 core/src/main/java/com/agifans/agile/Character.java create mode 100644 core/src/main/java/com/agifans/agile/Commands.java create mode 100644 core/src/main/java/com/agifans/agile/Defines.java create mode 100644 core/src/main/java/com/agifans/agile/Detection.java create mode 100644 core/src/main/java/com/agifans/agile/EgaPalette.java create mode 100644 core/src/main/java/com/agifans/agile/GameScreen.java create mode 100644 core/src/main/java/com/agifans/agile/GameState.java create mode 100644 core/src/main/java/com/agifans/agile/Interpreter.java create mode 100644 core/src/main/java/com/agifans/agile/Inventory.java create mode 100644 core/src/main/java/com/agifans/agile/Menu.java create mode 100644 core/src/main/java/com/agifans/agile/Parser.java create mode 100644 core/src/main/java/com/agifans/agile/QuitAction.java create mode 100644 core/src/main/java/com/agifans/agile/SaveArea.java create mode 100644 core/src/main/java/com/agifans/agile/SavedGames.java create mode 100644 core/src/main/java/com/agifans/agile/ScriptBuffer.java create mode 100644 core/src/main/java/com/agifans/agile/SoundPlayer.java create mode 100644 core/src/main/java/com/agifans/agile/TextGraphics.java create mode 100644 core/src/main/java/com/agifans/agile/UserInput.java create mode 100644 core/src/main/java/com/agifans/agile/WavePlayer.java create mode 100644 core/src/main/java/com/agifans/agile/agilib/AgileLogicProvider.java create mode 100644 core/src/main/java/com/agifans/agile/agilib/AgileSoundProvider.java create mode 100644 core/src/main/java/com/agifans/agile/agilib/Game.java create mode 100644 core/src/main/java/com/agifans/agile/agilib/Logic.java create mode 100644 core/src/main/java/com/agifans/agile/agilib/Objects.java create mode 100644 core/src/main/java/com/agifans/agile/agilib/Picture.java create mode 100644 core/src/main/java/com/agifans/agile/agilib/Resource.java create mode 100644 core/src/main/java/com/agifans/agile/agilib/Sound.java create mode 100644 core/src/main/java/com/agifans/agile/agilib/View.java create mode 100644 core/src/main/java/com/agifans/agile/agilib/Words.java create mode 100644 core/src/main/java/com/sierra/agi/awt/EgaUtils.java create mode 100644 core/src/main/java/com/sierra/agi/inv/InventoryObject.java create mode 100644 core/src/main/java/com/sierra/agi/inv/InventoryObjects.java create mode 100644 core/src/main/java/com/sierra/agi/inv/InventoryProvider.java create mode 100644 core/src/main/java/com/sierra/agi/io/ByteCaster.java create mode 100644 core/src/main/java/com/sierra/agi/io/ByteCasterStream.java create mode 100644 core/src/main/java/com/sierra/agi/io/CryptedInputStream.java create mode 100644 core/src/main/java/com/sierra/agi/io/IOUtils.java create mode 100644 core/src/main/java/com/sierra/agi/io/LZWInputStream.java create mode 100644 core/src/main/java/com/sierra/agi/io/LittleEndianOutputStream.java create mode 100644 core/src/main/java/com/sierra/agi/io/PictureInputStream.java create mode 100644 core/src/main/java/com/sierra/agi/io/PublicByteArrayInputStream.java create mode 100644 core/src/main/java/com/sierra/agi/io/SegmentedInputStream.java create mode 100644 core/src/main/java/com/sierra/agi/logic/Logic.java create mode 100644 core/src/main/java/com/sierra/agi/logic/LogicException.java create mode 100644 core/src/main/java/com/sierra/agi/logic/LogicProvider.java create mode 100644 core/src/main/java/com/sierra/agi/pic/CorruptedPictureException.java create mode 100644 core/src/main/java/com/sierra/agi/pic/Picture.java create mode 100644 core/src/main/java/com/sierra/agi/pic/PictureContext.java create mode 100644 core/src/main/java/com/sierra/agi/pic/PictureEntry.java create mode 100644 core/src/main/java/com/sierra/agi/pic/PictureEntryAbsLine.java create mode 100644 core/src/main/java/com/sierra/agi/pic/PictureEntryChangePen.java create mode 100644 core/src/main/java/com/sierra/agi/pic/PictureEntryChangePicColor.java create mode 100644 core/src/main/java/com/sierra/agi/pic/PictureEntryChangePriColor.java create mode 100644 core/src/main/java/com/sierra/agi/pic/PictureEntryDrawX.java create mode 100644 core/src/main/java/com/sierra/agi/pic/PictureEntryDrawY.java create mode 100644 core/src/main/java/com/sierra/agi/pic/PictureEntryFill.java create mode 100644 core/src/main/java/com/sierra/agi/pic/PictureEntryMulti.java create mode 100644 core/src/main/java/com/sierra/agi/pic/PictureEntryPlot.java create mode 100644 core/src/main/java/com/sierra/agi/pic/PictureEntryRelLine.java create mode 100644 core/src/main/java/com/sierra/agi/pic/PictureException.java create mode 100644 core/src/main/java/com/sierra/agi/pic/PictureProvider.java create mode 100644 core/src/main/java/com/sierra/agi/pic/StandardPictureProvider.java create mode 100644 core/src/main/java/com/sierra/agi/res/CorruptedResourceException.java create mode 100644 core/src/main/java/com/sierra/agi/res/NoDirectoryAvailableException.java create mode 100644 core/src/main/java/com/sierra/agi/res/NoVolumeAvailableException.java create mode 100644 core/src/main/java/com/sierra/agi/res/ResourceCache.java create mode 100644 core/src/main/java/com/sierra/agi/res/ResourceCacheFile.java create mode 100644 core/src/main/java/com/sierra/agi/res/ResourceCacheFileDebug.java create mode 100644 core/src/main/java/com/sierra/agi/res/ResourceConfiguration.java create mode 100644 core/src/main/java/com/sierra/agi/res/ResourceException.java create mode 100644 core/src/main/java/com/sierra/agi/res/ResourceNotExistingException.java create mode 100644 core/src/main/java/com/sierra/agi/res/ResourceProvider.java create mode 100644 core/src/main/java/com/sierra/agi/res/ResourceProviderZip.java create mode 100644 core/src/main/java/com/sierra/agi/res/ResourceTypeInvalidException.java create mode 100644 core/src/main/java/com/sierra/agi/res/VolumeNotFoundException.java create mode 100644 core/src/main/java/com/sierra/agi/res/dir/ResourceDirectory.java create mode 100644 core/src/main/java/com/sierra/agi/res/v2/ResourceProviderV2.java create mode 100644 core/src/main/java/com/sierra/agi/res/v3/ResourceProviderV3.java create mode 100644 core/src/main/java/com/sierra/agi/sound/Sound.java create mode 100644 core/src/main/java/com/sierra/agi/sound/SoundProvider.java create mode 100644 core/src/main/java/com/sierra/agi/view/Cel.java create mode 100644 core/src/main/java/com/sierra/agi/view/Loop.java create mode 100644 core/src/main/java/com/sierra/agi/view/StandardViewProvider.java create mode 100644 core/src/main/java/com/sierra/agi/view/View.java create mode 100644 core/src/main/java/com/sierra/agi/view/ViewException.java create mode 100644 core/src/main/java/com/sierra/agi/view/ViewProvider.java create mode 100644 core/src/main/java/com/sierra/agi/word/Word.java create mode 100644 core/src/main/java/com/sierra/agi/word/Words.java create mode 100644 core/src/main/java/com/sierra/agi/word/WordsProvider.java create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 html/build.gradle create mode 100644 html/src/main/java/com/agifans/agile/GdxDefinition.gwt.xml create mode 100644 html/src/main/java/com/agifans/agile/GdxDefinitionSuperdev.gwt.xml create mode 100644 html/src/main/java/com/agifans/agile/gwt/GwtAgileRunner.java create mode 100644 html/src/main/java/com/agifans/agile/gwt/GwtLauncher.java create mode 100644 html/src/main/java/com/agifans/agile/gwt/GwtWavePlayer.java create mode 100644 html/webapp/WEB-INF/web.xml create mode 100644 html/webapp/index.html create mode 100644 html/webapp/refresh.png create mode 100644 html/webapp/styles.css create mode 100644 lwjgl3/build.gradle create mode 100644 lwjgl3/icons/logo.icns create mode 100644 lwjgl3/icons/logo.ico create mode 100644 lwjgl3/icons/logo.png create mode 100644 lwjgl3/nativeimage.gradle create mode 100644 lwjgl3/src/main/java/com/agifans/agile/lwjgl3/DesktopAgileRunner.java create mode 100644 lwjgl3/src/main/java/com/agifans/agile/lwjgl3/DesktopWavePlayer.java create mode 100644 lwjgl3/src/main/java/com/agifans/agile/lwjgl3/Lwjgl3Launcher.java create mode 100644 lwjgl3/src/main/java/com/agifans/agile/lwjgl3/StartupHelper.java create mode 100644 lwjgl3/src/main/resources/META-INF/native-image/com/agifans/agile/agile/resource-config.json create mode 100644 lwjgl3/src/main/resources/libgdx128.png create mode 100644 lwjgl3/src/main/resources/libgdx16.png create mode 100644 lwjgl3/src/main/resources/libgdx32.png create mode 100644 lwjgl3/src/main/resources/libgdx64.png create mode 100644 settings.gradle 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 0000000000000000000000000000000000000000..0f599020dc437e8aafe363e1030ccd622a3492e9 GIT binary patch literal 6865 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 R8JpjemgWa(kId+?{{sXmZfXDk literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9fca30cb61fead5b21c9c4bf7bd43d8137bef4b9 GIT binary patch literal 1380 zcmV-q1)KVbP)^vbDs^=9Y@ONM{56i->2m)9 zW@~$h;@Gmucy@;@J03g5*>Dagac-<$92XHTYPAbOxt_099LJF4l@B2KiheuxtgmW^ zGJ&^1Qg}zHmln4ID^4N~lf>!zoG&ae&v|itgb)NG`D#NF*Ror|!;14J2_&+r%0QBQ z!%F2@rZ_Jt^R56ieHBFjnnHq78>Xdt;iRjY6z5(gk$~lnmCC*NJh+A=s5m@*AuUO1 zp$|O1$05n~mg#v2Bw=Ba0ZQ$9esUftlPG|mR8?-OC`~z?0zC~{D(AdXxmj`UU^=oo zNgt-ePmTNOKMRsa9~e^7p%NJ4n}2E?{5%*VsZ+^-F>h(9Hl9F<2EZang4x6qbD>!! z^%Tm_NQGq}smY;Oj6+Rn1*oa!2~tRTvH_mkxv}UP`cS{A_zE?LJ%5Sbsxnh5PY;%Y*~l8 zwxF~QRqa5Tybk3vHXs}h0oZ~<92(n$j@le*E4H9B9cs^kzAOioK#A&5RCxjo_LqaI za3<;y`V?lM*ULdNgv4n`97ERt4Yc`CP}3?c9g^WFYJ{RnDyBodNcbm^hEf5Z3@9BW zlsR;QxQ>#_P~|+Vp)7#39!zqU&33YAc{tHAeC}kkXF!ufhzWE9#^4FHKZxR?VbE(1 zg&7bJ)NnvS(;>40AOnFohQj8MmqE1qxPTCcLNH`QD3OIw{XK#X800IUG~!U!p;`lv z`KH$PzCH)SIX%`i2Jxb|Ido$9!RSEKew@;a*4(S#r%S$RHJluT4k>gvh8$n}DC`Xf zFCTcrH2*SMydzHQoInF?9)WU%Z9ddGK;Bi6T}I8W$2V*!kHluS4?$>!I zVcN|E@hXDBR#0OJwqAR^WPf;%Px*!2w1xQXclyh7=YHlK_;36@vVH#eq_+^}O-=Dn zys#E73`?X{A>v;}*3>N9gP!iAX;en^ujj3xb&?(Uw}p;$kCp1-SmTM%y?p!+Z~Uya zqQ$FA>UaSc{ql;pqSl+^`oDgZTm%Mw|9|zB-Vr>r?K3J m`>g9Big&wC_xc~}?B{!& literal 0 HcmV?d00001 diff --git a/android/res/drawable-mdpi/ic_launcher.png b/android/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d0befa92d8c3b0c32993b6427dbe6a7937fa2b73 GIT binary patch literal 1015 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBgK_VAKil32_B-pL@9ykAUj`ow5A? z|Ns7(+B<5c?rk=*)#BX0f4{$j(AQI4J?{5@jzb++BLiSoDn9L@$>zbkfpyWQ;ddh-j3mfsiK-d<$vV9a-GmWh{RQ`&JNp?Jn6f-w978JN)?T|>^w>az?LqG4Et>Bp zrnDItPn{__ZRY*|vfn(qvdbr3dFS(Qb;hyyKi=Z*b*2lCge^KAr^Y0$SvUWB zWd6T$r9hB(hr7AC`z>yU7&*DPL*5O3E9C?ewk%`uty{5h!RiuIS0>lPu1ma1IbTiu zzQA*XlxT!PW2!65h3O0U0}obn204Csb~~crSD%+daisi)Qx+GtaLHX*yNrF+6U`L@ zo3tIT`3CY|m@-?mw1-EU?MX~mx6svMRWEPLxzqF*l4AtV7~FVxz^{19qFsK$bC%~e z@JR9+$*9Qk>{;5&rrXf$$@Hd=;oL{&M421X5({Jk9_-I$&=C6c;jW{AjPcuR?0jVs z$?O3W@+}S=<&&6s)?c^&%#Y?b+3ZhVn+rZGT47$8I7j$X<$V54PbV(ko-6b(~ar?)Q{7$Rd77FngSM^MozyFAVob|igx{JOY*(h?7aaCA!&+q4X zhyPk1I@}=mDSAqOdu-%X`ySE%Qg7yK% z9SI3n92!kD3#f7!ds7n5rY>(;8Na{35D*YRGY;OfGRKD;-{0T+ygQ$tpDrHRLn~ARs_MKx;`4?Q23S4h{92K97%&udlC=E-Qj6C-8egnP?Wll#-t_FJ&Me z>taLkgFy6?KGSkIid_`$b3wCMF-|xWP*6~UC?$h3CeP2$`m{SfD-Xe4HAy`LzW@LL z5Oh*bQviSv@4w$5K%ZbxFpqGr&!^GoyZ`_M<4Ht8RCr$Hn&)oYN)Se4MV2k6_+wM= zy?43y-ksjd?eG5xcSSCCm!u<56#2u*fdRqTfJd6yd;(_tuXJs96YxysXZ$M%l?^nBuu-t$4wT|Dgnwc3CUJ z-ds&q{DTp6Mw=RCb^ca_f~-1aFh1^Rl$Z0hBJiA5JQIaL^bI>$n4=Y8R{Y%(46=C` z+_F|IhTRr_RikW1ju45(=pg2kodnnsEGz+@0Gd;)`#89@AjajhN$QtAec7Yhmc4VtShJYC4(*-0K zTnc!4OcP{)c?CdbRzXF6P(V*S5FGAv!MjSO$_0mqUI-HU!(@K@6I6f-(Rd)JlJ`vo z*TC8uP}2qUWNtxzk_?7mAIM2k4%jyY`pKLETAQIR$YM>Bq>W46Cesu50^S1pv?|b) z2m_$Y1oGU18WSXNDe1Iu?Ta9b)AxT8-sd3uA^_G6!8-701(J?Wg0w#Zo~HYvpv#`@ zo#2}&c;}r!t`V|CkPrpeM3B`_yz9YG6v*B;ISo+&?*;p2LDzFZ%`EulxnO7(RJ;~s zVHP0ku>e*9JQrMwf=eF+15t4Ay`Z%r3VPlPa*`;Ja=#Rezq%9j%!1yp1Zrn{c0m_7 zmCOPuhy39~g19ofAUz}7}^C;BQ699peb1e(kmcg7bKObg0FQYk{X)@^ozhI zkWPugCfJ)U*j5M;lcAOXa*|y@$Poh<6q3cgh@vgJ!e&3iv~!T%pw@4AxshX=V|e;X!)j?lha^KK%_Fic^8LM$u6elq*t{rg5Rx zKq--97Xk$gI^;Om1=hLX6xiLQC(uc`6pSRzi0FbZTiR3s;nR^QNX8p&!We*L7L413 zM*LU^3FYX54MV2Q(%$x&prBCpfU7~z5cCFD!o@EDDrs>&73qKHleOtvf~~)zFle6{ zf=&`TYm*xkJOwE1#eCFI|*jlNu7e%B9s5E$l;)mSrw|IJR3Pvi3 zxv;u4_lE9!pM3!O{QeCe^Ln!8`th?pAz1*(%g_J3SabQmsI#vW?8^c#5YnS$UU&Gt z?tMH9Yrb0Jx$wCDXn?1O^H6rbpe3tUKM9AQ{BHaY$E?&kKUlk)00000NkvXXu0mjf DFK6jQ literal 0 HcmV?d00001 diff --git a/android/res/drawable-xxhdpi/ic_launcher.png b/android/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4225649650957afec8e8c35ebd2f390bf956aa8c GIT binary patch literal 2302 zcmVdIb3yTgKdU|?YSv^z>4 z74PKanO`U9pE%HAIq28cm{%uxMj*{mI{*Lx=SoEm1qAh`BXlJr%0WD>I5Zsz3E4$K zIuj9B92y@L1YAB1NEa4+QxcyuFM=v3=A0ob4h>}>9*{084+;V<9|uh|3%ooyxP2O# zXclWp57v<%sB#$GX)d(@000nlQchC%fsKgg&0KAPDR@d*X!B0!j`zqv0xhqJvNSTP8n}0Jvj*-cuZfM3QMMpFo4dmk z2uqPt-uK98P9Uug=Euhc1YIk<*Kr&)fQ85Tu69%l3#E{AiZ zIX386=E7ZCewOakbPPJ8^)Te28279r0x@RuJoo!#I>N!atD))h71UB$kIgP>1j4`L4{e9+jqMG)j7GmxDOG~s~8S%D@CI|;~M*g1h_rbU3Jy5r2I z01CZUKA?#Qz(Gsf|GSHU?5R1ma{(d8`E}5YY$YmOmIr9z8Qg22CDr}a)n&~*Kx)*{ zE%@qFZMW<2l}L^H$|MJn?laCKkb^vmqSTOR9)Ke4JO_{)JvWva9$urScxgAngEH0V zvSkh+qzD|=>IMxIKV{Exz5*?R%W?-TJVZ+gQfr`M4?$*-+(2ds(mhaF3G*!~eZe5~ z+3jCG_wz#sota>ejZW;vXMVm^Jcue7glZl@RV4WzfbKw1D9|0qr9q3FK@bGeX^@LR zkeoqt8e|CtsWiyR8>9vaKn4w(AP`)ZZWbD;CHR2bvlz zkSz|hWP*&QFwmS0(uIMp*dRv~NHy3XV+%%2WrruhEPzK2K9x4+BE1yBxtD6 zAf+V`)MJ4LNkQLd4r(bZP_2;+wDVKupaB!4loNq=3I|z&mi`T5f`VJ*@<5uLAt?9- zx)%ddy08)w>vG}d!1;QRmMKX09F2B7=mY^~>p(klmY`Ws@^PT>(2V99 zP~}FTA1j)s?LJ_GLQh|b0m09Wb~7GSxDn_;_M;V9Ao%XH4pgiAA|^8)=NY+>& z9eL`t@N=0U5b1Fg#Wstl_ z0M5olUuol@4vrX)VS(f*6OcrBR@(?8FMq;`L9rpm2xYaj5s8YKfJ!(BTWc9%j4D(< zwjuNe+Qlt^M%e0}$0(DaAzTIQAQAyOO$<7G9$RLw$6hYpc)1+?frFs%FGrW?iVJk@ zYrLaE_MaRk1fd*5_gJ3}4T?{(0G`w18!j}xdb}BEKp@!f>hGQ_e$y~*|@FU8_h&-45c->=+GzBc&H zfksWjLeG>jCf7DxN_U@xu3i1H;m~$*fT5-?`Gc47ph5pJu*7z8H5` zUTe^xGp~TQ=LvQW&YL8VW^pbibE`#Uq##?XH?AtPv~f<}gvaavlGur~ndy+0)Hbm! zcA43`%2VGw!#z8fm{LU;+u5MiQ&nO{CAG}*YEO7>9&VUiM!Ar+-*P0!QBh>%I7JaD zJ2*X;nHd}r-()ckPQMHre@BFNPp0~$>*~6Qe@<$yu8EPeI7{iQJxz=t1wqWiTHngi z4ABHoaU7VKRJlhtZIP;9>~=sEXZALk;D0P=gN#in(H>;@-W%=$5-_oq<&9oQHBPIj z3K>$!@I7fmh&z2`#CbLrx(h;5Bah2fsFGIzK*S@H@$)(r=KK zb3;Z87pgklHfc~|&jaiUKuxB~S)ve`j6uo+bUQRRZoM5BJMSQ6lSh38q*g9D82yL@ z|7SM9wD4`hppGM8!T=cvm@w=K1WW+P;Mpb&kb!~;1{o}10_+JUFaaQgrTNXyqe*gdg07*qoM6N<$g6NYL=Kufz literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6c7bfca2af8281213496bb32eb5352293bacacd6 GIT binary patch literal 2458 zcmYjS4LH+l8($M49d(j)5{l})gi7T^81_BeQoNiF7WMk5qj^gl-x5kGgo9!TQ%PP? zaxnE)B5Bh~Nh4wVFRdLmQ)70X{il!XdamcXpZod!e)sRbpZ~RG?55zB=>DXOKp>V7 z@aR1V#DZ85yJ%~Gy}ww}g+L(XyFI)x;3y=a`6Mi#OjatDAW+)bfnbytEg-3qV27|+0Uj?Ps*qzK z23K%iKtihp-xE=QIG-e{#MNA7axz4~4iE^cE1--(r~>sOsJIXbJL0`_3~)(sU81@= zX2e6~1i0c8-Bd7`qB2QPc>qgQLS6C870B>mhidQ_3|0iHldu8;n(qO(37W#;U}cpE zU)3WT*nsKt$|`ZVgwO*g1YA(U0|&u<4WZypL21=e%>jVbczDRFxs7_dkHe#W;q47x z2UJ%B<}InGs-6;f8u%7W36_DSNGf|@k8nHitiHqW@n|(h2=X)Q+;{hEIK?@(z>hIyZ9e(<+`YNQy zx1qrk`#L}{y7?G5{MG{eL8=u%QoX>j`E%eb`1)rf_fYmC*4gP+25XisGcNo5wlL>M z(R&s>h?(N9?~UuCX^rs>EZzwqvCN`C44!EIo=-8D2sviIP>iKz!= zQM*Erm{{MpcMMx)g2yG?2b;UJZ~wLC&*sEyN$G`GommZH|L%zG5x?{VVk>8MY}Dch z>G$NjzA|1hIE(q^2#O&O`t=fo>K&~zNH6_lTlSG1__Q%?`)%;^pt+ZTcJ=xrK=dXg z(!kr`?!;dnylH=^Km_w@uG)&2i^V z-{kf-Hw-4w`E8eq?<>c7`VH-NG=A~4jRo&`nk;I={i)*Nh^>C6UNr;9Ad~#uiYq)O zC%|61_wvwx&6$Fe%Ts$>_C9v4;_E=XwHKu2sHJVBRd~B)BFSS61Ad`@|?0G>~8wvo6@-BTJlfB zKgn8KH!03|mYgt|PE$U&IySS^md$miMoVV7E~grG1dZu8Bliuz_2_`i?#z&p_Q6wo=ta;%wI&56}Q?b9AXCVdq{3MDM4D%X_wvComh= z_~}~I)%E*nNFOlJW7Zt*hNsSVU7TwFbG*&iNzW^<2~BMoXCR=kno~NQ!>`2JN2OgC zC1lQl`)No(DJZC68Baud;`zvg_fcS~e$((QFccSvbS)2Kg81DUf(g`u^m!+BasK+{E&wF&zP^tXdw_qosFaeLD0qv`ZpGO6e!K14h19 zWS$|rFIONEaoaU0ibsJcOu_2QrR7%afdOV|c(7fkuLII2KWIhvNIb`o{ zO1ox56`frbXC*aF91ZB+ld&MrM?V+}3G3t%x&7jxEY`&Gh-u*;QvZ0H!gA%4e#e(9 z+pGddSH=A%Elpa`D6fYaJZUMbdh~AB-6$W*U{hO|ytHKwDePWC#n!HC??e8ptfZqgKF|h*kZB2% zW)hy5O1Aco@=`3fD|R+GEHaCbr~kiQ*R9N^j;^%Nh+%SCKdlvmzZnDpLqXqg+n@3e DtE*cX literal 0 HcmV?d00001 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 items; + public MenuItem currentItem; + public int height; + } + + class MenuItem { + public String name; + public int row; + public int col; + public boolean enabled; + public int controller; + } + + /** + * static finalructor for Menu. + * + * @param state + * @param textGraphics + * @param pixels + * @param userInput + */ + public Menu(GameState state, TextGraphics textGraphics, short[] pixels, UserInput userInput) { + this.state = state; + this.textGraphics = textGraphics; + this.headers = new ArrayList(); + this.pixels = pixels; + this.userInput = userInput; + } + + /** + * Creates a new menu with the given name. + * + * @param menuName The name of the new menu. + */ + public void setMenu(String menuName) { + // We can't accept any more menu definitions if submit.menu has already been executed. + if (menuSubmitted) return; + + if (currentHeader == null) { + // The first menu header starts at column 1. + menuCol = 1; + } + else if (currentHeader.items.size() == 0) { + // If the last header didn't have any items, then disable it. + currentHeader.title.enabled = false; + } + + // Create a new MenuHeader. + MenuHeader header = new MenuHeader(); + + // Set the position of this menu name in the menu strip (leave two + // chars between menu titles). + header.title = new MenuItem(); + header.title.row = 0; + header.title.name = menuName; + header.title.col = menuCol; + header.title.enabled = true; + header.items = new ArrayList(); + header.height = 0; + + this.currentHeader = header; + this.headers.add(header); + + // Adjust the menu column for the next header. + menuCol += menuName.length() + 1; + + // Initialize stuff for the menu items to follow. + currentItem = null; + itemRow = 1; + } + + /** + * Creates a new menu item in the current menu, of the given name and mapped + * to the given controller number. + * + * @param itemName The name of the new menu item. + * @param controller The number of the controller to map this menu item to. + */ + public void setMenuItem(String itemName, int controller) { + // We can't accept any more menu definitions if submit.menu has already been executed. + if (menuSubmitted) return; + + // Create and define the new menu item and its position. + MenuItem menuItem = new MenuItem(); + menuItem.name = itemName; + menuItem.controller = controller; + if (itemRow == 1) { + if (currentHeader.title.col + itemName.length() < 39) { + itemCol = currentHeader.title.col; + } + else { + itemCol = 39 - itemName.length(); + } + } + menuItem.row = ++itemRow; + menuItem.col = itemCol; + menuItem.enabled = true; + + // Add the menu item to the current header's item list. + currentItem = menuItem; + currentHeader.items.add(menuItem); + currentHeader.height++; + if (currentHeader.currentItem == null) { + currentHeader.currentItem = menuItem; + } + } + + /** + * Signals to the menu system that the menu has now been fully defined. No further SetMenu + * or SetMenuItem calls will be processed. The current header and item is reset back to the + * first item in the first menu, ready for usage when the menu is activated. + */ + public void submitMenu() { + // If the last menu didn't have any items, disable it. + if (currentHeader.items.size() == 0) { + currentHeader.title.enabled = false; + } + + // Make the first menu the current one. + currentHeader = (headers.size() > 0? headers.get(0) : null); + currentItem = ((currentHeader != null) && (currentHeader.items.size() > 0) ? currentHeader.items.get(0) : null); + + // Remember that the submit has happened. We can't process menu definitions after submit.menu + menuSubmitted = true; + } + + /** + * Enables all MenuItems that map to the given controller number. + * + * @param controller The controller whose menu items should be enabled. + */ + public void enableItem(int controller) { + for (MenuHeader header : headers) { + for (MenuItem item : header.items) { + if (item.controller == controller) { + item.enabled = true; + } + } + } + } + + /** + * Enables all MenuItems. + */ + public void enableAllMenus() { + for (MenuHeader header : headers) { + for (MenuItem item : header.items) { + item.enabled = true; + } + } + } + + /** + * Disables all MenuItems that map to the given controller number. + * + * @param controller The controller whose menu items should be disabled. + */ + public void disableItem(int controller) { + for (MenuHeader header : headers) { + for (MenuItem item : header.items) { + if (item.controller == controller) { + item.enabled = false; + } + } + } + } + + /** + * Opens the menu system and processes all the navigation events until an item is either + * selected or the ESC key is pressed. + */ + public void menuInput() { + // Not sure why there is an ENABLE_MENU flag and the allow.menu command, but there is. + if (state.flags[Defines.ENABLE_MENU] && state.menuEnabled) { + // Clear the menu bar to white. + textGraphics.clearLines(0, 0, 15); + + // Draw each of the header titles in deselected mode. + for (MenuHeader header : headers) deselect(header.title); + + // Starts by showing the currently selected menu header and item. + showMenu(currentHeader); + + // Now we process all navigation keys until we the user either makes a selection + // or exits the menu system. + while (true) { + int index; + + switch (userInput.waitForKey()) { + + case (UserInput.ASCII | Character.ENTER): // Select the currently highlighted menu item. + if (!currentItem.enabled) continue; + state.controllers[currentItem.controller] = true; + putAwayMenu(currentHeader, currentItem); + restoreMenuLine(); + state.menuOpen = false; + return; + + case (UserInput.ASCII | Character.ESC): // Exit the menu system without a selection. + putAwayMenu(currentHeader, currentItem); + restoreMenuLine(); + state.menuOpen = false; + return; + + case Keys.UP: // Moving up within current menu. + deselect(currentItem); + index = (currentHeader.items.indexOf(currentItem) + currentHeader.items.size() - 1) % currentHeader.items.size(); + currentItem = currentHeader.items.get(index); + select(currentItem); + break; + + case Keys.PAGE_UP: // Move to top item of current menu. + deselect(currentItem); + currentItem = currentHeader.items.get(0); + select(currentItem); + break; + + case Keys.RIGHT: // Move to the menu on the right of the current menu.. + putAwayMenu(currentHeader, currentItem); + index = headers.indexOf(currentHeader); + do { currentHeader = headers.get((index = ((index + 1) % headers.size()))); } + while (!currentHeader.title.enabled); + currentItem = currentHeader.currentItem; + showMenu(currentHeader); + break; + + case Keys.PAGE_DOWN: // Move to bottom item of current menu. + deselect(currentItem); + currentItem = currentHeader.items.get(headers.size() - 1); + select(currentItem); + break; + + case Keys.DOWN: // Move down within current menu. + deselect(currentItem); + index = (currentHeader.items.indexOf(currentItem) + 1) % currentHeader.items.size(); + currentItem = currentHeader.items.get(index); + select(currentItem); + break; + + case Keys.END: // Move to the rightmost menu. + putAwayMenu(currentHeader, currentItem); + currentHeader = headers.get(headers.size() - 1); + currentItem = currentHeader.currentItem; + showMenu(currentHeader); + break; + + case Keys.LEFT: // Move left within current menu. + putAwayMenu(currentHeader, currentItem); + index = headers.indexOf(currentHeader); + do { currentHeader = headers.get((index = ((index + headers.size() - 1) % headers.size()))); } + while (!currentHeader.title.enabled); + currentItem = currentHeader.currentItem; + showMenu(currentHeader); + break; + + case Keys.HOME: // Move to the leftmost menu. + putAwayMenu(currentHeader, currentItem); + currentHeader = headers.get(0); + currentItem = currentHeader.currentItem; + showMenu(currentHeader); + break; + } + } + } + } + + /** + * Restores the state of what the menu line would have looked like prior to the menu being activated. + */ + private void restoreMenuLine() { + if (state.showStatusLine) { + textGraphics.updateStatusLine(); + } + else { + textGraphics.clearLines(0, 0, 0); + } + } + + /** + * Shows the menu items for the given MenuHeader. + * + * @param header The MenuHeader to show the menu items of. + */ + private void showMenu(MenuHeader header) { + // Interestingly, it would seem that the width is always calculated using the first item. The + // original AGI games tended to make the item names a consistent length within each menu. + MenuItem firstItem = (header.items.size() > 0 ? header.items.get(0) : null); + int height = header.height; + int width = (firstItem != null ? firstItem.name.length() : header.title.name.length()); + int column = (firstItem != null ? firstItem.col : header.title.col); + + // Compute window size and position and put them into the appropriate bytes of the words. + int menuDim = ((height * CHARHEIGHT + 2 * VMARGIN) << 8) | (width * CHARWIDTH + 2 * HMARGIN); + int menuPos = (((column - 1) * CHARWIDTH) << 8) | ((height + 1) * CHARHEIGHT + VMARGIN - 1); + + // Show the menu title as being selected. + select(header.title); + + // Open a window for this menu using the calculated position and dimensions. + textGraphics.openWindow(new TextWindow(menuPos, menuDim, 15, 0)); + + // Render each of the items in this menu. + for (MenuItem item : header.items) { + if (item == header.currentItem) { + select(item); + } + else { + deselect(item); + } + } + } + + /** + * Puts away the menu so that it is no longer displayed, but remembers what item + * in the list was selected at the time it was put away. + * + * @param header The MenuHeader representing the menu to put away. + * @param item The MenuItem that was currently selected in the menu when it was put away. + */ + private void putAwayMenu(MenuHeader header, MenuItem item) { + header.currentItem = item; + deselect(header.title); + textGraphics.closeWindow(); + } + + /** + * Renders the given MenuItem in a selected state. + * + * @param item The MenuItem to render in the selected state. + */ + private void select(MenuItem item) { + textGraphics.drawString(pixels, item.name, item.col * 8, item.row * 8, 15, 0, !item.enabled); + } + + /** + * Renders the given MenuItem in a deselected state. + * + * @param item The MenuItem to render in the deselected state. + */ + private void deselect(MenuItem item) { + textGraphics.drawString(pixels, item.name, item.col * 8, item.row * 8, 0, 15, !item.enabled); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/agifans/agile/Parser.java b/core/src/main/java/com/agifans/agile/Parser.java new file mode 100644 index 0000000..a02c2bd --- /dev/null +++ b/core/src/main/java/com/agifans/agile/Parser.java @@ -0,0 +1,188 @@ +package com.agifans.agile; + +import java.util.ArrayList; +import java.util.List; + +/** + * The Parser class is responsible for parsing the user input line to match known words and + * also to implement the 'said' and 'parse' commands. + */ +public class Parser { + + /** + * The List of word numbers for the recognised words from the current user input line. + */ + private List recognisedWordNumbers; + + /** + * These are the characters that separate words in the user input string (although + * usually it would be space). + */ + private String SEPARATORS = "[ ,.?!();:\\[\\]{}]+"; + + /** + * A regex matching the characters to be deleted from the user input string. + */ + private String IGNORE_CHARS = "['`\\-\"]"; + + /** + * Special word number that matches any word. + */ + private int ANYWORD = 1; + + /** + * Special word number that matches the rest of the line. + */ + private int REST_OF_LINE = 9999; + + /** + * The GameState class holds all of the data and state for the Game currently + * being run by the interpreter. + */ + private GameState state; + + /** + * Constructor for Parser. + * + * @param state The GameState class holds all of the data and state for the Game currently being run. + */ + public Parser(GameState state) { + this.state = state; + this.recognisedWordNumbers = new ArrayList(); + } + + /** + * Parses the given user input line value. This is the method invoked by the main keyboard + * processing logic. After execution of this method, the RecognisedWords List will contain + * the words that were recognised from the input line, and the recognisedWordNumbers List + * will contain the word numbers for the recognised words. If the RecognisedWords List + * contains one more item than the recognisedWordNumbers List then the additional word + * will actually be an unrecognised word and the UNKNOWN_WORD var will contain the index + * of that word within the List + 1. The INPUT flag will be set if the RecognisedWords + * List contains at least one word. + * + * @param inputLine + */ + public void parse(String inputLine) { + // Clear the words matched from last time. + state.recognisedWords.clear(); + this.recognisedWordNumbers.clear(); + + // Remove ignored characters and collapse separators into a single space char. + String sanitisedInputLine = inputLine.toLowerCase().replaceAll(IGNORE_CHARS, "").replaceAll(SEPARATORS, " ").trim(); + + if (sanitisedInputLine.length() > 0) { + int inputLineStartPos = 0; + + while (inputLineStartPos < sanitisedInputLine.length()) { + // Scan backwards from the end of the input line, to the current input line start pos, to find the longest word match. + for (int inputLineEndPos = sanitisedInputLine.length(); inputLineEndPos >= inputLineStartPos; inputLineEndPos--) { + if ((inputLineEndPos == sanitisedInputLine.length()) || (sanitisedInputLine.charAt(inputLineEndPos) == ' ')) { + // This is the end of a word in the input line. Check if we have a match. + String wordToMatch = sanitisedInputLine.substring(inputLineStartPos, inputLineEndPos); + + if (state.words.wordToNumber.containsKey(wordToMatch)) { + // The word is recognised. This is the longest match possible, so let's get the word number for it. + int matchedWordNum = state.words.wordToNumber.get(wordToMatch); + + // If the word number is 0, it is ignored. + if (matchedWordNum > 0) { + // Otherwise store matched word details. + state.recognisedWords.add(wordToMatch); + this.recognisedWordNumbers.add(matchedWordNum); + } + + // Set the next start position to character after the separator that ended the matched word + // so that we can continue scanning the rest of the input line for more words. + inputLineStartPos = inputLineEndPos + 1; + break; + } + else if (wordToMatch.equals("a") || wordToMatch.equals("i")) { + // Skip "a" and "i". Move input line start position beyond it. + inputLineStartPos = inputLineEndPos + 1; + break; + } + else if (!wordToMatch.contains(" ")) { + // Unrecognised single word. Stores the word, use ANYWORD (word number 1, place holder for any word) + state.recognisedWords.add(wordToMatch); + this.recognisedWordNumbers.add(ANYWORD); + state.vars[Defines.UNKNOWN_WORD] = (byte)(state.recognisedWords.size()); + inputLineStartPos = sanitisedInputLine.length(); + break; + } + } + } + } + } + + if (state.recognisedWords.size() > 0) { + state.flags[Defines.INPUT] = true; + } + } + + /** + * Implements the 'parse' AGI command. What it does is to parse a string as if it + * was the normal user input line. It does this simply by calling the Parse method + * above with the value from the identified AGI string. It resets both the INPUT + * and HADMATCH flags prior to calling it so that the normal user input parsing + * state is cleared. The words will be available to all said() tests for the + * remainder of the current logic scan. + * + * @param strNum The number of the AGI string to parse the value of. + */ + public void parseString(int strNum) { + // Clear the state from the most recent parse. + state.flags[Defines.INPUT] = false; + state.flags[Defines.HADMATCH] = false; + + // If the given string number is less that the total number of strings. + if (strNum < Defines.NUMSTRINGS) { + // Parse the value of the string as if it was user input. + parse(state.strings[strNum]); + } + } + + /** + * Returns true if the number of non-ignored words in the input line is the same + * as that in the word list and the non-ignored words in the input match, in order, + * the words in the word list. The special word 'anyword' (or whatever is defined + * word list as word 1 in 'WORDS.TOK') matches any non-ignored word in the input. + * + * @param words The List of words to test if the user has said. + * + * @param true if the user has said the given words; otherwise false. + */ + public boolean said(List wordNumbers) { + // If there are no recognised words then we obviously didn't say what we're testing against. + if (this.recognisedWordNumbers.size() == 0) return false; + + // We should only perform the check if we have input, and there hasn't been a match already. + if (!state.flags[Defines.INPUT] || state.flags[Defines.HADMATCH]) return false; + + // Compare each word number in order. + for (int i=0; i < wordNumbers.size(); i++) { + int testWordNumber = wordNumbers.get(i); + + // If test word number matches the rest of the line, then it's a match. + if (testWordNumber == REST_OF_LINE) { + state.flags[Defines.HADMATCH] = true; + return true; + } + + // Exit if we have reached the end of the user entered words. No match. + if (i >= recognisedWordNumbers.size()) return false; + + int inputWordNumber = this.recognisedWordNumbers.get(i); + + // If word numbers don't match, and test word number doesn't represent anyword, then no match. + if ((testWordNumber != inputWordNumber) && (testWordNumber != ANYWORD)) return false; + } + + // If more words were entered than in the said, and there obviously wasn't a REST_OF_LINE, then no match. + if (state.recognisedWords.size() > wordNumbers.size()) return false; + + // Otherwise if we get this far without having exited already, it is a match. + state.flags[Defines.HADMATCH] = true; + return true; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/agifans/agile/QuitAction.java b/core/src/main/java/com/agifans/agile/QuitAction.java new file mode 100644 index 0000000..647e5fd --- /dev/null +++ b/core/src/main/java/com/agifans/agile/QuitAction.java @@ -0,0 +1,14 @@ +package com.agifans.agile; + +/** + * Not really an Exception as such. This is how we exit out of AGILE from the + * quit() AGI command. Rather than doing an immediate System.exit or something + * similar, the Interpreter will instead throw an instance of QuitAction, + * indicating that the Interpreter should exit cleanly. + */ +public class QuitAction extends RuntimeException { + + public static void exit() { + throw new QuitAction(); + } +} diff --git a/core/src/main/java/com/agifans/agile/SaveArea.java b/core/src/main/java/com/agifans/agile/SaveArea.java new file mode 100644 index 0000000..4a68759 --- /dev/null +++ b/core/src/main/java/com/agifans/agile/SaveArea.java @@ -0,0 +1,20 @@ +package com.agifans.agile; + +/** + * Holds data about an AnimatedObject's background save area. + */ +public class SaveArea { + + public short x; + + public short y; + + public int width; + + public int height; + + public short[][] visBackPixels; + + public int[][] priBackPixels; + +} diff --git a/core/src/main/java/com/agifans/agile/SavedGames.java b/core/src/main/java/com/agifans/agile/SavedGames.java new file mode 100644 index 0000000..39dd50f --- /dev/null +++ b/core/src/main/java/com/agifans/agile/SavedGames.java @@ -0,0 +1,1195 @@ +package com.agifans.agile; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.text.MessageFormat; +import java.util.Map.Entry; + +import com.agifans.agile.AnimatedObject.CycleType; +import com.agifans.agile.AnimatedObject.MotionType; +import com.agifans.agile.ScriptBuffer.ScriptBufferEvent; +import com.agifans.agile.ScriptBuffer.ScriptBufferEventType; +import com.agifans.agile.TextGraphics.TextWindow; +import com.badlogic.gdx.Input.Keys; + +/** + * A class or saving and restoring saved games. + */ +public class SavedGames { + + private static final int SAVENAME_LEN = 30; + private static final int NUM_GAMES = 12; + private static final int GAME_INDENT = 3; + private static final char POINTER_CHAR = (char)26; + private static final char ERASE_CHAR = (char)32; + + // Keeps track of whether it is the first time a save/restore is happening in simple mode. + private boolean firstTime = true; + + // Messages for the various window dialogs that are shown as part of the Save / Restore functionality. + private String simpleFirstMsg = "Use the arrow keys to move\n the pointer to your name.\nThen press ENTER\n"; + private String simpleSelectMsg = " Sorry, this disk is full.\nPosition pointer and press ENTER\n to overwrite a saved game\nor press ESC and try again \n with another disk\n"; + private String selectSaveMsg = "Use the arrow keys to select the slot in which you wish to save the game. Press ENTER to save in the slot, ESC to not save a game."; + private String selectRestoreMsg = "Use the arrow keys to select the game which you wish to restore. Press ENTER to restore the game, ESC to not restore a game."; + private String newDescriptMsg = "How would you like to describe this saved game?\n\n"; + private String noGamesMsg = "There are no games to\nrestore in\n\n{0}\n\nPress ENTER to continue."; + + // Data type for storing data about a single saved game file. + class SavedGame { + public int num; + public boolean exists; + public String fileName; + public long fileTime; + public String description; + public byte[] savedGameData; + } + + /** + * 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 on which the background Picture and + * AnimatedObjects will be drawn to. + */ + private short[] pixels; + + /** + * Constructor for SavedGames. + * + * @param state + * @param userInput + * @param textGraphics + * @param pixels + */ + public SavedGames(GameState state, UserInput userInput, TextGraphics textGraphics, short[] pixels) { + this.state = state; + this.userInput = userInput; + this.textGraphics = textGraphics; + this.pixels = pixels; + } + + /** + * Chooses a saved game to either save to or restore from. The choice is either automatic + * such as in the case of simple save, or by the user. + * + * @param function 's' for save, 'r' for restore. + * + * @return + */ + private SavedGame chooseGame(char function) { + SavedGame[] game = new SavedGame[NUM_GAMES]; + int gameNum, numGames, mostRecentGame = 0; + long mostRecentTime = 0; + boolean simpleSave = (state.simpleName.length() > 0); + + try { + // Create saved game directory for this game if it doesn't yet exist. + Files.createDirectories(Paths.get(getSavePath())); + } catch (IOException ioe) { + // TODO: Handle this exception. + } + + // Look for the game files and get their data and meta data. + if (function == 's') { + // We're saving a game. + for (gameNum = 0; gameNum < NUM_GAMES; gameNum++) { + game[gameNum] = getGameByNumber(gameNum + 1); + + if (game[gameNum].exists && (game[gameNum].fileTime > mostRecentTime)) { + mostRecentTime = game[gameNum].fileTime; + mostRecentGame = gameNum; + } + } + + numGames = NUM_GAMES; + } + else { + // We're restoring a game. + for (gameNum = numGames = 0; gameNum < NUM_GAMES; gameNum++) { + game[numGames] = getGameByNumber(gameNum + 1); + + if (game[numGames].exists) { + if (game[numGames].fileTime > mostRecentTime) { + mostRecentTime = game[numGames].fileTime; + mostRecentGame = numGames; + } + + // Count how many saved games we currently have. + numGames++; + } + } + + if (numGames == 0) { + if (!simpleSave) { + // For normal save, if there are no games to display, tell the user so. + textGraphics.windowPrint(MessageFormat.format(noGamesMsg, getSavePath().replace("\\", "\\\\"))); + } + + // If there are no games to restore, exit at this point. + return null; + } + } + + if (simpleSave && !firstTime) { + // See if we have a slot for the current simple name value. + for (gameNum = 0; gameNum < NUM_GAMES; gameNum++) { + if (game[gameNum].description.equals(state.simpleName)) { + return (game[gameNum]); + } + } + + if (function == 's') { + // For simple save, we automatically find an empty slot for new saved game. + for (gameNum = 0; gameNum < NUM_GAMES; gameNum++) { + if ((game[gameNum].description == null) || (game[gameNum].description.equals(""))) { + // Description is automatically set to the SimpleName value if it is set. + game[gameNum].description = state.simpleName; + return (game[gameNum]); + } + } + } + + // If none available, fall thru to window. + + // We shouldn't be able to get to this point in restore mode, but just in case, return null. + if (function == 'r') return null; + } + + // Compute the height of the window desired and put it up + int descriptTop = 5; + int height = numGames + descriptTop; + TextWindow textWin = textGraphics.windowNoWait(simpleSave ? (firstTime ? simpleFirstMsg : simpleSelectMsg) : + (function == 's') ? selectSaveMsg : selectRestoreMsg, + height, SAVENAME_LEN + GAME_INDENT + 1, true); + + descriptTop += textWin.top; + firstTime = false; + + // Print the game descriptions within the open window.. + for (gameNum = 0; gameNum < numGames; gameNum++) { + textGraphics.drawString(this.pixels, MessageFormat.format(" - {0}", game[gameNum].description), + textWin.left * 8, (descriptTop + gameNum) * 8, 0, 15); + } + + // Put up the pointer, defaulting to most recently saved game, and then let the user start + // scrolling around with it to make a choice. + gameNum = mostRecentGame; + writePointer(textWin.left, descriptTop + gameNum); + + while (true) { + switch (userInput.waitForKey()) { + case (UserInput.ASCII | Character.ENTER): + if (simpleSave && (function == 'r')) { + // If this is a restore in simple save mode, it must be the first one, in which + // case we remember the selection in the SimpleName var so that it automatically + // restores next time the user restores. + state.simpleName = game[gameNum].description; + } + if (!simpleSave && (function == 's')) { + // If this is a save in normal save mode, then we ask the user to confirm/enter + // the description for the save game. + if ((game[gameNum].description = getWindowStr(newDescriptMsg, game[gameNum].description)) == null) { + // If they have pressed ESC, we return null to indicate not to continue. + return null; + } + } + textGraphics.closeWindow(); + return (game[gameNum]); + + case (UserInput.ASCII | Character.ESC): + textGraphics.closeWindow(); + return null; + + case Keys.UP: + erasePointer(textWin.left, descriptTop + gameNum); + gameNum = (gameNum == 0) ? numGames - 1 : gameNum - 1; + writePointer(textWin.left, descriptTop + gameNum); + break; + + case Keys.DOWN: + erasePointer(textWin.left, descriptTop + gameNum); + gameNum = (gameNum == numGames - 1) ? 0 : gameNum + 1; + writePointer(textWin.left, descriptTop + gameNum); + break; + } + } + } + + /** + * + * @param num + * + * @return + */ + private SavedGame getGameByNumber(int num) { + SavedGame theGame = new SavedGame(); + theGame.num = num; + + // Build full path to the saved game of this number for this game ID. + theGame.fileName = MessageFormat.format("{0}\\{1}SG.{2}", getSavePath(), state.gameId, num); + + File savedGameFile = new File(theGame.fileName); + theGame.savedGameData = new byte[(int)savedGameFile.length()]; + + try (FileInputStream fis = new FileInputStream(savedGameFile)) { + int bytesRead = fis.read(theGame.savedGameData); + if (bytesRead != savedGameFile.length()) { + theGame.description = ""; + theGame.exists = false; + return theGame; + } + } + catch (FileNotFoundException fnfe) { + // There is no saved game file of this name, so return false. + theGame.description = ""; + theGame.exists = false; + return theGame; + } + catch (Exception e) { + // Something unexpected happened. Bad file I guess. Return false. + theGame.description = ""; + theGame.exists = false; + return theGame; + } + + // Get last modified time as an epoch time, i.e. seconds since start of + // 1970 (which I guess must have been when the big bang was). + theGame.fileTime = ((new File(theGame.fileName)).lastModified() / 1000); + + // 0 - 30(31 bytes) SAVED GAME DESCRIPTION. + int textEnd = 0; + while (theGame.savedGameData[textEnd] != 0) textEnd++; + String savedGameDescription = new String(theGame.savedGameData, 0, textEnd, Charset.forName("Cp437")); + + // 33 - 39(7 bytes) Game ID("SQ2", "KQ3", "LLLLL", etc.), NUL padded. + textEnd = 33; + while ((theGame.savedGameData[textEnd] != 0) && ((textEnd - 33) < 7)) textEnd++; + String gameId = new String(theGame.savedGameData, 33, textEnd - 33, Charset.forName("Cp437")); + + // If the saved Game ID doesn't match the current, don't use this game. + if (!gameId.equals(state.gameId)) { + theGame.description = ""; + theGame.exists = false; + return theGame; + } + + // If we get this far, there is a valid saved game with this number for this game. + theGame.description = savedGameDescription; + theGame.exists = true; + return theGame; + } + + /** + * Displays the pointer character at the specified screen position. + * + * @param col + * @param row + */ + private void writePointer(int col, int row) { + textGraphics.drawChar(this.pixels, (byte)POINTER_CHAR, col * 8, row * 8, 0, 15); + } + + /** + * Erases the pointer character from the specified screen position. + * + * @param col + * @param row + */ + private void erasePointer(int col, int row) { + textGraphics.drawChar(this.pixels, (byte)ERASE_CHAR, col * 8, row * 8, 0, 15); + } + + /** + * Gets a String from the user by opening a window dialog. + * + * @param msg + * + * @return The entered text. + */ + private String getWindowStr(String msg) { + return getWindowStr(msg, ""); + } + + /** + * Gets a String from the user by opening a window dialog. + * + * @param msg + * @param str + * + * @return The entered text. + */ + private String getWindowStr(String msg, String str) { + // Open a new window with the message text displayed. + TextWindow textWin = textGraphics.windowNoWait(msg, 0, SAVENAME_LEN+1, true); + + // Clear the input row to black on top of the window. + textGraphics.clearRect(textWin.bottom, textWin.left, textWin.bottom, textWin.right - 1, 0); + + // Get the line of text from the user. + String line = textGraphics.getLine(SAVENAME_LEN, (byte)textWin.bottom, (byte)textWin.left, str, 15, 0); + + textGraphics.closeWindow(); + + return line; + } + + /** + * Gets the full path of the folder to use for reading and writing saved games. + * + * @return The full path of the folder to use for reading and writing saved games. + */ + private String getSavePath() { + // TODO: Will need an alternative approach when GWT is supported. + StringBuilder savedGamesPath = new StringBuilder(); + savedGamesPath.append(System.getProperty("user.home")); + savedGamesPath.append(System.getProperty("file.separator")); + savedGamesPath.append("Saved Games"); + savedGamesPath.append(System.getProperty("file.separator")); + savedGamesPath.append(state.gameId); + return savedGamesPath.toString(); + } + + /** + * Returns the length of the save variables part of a saved game. + * + * @param version The AGI interpreter version string. + * + * @return The length of the save variables part of a saved game + */ + private int getSaveVariablesLength(String version) { + switch (version) { + case "2.089": + case "2.272": + case "2.277": + return 0x03DB; + + case "2.411": + case "2.425": + case "2.426": + case "2.435": + case "2.439": + case "2.440": + return 0x05DF; + + case "3.002.102": + case "3.002.107": + // TODO: Not yet sure what the additional 3 bytes are used for. + return 0x05E4; + + case "3.002.149": + // This difference between 3.002.107 and 3.002.149 is that the latter has only 12 strings (12x40=480=0x1E0) + return 0x0404; + + // Default covers all the 2.9XX versions, 3.002.086 and 3.002.098. + default: + return 0x05E1; + } + } + + /** + * Returns the number of strings for the given AGI version. + * + * @param version The AGI version to return the number of strings for. + * + * @return The number of strings for the given AGI version. + */ + private int getNumberOfStrings(String version) { + switch (version) { + case "2.089": + case "2.272": + case "2.277": + case "3.002.149": + return 12; + // Most versions have 24 strings, as defined in the Defines constant. + default: + return Defines.NUMSTRINGS; + } + } + + /** + * Returns the number of controllers for the given AGI version. + * + * @param version The AGI version to return the number of controllers for. + * + * @return The number of controllers for the given AGI version. + */ + private int getNumberOfControllers(String version) { + switch (version) { + case "2.089": + case "2.272": + case "2.277": + return 40; + // Most versions have a max of 50 controllers, as defined in the Defines constant. + default: + return Defines.NUMCONTROL; + } + } + + /** + * Used to encrypt/decrypt the OBJECT section of the saved game file for AGI V3 games. Can't + * reuse the Objects class to do the crypting, as the saved game encrypts it from a different + * starting index, so the output is incompatible. + * + * @param data The byte array to crypt part of. + * @param start The start index to start crypting from. + * @param end The end index (exclusive) to crypt to. + */ + private void crypt(byte[] data, int start, int end) { + for (int i=0, j=start; j 0); + SavedGame savedGame = null; + + // Get the saved game file to save. + if ((savedGame = chooseGame('s')) == null) return; + + // If it is Simple Save mode then we skip asking them if they want to save. + if (!simpleSave) { + // Otherwise we prompt the user to confirm. + String msg = MessageFormat.format( + "About to save the game\ndescribed as:\n\n{0}\n\nin file:\n{1}\n\n{2}", + savedGame.description, savedGame.fileName.replace("\\", "\\\\"), + "Press ENTER to continue.\nPress ESC to cancel."); + textGraphics.windowNoWait(msg, 0, 35, false); + boolean abort = (userInput.waitAcceptAbort() == UserInput.ABORT); + textGraphics.closeWindow(); + if (abort) return; + } + + // No saved game will ever be as big as 20000, but we put that as a theoretical lid + // on the size based on rough calculations with all parts set to maximum size. We'll + // only write the bytes that use when created the file. + byte[] savedGameData = new byte[20000]; + int pos = 0; + + // 0 - 30(31 bytes) SAVED GAME DESCRIPTION. + for (byte b : savedGame.description.getBytes(Charset.forName("Cp437"))) { + savedGameData[pos++] = b; + } + + // FIRST PIECE: SAVE VARIABLES + // [0] 31 - 32(2 bytes) Length of save variables piece. Length depends on AGI interpreter version. + int saveVarsLength = getSaveVariablesLength(state.version); + int aniObjsOffset = 33 + saveVarsLength; + savedGameData[31] = (byte)(saveVarsLength & 0xFF); + savedGameData[32] = (byte)((saveVarsLength >> 8) & 0xFF); + + // [2] 33 - 39(7 bytes) Game ID("SQ2", "KQ3", "LLLLL", etc.), NUL padded. + pos = 33; + for (byte b : state.gameId.getBytes(Charset.forName("Cp437"))) { + savedGameData[pos++] = b; + } + + // [9] 40 - 295(256 bytes) Variables, 1 variable per byte + for (int i = 0; i < 256; i++) savedGameData[40 + i] = (byte)state.vars[i]; + + // [265] 296 - 327(32 bytes) Flags, 8 flags per byte + pos = 296; + for (int i = 0; i < 256; i+=8) { + savedGameData[pos++] = (byte)( + (state.flags[i + 0] ? 0x80 : 0x00) | (state.flags[i + 1] ? 0x40 : 0x00) | + (state.flags[i + 2] ? 0x20 : 0x00) | (state.flags[i + 3] ? 0x10 : 0x00) | + (state.flags[i + 4] ? 0x08 : 0x00) | (state.flags[i + 5] ? 0x04 : 0x00) | + (state.flags[i + 6] ? 0x02 : 0x00) | (state.flags[i + 7] ? 0x01 : 0x00)); + } + + // [297] 328 - 331(4 bytes) Clock ticks since game started. 1 clock tick == 50ms. + int saveGameTicks = (int)(state.totalTicks / 3); + savedGameData[328] = (byte)(saveGameTicks & 0xFF); + savedGameData[329] = (byte)((saveGameTicks >> 8) & 0xFF); + savedGameData[330] = (byte)((saveGameTicks >> 16) & 0xFF); + savedGameData[331] = (byte)((saveGameTicks >> 24) & 0xFF); + + // [301] 332 - 333(2 bytes) Horizon + savedGameData[332] = (byte)(state.horizon & 0xFF); + savedGameData[333] = (byte)((state.horizon >> 8) & 0xFF); + + // [303] 334 - 335(2 bytes) Key Dir + // TODO: Not entirely sure what this is for, so not currently saving this. + + // Currently active block. + // [305] 336 - 337(2 bytes) Upper left X position for active block. + savedGameData[336] = (byte)(state.blockUpperLeftX & 0xFF); + savedGameData[337] = (byte)((state.blockUpperLeftX >> 8) & 0xFF); + // [307] 338 - 339(2 bytes) Upper Left Y position for active block. + savedGameData[338] = (byte)(state.blockUpperLeftY & 0xFF); + savedGameData[339] = (byte)((state.blockUpperLeftY >> 8) & 0xFF); + // [309] 340 - 341(2 bytes) Lower Right X position for active block. + savedGameData[340] = (byte)(state.blockLowerRightX & 0xFF); + savedGameData[341] = (byte)((state.blockLowerRightX >> 8) & 0xFF); + // [311] 342 - 343(2 bytes) Lower Right Y position for active block. + savedGameData[342] = (byte)(state.blockLowerRightY & 0xFF); + savedGameData[343] = (byte)((state.blockLowerRightY >> 8) & 0xFF); + + // [313] 344 - 345(2 bytes) Player control (1) / Program control (0) + savedGameData[344] = (byte)(state.userControl ? 1 : 0); + // [315] 346 - 347(2 bytes) Current PICTURE number + savedGameData[346] = (byte)state.currentPicture.index; + // [317] 348 - 349(2 bytes) Blocking flag (1 = true, 0 = false) + savedGameData[348] = (byte)(state.blocking ? 1 : 0); + + // [319] 350 - 351(2 bytes) Max drawn. Always set to 15. Maximum number of animated objects that can be drawn at a time. Set by old max.drawn command in AGI v2.001. + savedGameData[350] = (byte)state.maxDrawn; + // [321] 352 - 353(2 bytes) Script size. Set by script.size. Max number of script event items. Default is 50. + savedGameData[352] = (byte)state.scriptBuffer.scriptSize; + // [323] 354 - 355(2 bytes) Current number of script event entries. + savedGameData[354] = (byte)state.scriptBuffer.scriptEntries(); + + // [325] 356 - 555(200 or 160 bytes) ? Key to controller map (4 bytes each). Earlier versions had less entries. + pos = 356; + int keyMapSize = getNumberOfControllers(state.version); + for (Entry entry : state.keyToControllerMap.entrySet()) { + if (entry.getKey() != 0) { + int keyCode = userInput.reverseKeyCodeMap.get(entry.getKey()); + int controllerNum = entry.getValue(); + savedGameData[pos++] = (byte)(keyCode & 0xFF); + savedGameData[pos++] = (byte)((keyCode >> 8) & 0xFF); + savedGameData[pos++] = (byte)(controllerNum & 0xFF); + savedGameData[pos++] = (byte)((controllerNum >> 8) & 0xFF); + } + } + + int postKeyMapOffset = 356 + (keyMapSize << 2); + + // [525] 556 - 1515(480 or 960 bytes) 12 or 24 strings, each 40 bytes long. For 2.4XX to 2.9XX, it was 24 strings. + int numOfStrings = getNumberOfStrings(state.version); + for (int i = 0; i < numOfStrings; i++) { + pos = postKeyMapOffset + (i * Defines.STRLENGTH); + if ((state.strings[i] != null) && (state.strings[i].length() > 0)) { + for (byte b : state.strings[i].getBytes(Charset.forName("Cp437"))) { + savedGameData[pos++] = b; + } + } + } + + int postStringsOffset = postKeyMapOffset + (numOfStrings * Defines.STRLENGTH); + + // [1485] 1516(2 bytes) Foreground colour + savedGameData[postStringsOffset + 0] = (byte)state.foregroundColour; + + // TODO: Need to fix the foreground and background colour storage. + + // [1487] 1518(2 bytes) Background colour + //int backgroundColour = (savedGameData[postStringsOffset + 2] + (savedGameData[postStringsOffset + 3] << 8)); + // TODO: Interpreter doesn't yet properly handle AGI background colour. + + // [1489] 1520(2 bytes) Text Attribute value (combined foreground/background value) + //int textAttribute = (savedGameData[postStringsOffset + 4] + (savedGameData[postStringsOffset + 5] << 8)); + + // [1491] 1522(2 bytes) Accept input = 1, Prevent input = 0 + savedGameData[postStringsOffset + 6] = (byte)(state.acceptInput ? 1 : 0); + + // [1493] 1524(2 bytes) User input row on the screen + savedGameData[postStringsOffset + 8] = (byte)state.inputLineRow; + + // [1495] 1526(2 bytes) Cursor character + savedGameData[postStringsOffset + 10] = (byte)state.cursorCharacter; + + // [1497] 1528(2 bytes) Show status line = 1, Don't show status line = 0 + savedGameData[postStringsOffset + 12] = (byte)(state.showStatusLine ? 1 : 0); + + // [1499] 1530(2 bytes) Status line row on the screen + savedGameData[postStringsOffset + 14] = (byte)state.statusLineRow; + + // [1501] 1532(2 bytes) Picture top row on the screen + savedGameData[postStringsOffset + 16] = (byte)state.pictureRow; + + // [1503] 1534(2 bytes) Picture bottom row on the screen + savedGameData[postStringsOffset + 18] = (byte)(state.pictureRow + 21); + + // [1505] 1536(2 bytes) Stores a pushed position within the script event list + // Note: Depends on interpreter version. 2.4xx and below didn't have push.script/pop.script, so they didn't have this saved game field. + if ((postStringsOffset + 20) < aniObjsOffset) { + // The spec is 2 bytes, but as with the fields above, there shouldn't be more than 255. + savedGameData[1536] = (byte)(state.scriptBuffer.savedScript); + } + + // Some AGI V3 versions have 3 additional bytes at this point. + // TODO: Work out what these 3 bytes are for and write them out here. + + // SECOND PIECE: ANIMATED OBJECT STATE + // 1538 - 1539(2 bytes) Length of piece + // Each ANIOBJ entry is 0x2B in length, i.e. 43 bytes. + int aniObjectsLength = ((state.objects.numOfAnimatedObjects + 1) * 0x2B); + savedGameData[aniObjsOffset + 0] = (byte)(aniObjectsLength & 0xFF); + savedGameData[aniObjsOffset + 1] = (byte)((aniObjectsLength >> 8) & 0xFF); + + for (int i=0; i < (state.objects.numOfAnimatedObjects + 1); i++) { + int aniObjOffset = aniObjsOffset + 2 + (i * 0x2B); + AnimatedObject aniObj = state.animatedObjects[i]; + + //UBYTE movefreq; /* number of animation cycles between motion */ e.g. 01 + savedGameData[aniObjOffset + 0] = (byte)aniObj.stepTime; + //UBYTE moveclk; /* number of cycles between moves of object */ e.g. 01 + savedGameData[aniObjOffset + 1] = (byte)aniObj.stepTimeCount; + //UBYTE num; /* object number */ e.g. 00 + savedGameData[aniObjOffset + 2] = aniObj.objectNumber; + //COORD x; /* current x coordinate */ e.g. 6e 00 (0x006e = ) + savedGameData[aniObjOffset + 3] = (byte)(aniObj.x & 0xFF); + savedGameData[aniObjOffset + 4] = (byte)((aniObj.x >> 8) & 0xFF); + //COORD y; /* current y coordinate */ e.g. 64 00 (0x0064 = ) + savedGameData[aniObjOffset + 5] = (byte)(aniObj.y & 0xFF); + savedGameData[aniObjOffset + 6] = (byte)((aniObj.y >> 8) & 0xFF); + //UBYTE view; /* current view number */ e.g. 00 + savedGameData[aniObjOffset + 7] = (byte)aniObj.currentView; + //VIEW* viewptr; /* pointer to current view */ e.g. 17 6b (0x6b17 = ) IGNORE. + //UBYTE loop; /* current loop in view */ e.g. 00 + savedGameData[aniObjOffset + 10] = (byte)aniObj.currentLoop; + //UBYTE loopcnt; /* number of loops in view */ e.g. 04 + if (aniObj.view() != null) savedGameData[aniObjOffset + 11] = (byte)aniObj.numberOfLoops(); + //LOOP* loopptr; /* pointer to current loop */ e.g. 24 6b (0x6b24 = ) IGNORE + //UBYTE cel; /* current cell in loop */ e.g. 00 + savedGameData[aniObjOffset + 14] = (byte)aniObj.currentCel; + //UBYTE celcnt; /* number of cells in current loop */ e.g. 06 + if (aniObj.view() != null) savedGameData[aniObjOffset + 15] = (byte)aniObj.numberOfCels(); + //CEL* celptr; /* pointer to current cell */ e.g. 31 6b (0x6b31 = ) IGNORE + //CEL* prevcel; /* pointer to previous cell */ e.g. 31 6b (0x6b31 = ) IGNORE + //STRPTR save; /* pointer to background save area */ e.g. 2f 9c (0x9c2f = ) IGNORE + //COORD prevx; /* previous x coordinate */ e.g. 6e 00 (0x006e = ) + savedGameData[aniObjOffset + 22] = (byte)(aniObj.prevX & 0xFF); + savedGameData[aniObjOffset + 23] = (byte)((aniObj.prevX >> 8) & 0xFF); + //COORD prevy; /* previous y coordinate */ e.g. 64 00 (0x0064 = ) + savedGameData[aniObjOffset + 24] = (byte)(aniObj.prevY & 0xFF); + savedGameData[aniObjOffset + 25] = (byte)((aniObj.prevY >> 8) & 0xFF); + //COORD xsize; /* x dimension of current cell */ e.g. 06 00 (0x0006 = ) + if (aniObj.view() != null) savedGameData[aniObjOffset + 26] = (byte)(aniObj.xSize() & 0xFF); + if (aniObj.view() != null) savedGameData[aniObjOffset + 27] = (byte)((aniObj.xSize() >> 8) & 0xFF); + //COORD ysize; /* y dimension of current cell */ e.g. 20 00 (0x0020 = ) + if (aniObj.view() != null) savedGameData[aniObjOffset + 28] = (byte)(aniObj.ySize() & 0xFF); + if (aniObj.view() != null) savedGameData[aniObjOffset + 29] = (byte)((aniObj.ySize() >> 8) & 0xFF); + //UBYTE stepsize; /* distance object can move */ e.g. 01 + savedGameData[aniObjOffset + 30] = (byte)aniObj.stepSize; + //UBYTE cyclfreq; /* time interval between cells of object */ e.g. 01 + savedGameData[aniObjOffset + 31] = (byte)aniObj.cycleTime; + //UBYTE cycleclk; /* counter for determining when object cycles */ e.g. 01 + savedGameData[aniObjOffset + 32] = (byte)aniObj.cycleTimeCount; + //UBYTE dir; /* object direction */ e.g. 00 + savedGameData[aniObjOffset + 33] = aniObj.direction; + //UBYTE motion; /* object motion type */ e.g. 00 + // #define WANDER 1 /* random movement */ + // #define FOLLOW 2 /* follow an object */ + // #define MOVETO 3 /* move to a given coordinate */ + savedGameData[aniObjOffset + 34] = (byte)aniObj.motionType.ordinal(); + //UBYTE cycle; /* cell cycling type */ e.g. 00 + // #define NORMAL 0 /* normal repetative cycling of object */ + // #define ENDLOOP 1 /* animate to end of loop and stop */ + // #define RVRSLOOP 2 /* reverse of ENDLOOP */ + // #define REVERSE 3 /* cycle continually in reverse */ + savedGameData[aniObjOffset + 35] = (byte)aniObj.cycleType.ordinal(); + //UBYTE pri; /* priority of object */ e.g. 09 + savedGameData[aniObjOffset + 36] = aniObj.priority; + + //UWORD control; /* object control flag (bit mapped) */ e.g. 53 40 (0x4053 = ) + int controlBits = + (aniObj.drawn ? 0x0001 : 0x00) | + (aniObj.ignoreBlocks ? 0x0002 : 0x00) | + (aniObj.fixedPriority ? 0x0004 : 0x00) | + (aniObj.ignoreHorizon ? 0x0008 : 0x00) | + (aniObj.update ? 0x0010 : 0x00) | + (aniObj.cycle ? 0x0020 : 0x00) | + (aniObj.animated ? 0x0040 : 0x00) | + (aniObj.blocked ? 0x0080 : 0x00) | + (aniObj.stayOnWater ? 0x0100 : 0x00) | + (aniObj.ignoreObjects ? 0x0200 : 0x00) | + (aniObj.repositioned ? 0x0400 : 0x00) | + (aniObj.stayOnLand ? 0x0800 : 0x00) | + (aniObj.noAdvance ? 0x1000 : 0x00) | + (aniObj.fixedLoop ? 0x2000 : 0x00) | + (aniObj.stopped ? 0x4000 : 0x00); + savedGameData[aniObjOffset + 37] = (byte)(controlBits & 0xFF); + savedGameData[aniObjOffset + 38] = (byte)((controlBits >> 8) & 0xFF); + + //UBYTE parms[4]; /* space for various motion parameters */ e.g. 00 00 00 00 + savedGameData[aniObjOffset + 39] = (byte)aniObj.motionParam1; + savedGameData[aniObjOffset + 40] = (byte)aniObj.motionParam2; + savedGameData[aniObjOffset + 41] = (byte)aniObj.motionParam3; + savedGameData[aniObjOffset + 42] = (byte)aniObj.motionParam4; + } + + // THIRD PIECE: OBJECTS + // Almost an exact copy of the OBJECT file, but with the 3 byte header removed, and room + // numbers reflecting the current location of each object. + byte[] objectData = state.objects.encode(); + int objectsOffset = aniObjsOffset + 2 + aniObjectsLength; + int objectsLength = objectData.length - 3; + savedGameData[objectsOffset + 0] = (byte)(objectsLength & 0xFF); + savedGameData[objectsOffset + 1] = (byte)((objectsLength >> 8) & 0xFF); + pos = objectsOffset + 2; + if (state.isAGIV3()) { + // AGI V3 games xor encrypt the data with Avis Durgan. Note that unlike the OBJECT + // file itself, the saved game OBJECT section crypts from index 3, since it does + // not output the 3 byte header, so starts the crypting after that. + crypt(objectData, 3, objectData.length); + } + for (int i=3; i> 8) & 0xFF); + pos = scriptsOffset + 2; + for (int i = 0; i < scriptEventData.length; i++) { + savedGameData[pos++] = scriptEventData[i]; + } + + // FIFTH PIECE: SCAN OFFSETS + int scanOffsetsOffset = scriptsOffset + 2 + scriptsLength; + int loadedLogicCount = 0; + // There is a scan offset for each loaded logic. + for (ScriptBufferEvent e : state.scriptBuffer.events) if (e.type == ScriptBufferEventType.LOAD_LOGIC) loadedLogicCount++; + // The scan offset data contains the offsets for loaded logics plus a 4 byte header, 4 bytes for logic 0, and 4 byte trailer. + int scanOffsetsLength = (loadedLogicCount * 4) + 12; + savedGameData[scanOffsetsOffset + 0] = (byte)(scanOffsetsLength & 0xFF); + savedGameData[scanOffsetsOffset + 1] = (byte)((scanOffsetsLength >> 8) & 0xFF); + pos = scanOffsetsOffset + 2; + // The scan offsets start with 00 00 00 00. + savedGameData[pos++] = 0; + savedGameData[pos++] = 0; + savedGameData[pos++] = 0; + savedGameData[pos++] = 0; + // And this is then always followed by an entry for Logic 0 + savedGameData[pos++] = 0; + savedGameData[pos++] = 0; + savedGameData[pos++] = (byte)(state.scanStart[0] & 0xFF); + savedGameData[pos++] = (byte)((state.scanStart[0] >> 8) & 0xFF); + // The scan offsets for the rest are stored in the order in which the logics were loaded. + for (ScriptBufferEvent e : state.scriptBuffer.events) { + if (e.type == ScriptBufferEventType.LOAD_LOGIC) { + int logicNum = e.resourceNumber; + int scanOffset = state.scanStart[logicNum]; + savedGameData[pos++] = (byte)(logicNum & 0xFF); + savedGameData[pos++] = (byte)((logicNum >> 8) & 0xFF); + savedGameData[pos++] = (byte)(scanOffset & 0xFF); + savedGameData[pos++] = (byte)((scanOffset >> 8) & 0xFF); + } + } + // The scan offset section ends with FF FF 00 00. + savedGameData[pos++] = (byte)0xFF; + savedGameData[pos++] = (byte)0xFF; + savedGameData[pos++] = 0; + savedGameData[pos++] = 0; + + // Write out the saved game data to the file. + try { + try (FileOutputStream outputStream = new FileOutputStream(savedGame.fileName)) { + outputStream.write(savedGameData, 0, pos); + } + } + catch (Exception e) { + this.textGraphics.print("Error in saving game.\nPress ENTER to continue."); + } + } + + /** + * Restores the GameState of the Interpreter from a saved game file. + * + * @return true if a game was restored; otherwise false + */ + public boolean restoreGameState() { + boolean simpleSave = (state.simpleName.length() > 0); + SavedGame savedGame = null; + + // Get the saved game file to restore. + if ((savedGame = chooseGame('r')) == null) return false; + + // If it is Simple Save mode then we skip asking them if they want to restore. + if (!simpleSave) { + // Otherwise we prompt the user to confirm. + String msg = MessageFormat.format( + "About to restore the game\ndescribed as:\n\n{0}\n\nfrom file:\n{1}\n\n{2}", + savedGame.description, savedGame.fileName.replace("\\", "\\\\"), + "Press ENTER to continue.\nPress ESC to cancel."); + textGraphics.windowNoWait(msg, 0, 35, false); + boolean abort = (userInput.waitAcceptAbort() == UserInput.ABORT); + textGraphics.closeWindow(); + if (abort) return false; + } + + byte[] savedGameData = savedGame.savedGameData; + + // 0 - 30(31 bytes) SAVED GAME DESCRIPTION. + int textEnd = 0; + while (savedGameData[textEnd] != 0) textEnd++; + String savedGameDescription = new String(savedGameData, 0, textEnd, Charset.forName("Cp437")); + + // FIRST PIECE: SAVE VARIABLES + // [0] 31 - 32(2 bytes) Length of save variables piece. Length depends on AGI interpreter version. [e.g. (0xE1 0x05) for some games, (0xDB 0x03) for some] + int saveVarsLength = (savedGameData[31] & 0xFF) + ((savedGameData[32] & 0xFF) << 8); + int aniObjsOffset = 33 + saveVarsLength; + + // [2] 33 - 39(7 bytes) Game ID("SQ2", "KQ3", "LLLLL", etc.), NUL padded. + textEnd = 33; + while ((savedGameData[textEnd] != 0) && ((textEnd - 33) < 7)) textEnd++; + String gameId = new String(savedGameData, 33, textEnd - 33, Charset.forName("Cp437")); + if (!gameId.equals(state.gameId)) return false; + + // If we're sure that this saved game file is for this game, then continue. + state.init(); + textGraphics.clearLines(0, 24, 0); + + // [9] 40 - 295(256 bytes) Variables, 1 variable per byte + for (int i=0; i<256; i++) state.vars[i] = (savedGameData[40 + i] & 0xFF); + + // [265] 296 - 327(32 bytes) Flags, 8 flags per byte + for (int i=0; i<256; i++) state.flags[i] = ((savedGameData[(i >> 3) + 296] & 0xFF) & (0x80 >> (i & 0x07))) > 0; + + // [297] 328 - 331(4 bytes) Clock ticks since game started. 1 clock tick == 50ms. + state.totalTicks = ((savedGameData[328] & 0xFF) + ((savedGameData[329] & 0xFF) << 8) + ((savedGameData[330] & 0xFF) << 16) + ((savedGameData[331] & 0xFF) << 24)) * 3; + + // [301] 332 - 333(2 bytes) Horizon + state.horizon = ((savedGameData[332] & 0xFF) + ((savedGameData[333] & 0xFF) << 8)); + + // [303] 334 - 335(2 bytes) Key Dir + // TODO: Not entirely sure what this is for. + int keyDir = ((savedGameData[334] & 0xFF) + ((savedGameData[335] & 0xFF) << 8)); + + // Currently active block. + // [305] 336 - 337(2 bytes) Upper left X position for active block. + state.blockUpperLeftX = (short)((savedGameData[336] & 0xFF) + ((savedGameData[337] & 0xFF) << 8)); + // [307] 338 - 339(2 bytes) Upper Left Y position for active block. + state.blockUpperLeftY = (short)((savedGameData[338] & 0xFF) + ((savedGameData[339] & 0xFF) << 8)); + // [309] 340 - 341(2 bytes) Lower Right X position for active block. + state.blockLowerRightX = (short)((savedGameData[340] & 0xFF) + ((savedGameData[341] & 0xFF) << 8)); + // [311] 342 - 343(2 bytes) Lower Right Y position for active block. + state.blockLowerRightY = (short)((savedGameData[342] & 0xFF) + ((savedGameData[343] & 0xFF) << 8)); + + // [313] 344 - 345(2 bytes) Player control (1) / Program control (0) + state.userControl = ((savedGameData[344] & 0xFF) + ((savedGameData[345] & 0xFF) << 8)) == 1; + // [315] 346 - 347(2 bytes) Current PICTURE number + state.currentPicture = null; // Will be set via load.pic script entry later on. + // [317] 348 - 349(2 bytes) Blocking flag (1 = true, 0 = false) + state.blocking = ((savedGameData[348] & 0xFF) + ((savedGameData[349] & 0xFF) << 8)) == 1; + + // [319] 350 - 351(2 bytes) Max drawn. Always set to 15. Maximum number of animated objects that can be drawn at a time. Set by old max.drawn command in AGI v2.001. + state.maxDrawn = ((savedGameData[350] & 0xFF) + ((savedGameData[351] & 0xFF) << 8)); + // [321] 352 - 353(2 bytes) Script size. Set by script.size. Max number of script event items. Default is 50. + state.scriptBuffer.setScriptSize((savedGameData[352] & 0xFF) + ((savedGameData[353] & 0xFF) << 8)); + // [323] 354 - 355(2 bytes) Current number of script event entries. + int scriptEntryCount = ((savedGameData[354] & 0xFF) + ((savedGameData[355] & 0xFF) << 8)); + + // [325] 356 - 555(200 or 160 bytes) ? Key to controller map (4 bytes each) + int keyMapSize = getNumberOfControllers(state.version); + for (int i = 0; i < keyMapSize; i++) { + int keyMapOffset = i << 2; + int keyCode = ((savedGameData[356 + keyMapOffset] & 0xFF) + ((savedGameData[357 + keyMapOffset] & 0xFF) << 8)); + int controllerNum = ((savedGameData[358 + keyMapOffset] & 0xFF) + ((savedGameData[359 + keyMapOffset] & 0xFF) << 8)); + if (!((keyCode == 0) && (controllerNum == 0)) && userInput.keyCodeMap.containsKey(keyCode)) { + int interKeyCode = userInput.keyCodeMap.get(keyCode); + if (state.keyToControllerMap.containsKey(interKeyCode)) { + state.keyToControllerMap.remove(interKeyCode); + } + state.keyToControllerMap.put(userInput.keyCodeMap.get(keyCode), controllerNum); + } + } + + int postKeyMapOffset = 356 + (keyMapSize << 2); + + // [525] 556 - 1515(480 or 960 bytes) 12 or 24 strings, each 40 bytes long + int numOfStrings = getNumberOfStrings(state.version); + for (int i = 0; i < numOfStrings; i++) { + int stringOffset = postKeyMapOffset + (i * Defines.STRLENGTH); + textEnd = stringOffset; + while (((savedGameData[textEnd] & 0xFF) != 0) && ((textEnd - stringOffset) < Defines.STRLENGTH)) textEnd++; + state.strings[i] = new String(savedGameData, stringOffset, textEnd - stringOffset, Charset.forName("Cp437")); + } + + int postStringsOffset = postKeyMapOffset + (numOfStrings * Defines.STRLENGTH); + + // [1485] 1516(2 bytes) Foreground colour + state.foregroundColour = ((savedGameData[postStringsOffset + 0] & 0xFF) + ((savedGameData[postStringsOffset + 1] & 0xFF) << 8)); + + // [1487] 1518(2 bytes) Background colour + int backgroundColour = ((savedGameData[postStringsOffset + 2] & 0xFF) + ((savedGameData[postStringsOffset + 3] & 0xFF) << 8)); + // TODO: Interpreter doesn't yet properly handle AGI background colour. + + // [1489] 1520(2 bytes) Text Attribute value (combined foreground/background value) + int textAttribute = ((savedGameData[postStringsOffset + 4] & 0xFF) + ((savedGameData[postStringsOffset + 5] & 0xFF) << 8)); + + // [1491] 1522(2 bytes) Accept input = 1, Prevent input = 0 + state.acceptInput = ((savedGameData[postStringsOffset + 6] & 0xFF) + ((savedGameData[postStringsOffset + 7] & 0xFF) << 8)) == 1; + + // [1493] 1524(2 bytes) User input row on the screen + state.inputLineRow = ((savedGameData[postStringsOffset + 8] & 0xFF) + ((savedGameData[postStringsOffset + 9] & 0xFF) << 8)); + + // [1495] 1526(2 bytes) Cursor character + state.cursorCharacter = (char)((savedGameData[postStringsOffset + 10] & 0xFF) + ((savedGameData[postStringsOffset + 11] & 0xFF) << 8)); + + // [1497] 1528(2 bytes) Show status line = 1, Don't show status line = 0 + state.showStatusLine = ((savedGameData[postStringsOffset + 12] & 0xFF) + ((savedGameData[postStringsOffset + 13] & 0xFF) << 8)) == 1; + + // [1499] 1530(2 bytes) Status line row on the screen + state.statusLineRow = ((savedGameData[postStringsOffset + 14] & 0xFF) + ((savedGameData[postStringsOffset + 15] & 0xFF) << 8)); + + // [1501] 1532(2 bytes) Picture top row on the screen + state.pictureRow = ((savedGameData[postStringsOffset + 16] & 0xFF) + ((savedGameData[postStringsOffset + 17] & 0xFF) << 8)); + + // [1503] 1534(2 bytes) Picture bottom row on the screen + // Note: Not needed by this intepreter. + int picBottom = ((savedGameData[postStringsOffset + 18] & 0xFF) + ((savedGameData[postStringsOffset + 19] & 0xFF) << 8)); + + if ((postStringsOffset + 20) < aniObjsOffset) { + // [1505] 1536(2 bytes) Stores a pushed position within the script event list + // Note: Depends on interpreter version. 2.4xx and below didn't have push.script/pop.script, so they didn't have this saved game field. + state.scriptBuffer.savedScript = ((savedGameData[postStringsOffset + 20] & 0xFF) + ((savedGameData[postStringsOffset + 21] & 0xFF) << 8)); + } + + // SECOND PIECE: ANIMATED OBJECT STATE + // 17 aniobjs = 0x02DB length, 18 aniobjs = 0x0306, 20 aniobjs = 0x035C, 21 aniobjs = 0x0387, 91 = 0x0F49] 2B, 2B, 2B, 2B, 2B + // 1538 - 1539(2 bytes) Length of piece (ANIOBJ should divide evenly in to this length) + int aniObjectsLength = ((savedGameData[aniObjsOffset + 0] & 0xFF) + ((savedGameData[aniObjsOffset + 1] & 0xFF) << 8)); + // Each ANIOBJ entry is 0x2B in length, i.e. 43 bytes. + // 17 aniobjs = 0x02DB length, 18 aniobjs = 0x0306, 20 aniobjs = 0x035C, 21 aniobjs = 0x0387, 91 = 0x0F49] 2B, 2B, 2B, 2B, 2B + int numOfAniObjs = (aniObjectsLength / 0x2B); + + for (int i = 0; i < numOfAniObjs; i++) { + int aniObjOffset = aniObjsOffset + 2 + (i * 0x2B); + AnimatedObject aniObj = state.animatedObjects[i]; + aniObj.reset(); + + // Each ANIOBJ entry is 0x2B in length, i.e. 43 bytes. + // Example: KQ1 - ego - starting position in room 1 + // 01 01 00 6e 00 64 00 00 17 6b 00 04 24 6b 00 06 + // 31 6b 31 6b 2f 9c 6e 00 64 00 06 00 20 00 01 01 + // 01 00 00 00 09 53 40 00 00 00 00 + + //UBYTE movefreq; /* number of animation cycles between motion */ e.g. 01 + aniObj.stepTime = (savedGameData[aniObjOffset + 0] & 0xFF); + //UBYTE moveclk; /* number of cycles between moves of object */ e.g. 01 + aniObj.stepTimeCount = (savedGameData[aniObjOffset + 1] & 0xFF); + //UBYTE num; /* object number */ e.g. 00 + aniObj.objectNumber = savedGameData[aniObjOffset + 2]; + //COORD x; /* current x coordinate */ e.g. 6e 00 (0x006e = ) + aniObj.x = (short)((savedGameData[aniObjOffset + 3] & 0xFF) + ((savedGameData[aniObjOffset + 4] & 0xFF) << 8)); + //COORD y; /* current y coordinate */ e.g. 64 00 (0x0064 = ) + aniObj.y = (short)((savedGameData[aniObjOffset + 5] & 0xFF) + ((savedGameData[aniObjOffset + 6] & 0xFF) << 8)); + //UBYTE view; /* current view number */ e.g. 00 + aniObj.currentView = (savedGameData[aniObjOffset + 7] & 0xFF); + //VIEW* viewptr; /* pointer to current view */ e.g. 17 6b (0x6b17 = ) IGNORE. + //UBYTE loop; /* current loop in view */ e.g. 00 + aniObj.currentLoop = (savedGameData[aniObjOffset + 10] & 0xFF); + //UBYTE loopcnt; /* number of loops in view */ e.g. 04 IGNORE + //LOOP* loopptr; /* pointer to current loop */ e.g. 24 6b (0x6b24 = ) IGNORE + //UBYTE cel; /* current cell in loop */ e.g. 00 + aniObj.currentCel = (savedGameData[aniObjOffset + 14] & 0xFF); + //UBYTE celcnt; /* number of cells in current loop */ e.g. 06 IGNORE + //CEL* celptr; /* pointer to current cell */ e.g. 31 6b (0x6b31 = ) IGNORE + //CEL* prevcel; /* pointer to previous cell */ e.g. 31 6b (0x6b31 = ) + if (aniObj.view() != null) aniObj.previousCel = aniObj.cel(); + //STRPTR save; /* pointer to background save area */ e.g. 2f 9c (0x9c2f = ) IGNORE + //COORD prevx; /* previous x coordinate */ e.g. 6e 00 (0x006e = ) + aniObj.prevX = (short)((savedGameData[aniObjOffset + 22] & 0xFF) + ((savedGameData[aniObjOffset + 23] & 0xFF) << 8)); + //COORD prevy; /* previous y coordinate */ e.g. 64 00 (0x0064 = ) + aniObj.prevY = (short)((savedGameData[aniObjOffset + 24] & 0xFF) + ((savedGameData[aniObjOffset + 25] & 0xFF) << 8)); + //COORD xsize; /* x dimension of current cell */ e.g. 06 00 (0x0006 = ) IGNORE + //COORD ysize; /* y dimension of current cell */ e.g. 20 00 (0x0020 = ) IGNORE + //UBYTE stepsize; /* distance object can move */ e.g. 01 + aniObj.stepSize = (savedGameData[aniObjOffset + 30] & 0xFF); + //UBYTE cyclfreq; /* time interval between cells of object */ e.g. 01 + aniObj.cycleTime = (savedGameData[aniObjOffset + 31] & 0xFF); + //UBYTE cycleclk; /* counter for determining when object cycles */ e.g. 01 + aniObj.cycleTimeCount = (savedGameData[aniObjOffset + 32] & 0xFF); + //UBYTE dir; /* object direction */ e.g. 00 + aniObj.direction = savedGameData[aniObjOffset + 33]; + //UBYTE motion; /* object motion type */ e.g. 00 + // #define WANDER 1 /* random movement */ + // #define FOLLOW 2 /* follow an object */ + // #define MOVETO 3 /* move to a given coordinate */ + aniObj.motionType = MotionType.values()[savedGameData[aniObjOffset + 34]]; + //UBYTE cycle; /* cell cycling type */ e.g. 00 + // #define NORMAL 0 /* normal repetative cycling of object */ + // #define ENDLOOP 1 /* animate to end of loop and stop */ + // #define RVRSLOOP 2 /* reverse of ENDLOOP */ + // #define REVERSE 3 /* cycle continually in reverse */ + aniObj.cycleType = CycleType.values()[savedGameData[aniObjOffset + 35]]; + //UBYTE pri; /* priority of object */ e.g. 09 + aniObj.priority = savedGameData[aniObjOffset + 36]; + //UWORD control; /* object control flag (bit mapped) */ e.g. 53 40 (0x4053 = ) + int controlBits = ((savedGameData[aniObjOffset + 37] & 0xFF) + ((savedGameData[aniObjOffset + 38] & 0xFF) << 8)); + /* object control bits */ + // DRAWN 0x0001 /* 1 -> object is drawn on screen */ + aniObj.drawn = ((controlBits & 0x0001) > 0); + // IGNRBLK 0x0002 /* 1 -> object ignores blocks */ + aniObj.ignoreBlocks = ((controlBits & 0x0002) > 0); + // FIXEDPRI 0x0004 /* 1 -> object has fixed priority */ + aniObj.fixedPriority = ((controlBits & 0x0004) > 0); + // IGNRHRZ 0x0008 /* 1 -> object ignores the horizon */ + aniObj.ignoreHorizon = ((controlBits & 0x0008) > 0); + // UPDATE 0x0010 /* 1 -> update the object */ + aniObj.update = ((controlBits & 0x0010) > 0); + // CYCLE 0x0020 /* 1 -> cycle the object */ + aniObj.cycle = ((controlBits & 0x0020) > 0); + // ANIMATED 0x0040 /* 1 -> object can move */ + aniObj.animated = ((controlBits & 0x0040) > 0); + // BLOCKED 0x0080 /* 1 -> object is blocked */ + aniObj.blocked = ((controlBits & 0x0080) > 0); + // PRICTRL1 0x0100 /* 1 -> object must be on 'water' priority */ + aniObj.stayOnWater = ((controlBits & 0x0100) > 0); + // IGNROBJ 0x0200 /* 1 -> object won't collide with objects */ + aniObj.ignoreObjects = ((controlBits & 0x0200) > 0); + // REPOS 0x0400 /* 1 -> object being reposn'd in this cycle */ + aniObj.repositioned = ((controlBits & 0x0400) > 0); + // PRICTRL2 0x0800 /* 1 -> object must not be entirely on water */ + aniObj.stayOnLand = ((controlBits & 0x0800) > 0); + // NOADVANC 0x1000 /* 1 -> don't advance object's cel in this loop */ + aniObj.noAdvance = ((controlBits & 0x1000) > 0); + // FIXEDLOOP 0x2000 /* 1 -> object's loop is fixed */ + aniObj.fixedLoop = ((controlBits & 0x2000) > 0); + // STOPPED 0x4000 /* 1 -> object did not move during last animation cycle */ + aniObj.stopped = ((controlBits & 0x4000) > 0); + //UBYTE parms[4]; /* space for various motion parameters */ e.g. 00 00 00 00 + aniObj.motionParam1 = (short)(savedGameData[aniObjOffset + 39] & 0xFF); + aniObj.motionParam2 = (short)(savedGameData[aniObjOffset + 40] & 0xFF); + aniObj.motionParam3 = (short)(savedGameData[aniObjOffset + 41] & 0xFF); + aniObj.motionParam4 = (short)(savedGameData[aniObjOffset + 42] & 0xFF); + // If motion type is follow, then force a re-initialisation of the follow path. + if (aniObj.motionType == MotionType.FOLLOW) aniObj.motionParam3 = -1; + } + + // THIRD PIECE: OBJECTS + // Almost an exact copy of the OBJECT file, but with the 3 byte header removed, and room + // numbers reflecting the current location of each object. + int objectsOffset = aniObjsOffset + 2 + aniObjectsLength; + int objectsLength = (savedGameData[objectsOffset + 0] + (savedGameData[objectsOffset + 1] << 8)); + // The NumOfAnimatedObjects, as stored in OBJECT, should be 1 less than the number of animated object slots + // (due to add.to.pic slot), otherwise this number increments by 1 on every save followed by restore. + state.objects.numOfAnimatedObjects = (numOfAniObjs - 1); + if (state.isAGIV3()) { + // AGI V3 games xor encrypt the data with Avis Durgan. + crypt(savedGameData, objectsOffset + 2, objectsOffset + objectsLength); + } + int numOfObjects = ((savedGameData[objectsOffset + 2] & 0xFF) + ((savedGameData[objectsOffset + 3] & 0xFF) << 8)) / 3; + // Set the saved room number of each Object. + for (int objectNum = 0, roomPos = objectsOffset + 4; objectNum < numOfObjects; objectNum++, roomPos += 3) { + state.objects.objects.get(objectNum).room = (savedGameData[roomPos] & 0xFF); + } + + // FOURTH PIECE: SCRIPT BUFFER EVENTS + // A transcript of events leading to the current state in the current room. + int scriptsOffset = objectsOffset + 2 + objectsLength; + int scriptsLength = ((savedGameData[scriptsOffset + 0] & 0xFF) + ((savedGameData[scriptsOffset + 1] & 0xFF) << 8)); + // Each script entry is two unsigned bytes long: + // UBYTE action; + // UBYTE who; + // + // Action byte is a code defined as follows: + // S_LOADLOG 0 + // S_LOADVIEW 1 + // S_LOADPIC 2 + // S_LOADSND 3 + // S_DRAWPIC 4 + // S_ADDPIC 5 + // S_DSCRDPIC 6 + // S_DSCRDVIEW 7 + // S_OVERLAYPIC 8 + // + // Example: + // c8 00 Length + // 00 01 load.logic 0x01 + // 01 00 load.view 0x00 + // 00 66 load.logic 0x66 + // 01 4b load.view 0x4B + // 01 57 load.view 0x57 + // 01 6e load.view 0x6e + // 02 01 load.pic 0x01 + // 04 01 draw.pic 0x01 + // 06 01 discard.pic 0x01 + // 00 65 load.logic 0x65 + // 01 6b load.view 0x6B + // 01 61 load.view 0x61 + // 01 5d load.view 0x5D + // 01 46 load.view 0x46 + // 03 0d load.sound 0x0D + // etc... + state.scriptBuffer.initScript(); + for (int i = 0; i < scriptEntryCount; i++) { + int scriptOffset = scriptsOffset + 2 + (i * 2); + int action = (savedGameData[scriptOffset + 0] & 0xFF); + ScriptBufferEventType eventType = ScriptBufferEventType.values()[action]; + int resourceNum = (savedGameData[scriptOffset + 1] & 0xFF); + byte[] data = null; + if (eventType == ScriptBufferEventType.ADD_TO_PIC) { + // The add.to.pics are stored in the saved game file across 8 bytes, i.e. 4 separate script + // entries (that is also how the original AGI interpreter stored it in memory). + // What we do though is store these in an additional data array associated with + // the script event since utilitising multiple event entries is a bit of a hack + // really. I can understand why they did it though. + data = new byte[] { + savedGameData[scriptOffset + 2], savedGameData[scriptOffset + 3], savedGameData[scriptOffset + 4], + savedGameData[scriptOffset + 5], savedGameData[scriptOffset + 6], savedGameData[scriptOffset + 7] + }; + + // Increase i to account for the fact that we've processed an additional 3 slots. + i += 3; + } + state.scriptBuffer.restoreScript(eventType, resourceNum, data); + } + + // FIFTH PIECE: SCAN OFFSETS + // Note: Not every logic can set a scan offset, as there is a max of 30. But only + // loaded logics can have this set and I'd imagine you'd run out of memory before + // loading that many logics at once. + int scanOffsetsOffset = scriptsOffset + 2 + scriptsLength; + int scanOffsetsLength = ((savedGameData[scanOffsetsOffset + 0] & 0xFF) + ((savedGameData[scanOffsetsOffset + 1] & 0xFF) << 8)); + int numOfScanOffsets = (scanOffsetsLength / 4); + // Each entry is 4 bytes long, made up of 2 16-bit words: + // COUNT num; /* logic number */ + // COUNT ofs; /* offset to scan start */ + // + // Example: + // 18 00 + // 00 00 00 00 Start of list. Seems to always be 4 zeroes. + // 00 00 00 00 Logic 0 - Offset 0 + // 01 00 00 00 Logic 1 - Offset 0 + // 66 00 00 00 Logic 102 - Offset 0 + // 65 00 00 00 Logic 101 - Offset 0 + // ff ff 00 00 End of list + // + // Quick Analysis of the above: + // * Only logics that are current loaded are in the scan offset list, i.e. they're removed when the room changes. + // * The order logics appear in this list is the order that they are loaded. + // * Logics disappear from this list when they are unloaded (on new.room). + // * The new.room command unloads all logics except for logic 0, so it never leaves this list. + for (int i = 0; i < 256; i++) state.scanStart[i] = 0; + for (int i = 1; i < numOfScanOffsets; i++) { + int scanOffsetOffset = scanOffsetsOffset + 2 + (i * 4); + int logicNumber = ((savedGameData[scanOffsetOffset + 0] & 0xFF) + ((savedGameData[scanOffsetOffset + 1] & 0xFF) << 8)); + if (logicNumber < 256) { + state.scanStart[logicNumber] = ((savedGameData[scanOffsetOffset + 2] & 0xFF) + ((savedGameData[scanOffsetOffset + 3] & 0xFF) << 8)); + } + } + + state.flags[Defines.RESTORE] = true; + + // Return true to say that we have successfully restored a saved game file. + return true; + } +} diff --git a/core/src/main/java/com/agifans/agile/ScriptBuffer.java b/core/src/main/java/com/agifans/agile/ScriptBuffer.java new file mode 100644 index 0000000..2f7e794 --- /dev/null +++ b/core/src/main/java/com/agifans/agile/ScriptBuffer.java @@ -0,0 +1,213 @@ +package com.agifans.agile; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; + +public class ScriptBuffer { + + public enum ScriptBufferEventType { + LOAD_LOGIC, + LOAD_VIEW, + LOAD_PIC, + LOAD_SOUND, + DRAW_PIC, + ADD_TO_PIC, + DISCARD_PIC, + DISCARD_VIEW, + OVERLAY_PIC + } + + public class ScriptBufferEvent { + public ScriptBufferEventType type; + public int resourceNumber; + public byte[] data; + + public ScriptBufferEvent(ScriptBufferEventType type, int resourceNumber, byte[] data) { + this.type = type; + this.resourceNumber = resourceNumber; + this.data = data; + } + } + + /** + * The GameState class holds all of the data and state for the Game currently + * being run by the interpreter. + */ + private GameState state; + + /** + * A transcript of events leading to the current state in the current room. + */ + public List events; + + /** + * Whether or not the storage of script events in the buffer is enabled or not. + */ + private boolean doScript; + + public int maxScript; + public int scriptSize; + public int scriptEntries() { + int count = 0; + for (ScriptBufferEvent e : events) + { + // in AGI, the add.to.pic script event consist of 4 entries + // (who, action, loop #, view #, X, Y, cel #, priority) + // the rest of the events are just 1 entry (who, action) + if (e.type == ScriptBufferEventType.ADD_TO_PIC) + { + count += 4; + } + else + { + count += 1; + } + } + return count; + } + public int savedScript; + + /** + * Constructor for ScriptBuffer. + * + * @param state + */ + public ScriptBuffer(GameState state) { + // Default script size is 50 according to original AGI specs. + this.scriptSize = 50; + this.events = new ArrayList(); + this.state = state; + initScript(); + } + + /** + * + */ + public void scriptOff() { + doScript = false; + } + + /** + * + */ + public void scriptOn() { + doScript = true; + } + + /** + * Initialize the script buffer. + */ + public void initScript() { + events.clear(); + } + + /** + * Add an event to the script buffer + * + * @param action + * @param who + */ + public void addScript(ScriptBufferEventType action, int who) { + addScript(action, who, null); + } + + /** + * Add an event to the script buffer + * + * @param action + * @param who + * @param data + */ + public void addScript(ScriptBufferEventType action, int who, byte[] data) { + if (state.flags[Defines.NO_SCRIPT]) return; + + if (doScript) { + if (events.size() >= this.scriptSize) { + // TODO: Error. Error(11, maxScript); + return; + } + else { + events.add(new ScriptBufferEvent(action, who, data)); + } + } + + if (events.size() > maxScript) { + maxScript = events.size(); + } + } + + /** + * + * @param scriptSize + */ + public void setScriptSize(int scriptSize) { + this.scriptSize = scriptSize; + this.events.clear(); + } + + /** + * + */ + public void pushScript() { + this.savedScript = events.size(); + } + + /** + * + */ + public void popScript() { + if (events.size() > this.savedScript) { + events = events.subList(0, this.savedScript); + } + } + + /** + * Returns the script event buffer as a raw byte array. + * + * @return + */ + public byte[] encode() { + // Each script entry is two bytes long. + ByteArrayOutputStream stream = new ByteArrayOutputStream(this.scriptSize * 2); + + for (ScriptBufferEvent e : events) { + stream.write((byte)(e.type.ordinal())); + stream.write((byte)e.resourceNumber); + if (e.data != null) { + stream.write(e.data, 0, e.data.length); + } + } + + // If we didn't write exactly the expected size, then fill the rest with 0. + while (stream.size() < (this.scriptSize * 2)) { + stream.write(0); + } + + return stream.toByteArray(); + } + + /** + * Add an event to the script buffer without checking NO_SCRIPT flag. Used primarily by restore save game function. + * + * @param action + * @param who + */ + public void restoreScript(ScriptBufferEventType action, int who) { + restoreScript(action, who, null); + } + + /** + * Add an event to the script buffer without checking NO_SCRIPT flag. Used primarily by restore save game function. + * + * @param action + * @param who + */ + public void restoreScript(ScriptBufferEventType action, int who, byte[] data) { + events.add(new ScriptBufferEvent(action, who, data)); + + if (events.size() > maxScript) { + maxScript = events.size(); + } + } +} diff --git a/core/src/main/java/com/agifans/agile/SoundPlayer.java b/core/src/main/java/com/agifans/agile/SoundPlayer.java new file mode 100644 index 0000000..0775361 --- /dev/null +++ b/core/src/main/java/com/agifans/agile/SoundPlayer.java @@ -0,0 +1,441 @@ +package com.agifans.agile; + +import java.io.ByteArrayOutputStream; +import java.util.HashMap; +import java.util.Map; + +import com.agifans.agile.agilib.Sound; +import com.agifans.agile.agilib.Sound.Note; + +/** + * A class for playing AGI sounds. + */ +public class SoundPlayer { + + private static final int SAMPLE_RATE = 44100; + + /** + * The GameState class holds all of the data and state for the Game currently + * being run by the interpreter. + */ + private GameState state; + + /** + * A cache of the generated WAVE data for loaded sounds. + */ + public Map soundCache; + + /** + * The number of the Sound resource currently playing, or -1 if none should be playing. + */ + private int soundNumPlaying; + + /** + * The WavePlayer that will play the generated WAV file data. + */ + private WavePlayer wavePlayer; + + private static final short[] dissolveDataV2 = new short[] { + -2, -3, -2, -1, 0x00, 0x00, 0x01, 0x01, + 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, + 0x05, 0x06, 0x06, 0x06, 0x06, 0x06, 0x07, 0x07, + 0x07, 0x07, 0x08, 0x08, 0x08, 0x08, 0x09, 0x09, + 0x09, 0x09, 0x0A, 0x0A, 0x0A, 0x0A, 0x0B, 0x0B, + 0x0B, 0x0B, 0x0B, 0x0B, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0D, -100 + }; + + private static final short[] dissolveDataV3 = new short[] { + -2, -3, -2, -1, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x04, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x07, 0x07, 0x07, 0x07, 0x08, 0x08, 0x08, 0x08, + 0x09, 0x09, 0x09, 0x09, 0x0A, 0x0A, 0x0A, 0x0A, + 0x0B, 0x0B, 0x0B, 0x0B, 0x0B, 0x0B, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0D, -100 + }; + + private short[] dissolveData; + + /** + * Constructor for SoundPlayer. + * + * @param state + * @param wavePlayer The WavePlayer that will play the generated WAV file data. + */ + public SoundPlayer(GameState state, WavePlayer wavePlayer) { + this.state = state; + this.wavePlayer = wavePlayer; + this.soundCache = new HashMap(); + this.soundNumPlaying = -1; + this.dissolveData = (state.isAGIV3()? dissolveDataV3 : dissolveDataV2); + } + + /** + * Loads and generates an AGI Sound, caching it in a ready to play state. + * + * @param sound The AGI sound to load. + */ + public void loadSound(Sound sound) { + Note[] voiceCurrentNote = new Note[4]; + boolean[] voicePlaying = new boolean[] { true, true, true, true }; + int[] voiceSampleCount = new int[4]; + int[] voiceNoteNum = new int[4]; + int[] voiceDissolveCount = new int[4]; + int durationUnitCount = 0; + + // A single note duration unit is 1/60th of a second + int samplesPerDurationUnit = SAMPLE_RATE / 60; + ByteArrayOutputStream sampleStream = new ByteArrayOutputStream(); + + // Create a new PSG for each sound, to guarantee a clean state. + SN76496 psg = new SN76496(); + + // Start by converting the Notes into samples. + while (voicePlaying[0] || voicePlaying[1] || voicePlaying[2] || voicePlaying[3]) { + for (int voiceNum = 0; voiceNum < 4; voiceNum++) { + if (voicePlaying[voiceNum]) { + if (voiceSampleCount[voiceNum]-- <= 0) { + if (voiceNoteNum[voiceNum] < sound.notes.get(voiceNum).size()) { + voiceCurrentNote[voiceNum] = sound.notes.get(voiceNum).get(voiceNoteNum[voiceNum]++); + byte[] psgBytes = voiceCurrentNote[voiceNum].rawData; + psg.write(psgBytes[3] & 0xFF); + psg.write(psgBytes[2] & 0xFF); + psg.write(psgBytes[4] & 0xFF); + voiceSampleCount[voiceNum] = voiceCurrentNote[voiceNum].duration * samplesPerDurationUnit; + voiceDissolveCount[voiceNum] = 0; + } + else { + voicePlaying[voiceNum] = false; + psg.setVolByNumber(voiceNum, 0x0F); + } + } + if ((durationUnitCount == 0) && (voicePlaying[voiceNum])) { + voiceDissolveCount[voiceNum] = updateVolume(psg, voiceCurrentNote[voiceNum].origVolume, voiceNum, voiceDissolveCount[voiceNum]); + } + } + } + + // This count hits zero 60 times a second. It counts samples from 0 to 734 (i.e. (44100 / 60) - 1). + durationUnitCount = ((durationUnitCount + 1) % samplesPerDurationUnit); + + // Use the SN76496 PSG emulation to generate the sample data. + short sample = (short)(psg.render()); + sampleStream.write(sample & 0xFF); + sampleStream.write((sample >> 8) & 0xFF); + sampleStream.write(sample & 0xFF); + sampleStream.write((sample >> 8) & 0xFF); + } + + // Use the samples to create a Wave file. These can be several MB in size (e.g. 5MB, 8MB, 10MB) + byte[] waveData = createWave(sampleStream.toByteArray()); + + // Cache for use when the sound is played. This reduces overhead of generating WAV on every play. + this.soundCache.put(sound.index, waveData); + } + + /** + * Creates a WAVE file from the given sample data by pre-pending the + * standard WAV file format header to the start. + * + * @param sampleData The sample data to create the WAVE file from. + * + * @return byte array containing the WAV file data. + */ + private byte[] createWave(byte[] sampleData) { + // Create WAVE header + int headerLen = 44; + int l1 = (sampleData.length + headerLen) - 8; // Total size of file minus 8. + int l2 = sampleData.length; + byte[] wave = new byte[headerLen + sampleData.length]; + byte[] header = new byte[] { + 82, 73, 70, 70, // RIFF + (byte)(l1 & 255), (byte)((l1 >> 8) & 255), (byte)((l1 >> 16) & 255), (byte)((l1 >> 24) & 255), + 87, 65, 86, 69, // WAVE + 102, 109, 116, 32, // fmt (chunk ID) + 16, 0, 0, 0, // size (chunk size) + 1, 0, // audio format (PCM = 1, i.e. Linear quantization) + 2, 0, // number of channels + 68, (byte)172, 0, 0, // sample rate (samples per second), i.e. 44100 + 16, (byte)177, 2, 0, // byte rate (average bytes per second, == SampleRate * NumChannels * BitsPerSample/8) + 4, 0, // block align (== NumChannels * BitsPerSample/8) + 16, 0, // bits per sample (i.e 16 bits per sample) + 100, 97, 116, 97, // data (chunk ID) + (byte)(l2 & 255), (byte)((l2 >> 8) & 255), (byte)((l2 >> 16) & 255), (byte)((l2 >> 24) & 255) + }; + + System.arraycopy(header, 0, wave, 0, headerLen); + System.arraycopy(sampleData, 0, wave, headerLen, sampleData.length); + + // Return the WAVE formatted typed array + return wave; + } + + /** + * Updates the volume of the given channel, by applying the dissolve data and master volume to the + * given base volume and then sets that in the SN76496 PSG. The noise channel does not apply the + * dissolve data, so skips that bit. + * + * @param psg The SN76496 PSG to set the calculated volume in. + * @param baseVolume The base volume to apply the dissolve data and master volume to. + * @param channel The channel to update the volume for. + * @param dissolveCount The current dissolve count value for the note being played by the given channel. + * + * @return The new dissolve count value for the channel. + */ + private int updateVolume(SN76496 psg, int baseVolume, int channel, int dissolveCount) { + int volume = baseVolume; + + if (volume != 0x0F) { + int dissolveValue = (dissolveData[dissolveCount] == -100 ? dissolveData[dissolveCount - 1] : dissolveData[dissolveCount++]); + + // Add master volume and dissolve value to current channel volume. Noise channel doesn't dissolve. + if (channel < 3) volume += dissolveValue; + + volume += this.state.vars[Defines.ATTENUATION]; + + if (volume < 0) volume = 0; + if (volume > 0x0F) volume = 0x0F; + if (volume < 8) volume += 2; + + // Apply calculated volume to PSG channel. + psg.setVolByNumber(channel, volume); + } + + return dissolveCount; + } + + /** + * Plays the given AGI Sound. + * + * @param sound The AGI Sound to play. + * @param endFlag The flag to set when the sound ends. + */ + public void playSound(Sound sound, int endFlag) { + // Stop any currently playing sound. Will set the end flag for the previous sound. + stopSound(); + + // Set the starting state of the sound end flag to false. + state.flags[endFlag] = false; + + // Get WAV data from the cache. + byte[] waveData = this.soundCache.get(sound.index); + if (waveData != null) { + // Now play the Wave file. + if (this.state.flags[Defines.SOUNDON]) { + soundNumPlaying = sound.index; + wavePlayer.playWaveData(waveData, () -> { + // This is run when the WAV data finishes playing. + soundNumPlaying = -1; + state.flags[endFlag] = true; + }); + } + else { + // If sound is not on, then it ends immediately. + soundNumPlaying = -1; + state.flags[endFlag] = true; + } + } + } + + /** + * Resets the internal state of the SoundPlayer. + */ + public void reset() { + stopSound(); + soundCache.clear(); + wavePlayer.reset(); + } + + /** + * Fully shuts down the SoundPlayer. Only intended for when AGILE is closing down. + */ + public void shutdown() { + reset(); + wavePlayer.dispose(); + } + + /** + * Stops the currently playing sound. This version of the method will always wait + * for the WAV player to finish playing before returning. + */ + public void stopSound() { + stopSound(true); + } + + /** + * Stops the currently playing sound. + * + * @param wait true to wait for the WAV player to finish playing; otherwise false to not wait. + */ + public void stopSound(boolean wait) { + if (soundNumPlaying >= 0) { + // Store that we're now not playing a sound. + soundNumPlaying = -1; + + // Ask WAV player to stop playing. The wait parameter tells the WAV + // player whether or not to wait until it has finished playing. + wavePlayer.stopPlaying(wait); + } + } + + /** + * SN76496 is the audio chip used in the IBM PC JR and therefore what the original AGI sound format was designed for. + */ + public static class SN76496 { + + private static final float IBM_PCJR_CLOCK = 3579545f; + + private static float[] volumeTable = new float[] { + 8191.5f, + 6506.73973474395f, + 5168.4870873095f, + 4105.4752242578f, + 3261.09488758897f, + 2590.37974532693f, + 2057.61177037107f, + 1634.41912530676f, + 1298.26525860452f, + 1031.24875107119f, + 819.15f, + 650.673973474395f, + 516.84870873095f, + 410.54752242578f, + 326.109488758897f, + 0.0f + }; + + private int[] channelVolume = new int[] { 15, 15, 15, 15 }; + private int[] channelCounterReload = new int[4]; + private int[] channelCounter = new int[4]; + private int[] channelOutput = new int[4]; + private int lfsr; + private int latchedChannel; + private boolean updateVolume; + private float ticksPerSample; + private float ticksCount; + + public SN76496() { + ticksPerSample = IBM_PCJR_CLOCK / 16 / SAMPLE_RATE; + ticksCount = ticksPerSample; + latchedChannel = 0; + updateVolume = false; + lfsr = 0x4000; + } + + public void setVolByNumber(int channel, int volume) { + channelVolume[channel] = (int)(volume & 0x0F); + } + + public int getVolByNumber(int channel) { + return (channelVolume[channel] & 0x0F); + } + + public void write(int data) { + /* + * A tone is produced on a voice by passing the sound chip a 3-bit register address + * and then a 10-bit frequency divisor. The register address specifies which voice + * the tone will be produced on. + * + * The actual frequency produced is the 10-bit frequency divisor given by F0 to F9 + * divided into 1/32 of the system clock frequency (3.579 MHz) which turns out to be + * 111,860 Hz. Keeping all this in mind, the following is the formula for calculating + * the frequency: + * + * f = 111860 / (((Byte2 & 0x3F) << 4) + (Byte1 & 0x0F)) + */ + int counterReloadValue; + + if ((data & 0x80) != 0) { + // First Byte + // 7 6 5 4 3 2 1 0 + // 1 . . . . . . . Identifies first byte (command byte) + // . R0 R1 . . . . . Voice number (i.e. channel) + // . . . R2 . . . . 1 = Update attenuation, 0 = Frequency count + // . . . . A0 A1 A2 A3 4-bit attenuation value. + // . . . . F6 F7 F8 F9 4 of 10 - bits in frequency count. + latchedChannel = (data >> 5) & 0x03; + counterReloadValue = (int)((channelCounterReload[latchedChannel] & 0xfff0) | (data & 0x0F)); + updateVolume = ((data & 0x10) != 0) ? true : false; + } + else { + // Second Byte - Frequency count only + // 7 6 5 4 3 2 1 0 + // 0 . . . . . . . Identifies second byte (completing byte for frequency count) + // . X . . . . . . Unused, ignored. + // . . F0 F1 F2 F3 F4 F5 6 of 10 - bits in frequency count. + counterReloadValue = (int)((channelCounterReload[latchedChannel] & 0x000F) | ((data & 0x3F) << 4)); + } + + if (updateVolume) { + // Volume latched. Update attenuation for latched channel. + channelVolume[latchedChannel] = (data & 0x0F); + } + else { + // Data latched. Update counter reload register for channel. + channelCounterReload[latchedChannel] = counterReloadValue; + + // If it is for the noise control register, then set LFSR back to starting value. + if (latchedChannel == 3) lfsr = 0x4000; + } + } + + private void updateToneChannel(int channel) { + // If the tone counter reload register is 0, then skip update. + if (channelCounterReload[channel] == 0) return; + + // Note: For some reason SQ2 intro, in docking scene, is quite sensitive to how this is decremented and tested. + + // Decrement channel counter. If zero, then toggle output and reload from + // the tone counter reload register. + if (--channelCounter[channel] <= 0) { + channelCounter[channel] = channelCounterReload[channel]; + channelOutput[channel] ^= 1; + } + } + + public float render() { + while (ticksCount > 0) { + updateToneChannel(0); + updateToneChannel(1); + updateToneChannel(2); + + channelCounter[3] -= 1; + if (channelCounter[3] < 0) { + // Reload noise counter. + if ((channelCounterReload[3] & 0x03) < 3) { + channelCounter[3] = (0x20 << (channelCounterReload[3] & 3)); + } + else { + // In this mode, the counter reload value comes from tone register 2. + channelCounter[3] = channelCounterReload[2]; + } + + int feedback = ((channelCounterReload[3] & 0x04) == 0x04) ? + // White noise. Taps bit 0 and bit 1 of the LFSR as feedback, with XOR. + ((lfsr & 0x0001) ^ ((lfsr & 0x0002) >> 1)) : + // Periodic. Taps bit 0 for the feedback. + (lfsr & 0x0001); + + // LFSR is shifted every time the counter times out. SR is 15-bit. Feedback added to top bit. + lfsr = (lfsr >> 1) | (feedback << 14); + channelOutput[3] = (int)(lfsr & 1); + } + + ticksCount -= 1; + } + + ticksCount += ticksPerSample; + + return (float)((volumeTable[channelVolume[0] & 0x0F] * ((channelOutput[0] - 0.5) * 2)) + + (volumeTable[channelVolume[1] & 0x0F] * ((channelOutput[1] - 0.5) * 2)) + + (volumeTable[channelVolume[2] & 0x0F] * ((channelOutput[2] - 0.5) * 2)) + + (volumeTable[channelVolume[3] & 0x0F] * ((channelOutput[3] - 0.5) * 2))); + } + } +} diff --git a/core/src/main/java/com/agifans/agile/TextGraphics.java b/core/src/main/java/com/agifans/agile/TextGraphics.java new file mode 100644 index 0000000..7dd5978 --- /dev/null +++ b/core/src/main/java/com/agifans/agile/TextGraphics.java @@ -0,0 +1,1341 @@ +package com.agifans.agile; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +/** + * Provides methods for drawing text on to the AGI screen. + */ +public class TextGraphics { + + private static final int WINTOP = 1; + private static final int WINBOT = 20; + private static final int WINWIDTH = 30; + private static final int VMARGIN = 5; + private static final int HMARGIN = 5; + private static final int CHARWIDTH = 4; /* in our coordinates */ + private static final int CHARHEIGHT = 8; + private static final int INVERSE = 0x8f; /* inverse video, i.e. black on white */ + private static final int UNASSIGNED = -1; + + /** + * Stores details about the currently displayed text window. + */ + public static class TextWindow { + + // Mandatory items required by OpenWindow. + public int position; + public int dimensions; + public int x() { return ((((position >> 8) & 0xFF) << 1)); } + public int y() { return ((position & 0xFF) - (((dimensions >> 8) & 0xFF) - 1) + 8); } + public int width() { return ((dimensions & 0xFF) << 1); } + public int height() { return ((dimensions >> 8) & 0xFF); } + public int backgroundColour; + public int borderColour; + + // Items set by OpenWindow. + public short[] backPixels; + + // Items always set by WindowNoWait. + public int top; + public int left; + public int bottom; + public int right; + public String[] textLines; + public int textColour; + + // Items optionally set by WindowNoWait. + public AnimatedObject aniObj; + + public TextWindow(int position, int dimensions, int backgroundColour, int borderColour) { + this(position, dimensions, backgroundColour, borderColour, 0, 0, 0, 0, null, 0, null); + } + + public TextWindow( + int position, int dimensions, int backgroundColour, int borderColour, int top, int left, + int bottom, int right, String[] textLines, int textColour, AnimatedObject aniObj) { + this.position = position; + this.dimensions = dimensions; + this.backgroundColour = backgroundColour; + this.borderColour = borderColour; + this.top = top; + this.left = left; + this.bottom = bottom; + this.right = right; + this.textLines = textLines; + this.textColour = textColour; + this.aniObj = aniObj; + } + } + + /** + * Stores details about the currently displayed text window. + */ + private TextWindow openWindow; + + private int winWidth = -1; + private int winULRow = -1; + private int winULCol = -1; + private int maxLength; + + private char escapeChar = '\\'; /* the escape character */ + + /** + * The GameState class holds all of the data and state for the Game currently + */ + 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, in which the text will be drawn. + */ + private short[] pixels; + + /** + * Constructor for TextGraphics. + * + * @param pixels The GameScreen pixels. This is what TextGraphics draws windows (and indirectly menus) to. + * @param state The GameState class 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. + */ + public TextGraphics(short[] pixels, GameState state, UserInput userInput) { + this.state = state; + this.userInput = userInput; + this.pixels = pixels; + this.openWindow = null; + this.clearLines(0, 24, 0); + } + + /** + * Sets the text colour attributes used when drawing text characters. + * + * @param foregroundColour + * @param backgroundColour + */ + public void setTextAttribute(int foregroundColour, int backgroundColour) { + state.foregroundColour = (foregroundColour & 0xFF); + state.backgroundColour = makeBackgroundColour(backgroundColour); + state.textAttribute = makeTextAttribute(foregroundColour, backgroundColour); + } + + /** + * Return the requested text attribute in it's internal representation. + * + * @param foregroundColour + * @param backgroundColour + * + * @return + */ + private int makeTextAttribute(int foregroundColour, int backgroundColour) { + if (!state.graphicsMode) { + // For text mode, put background in high nibble, fore in low. + return (((backgroundColour << 4) | foregroundColour) & 0xFF); + } + else { + // In graphics mode, if back is not black, approximate with inverse text (black on white). + return ((backgroundColour == 0? foregroundColour : INVERSE) & 0xFF); + } + } + + /** + * Return the internal representation for the requested background color. + * + * @param backgroundColour + * + * @return The internal representation for the requested background color. + */ + public int makeBackgroundColour(int backgroundColour) { + if (state.graphicsMode && (backgroundColour != 0)) { + // In graphics if back is not black, approximate with inverse text (black on white). + return (0xff); /* mask off inverse */ + } + else { + // This is rather strange, but for clear.lines and clear.text.rect, in text mode the + // background colour is black regardless of the colour parameter value. + return (0); + } + } + + /** + * Clears the lines from the specified top line to the specified bottom line using the + * + * @param top + * @param bottom + * @param backgroundColour + */ + public void clearLines(int top, int bottom, int backgroundColour) { + int startPos = top * 8 * 320; + int endPos = ((bottom + 1) * 8 * 320) - 1; + short colour = EgaPalette.colours[backgroundColour & 0x0F]; + + for (int i=startPos; i <= endPos; i++) { + this.pixels[i] = colour; + } + } + + /** + * Clears a text rectangle as specified by the top, left, bottom and right values. The top and + * + * @param top + * @param left + * @param bottom + * @param right + * @param backgroundColour + */ + public void clearRect(int top, int left, int bottom, int right, int backgroundColour) { + short backgroundRGB565 = EgaPalette.colours[backgroundColour & 0x0F]; + int height = ((bottom - top) + 1) * 8; + int width = ((right - left) + 1) * 8; + int startY = (top * 8); + int startX = (left * 8); + int startScreenPos = ((startY * 320) + startX); + int screenYAdd = 320 - width; + + for (int y = 0, screenPos = startScreenPos; y < height; y++, screenPos += screenYAdd) { + for (int x = 0; x < width; x++, screenPos++) { + this.pixels[screenPos] = backgroundRGB565; + } + } + } + + public void textScreen() { + textScreen(UNASSIGNED); + } + + public void textScreen(int backgroundColour) { + state.graphicsMode = false; + + if (backgroundColour == UNASSIGNED) { + setTextAttribute((byte)state.foregroundColour, (byte)state.backgroundColour); + // Note that the original AGI interpreter uses the background from the TextAttribute + // value rather than the current BackgroundColour. + backgroundColour = ((state.textAttribute >> 4) & 0x0F); + } + + // Clear the whole screen to the background colour. + clearLines(0, 24, backgroundColour); + } + + public void graphicsScreen() { + state.graphicsMode = true; + + setTextAttribute((byte)state.foregroundColour, (byte)state.backgroundColour); + + // Clear whole screen to black. + clearLines(0, 24, 0); + + // Copy VisualPixels to game screen. + System.arraycopy(state.visualPixels, 0, this.pixels, (8 * state.pictureRow) * 320, state.visualPixels.length); + + updateStatusLine(); + updateInputLine(); + } + + /** + * Draws a character to the AGI screen. Depending on the usage, this may either be done + * to the VisualPixels or directly to the GameScreen pixels. Windows and menu text is + * drawn directly to the GameScreen pixels, but Display action commands are drawn to the + * VisualPixels array. + * + * @param pixels The pixel array to draw the character to. + * @param charNum The ASCII code number of the character to draw. + * @param x The X position of the character. + * @param y The Y position of the character. + * @param foregroundColour The foreground colour of the character. + * @param backgroundColour The background colour of the character. + */ + public void drawChar(short[] pixels, byte charNum, int x, int y, int foregroundColour, int backgroundColour) { + drawChar(pixels, charNum, x, y, foregroundColour, backgroundColour, false); + } + + /** + * Draws a character to the AGI screen. Depending on the usage, this may either be done + * to the VisualPixels or directly to the GameScreen pixels. Windows and menu text is + * drawn directly to the GameScreen pixels, but Display action commands are drawn to the + * VisualPixels array. + * + * @param pixels The pixel array to draw the character to. + * @param charNum The ASCII code number of the character to draw. + * @param x The X position of the character. + * @param y The Y position of the character. + * @param foregroundColour The foreground colour of the character. + * @param backgroundColour The background colour of the character. + * @param halfTone If true then character are only half drawn. + */ + public void drawChar(short[] pixels, byte charNum, int x, int y, int foregroundColour, int backgroundColour, boolean halfTone) { + for (int byteNum = 0; byteNum < 8; byteNum++) { + int fontByte = (IBM_BIOS_FONT[(charNum << 3) + byteNum] & 0xFF); + boolean halfToneState = ((byteNum % 2) == 0); + + for (int bytePos = 7; bytePos >= 0; bytePos--) { + if (!halfTone || halfToneState) { + if ((fontByte & (1 << bytePos)) != 0) { + pixels[((y + byteNum) * 320) + x + (7 - bytePos)] = EgaPalette.colours[foregroundColour]; + } + else { + pixels[((y + byteNum) * 320) + x + (7 - bytePos)] = EgaPalette.colours[backgroundColour]; + } + } + + halfToneState = !halfToneState; + } + } + } + + /** + * Draws the given string to the AGI screen, at the given x/y position, in the given colours. + * + * @param pixels The pixel array to draw the character to. + * @param text The text to draw to the screen. + * @param x The X position of the text. + * @param y The Y position of the text. + */ + public void drawString(short[] pixels, String text, int x, int y) { + drawString(pixels, text, x, y, UNASSIGNED, UNASSIGNED, false); + } + + /** + * Draws the given string to the AGI screen, at the given x/y position, in the given colours. + * + * @param pixels The pixel array to draw the character to. + * @param text The text to draw to the screen. + * @param x The X position of the text. + * @param y The Y position of the text. + * @param foregroundColour Optional foreground colour. Defaults to currently active foreground colour if not specified. + * @param backgroundColour Optional background colour. Defaults to currently active background colour if not specified. + */ + public void drawString(short[]pixels, String text, int x, int y, int foregroundColour, int backgroundColour) { + drawString(pixels, text, x, y, foregroundColour, backgroundColour, false); + } + + /** + * Draws the given string to the AGI screen, at the given x/y position, in the given colours. + * + * @param pixels The pixel array to draw the character to. + * @param text The text to draw to the screen. + * @param x The X position of the text. + * @param y The Y position of the text. + * @param foregroundColour Optional foreground colour. Defaults to currently active foreground colour if not specified. + * @param backgroundColour Optional background colour. Defaults to currently active background colour if not specified. + * @param halfTone If true then character are only half drawn. + */ + public void drawString(short[]pixels, String text, int x, int y, int foregroundColour, int backgroundColour, boolean halfTone) { + // This method is used as both a general text drawing method, for things like the menu + // and inventory, and also for the print and display commands. The print and display + // commands will operate using the currently set text attribute, foreground and background + // values. The more general use cases would pass in the exact colours that they want to + // use, no questions asked. + + // Foreground colour. + if (foregroundColour == UNASSIGNED) { + if (state.graphicsMode) { + // In graphics mode, if background is not black, foreground is black; otherwise as is. + foregroundColour = (state.backgroundColour == 0? state.foregroundColour : 0); + } + else { + // In text mode, we use the text attribute foreground colour as is. + foregroundColour = (state.textAttribute & 0x0F); + } + } + + // Background colour. + if (backgroundColour == UNASSIGNED) { + if (state.graphicsMode) { + // In graphics mode, background can only be black or white. + backgroundColour = (state.backgroundColour == 0 ? 0 : 15); + } + else { + // In text mode, we use the text attribute background colour as is. + backgroundColour = ((state.textAttribute >> 4) & 0x0F); + } + } + + try { + byte[] textBytes = text.getBytes("Cp437"); + + for (int charPos = 0; charPos < textBytes.length; charPos++) { + drawChar(pixels, textBytes[charPos], x + (charPos * 8), y, foregroundColour, backgroundColour, halfTone); + } + } catch (UnsupportedEncodingException e) { + // Shouldn't happen. + System.out.println("Unsupport encoding Cp437. AGI games need this to render text."); + } + } + + /** + * Display the given string at the given row and col. This method renders only the text and + * does not pop up a message window. + * + * @param str + * @param row + * @param col + */ + public void display(String str, int row, int col) { + // Expand references and split on new lines. + String[] lines = buildMessageLines(str, Defines.TEXTCOLS + 1, col); + + for (int i = 0; i < lines.length; i++) { + drawString(this.pixels, lines[i], col * 8, (row + i) * 8); + + // For subsequent lines, we start at column 0 and ignore what was passed in. + col = 0; + } + } + + /** + * Print the given string in an AGI message window. + * + * @param str The text to include in the message window. + */ + public void print(String str) { + windowPrint(str); + } + + /** + * Print the given string in an AGI message window, the window positioned at the given row + * and col, and of the given width. + * + * @param str + * @param row + * @param col + * @param width + */ + public void printAt(String str, int row, int col, int width) { + winULRow = row; + winULCol = col; + + if ((winWidth = width) == 0) { + winWidth = WINWIDTH; + } + + windowPrint(str); + + winWidth = winULRow = winULCol = -1; + } + + /** + * Updates the status line with the score and sound status. + */ + public void updateStatusLine() { + if (state.showStatusLine) { + clearLines(state.statusLineRow, state.statusLineRow, 15); + + StringBuilder scoreStatus = new StringBuilder(); + scoreStatus.append(" Score:"); + scoreStatus.append(state.vars[Defines.SCORE]); + scoreStatus.append(" of "); + scoreStatus.append(state.vars[Defines.MAXSCORE]); + drawString(this.pixels, String.format("%-30s", scoreStatus.toString()), 0, state.statusLineRow * 8, 0, 15); + + StringBuilder soundStatus = new StringBuilder(); + soundStatus.append("Sound:"); + soundStatus.append(state.flags[Defines.SOUNDON] ? "on" : "off"); + drawString(this.pixels, String.format("%-10s", soundStatus.toString()), 30 * 8, state.statusLineRow * 8, 0, 15); + } + } + + public void updateInputLine() { + updateInputLine(true); + } + + /** + * Updates the user input line based on current state. + * + * @param clearWhenNotEnabled + */ + public void updateInputLine(boolean clearWhenNotEnabled) { + if (state.graphicsMode) { + if (state.acceptInput) { + // Input line has the prompt string at the start, then the user input. + StringBuilder inputLine = new StringBuilder(); + if (state.strings[0] != null) { + inputLine.append(expandReferences(state.strings[0])); + } + inputLine.append(state.currentInput.toString()); + if (state.cursorCharacter > 0) { + // Cursor character is optional. There isn't one at the start of the game. + inputLine.append(state.cursorCharacter); + } + + drawString(this.pixels, String.format("%-" + Defines.MAXINPUT +"s", inputLine.toString()), 0, state.inputLineRow * 8); + } + else if (clearWhenNotEnabled) { + // If not accepting input, clear the prompt and text input. + clearLines(state.inputLineRow, state.inputLineRow, 0); + } + } + } + + /** + * Prints the message as a prompt at column 0 of the current input row, then allows the user to + * enter some text. The entered text will have everything other than digits stripped from it, then + * it is converted into a number and returned. + * + * @param message The message to display to the player instructing them what to enter. + * + * @returns The entered number as a byte, or 0 if it can't be converted. + */ + public byte getNum(String message) { + clearLines(state.inputLineRow, state.inputLineRow, 0); + + // Show the prompt message to the user at the specified position. + display(message, state.inputLineRow, 0); + + // Get a line of text from the user. + String line = getLine(4, (byte)state.inputLineRow, (byte)message.length()); + + // Strip out everything that isn't a digit. A little more robust than the original AGI interpreter. + String digitsInLine = line.replaceAll("[^\\d]", ""); + + updateInputLine(); + + return (byte)(digitsInLine.length() > 0? Integer.parseInt(digitsInLine) : 0); + } + + /** + * Prints the message as a prompt at the given screen position, then allows the user to enter + * the string for string number. + * + * @param strNum The number of the user string to put the entered value in to. + * @param message A message to display to the player instructing them what to enter. + * @param row The row to display the message at. + * @param col The column to display the message at. + * @param length The maximum length of the string to get. + */ + public void getString(int strNum, String message, int row, int col, int length) { + // The string cannot be longer than the maximum length for a user string. + length = (byte)(length > Defines.STRLENGTH? Defines.STRLENGTH : length); + + // Show the prompt message to the user at the specified position. + display(message, row, col); + + // Position the input area immediately after the message. + col += (byte)message.length(); + + // Get a line of text from the user. + String line = getLine(length, row, col); + + // If it is not null, i.e. the user didn't hit ESC, then store in user string. + if (line != null) state.strings[strNum] = line; + } + + /** + * Gets a line of user input, echoing the prompt char and entered text at the specified position. + * + * @param length The maximum length of the line of text to get. + * @param row The row on the screen to position the text entry field. + * @param col The column on the screen to position the start of the text entry field. + */ + public String getLine(int length, int row, int col) { + return getLine(length, row, col, "", -1, -1); + } + + /** + * Gets a line of user input, echoing the prompt char and entered text at the specified position. + * + * @param length The maximum length of the line of text to get. + * @param row The row on the screen to position the text entry field. + * @param col The column on the screen to position the start of the text entry field. + * @param str The value to initialise the text entry field with; defaults to empty. + * @param foregroundColour The foreground colour of the text in the text entry field. + * @param backgroundColour The background colour of the text in the text entry field. + * + * @return The entered string if ENTER was hit, otherwise null if ESC was hit. + */ + public String getLine(int length, int row, int col, String str, int foregroundColour, int backgroundColour) { + StringBuilder line = new StringBuilder(str); + + // The string cannot be longer than the maximum length for a GetLine call. + length = (byte)(length > Defines.GLSIZE ? Defines.GLSIZE : length); + + // Process entered keys until either ENTER or ESC is pressed. + while (true) { + // Show the currently entered text. + drawString(this.pixels, (line.toString() + state.cursorCharacter), col * 8, row * 8, foregroundColour, backgroundColour); + + int key = userInput.waitForKey(false); + + if ((key & 0xF0000) == UserInput.ASCII) { + char character = (char)(key & 0xFF); + + if (character == Character.ESC) { + // Exits without returning any entered text. + return null; + } + else if (character == Character.ENTER) { + // If ENTER is hit, we break out of the loop and return the entered line of text. + // Render Line without the cursor by replacing the cursor with empty string + drawString(this.pixels, line.toString() + " ", col * 8, row * 8, foregroundColour, backgroundColour); + break; + } + else if (character == Character.BACKSPACE) { + // Removes one from the end of the currently entered input. + if (line.length() > 0) line.delete(line.length() - 1, line.length()); + + // Render Line with a space overwriting the previous position of the cursor. + drawString(this.pixels, (line.toString() + state.cursorCharacter + " "), col * 8, row * 8, foregroundColour, backgroundColour); + } + else { // Standard char from a keypress event. + // If we haven't reached the max length, add the char to the line of text. + if (line.length() < length) line.append((char)(key & 0xff)); + } + } + } + + return line.toString(); + } + + /** + * Print the string 'str' in a window on the screen and wait for ACCEPT or ABORT + * before disposing of it.Return TRUE for ACCEPT, FALSE for ABORT. + * + * @param str + * + * @return true for ACCEPT, false for ABORT. + */ + public boolean windowPrint(String str) { + return windowPrint(str, null); + } + + /** + * Print the string 'str' in a window on the screen and wait for ACCEPT or ABORT + * before disposing of it.Return TRUE for ACCEPT, FALSE for ABORT. + * + * @param str + * @param aniObj Optional AnimatedObject to draw when the window is opened. + * + * @return true for ACCEPT, false for ABORT. + */ + public boolean windowPrint(String str, AnimatedObject aniObj) { + boolean retVal; + long timeOut; + + // Display the window. + windowNoWait(str, 0, 0, false, aniObj); + + // If we're to leave the window up, just return. + if (state.flags[Defines.LEAVE_WIN] == true) { + state.flags[Defines.LEAVE_WIN] = false; + return true; + } + + // Get the response. + if (state.vars[Defines.PRINT_TIMEOUT] == 0) { + retVal = (userInput.waitAcceptAbort() == UserInput.ACCEPT); + } + else { + // The timeout value is given in half seconds and the TotalTicks in 1/60ths of a second. + timeOut = state.totalTicks + state.vars[Defines.PRINT_TIMEOUT] * 30; + + while ((state.totalTicks < timeOut) && (userInput.checkAcceptAbort() == -1)) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // Ignore. + } + } + + retVal = true; + + state.vars[Defines.PRINT_TIMEOUT] = 0; + } + + // Close the window. + closeWindow(); + + return retVal; + } + + /** + * + * + * @param str + * @param height + * @param width + * @param fixedSize + * + * @return TextWindow + */ + public TextWindow windowNoWait(String str, int height, int width, boolean fixedSize) { + return windowNoWait(str, height, width, fixedSize, null); + } + + /** + * + * + * @param str + * @param height + * @param width + * @param fixedSize + * @param aniObj Optional AnimatedObject to draw when the window is opened. + * + * @return TextWindow + */ + public TextWindow windowNoWait(String str, int height, int width, boolean fixedSize, AnimatedObject aniObj) { + String[] lines; + int numLines = 0; + + if (openWindow != null) { + closeWindow(); + } + + if ((winWidth == -1) && (width == 0)) { + width = WINWIDTH; + } + else if (winWidth != -1) { + width = winWidth; + } + + while (true) { + // First make a formatting pass through the message, getting maximum line length and number of lines. + lines = buildMessageLines(str, width); + numLines = lines.length; + + if (fixedSize) { + maxLength = width; + if (height != 0) { + numLines = height; + } + } + + if (numLines > (WINBOT - WINTOP)) { + str = String.format("Message too verbose:\n\n\"%s...\"\n\nPress ESC to continue.", str.substring(0, 20)); + } + else { + break; + } + } + + int top = (winULRow == -1 ? WINTOP + (WINBOT - WINTOP - numLines) / 2 : winULRow) + state.pictureRow; + int bottom = top + numLines - 1; + int left = (winULCol == -1 ? (Defines.TEXTCOLS - maxLength) / 2 : winULCol); + int right = left + maxLength; + + // Compute window size and position and put them into the appropriate bytes of the words. + int windowDim = ((numLines * CHARHEIGHT + 2 * VMARGIN) << 8) | (maxLength * CHARWIDTH + 2 * HMARGIN); + int windowPos = ((left * CHARWIDTH - HMARGIN) << 8) | (bottom * CHARHEIGHT + VMARGIN - 1); + + // Open the window, white with a red border and black text. + return openWindow(new TextWindow(windowPos, windowDim, 15, 4, top, left, bottom, right, lines, 0, aniObj)); + } + + /** + * Builds the array of message lines to be included in a message window. The str parameter + * provides the message text, which may contain special % command references that need + * expanding first. After that substitution, the resulting message text is split up on to + * lines that are no longer than the given width, words wrapping down a line if required. + * + * @param str The message text to expand references and split in to lines. + * @param width The maximum width that a message line can be. + * + * @return A String array containing the message lines. + */ + private String[] buildMessageLines(String str, int width) { + return buildMessageLines(str, width, 0); + } + + /** + * Builds the array of message lines to be included in a message window. The str parameter + * provides the message text, which may contain special % command references that need + * expanding first. After that substitution, the resulting message text is split up on to + * lines that are no longer than the given width, words wrapping down a line if required. + * + * @param str The message text to expand references and split in to lines. + * @param width The maximum width that a message line can be. + * @param startColumn Optional starting column value. + * + * @return A String array containing the message lines. + */ + private String[] buildMessageLines(String str, int width, int startColumn) { + List lines = new ArrayList(); + + maxLength = 0; + + if (str != null) { + // Recursively expand/substitute references to other strings. + String processedMessage = expandReferences(str); + + // Now that we have the processed message text, split it in to lines. + StringBuilder currentLine = new StringBuilder(); + + // Pad the first line with however many spaces required to begin at starting column. + if (startColumn > 0) currentLine.append(String.format("%-" + startColumn + "s", "")); + + for (int i = 0; i < processedMessage.length(); i++) { + int addLines = (i == (processedMessage.length() - 1)) ? 1 : 0; + + if (processedMessage.charAt(i) == 0x0A) { + addLines++; + } + else { + // Add the character to the current line. + currentLine.append(processedMessage.charAt(i)); + + // If the current line has reached the width, then word wrap. + if (currentLine.length() >= width) { + i = wrapWord(currentLine, i); + + addLines = 1; + } + } + + while (addLines-- > 0) { + if ((startColumn > 0) && (lines.size() == 0)) { + // Remove the extra padding that we added at the start of first line. + currentLine.delete(0, startColumn); + startColumn = 0; + } + + lines.add(currentLine.toString()); + + if (currentLine.length() > maxLength) { + maxLength = currentLine.length(); + } + + currentLine.setLength(0); + } + } + } + + return (String[])lines.toArray(new String[0]); + } + + /** + * Winds back the given StringBuilder to the last word separate (i.e. space) and adjusts the + * pos index value so that the word that overlapped the max line length is wrapped to the + * next line. + * + * @param str + * + * @return The new position. + */ + private int wrapWord(StringBuilder str, int pos) { + for (int i = str.length() - 1; i >= 0; i--) { + if (str.charAt(i) == ' ') { + pos -= (str.length() - i - 1); + str.delete(i, str.length()); + return pos; + } + } + return pos; + } + + /** + * Scans the given string from the given position for a consecutive sequence of digits. When + * the end is reached, the string of digits is converted in to numeric form and returned. Any + * characters before the given position, and after the end of the sequence of digits, is + * ignored. + * + * @param str + * @param startPos + * + * @return An array containing the number in the first slot and new position in the second. + */ + private int[] numberFromString(String str, int pos) { + int startPos = pos; + while ((pos < str.length()) && (str.charAt(pos) >= '0') && (str.charAt(pos) <= '9')) pos++; + int number = Integer.parseInt(str.substring(startPos, pos--)); + return new int[] { number, pos }; + } + + /** + * Expands the special commands that reference other types of text, such as + * object names, words, other messages, etc. + * + * Messages are strings of fewer than 255 characters which may contain + * the following special commands: + * + * \ Take the next character(except '\n' below) literally + * \n Begin a new line + * %wn Include word number n from the parsed line (1 < = n <= 255) + * %sn Include user defined string number n (0 <= n <= 255) + * %mn Include message number n from this room (0 <= n <= 255) + * %gn Include global message number n from room 0 (0 <= n <= 255) + * %vn|m Print the value of var #n. If the optional '|m' is present, print in a field of width m with leading zeros. + * %on Print the name of the object whose number is in var number n. + * + * + * @param str The string to expand the references of. + * + * @return + */ + private String expandReferences(String str) { + StringBuilder output = new StringBuilder(); + + // Iterate over each character in the message string looking for % codes. + for (int i = 0; i < str.length(); i++) { + if (str.charAt(i) == escapeChar) { + // The '\' character escapes the next character (e.g. \%) + output.append(str.charAt(++i)); + } + else if (str.charAt(i) == '%') { + int num, width; + int[] numPos; + + i++; + + switch (str.charAt(i++)) { + case 'v': + numPos = numberFromString(str, i); + num = numPos[0]; + i = numPos[1]; + if ((i < (str.length() - 1)) && (str.charAt(i + 1) == '|')) { + i += 2; + numPos = numberFromString(str, i); + width = numPos[0]; + i = numPos[1]; + output.append(String.format("%0" + width + "d", state.vars[num])); + } + else { + output.append(state.vars[num]); + } + break; + + case 'm': + numPos = numberFromString(str, i); + num = numPos[0]; + i = numPos[1]; + output.append(state.logics[state.currentLogNum].messages.get(num)); + break; + + case 'g': + numPos = numberFromString(str, i); + num = numPos[0]; + i = numPos[1]; + output.append(state.logics[0].messages.get(num)); + break; + + case 'w': + numPos = numberFromString(str, i); + num = numPos[0]; + i = numPos[1]; + if (num <= state.recognisedWords.size()) { + output.append(state.recognisedWords.get(num - 1)); + } + break; + + case 's': + numPos = numberFromString(str, i); + num = numPos[0]; + i = numPos[1]; + output.append(state.strings[num]); + break; + + case 'o': + numPos = numberFromString(str, i); + num = numPos[0]; + i = numPos[1]; + output.append(state.objects.objects.get(num).name); + break; + + default: // ignore the second character. + break; + } + } + else { + // Default is simply to append the character. + output.append(str.charAt(i)); + } + } + + // Recursive part to make sure all % formatting codes are dealt with. + if (output.toString().contains("%")) { + return expandReferences(output.toString()); + } + else { + return output.toString(); + } + } + + /** + * Opens an AGI window on the game screen. + * + * @param textWindow + * + * @return The same TextWindow with the BackPixels populated. + */ + public TextWindow openWindow(TextWindow textWindow) { + drawWindow(textWindow); + + // Remember this as the currently open window. + this.openWindow = textWindow; + + return textWindow; + } + + /** + * + */ + public void drawWindow() { + drawWindow(null); + } + + /** + * + * + * @param textWindow + */ + public void drawWindow(TextWindow textWindow) { + // Defaults to the currently open window if one was not provided by the caller. + textWindow = (textWindow == null ? openWindow : textWindow); + + if (textWindow != null) { + short backgroundRGB565 = EgaPalette.colours[textWindow.backgroundColour]; + short borderRGB565 = EgaPalette.colours[textWindow.borderColour]; + int startScreenPos = (textWindow.y() * 320) + textWindow.x(); + int screenYAdd = (320 - textWindow.width()); + + // The first time that DrawWindow is invoke for a TextWindow, we store the back pixels. + boolean storeBackPixels = (textWindow.backPixels == null); + if (storeBackPixels) textWindow.backPixels = new short[textWindow.width() * textWindow.height()]; + + // Draw a box in the background colour and store the pixels that were behind it. + int backPixelsPos = 0; + for (int y = 0, screenPos = startScreenPos; y < textWindow.height(); y++, screenPos += screenYAdd) { + for (int x = 0; x < textWindow.width(); x++, screenPos++) { + // Store the pixel currently at this position (if applicable). + if (storeBackPixels) textWindow.backPixels[backPixelsPos++] = this.pixels[screenPos]; + + // Overwrite the pixel with the window's background colour. + this.pixels[screenPos] = backgroundRGB565; + } + } + + // Draw a line just in a bit from the edge of the box in the border colour. + for (int x = 0, screenPos = (startScreenPos + 320 + 2); x < (textWindow.width() - 4); x++, screenPos++) { + this.pixels[screenPos] = borderRGB565; + } + for (int x = 0, screenPos = (startScreenPos + (320 * (textWindow.height() - 2) + 2)); x < (textWindow.width() - 4); x++, screenPos++) { + this.pixels[screenPos] = borderRGB565; + } + for (int y = 1, screenPos = (startScreenPos + 640 + 2); y < (textWindow.height() - 2); y++, screenPos += 320) { + this.pixels[screenPos] = borderRGB565; + this.pixels[screenPos + 1] = borderRGB565; + this.pixels[screenPos + (textWindow.width() - 6)] = borderRGB565; + this.pixels[screenPos + (textWindow.width() - 5)] = borderRGB565; + } + + // Draw the text lines (if applicable). + if (textWindow.textLines != null) { + // Draw the text black on white. + for (int i = 0; i < textWindow.textLines.length; i++) { + drawString(this.pixels, textWindow.textLines[i], (textWindow.left << 3), ((textWindow.top + i) << 3), textWindow.textColour, textWindow.backgroundColour); + } + } + + // Draw the embedded AnimatedObject (if applicable). Supports inventory item description windows. + if (textWindow.aniObj != null) { + textWindow.aniObj.draw(); + textWindow.aniObj.show(pixels); + } + } + } + + /** + * Checks if there is a text window currently open. + * + * @return true if there is a window open; otherwise false. + */ + public boolean isWindowOpen() { + return (this.openWindow != null); + } + + /** + * Closes the current message window. + */ + public void closeWindow() { + closeWindow(true); + } + + /** + * Closes the current message window. + * + * @param restoreBackPixels Whether to restore back pixels or not (defaults to true) + */ + public void closeWindow(boolean restoreBackPixels) { + if (this.openWindow != null) { + if (restoreBackPixels) { + int startScreenPos = (openWindow.y() * 320) + openWindow.x(); + int screenYAdd = (320 - openWindow.width()); + + // Copy each of the stored background pixels back in to their original places. + int backPixelsPos = 0; + for (int y = 0, screenPos = startScreenPos; y < openWindow.height(); y++, screenPos += screenYAdd) { + for (int x = 0; x < openWindow.width(); x++, screenPos++) { + this.pixels[screenPos] = openWindow.backPixels[backPixelsPos++]; + } + } + } + + // Clear the currently open window variable. + this.openWindow = null; + } + } + + /** + * The raw bitmap data for the original IBM PC/PCjr BIOS 8x8 font. + */ + private static final int[] IBM_BIOS_FONT = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7E, 0x81, 0xA5, 0x81, 0xBD, 0x99, 0x81, 0x7E, + 0x7E, 0xFF, 0xDB, 0xFF, 0xC3, 0xE7, 0xFF, 0x7E, + 0x6C, 0xFE, 0xFE, 0xFE, 0x7C, 0x38, 0x10, 0x00, + 0x10, 0x38, 0x7C, 0xFE, 0x7C, 0x38, 0x10, 0x00, + 0x38, 0x7C, 0x38, 0xFE, 0xFE, 0x7C, 0x38, 0x7C, + 0x10, 0x10, 0x38, 0x7C, 0xFE, 0x7C, 0x38, 0x7C, + 0x00, 0x00, 0x18, 0x3C, 0x3C, 0x18, 0x00, 0x00, + 0xFF, 0xFF, 0xE7, 0xC3, 0xC3, 0xE7, 0xFF, 0xFF, + 0x00, 0x3C, 0x66, 0x42, 0x42, 0x66, 0x3C, 0x00, + 0xFF, 0xC3, 0x99, 0xBD, 0xBD, 0x99, 0xC3, 0xFF, + 0x0F, 0x07, 0x0F, 0x7D, 0xCC, 0xCC, 0xCC, 0x78, + 0x3C, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x7E, 0x18, + 0x3F, 0x33, 0x3F, 0x30, 0x30, 0x70, 0xF0, 0xE0, + 0x7F, 0x63, 0x7F, 0x63, 0x63, 0x67, 0xE6, 0xC0, + 0x99, 0x5A, 0x3C, 0xE7, 0xE7, 0x3C, 0x5A, 0x99, + 0x80, 0xE0, 0xF8, 0xFE, 0xF8, 0xE0, 0x80, 0x00, + 0x02, 0x0E, 0x3E, 0xFE, 0x3E, 0x0E, 0x02, 0x00, + 0x18, 0x3C, 0x7E, 0x18, 0x18, 0x7E, 0x3C, 0x18, + 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x66, 0x00, + 0x7F, 0xDB, 0xDB, 0x7B, 0x1B, 0x1B, 0x1B, 0x00, + 0x3E, 0x63, 0x38, 0x6C, 0x6C, 0x38, 0xCC, 0x78, + 0x00, 0x00, 0x00, 0x00, 0x7E, 0x7E, 0x7E, 0x00, + 0x18, 0x3C, 0x7E, 0x18, 0x7E, 0x3C, 0x18, 0xFF, + 0x18, 0x3C, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x00, + 0x18, 0x18, 0x18, 0x18, 0x7E, 0x3C, 0x18, 0x00, + 0x00, 0x18, 0x0C, 0xFE, 0x0C, 0x18, 0x00, 0x00, + 0x00, 0x30, 0x60, 0xFE, 0x60, 0x30, 0x00, 0x00, + 0x00, 0x00, 0xC0, 0xC0, 0xC0, 0xFE, 0x00, 0x00, + 0x00, 0x24, 0x66, 0xFF, 0x66, 0x24, 0x00, 0x00, + 0x00, 0x18, 0x3C, 0x7E, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0xFF, 0xFF, 0x7E, 0x3C, 0x18, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x30, 0x78, 0x78, 0x30, 0x30, 0x00, 0x30, 0x00, + 0x6C, 0x6C, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x6C, 0x6C, 0xFE, 0x6C, 0xFE, 0x6C, 0x6C, 0x00, + 0x30, 0x7C, 0xC0, 0x78, 0x0C, 0xF8, 0x30, 0x00, + 0x00, 0xC6, 0xCC, 0x18, 0x30, 0x66, 0xC6, 0x00, + 0x38, 0x6C, 0x38, 0x76, 0xDC, 0xCC, 0x76, 0x00, + 0x60, 0x60, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x30, 0x60, 0x60, 0x60, 0x30, 0x18, 0x00, + 0x60, 0x30, 0x18, 0x18, 0x18, 0x30, 0x60, 0x00, + 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00, + 0x00, 0x30, 0x30, 0xFC, 0x30, 0x30, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x60, + 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, + 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0x80, 0x00, + 0x7C, 0xC6, 0xCE, 0xDE, 0xF6, 0xE6, 0x7C, 0x00, + 0x30, 0x70, 0x30, 0x30, 0x30, 0x30, 0xFC, 0x00, + 0x78, 0xCC, 0x0C, 0x38, 0x60, 0xCC, 0xFC, 0x00, + 0x78, 0xCC, 0x0C, 0x38, 0x0C, 0xCC, 0x78, 0x00, + 0x1C, 0x3C, 0x6C, 0xCC, 0xFE, 0x0C, 0x1E, 0x00, + 0xFC, 0xC0, 0xF8, 0x0C, 0x0C, 0xCC, 0x78, 0x00, + 0x38, 0x60, 0xC0, 0xF8, 0xCC, 0xCC, 0x78, 0x00, + 0xFC, 0xCC, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x00, + 0x78, 0xCC, 0xCC, 0x78, 0xCC, 0xCC, 0x78, 0x00, + 0x78, 0xCC, 0xCC, 0x7C, 0x0C, 0x18, 0x70, 0x00, + 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, + 0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x60, + 0x18, 0x30, 0x60, 0xC0, 0x60, 0x30, 0x18, 0x00, + 0x00, 0x00, 0xFC, 0x00, 0x00, 0xFC, 0x00, 0x00, + 0x60, 0x30, 0x18, 0x0C, 0x18, 0x30, 0x60, 0x00, + 0x78, 0xCC, 0x0C, 0x18, 0x30, 0x00, 0x30, 0x00, + 0x7C, 0xC6, 0xDE, 0xDE, 0xDE, 0xC0, 0x78, 0x00, + 0x30, 0x78, 0xCC, 0xCC, 0xFC, 0xCC, 0xCC, 0x00, + 0xFC, 0x66, 0x66, 0x7C, 0x66, 0x66, 0xFC, 0x00, + 0x3C, 0x66, 0xC0, 0xC0, 0xC0, 0x66, 0x3C, 0x00, + 0xF8, 0x6C, 0x66, 0x66, 0x66, 0x6C, 0xF8, 0x00, + 0xFE, 0x62, 0x68, 0x78, 0x68, 0x62, 0xFE, 0x00, + 0xFE, 0x62, 0x68, 0x78, 0x68, 0x60, 0xF0, 0x00, + 0x3C, 0x66, 0xC0, 0xC0, 0xCE, 0x66, 0x3E, 0x00, + 0xCC, 0xCC, 0xCC, 0xFC, 0xCC, 0xCC, 0xCC, 0x00, + 0x78, 0x30, 0x30, 0x30, 0x30, 0x30, 0x78, 0x00, + 0x1E, 0x0C, 0x0C, 0x0C, 0xCC, 0xCC, 0x78, 0x00, + 0xE6, 0x66, 0x6C, 0x78, 0x6C, 0x66, 0xE6, 0x00, + 0xF0, 0x60, 0x60, 0x60, 0x62, 0x66, 0xFE, 0x00, + 0xC6, 0xEE, 0xFE, 0xFE, 0xD6, 0xC6, 0xC6, 0x00, + 0xC6, 0xE6, 0xF6, 0xDE, 0xCE, 0xC6, 0xC6, 0x00, + 0x38, 0x6C, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x00, + 0xFC, 0x66, 0x66, 0x7C, 0x60, 0x60, 0xF0, 0x00, + 0x78, 0xCC, 0xCC, 0xCC, 0xDC, 0x78, 0x1C, 0x00, + 0xFC, 0x66, 0x66, 0x7C, 0x6C, 0x66, 0xE6, 0x00, + 0x78, 0xCC, 0xE0, 0x70, 0x1C, 0xCC, 0x78, 0x00, + 0xFC, 0xB4, 0x30, 0x30, 0x30, 0x30, 0x78, 0x00, + 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xFC, 0x00, + 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x78, 0x30, 0x00, + 0xC6, 0xC6, 0xC6, 0xD6, 0xFE, 0xEE, 0xC6, 0x00, + 0xC6, 0xC6, 0x6C, 0x38, 0x38, 0x6C, 0xC6, 0x00, + 0xCC, 0xCC, 0xCC, 0x78, 0x30, 0x30, 0x78, 0x00, + 0xFE, 0xC6, 0x8C, 0x18, 0x32, 0x66, 0xFE, 0x00, + 0x78, 0x60, 0x60, 0x60, 0x60, 0x60, 0x78, 0x00, + 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x02, 0x00, + 0x78, 0x18, 0x18, 0x18, 0x18, 0x18, 0x78, 0x00, + 0x10, 0x38, 0x6C, 0xC6, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x30, 0x30, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0x76, 0x00, + 0xE0, 0x60, 0x60, 0x7C, 0x66, 0x66, 0xDC, 0x00, + 0x00, 0x00, 0x78, 0xCC, 0xC0, 0xCC, 0x78, 0x00, + 0x1C, 0x0C, 0x0C, 0x7C, 0xCC, 0xCC, 0x76, 0x00, + 0x00, 0x00, 0x78, 0xCC, 0xFC, 0xC0, 0x78, 0x00, + 0x38, 0x6C, 0x60, 0xF0, 0x60, 0x60, 0xF0, 0x00, + 0x00, 0x00, 0x76, 0xCC, 0xCC, 0x7C, 0x0C, 0xF8, + 0xE0, 0x60, 0x6C, 0x76, 0x66, 0x66, 0xE6, 0x00, + 0x30, 0x00, 0x70, 0x30, 0x30, 0x30, 0x78, 0x00, + 0x0C, 0x00, 0x0C, 0x0C, 0x0C, 0xCC, 0xCC, 0x78, + 0xE0, 0x60, 0x66, 0x6C, 0x78, 0x6C, 0xE6, 0x00, + 0x70, 0x30, 0x30, 0x30, 0x30, 0x30, 0x78, 0x00, + 0x00, 0x00, 0xCC, 0xFE, 0xFE, 0xD6, 0xC6, 0x00, + 0x00, 0x00, 0xF8, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, + 0x00, 0x00, 0x78, 0xCC, 0xCC, 0xCC, 0x78, 0x00, + 0x00, 0x00, 0xDC, 0x66, 0x66, 0x7C, 0x60, 0xF0, + 0x00, 0x00, 0x76, 0xCC, 0xCC, 0x7C, 0x0C, 0x1E, + 0x00, 0x00, 0xDC, 0x76, 0x66, 0x60, 0xF0, 0x00, + 0x00, 0x00, 0x7C, 0xC0, 0x78, 0x0C, 0xF8, 0x00, + 0x10, 0x30, 0x7C, 0x30, 0x30, 0x34, 0x18, 0x00, + 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0x76, 0x00, + 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0x78, 0x30, 0x00, + 0x00, 0x00, 0xC6, 0xD6, 0xFE, 0xFE, 0x6C, 0x00, + 0x00, 0x00, 0xC6, 0x6C, 0x38, 0x6C, 0xC6, 0x00, + 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0x7C, 0x0C, 0xF8, + 0x00, 0x00, 0xFC, 0x98, 0x30, 0x64, 0xFC, 0x00, + 0x1C, 0x30, 0x30, 0xE0, 0x30, 0x30, 0x1C, 0x00, + 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00, + 0xE0, 0x30, 0x30, 0x1C, 0x30, 0x30, 0xE0, 0x00, + 0x76, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x10, 0x38, 0x6C, 0xC6, 0xC6, 0xFE, 0x00, + 0x78, 0xCC, 0xC0, 0xCC, 0x78, 0x18, 0x0C, 0x78, + 0x00, 0xCC, 0x00, 0xCC, 0xCC, 0xCC, 0x7E, 0x00, + 0x1C, 0x00, 0x78, 0xCC, 0xFC, 0xC0, 0x78, 0x00, + 0x7E, 0xC3, 0x3C, 0x06, 0x3E, 0x66, 0x3F, 0x00, + 0xCC, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0x7E, 0x00, + 0xE0, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0x7E, 0x00, + 0x30, 0x30, 0x78, 0x0C, 0x7C, 0xCC, 0x7E, 0x00, + 0x00, 0x00, 0x78, 0xC0, 0xC0, 0x78, 0x0C, 0x38, + 0x7E, 0xC3, 0x3C, 0x66, 0x7E, 0x60, 0x3C, 0x00, + 0xCC, 0x00, 0x78, 0xCC, 0xFC, 0xC0, 0x78, 0x00, + 0xE0, 0x00, 0x78, 0xCC, 0xFC, 0xC0, 0x78, 0x00, + 0xCC, 0x00, 0x70, 0x30, 0x30, 0x30, 0x78, 0x00, + 0x7C, 0xC6, 0x38, 0x18, 0x18, 0x18, 0x3C, 0x00, + 0xE0, 0x00, 0x70, 0x30, 0x30, 0x30, 0x78, 0x00, + 0xC6, 0x38, 0x6C, 0xC6, 0xFE, 0xC6, 0xC6, 0x00, + 0x30, 0x30, 0x00, 0x78, 0xCC, 0xFC, 0xCC, 0x00, + 0x1C, 0x00, 0xFC, 0x60, 0x78, 0x60, 0xFC, 0x00, + 0x00, 0x00, 0x7F, 0x0C, 0x7F, 0xCC, 0x7F, 0x00, + 0x3E, 0x6C, 0xCC, 0xFE, 0xCC, 0xCC, 0xCE, 0x00, + 0x78, 0xCC, 0x00, 0x78, 0xCC, 0xCC, 0x78, 0x00, + 0x00, 0xCC, 0x00, 0x78, 0xCC, 0xCC, 0x78, 0x00, + 0x00, 0xE0, 0x00, 0x78, 0xCC, 0xCC, 0x78, 0x00, + 0x78, 0xCC, 0x00, 0xCC, 0xCC, 0xCC, 0x7E, 0x00, + 0x00, 0xE0, 0x00, 0xCC, 0xCC, 0xCC, 0x7E, 0x00, + 0x00, 0xCC, 0x00, 0xCC, 0xCC, 0x7C, 0x0C, 0xF8, + 0xC3, 0x18, 0x3C, 0x66, 0x66, 0x3C, 0x18, 0x00, + 0xCC, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0x78, 0x00, + 0x18, 0x18, 0x7E, 0xC0, 0xC0, 0x7E, 0x18, 0x18, + 0x38, 0x6C, 0x64, 0xF0, 0x60, 0xE6, 0xFC, 0x00, + 0xCC, 0xCC, 0x78, 0xFC, 0x30, 0xFC, 0x30, 0x30, + 0xF8, 0xCC, 0xCC, 0xFA, 0xC6, 0xCF, 0xC6, 0xC7, + 0x0E, 0x1B, 0x18, 0x3C, 0x18, 0x18, 0xD8, 0x70, + 0x1C, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0x7E, 0x00, + 0x38, 0x00, 0x70, 0x30, 0x30, 0x30, 0x78, 0x00, + 0x00, 0x1C, 0x00, 0x78, 0xCC, 0xCC, 0x78, 0x00, + 0x00, 0x1C, 0x00, 0xCC, 0xCC, 0xCC, 0x7E, 0x00, + 0x00, 0xF8, 0x00, 0xF8, 0xCC, 0xCC, 0xCC, 0x00, + 0xFC, 0x00, 0xCC, 0xEC, 0xFC, 0xDC, 0xCC, 0x00, + 0x3C, 0x6C, 0x6C, 0x3E, 0x00, 0x7E, 0x00, 0x00, + 0x38, 0x6C, 0x6C, 0x38, 0x00, 0x7C, 0x00, 0x00, + 0x30, 0x00, 0x30, 0x60, 0xC0, 0xCC, 0x78, 0x00, + 0x00, 0x00, 0x00, 0xFC, 0xC0, 0xC0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFC, 0x0C, 0x0C, 0x00, 0x00, + 0xC3, 0xC6, 0xCC, 0xDE, 0x33, 0x66, 0xCC, 0x0F, + 0xC3, 0xC6, 0xCC, 0xDB, 0x37, 0x6F, 0xCF, 0x03, + 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x18, 0x00, + 0x00, 0x33, 0x66, 0xCC, 0x66, 0x33, 0x00, 0x00, + 0x00, 0xCC, 0x66, 0x33, 0x66, 0xCC, 0x00, 0x00, + 0x22, 0x88, 0x22, 0x88, 0x22, 0x88, 0x22, 0x88, + 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, + 0xDB, 0x77, 0xDB, 0xEE, 0xDB, 0x77, 0xDB, 0xEE, + 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0xF8, 0x18, 0x18, 0x18, + 0x18, 0x18, 0xF8, 0x18, 0xF8, 0x18, 0x18, 0x18, + 0x36, 0x36, 0x36, 0x36, 0xF6, 0x36, 0x36, 0x36, + 0x00, 0x00, 0x00, 0x00, 0xFE, 0x36, 0x36, 0x36, + 0x00, 0x00, 0xF8, 0x18, 0xF8, 0x18, 0x18, 0x18, + 0x36, 0x36, 0xF6, 0x06, 0xF6, 0x36, 0x36, 0x36, + 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, + 0x00, 0x00, 0xFE, 0x06, 0xF6, 0x36, 0x36, 0x36, + 0x36, 0x36, 0xF6, 0x06, 0xFE, 0x00, 0x00, 0x00, + 0x36, 0x36, 0x36, 0x36, 0xFE, 0x00, 0x00, 0x00, + 0x18, 0x18, 0xF8, 0x18, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xF8, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x1F, 0x00, 0x00, 0x00, + 0x18, 0x18, 0x18, 0x18, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x1F, 0x18, 0x18, 0x18, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, + 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x1F, 0x18, 0x1F, 0x18, 0x18, 0x18, + 0x36, 0x36, 0x36, 0x36, 0x37, 0x36, 0x36, 0x36, + 0x36, 0x36, 0x37, 0x30, 0x3F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3F, 0x30, 0x37, 0x36, 0x36, 0x36, + 0x36, 0x36, 0xF7, 0x00, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0x00, 0xF7, 0x36, 0x36, 0x36, + 0x36, 0x36, 0x37, 0x30, 0x37, 0x36, 0x36, 0x36, + 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, + 0x36, 0x36, 0xF7, 0x00, 0xF7, 0x36, 0x36, 0x36, + 0x18, 0x18, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, + 0x36, 0x36, 0x36, 0x36, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x18, 0x18, 0x18, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0x36, 0x36, 0x36, + 0x36, 0x36, 0x36, 0x36, 0x3F, 0x00, 0x00, 0x00, + 0x18, 0x18, 0x1F, 0x18, 0x1F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1F, 0x18, 0x1F, 0x18, 0x18, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x3F, 0x36, 0x36, 0x36, + 0x36, 0x36, 0x36, 0x36, 0xFF, 0x36, 0x36, 0x36, + 0x18, 0x18, 0xFF, 0x18, 0xFF, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0xF8, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x18, 0x18, 0x18, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x76, 0xDC, 0xC8, 0xDC, 0x76, 0x00, + 0x00, 0x78, 0xCC, 0xF8, 0xCC, 0xF8, 0xC0, 0xC0, + 0x00, 0xFC, 0xCC, 0xC0, 0xC0, 0xC0, 0xC0, 0x00, + 0x00, 0xFE, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x00, + 0xFC, 0xCC, 0x60, 0x30, 0x60, 0xCC, 0xFC, 0x00, + 0x00, 0x00, 0x7E, 0xD8, 0xD8, 0xD8, 0x70, 0x00, + 0x00, 0x66, 0x66, 0x66, 0x66, 0x7C, 0x60, 0xC0, + 0x00, 0x76, 0xDC, 0x18, 0x18, 0x18, 0x18, 0x00, + 0xFC, 0x30, 0x78, 0xCC, 0xCC, 0x78, 0x30, 0xFC, + 0x38, 0x6C, 0xC6, 0xFE, 0xC6, 0x6C, 0x38, 0x00, + 0x38, 0x6C, 0xC6, 0xC6, 0x6C, 0x6C, 0xEE, 0x00, + 0x1C, 0x30, 0x18, 0x7C, 0xCC, 0xCC, 0x78, 0x00, + 0x00, 0x00, 0x7E, 0xDB, 0xDB, 0x7E, 0x00, 0x00, + 0x06, 0x0C, 0x7E, 0xDB, 0xDB, 0x7E, 0x60, 0xC0, + 0x38, 0x60, 0xC0, 0xF8, 0xC0, 0x60, 0x38, 0x00, + 0x78, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, + 0x00, 0xFC, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0x00, + 0x30, 0x30, 0xFC, 0x30, 0x30, 0x00, 0xFC, 0x00, + 0x60, 0x30, 0x18, 0x30, 0x60, 0x00, 0xFC, 0x00, + 0x18, 0x30, 0x60, 0x30, 0x18, 0x00, 0xFC, 0x00, + 0x0E, 0x1B, 0x1B, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x18, 0x18, 0x18, 0x18, 0x18, 0xD8, 0xD8, 0x70, + 0x30, 0x30, 0x00, 0xFC, 0x00, 0x30, 0x30, 0x00, + 0x00, 0x76, 0xDC, 0x00, 0x76, 0xDC, 0x00, 0x00, + 0x38, 0x6C, 0x6C, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x0F, 0x0C, 0x0C, 0x0C, 0xEC, 0x6C, 0x3C, 0x1C, + 0x78, 0x6C, 0x6C, 0x6C, 0x6C, 0x00, 0x00, 0x00, + 0x70, 0x18, 0x30, 0x60, 0x78, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3C, 0x3C, 0x3C, 0x3C, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; +} diff --git a/core/src/main/java/com/agifans/agile/UserInput.java b/core/src/main/java/com/agifans/agile/UserInput.java new file mode 100644 index 0000000..fecd5c7 --- /dev/null +++ b/core/src/main/java/com/agifans/agile/UserInput.java @@ -0,0 +1,480 @@ +package com.agifans.agile; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedQueue; + +import com.badlogic.gdx.Input.Keys; +import com.badlogic.gdx.InputAdapter; + +/** + * Handles the input of keyboard events, mapping them to a form that the AGILE + * interpreter can query as required. + */ +public class UserInput extends InputAdapter { + + /** + * The SHIFT modifier key. + */ + private static final int SHIFT_MODIFIER = 0x10000; + + /** + * The CTRL modifier key. + */ + private static final int CONTROL_MODIFIER = 0x20000; + + /** + * The ALT modifier key. + */ + private static final int ALT_MODIFIER = 0x40000; + + /** + * Marks the enqueued keycode value as an ASCII character. + */ + public static final int ASCII = 0x80000; + + // AGI ACCEPT/ABORT input values. + public static final int ACCEPT = 0; + public static final int ABORT = 1; + + /** + * A queue of all key presses that the user has made. + */ + public ConcurrentLinkedQueue keyPressQueue; + + /** + * Current state of every key on the keyboard. + */ + public boolean[] keys; + + /** + * Stores the state of every key on the previous cycle. + */ + public boolean[] oldKeys; + + /** + * Current state of the ALT/SHIFT/CONTROL modifiers, as bit mask. + */ + private int modifiers; + + /** + * A Map between IBM PC key codes as understood by the PC AGI interpreter and the C# Key codes. + */ + public Map keyCodeMap; + + public Map reverseKeyCodeMap; + + /** + * Unmodified LibGDX key values that we will enqueue as-is. + */ + private static final List UNMODIFIED_KEY_LIST = Arrays.asList( + Keys.F1, + Keys.F2, + Keys.F3, + Keys.F4, + Keys.F5, + Keys.F6, + Keys.F7, + Keys.F8, + Keys.F9, + Keys.F10, + Keys.HOME, + Keys.UP, + Keys.PAGE_UP, + Keys.LEFT, + Keys.RIGHT, + Keys.END, + Keys.DOWN, + Keys.PAGE_DOWN, + Keys.HOME, + Keys.INSERT, + Keys.DEL); + + /** + * Constructor for UserInput. + */ + public UserInput() { + this.keys = new boolean[256]; + this.oldKeys = new boolean[256]; + this.keyPressQueue = new ConcurrentLinkedQueue(); + this.keyCodeMap = createKeyConversionMap(); + this.reverseKeyCodeMap = new HashMap(); + for (Map.Entry entry : keyCodeMap.entrySet()) { + if (!reverseKeyCodeMap.containsKey(entry.getValue()) && (entry.getValue() != 0)) { + reverseKeyCodeMap.put(entry.getValue(), entry.getKey()); + } + } + } + + /** + * Handles the key down event. + * + * @param keycode one of the constants in {@link Character.Keys} + * + * @return whether the input was processed + */ + public boolean keyDown (int keycode) { + //System.out.println(String.format("keyDown: 0x%04X [modifiers=0x%05X]", + // (int)keycode, modifiers)); + + // AGILE interpreter ignores some keys completely, e.g. F11. + if (keycode == Keys.F11) { + return false; + } + + this.keys[keycode & 0xFF] = true; + + // Update modifies for ALT/SHIFT/CONTROL but do not enqueue key presses that + // are Alt/Shift/Ctrl by themselves. AGI doesn't support mapping those. + if ((keycode == Keys.SHIFT_LEFT) || (keycode == Keys.SHIFT_RIGHT)) { + return true; + } + if ((keycode == Keys.ALT_LEFT) || (keycode == Keys.ALT_RIGHT)) { + modifiers |= ALT_MODIFIER; + return true; + } + if ((keycode == Keys.CONTROL_LEFT) || (keycode == Keys.CONTROL_RIGHT)) { + modifiers |= CONTROL_MODIFIER; + return true; + } + + // Some keys and key combinations we need to map to ASCII characters. + Integer character = Character.KEYSTROKE_TO_CHAR_MAP.get(modifiers + keycode); + if (character != null) { + // Enqueue the mapped character. This covers CTRL combinations and ESC. + keyPressQueue.add(ASCII | character); + } + else if (modifiers != 0) { + // Enqueues the ALT combinations. + keyPressQueue.add(modifiers | keycode); + } + else if (UNMODIFIED_KEY_LIST.contains(keycode)) { + // Any other keycode that didn't map to a character, and isn't affected + // by modifiers, is enqueued as-is. + keyPressQueue.add(keycode); + } + + return true; + } + + /** + * Handles the key up event. + */ + public boolean keyUp(int keycode) { + //System.out.println(String.format("keyUp: 0x%04X [modifiers=0x%05X]", + // (int)keycode, modifiers)); + + this.keys[keycode & 0xFF] = false; + + // Update modifiers for ALT/CONTROL + if ((keycode == Keys.ALT_LEFT) || (keycode == Keys.ALT_RIGHT)) { + modifiers &= (~ALT_MODIFIER); + return true; + } + if ((keycode == Keys.CONTROL_LEFT) || (keycode == Keys.CONTROL_RIGHT)) { + modifiers &= (~CONTROL_MODIFIER); + return true; + } + + return true; + } + + /** + * Handles the key pressed event. + * + * @param character The character that was typed. + */ + public boolean keyTyped(char character) { + //System.out.println(String.format("keyTyped: 0x%02X [modifiers=0x%05X]", + // (int)character, modifiers)); + + // NOTE: The keyTyped method isn't invoked when ALT and CTRL are used. + + // We handle ENTER ourselves in keyDown, via the HashMap in Character class. + if ((character != 0x0A) && (character != 0x0D)) { + keyPressQueue.add(ASCII | (int)character); + } + + return true; + } + + /** + * Wait for and return either ACCEPT or ABORT. + * + * @return Either ACCEPT or ABORT, depending on what was chosen. + */ + public int waitAcceptAbort() { + int action; + + // Ignore anything currently on the key press queue. + while (keyPressQueue.poll() != null) ; + + // Now wait for the the next key. + while ((action = checkAcceptAbort()) == -1) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // Ignore. + } + } + + return action; + } + + /** + * Waits for the next key to be pressed then returns the value. Always clears + * the key press queue beforehand. + * + * @return The key that was pressed. + */ + public int waitForKey() { + return waitForKey(true); + } + + /** + * Waits for the next key to be pressed then returns the value. + * + * @param clearQueue Whether to clear what is on the queue before waiting. + * + * @returnThe key that was pressed. + */ + public int waitForKey(boolean clearQueue) { + int key; + + if (clearQueue) { + // Ignore anything currently on the key press queue. + while (keyPressQueue.poll() != null) ; + } + + // Now wait for the the next key. + while ((key = getKey()) == 0) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // Ignore. + } + } + + return key; + } + + /** + * Check if either ACCEPT or ABORT has been selected. Return the value if so, -1 otherwise. + * + * @return Either ACCEPT or ABORT; otherwise -1 if neither was selected. + */ + public int checkAcceptAbort() { + int c; + + if ((c = getKey()) == (ASCII | Character.ENTER)) { + return ACCEPT; + } + else if (c == (ASCII | Character.ESC)) { + return ABORT; + } + else { + return -1; + } + } + + /** + * Gets a key from the key queue. Return 0 if none available. + * + * @return Either the key from the queue, or 0 if none available. + */ + public int getKey() { + return (keyPressQueue.peek() != null? keyPressQueue.poll() : 0); + } + + /** + * Creates the Map between key codes as understood by the PC AGI interpreter + * and the libGDX Key codes. + */ + private Map createKeyConversionMap() { + Map controllerMap = new HashMap<>(); + + controllerMap.put(9, ASCII | Character.TAB); + controllerMap.put(27, ASCII | Character.ESC); + controllerMap.put(13, ASCII | Character.ENTER); + + // Function keys. + controllerMap.put((59 << 8) + 0, Keys.F1); + controllerMap.put((60 << 8) + 0, Keys.F2); + controllerMap.put((61 << 8) + 0, Keys.F3); + controllerMap.put((62 << 8) + 0, Keys.F4); + controllerMap.put((63 << 8) + 0, Keys.F5); + controllerMap.put((64 << 8) + 0, Keys.F6); + controllerMap.put((65 << 8) + 0, Keys.F7); + controllerMap.put((66 << 8) + 0, Keys.F8); + controllerMap.put((67 << 8) + 0, Keys.F9); + controllerMap.put((68 << 8) + 0, Keys.F10); + + // Control and another key. + controllerMap.put(1, ASCII | Character.CTRL_A); + controllerMap.put(2, ASCII | Character.CTRL_B); + controllerMap.put(3, ASCII | Character.CTRL_C); + controllerMap.put(4, ASCII | Character.CTRL_D); + controllerMap.put(5, ASCII | Character.CTRL_E); + controllerMap.put(6, ASCII | Character.CTRL_F); + controllerMap.put(7, ASCII | Character.CTRL_G); + controllerMap.put(8, ASCII | Character.CTRL_H); + controllerMap.put(10, ASCII | Character.CTRL_J); + controllerMap.put(11, ASCII | Character.CTRL_K); + controllerMap.put(12, ASCII | Character.CTRL_L); + controllerMap.put(14, ASCII | Character.CTRL_N); + controllerMap.put(15, ASCII | Character.CTRL_O); + controllerMap.put(16, ASCII | Character.CTRL_P); + controllerMap.put(17, ASCII | Character.CTRL_Q); + controllerMap.put(18, ASCII | Character.CTRL_R); + controllerMap.put(19, ASCII | Character.CTRL_S); + controllerMap.put(20, ASCII | Character.CTRL_T); + controllerMap.put(21, ASCII | Character.CTRL_U); + controllerMap.put(22, ASCII | Character.CTRL_V); + controllerMap.put(23, ASCII | Character.CTRL_W); + controllerMap.put(24, ASCII | Character.CTRL_X); + controllerMap.put(25, ASCII | Character.CTRL_Y); + controllerMap.put(26, ASCII | Character.CTRL_Z); + + // Alt and another key. + controllerMap.put((16 << 8) + 0, ALT_MODIFIER | Keys.Q); + controllerMap.put((17 << 8) + 0, ALT_MODIFIER | Keys.W); + controllerMap.put((18 << 8) + 0, ALT_MODIFIER | Keys.E); + controllerMap.put((19 << 8) + 0, ALT_MODIFIER | Keys.R); + controllerMap.put((20 << 8) + 0, ALT_MODIFIER | Keys.T); + controllerMap.put((21 << 8) + 0, ALT_MODIFIER | Keys.Y); + controllerMap.put((22 << 8) + 0, ALT_MODIFIER | Keys.U); + controllerMap.put((23 << 8) + 0, ALT_MODIFIER | Keys.I); + controllerMap.put((24 << 8) + 0, ALT_MODIFIER | Keys.O); + controllerMap.put((25 << 8) + 0, ALT_MODIFIER | Keys.P); + controllerMap.put((30 << 8) + 0, ALT_MODIFIER | Keys.A); + controllerMap.put((31 << 8) + 0, ALT_MODIFIER | Keys.S); + controllerMap.put((32 << 8) + 0, ALT_MODIFIER | Keys.D); + controllerMap.put((33 << 8) + 0, ALT_MODIFIER | Keys.F); + controllerMap.put((34 << 8) + 0, ALT_MODIFIER | Keys.G); + controllerMap.put((35 << 8) + 0, ALT_MODIFIER | Keys.H); + controllerMap.put((36 << 8) + 0, ALT_MODIFIER | Keys.J); + controllerMap.put((37 << 8) + 0, ALT_MODIFIER | Keys.K); + controllerMap.put((38 << 8) + 0, ALT_MODIFIER | Keys.L); + controllerMap.put((44 << 8) + 0, ALT_MODIFIER | Keys.Z); + controllerMap.put((45 << 8) + 0, ALT_MODIFIER | Keys.X); + controllerMap.put((46 << 8) + 0, ALT_MODIFIER | Keys.C); + controllerMap.put((47 << 8) + 0, ALT_MODIFIER | Keys.V); + controllerMap.put((48 << 8) + 0, ALT_MODIFIER | Keys.B); + controllerMap.put((49 << 8) + 0, ALT_MODIFIER | Keys.N); + controllerMap.put((50 << 8) + 0, ALT_MODIFIER | Keys.M); + + controllerMap.put(28, ASCII | Character.CTRL_BACK_SLASH); + controllerMap.put(29, ASCII | Character.CTRL_CLOSE_SQUARE_BRACKET); + controllerMap.put(30, ASCII | Character.CTRL_6); + controllerMap.put(31, ASCII | Character.CTRL_MINUS); + + // Normal printable chars. + controllerMap.put(32, (ASCII | ' ')); + controllerMap.put(33, (ASCII | '!')); + controllerMap.put(34, (ASCII | '"')); + controllerMap.put(35, (ASCII | '#')); + controllerMap.put(36, (ASCII | '$')); + controllerMap.put(37, (ASCII | '%')); + controllerMap.put(38, (ASCII | '&')); + controllerMap.put(39, (ASCII | '\'')); + controllerMap.put(40, (ASCII | '(')); + controllerMap.put(41, (ASCII | ')')); + controllerMap.put(42, (ASCII | '*')); + controllerMap.put(43, (ASCII | '+')); + controllerMap.put(44, (ASCII | ',')); + controllerMap.put(45, (ASCII | '-')); + controllerMap.put(46, (ASCII | '.')); + controllerMap.put(47, (ASCII | '/')); + controllerMap.put(48, (ASCII | '0')); + controllerMap.put(49, (ASCII | '1')); + controllerMap.put(50, (ASCII | '2')); + controllerMap.put(51, (ASCII | '3')); + controllerMap.put(52, (ASCII | '4')); + controllerMap.put(53, (ASCII | '5')); + controllerMap.put(54, (ASCII | '6')); + controllerMap.put(55, (ASCII | '7')); + controllerMap.put(56, (ASCII | '8')); + controllerMap.put(57, (ASCII | '9')); + controllerMap.put(58, (ASCII | ':')); + controllerMap.put(59, (ASCII | ';')); + controllerMap.put(60, (ASCII | '<')); + controllerMap.put(61, (ASCII | '=')); + controllerMap.put(62, (ASCII | '>')); + controllerMap.put(63, (ASCII | '?')); + controllerMap.put(64, (ASCII | '@')); + + // Manhunter games use unmodified alpha chars as controllers, e.g. C and S. AGI Demo Packs do as well. + controllerMap.put(65, (ASCII | 'a')); + controllerMap.put(66, (ASCII | 'b')); + controllerMap.put(67, (ASCII | 'c')); + controllerMap.put(68, (ASCII | 'd')); + controllerMap.put(69, (ASCII | 'e')); + controllerMap.put(70, (ASCII | 'f')); + controllerMap.put(71, (ASCII | 'g')); + controllerMap.put(72, (ASCII | 'h')); + controllerMap.put(73, (ASCII | 'i')); + controllerMap.put(74, (ASCII | 'j')); + controllerMap.put(75, (ASCII | 'k')); + controllerMap.put(76, (ASCII | 'l')); + controllerMap.put(77, (ASCII | 'm')); + controllerMap.put(78, (ASCII | 'n')); + controllerMap.put(79, (ASCII | 'o')); + controllerMap.put(80, (ASCII | 'p')); + controllerMap.put(81, (ASCII | 'q')); + controllerMap.put(82, (ASCII | 'r')); + controllerMap.put(83, (ASCII | 's')); + controllerMap.put(84, (ASCII | 't')); + controllerMap.put(85, (ASCII | 'u')); + controllerMap.put(86, (ASCII | 'v')); + controllerMap.put(87, (ASCII | 'w')); + controllerMap.put(88, (ASCII | 'x')); + controllerMap.put(89, (ASCII | 'y')); + controllerMap.put(90, (ASCII | 'z')); + controllerMap.put(91, (ASCII | '[')); + controllerMap.put(92, (ASCII | '\\')); + controllerMap.put(93, (ASCII | ']')); + controllerMap.put(94, (ASCII | '^')); + controllerMap.put(95, (ASCII | '_')); + controllerMap.put(96, (ASCII | '`')); + controllerMap.put(97, (ASCII | 'A')); + controllerMap.put(98, (ASCII | 'B')); + controllerMap.put(99, (ASCII | 'C')); + controllerMap.put(100, (ASCII | 'D')); + controllerMap.put(101, (ASCII | 'E')); + controllerMap.put(102, (ASCII | 'F')); + controllerMap.put(103, (ASCII | 'G')); + controllerMap.put(104, (ASCII | 'H')); + controllerMap.put(105, (ASCII | 'I')); + controllerMap.put(106, (ASCII | 'J')); + controllerMap.put(107, (ASCII | 'K')); + controllerMap.put(108, (ASCII | 'L')); + controllerMap.put(109, (ASCII | 'M')); + controllerMap.put(110, (ASCII | 'N')); + controllerMap.put(111, (ASCII | 'O')); + controllerMap.put(112, (ASCII | 'P')); + controllerMap.put(113, (ASCII | 'Q')); + controllerMap.put(114, (ASCII | 'R')); + controllerMap.put(115, (ASCII | 'S')); + controllerMap.put(116, (ASCII | 'T')); + controllerMap.put(117, (ASCII | 'U')); + controllerMap.put(118, (ASCII | 'V')); + controllerMap.put(119, (ASCII | 'W')); + controllerMap.put(120, (ASCII | 'X')); + controllerMap.put(121, (ASCII | 'Y')); + controllerMap.put(122, (ASCII | 'Z')); + controllerMap.put(123, (ASCII | '{')); + controllerMap.put(124, (ASCII | '|')); + controllerMap.put(125, (ASCII | '}')); + controllerMap.put(126, (ASCII | '~')); + + // Joysick codes. We're going to ignore these for now. Who uses a Joystick anyway? Maybe in the 80s. :) + controllerMap.put((1 << 8) + 1, 0); + controllerMap.put((1 << 8) + 2, 0); + controllerMap.put((1 << 8) + 3, 0); + controllerMap.put((1 << 8) + 4, 0); + + return controllerMap; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/agifans/agile/WavePlayer.java b/core/src/main/java/com/agifans/agile/WavePlayer.java new file mode 100644 index 0000000..e061994 --- /dev/null +++ b/core/src/main/java/com/agifans/agile/WavePlayer.java @@ -0,0 +1,50 @@ +package com.agifans.agile; + +/** + * An Interface for playing WAV data. The desktop, mobile, and HTML platforms will + * implement this in their own way. We generally have a lot more control over sound + * if we ignore the libgdx platform independent audio classes and instead use + * platform specific audio techniques. This is particularly the case for HTML. It is + * doesn't seem possible to play a WAV file from a byte array and request callback + * at the end when using libgdx, but its perfectly possible in JavaScript. + */ +public interface WavePlayer { + + /** + * Plays the given WAV file data, and when finished, calls the given + * endCallback Runnable. + * + * @param waveData A byte array containing the WAV data to play. + * @param endedCallback The callback Runnable to run when finished. + */ + void playWaveData(byte[] waveData, Runnable endedCallback); + + /** + * Request the WavePlayer implementation to stop playing the WAV. + * + * @param wait @param wait true to wait for the WAV player to finish playing; otherwise false to not wait. + */ + void stopPlaying(boolean wait); + + /** + * Returns true if the sound is still playing. + * + * @return true if the sound is still playing; otherwise false. + */ + boolean isPlaying(); + + /** + * Resets the state of the WavePlayer, as if it is newly instantiated. This is + * intended to be calling in scenarios such as when the room has changed, or + * when a saved game has been restored. The platform specific implementations may + * or may not actually do anything. + */ + void reset(); + + /** + * Dispose of any audio device objects that were created to support sound + * play back. Whether this does anything or not depends on the platform specific + * implementation. + */ + void dispose(); +} diff --git a/core/src/main/java/com/agifans/agile/agilib/AgileLogicProvider.java b/core/src/main/java/com/agifans/agile/agilib/AgileLogicProvider.java new file mode 100644 index 0000000..b8f4421 --- /dev/null +++ b/core/src/main/java/com/agifans/agile/agilib/AgileLogicProvider.java @@ -0,0 +1,36 @@ +package com.agifans.agile.agilib; + +import java.io.IOException; +import java.io.InputStream; + +import com.sierra.agi.io.IOUtils; +import com.sierra.agi.logic.Logic; +import com.sierra.agi.logic.LogicException; +import com.sierra.agi.logic.LogicProvider; + +/** + * An implementation of the JAGI LogicProvider interface that loads the Logic + * in a form more easily used by AGILE. + */ +public class AgileLogicProvider implements LogicProvider { + + @Override + public Logic loadLogic(short logicNumber, InputStream inputStream, int size) throws IOException, LogicException { + byte[] rawData = new byte[size]; + IOUtils.fill(inputStream, rawData, 0, size); + return new AgileLogicWrapper(new com.agifans.agile.agilib.Logic(rawData, false)); + } + + public static class AgileLogicWrapper implements Logic { + + private com.agifans.agile.agilib.Logic agileLogic; + + public AgileLogicWrapper(com.agifans.agile.agilib.Logic agileLogic) { + this.agileLogic = agileLogic; + } + + public com.agifans.agile.agilib.Logic getAgileLogic() { + return agileLogic; + } + } +} diff --git a/core/src/main/java/com/agifans/agile/agilib/AgileSoundProvider.java b/core/src/main/java/com/agifans/agile/agilib/AgileSoundProvider.java new file mode 100644 index 0000000..79ae130 --- /dev/null +++ b/core/src/main/java/com/agifans/agile/agilib/AgileSoundProvider.java @@ -0,0 +1,46 @@ +package com.agifans.agile.agilib; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import com.sierra.agi.sound.Sound; +import com.sierra.agi.sound.SoundProvider; + +/** + * An implementation of the JAGI SoundProvider interface that loads the Sound + * in a form more easily used by AGILE. + */ +public class AgileSoundProvider implements SoundProvider { + + @Override + public Sound loadSound(InputStream is) throws IOException { + // At this point, JAGI has already read the 5 byte header, i.e. + // 0x12 0x34, etc., which means that the InputStream does not contain + // the length. We therefore have to fully read the resource from + // the InputStream so as to create the byte array required by + // the AGILE Sound resource. Avoiding Java 9 at present, as it is + // unclear whether GWT will support this. + int numOfBytesReads; + byte[] data = new byte[256]; + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + while ((numOfBytesReads = is.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, numOfBytesReads); + } + buffer.flush(); + return new AgileSoundWrapper(new com.agifans.agile.agilib.Sound(buffer.toByteArray())); + } + + public static class AgileSoundWrapper implements Sound { + + private com.agifans.agile.agilib.Sound agileSound; + + public AgileSoundWrapper(com.agifans.agile.agilib.Sound agileSound) { + this.agileSound = agileSound; + } + + public com.agifans.agile.agilib.Sound getAgileSound() { + return agileSound; + } + } +} diff --git a/core/src/main/java/com/agifans/agile/agilib/Game.java b/core/src/main/java/com/agifans/agile/agilib/Game.java new file mode 100644 index 0000000..c9751c8 --- /dev/null +++ b/core/src/main/java/com/agifans/agile/agilib/Game.java @@ -0,0 +1,129 @@ +package com.agifans.agile.agilib; + +import java.io.File; +import java.io.IOException; + +import com.agifans.agile.agilib.AgileLogicProvider.AgileLogicWrapper; +import com.agifans.agile.agilib.AgileSoundProvider.AgileSoundWrapper; +import com.sierra.agi.res.ResourceCache; +import com.sierra.agi.res.ResourceCacheFile; +import com.sierra.agi.res.ResourceException; + +/** + * An adapter between the interface that AGILE expects and the JAGI library. + */ +public class Game { + + private ResourceCache resourceCache; + + public String gameFolder; + + public String v3GameSig; + + public String version; + + public Words words; + + public Objects objects; + + public Logic[] logics; + + public Picture[] pictures; + + public View[] views; + + public Sound[] sounds; + + /** + * Constructor for Game. + * + * @param gameFolder The folder to load the AGI game from. + */ + public Game(String gameFolder) { + this.gameFolder = gameFolder; + + // The aim is to try to use JAGI as untouched as possible to load resources + // for use in AGILE, i.e. JAGI becomes the AGI library for the Java version + // of AGILE. + + try { + // We use our own LogicProvider & SoundProvider implementations, so that we can + // load LOGICs and SOUNDs directly in the form required by AGILE. The other + // types are converted from the JAGI types, after being loaded. It didn't make + // sense to do that for the Logic and Sound types, as it is quite different. Luckily + // JAGI already provided a way to plug in a custom implementations via properties. + System.setProperty("com.sierra.agi.logic.LogicProvider", "com.agifans.agile.agilib.AgileLogicProvider"); + System.setProperty("com.sierra.agi.sound.SoundProvider", "com.agifans.agile.agilib.AgileSoundProvider"); + + // Use JAGI to fully load the AGI game's files. + resourceCache = new ResourceCacheFile(new File(gameFolder)); + version = resourceCache.getVersion(); + v3GameSig = resourceCache.getV3GameSig(); + logics = loadLogics(); + pictures = loadPictures(); + views = loadViews(); + sounds = loadSounds(); + objects = new Objects(resourceCache.getObjects()); + words = new Words(resourceCache.getWords()); + + } catch (ResourceException | IOException e) { + throw new RuntimeException("Decode of game failed.", e); + } + } + + private Logic[] loadLogics() { + Logic[] logics = new Logic[256]; + for (short i=0; i<256; i++) { + try { + Logic logic = ((AgileLogicWrapper)resourceCache.getLogic(i)).getAgileLogic(); + logic.index = i; + logics[i] = logic; + } catch (Exception rnee) { + // Ignore. The LOGIC doesn't exist. + } + } + return logics; + } + + private Picture[] loadPictures() { + Picture[] pictures = new Picture[256]; + for (short i=0; i<256; i++) { + try { + Picture picture = new Picture(resourceCache.getPicture(i)); + picture.index = i; + pictures[i] = picture; + } catch (Exception e) { + // Ignore. The PICTURE doesn't exist. + } + } + return pictures; + } + + private View[] loadViews() { + View[] views = new View[256]; + for (short i=0; i<256; i++) { + try { + View view = new View(resourceCache.getView(i)); + view.index = i; + views[i] = view; + } catch (Exception e) { + // Ignore. The VIEW doesn't exist. + } + } + return views; + } + + private Sound[] loadSounds() { + Sound[] sounds = new Sound[256]; + for (short i=0; i<256; i++) { + try { + Sound sound = ((AgileSoundWrapper)resourceCache.getSound(i)).getAgileSound(); + sound.index = i; + sounds[i] = sound; + } catch (Exception e) { + // Ignore. The SOUND doesn't exist. + } + } + return sounds; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/agifans/agile/agilib/Logic.java b/core/src/main/java/com/agifans/agile/agilib/Logic.java new file mode 100644 index 0000000..82ba1ea --- /dev/null +++ b/core/src/main/java/com/agifans/agile/agilib/Logic.java @@ -0,0 +1,810 @@ +package com.agifans.agile.agilib; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Logic extends Resource { + + /** + * At the top level, all Instructions are Actions. The Conditions only exist as + * members of an IfAction or OrCondition. + */ + public List actions; + + /** + * Holds the messages for this Logic. + */ + public List messages; + + /** + * A Lookup mapping between an address value and the index within the Actions List + * of the Action that is at that address. + */ + public Map addressToActionIndex; + + /** + * Whether the messages are crypted or not. + */ + private boolean messagesCrypted; + + /** + * Constructor for Logic. + * + * @param rawData + * @param messagesCrypted + */ + public Logic(byte[] rawData, boolean messagesCrypted) { + // A Logic is simply a collection of Actions and a collection of Messages. + this.actions = new ArrayList<>(); + this.messages = new ArrayList<>(); + this.addressToActionIndex = new HashMap<>(); + this.messagesCrypted = messagesCrypted; + + // Decode the raw LOGIC resource data into the Actions and Messages. + decode(rawData); + } + + /** + * Decode the raw LOGIC resource data into the Actions and Messages. + * + * @param rawData + */ + public void decode(byte[] rawData) { + // Read the Instructions. The first two bytes are the length of the Instructions section. + readActions(ByteBuffer.wrap(rawData, 2, ((rawData[0] & 0xFF) + ((rawData[1] & 0xFF) << 8)))); + + // Read the messages. + readMessages(rawData); + } + + /** + * Reads all Action commands from the given Stream. + * + * @param stream The Stream to read the Actions from. + */ + private void readActions(ByteBuffer stream) { + Action action; + int actionNumber = 0; + + while ((action = readAction(stream)) != null) { + actions.add(action); + addressToActionIndex.put(action.address, actionNumber++); + } + } + + /** + * Reads an Action from the given Stream. If the end of the Stream has been reached, + * will return null. + * + * @param stream The Stream to read the Action from. + * + * @return The Action that was read in, or null if the end of the Stream was reached. + */ + private Action readAction(ByteBuffer stream) { + Action action = null; + int address = stream.position(); + int actionOpcode = readUnsignedByte(stream); + + if (actionOpcode >= 0) { + if (actionOpcode == 0xFF) { // IF + List operands = new ArrayList(); + List conditions = new ArrayList(); + Condition condition = null; + + while ((condition = readCondition(stream, 0xFF)) != null) { + conditions.add(condition); + } + + operands.add(new Operand(OperandType.TESTLIST, conditions)); + operands.add(new Operand(OperandType.ADDRESS, + ((short)(readUnsignedByte(stream) + (readUnsignedByte(stream) << 8))) + + stream.position())); + action = new IfAction(operands); + } + else if (actionOpcode == 0xFE) { // GOTO + List operands = new ArrayList(); + operands.add(new Operand(OperandType.ADDRESS, + ((short)(readUnsignedByte(stream) + (readUnsignedByte(stream) << 8))) + + stream.position())); + action = new GotoAction(operands); + } + else { + // Otherwise it is a normal Action. + Operation operation = ACTION_OPERATIONS[actionOpcode]; + List operands = new ArrayList(); + + for (OperandType operandType : operation.operandTypes) { + operands.add(new Operand(operandType, readUnsignedByte(stream))); + } + + action = new Action(operation, operands); + } + + // Keep track of each Instruction's address and Logic as we read them in. + action.address = address; + action.logic = this; + } + + return action; + } + + /** + * Reads a Condition from the given Stream. If the first byte read matches the endCode, then + * we return null to indicate that there is no Condition to read. Conditions always appear + * within an IF block or an OR block, so the endCode will be either 0xFF (for if) or 0xFC (for + * or). + * + * @param stream The Stream to read from. + * @param endCode The code that we return null for. Will be either 0xFF or 0xFC. + * + * @return The Condition that was read if there was one to read; otherwise null if there wasn't one. + */ + private Condition readCondition(ByteBuffer stream, int endCode) { + Condition condition = null; + int address = (int)stream.position() - 2; + int conditionOpcode = readUnsignedByte(stream); + + if (conditionOpcode != endCode) { + if (conditionOpcode == 0xFC) { // OR + List operands = new ArrayList(); + List conditions = new ArrayList(); + Condition orCondition = null; + + while ((orCondition = readCondition(stream, 0xFC)) != null) { + conditions.add(orCondition); + } + + operands.add(new Operand(OperandType.TESTLIST, conditions)); + condition = new OrCondition(operands); + } + else if (conditionOpcode == 0xFD) { // NOT + List operands = new ArrayList(); + operands.add(new Operand(OperandType.TEST, readCondition(stream, 0xFF))); + condition = new NotCondition(operands); + } + else if (conditionOpcode == 0x0E) { // SAID + // The said command has a variable number of 16 bit word numbers, so needs special handling. + Operation operation = TEST_OPERATIONS[conditionOpcode]; + List operands = new ArrayList(); + List wordNumbers = new ArrayList(); + int numOfWords = readUnsignedByte(stream); + + for (int i=0; i < numOfWords; i++) { + wordNumbers.add(readUnsignedByte(stream) + (readUnsignedByte(stream) << 8)); + } + + operands.add(new Operand(OperandType.WORDLIST, wordNumbers)); + condition = new Condition(operation, operands); + } + else { + // Otherwise it's a normal condition. + Operation operation = TEST_OPERATIONS[conditionOpcode]; + List operands = new ArrayList(); + + for (OperandType operandType : operation.operandTypes) { // TODO: This is where the null reference is. + operands.add(new Operand(operandType, readUnsignedByte(stream))); + } + + condition = new Condition(operation, operands); + } + + // Keep track of each Instruction's address and Logic as we read them in. + condition.address = address; + condition.logic = this; + } + + return condition; + } + + /** + * Reads the Logic's messages from the raw data. + * + * @param rawData The raw data to read the messages from. + */ + private void readMessages(byte[] rawData) { + int messagesOffset = ((rawData[0] & 0xFF) + ((rawData[1] & 0xFF) << 8)) + 2; + int numOfMessages = (rawData[messagesOffset + 0] & 0xFF); + int startOfText = messagesOffset + 3 + (numOfMessages * 2); + + if (messagesCrypted) { + // Decrypt the message text section. + crypt(rawData, startOfText, rawData.length); + } + + // Message numbers start at 1, so we'll set index 0 to empty. + this.messages.add(""); + + // Add each message to the Messages List. + for (int messNum = 1, marker = messagesOffset + 3; messNum <= numOfMessages; messNum++, marker += 2) { + // Calculate the start of this message text. + int msgStart = (rawData[marker] & 0xFF) + ((rawData[marker + 1] & 0xFF) << 8); + String msgText = ""; + + // Message text will only exist for those where the start offset is greater than 0. + if (msgStart > 0) { + int msgEnd = (msgStart += (messagesOffset + 1)); + + // Find the end of the message text. It is 0 terminated. + while ((rawData[msgEnd++] & 0xFF) != 0) ; + + // Convert the byte data between the message start and end in to an ASCII string. + msgText = new String(rawData, msgStart, msgEnd - msgStart - 1, Charset.forName("Cp437")); + } + + this.messages.add(msgText); + } + } + + /** + * Reads a byte from the ByteBuffer then converts to unsigned int. + * + * @param byteBuffer ByteBuffer to read the byte from. + * + * @return An int containing the unsigned byte value, or -1 if end reached. + */ + private int readUnsignedByte(ByteBuffer byteBuffer) { + try { + return ((int)byteBuffer.get() & 0xFF); + } catch (BufferUnderflowException e) { + return -1; + } + } + + /** + * Represents an AGI Instruction, being an Operation and it's List of Operands. This class + * is abstract since all Instructions will be either an Action or a Condition. + */ + public abstract class Instruction { + + /** + * The Operation for this Instruction. + */ + public Operation operation; + + /** + * The List of Operands for this Instruction. + */ + public List operands; + + /** + * The address of this Instruction within the Logic file. + */ + public int address; + + /** + * Holds a reference to the Logic that this Instruction belongs to. + */ + public Logic logic; + + /** + * Constructor for Instruction. + * + * @param operation The Operation for this Instruction. + * @param operands The List of Operands for this Instruction. + */ + public Instruction(Operation operation, List operands) { + this.operation = operation; + this.operands = operands; + } + + public String toString() { + StringBuilder str = new StringBuilder(); + if (logic != null) { + str.append("LOGIC."); + str.append(logic.index); + str.append(": Address "); + str.append(address); + str.append(": "); + } + str.append(operation.format); + str.append(" ] "); + for (Operand operand : operands) { + str.append(operand.getValue().toString()); + str.append(", "); + } + return str.toString(); + } + } + + /** + * A Condition is a type of AGI Instruction that tests something and returns + * a boolean value. + */ + public class Condition extends Instruction { + public Condition(Operation operation, List operands) { + super(operation, operands); + } + } + + /** + * An Action is a type of AGI Instruction that performs an action. + */ + public class Action extends Instruction { + + public Action(Operation operation, List operands) { + super(operation, operands); + } + + /** + * Get the index of this Action within it's Logic's Action List. + */ + public int getActionNumber() { + return this.logic.addressToActionIndex.get(this.address); + } + } + + /** + * The JumpAction is an abstract base class of both the IfAction and GotoAction. + */ + public abstract class JumpAction extends Action { + + public JumpAction(Operation operation, List operands) { + super(operation, operands); + } + + /** + * Gets the index of the Action that this JumpAction jumps to. + * + * @return The index of the Action that this JumpAction jumps to. + */ + public int getDestinationActionIndex() { + return this.logic.addressToActionIndex.get(this.getDestinationAddress()); + } + + /** + * Gets the destination address of this JumpAction. + * + * @return The destination address of this JumpAction. + */ + public abstract int getDestinationAddress(); + } + + /** + * The IfAction is a special type of AGI Instruction that tests one or more Conditions + * to decide whether to jump over the block of immediately following Actions. It's operands + * are a List of Conditions and a jump address. + */ + public class IfAction extends JumpAction { + + public IfAction(List operands) { + super(new Operation(255, "if(TESTLIST,ADDRESS)", "InstructionIf"), operands); + } + + public int getDestinationAddress() { + return this.operands.get(1).asInt(); + } + } + + /** + * The GotoAction is a special type of AGI Instruction that performs an unconditional + * jump to a given address. It's one and only operand is the jump address. This Instruction + * is mainly used for the 'else' keyword, but also for the 'goto' keyword. + */ + public class GotoAction extends JumpAction { + + public GotoAction(List operands) { + super(new Operation(254, "goto(ADDRESS)", "InstructionGoto"), operands); + } + + public int getDestinationAddress() { + return this.operands.get(0).asInt(); + } + } + + /** + * The NotCondition is a special type of AGI Instruction that tests that the test + * command immediately following it evaluates to false. It's one and only operand will + * be a Condition, and that Condition cannot be an OrCondition. + */ + public class NotCondition extends Condition { + + public NotCondition(List operands) { + super(new Operation(253, "not(TEST)", "ExpressionNot"), operands); + } + } + + /** + * The OrCondition is a special type of AGI Instruction that tests two or more + * Conditions to see if at least one of them evaluates to true. It's operand is + * a List of Conditions. + */ + public class OrCondition extends Condition { + + public OrCondition(List operands) { + super(new Operation(252, "or(TESTLIST)", "ExpressionOr"), operands); + } + } + + /** + * An Instruction usually has one or more Operands, although there are some that don't. An + * Operand is of a particular OperandType and has a Value. + */ + public class Operand { + + public OperandType operandType; + + private Object value; + + /** + * Constructor for Operand. + * + * @param operandType The OperandType for this Operand. + * @param value The value for this Operand. + */ + public Operand(OperandType operandType, Object value) + { + this.operandType = operandType; + this.value = value; + } + + /** + * Gets the Operand's value as an int. + */ + public int asInt() { + return ((Number)value).intValue(); + } + + /** + * Gets the Operand's value as a short. + */ + public short asShort() { + return ((Number)value).shortValue(); + } + + /** + * Gets the Operand's value as unsigned byte. + */ + public int asByte() { + return (int)(((Number)value).intValue() & 0xFF); + } + + /** + * Gets the Operand's value as a signed byte. + */ + public byte asSByte() { + return ((Number)value).byteValue(); + } + + /** + * Gets the Operand's value as a Condition. + */ + public Condition asCondition() { + return (Condition)value; + } + + /** + * Gets the Operand's value as a List of Conditions. + */ + @SuppressWarnings("unchecked") + public List asConditions() { + return (List)value; + } + + @SuppressWarnings("unchecked") + public List asInts() { + return (List)value; + } + + public Object getValue() { + return value; + } + } + + /** + * The different types of Operand that the AGI Action and Condition instructions can have. + */ + public enum OperandType { + VAR, + NUM, + FLAG, + OBJECT, + WORDLIST, + VIEW, + MSGNUM, + TEST, + TESTLIST, + ADDRESS + } + + /** + * The Operation class represents an AGI command, e.g. the add operation, or isset + * operation. The distinction between the Operation class and the Instruction classes + * is that an Operation instance holds information about the AGI command, whereas the + * Instruction classes hold information about an instance of the usage of an AGI + * command. So the Operation instances are essentially reference data that is referenced + * by the Instructions. Multiple Instruction instances can and will refer to the same + * Operation. + */ + public static class Operation { + + /** + * The AGI opcode or bytecode value for this Operation. + */ + public int opcode; + + /** + * A format string that describes the name and arguments for this Operation. + */ + public String format; + + /** + * The name of this Operation, e.g. set.view + */ + public String name; + + /** + * The List of OperandTypes for this Operation. + */ + public List operandTypes; + + /** + * The name of the interpreter class that executes this Operation. + */ + public String executionClass; + + /** + * Constructor for Operation. + * + * @param opcode The AGI opcode or bytecode value for this Operation. + * @param format A format string that describes the name and arguments for this Operation. + * @param executionClass The name of the interpreter class that executes this Operation. + */ + public Operation(int opcode, String format, String executionClass) { + this.opcode = opcode; + this.format = format; + this.executionClass = executionClass; + this.operandTypes = new ArrayList(); + + // Work out the position of the two brackets in the format string. + int openBracket = format.indexOf("("); + int closeBracket = format.indexOf(")"); + + // The Name is the bit before the open bracket. + this.name = format.substring(0, openBracket); + + // If the brackets are not next to each other, the operation has operands. + if ((closeBracket - openBracket) > 1) { + String operandsStr = format.substring(openBracket + 1, closeBracket); + + for (String operandTypeStr : operandsStr.split(",")) { + OperandType operandType = OperandType.valueOf(operandTypeStr); + this.operandTypes.add(operandType); + } + } + } + } + + /** + * Static array of the AGI TEST Operations. + */ + private static Operation[] TEST_OPERATIONS = new Operation[] + { + null, + new Operation(1, "equaln(VAR,NUM)", "ExpressionEqual"), + new Operation(2, "equalv(VAR,VAR)", "ExpressionEqualV"), + new Operation(3, "lessn(VAR,NUM)", "ExpressionLess"), + new Operation(4, "lessv(VAR,VAR)", "ExpressionLessV"), + new Operation(5, "greatern(VAR,NUM)", "ExpressionGreater"), + new Operation(6, "greaterv(VAR,VAR)", "ExpressionGreaterV"), + new Operation(7, "isset(FLAG)", "ExpressionIsSet"), + new Operation(8, "isset.v(VAR)", "ExpressionIsSetV"), + new Operation(9, "has(OBJECT)", "ExpressionHas"), + new Operation(10, "obj.in.room(OBJECT,VAR)", "ExpressionObjInRoom"), + new Operation(11, "posn(OBJECT,NUM,NUM,NUM,NUM)", "ExpressionPosN"), + new Operation(12, "controller(NUM)", "ExpressionController"), + new Operation(13, "have.key()", "ExpressionHaveKey"), + new Operation(14, "said(WORDLIST)", "ExpressionSaid"), + new Operation(15, "compare.strings(NUM,NUM)", "ExpressionStringCompare"), + new Operation(16, "obj.in.box(OBJECT,NUM,NUM,NUM,NUM)", "ExpressionObjInBox"), + new Operation(17, "center.posn(OBJECT,NUM,NUM,NUM,NUM)", "ExpressionCentrePosition"), + new Operation(18, "right.posn(OBJECT,NUM,NUM,NUM,NUM)", "ExpressionRightPosition") + }; + + /** + * Adjusts the AGI command definitions to match the given AGI version. + * + * @param version The AGI version to adjust the command definitions to match. + */ + public static void AdjustCommandsForVersion(String version) + { + if (version.equals("2.089")) { + ACTION_OPERATIONS[134] = new Operation(134, "quit()", "InstructionQuit"); + } + } + + /** + * Static array of the AGI ACTION Operations. + */ + private static Operation[] ACTION_OPERATIONS = new Operation[] { + new Operation(0, "return()", "InstructionReturn"), + new Operation(1, "increment(VAR)", "InstructionIncrement"), + new Operation(2, "decrement(VAR)", "InstructionDecrement"), + new Operation(3, "assignn(VAR,NUM)", "InstructionAssign"), + new Operation(4, "assignv(VAR,VAR)", "InstructionAssignV"), + new Operation(5, "addn(VAR,NUM)", "InstructionAdd"), + new Operation(6, "addv(VAR,VAR)", "InstructionAddV"), + new Operation(7, "subn(VAR,NUM)", "InstructionSubstract"), + new Operation(8, "subv(VAR,VAR)", "InstructionSubstractV"), + new Operation(9, "lindirectv(VAR,VAR)", "InstructionIndirect"), + new Operation(10, "rindirect(VAR,VAR)", "InstructionIndirect"), + new Operation(11, "lindirectn(VAR,NUM)", "InstructionIndirect"), + new Operation(12, "set(FLAG)", "InstructionSet"), + new Operation(13, "reset(FLAG)", "InstructionReset"), + new Operation(14, "toggle(FLAG)", "InstructionToggle"), + new Operation(15, "set.v(VAR)", "InstructionSet"), + new Operation(16, "reset.v(VAR)", "InstructionReset"), + new Operation(17, "toggle.v(VAR)", "InstructionToggle"), + new Operation(18, "new.room(NUM)", "InstructionNewRoom"), + new Operation(19, "new.room.f(VAR)", "InstructionNewRoomV"), + new Operation(20, "load.logics(NUM)", "InstructionLoadLogic"), + new Operation(21, "load.logics.f(VAR)", "InstructionLoadLogicV"), + new Operation(22, "call(NUM)", "InstructionCall"), + new Operation(23, "call.f(VAR)", "InstructionCallV"), + new Operation(24, "load.pic(VAR)", "InstructionLoadPic"), + new Operation(25, "draw.pic(VAR)", "InstructionDrawPic"), + new Operation(26, "show.pic()", "InstructionShowPic"), + new Operation(27, "discard.pic(VAR)", "InstructionDiscardPic"), + new Operation(28, "overlay.pic(VAR)", "InstructionOverlayPic"), + new Operation(29, "show.pri.screen()", "InstructionShowPriScreen"), + new Operation(30, "load.view(VIEW)", "InstructionLoadView"), + new Operation(31, "load.view.f(VAR)", "InstructionLoadViewV"), + new Operation(32, "discard.view(VIEW)", "InstructionDiscardView"), + new Operation(33, "animate.obj(OBJECT)", "InstructionAnimateObject"), + new Operation(34, "unanimate.all()", "InstructionUnanimateAll"), + new Operation(35, "draw(OBJECT)", "InstructionDraw"), + new Operation(36, "erase(OBJECT)", "InstructionErase"), + new Operation(37, "position(OBJECT,NUM,NUM)", "InstructionPosition"), + new Operation(38, "position.f(OBJECT,VAR,VAR)", "InstructionPositionV"), + new Operation(39, "get.posn(OBJECT,VAR,VAR)", "InstructionGetPosition"), + new Operation(40, "reposition(OBJECT,VAR,VAR)", "InstructionReposition"), + new Operation(41, "set.view(OBJECT,VIEW)", "InstructionSetView"), + new Operation(42, "set.view.f(OBJECT,VAR)", "InstructionSetViewV"), + new Operation(43, "set.loop(OBJECT,NUM)", "InstructionSetLoop"), + new Operation(44, "set.loop.f(OBJECT,VAR)", "InstructionSetLoopV"), + new Operation(45, "fix.loop(OBJECT)", "InstructionFixLoop"), + new Operation(46, "release.loop(OBJECT)", "InstructionReleaseLoop"), + new Operation(47, "set.cel(OBJECT,NUM)", "InstructionSetCell"), + new Operation(48, "set.cel.f(OBJECT,VAR)", "InstructionSetCellV"), + new Operation(49, "last.cel(OBJECT,VAR)", "InstructionLastCell"), + new Operation(50, "current.cel(OBJECT,VAR)", "InstructionCurrentCell"), + new Operation(51, "current.loop(OBJECT,VAR)", "InstructionCurrentLoop"), + new Operation(52, "current.view(OBJECT,VAR)", "InstructionCurrentView"), + new Operation(53, "number.of.loops(OBJECT,VAR)", "InstructionLastLoop"), + new Operation(54, "set.priority(OBJECT,NUM)", "InstructionSetPriority"), + new Operation(55, "set.priority.f(OBJECT,VAR)", "InstructionSetPriorityV"), + new Operation(56, "release.priority(OBJECT)", "InstructionReleasePriority"), + new Operation(57, "get.priority(OBJECT,VAR)", "InstructionGetPriority"), + new Operation(58, "stop.update(OBJECT)", "InstructionStopUpdate"), + new Operation(59, "start.update(OBJECT)", "InstructionStartUpdate"), + new Operation(60, "force.update(OBJECT)", "InstructionForceUpdate"), + new Operation(61, "ignore.horizon(OBJECT)", "InstructionIgnoreHorizon"), + new Operation(62, "observe.horizon(OBJECT)", "InstructionObserveHorizon"), + new Operation(63, "set.horizon(NUM)", "InstructionSetHorizon"), + new Operation(64, "object.on.water(OBJECT)", "InstructionObjectOnWater"), + new Operation(65, "object.on.land(OBJECT)", "InstructionObjectOnLand"), + new Operation(66, "object.on.anything(OBJECT)", "InstructionObjectOnAnything"), + new Operation(67, "ignore.objs(OBJECT)", "InstructionIgnoreObjects"), + new Operation(68, "observe.objs(OBJECT)", "InstructionObserveObjects"), + new Operation(69, "distance(OBJECT,OBJECT,VAR)", "InstructionDistance"), + new Operation(70, "stop.cycling(OBJECT)", "InstructionStopCycling"), + new Operation(71, "start.cycling(OBJECT)", "InstructionStartCycling"), + new Operation(72, "normal.cycle(OBJECT)", "InstructionNormalCycling"), + new Operation(73, "end.of.loop(OBJECT,FLAG)", "InstructionEndOfLoop"), + new Operation(74, "reverse.cycle(OBJECT)", "InstructionReverseCycling"), + new Operation(75, "reverse.loop(OBJECT,FLAG)", "InstructionReverseLoop"), + new Operation(76, "cycle.time(OBJECT,VAR)", "InstructionCycleTime"), + new Operation(77, "stop.motion(OBJECT)", "InstructionStopMotion"), + new Operation(78, "start.motion(OBJECT)", "InstructionStartMotion"), + new Operation(79, "step.size(OBJECT,VAR)", "InstructionStepSize"), + new Operation(80, "step.time(OBJECT,VAR)", "InstructionStepTime"), + new Operation(81, "move.obj(OBJECT,NUM,NUM,NUM,FLAG)", "InstructionMoveObject"), + new Operation(82, "move.obj.f(OBJECT,VAR,VAR,VAR,FLAG)", "InstructionMoveObjectV"), + new Operation(83, "follow.ego(OBJECT,NUM,FLAG)", "InstructionFollowEgo"), + new Operation(84, "wander(OBJECT)", "InstructionWander"), + new Operation(85, "normal.motion(OBJECT)", "InstructionNormalMotion"), + new Operation(86, "set.dir(OBJECT,VAR)", "InstructionSetDir"), + new Operation(87, "get.dir(OBJECT,VAR)", "InstructionGetDir"), + new Operation(88, "ignore.blocks(OBJECT)", "InstructionIgnoreBlocks"), + new Operation(89, "observe.blocks(OBJECT)", "InstructionObserveBlocks"), + new Operation(90, "block(NUM,NUM,NUM,NUM)", "InstructionBlock"), + new Operation(91, "unblock()", "InstructionUnblock"), + new Operation(92, "get(OBJECT)", "InstructionGet"), + new Operation(93, "get.f(VAR)", "InstructionGetV"), + new Operation(94, "drop(OBJECT)", "InstructionDrop"), + new Operation(95, "put(OBJECT,VAR)", "InstructionPut"), + new Operation(96, "put.f(VAR,VAR)", "InstructionPutV"), + new Operation(97, "get.room.f(VAR,VAR)", "InstructionGetRoom"), + new Operation(98, "load.sound(NUM)", "InstructionLoadSound"), + new Operation(99, "sound(NUM,FLAG)", "InstructionPlaySound"), + new Operation(100, "stop.sound()", "InstructionStopSound"), + new Operation(101, "print(MSGNUM)", "InstructionPrint"), + new Operation(102, "print.f(VAR)", "InstructionPrintV"), + new Operation(103, "display(NUM,NUM,MSGNUM)", "InstructionDisplay"), + new Operation(104, "display.f(VAR,VAR,VAR)", "InstructionDisplayV"), + new Operation(105, "clear.lines(NUM,NUM,NUM)", "InstructionClearLine"), + new Operation(106, "text.screen()", "InstructionTextScreen"), + new Operation(107, "graphics()", "InstructionGraphics"), + new Operation(108, "set.cursor.char(MSGNUM)", "InstructionSetCursorChar"), + new Operation(109, "set.text.attribute(NUM,NUM)", "InstructionSetTextAttributes"), + new Operation(110, "shake.screen(NUM)", "InstructionShakeScreen"), + new Operation(111, "configure.screen(NUM,NUM,NUM)", "InstructionConfigureScreen"), + new Operation(112, "status.line.on()", "InstructionStatusLineOn"), + new Operation(113, "status.line.off()", "InstructionStatusLineOff"), + new Operation(114, "set.string(NUM,MSGNUM)", "InstructionSetString"), + new Operation(115, "get.string(NUM,MSGNUM,NUM,NUM,NUM)", "InstructionGetString"), + new Operation(116, "word.to.string(NUM,NUM)", "InstructionWordToString"), + new Operation(117, "parse(NUM)", "InstructionParse"), + new Operation(118, "get.num(MSGNUM,VAR)", "InstructionGetNum"), + new Operation(119, "prevent.input()", "InstructionPreventInput"), + new Operation(120, "accept.input()", "InstructionAcceptInput"), + new Operation(121, "set.key(NUM,NUM,NUM)", "InstructionSetKey"), + new Operation(122, "add.to.pic(VIEW,NUM,NUM,NUM,NUM,NUM,NUM)", "InstructionAddToPic"), + new Operation(123, "add.to.pic.f(VAR,VAR,VAR,VAR,VAR,VAR,VAR)", "InstructionAddToPicV"), + new Operation(124, "status()", "InstructionStatus"), + new Operation(125, "save.game()", "InstructionSaveGame"), + new Operation(126, "restore.game()", "InstructionRestoreGame"), + new Operation(127, "init.disk()", "InstructionInitDisk"), + new Operation(128, "restart.game()", "InstructionRestartGame"), + new Operation(129, "show.obj(VIEW)", "InstructionShowObject"), + new Operation(130, "random(NUM,NUM,VAR)", "InstructionRandom"), + new Operation(131, "program.control()", "InstructionProgramControl"), + new Operation(132, "player.control()", "InstructionPlayerControl"), + new Operation(133, "obj.status.f(VAR)", "InstructionObjectStatus"), + new Operation(134, "quit(NUM)", "InstructionQuit"), // Remove parameter for AGI v2.001/v2.089 + new Operation(135, "show.mem()", "InstructionShowMem"), + new Operation(136, "pause()", "InstructionPause"), + new Operation(137, "echo.line()", "InstructionEchoLine"), + new Operation(138, "cancel.line()", "InstructionCancelLine"), + new Operation(139, "init.joy()", "InstructionInitJoystick"), + new Operation(140, "toggle.monitor()", "InstructionToggleMonitor"), + new Operation(141, "version()", "InstructionVersion"), + new Operation(142, "script.size(NUM)", "InstructionSetScriptSize"), + new Operation(143, "set.game.id(MSGNUM)", "InstructionSetGameID"), // Command is max.drawn(NUM) for AGI v2.001 + new Operation(144, "log(MSGNUM)", "InstructionLog"), + new Operation(145, "set.scan.start()", "InstructionSetScanStart"), + new Operation(146, "reset.scan.start()", "InstructionSetScanStart"), + new Operation(147, "reposition.to(OBJECT,NUM,NUM)", "InstructionPosition"), + new Operation(148, "reposition.to.f(OBJECT,VAR,VAR)", "InstructionPositionV"), + new Operation(149, "trace.on()", "InstructionTraceOn"), + new Operation(150, "trace.info(NUM,NUM,NUM)", "InstructionTraceInfo"), + new Operation(151, "print.at(MSGNUM,NUM,NUM,NUM)", "InstructionPrintAt"), + new Operation(152, "print.at.v(VAR,NUM,NUM,NUM)", "InstructionPrintAtV"), + new Operation(153, "discard.view.v(VAR)", "InstructionDiscardView"), + new Operation(154, "clear.text.rect(NUM,NUM,NUM,NUM,NUM)", "InstructionClearTextRect"), + new Operation(155, "set.upper.left(NUM,NUM)", "InstructionUpperLeft"), + new Operation(156, "set.menu(MSGNUM)", "InstructionSetMenu"), + new Operation(157, "set.menu.item(MSGNUM,NUM)", "InstructionSetMenuItem"), + new Operation(158, "submit.menu()", "InstructionSubmitMenu"), + new Operation(159, "enable.item(NUM)", "InstructionEnableItem"), + new Operation(160, "disable.item(NUM)", "InstructionDisableItem"), + new Operation(161, "menu.input()", "InstructionMenuInput"), + new Operation(162, "show.obj.v(VAR)", "InstructionShowObject"), + new Operation(163, "open.dialogue()", "InstructionOpenDialogue"), + new Operation(164, "close.dialogue()", "InstructionCloseDialogue"), + new Operation(165, "mul.n(VAR,NUM)", "InstructionMultiply"), + new Operation(166, "mul.v(VAR,VAR)", "InstructionMultiplyV"), + new Operation(167, "div.n(VAR,NUM)", "InstructionDivide"), + new Operation(168, "div.v(VAR,VAR)", "InstructionDivideV"), + new Operation(169, "close.window()", "InstructionCloseWindow"), + new Operation(170, "set.simple(NUM)", "InstructionSetSimple"), + new Operation(171, "push.script()", "InstructionPushScript"), + new Operation(172, "pop.script()", "InstructionPopScript"), + new Operation(173, "hold.key()", "InstructionHoldKey"), + new Operation(174, "set.pri.base(NUM)", "InstructionSetPriorityBase"), + new Operation(175, "discard.sound(NUM)", "InstructionDiscardSound"), + new Operation(176, "hide.mouse()", "InstructionHideMouse"), + new Operation(177, "allow.menu(NUM)", "InstructionAllowMenu"), + new Operation(178, "show.mouse()", "InstructionShowMouse"), + new Operation(179, "fence.mouse(NUM,NUM,NUM,NUM)", "InstructionFenceMouse"), + new Operation(180, "mouse.posn(VAR,VAR)", "InstructionMousePosition"), + new Operation(181, "release.key()", "InstructionReleaseKey"), + new Operation(182, "adj.ego.move.to.x.y(NUM,NUM)", "InstructionAdjustEgoMoveToXY"), + new Operation(254, "goto(ADDRESS)", "InstructionGoto"), + new Operation(255, "if(TESTLIST,ADDRESS)", "InstructionIf") + }; +} diff --git a/core/src/main/java/com/agifans/agile/agilib/Objects.java b/core/src/main/java/com/agifans/agile/agilib/Objects.java new file mode 100644 index 0000000..2a64540 --- /dev/null +++ b/core/src/main/java/com/agifans/agile/agilib/Objects.java @@ -0,0 +1,115 @@ +package com.agifans.agile.agilib; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.sierra.agi.inv.InventoryObject; +import com.sierra.agi.inv.InventoryObjects; + +public class Objects extends Resource { + + public List objects; + + public int count() { return objects.size(); } + + public int numOfAnimatedObjects; + + public Objects(InventoryObjects jagiObjects) { + numOfAnimatedObjects = jagiObjects.getNumOfAnimatedObjects(); + objects = new ArrayList<>(); + + for (InventoryObject jagiObject : jagiObjects.getObjects()) { + objects.add(new Object(jagiObject.getName(), jagiObject.getLocation())); + } + } + + public Objects(Objects objects) { + this.numOfAnimatedObjects = objects.numOfAnimatedObjects; + this.objects = new ArrayList(); + for (Object obj : objects.objects) { + this.objects.add(new Object(obj.name, obj.room)); + } + } + + public byte[] encode() { + //MemoryStream stream = new MemoryStream(); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + // The first two bytes point the start of the object names. + int numOfObjects = this.objects.size(); + int startOfNames = numOfObjects * 3; + stream.write(startOfNames & 0xFF); + stream.write((startOfNames >> 8) & 0xFF); + + // Number of animated objects appears next. + stream.write(this.numOfAnimatedObjects); + + // Write out the name offsets and room numbers. + Map nameToOffsetMap = new HashMap<>(); + List distinctNames = new ArrayList<>(); + int nextNameOffset = startOfNames; + for (int i=0; i < numOfObjects; i++) + { + Object o = this.objects.get(i); + int nameOffset = nextNameOffset; + if (nameToOffsetMap.containsKey(o.name)) + { + // Reuse existing name offset if the name matches one we've already seen. + nameOffset = nameToOffsetMap.get(o.name); + } + else + { + // Otherwise use a new name slot. + nameToOffsetMap.put(o.name, nameOffset); + distinctNames.add(o.name); + nextNameOffset += (o.name.length() + 1); + } + stream.write(nameOffset & 0xFF); + stream.write((nameOffset >> 8) & 0xFF); + stream.write(o.room); + } + + // Write out the distinct names. + for (String name : distinctNames) + { + for (byte b : name.getBytes(Charset.forName("Cp437"))) + { + stream.write(b); + } + stream.write(0); + } + + byte[] rawData = stream.toByteArray(); + + // Encrypt the raw data if required. + // TODO: Don't think this is required. SavedGames handles crypt. + //if (crypted) + //{ + // crypt(rawData, 0, rawData.length); + //} + + return rawData; + } + + public static class Object { + + /** + * The name of the object. + */ + public String name; + + /** + * The room in which the object first appears in the game. + */ + public int room; + + public Object(String name, int room) { + this.name = name; + this.room = room; + } + } +} diff --git a/core/src/main/java/com/agifans/agile/agilib/Picture.java b/core/src/main/java/com/agifans/agile/agilib/Picture.java new file mode 100644 index 0000000..fec45da --- /dev/null +++ b/core/src/main/java/com/agifans/agile/agilib/Picture.java @@ -0,0 +1,52 @@ +package com.agifans.agile.agilib; + +import com.sierra.agi.pic.PictureContext; +import com.sierra.agi.pic.PictureException; + +/** + * A wrapper around the JAGI Picture to provide the methods that AGILE needs. + */ +public class Picture extends Resource { + + private com.sierra.agi.pic.Picture jagiPicture; + + private PictureContext jagiPictureContext; + + public Picture(com.sierra.agi.pic.Picture jagiPicture) { + this.jagiPicture = jagiPicture; + this.jagiPictureContext = new PictureContext(); + } + + public Picture clone() { + // It doesn't matter that we're using the same JAGI Picture. The actual + // drawing state is in the PictureContext, which will be a different + // instance. The JAGI Picture contains only the Vector of picture codes. + return new Picture(jagiPicture); + } + + public void drawPicture() { + drawPicture(jagiPictureContext); + } + + protected void drawPicture(PictureContext jagiPictureContext) { + try { + this.jagiPicture.draw(jagiPictureContext); + } catch (PictureException pe) { + throw new RuntimeException("Failed to draw JAGI Picture.", pe); + } + } + + public void overlayPicture(Picture picture) { + picture.drawPicture(jagiPictureContext); + } + + public int[] getVisualPixels() { + // This int array is already ARGB values. + return jagiPictureContext.getPictureData(); + } + + public int[] getPriorityPixels() { + // This int array has the priority values, 0, 1, 2, 3, ... (i.e. not ARGB) + return jagiPictureContext.getPriorityData(); + } +} diff --git a/core/src/main/java/com/agifans/agile/agilib/Resource.java b/core/src/main/java/com/agifans/agile/agilib/Resource.java new file mode 100644 index 0000000..b588c19 --- /dev/null +++ b/core/src/main/java/com/agifans/agile/agilib/Resource.java @@ -0,0 +1,28 @@ +package com.agifans.agile.agilib; + +public abstract class Resource { + + /** + * True if this resource has been "loaded" by the interpreter. + */ + public boolean isLoaded; + + public int index; + + /** + * Handles both the encrypt and decrypt operations. They're both the same, as the XOR is reversed + * if you do it a second time. + * + * @param rawData + * @param start + * @param end + */ + protected void crypt(byte[] rawData, int start, int end) { + int avisDurganPos = 0; + + for (int i = start; i < end; i++) { + rawData[i] ^= (byte)"Avis Durgan".charAt(avisDurganPos++ % 11); + } + } + +} diff --git a/core/src/main/java/com/agifans/agile/agilib/Sound.java b/core/src/main/java/com/agifans/agile/agilib/Sound.java new file mode 100644 index 0000000..9352f5c --- /dev/null +++ b/core/src/main/java/com/agifans/agile/agilib/Sound.java @@ -0,0 +1,96 @@ +package com.agifans.agile.agilib; + +import java.util.ArrayList; +import java.util.List; + +public class Sound extends Resource { + + /** + * The three tone channels. + */ + public List> notes; + + byte[] rawData = null; + + /** + * Constructor for Sound. + * + * @param rawData The raw encoded AGI SOUND data for this Sound. + */ + public Sound(byte[] rawData) { + this.notes = new ArrayList>(); + + for (int i=0; i < 4; i++) + { + this.notes.add(new ArrayList()); + } + + decode(rawData); + } + + public void decode(byte[] rawData) { + for (int n = 0; n < 4; n++) { + int start = (rawData[n * 2 + 0] & 0xFF) | ((rawData[n * 2 + 1] & 0xFF) << 8); + int end = (n < 3? (((rawData[n * 2 + 2] & 0xFF) | ((rawData[n * 2 + 3] & 0xFF) << 8)) - 5) : rawData.length); + + for (int pos = start; pos < end; pos += 5) { + Note note = new Note(n); + // TODO: Decide if byte array is appropriate in Java version. + byte[] noteData = new byte[5]; + noteData[0] = (byte)(pos + 0 < rawData.length ? rawData[pos + 0] : 0); + noteData[1] = (byte)(pos + 1 < rawData.length ? rawData[pos + 1] : 0); + noteData[2] = (byte)(pos + 2 < rawData.length ? rawData[pos + 2] : 0); + noteData[3] = (byte)(pos + 3 < rawData.length ? rawData[pos + 3] : 0); + noteData[4] = (byte)(pos + 4 < rawData.length ? rawData[pos + 4] : 0); + if (note.decode(noteData)) { + this.notes.get(n).add(note); + } + } + } + } + + public static class Note { + + public int voiceNum; + public int duration; + public double frequency; + public int volume; + public int origVolume; + public int frequencyCount; + public byte[] rawData = null; + + public Note(int voiceNum) { + this.voiceNum = voiceNum; + } + + public boolean decode(byte[] rawData) { + int duration = ((rawData[0] & 0xFF) | ((rawData[1] & 0xFF) << 8)); + if (duration == 0xFFFF) { + // Two 0xFF bytes in a row at this point ends the current voice. + return false; + } + else { + this.duration = duration; + this.frequencyCount = ((rawData[2] & 0x3F) << 4) + (rawData[3] & 0x0F); + this.origVolume = rawData[4] & 0x0F; + this.volume = 0x8; // Volume is set to 0 for PC version, so let's go with 8. + this.frequency = (frequencyCount > 0 ? 111860.0 / (double)frequencyCount : 0); + this.rawData = rawData; + return true; + } + } + + public byte[] encode() { + byte[] rawData = new byte[5]; + int freqdiv = (frequency == 0 ? 0 : (int)(111860 / frequency)); + // Note that the order of the first two bytes is switched around from how it is stored in an AGI SOUND. + rawData[0] = (byte)(duration & 0xFF); + rawData[1] = (byte)((duration >> 8) & 0xFF); + rawData[2] = (byte)((freqdiv >> 4) & 0x3F); + rawData[3] = (byte)(0x80 | ((voiceNum << 5) & 0x60) | (freqdiv & 0x0F)); + rawData[4] = (byte)(0x90 | ((voiceNum << 5) & 0x60) | (volume & 0x0F)); + this.rawData = rawData; + return rawData; + } + } +} diff --git a/core/src/main/java/com/agifans/agile/agilib/View.java b/core/src/main/java/com/agifans/agile/agilib/View.java new file mode 100644 index 0000000..a2c45bf --- /dev/null +++ b/core/src/main/java/com/agifans/agile/agilib/View.java @@ -0,0 +1,54 @@ +package com.agifans.agile.agilib; + +import java.util.ArrayList; + +public class View extends Resource { + + public ArrayList loops; + public String description; + + public View(com.sierra.agi.view.View jagiView) { + description = jagiView.getDescription(); + loops = new ArrayList<>(); + for (short loopNum = 0; loopNum < jagiView.getLoopCount(); loopNum++) { + loops.add(new Loop(jagiView.getLoop(loopNum))); + } + } + + public class Loop { + + public ArrayList cels; + + public Loop(com.sierra.agi.view.Loop jagiLoop) { + cels = new ArrayList<>(); + for (short cellNum = 0; cellNum < jagiLoop.getCellCount(); cellNum++) { + cels.add(new Cel(jagiLoop.getCell(cellNum))); + } + } + } + + public class Cel { + + private com.sierra.agi.view.Cel jagiCel; + + public Cel(com.sierra.agi.view.Cel jagiCel) { + this.jagiCel = jagiCel; + } + + public short getWidth() { + return jagiCel.getWidth(); + } + + public short getHeight() { + return jagiCel.getHeight(); + } + + public int[] getPixelData() { + return jagiCel.getPixelData(); + } + + public int getTransparentPixel() { + return jagiCel.getTransparentPixel(); + } + } +} diff --git a/core/src/main/java/com/agifans/agile/agilib/Words.java b/core/src/main/java/com/agifans/agile/agilib/Words.java new file mode 100644 index 0000000..5d50689 --- /dev/null +++ b/core/src/main/java/com/agifans/agile/agilib/Words.java @@ -0,0 +1,71 @@ +package com.agifans.agile.agilib; + +import java.util.HashMap; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Represents the AGI WORDS.TOK file. + * + * The following word numbers have special meaning. + * + * 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 + * ----- ----------------------------------------------------------- + * + * All other word numbers are free for use. + */ +public class Words extends Resource { + + /** + * A Map between a word's text and the word number for that word. + */ + public Map wordToNumber; + + /** + * A Map between a word number and the set of words that the word number is for (i.e. the synonym set). + */ + public Map> numberToWords; + + /** + * Constructor for Words. + * + * @param jagiWords The JAGI Words object to construct an AGILE Words object from. + */ + public Words(com.sierra.agi.word.Words jagiWords) { + this.wordToNumber = new HashMap(); + this.numberToWords = new HashMap>(); + for (com.sierra.agi.word.Word jagiWord : jagiWords.words()) { + addWord(jagiWord.number, jagiWord.text); + } + } + + /** + * Adds a new word for the given word text and word number. The word number does not need + * to be unique. When the word number is already in use, then the new word being added is + * a synonym for the existing word(s) using that word number. + * + * @param wordNum The word number for the word being added. + * @param wordText The word text for the word being added. + */ + public void addWord(int wordNum, String wordText) { + // Add a mapping from the word text to its word number. + this.wordToNumber.put(wordText, wordNum); + + // Add the word text to the set of words for the given word number. + SortedSet words; + if (this.numberToWords.containsKey(wordNum)) { + words = this.numberToWords.get(wordNum); + } + else { + words = new TreeSet(); + this.numberToWords.put(wordNum, words); + } + words.add(wordText); + } +} diff --git a/core/src/main/java/com/sierra/agi/awt/EgaUtils.java b/core/src/main/java/com/sierra/agi/awt/EgaUtils.java new file mode 100644 index 0000000..5ae1ac1 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/awt/EgaUtils.java @@ -0,0 +1,101 @@ +/* + * EgaUtil.java + * Adventure Game Interpreter AWT Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.awt; + +import java.awt.*; +import java.awt.image.ColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.DirectColorModel; +import java.awt.image.IndexColorModel; + +/** + * Misc. Utilities for EGA support in Java's AWT. + * + * @author Dr. Z + * @version 0.00.00.01 + */ +public abstract class EgaUtils { + + /** + * EGA Colors Red Band + */ + protected static final byte[] r = {(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xaa, (byte) 0xaa, (byte) 0xaa, (byte) 0xaa, (byte) 0x55, (byte) 0x55, (byte) 0x55, (byte) 0x55, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff}; + + /** + * EGA Colors Green Band + */ + protected static final byte[] g = {(byte) 0x00, (byte) 0x00, (byte) 0xaa, (byte) 0xaa, (byte) 0x00, (byte) 0x00, (byte) 0x55, (byte) 0xaa, (byte) 0x55, (byte) 0x55, (byte) 0xff, (byte) 0xff, (byte) 0x55, (byte) 0x55, (byte) 0xff, (byte) 0xff}; + + /** + * EGA Colors Blue Band + */ + protected static final byte[] b = {(byte) 0x00, (byte) 0xaa, (byte) 0x00, (byte) 0xaa, (byte) 0x00, (byte) 0xaa, (byte) 0x00, (byte) 0xaa, (byte) 0x55, (byte) 0xff, (byte) 0x55, (byte) 0xff, (byte) 0x55, (byte) 0xff, (byte) 0x55, (byte) 0xff}; + + /** + * EGA Color Model Cache + */ + protected static IndexColorModel indexModel; + + /** + * Native Color Model Cache + */ + protected static DirectColorModel nativeModel; + + /** + * Returns the ColorModel used by EGA Adapters. + *

+ * Used to convert visual resource from EGA Color Model to the + * Native Color Model. + */ + public static synchronized IndexColorModel getIndexColorModel() { + int i; + + if (indexModel == null) { + indexModel = new IndexColorModel(8, 16, r, g, b); + } + + return indexModel; + } + + /** + * Returns a ColorModel representing the nativiest ColorModel of the + * current system configuration. + *

+ * In order to reduce the number of ColorModel convertions, each visual + * resource is converted as soon as possible to this ColorModel. + */ + public static synchronized DirectColorModel getNativeColorModel() { + if (nativeModel == null) { + ColorModel model = Toolkit.getDefaultToolkit().getColorModel(); + DirectColorModel direct; + + if ((model.getTransferType() != DataBuffer.TYPE_INT) || + !(model instanceof DirectColorModel)) { + model = ColorModel.getRGBdefault(); + } + + if (model.getTransparency() != Transparency.OPAQUE) { + direct = (DirectColorModel) model; + model = new DirectColorModel( + direct.getColorSpace(), + direct.getPixelSize(), + direct.getRedMask(), + direct.getGreenMask(), + direct.getBlueMask(), + 0, + false, + DataBuffer.TYPE_INT); + } + + nativeModel = (DirectColorModel) model; + } + + return nativeModel; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/inv/InventoryObject.java b/core/src/main/java/com/sierra/agi/inv/InventoryObject.java new file mode 100644 index 0000000..b790e4f --- /dev/null +++ b/core/src/main/java/com/sierra/agi/inv/InventoryObject.java @@ -0,0 +1,32 @@ +/* + * InventoryObject.java + */ + +package com.sierra.agi.inv; + +/** + * @author Dr. Z + * @version 0.00.00.01 + */ +public final class InventoryObject { + /** + * Name + */ + public String name; + /** + * Location + */ + private final short location; + + public InventoryObject(short location) { + this.location = location; + } + + public String getName() { + return name; + } + + public short getLocation() { + return location; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/inv/InventoryObjects.java b/core/src/main/java/com/sierra/agi/inv/InventoryObjects.java new file mode 100644 index 0000000..a6dfe23 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/inv/InventoryObjects.java @@ -0,0 +1,242 @@ +/* + * InventoryObjects.java + */ + +package com.sierra.agi.inv; + +import com.sierra.agi.io.ByteCasterStream; +import com.sierra.agi.res.ResourceConfiguration; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +/** + * Stores Objects of the game. + *

+ * Object File Format
+ * The object file stores two bits of information about the inventory items used + * in an AGI game. The starting room location and the name of the inventory item. + * It also has a byte that determines the maximum number of animated objects. + *

+ * File Encryption
+ * The first obstacle to overcome is the fact that most object files are + * encrypted. I say most because some of the earlier AGI games were not, in + * which case you can skip to the next section. Those that are encrypted are done + * so with the string "Avis Durgan" (or, in case of AGDS games, "Alex Simkin"). + * The process of unencrypting the file is to simply taken every eleven bytes + * from the file and XOR each element of those eleven bytes with the corresponding + * element in the string "Avis Durgan". This sort of encryption is very easy to + * crack if you know what you are doing and is simply meant to act as a shield + * so as not to encourage cheating. In some games, however, the object names are + * clearly visible in the saved game files even when the object file is encrypted, + * so it's not a very effective shield. + *

+ * File Format
+ * + * + * + * + * + *
ByteMeaning
0-1Offset of the start of inventory item names
2Maximum number of animated objects
+ *

+ * Following the first three bytes as a section containing a three byte entry + * for each inventory item all of which conform to the following format: + *

+ * + * + * + * + * + *
ByteMeaning
0-1Offset of inventory item name i
2Starting room number for inventory item i or 255 carried
+ *

+ * Where i is the entry number starting at 0. All offsets are taken from the + * start of entry for inventory item 0 (not the start of the file). + *

+ * Then comes the textual names themselves. This is simply a list of NULL + * terminated strings. The offsets mentioned in the above section point to the + * first character in the string and the last character is the one before the + * 0x00. + * + * @author Dr. Z, Lance Ewing (Documentation) + * @version 0.00.00.01 + */ +public class InventoryObjects implements InventoryProvider { + /** + * Object list. + */ + protected InventoryObject[] objects = null; + protected int numOfAnimatedObjects; + + public InventoryObjects(ResourceConfiguration config) { + + } + + /** + * Loads the String Table from a AGI Object file. Internal Uses only. + * + * @param stream AGI Object file's Stream. + * @param offset Starting offset. + * @return Returns a Hashtable containing the strings with their offset has + * the Hash key. + * @throws IOException Caller must handle IOException from his stream. + */ + protected static Hashtable loadStringTable(InputStream stream, int offset) throws IOException { + Hashtable h = new Hashtable(64); + String o = ""; + int s = offset; + + while (true) { + int c = stream.read(); + offset++; + + if (c < 0) { + break; + } + + if (c == 0) { + h.put(Integer.valueOf(s), o); + o = ""; + s = offset; + } else { + o += (char) c; + } + } + + return h; + } + + /** + * Loads a AGI Object File from a stream. + * + * @param stream Stream where the Objects are contained. Must be a AGI + * compliant format. + * @return Returns the number of object contained in the stream. + * @throws IOException Caller must handle IOException from his stream. + */ + public InventoryObjects loadInventory(InputStream stream) throws IOException { + ByteCasterStream rawStream = new ByteCasterStream(stream); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(rawStream.readAllBytes()); + ByteCasterStream bstream = new ByteCasterStream(byteArrayInputStream); + + /* Calculate Inventory Object Count */ + int padSize = 3; + int nobject = bstream.lohiReadUnsignedShort(); + nobject /= padSize; + + this.objects = new InventoryObject[nobject]; + int[] offsets = new int[nobject]; + + + this.numOfAnimatedObjects = bstream.readUnsignedByte(); + + int offset = 0; + + for (int i = 0; i < nobject; i++) { + offsets[i] = bstream.lohiReadUnsignedShort(); + objects[i] = new InventoryObject(bstream.readUnsignedByte()); + offset += padSize; + } + + Hashtable hash = loadStringTable(byteArrayInputStream, offset); + + for (int i = 0; i < nobject; i++) { + objects[i].name = (String) hash.get(Integer.valueOf(offsets[i])); + } + + byteArrayInputStream.close(); + rawStream.close(); + bstream.close(); + return this; + } + + /** + * Returns the number of objects contained in this object. + * + * @return Returns the number of objects. + */ + public short getCount() { + return (short) objects.length; + } + + /** + * Returns an Object contained in this object based on his index. + * + * @param index Index number of the wanted object. + * @return Returns the wanted object. + */ + public InventoryObject getObject(short index) { + return objects[index]; + } + + public void resetLocationTable(short[] locations) { + int i; + + for (i = 0; i < objects.length; i++) { + locations[i] = objects[i].getLocation(); + } + } + + public InventoryObject[] getObjects() { + return objects; + } + + public int getNumOfAnimatedObjects() { + return numOfAnimatedObjects; + } + + public byte[] encode(short[] locations) throws Exception{ + // Recreate the Item Entries + // key = object name + // value = offset + Map itemEntries = new HashMap<>(); + // We need to preserve the order of the objects in the list + List itemList = new ArrayList<>(); + + int count = objects.length; + int num = count * 3; + + int offset = num; + for (int i = 0; i < count; i++) { + InventoryObject inventoryObject = objects[i]; + if (!itemEntries.containsKey(inventoryObject.name)) { + itemEntries.put(inventoryObject.name, offset); + itemList.add(inventoryObject); + // 1 = NUL char + offset = offset + (inventoryObject.name.length() + 1); + } + } + + // Dump of the in memory OBJECT file including updates made by get, put and drop commands + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write((num & 0xFF)); + outputStream.write((num >> 8) & 0xFF); + outputStream.write(numOfAnimatedObjects); + + for (int i = 0; i < count; i++) { + InventoryObject invObject = objects[i]; + short location = locations[i]; + + int itemOffset = itemEntries.get(invObject.name); + outputStream.write(itemOffset & 0xFF); + outputStream.write((itemOffset >> 8) & 0xFF); + outputStream.write(location); + } + + for (InventoryObject inventoryObject : itemList) { + byte[] nameBytes = inventoryObject.getName().getBytes(); + outputStream.write(nameBytes); + outputStream.write(0); + } + + byte[] buffer = outputStream.toByteArray(); + + return buffer; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/inv/InventoryProvider.java b/core/src/main/java/com/sierra/agi/inv/InventoryProvider.java new file mode 100644 index 0000000..7dc5d7d --- /dev/null +++ b/core/src/main/java/com/sierra/agi/inv/InventoryProvider.java @@ -0,0 +1,16 @@ +/** + * InventoryProvider.java + * Adventure Game Interpreter Inventory Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.inv; + +import java.io.IOException; +import java.io.InputStream; + +public interface InventoryProvider { + InventoryObjects loadInventory(InputStream in) throws IOException; +} diff --git a/core/src/main/java/com/sierra/agi/io/ByteCaster.java b/core/src/main/java/com/sierra/agi/io/ByteCaster.java new file mode 100644 index 0000000..d5a06b1 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/io/ByteCaster.java @@ -0,0 +1,49 @@ +/** + * ByteCaster.java + * Adventure Game Interpreter I/O Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.io; + +/** + * Interprets byte arrays. + * + * @author Dr. Z + * @version 0.00.00.01 + */ +abstract public class ByteCaster { + public static short hiloUnsignedByte(byte[] b, int off) { + return (short) (b[off] & 0xFF); + } + + public static int hiloUnsignedShort(byte[] b, int off) { + return ((b[off] & 0xFF) << 8) | + (b[off + 1] & 0xFF); + } + + public static long hiloUnsignedInt(byte[] b, int off) { + return ((long) (b[off] & 0xFF) << 24) | + ((b[off + 1] & 0xFF) << 16) | + ((b[off + 2] & 0xFF) << 8) | + (b[off + 3] & 0xFF); + } + + public static short lohiUnsignedByte(byte[] b, int off) { + return (short) (b[off] & 0xFF); + } + + public static int lohiUnsignedShort(byte[] b, int off) { + return ((b[off + 1] & 0xFF) << 8) | + (b[off] & 0xFF); + } + + public static long lohiUnsignedInt(byte[] b, int off) { + return ((long) (b[off + 3] & 0xFF) << 24) | + ((b[off + 2] & 0xFF) << 16) | + ((b[off + 1] & 0xFF) << 8) | + (b[off] & 0xFF); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/io/ByteCasterStream.java b/core/src/main/java/com/sierra/agi/io/ByteCasterStream.java new file mode 100644 index 0000000..3a07bf3 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/io/ByteCasterStream.java @@ -0,0 +1,76 @@ +/** + * ByteCasterStream.java + * Adventure Game Interpreter I/O Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.io; + +import java.io.EOFException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Interprets stream's data. + * + * @author Dr. Z + * @version 0.00.00.02 + */ +public class ByteCasterStream extends FilterInputStream { + public ByteCasterStream(InputStream in) { + super(in); + } + + public short readUnsignedByte() throws IOException { + int v = in.read(); + + if (v < 0) { + throw new EOFException(); + } + + return (short) v; + } + + public int hiloReadUnsignedShort() throws IOException { + byte[] b = new byte[2]; + + IOUtils.fill(in, b, 0, 2); + + return ((b[0] & 0xFF) << 8) | + (b[1] & 0xFF); + } + + public long hiloReadUnsignedInt() throws IOException { + byte[] b = new byte[4]; + + IOUtils.fill(in, b, 0, 4); + + return ((long) (b[0] & 0xFF) << 24) | + ((b[1] & 0xFF) << 16) | + ((b[2] & 0xFF) << 8) | + (b[3] & 0xFF); + } + + public int lohiReadUnsignedShort() throws IOException { + byte[] b = new byte[2]; + + IOUtils.fill(in, b, 0, 2); + + return ((b[1] & 0xFF) << 8) | + (b[0] & 0xFF); + } + + public long lohiReadUnsignedInt() throws IOException { + byte[] b = new byte[4]; + + IOUtils.fill(in, b, 0, 4); + + return ((long) (b[3] & 0xFF) << 24) | + ((b[2] & 0xFF) << 16) | + ((b[1] & 0xFF) << 8) | + (b[0] & 0xFF); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/io/CryptedInputStream.java b/core/src/main/java/com/sierra/agi/io/CryptedInputStream.java new file mode 100644 index 0000000..3811758 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/io/CryptedInputStream.java @@ -0,0 +1,114 @@ +/** + * CryptedInputStream.java + * Adventure Game Interpreter I/O Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.io; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Layer to support decryption of sierra's resources. + * (simple XOR cryption) + * + * @author Dr. Z + * @version 0.00.00.01 + */ +public class CryptedInputStream extends FilterInputStream { + /** Decryption key. */ + protected char[] key; + + /** Current offset. */ + protected int offset; + + /** Cryption begin offset. */ + protected int boffset; + + /** + * Offset of the last mark method call. + * + * @see #mark(int) + */ + protected int marked; + + /** + * Creates a new decryption layer. + * + * @param key Decryption key. + * @param stream InputStream to decrypt. + */ + public CryptedInputStream(InputStream in, String key) { + super(in); + this.key = key.toCharArray(); + } + + /** + * Creates a new decryption layer. + * + * @param key Decryption key. + * @param stream InputStream to decrypt. + */ + public CryptedInputStream(InputStream in, String key, int boffset) { + super(in); + this.boffset = boffset; + this.key = key.toCharArray(); + } + + public void mark(int readlimit) { + in.mark(readlimit); + marked = offset; + } + + public void reset() throws IOException { + in.reset(); + offset = marked; + } + + public long skip(long n) throws IOException { + long r = in.skip(n); + + offset += r; + return r; + } + + public int read() throws IOException { + int r = in.read(); + + if (r < 0) + return r; + + if (offset >= boffset) { + r ^= key[(offset - boffset) % key.length]; + } + + offset++; + return r; + } + + public int read(byte[] b, int off, int len) throws IOException { + int i, j, off2; + int r = in.read(b, off, len); + + if (r < 0) { + return r; + } + + off2 = off + len; + for (i = off; i < off2; i++) { + if (offset >= boffset) { + j = (b[i] & 0xFF); + j ^= key[(offset - boffset) % key.length]; + b[i] = (byte) j; + } + + offset++; + } + + return r; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/io/IOUtils.java b/core/src/main/java/com/sierra/agi/io/IOUtils.java new file mode 100644 index 0000000..50d1d69 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/io/IOUtils.java @@ -0,0 +1,50 @@ +/** + * IOUtils.java + * Adventure Game Interpreter I/O Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.io; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +public abstract class IOUtils { + public static int fill(InputStream in, byte[] b, int off, int len) throws IOException { + int c, r = 0; + + while (len != 0) { + c = in.read(b, off, len); + + if (c <= 0) { + throw new EOFException(); + } + + r += c; + off += c; + len -= c; + } + + return r; + } + + public static int skip(InputStream in, int len) throws IOException { + int c, r = 0; + + while (len != 0) { + c = (int) in.skip(len); + + if (c <= 0) { + throw new EOFException(); + } + + r += c; + len -= c; + } + + return r; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/io/LZWInputStream.java b/core/src/main/java/com/sierra/agi/io/LZWInputStream.java new file mode 100644 index 0000000..468eb35 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/io/LZWInputStream.java @@ -0,0 +1,232 @@ +/** + * LZWInputStream.java + * Adventure Game Interpreter I/O Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.io; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +/** + * LZW Decompressor. This class is a Input Stream Layer + * to uncompress on-the-fly resource stream using the LZW + * algorithm used in AGI v3. + * + * @author Dr. Z + * @version 0.00.00.01 + */ +public class LZWInputStream extends InputStream { + protected final static int MAX_BITS = 12; + protected final static int TABLE_SIZE = 18041; + protected final static int START_BITS = 9; + protected boolean endOfStream = false; + protected int bits; + protected int maxValues; + protected int maxCodes; + protected InputStream in; + protected byte[] appendChars = new byte[TABLE_SIZE]; + protected byte[] decodeStack = new byte[8192]; + protected int decodeStackSize = -1; + protected int[] prefixCode = new int[TABLE_SIZE]; + + protected int bitCount = 0; + protected long bitBuffer = 0; + protected int unext; + protected int unew; + protected int uold; + protected int ubits; + protected int uc; + /** Creates new LZWException */ + public LZWInputStream(InputStream in) throws IOException { + this.in = in; + ubits = setBits(START_BITS); + unext = 257; + uold = inputCode(); + uc = uold; + unew = inputCode(); + } + + protected int setBits(int value) { + if (value == MAX_BITS) { + return 1; + } + + bits = value; + maxValues = (1 << bits) - 1; + maxCodes = maxValues - 1; + return 0; + } + + protected int inputCode() throws IOException { + long b; + int r; + + long q = bitBuffer; + int s = bitCount; + + while (s <= 24) { + b = in.read(); + + if (b < 0) { + if (s == 0) { + throw new EOFException(); + } + + break; + } + + b <<= s; + q |= b; + s += 8; + } + + r = (int) (q & 0x7fff); + r %= (1 << bits); + + bitBuffer = (q >> bits); + bitCount = (s - bits); + + return r; + } + + protected int decodeString(int offset, int code) throws IOException { + int i; + + for (i = 0; code > 255; ) { + decodeStack[offset] = appendChars[code]; + offset++; + code = prefixCode[code]; + + if (i++ >= 4000) { + throw new IOException("LZW: Error in Code Expansion"); + } + } + + decodeStack[offset] = (byte) code; + return offset; + } + + protected void unpack() throws IOException { + if (endOfStream) { + return; + } + + if (decodeStackSize > 0) { + return; + } + + if (unew == 0x101) { + endOfStream = true; + return; + } + + if (unew == 0x100) { + unext = 258; + ubits = setBits(START_BITS); + uold = inputCode(); + uc = uold; + + decodeStack[0] = (byte) uc; + decodeStackSize = 0; + + unew = inputCode(); + } else { + if (unew >= unext) { + decodeStack[0] = (byte) uc; + decodeStackSize = decodeString(1, uold); + } else { + decodeStackSize = decodeString(0, unew); + } + + uc = decodeStack[decodeStackSize]; + + if (unext > maxCodes) { + ubits = setBits(bits + 1); + } + + prefixCode[unext] = uold; + appendChars[unext] = (byte) uc; + + unext++; + uold = unew; + + unew = inputCode(); + } + } + + public int read() throws IOException { + int c; + + while (decodeStackSize < 0) { + try { + unpack(); + } catch (EOFException eex) { + endOfStream = true; + } + + if (endOfStream) { + close(); + return -1; + } + } + + c = decodeStack[decodeStackSize]; + decodeStackSize--; + + return c; + } + + public int read(byte[] b, int off, int len) throws IOException { + int c = 0; + + while (!endOfStream) { + if (decodeStackSize >= 0) { + while ((decodeStackSize >= 0) && (len > 0)) { + b[off] = decodeStack[decodeStackSize]; + decodeStackSize--; + off++; + len--; + c++; + } + + if (len == 0) { + break; + } + } + + try { + unpack(); + } catch (EOFException eex) { + endOfStream = true; + } + } + + if (endOfStream) { + close(); + } + + if (c == 0) + c = -1; + + return c; + } + + public void close() throws IOException { + endOfStream = true; + + if (in != null) { + in.close(); + } + + /** Garbage Collector Optimization */ + in = null; + appendChars = null; + decodeStack = null; + prefixCode = null; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/io/LittleEndianOutputStream.java b/core/src/main/java/com/sierra/agi/io/LittleEndianOutputStream.java new file mode 100644 index 0000000..e71f5b8 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/io/LittleEndianOutputStream.java @@ -0,0 +1,137 @@ +package com.sierra.agi.io; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UTFDataFormatException; + +public class LittleEndianOutputStream extends FilterOutputStream { + protected int written; + + public LittleEndianOutputStream(OutputStream out) { + super(out); + } + + public void write(int b) throws IOException { + out.write(b); + written++; + } + + public void write(byte[] data, int offset, int length) + throws IOException { + out.write(data, offset, length); + written += length; + } + + public void writeBoolean(boolean b) throws IOException { + if (b) this.write(1); + else this.write(0); + } + + public void writeByte(int b) throws IOException { + out.write(b); + written++; + } + + public void writeShort(int s) throws IOException { + out.write(s & 0xFF); + out.write((s >>> 8) & 0xFF); + written += 2; + } + + public void writeChar(int c) throws IOException { + out.write(c & 0xFF); + out.write((c >>> 8) & 0xFF); + written += 2; + } + + public void writeInt(int i) throws IOException { + + out.write(i & 0xFF); + out.write((i >>> 8) & 0xFF); + out.write((i >>> 16) & 0xFF); + out.write((i >>> 24) & 0xFF); + written += 4; + + } + + public void writeLong(long l) throws IOException { + + out.write((int) l & 0xFF); + out.write((int) (l >>> 8) & 0xFF); + out.write((int) (l >>> 16) & 0xFF); + out.write((int) (l >>> 24) & 0xFF); + out.write((int) (l >>> 32) & 0xFF); + out.write((int) (l >>> 40) & 0xFF); + out.write((int) (l >>> 48) & 0xFF); + out.write((int) (l >>> 56) & 0xFF); + written += 8; + + } + + public final void writeFloat(float f) throws IOException { + this.writeInt(Float.floatToIntBits(f)); + } + + public final void writeDouble(double d) throws IOException { + this.writeLong(Double.doubleToLongBits(d)); + } + + public void writeBytes(String s) throws IOException { + int length = s.length(); + for (int i = 0; i < length; i++) { + out.write((byte) s.charAt(i)); + } + written += length; + } + + public void writeChars(String s) throws IOException { + int length = s.length(); + for (int i = 0; i < length; i++) { + int c = s.charAt(i); + out.write(c & 0xFF); + out.write((c >>> 8) & 0xFF); + } + written += length * 2; + } + + public void writeUTF(String s) throws IOException { + + int numchars = s.length(); + int numbytes = 0; + + for (int i = 0; i < numchars; i++) { + int c = s.charAt(i); + if ((c >= 0x0001) && (c <= 0x007F)) numbytes++; + else if (c > 0x07FF) numbytes += 3; + else numbytes += 2; + } + + if (numbytes > 65535) throw new UTFDataFormatException(); + + out.write((numbytes >>> 8) & 0xFF); + out.write(numbytes & 0xFF); + for (int i = 0; i < numchars; i++) { + int c = s.charAt(i); + if ((c >= 0x0001) && (c <= 0x007F)) { + out.write(c); + } else if (c > 0x07FF) { + out.write(0xE0 | ((c >> 12) & 0x0F)); + out.write(0x80 | ((c >> 6) & 0x3F)); + out.write(0x80 | (c & 0x3F)); + written += 2; + } else { + out.write(0xC0 | ((c >> 6) & 0x1F)); + out.write(0x80 | (c & 0x3F)); + written += 1; + } + } + + written += numchars + 2; + + } + + public int size() { + return this.written; + } +} diff --git a/core/src/main/java/com/sierra/agi/io/PictureInputStream.java b/core/src/main/java/com/sierra/agi/io/PictureInputStream.java new file mode 100644 index 0000000..9f175c0 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/io/PictureInputStream.java @@ -0,0 +1,117 @@ +/** + * PictureInputStream.java + * Adventure Game Interpreter I/O Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.io; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Picture Input Stream. + *

+ * Pictures in AGI version 3 use a simple form of compression to shrink their + * size by a tiny amount. It was obviously recognised by the interpreter coders + * that four bits were being wasted for picture codes 0xF0 and + * 0xF2. These are the two codes that change the visual and the + * priority colour respectively. Since there are only 16 colours, there need + * not be a whole byte set aside for storing the colour. All the picture + * compression does is store these colours in 4 bits rather than 8. + *

+ * Example:
+ * Original picture codes: F0 06 F8 12 45 F0 07 F2 05 F8 14 67 ...
+ * Compressed picture code: F0 6F 81 24 5F 07 F2 5F 81 46 7 ... + *

+ * + * @author Dr. Z + * @version 0.00.00.01 + */ +public class PictureInputStream extends FilterInputStream { + /** Previous Byte */ + protected int previous; + + /** Current Byte */ + protected int current; + + protected int mode = 0; + + /** + * Creates new Picture Input Stream + */ + public PictureInputStream(InputStream in) { + super(in); + } + + public int read(byte[] b, int off, int len) throws IOException { + int t = 0; + int n; + + while (len > 0) { + n = read(); + + if (n < 0) { + break; + } + } + + return t; + } + + public int read() throws IOException { + int x = 0, y; + + if (in == null) { + return -1; + } + + if (mode <= 1) { + current = in.read(); + + if (mode == 0) { + x = current; + } else { + x = (current & 0xf0); + x >>= 4; + y = (previous & 0x0f); + y <<= 4; + x |= y; + } + + if (x == 0xff) { + close(); + return -1; + } + + if (x == 0xf0 || x == 0xf2) { + if (mode == 1) { + mode = 2; + } else { + mode = 3; + } + } + } else if (mode == 2) { + mode = 0; + return current & 0x0f; + } else if (mode == 3) { + mode = 1; + current = in.read(); + x = current & 0xf0; + x >>= 4; + } + + previous = current; + return x; + } + + public void close() throws IOException { + if (in != null) { + in.close(); + in = null; + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/io/PublicByteArrayInputStream.java b/core/src/main/java/com/sierra/agi/io/PublicByteArrayInputStream.java new file mode 100644 index 0000000..6a4f47b --- /dev/null +++ b/core/src/main/java/com/sierra/agi/io/PublicByteArrayInputStream.java @@ -0,0 +1,50 @@ +package com.sierra.agi.io; + +import java.io.ByteArrayInputStream; + +/** + * A sub-class of ByteArrayInputStream that makes public some of the internal state + * of its super class, such as the count and pos. + * + * @author Lance Ewing + */ +public class PublicByteArrayInputStream extends ByteArrayInputStream { + + /** + * Constructor for PublicByteArrayInputStream. + * + * @param buf The byte array from which the InputStream is to be created. + */ + public PublicByteArrayInputStream(byte[] buf) { + super(buf); + } + + /** + * Constructor for PublicByteArrayInputStream. + * + * @param buf The input buffer. + * @param offset The offset in the buffer of the first byte to read. + * @param length The maximum number of bytes to read from the buffer. + */ + public PublicByteArrayInputStream(byte[] buf, int offset, int length) { + super(buf, offset, length); + } + + /** + * Gets the current position within the byte array. + * + * @return The current position within the byte array. + */ + public int getPosition() { + return this.pos; + } + + /** + * Gets the number of bytes in the byte array. + * + * @return The number of bytes in the byte array. + */ + public int getCount() { + return this.count; + } +} diff --git a/core/src/main/java/com/sierra/agi/io/SegmentedInputStream.java b/core/src/main/java/com/sierra/agi/io/SegmentedInputStream.java new file mode 100644 index 0000000..43ccf32 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/io/SegmentedInputStream.java @@ -0,0 +1,176 @@ +/** + * SegmentedInputStream.java + * Adventure Game Interpreter I/O Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; + +/** + * Implentation of InputStream that gives access + * to a part of a file. + * + * @author Dr. Z + * @version 0.00.00.01 + */ +public class SegmentedInputStream extends InputStream { + /** File pointer to the opened volume file. */ + protected RandomAccessFile randomFile; + + /** Current offset in the volume file. */ + protected int offset; + + /** Length of the resource data remaining. */ + protected int length; + + /** + * Offset of the last call to mark. + * + * @see #mark(int) + */ + protected int marked; + + /** + * Creates a new SegmentedInputStream. + * + * @param pEntry Pointer to a entry in the directory table. + * @throws IOException Can throw IOException. + */ + public SegmentedInputStream(RandomAccessFile file, int offset, int length) throws IOException { + this.offset = offset; + this.length = length; + this.randomFile = file; + this.marked = offset; + + randomFile.seek(offset); + } + + public int available() { + return length; + } + + public void close() throws IOException { + randomFile.close(); + randomFile = null; + } + + public void mark(int readlimit) { + marked = offset; + } + + public void reset() throws IOException { + int l = offset - marked; + + offset = marked; + length -= l; + + randomFile.seek(offset); + } + + public int diff() { + return offset - marked; + } + + public int getOffset() { + return offset; + } + + public RandomAccessFile getRandomAccessFile() { + return randomFile; + } + + /** + * Tests if this input stream supports the mark + * and reset methods. + *

+ * In this implentation of InputStream, it always + * returns true. + * + * @see #mark(int) + * @see #reset() + * @see java.io.InputStream + * @return Returns true + */ + public boolean markSupported() { + return true; + } + + public int read() throws IOException { + int r; + + if (length <= 0) + return -1; + + r = randomFile.read(); + + if (r >= 0) { + offset++; + length--; + } + + return r; + } + + public int read(byte[] b) throws IOException { + int r, l = b.length; + + if (length <= 0) { + return -1; + } + + if (l > length) { + l = length; + } + + r = randomFile.read(b, 0, l); + + if (r > 0) { + offset += r; + length -= r; + } + + return r; + } + + public int read(byte[] b, int off, int len) throws IOException { + int r; + + if (length <= 0) { + return -1; + } + + if (len > length) { + len = length; + } + + r = randomFile.read(b, off, len); + + if (r > 0) { + offset += r; + length -= r; + } + + return r; + } + + public long skip(long n) throws IOException { + if (length <= 0) { + return 0; + } + + if (n > length) { + n = length; + } + + offset += n; + length -= n; + randomFile.seek(offset); + return n; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/logic/Logic.java b/core/src/main/java/com/sierra/agi/logic/Logic.java new file mode 100644 index 0000000..8c8c8c6 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/logic/Logic.java @@ -0,0 +1,12 @@ +/** + * Logic.java + * Adventure Game Interpreter Logic Package + *

+ * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.logic; + +public interface Logic { +} diff --git a/core/src/main/java/com/sierra/agi/logic/LogicException.java b/core/src/main/java/com/sierra/agi/logic/LogicException.java new file mode 100644 index 0000000..0e6f9e3 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/logic/LogicException.java @@ -0,0 +1,20 @@ +package com.sierra.agi.logic; + +public class LogicException extends Exception { + /** + * Creates new LogicException without detail message. + */ + public LogicException() { + } + + /** + * Constructs a LogicException with the specified detail + * message. + * + * @param msg the detail message. + */ + public LogicException(String msg) { + super(msg); + } + +} diff --git a/core/src/main/java/com/sierra/agi/logic/LogicProvider.java b/core/src/main/java/com/sierra/agi/logic/LogicProvider.java new file mode 100644 index 0000000..8b1c980 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/logic/LogicProvider.java @@ -0,0 +1,16 @@ +/** + * LogicProvider.java + * Adventure Game Interpreter Logic Package + *

+ * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.logic; + +import java.io.IOException; +import java.io.InputStream; + +public interface LogicProvider { + Logic loadLogic(short logicNumber, InputStream inputStream, int size) throws IOException, LogicException; +} diff --git a/core/src/main/java/com/sierra/agi/pic/CorruptedPictureException.java b/core/src/main/java/com/sierra/agi/pic/CorruptedPictureException.java new file mode 100644 index 0000000..f521cf8 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/CorruptedPictureException.java @@ -0,0 +1,21 @@ +/* + * CorruptedPictureException.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +/** + * @author Dr. Z + * @version 0.00.00.01 + */ +public class CorruptedPictureException extends PictureException { + /** + * Creates new CorruptedPictureException without detail message. + */ + public CorruptedPictureException() { + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/pic/Picture.java b/core/src/main/java/com/sierra/agi/pic/Picture.java new file mode 100644 index 0000000..1fbf0ce --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/Picture.java @@ -0,0 +1,43 @@ +/* + * Picture.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +import java.util.Enumeration; +import java.util.Vector; + +/** + * @author Dr. Z + * @version 0.00.00.01 + */ +public class Picture { + protected Vector entries; + + /** + * Creates new Picture + */ + public Picture(Vector entries) { + this.entries = entries; + } + + public PictureContext draw() throws PictureException { + PictureContext pictureContext = new PictureContext(); + + draw(pictureContext); + return pictureContext; + } + + public void draw(PictureContext pictureContext) throws PictureException { + Enumeration en = entries.elements(); + + while (en.hasMoreElements()) { + PictureEntry entry = (PictureEntry) en.nextElement(); + entry.draw(pictureContext); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/pic/PictureContext.java b/core/src/main/java/com/sierra/agi/pic/PictureContext.java new file mode 100644 index 0000000..90e0648 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/PictureContext.java @@ -0,0 +1,310 @@ +/* + * PictureContext.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +import com.sierra.agi.awt.EgaUtils; + +import java.awt.Image; +import java.awt.Toolkit; +import java.awt.image.MemoryImageSource; +import java.util.Arrays; + +/** + * @author Dr. Z + * @version 0.00.00.01 + */ +public class PictureContext { + /** + * Picture Dimensions + */ + public int width = 160; + public int height = 168; + + /** + * Picture data. + */ + public int[] picData; + + /** + * Priority data. + */ + public int[] priData; + + /** + * Picture Picture Color. + */ + public int picColor = -1; + + /** + * Picture Priority Color. + */ + public byte priColor = -1; + + /** + * Pen Style + */ + public byte penStyle = 0; + + protected int[] pixel = new int[1]; + + protected int whitePixel; + + /** + * Creates new Picture Context. + */ + public PictureContext() { + picData = new int[width * height]; + priData = new int[width * height]; + + whitePixel = translatePixel((byte) 15); + + Arrays.fill(picData, whitePixel); + Arrays.fill(priData, 4); + } + + /** + * Clips a variable with a maximum. + * + * @param v Variable to be clipped. + * @param max Maximum value that the to be clipped variable can have. + * @return The Variable clipped. + */ + public static int clip(int v, int max) { + if (v > max) + v = max; + + return v; + } + + public int translatePixel(byte b) { + if (b == -1) { + return -1; + } else { + EgaUtils.getNativeColorModel().getDataElements(EgaUtils.getIndexColorModel().getRGB(b), pixel); + return pixel[0]; + } + } + + /** + * Obtain the index in the buffer where (x,y) is located. + * + * @param x X coordinate. + * @param y Y coordinate. + * @return Index in the buffer. + */ + public final int getIndex(int x, int y) { + return (y * width) + x; + } + + /** + * Obtain the color of the pixel asked. + * + * @param x X coordinate. + * @param y Y coordinate. + * @return Color at the specified pixel. + */ + public final int getPixel(int x, int y) { + return picData[(y * width) + x]; + } + + /** + * Obtain the priority of the pixel asked. + * + * @param x X coordinate. + * @param y Y coordinate. + * @return Priority at the specified pixel. + */ + public final int getPriorityPixel(int x, int y) { + return priData[(y * width) + x]; + } + + /** + * Set the (x,y) pixel to the current color and priority. + * + * @param x X coordinate. + * @param y Y coordinate. + * @see #picColor + * @see #priColor + */ + public final void putPixel(int x, int y) { + int i; + + if ((x >= width) || (y >= height)) { + return; + } + + i = (y * width) + x; + + if (picColor >= 0) { + picData[i] = picColor; + } + + if (priColor >= 0) { + priData[i] = priColor; + } + } + + /** + * Draw a line with current color and current priority. + * + * @param x1 Start X Coordinate. + * @param y1 Start Y Coordinate. + * @param x2 End X Coordinate. + * @param y2 End Y Coordinate. + * @see #picColor + * @see #priColor + * @see #putPixel(int, int) + */ + public void drawLine(int x1, int y1, int x2, int y2) { + int x, y; + + /* Clip! */ + x1 = clip(x1, width - 1); + x2 = clip(x2, width - 1); + y1 = clip(y1, height - 1); + y2 = clip(y2, height - 1); + + /* Vertical Line */ + if (x1 == x2) { + if (y1 > y2) { + y = y1; + y1 = y2; + y2 = y; + } + + for (; y1 <= y2; y1++) { + putPixel(x1, y1); + } + } + /* Horizontal Line */ + else if (y1 == y2) { + if (x1 > x2) { + x = x1; + x1 = x2; + x2 = x; + } + + for (; x1 <= x2; x1++) { + putPixel(x1, y1); + } + } else { + int deltaX = x2 - x1; + int deltaY = y2 - y1; + int stepX = 1; + int stepY = 1; + int detDelta; + int errorX; + int errorY; + int count; + + if (deltaY < 0) { + stepY = -1; + deltaY = -deltaY; + } + + if (deltaX < 0) { + stepX = -1; + deltaX = -deltaX; + } + + if (deltaY > deltaX) { + count = deltaY; + detDelta = deltaY; + errorX = deltaY / 2; + errorY = 0; + } else { + count = deltaX; + detDelta = deltaX; + errorX = 0; + errorY = deltaX / 2; + } + + x = x1; + y = y1; + putPixel(x, y); + + do { + errorY = (errorY + deltaY); + if (errorY >= detDelta) { + errorY -= detDelta; + y += stepY; + } + + errorX = (errorX + deltaX); + if (errorX >= detDelta) { + errorX -= detDelta; + x += stepX; + } + + putPixel(x, y); + count--; + } while (count > 0); + + putPixel(x, y); + } + } + + public boolean isFillCorrect(int x, int y) { + if ((picColor < 0) && (priColor < 0)) { + return false; + } + + if ((priColor < 0) && (picColor >= 0) && (picColor != whitePixel)) { + return (getPixel(x, y) == whitePixel); + } + + if ((priColor >= 0) && (picColor < 0) && (priColor != 4)) { + return (getPriorityPixel(x, y) == 4); + } + + return ((picColor >= 0) && (getPixel(x, y) == whitePixel) && (picColor != whitePixel)); + } + + protected Image loadImage(Toolkit toolkit, byte[] data) { + MemoryImageSource mis; + + if (toolkit == null) { + toolkit = Toolkit.getDefaultToolkit(); + } + + mis = new MemoryImageSource(width, height, EgaUtils.getIndexColorModel(), data, 0, width); + return toolkit.createImage(mis); + } + + protected Image loadImage(Toolkit toolkit, int[] data) { + MemoryImageSource mis; + + if (toolkit == null) { + toolkit = Toolkit.getDefaultToolkit(); + } + + mis = new MemoryImageSource(width, height, EgaUtils.getNativeColorModel(), data, 0, width); + return toolkit.createImage(mis); + } + + public Image getPictureImage(Toolkit toolkit) { + return loadImage(toolkit, picData); + } + + public Image getPriorityImage(Toolkit toolkit) { + byte[] asByte = new byte[priData.length]; + for (int i = 0; i < priData.length; i++) { + asByte[i] = (byte) priData[i]; + } + return loadImage(toolkit, asByte); + } + + public int[] getPictureData() { + return picData; + } + + public int[] getPriorityData() { + return priData; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/pic/PictureEntry.java b/core/src/main/java/com/sierra/agi/pic/PictureEntry.java new file mode 100644 index 0000000..5be569d --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/PictureEntry.java @@ -0,0 +1,13 @@ +/* + * PictureEntry.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +public abstract class PictureEntry { + public abstract void draw(PictureContext pictureContext); +} diff --git a/core/src/main/java/com/sierra/agi/pic/PictureEntryAbsLine.java b/core/src/main/java/com/sierra/agi/pic/PictureEntryAbsLine.java new file mode 100644 index 0000000..e17f280 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/PictureEntryAbsLine.java @@ -0,0 +1,44 @@ +/* + * PictureEntryAbsLine.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +import java.awt.*; +import java.util.Enumeration; + +/** + *

0xF6: Absolute line

+ *

+ * Function: Draws lines between points. The first two arguments are the + * starting coordinates. The remaining arguments are in groups of two which + * give the coordinates of the next location to draw a line to. There can be + * any number of arguments but there should always be an even number. + *

+ * Example: F6 30 50 34 51 38 53 F? + *

+ * This sequence draws a line from (48, 80) to (52, 81), and a line from + * (52, 81) to (56, 83). + *

+ */ +public class PictureEntryAbsLine extends PictureEntryMulti { + public void draw(PictureContext pictureContext) { + Enumeration en = points.elements(); + + Point p1 = (Point) en.nextElement(); + + if (points.size() == 1) { + pictureContext.drawLine(p1.x, p1.y, p1.x, p1.y); + } else { + while (en.hasMoreElements()) { + Point p2 = (Point) en.nextElement(); + pictureContext.drawLine(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + } + } +} diff --git a/core/src/main/java/com/sierra/agi/pic/PictureEntryChangePen.java b/core/src/main/java/com/sierra/agi/pic/PictureEntryChangePen.java new file mode 100644 index 0000000..9039f9b --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/PictureEntryChangePen.java @@ -0,0 +1,21 @@ +/* + * PictureEntryChangePen.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +public class PictureEntryChangePen extends PictureEntry { + protected byte penStyle; + + public PictureEntryChangePen(byte penStyle) { + this.penStyle = penStyle; + } + + public void draw(PictureContext pictureContext) { + pictureContext.penStyle = penStyle; + } +} diff --git a/core/src/main/java/com/sierra/agi/pic/PictureEntryChangePicColor.java b/core/src/main/java/com/sierra/agi/pic/PictureEntryChangePicColor.java new file mode 100644 index 0000000..985e9e6 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/PictureEntryChangePicColor.java @@ -0,0 +1,21 @@ +/* + * PictureEntryChangePicColor.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +public class PictureEntryChangePicColor extends PictureEntry { + protected byte picColor; + + public PictureEntryChangePicColor(byte picColor) { + this.picColor = picColor; + } + + public void draw(PictureContext pictureContext) { + pictureContext.picColor = pictureContext.translatePixel(picColor); + } +} diff --git a/core/src/main/java/com/sierra/agi/pic/PictureEntryChangePriColor.java b/core/src/main/java/com/sierra/agi/pic/PictureEntryChangePriColor.java new file mode 100644 index 0000000..08de391 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/PictureEntryChangePriColor.java @@ -0,0 +1,21 @@ +/* + * PictureEntryChangePicColor.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +public class PictureEntryChangePriColor extends PictureEntry { + protected byte priColor; + + public PictureEntryChangePriColor(byte priColor) { + this.priColor = priColor; + } + + public void draw(PictureContext pictureContext) { + pictureContext.priColor = priColor; + } +} diff --git a/core/src/main/java/com/sierra/agi/pic/PictureEntryDrawX.java b/core/src/main/java/com/sierra/agi/pic/PictureEntryDrawX.java new file mode 100644 index 0000000..f592328 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/PictureEntryDrawX.java @@ -0,0 +1,60 @@ +/* + * PictureEntryDrawX.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +import java.awt.*; +import java.util.Enumeration; + +/** + *

0xF5: Draw an X corner

+ * + *

Function: The first two arguments for this action are the coordinates of + * the starting position on the screen in the order x and then y. The remaining + * arguments are in the order x1, y1, x2, y2, ... + *

+ * Note that the x component is the first to be changed and also note that this + * action does not necessarily end on either component, it just ends when the + * next byte of 0xF0 or above is encountered. A line is drawn after each byte + * is processed. + *

+ * Example: F5 16 16 18 12 16 F? + *

+ * (0x16, 0x12)   (0x18, 0x12)
+ * EXX
+ * X            S = Start
+ * X            E = End
+ * X            X = normal piXel
+ * SXX
+ * (0x16, 0x16)   (0x18, 0x16)
+ */ +public class PictureEntryDrawX extends PictureEntryMulti { + public void draw(PictureContext pictureContext) { + Enumeration en = points.elements(); + int x1, y1, x2, y2; + boolean b = true; + Point p; + + p = (Point) en.nextElement(); + x1 = x2 = p.x; + y1 = y2 = p.y; + + while (en.hasMoreElements()) { + if (b) { + x2 = ((Integer) en.nextElement()).intValue(); + } else { + y2 = ((Integer) en.nextElement()).intValue(); + } + + pictureContext.drawLine(x1, y1, x2, y2); + x1 = x2; + y1 = y2; + b = !b; + } + } +} diff --git a/core/src/main/java/com/sierra/agi/pic/PictureEntryDrawY.java b/core/src/main/java/com/sierra/agi/pic/PictureEntryDrawY.java new file mode 100644 index 0000000..3c7243d --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/PictureEntryDrawY.java @@ -0,0 +1,58 @@ +/* + * PictureEntryDrawY.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +import java.awt.*; +import java.util.Enumeration; + +/** + *

0xF4: Draw a Y corner

+ *

+ * Function: The first two arguments for this action are the coordinates of + * the starting position on the screen in the order x and then y. The remaining + * arguments are in the order y1, x1, y2, x2, ... + *

+ * Note that the y component is the first to be changed and also note that this + * action does not necessarily end on either component, it just ends when the + * next byte of 0xF0 or above is encountered. A line is drawn after each byte + * is processed. + *

+ * Example: F4 16 16 18 12 16 F? + *

+ * (0x12, 0x16)     (0x16, 0x16)
+ * E   S                  S = Start
+ * X   X                  E = End
+ * XXXXX                  X = normal piXel
+ * (0x12, 0x18)     (0x16, 0x18)

+ */ +public class PictureEntryDrawY extends PictureEntryMulti { + public void draw(PictureContext pictureContext) { + Enumeration en = points.elements(); + int x1, y1, x2, y2; + boolean b = true; + Point p; + + p = (Point) en.nextElement(); + x1 = x2 = p.x; + y1 = y2 = p.y; + + while (en.hasMoreElements()) { + if (b) { + y2 = ((Integer) en.nextElement()).intValue(); + } else { + x2 = ((Integer) en.nextElement()).intValue(); + } + + pictureContext.drawLine(x1, y1, x2, y2); + x1 = x2; + y1 = y2; + b = !b; + } + } +} diff --git a/core/src/main/java/com/sierra/agi/pic/PictureEntryFill.java b/core/src/main/java/com/sierra/agi/pic/PictureEntryFill.java new file mode 100644 index 0000000..74b0933 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/PictureEntryFill.java @@ -0,0 +1,141 @@ +/* + * PictureEntryFill.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +import java.awt.*; +import java.util.EmptyStackException; +import java.util.Enumeration; + +/** + *

0xF8: Fill

+ *

+ * Function: Flood fill from the locations given. Arguments are given in groups + * of two bytes which give the coordinates of the location to start the fill + * at. If picture drawing is enabled then it flood fills from that location on + * the picture screen to all pixels locations that it can reach which are white + * in colour. The boundary is given by any pixels which are not white. + *

+ * If priority drawing is enabled, and picture drawing is not enabled, then it + * flood fills from that location on the priority screen to all pixels that it + * can reach which are red in colour. The boundary in this case is given by any + * pixels which are not red. + *

+ * If both picture drawing and priority drawing are enabled, then a flood fill + * naturally enough takes place on both screens. In this case there is a + * difference in the way the fill takes place in the priority screen. The + * difference is that it not only looks for its own boundary, but also stops if + * it reaches a boundary that exists in the picture screen but does not + * necessarily exist in the priority screen. + *

+ */ +public class PictureEntryFill extends PictureEntryMulti { + public void draw(PictureContext pictureContext) { + Point current; + Enumeration en = points.elements(); + PointStack stack = new PointStack(200, 200); + int width = pictureContext.width - 1; + int height = pictureContext.height - 1; + + while (en.hasMoreElements()) { + current = (Point) en.nextElement(); + + stack.push(current.x, current.y); + + try { + while (true) { + stack.pop(current); + + if (pictureContext.isFillCorrect(current.x, current.y)) { + pictureContext.putPixel(current.x, current.y); + + if (current.x > 0 && pictureContext.isFillCorrect(current.x - 1, current.y)) { + stack.push(current.x - 1, current.y); + } + + if (current.x < width && pictureContext.isFillCorrect(current.x + 1, current.y)) { + stack.push(current.x + 1, current.y); + } + + if (current.y < height && pictureContext.isFillCorrect(current.x, current.y + 1)) { + stack.push(current.x, current.y + 1); + } + + if (current.y > 0 && pictureContext.isFillCorrect(current.x, current.y - 1)) { + stack.push(current.x, current.y - 1); + } + } + } + } catch (EmptyStackException esex) { + } + } + } + + public static class PointStack { + protected int increment; + protected int elementCount; + protected short[] x; + protected short[] y; + + /** + * Creates new Point Stack + */ + public PointStack() { + increment = 15; + } + + public PointStack(int initialSize, int increment) { + this.increment = increment; + ensureCapacity(initialSize); + } + + public void pop(Point pt) { + if (elementCount == 0) { + throw new EmptyStackException(); + } + + elementCount--; + pt.x = x[elementCount]; + pt.y = y[elementCount]; + } + + public void push(int x, int y) { + ensureCapacity(elementCount + 1); + + this.x[elementCount] = (short) x; + this.y[elementCount] = (short) y; + elementCount++; + } + + public void clear() { + elementCount = 0; + } + + public void ensureCapacity(int minCapacity) { + if (x == null) { + x = new short[minCapacity + increment]; + y = new short[minCapacity + increment]; + } else if (x.length < minCapacity) { + short[] nx = new short[minCapacity + increment]; + short[] ny = new short[minCapacity + increment]; + int i, l = elementCount; + + for (i = 0; i < l; i++) { + nx[i] = x[i]; + } + + for (i = 0; i < l; i++) { + ny[i] = y[i]; + } + + x = nx; + y = ny; + } + } + } +} diff --git a/core/src/main/java/com/sierra/agi/pic/PictureEntryMulti.java b/core/src/main/java/com/sierra/agi/pic/PictureEntryMulti.java new file mode 100644 index 0000000..40122de --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/PictureEntryMulti.java @@ -0,0 +1,28 @@ +/* + * PictureEntryMulti.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +import java.awt.*; +import java.util.Vector; + +public abstract class PictureEntryMulti extends PictureEntry { + protected Vector points = new Vector(); + + public void add(int x, int y) { + points.add(new Point(x, y)); + } + + public void add(int c) { + points.add(Integer.valueOf(c)); + } + + public void add(int[] c) { + points.add(c); + } +} diff --git a/core/src/main/java/com/sierra/agi/pic/PictureEntryPlot.java b/core/src/main/java/com/sierra/agi/pic/PictureEntryPlot.java new file mode 100644 index 0000000..c4c46ad --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/PictureEntryPlot.java @@ -0,0 +1,164 @@ +/* + * PictureEntryPlot.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +import java.awt.*; +import java.util.Enumeration; + +public class PictureEntryPlot extends PictureEntryMulti { + /** + * Circle Bitmaps + */ + protected static final short[][] circles = new short[][] + { + {0x80}, + {0xfc}, + {0x5f, 0xf4}, + {0x66, 0xff, 0xf6, 0x60}, + {0x23, 0xbf, 0xff, 0xff, 0xee, 0x20}, + {0x31, 0xe7, 0x9e, 0xff, 0xff, 0xde, 0x79, 0xe3, 0x00}, + {0x38, 0xf9, 0xf3, 0xef, 0xff, 0xff, 0xff, 0xfe, 0xf9, 0xf3, 0xe3, 0x80}, + {0x18, 0x3c, 0x7e, 0x7e, 0x7e, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7e, 0x7e, 0x7e, 0x3c, 0x18} + }; + + /** + * Splatter Brush Bitmaps + */ + protected static final short[] splatterMap = new short[] + { + 0x20, 0x94, 0x02, 0x24, 0x90, 0x82, 0xa4, 0xa2, + 0x82, 0x09, 0x0a, 0x22, 0x12, 0x10, 0x42, 0x14, + 0x91, 0x4a, 0x91, 0x11, 0x08, 0x12, 0x25, 0x10, + 0x22, 0xa8, 0x14, 0x24, 0x00, 0x50, 0x24, 0x04 + }; + + /** + * Starting Bit Position + */ + protected static final short[] splatterStart = new short[] + { + 0x00, 0x18, 0x30, 0xc4, 0xdc, 0x65, 0xeb, 0x48, + 0x60, 0xbd, 0x89, 0x05, 0x0a, 0xf4, 0x7d, 0x7d, + 0x85, 0xb0, 0x8e, 0x95, 0x1f, 0x22, 0x0d, 0xdf, + 0x2a, 0x78, 0xd5, 0x73, 0x1c, 0xb4, 0x40, 0xa1, + 0xb9, 0x3c, 0xca, 0x58, 0x92, 0x34, 0xcc, 0xce, + 0xd7, 0x42, 0x90, 0x0f, 0x8b, 0x7f, 0x32, 0xed, + 0x5c, 0x9d, 0xc8, 0x99, 0xad, 0x4e, 0x56, 0xa6, + 0xf7, 0x68, 0xb7, 0x25, 0x82, 0x37, 0x3a, 0x51, + 0x69, 0x26, 0x38, 0x52, 0x9e, 0x9a, 0x4f, 0xa7, + 0x43, 0x10, 0x80, 0xee, 0x3d, 0x59, 0x35, 0xcf, + 0x79, 0x74, 0xb5, 0xa2, 0xb1, 0x96, 0x23, 0xe0, + 0xbe, 0x05, 0xf5, 0x6e, 0x19, 0xc5, 0x66, 0x49, + 0xf0, 0xd1, 0x54, 0xa9, 0x70, 0x4b, 0xa4, 0xe2, + 0xe6, 0xe5, 0xab, 0xe4, 0xd2, 0xaa, 0x4c, 0xe3, + 0x06, 0x6f, 0xc6, 0x4a, 0xa4, 0x75, 0x97, 0xe1 + }; + + public void draw(PictureContext pictureContext) { + if ((pictureContext.penStyle & 0x20) == 0x20) { + drawPlot(pictureContext); + } else { + drawPoints(pictureContext); + } + } + + public void drawPlot(PictureContext pictureContext) { + Enumeration en = points.elements(); + int circlePos = 0; + int bitPos; + int x, y, x1, y1, penSize, penSizeTrue; + boolean circle; + int[] p; + + circle = !((pictureContext.penStyle & 0x10) == 0x10); + penSize = (pictureContext.penStyle & 0x07); + penSizeTrue = penSize; + + while (en.hasMoreElements()) { + p = (int[]) en.nextElement(); + circlePos = 0; + bitPos = splatterStart[p[0]]; + x = p[1]; + y = p[2]; + + if (x < penSize) { + x = penSize - 1; + } + + if (y < penSize) { + y = penSize; + } + + for (y1 = y - penSize; y1 <= y + penSize; y1++) { + for (x1 = x - (penSize + 1) / 2; x1 <= x + penSize / 2; x1++) { + if (circle) { + if (!(((circles[penSizeTrue][circlePos >> 0x3] >> (0x7 - (circlePos & 0x7))) & 0x1) == 0x1)) { + circlePos++; + continue; + } + + circlePos++; + } + + if (((splatterMap[bitPos >> 3] >> (7 - (bitPos & 7))) & 1) == 1) { + pictureContext.putPixel(x1, y1); + } + + bitPos++; + + if (bitPos == 0xff) { + bitPos = 0; + } + } + } + } + } + + public void drawPoints(PictureContext pictureContext) { + Enumeration en = points.elements(); + int circlePos; + int x, y, x1, y1, penSize, penSizeTrue; + boolean circle; + Point p; + + circle = !((pictureContext.penStyle & 0x10) == 0x10); + penSize = (pictureContext.penStyle & 0x07); + penSizeTrue = penSize; + + while (en.hasMoreElements()) { + p = (Point) en.nextElement(); + x = p.x; + y = p.y; + circlePos = 0; + + if (x < penSize) { + x = penSize - 1; + } + + if (y < penSize) { + y = penSize; + } + + for (y1 = y - penSize; y1 <= y + penSize; y1++) { + for (x1 = x - (penSize + 1) / 2; x1 <= x + penSize / 2; x1++) { + if (circle) { + if (!(((circles[penSizeTrue][circlePos >> 0x3] >> (0x7 - (circlePos & 0x7))) & 0x1) == 0x1)) { + circlePos++; + continue; + } + + circlePos++; + } + + pictureContext.putPixel(x1, y1); + } + } + } + } +} diff --git a/core/src/main/java/com/sierra/agi/pic/PictureEntryRelLine.java b/core/src/main/java/com/sierra/agi/pic/PictureEntryRelLine.java new file mode 100644 index 0000000..49df638 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/PictureEntryRelLine.java @@ -0,0 +1,66 @@ +/* + * PictureEntryRelLine.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +import java.awt.*; +import java.util.Enumeration; + +/** + *

0xF7: Relative line

+ *

+ * Function: Draw short relative lines. By relative we mean that the data gives + * displacements which are relative from the current location. The first + * argument gives the standard starting coordinates. All the arguments which + * follow these first two are of the following format: + *

+ * +---+-----------+---+-----------+
+ * | S |   Xdisp   | S |   Ydisp   |
+ * +---+---+---+---+---+---+---+---+
+ * | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+ * +---+---+---+---+---+---+---+---+
+ *

+ * This gives a displacement range of between -7 and +7 for both the X and the Y + * direction. + *

+ * Example: F7 10 10 22 40 06 CC F? + *

+ * S
+ * +              S = Start
+ * X+++X         X = End of each line
+ * +         + = pixels in each line
+ * E   +         E = End
+ * +  +
+ * + +         Remember that CC = (x-4, y-4).
+ * ++
+ * X
+ */ + +public class PictureEntryRelLine extends PictureEntryMulti { + public void draw(PictureContext pictureContext) { + Enumeration en = points.elements(); + Point p; + int x1, y1, x2, y2; + + p = (Point) en.nextElement(); + x1 = x2 = p.x; + y1 = y2 = p.y; + + pictureContext.putPixel(x1, y1); + + while (en.hasMoreElements()) { + p = (Point) en.nextElement(); + x2 += p.x; + y2 += p.y; + + pictureContext.drawLine(x1, y1, x2, y2); + x1 = x2; + y1 = y2; + } + } +} diff --git a/core/src/main/java/com/sierra/agi/pic/PictureException.java b/core/src/main/java/com/sierra/agi/pic/PictureException.java new file mode 100644 index 0000000..adcdee4 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/PictureException.java @@ -0,0 +1,31 @@ +/* + * PictureException.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +/** + * @author Dr. Z + * @version 0.00.00.01 + */ +public class PictureException extends Exception { + /** + * Creates new PictureException without detail message. + */ + public PictureException() { + } + + /** + * Constructs an PictureException with the specified detail + * message. + * + * @param msg the detail message. + */ + public PictureException(String msg) { + super(msg); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/pic/PictureProvider.java b/core/src/main/java/com/sierra/agi/pic/PictureProvider.java new file mode 100644 index 0000000..22a742c --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/PictureProvider.java @@ -0,0 +1,16 @@ +/* + * PictureProvider.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +import java.io.IOException; +import java.io.InputStream; + +public interface PictureProvider { + Picture loadPicture(InputStream inputStream) throws IOException, PictureException; +} diff --git a/core/src/main/java/com/sierra/agi/pic/StandardPictureProvider.java b/core/src/main/java/com/sierra/agi/pic/StandardPictureProvider.java new file mode 100644 index 0000000..0e555fc --- /dev/null +++ b/core/src/main/java/com/sierra/agi/pic/StandardPictureProvider.java @@ -0,0 +1,414 @@ +/* + * StandardPictureProvider.java + * Adventure Game Interpreter Picture Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.pic; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Vector; + +public class StandardPictureProvider implements PictureProvider { + protected static final short CMD_START = (short) 0xF0; + + protected static final short CMD_CHANGEPICCOLOR = (short) 0xF0; + protected static final short CMD_DISABLEPICDRAW = (short) 0xF1; + protected static final short CMD_CHANGEPRICOLOR = (short) 0xF2; + protected static final short CMD_DISABLEPRIDRAW = (short) 0xF3; + protected static final short CMD_DRAWYCORNER = (short) 0xF4; + protected static final short CMD_DRAWXCORNER = (short) 0xF5; + protected static final short CMD_DRAWABSLINE = (short) 0xF6; + protected static final short CMD_DRAWRELLINE = (short) 0xF7; + protected static final short CMD_FILL = (short) 0xF8; + protected static final short CMD_CHANGEPEN = (short) 0xF9; + protected static final short CMD_PLOT = (short) 0xFA; + protected static final short CMD_EOP = (short) 0xFF; + + public Picture loadPicture(InputStream in) throws IOException, PictureException { + int command, c, x, y; + Vector entries = new Vector(); + int lastPen = 0; + PictureEntryMulti entry; + + try { + command = in.read(); + + while (true) { + if (command < 0) { + break; + } + + switch (command) { + case CMD_CHANGEPICCOLOR: + entries.add(new PictureEntryChangePicColor((byte) in.read())); + command = in.read(); + break; + + case CMD_CHANGEPRICOLOR: + entries.add(new PictureEntryChangePriColor((byte) in.read())); + command = in.read(); + break; + + case CMD_DISABLEPICDRAW: + entries.add(new PictureEntryChangePicColor((byte) -1)); + command = in.read(); + break; + + case CMD_DISABLEPRIDRAW: + entries.add(new PictureEntryChangePriColor((byte) -1)); + command = in.read(); + break; + + case CMD_DRAWXCORNER: + case CMD_DRAWYCORNER: + if (command == CMD_DRAWXCORNER) { + entry = new PictureEntryDrawX(); + } else { + entry = new PictureEntryDrawY(); + } + + entry.add(in.read(), in.read()); + + while (true) { + command = in.read(); + + if ((command >= CMD_START) || (command < 0)) { + break; + } + + entry.add(command); + } + + entries.add(entry); + break; + + case CMD_DRAWABSLINE: + entry = new PictureEntryAbsLine(); + entry.add(in.read(), in.read()); + + while (true) { + command = in.read(); + + if ((command >= CMD_START) || (command < 0)) { + break; + } + + entry.add(command, in.read()); + } + + entries.add(entry); + break; + + case CMD_DRAWRELLINE: + entry = new PictureEntryRelLine(); + entry.add(in.read(), in.read()); + + while (true) { + command = in.read(); + + if ((command >= CMD_START) || (command < 0)) { + break; + } + + x = (command & 0x70) >> 4; + y = (command & 0x07); + + if ((command & 0x80) == 0x80) { + x = -x; + } + + if ((command & 0x08) == 0x08) { + y = -y; + } + + entry.add(x, y); + } + + entries.add(entry); + break; + + case CMD_FILL: + entry = new PictureEntryFill(); + + while (true) { + command = in.read(); + + if ((command >= CMD_START) || (command < 0)) { + break; + } + + c = in.read(); + entry.add(command, c); + } + + entries.add(entry); + break; + + case CMD_CHANGEPEN: + lastPen = in.read(); + entries.add(new PictureEntryChangePen((byte) lastPen)); + command = in.read(); + break; + + case CMD_PLOT: + entry = new PictureEntryPlot(); + + while (true) { + command = in.read(); + + if ((command < 0) || (command >= CMD_START)) { + break; + } + + if ((lastPen & 0x20) == 0x20) { + command = (command >> 1) & 0x7f; + x = in.read(); + y = in.read(); + entry.add(new int[]{command, x, y}); + } else { + x = command; + y = in.read(); + entry.add(x, y); + } + } + + entries.add(entry); + break; + + case CMD_EOP: + command = -1; + break; + + default: + throw new CorruptedPictureException(); + } + } + } catch (EOFException eex) { + } + + in.close(); + return new Picture(entries); + } + + /* + protected boolean next() throws PictureException + { + if (in == null) + { + return false; + } + + try + { + if (nextCommand < 0) + { + nextCommand = in.read(); + + if (nextCommand < 0) + { + if (provider != null) + { + in.close(); + in = null; + } + + endReached = true; + return false; + } + } + + switch (nextCommand) + { + case CMD_CHANGEPICCOLOR: + picContext.picColor = (byte)in.read(); + nextCommand = -1; + break; + + case CMD_CHANGEPRICOLOR: + picContext.priColor = (byte)in.read(); + nextCommand = -1; + break; + + case CMD_DISABLEPICDRAW: + picContext.picColor = (byte)-1; + nextCommand = -1; + break; + + case CMD_DISABLEPRIDRAW: + picContext.priColor = (byte)-1; + nextCommand = -1; + break; + + case CMD_DRAWXCORNER: + drawXCorner(); + break; + + case CMD_DRAWYCORNER: + drawYCorner(); + break; + + case CMD_DRAWABSLINE: + drawAbsoluteLine(); + break; + + case CMD_DRAWRELLINE: + drawRelativeLine(); + break; + + case CMD_FILL: + drawFill(); + break; + + case CMD_CHANGEPEN: + picContext.penStyle = (byte)in.read(); + nextCommand = -1; + break; + + case CMD_PLOT: + drawPlot(); + break; + + case CMD_EOP: + in.close(); + in = null; + break; + + default: + throw new CorruptedPictureException(); + } + } + catch (IOException ioex) + { + in = null; + } + + return true; + }*/ + + /* + protected void drawPlot() throws IOException + { + int c, x, y; + + while (true) + { + c = in.read(); + + if ((c < 0) || (c >= CMD_START)) + { + nextCommand = c; + break; + } + + if ((picContext.penStyle & 0x20) == 0x20) + { + c = (c >> 1) & 0x7f; + x = in.read(); + y = in.read(); + drawPlot(c, x, y); + } + else + { + x = c; + y = in.read(); + drawPlot(x, y); + } + } + } + + protected void drawPlot(int patternNumber, int x, int y) + { + int circlePos = 0; + int bitPos = splatterStart[patternNumber]; + int x1, y1, penSize, penSizeTrue; + boolean circle; + + circle = !((picContext.penStyle & 0x10) == 0x10); + penSize = (picContext.penStyle & 0x07); + penSizeTrue = penSize; + + if (x < penSize) + { + x = penSize - 1; + } + + if (y < penSize) + { + y = penSize; + } + + for (y1 = y - penSize; y1 <= y + penSize; y1++) + { + for (x1 = x - (penSize + 1) / 2; x1 <= x + penSize / 2; x1++) + { + if (circle) + { + if (!(((circles[penSizeTrue][circlePos >> 0x3] >> (0x7 - (circlePos & 0x7))) & 0x1) == 0x1)) + { + circlePos++; + continue; + } + + circlePos++; + } + + if (((splatterMap[bitPos >> 3] >> (7 - (bitPos & 7))) & 1) == 1) + { + picContext.putPixel(x1, y1); + } + + bitPos++; + + if (bitPos == 0xff) + { + bitPos = 0; + } + } + } + } + + protected void drawPlot(int x, int y) + { + int circlePos = 0; + int x1, y1, penSize, penSizeTrue; + boolean circle; + + circle = !((picContext.penStyle & 0x10) == 0x10); + penSize = (picContext.penStyle & 0x07); + penSizeTrue = penSize; + + if (x < penSize) + { + x = penSize - 1; + } + + if (y < penSize) + { + y = penSize; + } + + for (y1 = y - penSize; y1 <= y + penSize; y1++) + { + for (x1 = x - (penSize + 1) / 2; x1 <= x + penSize / 2; x1++) + { + if (circle) + { + if (!(((circles[penSizeTrue][circlePos >> 0x3] >> (0x7 - (circlePos & 0x7))) & 0x1) == 0x1)) + { + circlePos++; + continue; + } + + circlePos++; + } + + picContext.putPixel(x1, y1); + } + } + }*/ +} diff --git a/core/src/main/java/com/sierra/agi/res/CorruptedResourceException.java b/core/src/main/java/com/sierra/agi/res/CorruptedResourceException.java new file mode 100644 index 0000000..401f97d --- /dev/null +++ b/core/src/main/java/com/sierra/agi/res/CorruptedResourceException.java @@ -0,0 +1,33 @@ +/** + * CorruptedResourceException.java + * Adventure Game Interpreter Resource Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.res; + +/** + * The resource is currupted. + * + * @author Dr. Z + * @version 0.00.00.01 + */ +public final class CorruptedResourceException extends ResourceException { + /** + * Creates new CorruptedResourceException without detail message. + */ + public CorruptedResourceException() { + super(); + } + + /** + * Constructs an CorruptedResourceException with the specified detail message. + * + * @param msg Detail message. + */ + public CorruptedResourceException(String msg) { + super(msg); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/res/NoDirectoryAvailableException.java b/core/src/main/java/com/sierra/agi/res/NoDirectoryAvailableException.java new file mode 100644 index 0000000..89b7c9e --- /dev/null +++ b/core/src/main/java/com/sierra/agi/res/NoDirectoryAvailableException.java @@ -0,0 +1,34 @@ +/** + * NoDirectoryAvailableException.java + * Adventure Game Interpreter Resource Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.res; + +/** + * There is no directory available. Throwed when a ResourceProvider is created + * with a folder that doesn't contain any resource directory. + * + * @author Dr. Z + * @version 0.00.00.01 + */ +public final class NoDirectoryAvailableException extends ResourceException { + /** + * Creates new NoDirectoryAvailableException without detail message. + */ + public NoDirectoryAvailableException() { + super(); + } + + /** + * Constructs an NoDirectoryAvailableException with the specified detail message. + * + * @param msg Detail message. + */ + public NoDirectoryAvailableException(String msg) { + super(msg); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/res/NoVolumeAvailableException.java b/core/src/main/java/com/sierra/agi/res/NoVolumeAvailableException.java new file mode 100644 index 0000000..3803ce9 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/res/NoVolumeAvailableException.java @@ -0,0 +1,34 @@ +/** + * NoVolumeAvailableException.java + * Adventure Game Interpreter Resource Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.res; + +/** + * There is no volume available. Throwed when a ResourceProvider is created + * with a folder that doesn't contain any resource directory. + * + * @author Dr. Z + * @version 0.00.00.01 + */ +public final class NoVolumeAvailableException extends ResourceException { + /** + * Creates new NoVolumeAvailableException without detail message. + */ + public NoVolumeAvailableException() { + super(); + } + + /** + * Constructs an NoVolumeAvailableException with the specified detail message. + * + * @param msg Detail message. + */ + public NoVolumeAvailableException(String msg) { + super(msg); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/res/ResourceCache.java b/core/src/main/java/com/sierra/agi/res/ResourceCache.java new file mode 100644 index 0000000..e1fff83 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/res/ResourceCache.java @@ -0,0 +1,370 @@ +/** + * ResourceCache.java + * Adventure Game Interpreter Resource Package + *

+ * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.res; + +import com.sierra.agi.inv.InventoryObjects; +import com.sierra.agi.inv.InventoryProvider; +import com.sierra.agi.logic.Logic; +import com.sierra.agi.logic.LogicException; +import com.sierra.agi.logic.LogicProvider; +import com.sierra.agi.pic.Picture; +import com.sierra.agi.pic.PictureException; +import com.sierra.agi.pic.PictureProvider; +import com.sierra.agi.sound.Sound; +import com.sierra.agi.sound.SoundProvider; +import com.sierra.agi.view.View; +import com.sierra.agi.view.ViewException; +import com.sierra.agi.view.ViewProvider; +import com.sierra.agi.word.Words; +import com.sierra.agi.word.WordsProvider; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.lang.reflect.Constructor; + +public class ResourceCache { + protected static Class[] providerParameters = {ResourceConfiguration.class}; + protected LogicProvider logProvider; + protected InventoryProvider invProvider; + protected PictureProvider picProvider; + protected ResourceProvider resProvider; + protected SoundProvider sndProvider; + protected ViewProvider viwProvider; + protected WordsProvider wrdProvider; + protected Object[] logics; + protected int[] logicsc; + protected Object[] pictures; + protected int[] picturesc; + protected Object[] sounds; + protected int[] soundsc; + protected Object[] views; + protected int[] viewsc; + protected Words words; + protected InventoryObjects objects; + + protected ResourceCache() { + } + + public ResourceCache(ResourceProvider resProvider) { + this.resProvider = resProvider; + } + + public synchronized ResourceProvider getResourceProvider() { + return resProvider; + } + + protected Object getProvider(String clazzName) { + try { + try { + return Class.forName(clazzName).newInstance(); + } catch (InstantiationException e) { + Class clazz = Class.forName(clazzName); + Constructor cons = clazz.getConstructor(providerParameters); + Object[] o = new Object[1]; + + o[0] = resProvider.getConfiguration(); + + return cons.newInstance(o); + } + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e.getClass().getName() + ": " + clazzName); + } + } + + public synchronized SoundProvider getSoundProvider() { + if (sndProvider == null) { + sndProvider = (SoundProvider) getProvider(System.getProperty("com.sierra.agi.sound.SoundProvider", "com.sierra.agi.sound.StandardSoundProvider")); + } + + return sndProvider; + } + + public synchronized void setSoundProvider(SoundProvider sndProvider) { + this.sndProvider = sndProvider; + } + + public synchronized InventoryProvider getInventoryProvider() { + if (invProvider == null) { + invProvider = (InventoryProvider) getProvider(System.getProperty("com.sierra.agi.inv.LogicProvider", "com.sierra.agi.inv.InventoryObjects")); + } + + return invProvider; + } + + public synchronized void setInventoryProvider(InventoryProvider invProvider) { + this.invProvider = invProvider; + } + + public synchronized LogicProvider getLogicProvider() { + if (logProvider == null) { + logProvider = (LogicProvider) getProvider(System.getProperty("com.sierra.agi.logic.LogicProvider", "com.sierra.agi.logic.StandardLogicProvider")); + } + + return logProvider; + } + + public synchronized void setLogicProvider(LogicProvider logProvider) { + this.logProvider = logProvider; + } + + public synchronized ViewProvider getViewProvider() { + if (viwProvider == null) { + viwProvider = (ViewProvider) getProvider(System.getProperty("com.sierra.agi.view.ViewProvider", "com.sierra.agi.view.StandardViewProvider")); + } + + return viwProvider; + } + + public synchronized void setViewProvider(ViewProvider viwProvider) { + this.viwProvider = viwProvider; + } + + public synchronized WordsProvider getWordsProvider() { + if (wrdProvider == null) { + wrdProvider = (WordsProvider) getProvider(System.getProperty("com.sierra.agi.word.WordsProvider", "com.sierra.agi.word.Words")); + } + + return wrdProvider; + } + + public synchronized void setWordsProvider(WordsProvider wrdProvider) { + this.wrdProvider = wrdProvider; + } + + public synchronized PictureProvider getPictureProvider() { + if (picProvider == null) { + picProvider = (PictureProvider) getProvider(System.getProperty("com.sierra.agi.pic.PictureProvider", "com.sierra.agi.pic.StandardPictureProvider")); + } + + return picProvider; + } + + public synchronized void setPictureProvider(PictureProvider picProvider) { + this.picProvider = picProvider; + } + + protected Object obtainResource(Object[] objects, int[] objectsc, short resNumber, boolean inc) { + Object obj = objects[resNumber]; + + if (obj != null) { + if (obj instanceof Reference) { + obj = ((Reference) obj).get(); + + if (inc) { + objects[resNumber] = obj; + } + + if (obj == null) { + return null; + } + } + + if (inc) { + objectsc[resNumber]++; + } + + return obj; + } + + return null; + } + + protected void flushResource(Object[] objects, int[] objectsc, short resNumber) { + if (objectsc[resNumber] > 0) { + objectsc[resNumber]--; + } + + if (objects[resNumber] == null) { + return; + } + + if (objectsc[resNumber] <= 0) { + if (!(objects[resNumber] instanceof Reference)) { + objects[resNumber] = generateReference(objects[resNumber]); + } + } + } + + protected synchronized Sound obtainSound(short resNumber, boolean inc) throws IOException, ResourceException { + Object o; + + if (sounds == null) { + sounds = new Object[256]; + soundsc = new int[256]; + } + + o = obtainResource(sounds, soundsc, resNumber, inc); + + if (o == null) { + o = getSoundProvider().loadSound(resProvider.open(ResourceProvider.TYPE_SOUND, resNumber)); + + if (inc) { + sounds[resNumber] = o; + soundsc[resNumber] = 1; + } else { + sounds[resNumber] = generateReference(o); + } + } + + return (Sound) o; + } + + public void loadSound(short resNumber) throws IOException, ResourceException { + obtainSound(resNumber, true); + } + + public Sound getSound(short resNumber) throws IOException, ResourceException { + return obtainSound(resNumber, false); + } + + public synchronized void unloadSound(short resNumber) { + flushResource(sounds, soundsc, resNumber); + } + + protected synchronized Logic obtainLogic(short resNumber, boolean inc) throws IOException, ResourceException, LogicException { + Object o; + + if (logics == null) { + logics = new Object[256]; + logicsc = new int[256]; + } + + o = obtainResource(logics, logicsc, resNumber, inc); + + if (o == null) { + o = getLogicProvider().loadLogic(resNumber, resProvider.open(ResourceProvider.TYPE_LOGIC, resNumber), resProvider.getSize(ResourceProvider.TYPE_LOGIC, resNumber)); + + if (inc) { + logics[resNumber] = o; + logicsc[resNumber] = 1; + } else { + logics[resNumber] = generateReference(o); + } + } + + return (Logic) o; + } + + public void loadLogic(short resNumber) throws IOException, ResourceException, LogicException { + obtainLogic(resNumber, true); + } + + public Logic getLogic(short resNumber) throws IOException, ResourceException, LogicException { + return obtainLogic(resNumber, false); + } + + public synchronized void unloadLogic(short resNumber) { + flushResource(logics, logicsc, resNumber); + } + + protected synchronized Picture obtainPicture(short resNumber, boolean inc) throws IOException, ResourceException, PictureException { + Object o; + + if (pictures == null) { + pictures = new Object[256]; + picturesc = new int[256]; + } + + o = obtainResource(pictures, picturesc, resNumber, inc); + + if (o == null) { + o = getPictureProvider().loadPicture(resProvider.open(ResourceProvider.TYPE_PICTURE, resNumber)); + + if (inc) { + pictures[resNumber] = o; + picturesc[resNumber] = 1; + } else { + pictures[resNumber] = generateReference(o); + } + } + + return (Picture) o; + } + + public void loadPicture(short resNumber) throws IOException, ResourceException, PictureException { + obtainPicture(resNumber, true); + } + + public Picture getPicture(short resNumber) throws IOException, ResourceException, PictureException { + return obtainPicture(resNumber, false); + } + + public synchronized void unloadPicture(short resNumber) { + flushResource(pictures, picturesc, resNumber); + } + + protected synchronized View obtainView(short resNumber, boolean inc) throws IOException, ResourceException, ViewException { + if (views == null) { + views = new Object[256]; + viewsc = new int[256]; + } + + Object o = obtainResource(views, viewsc, resNumber, inc); + + if (o == null) { + o = getViewProvider().loadView(resProvider.open(ResourceProvider.TYPE_VIEW, resNumber), resProvider.getSize(ResourceProvider.TYPE_VIEW, resNumber)); + + if (inc) { + views[resNumber] = o; + viewsc[resNumber] = 1; + } else { + views[resNumber] = generateReference(o); + } + } + + return (View) o; + } + + public void loadView(short resNumber) throws IOException, ResourceException, ViewException { + obtainView(resNumber, true); + } + + public View getView(short resNumber) throws IOException, ResourceException, ViewException { + return obtainView(resNumber, false); + } + + public synchronized void unloadView(short resNumber) { + flushResource(views, viewsc, resNumber); + } + + public synchronized Words getWords() throws IOException, ResourceException { + if (words == null) { + words = getWordsProvider().loadWords(resProvider.open(ResourceProvider.TYPE_WORD, (short) 0)); + } + + return words; + } + + public synchronized InventoryObjects getObjects() throws IOException, ResourceException { + if (objects == null) { + objects = getInventoryProvider().loadInventory(resProvider.open(ResourceProvider.TYPE_OBJECT, (short) 0)); + } + + return objects; + } + + protected Reference generateReference(Object o) { + return new WeakReference(o); + } + + public File getPath() { + return resProvider.getPath(); + } + + public String getVersion() { + return this.resProvider.getVersion(); + } + + public String getV3GameSig() { + return this.resProvider.getV3GameSig(); + } +} diff --git a/core/src/main/java/com/sierra/agi/res/ResourceCacheFile.java b/core/src/main/java/com/sierra/agi/res/ResourceCacheFile.java new file mode 100644 index 0000000..d566c48 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/res/ResourceCacheFile.java @@ -0,0 +1,44 @@ +/** + * ResourceCacheFile.java + * Adventure Game Interpreter Resource Package + *

+ * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.res; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +public class ResourceCacheFile extends ResourceCache { + public ResourceCacheFile(File file) throws IOException, ResourceException { + if (!file.exists()) { + throw new FileNotFoundException(); + } + + if (file.isDirectory()) { + loadFS(file); + } + + if (resProvider == null) { + String p = file.getPath(); + + if (p.endsWith(".zip")) { + //resProvider = new ResourceProviderZip(file); + } else { + loadFS(file); + } + } + } + + private void loadFS(File file) throws IOException, ResourceException { + try { + resProvider = new com.sierra.agi.res.v2.ResourceProviderV2(file); + } catch (ResourceException e) { + System.out.println("Found AGI Version 3 Game."); + resProvider = new com.sierra.agi.res.v3.ResourceProviderV3(file); + } + } +} diff --git a/core/src/main/java/com/sierra/agi/res/ResourceCacheFileDebug.java b/core/src/main/java/com/sierra/agi/res/ResourceCacheFileDebug.java new file mode 100644 index 0000000..bb4c890 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/res/ResourceCacheFileDebug.java @@ -0,0 +1,18 @@ +/* + * ResourceCacheFileDebug.java + * Adventure Game Interpreter Logic Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.res; + +import java.io.File; +import java.io.IOException; + +public class ResourceCacheFileDebug extends ResourceCacheFile { + public ResourceCacheFileDebug(File file) throws IOException, ResourceException { + super(file); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/res/ResourceConfiguration.java b/core/src/main/java/com/sierra/agi/res/ResourceConfiguration.java new file mode 100644 index 0000000..743e3af --- /dev/null +++ b/core/src/main/java/com/sierra/agi/res/ResourceConfiguration.java @@ -0,0 +1,16 @@ +/** + * ResourceConfiguration.java + * Adventure Game Interpreter Resource Package + *

+ * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.res; + +public class ResourceConfiguration { + public short engineEmulation; + public boolean amiga; + public boolean agds; + public String name; +} diff --git a/core/src/main/java/com/sierra/agi/res/ResourceException.java b/core/src/main/java/com/sierra/agi/res/ResourceException.java new file mode 100644 index 0000000..3f103f6 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/res/ResourceException.java @@ -0,0 +1,33 @@ +/** + * ResourceException.java + * Adventure Game Interpreter Resource Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.res; + +/** + * Base class for Resource Exceptions. + * + * @author Dr. Z + * @version 0.00.00.01 + */ +public class ResourceException extends Exception { + /** + * Creates new ResourceException without detail message. + */ + public ResourceException() { + super(); + } + + /** + * Constructs an ResourceException with the specified detail message. + * + * @param msg Detail message. + */ + public ResourceException(String msg) { + super(msg); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/res/ResourceNotExistingException.java b/core/src/main/java/com/sierra/agi/res/ResourceNotExistingException.java new file mode 100644 index 0000000..6abeafc --- /dev/null +++ b/core/src/main/java/com/sierra/agi/res/ResourceNotExistingException.java @@ -0,0 +1,33 @@ +/** + * ResourceNotExistingException.java + * Adventure Game Interpreter Resource Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.res; + +/** + * The resource doesn't exists. + * + * @author Dr. Z + * @version 0.00.00.01 + */ +public final class ResourceNotExistingException extends ResourceException { + /** + * Creates new ResourceNotExistingException without detail message. + */ + public ResourceNotExistingException() { + super(); + } + + /** + * Constructs an ResourceNotExistingException with the specified detail message. + * + * @param msg Detail message. + */ + public ResourceNotExistingException(String msg) { + super(msg); + } +} diff --git a/core/src/main/java/com/sierra/agi/res/ResourceProvider.java b/core/src/main/java/com/sierra/agi/res/ResourceProvider.java new file mode 100644 index 0000000..108a32d --- /dev/null +++ b/core/src/main/java/com/sierra/agi/res/ResourceProvider.java @@ -0,0 +1,114 @@ +/** + * ResourceProvider.java + * Adventure Game Interpreter Resource Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.res; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +/** + * The ResourceProvider interface is a standard + * way for loading resources dynamicly. Gives the + * interresing possibility of being able to read + * them from every kind of data container. + * + * @author Dr. Z + * @version 0.00.00.02 + */ +public interface ResourceProvider { + /** Logic Resource Type. */ + byte TYPE_LOGIC = 0; + + /** Picture Resource Type. */ + byte TYPE_PICTURE = 1; + + /** Sound Resource Type. */ + byte TYPE_SOUND = 2; + + /** View Resource Type. */ + byte TYPE_VIEW = 3; + + /** Compiled Logic Resource Type. (Reserverd for future uses) */ + byte TYPE_LOGIC_COMPILED = 4; + + /** Object File. */ + byte TYPE_OBJECT = 10; + + /** Word Tokenizer File. */ + byte TYPE_WORD = 11; + + /** Fast Provider Type. (ie. File system) */ + byte PROVIDER_TYPE_FAST = 0; + + /** Slow Provider Type. (ie. URL) */ + byte PROVIDER_TYPE_SLOW = 1; + + /** + * Calculate the CRC of the resources. + * + * @return CRC of the resources. + */ + long getCRC(); + + /** + * Retreive the count of resources of the specified type. + * + * @param resType Resource type + * @return Resource count. + */ + int count(byte resType) throws ResourceException; + + /** + * Enumerate the resource numbers of the specified type. + * + * @param resType Resource type + * @return Returns an array containing the resource numbers. + */ + short[] enumerate(byte resType) throws ResourceException; + + /** + * Retreive the size in bytes of the specified resource. + * + * @param resType Resource type + * @param resNumber Resource number + * @return Returns the size in bytes of the specified resource. + */ + int getSize(byte resType, short resNumber) throws ResourceException, IOException; + + /** + * Open the specified resource and return a pointer + * to the resource. The InputStream is decrypted/decompressed, + * if neccessary, by this function. (So you don't have to care + * about them.) + * + * @param resType Resource type + * @param resNumber Resource number + * @return InputStream linked to the specified resource. + */ + InputStream open(byte resType, short resNumber) throws ResourceException, IOException; + + /** + * Return the provider type. Used has a optimization hint by + * the resource cache. (For example, PROVIDER_TYPE_SLOW whould + * mean to never ask twice for the same resource because transfert + * rate may be slow.) + */ + byte getProviderType(); + + /** + * Return the resource configuration. + */ + ResourceConfiguration getConfiguration(); + + File getPath(); + + String getVersion(); + + String getV3GameSig(); +} diff --git a/core/src/main/java/com/sierra/agi/res/ResourceProviderZip.java b/core/src/main/java/com/sierra/agi/res/ResourceProviderZip.java new file mode 100644 index 0000000..d6fd800 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/res/ResourceProviderZip.java @@ -0,0 +1,61 @@ +/* + * ResourceProviderZip.java + * Adventure Game Interpreter Resource Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.res; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +public abstract class ResourceProviderZip implements ResourceProvider { + protected ZipFile file; + + protected int[] counts; + protected int[][] enums; + + public ResourceProviderZip(File file) throws IOException { + this.file = new ZipFile(file); + } + + public static void convert(File file, ResourceProvider provider) throws ResourceException, IOException { + ZipOutputStream outZip = new ZipOutputStream(new FileOutputStream(file)); + DataOutputStream outData = new DataOutputStream(outZip); + ResourceConfiguration config = provider.getConfiguration(); + + outZip.setLevel(9); + outZip.putNextEntry(new ZipEntry("info")); + + convert(outData, provider, TYPE_LOGIC); + convert(outData, provider, TYPE_PICTURE); + convert(outData, provider, TYPE_SOUND); + convert(outData, provider, TYPE_VIEW); + + outData.writeLong(provider.getCRC()); + outData.writeShort(config.engineEmulation); + outData.writeBoolean(config.amiga); + outData.writeBoolean(config.agds); + outData.writeUTF(config.name); + + outZip.close(); + } + + protected static void convert(DataOutputStream outData, ResourceProvider provider, byte resType) throws ResourceException, IOException { + short[] en = provider.enumerate(resType); + int index; + + outData.writeInt(en.length); + + for (index = 0; index < en.length; index++) { + outData.writeShort(en[index]); + } + } +} diff --git a/core/src/main/java/com/sierra/agi/res/ResourceTypeInvalidException.java b/core/src/main/java/com/sierra/agi/res/ResourceTypeInvalidException.java new file mode 100644 index 0000000..542e4c5 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/res/ResourceTypeInvalidException.java @@ -0,0 +1,32 @@ +/** + * ResourceTypeInvalidException.java + * Adventure Game Interpreter Resource Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.res; + +/** + * Resource type passed by parameter is invalid. + * + * @author Dr. Z + * @version 0.00.00.01 + */ +public class ResourceTypeInvalidException extends ResourceException { + /** + * Creates new ResourceTypeInvalidException without detail message. + */ + public ResourceTypeInvalidException() { + } + + /** + * Constructs an ResourceTypeInvalidException with the specified detail message. + * + * @param msg Detail message. + */ + public ResourceTypeInvalidException(String msg) { + super(msg); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/res/VolumeNotFoundException.java b/core/src/main/java/com/sierra/agi/res/VolumeNotFoundException.java new file mode 100644 index 0000000..7c4ff2a --- /dev/null +++ b/core/src/main/java/com/sierra/agi/res/VolumeNotFoundException.java @@ -0,0 +1,33 @@ +/** + * NoVolumeAvailableException.java + * Adventure Game Interpreter Resource Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.res; + +/** + * The volume is not found. + * + * @author Dr. Z + * @version 0.00.00.01 + */ +public final class VolumeNotFoundException extends ResourceException { + /** + * Creates new VolumeNotFoundException without detail message. + */ + public VolumeNotFoundException() { + super(); + } + + /** + * Constructs an VolumeNotFoundException with the specified detail message. + * + * @param msg Detail message. + */ + public VolumeNotFoundException(String msg) { + super(msg); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/res/dir/ResourceDirectory.java b/core/src/main/java/com/sierra/agi/res/dir/ResourceDirectory.java new file mode 100644 index 0000000..daf9969 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/res/dir/ResourceDirectory.java @@ -0,0 +1,127 @@ +/** + * ResourceDirectory.java + * Adventure Game Interpreter Resource Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.res.dir; + +import com.sierra.agi.io.IOUtils; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +/** + *

Directories
+ * Each directory file is of the same format. They contain a finite number + * of three byte entries, no more than 256. The size will vary depending on the + * number of files of the type that the directory file is pointing to. Dividing + * the filesize by three gives the maximum file number of that type of data + * file. Each entry is of the following format:

+ *
+ * Byte 1          Byte 2          Byte 3 
+ * 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 
+ * V V V V P P P P P P P P P P P P P P P P P P P P 
+ *
+ * V = VOL number.
+ * P = Position (offset into VOL file)
+ *

+ * The entry number itself gives the number of the data file that it is + * pointing to. For example, if the following three byte entry is entry + * number 45 in the SOUND directory file, 12 3D FE + * then SOUND.45 is located at position 0x23DFE in the VOL.1 file. The first + * entry number is entry 0. + *

+ * If the three bytes contain the value 0xFFFFFF, then the resource does not + * exist.

+ * + * @author Dr. Z + * @version 0.00.00.01 + */ +public class ResourceDirectory { + /** Maximum number of entries in a directory. */ + protected static final int MAX_ENTRIES = 256; + + /** Directory Entries */ + protected int[] entries = new int[MAX_ENTRIES]; + + /** Directory CRC */ + protected int crc; + + /** Directory */ + protected int count; + + /** Creates a new Resource Directory */ + public ResourceDirectory(InputStream in) throws IOException { + byte[] b = new byte[3]; + int i = 0, j = 0, e; + int c = 0, n = 0; + + try { + while (true) { + IOUtils.fill(in, b, 0, 3); + + for (j = 0; j < 3; j++) { + c += (b[j] & 0xff); + } + + if (b[0] != -1) { + entries[i] = ((b[0] & 0xf0) >> 4) << 24 | ((b[0] & 0x0f) << 16) | ((b[1] & 0xff) << 8) | (b[2] & 0xff); + n++; + } else { + entries[i] = -1; + } + + i++; + } + } catch (EOFException ex) { + } + + for (; i < MAX_ENTRIES; i++) { + entries[i] = -1; + } + + crc = c; + count = n; + } + + public int getCRC() { + return crc; + } + + public int getVolume(int resourceNumber) { + if (entries[resourceNumber] == -1) { + return -1; + } + + return (entries[resourceNumber] & 0xff000000) >> 24; + } + + public int getOffset(int resourceNumber) { + if (entries[resourceNumber] == -1) { + return -1; + } + + return (entries[resourceNumber] & 0x00ffffff); + } + + public int getCount() { + return count; + } + + public short[] getNumbers() { + short[] numbers = new short[count]; + short i, j; + + for (i = 0, j = 0; (i < 256) && (j < count); i++) { + if (entries[i] != -1) { + numbers[j++] = i; + } + } + + return numbers; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/res/v2/ResourceProviderV2.java b/core/src/main/java/com/sierra/agi/res/v2/ResourceProviderV2.java new file mode 100644 index 0000000..30a471a --- /dev/null +++ b/core/src/main/java/com/sierra/agi/res/v2/ResourceProviderV2.java @@ -0,0 +1,566 @@ +/** + * ResourceProviderV2.java + * Adventure Game Interpreter Resource Package + *

+ * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.res.v2; + +import com.sierra.agi.io.ByteCaster; +import com.sierra.agi.io.ByteCasterStream; +import com.sierra.agi.io.CryptedInputStream; +import com.sierra.agi.io.SegmentedInputStream; +import com.sierra.agi.res.CorruptedResourceException; +import com.sierra.agi.res.NoDirectoryAvailableException; +import com.sierra.agi.res.NoVolumeAvailableException; +import com.sierra.agi.res.ResourceConfiguration; +import com.sierra.agi.res.ResourceException; +import com.sierra.agi.res.ResourceNotExistingException; +import com.sierra.agi.res.ResourceProvider; +import com.sierra.agi.res.ResourceTypeInvalidException; +import com.sierra.agi.res.dir.ResourceDirectory; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Properties; + +/** + * Provide access to resources via the standard storage methods. + * It reads unmodified sierra's resource files. + *

+ * All AGI games have either one directory file, or more commonly, four. + * AGI version 2 games will have the files LOGDIR, PICDIR, VIEWDIR, and SNDDIR. + * This single file is basically the four version 2 files joined together + * except that it has an 8 byte header giving the position of each directory + * within the single file. + *

+ * The directory files give the location of the data types within the VOL + * files. The type of directory determines the type of data. For example, the + * LOGDIR gives the locations of the LOGIC files. + * + * Note: In this description and elsewhere in documents written by me, + * the AGI data called LOGIC, PICTURE, VIEW, and SOUND data are referred to by + * me as files even though they are part of a single VOL file. I think of + * the VOL file as sort of a virtual storage device in itself that holds many + * files. Some documents call the files contains in VOL files "resources". + * + * @author Dr. Z + * @version 0.00.00.01 + */ +public class ResourceProviderV2 implements ResourceProvider { + /** + * AGDS's Decryption Key. This key is used to decrypt + * AGDS games. + */ + public static final String AGDS_KEY = "Alex Simkin"; + /** + * Sierra's Decryption Key. This key used to decrypt + * original sierra games. + */ + public static final String SIERRA_KEY = "Avis Durgan"; + /** + * Resource's CRC. + */ + protected long crc; + /** + * Resource's Entries Tables. + */ + protected ResourceDirectory[] entries = new ResourceDirectory[4]; + /** + * Path to resources files. + */ + protected File path; + /** + * Resource Configuration + */ + protected ResourceConfiguration configuration = new ResourceConfiguration(); + + protected String version = "unknown"; + + /** + * Initialize the ResourceProvider implementation to access + * resource on the file system. + * + * @param folder Resource's folder or File inside the resource's + * folder. + */ + public ResourceProviderV2(File folder) throws IOException, ResourceException { + if (!folder.exists()) { + throw new FileNotFoundException(); + } + + if (folder.isDirectory()) { + path = folder.getAbsoluteFile(); + } else { + path = folder.getParentFile(); + } + + readVolumes(); + readDirectories(); + readVersion(); + calculateCRC(); + calculateConfiguration(); + } + + public static String getKey(boolean agds) { + return System.getProperty("com.sierra.agi.res.key", agds ? AGDS_KEY : SIERRA_KEY); + } + + public static boolean isCrypted(File file) { + boolean b = false; + + try { + ByteCasterStream bstream = new ByteCasterStream(new FileInputStream(file)); + + if (bstream.lohiReadUnsignedShort() > file.length()) { + b = true; + } + + bstream.close(); + return b; + } catch (Throwable t) { + return false; + } + } + + protected void validateType(byte resType) throws ResourceTypeInvalidException { + if ((resType > TYPE_WORD) || (resType < TYPE_LOGIC)) { + throw new ResourceTypeInvalidException(); + } + } + + /** + * Retreive the count of resources of the specified type. + * Only valid with Locic, Picture, Sound and View resource + * types. + * + * @param resType Resource type + * @return Resource count. + * @see com.sierra.agi.res.ResourceProvider#TYPE_LOGIC + * @see com.sierra.agi.res.ResourceProvider#TYPE_PICTURE + * @see com.sierra.agi.res.ResourceProvider#TYPE_SOUND + * @see com.sierra.agi.res.ResourceProvider#TYPE_VIEW + */ + public int count(byte resType) throws ResourceException { + validateType(resType); + + if (resType >= TYPE_OBJECT) { + return 1; + } + + return entries[resType].getCount(); + } + + /** + * Enumerate the resource numbers of the specified type. + * Only valid with Locic, Picture, Sound and View resource + * types. + * + * @param resType Resource type + * @return Array containing the resource numbers. + * @see com.sierra.agi.res.ResourceProvider#TYPE_LOGIC + * @see com.sierra.agi.res.ResourceProvider#TYPE_PICTURE + * @see com.sierra.agi.res.ResourceProvider#TYPE_SOUND + * @see com.sierra.agi.res.ResourceProvider#TYPE_VIEW + */ + public short[] enumerate(byte resType) throws ResourceException { + validateType(resType); + + return entries[resType].getNumbers(); + } + + /** + * Open the specified resource and return a pointer + * to the resource. The InputStream is decrypted/decompressed, + * if neccessary, by this function. (So you don't have to care + * about them.) + * + * @param resType Resource type + * @param resNumber Resource number. Ignored if resource type + * is TYPE_OBJECT or + * TYPE_WORD + * @return InputStream linked to the specified resource. + * @see com.sierra.agi.res.ResourceProvider#TYPE_LOGIC + * @see com.sierra.agi.res.ResourceProvider#TYPE_OBJECT + * @see com.sierra.agi.res.ResourceProvider#TYPE_PICTURE + * @see com.sierra.agi.res.ResourceProvider#TYPE_SOUND + * @see com.sierra.agi.res.ResourceProvider#TYPE_VIEW + * @see com.sierra.agi.res.ResourceProvider#TYPE_WORD + */ + public InputStream open(byte resType, short resNumber) throws ResourceException, IOException { + File volf; + + switch (resType) { + case ResourceProvider.TYPE_OBJECT: + volf = getDirectoryFile(resType); + + if (isCrypted(volf)) { + return new CryptedInputStream(new FileInputStream(volf), getKey(false)); + } else { + return new FileInputStream(volf); + } + + case ResourceProvider.TYPE_WORD: + return new FileInputStream(getDirectoryFile(resType)); + } + + try { + if (entries[resType] != null) { + int vol, offset, length; + + vol = entries[resType].getVolume(resNumber); + offset = entries[resType].getOffset(resNumber); + + if ((vol != -1) && (offset != -1)) { + byte[] b; + RandomAccessFile file; + InputStream in; + + b = new byte[5]; + file = new RandomAccessFile(getVolumeFile(vol), "r"); + file.seek(offset); + file.read(b, 0, 5); + + if ((b[0] != 0x12) || (b[1] != 0x34)) { + throw new CorruptedResourceException(); + } + + length = ByteCaster.lohiUnsignedShort(b, 3); + in = new SegmentedInputStream(file, offset + 5, length); + + if (resType == TYPE_LOGIC) { + int startPos, numMessages, offsetCrypted; + + // Calculate the Messages Offset + file.read(b, 0, 2); + startPos = ByteCaster.lohiUnsignedShort(b, 0) + 2; + file.seek(offset + startPos + 5); + file.read(b, 0, 3); + numMessages = ByteCaster.lohiUnsignedByte(b, 0); + offsetCrypted = startPos + 3 + (numMessages * 2); + file.seek(offset + 5); + + in = new CryptedInputStream(in, getKey(false), offsetCrypted); + } + + return in; + } + } + + throw new ResourceNotExistingException(); + } catch (IndexOutOfBoundsException e) { + throw new ResourceTypeInvalidException(); + } + } + + /** + * Calculate the CRC of the resources. In this implentation + * the CRC is not calculated by this function, it only return + * the cached CRC value. + * + * @return CRC of the resources. + */ + public long getCRC() { + return crc; + } + + public byte getProviderType() { + return PROVIDER_TYPE_FAST; + } + + public ResourceConfiguration getConfiguration() { + return configuration; + } + + protected File getVolumeFile(int vol) throws IOException { + File file = getGameFile(path, "vol." + vol); + + if (!file.exists()) { + throw new FileNotFoundException("File " + file.getPath() + " can't be found."); + } + + return file; + } + + /** + * To account for different platforms, where there may or may not be a case + * sensitive file system, and where the game files may or may not have been + * copied from another platform, we attempt here to look for the requested + * game file firstly as-is, then in uppercase form, and then in lowercase. + * + * @param path The File that represents the folder that contains the game files. + * @param fileName The name of the file to get. + * @return A File representing the game file. + */ + protected File getGameFile(File path, String fileName) { + File file = new File(path, fileName); + if (!file.exists()) { + file = new File(path, fileName.toUpperCase()); + if (!file.exists()) { + file = new File(path, fileName.toLowerCase()); + } + } + return file; + } + + protected File getDirectoryFile(byte resType) throws IOException { + File file; + + switch (resType) { + case ResourceProvider.TYPE_OBJECT: + file = getGameFile(path, "object"); + break; + case ResourceProvider.TYPE_WORD: + file = getGameFile(path, "words.tok"); + break; + case ResourceProvider.TYPE_LOGIC: + file = getGameFile(path, "logdir"); + break; + case ResourceProvider.TYPE_PICTURE: + file = getGameFile(path, "picdir"); + break; + case ResourceProvider.TYPE_SOUND: + file = getGameFile(path, "snddir"); + break; + case ResourceProvider.TYPE_VIEW: + file = getGameFile(path, "viewdir"); + break; + default: + return null; + } + + if (!file.exists()) { + throw new FileNotFoundException(); + } + + return file; + } + + /** + * Retreive the size in bytes of the specified resource. + * + * @param resType Resource type + * @param resNumber Resource number. Ignored if resource type + * is TYPE_OBJECT or + * TYPE_WORD + * @return Size in bytes of the specified resource. + * @see com.sierra.agi.res.ResourceProvider#TYPE_LOGIC + * @see com.sierra.agi.res.ResourceProvider#TYPE_OBJECT + * @see com.sierra.agi.res.ResourceProvider#TYPE_PICTURE + * @see com.sierra.agi.res.ResourceProvider#TYPE_SOUND + * @see com.sierra.agi.res.ResourceProvider#TYPE_VIEW + * @see com.sierra.agi.res.ResourceProvider#TYPE_WORD + */ + public int getSize(byte resType, short resNumber) throws ResourceException, IOException { + switch (resType) { + case ResourceProvider.TYPE_OBJECT: + case ResourceProvider.TYPE_WORD: + return (int) getDirectoryFile(resType).length(); + } + + try { + if (entries[resType] != null) { + int vol, offset; + + vol = entries[resType].getVolume(resNumber); + offset = entries[resType].getOffset(resNumber); + + if ((vol != -1) && (offset != -1)) { + byte[] b; + RandomAccessFile file; + + b = new byte[5]; + file = new RandomAccessFile(getVolumeFile(vol), "r"); + file.seek(offset); + file.read(b); + file.close(); + + if ((b[0] != 0x12) || (b[1] != 0x34)) { + throw new CorruptedResourceException(); + } + + return ByteCaster.lohiUnsignedShort(b, 3); + } + } + + throw new ResourceNotExistingException(); + } catch (IndexOutOfBoundsException e) { + throw new ResourceTypeInvalidException(); + } + } + + /** + * Find volumes files + */ + protected void readVolumes() throws NoVolumeAvailableException { + boolean founded = false; + int i = 0; + File volf; + + while (true) { + volf = getGameFile(path, "vol." + i); + + if (volf.exists()) { + founded = true; + break; + } + + if (i > 50) { + break; + } + + i++; + } + + if (!founded) { + throw new NoVolumeAvailableException(); + } + } + + /** + * Read all directory files + */ + protected void readDirectories() throws NoDirectoryAvailableException, IOException { + byte i; + int j; + File dir; + InputStream stream; + + for (i = 0, j = 0; i < 4; i++) { + dir = getDirectoryFile(i); + + if (dir != null) { + stream = new FileInputStream(dir); + entries[i] = new ResourceDirectory(stream); + stream.close(); + j++; + } + } + + if (j == 0) { + throw new NoDirectoryAvailableException(); + } + } + + /** + * Calculate the Resource's CRC + */ + protected void calculateCRC() throws IOException { + File dirf = new File(path, "vol.crc"); + + try { + /* Check if the CRC has been pre-calculated */ + DataInputStream meta = new DataInputStream(new FileInputStream(dirf)); + + crc = meta.readLong(); + meta.close(); + } catch (IOException ex) { + /* CRC need to be calculated from scratch */ + crc = calculateCRCFromScratch(); + + /* Write down the CRC for next times */ + DataOutputStream meta = new DataOutputStream(new FileOutputStream(dirf)); + + meta.writeLong(crc); + meta.close(); + } + } + + protected int calculateCRCFromScratch() throws IOException { + File[] dir; + int i, j, c; + + c = 0; + dir = new File[2]; + dir[0] = getGameFile(path, "object"); + dir[1] = getGameFile(path, "words.tok"); + + for (i = 0; i < entries.length; i++) { + if (entries[i] != null) { + c += entries[i].getCRC(); + } + } + + for (i = 0; i < dir.length; i++) { + FileInputStream stream = new FileInputStream(dir[i]); + + while (true) { + j = stream.read(); + + if (j == -1) + break; + + c += j; + } + + stream.close(); + } + + return c; + } + + protected void calculateConfiguration() { + Properties props = new Properties(); + String scrc = "0x" + Long.toString(crc, 16); + + String ver = props.getProperty(scrc, "0x2917"); + configuration.amiga = ver.indexOf('a') != -1; + configuration.agds = ver.indexOf('g') != -1; + ver = ver.substring(2); + + while (!Character.isDigit(ver.charAt(ver.length() - 1))) { + ver = ver.substring(0, ver.length() - 1); + } + + configuration.engineEmulation = (Integer.valueOf(ver, 16).shortValue()); + + props = new Properties(); + + configuration.name = props.getProperty(scrc, "Unknown Game"); + } + + public File getPath() { + return this.path; + } + + private void readVersion() throws IOException { + File file = getGameFile(path, "agidata.ovl"); + + if (!file.exists()) { + return; + } + + byte[] fileContent = Files.readAllBytes(file.toPath()); + + for (int i = 0; i < fileContent.length; i++) { + if (fileContent[i] == 86 && fileContent[i + 1] == 101 && fileContent[i + 2] == 114 && fileContent[i + 3] == 115 && fileContent[i + 4] == 105 && fileContent[i + 5] == 111 && fileContent[i + 6] == 110 && fileContent[i + 7] == 32) { + int j; + for (j = i + 8; fileContent[j] != 0; j++) { + } + + this.version = new String(fileContent, i + 8, j - (i + 8), StandardCharsets.US_ASCII); + break; + } + } + } + + public String getVersion() { + return this.version; + } + + public String getV3GameSig() { + // This is V2, so it doesn't apply. Return null; + return null; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/res/v3/ResourceProviderV3.java b/core/src/main/java/com/sierra/agi/res/v3/ResourceProviderV3.java new file mode 100644 index 0000000..66ebda2 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/res/v3/ResourceProviderV3.java @@ -0,0 +1,385 @@ +/* + * ResourceProviderV3.java + */ + +package com.sierra.agi.res.v3; + +import com.sierra.agi.io.*; +import com.sierra.agi.res.*; +import com.sierra.agi.res.dir.ResourceDirectory; + +import java.io.*; +import java.util.Arrays; + +/** + * Provide access to resources via the standard storage methods. + * It reads unmodified sierra's resource files. + *

+ * AGIv3 stores resources in a slightly different way from AGIv2. The first + * significant difference is in the length of the resource header which is + * now seven bytes. + *

+ * + * + * + * + * + * + *
ByteMeaning
0-1Signature (0x12--0x34)
2Vol number that the resource is contained in
3-4Uncompressed resource size (LO-HI)
5-6Compressed resource size (LO-HI)
+ *

+ * Instead of one resource size as in AGIv2, there are now two sizes. Most of + * the resources in AGIv3 games are compressed with a form of LZW. Some of them + * are not though. The interpreter determines whether the resource is compressed + * by comparing the values of the two sizes given in the header information. If + * they are equal, then it knows that the resource is stored uncompressed. + * However, if the sizes do not match, this does not mean that the file is + * compressed with LZW. If the file is a PICTURE file, then it is stored with + * its own limited form of compression. This is why the top bit of the third + * byte in the header is used to tell the interpreter that the resource is a + * PICTURE file, otherwise it would think that the resource was compressed with + * LZW. + *

+ * As far as I can tell, none of the PICTUREs are compressed with LZW. This may + * well be possible though. It could also be possible for the PICTURE to be + * totally uncompressed (i.e. it wouldn't use the PICTURE compression method), + * but I haven't seen any examples of either of the above two cases. + * + *

LZW compression + *

+ * The compression used with version 3 games is an adaptive form of LZW. The LZW + * algorithm is not explained here, but it basically compresses data by + * representing previous strings by single codes. When these strings are + * encountered again, the code can be stored instead. The following information + * states how the AGIv3 algorithm differs from the standard LZW algorithm. There + * are plenty of places on the net where you can find a description of the LZW + * algorithm if you are not familiar with it. + *

+ * AGIv3 uses an adaptive form of LZW that starts by using 9 bit codes and when + * the code space is full, it progresses on to 10 bits and so on. As with normal + * LZW, codes 0-255 represent the standard ASCII characters. The next two codes + * have a special meaning: + *

+ * 256 is used as a start over code. The table is cleared, the number of bits + * set back to 9, and the process begins again with the next code being 258. + *

+ * 257 tells the interpreter that it has reached the end of the resource. + *

+ * Code 256 seems to be the first code stored in all compressed resources. This + * is probably just to make sure everything is initialized for beginning the + * compression process. As was mentioned above, the first code used for the LZW + * table itself is code 258. From there it stores pairs of prefix codes and + * appended characters for each table entry until it reaches code 512 at which + * stage it switches to storing the codes using 10 bits and then 11 and so on. + * It appears that it will never get to 12 bits because code 256 always seems + * to turn up just before it needs to switch up to 12 bits, i.e. when code 2048 + * is required. Carl Muckenhoupt's decrypt routine for SCI games specifically + * prevents it from switching to 12 bits anyway. Whether there is ever a case + * where code 256 does not intervene, it has not yet been determined. + *

+ * Note: I should point out that Carl and myself both arrived at the above + * algorithm independently which confirms that the compression used in the early + * SCI games was identical to that used in AGIv3. + *

+ * + * @author Dr. Z + * @version 0.00.00.01 + */ +public class ResourceProviderV3 extends com.sierra.agi.res.v2.ResourceProviderV2 { + protected File[] vols; + protected File[] dirs; + + /** + * Initialize the ResourceProvider implentation to access + * resource on the file system. + * + * @param folder Resource's folder or File inside the resource's + * folder. + */ + public ResourceProviderV3(File folder) throws IOException, ResourceException { + super(folder); + } + + /** + * Find volumes files + */ + protected void readVolumes() throws NoVolumeAvailableException { + vols = path.listFiles(new VolumeFilenameFilter()); + + if (vols == null) { + throw new NoVolumeAvailableException(); + } + + if (vols.length == 0) { + throw new NoVolumeAvailableException(); + } + + Arrays.sort(vols, new VolumeSorter()); + } + + protected int calculateCRCFromScratch() throws IOException { + byte[] b = new byte[8]; + int c, i; + InputStream stream; + + c = super.calculateCRCFromScratch(); + + stream = new FileInputStream(dirs[0]); + stream.read(b, 0, 8); + stream.close(); + + for (i = 0; i < 8; i++) { + c += (b[i] & 0xff); + } + + return c; + } + + /** + * Read directory files + */ + protected void readDirectories() throws NoDirectoryAvailableException, IOException { + byte[] b = new byte[8]; + int[] o = new int[8]; + InputStream stream; + RandomAccessFile dirfile; + int i, j, ax; + + findDirectories(); + stream = new FileInputStream(dirs[0]); + stream.read(b, 0, 8); + stream.close(); + + for (i = 0; i < 4; i++) { + o[i] = ByteCaster.lohiUnsignedShort(b, i * 2); + } + + for (i = 0; i < 4; i++) { + ax = 0xffffff; + + for (j = 0; j < 4; j++) { + if ((o[j] > o[i]) && (o[j] < ax)) { + ax = o[j]; + } + } + + if (ax == 0xffffff) { + ax = (int) dirs[0].length(); + } + + o[i + 4] = ax; + } + + dirfile = new RandomAccessFile(dirs[0], "r"); + o[4] -= o[0]; + entries[0] = new ResourceDirectory(new SegmentedInputStream(dirfile, o[0], o[4])); + + o[5] -= o[1]; + entries[1] = new ResourceDirectory(new SegmentedInputStream(dirfile, o[1], o[5])); + + o[6] -= o[2]; + entries[3] = new ResourceDirectory(new SegmentedInputStream(dirfile, o[2], o[6])); + + o[7] -= o[3]; + entries[2] = new ResourceDirectory(new SegmentedInputStream(dirfile, o[3], o[7])); + } + + /** + * Find all directory files + */ + protected void findDirectories() throws NoDirectoryAvailableException { + dirs = path.listFiles(new DirectoryFilenameFilter()); + + if (dirs == null) { + throw new NoDirectoryAvailableException(); + } + + if (dirs.length == 0) { + throw new NoDirectoryAvailableException(); + } + + Arrays.sort(dirs, new DirectorySorter()); + } + + /** + * Open the specified resource and return a pointer + * to the resource. The InputStream is decrypted/decompressed, + * if neccessary, by this function. (So you don't have to care + * about them.) + * + * @param resType Resource type + * @param resNumber Resource number. Ignored if resource type + * is TYPE_OBJECT or + * TYPE_WORD + * @return InputStream linked to the specified resource. + * @see com.sierra.agi.res.ResourceProvider#TYPE_LOGIC + * @see com.sierra.agi.res.ResourceProvider#TYPE_OBJECT + * @see com.sierra.agi.res.ResourceProvider#TYPE_PICTURE + * @see com.sierra.agi.res.ResourceProvider#TYPE_SOUND + * @see com.sierra.agi.res.ResourceProvider#TYPE_VIEW + * @see com.sierra.agi.res.ResourceProvider#TYPE_WORD + */ + public InputStream open(byte resType, short resNumber) throws IOException, ResourceException { + if (resType > TYPE_WORD) { + throw new ResourceTypeInvalidException(); + } + + switch (resType) { + case ResourceProvider.TYPE_OBJECT: + if (isCrypted(dirs[1])) { + return new CryptedInputStream(new FileInputStream(dirs[1]), getKey(false)); + } + + return new FileInputStream(dirs[1]); + + case ResourceProvider.TYPE_WORD: + return new FileInputStream(dirs[2]); + } + + try { + if (entries[resType] != null) { + int vol, offset, compressed, uncompressed; + + vol = entries[resType].getVolume(resNumber); + offset = entries[resType].getOffset(resNumber); + + if ((vol != -1) && (offset != -1)) { + byte[] b; + RandomAccessFile file; + InputStream in; + + try { + b = new byte[7]; + file = new RandomAccessFile(vols[vol], "r"); + file.seek(offset); + file.read(b, 0, 7); + } catch (IndexOutOfBoundsException ioobex) { + throw new ResourceNotExistingException(); + } + + if ((b[0] != 0x12) || (b[1] != 0x34)) { + throw new CorruptedResourceException(); + } + + uncompressed = ByteCaster.lohiUnsignedShort(b, 3); + compressed = ByteCaster.lohiUnsignedShort(b, 5); + in = new SegmentedInputStream(file, offset + 7, compressed); + + if (resType == TYPE_PICTURE) { + in = new PictureInputStream(in); + } else { + if (compressed != uncompressed) { + in = new LZWInputStream(in); + } + } + + return in; + } + } + } catch (IndexOutOfBoundsException ioobex) { + throw new ResourceTypeInvalidException(); + } + + throw new ResourceNotExistingException(); + } + + protected File getVolumeFile(int vol) throws IOException { + File file = vols[vol]; + + if (!file.exists()) { + throw new FileNotFoundException(); + } + + return file; + } + + protected File getDirectoryFile(int resType) throws IOException { + File file; + + switch (resType) { + case ResourceProvider.TYPE_OBJECT: + file = dirs[1]; + break; + case ResourceProvider.TYPE_WORD: + file = dirs[2]; + break; + case ResourceProvider.TYPE_LOGIC: + case ResourceProvider.TYPE_PICTURE: + case ResourceProvider.TYPE_SOUND: + case ResourceProvider.TYPE_VIEW: + file = dirs[0]; + default: + return null; + } + + if (!file.exists()) { + throw new FileNotFoundException(); + } + + return file; + } + + public String getV3GameSig() { + return dirs[0].getName().toUpperCase().replaceAll("DIR$", ""); + } + + protected static class VolumeFilenameFilter implements java.io.FilenameFilter { + public boolean accept(File dir, String name) { + int c; + String s; + + c = name.lastIndexOf('.'); + + if (c == -1) { + return false; + } + + if (!Character.isDigit(name.charAt(c + 1))) { + return false; + } + + s = name.substring(0, c); + s = s.toLowerCase(); + + return s.endsWith("vol"); + } + } + + protected static class VolumeSorter implements java.util.Comparator { + public int compare(Object o1, Object o2) { + return ((File) o1).getName().compareToIgnoreCase(((File) o2).getName()); + } + } + + protected static class DirectoryFilenameFilter implements java.io.FilenameFilter { + public boolean accept(File dir, String name) { + if (name.equalsIgnoreCase("object")) { + return true; + } + + if (name.equalsIgnoreCase("words.tok")) { + return true; + } + + return name.toLowerCase().endsWith("dir"); + } + } + + protected static class DirectorySorter implements java.util.Comparator { + public int compare(Object o1, Object o2) { + String s1 = ((File) o1).getName(); + String s2 = ((File) o2).getName(); + + if (s1.toLowerCase().endsWith("dir")) { + if (!s2.toLowerCase().endsWith("dir")) { + return -1; + } + } else { + if (s2.toLowerCase().endsWith("dir")) { + return 1; + } + } + + return s1.compareToIgnoreCase(s2); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/sound/Sound.java b/core/src/main/java/com/sierra/agi/sound/Sound.java new file mode 100644 index 0000000..91f0300 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/sound/Sound.java @@ -0,0 +1,12 @@ +/* + * Sound.java + * Adventure Game Interpreter Sound Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.sound; + +public interface Sound { +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/sound/SoundProvider.java b/core/src/main/java/com/sierra/agi/sound/SoundProvider.java new file mode 100644 index 0000000..fe2b0e7 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/sound/SoundProvider.java @@ -0,0 +1,16 @@ +/* + * SoundProvider.java + * Adventure Game Interpreter Sound Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.sound; + +import java.io.IOException; +import java.io.InputStream; + +public interface SoundProvider { + Sound loadSound(InputStream inputStream) throws IOException; +} diff --git a/core/src/main/java/com/sierra/agi/view/Cel.java b/core/src/main/java/com/sierra/agi/view/Cel.java new file mode 100644 index 0000000..7823cdb --- /dev/null +++ b/core/src/main/java/com/sierra/agi/view/Cel.java @@ -0,0 +1,152 @@ +/* + * Cell.java + * Adventure Game Interpreter View Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.view; + +import com.sierra.agi.awt.EgaUtils; +import com.sierra.agi.io.ByteCaster; + +import java.awt.*; +import java.awt.image.ColorModel; +import java.awt.image.DirectColorModel; +import java.awt.image.IndexColorModel; +import java.awt.image.MemoryImageSource; + +/** + * @author Dr. Z + * @version 0.00.00.01 + */ +public class Cel { + /** + * Cell's Width + */ + protected short width; + + /** + * Cell's Height + */ + protected short height; + + /** + * Cell's Data + */ + protected int[] data; + + /** + * Cell's Transparent Color + */ + protected int transparent; + + /** + * Creates new Cell + */ + public Cel(byte[] b, int start, int loopNumber) { + width = ByteCaster.lohiUnsignedByte(b, start); + height = ByteCaster.lohiUnsignedByte(b, start + 1); + short trans = ByteCaster.lohiUnsignedByte(b, start + 2); + + byte transColor = (byte) (trans & 0x0F); + short mirrorInfo = (short) ((trans & 0xF0) >> 4); + + loadData(b, start + 3, transColor); + + if ((mirrorInfo & 0x8) != 0) { + if ((mirrorInfo & 0x7) != loopNumber) { + mirror(); + } + } + } + + protected void loadData(byte[] b, int off, byte transColor) { + int x; + + IndexColorModel indexModel = EgaUtils.getIndexColorModel(); + ColorModel nativeModel = EgaUtils.getNativeColorModel(); + + int[] pixel = new int[1]; + data = new int[width * height]; + + for (int j = 0, y = 0; y < height; y++) { + for (x = 0; b[off] != 0; off++) { + int color = (b[off] & 0xF0) >> 4; + int count = (b[off] & 0x0F); + + for (int i = 0; i < count; i++, j++, x++) { + nativeModel.getDataElements(indexModel.getRGB(color), pixel); + data[j] = pixel[0]; + } + } + + nativeModel.getDataElements(indexModel.getRGB(transColor), pixel); + + for (; x < width; j++, x++) { + data[j] = pixel[0]; + } + + off++; + } + + nativeModel.getDataElements(indexModel.getRGB(transColor), pixel); + transparent = pixel[0]; + } + + protected void mirror() { + for (int y = 0; y < height; y++) { + for (int x1 = width - 1, x2 = 0; x1 > x2; x1--, x2++) { + int i1 = (y * width) + x1; + int i2 = (y * width) + x2; + + int b = data[i1]; + data[i1] = data[i2]; + data[i2] = b; + } + } + } + + public short getWidth() { + return width; + } + + public short getHeight() { + return height; + } + + public int[] getPixelData() { + return data; + } + + public int getTransparentPixel() { + return transparent; + } + + /** + * Obtain an standard Image object that is a graphical representation of the + * cell. + * + * @param context Game context used to generate the image. + */ + public Image getImage() { + int[] data = this.data.clone(); + DirectColorModel colorModel = (DirectColorModel) ColorModel.getRGBdefault(); + DirectColorModel nativeModel = EgaUtils.getNativeColorModel(); + // int mask = colorModel.getAlphaMask(); + int[] pixel = new int[1]; + + for (int i = 0; i < (width * height); i++) { + colorModel.getDataElements(nativeModel.getRGB(data[i]), pixel); + + if (data[i] != transparent) { + data[i] = pixel[0]; + } else { + data[i] = 0x00ffffff; + } + } + + return Toolkit.getDefaultToolkit().createImage(new MemoryImageSource(width, height, colorModel, data, 0, width)); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/view/Loop.java b/core/src/main/java/com/sierra/agi/view/Loop.java new file mode 100644 index 0000000..0c4153e --- /dev/null +++ b/core/src/main/java/com/sierra/agi/view/Loop.java @@ -0,0 +1,51 @@ +/* + * Loop.java + * Adventure Game Interpreter View Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.view; + +import com.sierra.agi.io.ByteCaster; + +/** + * @author Dr. Z + * @version 0.00.00.01 + */ +public class Loop { + /** + * Cells + */ + protected Cel[] cels = null; + + /** + * Creates new Loop + */ + public Loop(Cel[] cels) { + this.cels = cels; + } + + public Loop(byte[] b, int start, int loopNumber) { + short cellCount; + int i, j; + + cellCount = ByteCaster.lohiUnsignedByte(b, start); + cels = new Cel[cellCount]; + + j = start + 1; + for (i = 0; i < cellCount; i++) { + cels[i] = new Cel(b, start + ByteCaster.lohiUnsignedShort(b, j), loopNumber); + j += 2; + } + } + + public Cel getCell(int cellNumber) { + return cels[cellNumber]; + } + + public short getCellCount() { + return (short) cels.length; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/view/StandardViewProvider.java b/core/src/main/java/com/sierra/agi/view/StandardViewProvider.java new file mode 100644 index 0000000..73830c3 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/view/StandardViewProvider.java @@ -0,0 +1,62 @@ +/* + * 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 com.sierra.agi.io.ByteCaster; +import com.sierra.agi.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public class StandardViewProvider implements ViewProvider { + public StandardViewProvider() { + } + + public View loadView(InputStream inputStream, int size) throws IOException, ViewException { + byte[] b; + int i, j; + short loopCount; + Loop[] loops = null; + String description = null; + + b = new byte[size]; + IOUtils.fill(inputStream, b, 0, size); + inputStream.close(); + + loopCount = b[2]; + if ((b[3] != 0) || (b[4] != 0)) { + /* Reads Description */ + int desc = ByteCaster.lohiUnsignedShort(b, 3); + + i = desc; + try { + while (true) { + if (b[i] == 0) { + break; + } + + i++; + } + } catch (IndexOutOfBoundsException e) { + } + + description = new String(b, desc, i - desc, StandardCharsets.US_ASCII); + } + + j = 5; + loops = new Loop[loopCount]; + for (i = 0; i < loopCount; i++) { + loops[i] = new Loop(b, ByteCaster.lohiUnsignedShort(b, j), i); + j += 2; + } + + return new View(loops, description); + } +} diff --git a/core/src/main/java/com/sierra/agi/view/View.java b/core/src/main/java/com/sierra/agi/view/View.java new file mode 100644 index 0000000..e2b8a45 --- /dev/null +++ b/core/src/main/java/com/sierra/agi/view/View.java @@ -0,0 +1,214 @@ +/* + * View.java + * Adventure Game Interpreter View Package + * + * Created by Dr. Z. + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.view; + +/** + * View object. + *

+ * View resources contain some of the graphics for the game. Unlike the picture + * resources which are full-screen background images, view resources are smaller + * 'sprites' used in the game, such as animations and objects. They are also + * stored as bitmaps, whereas pictures are stored in vector format. + *

+ * Each view resource consists of one or more 'loops'. Each loop in the resource + * consists of one or more 'cells' (frames). Thus several animations can be + * stored in one view, or a view can just be used for a single image. The + * maximum number of loops supported by the interpreter is 255 (0-254) and the + * maximum number of cells in each is 255 (0-254). + *

+ * View header (7+ bytes)
+ * Note: ls,ms means that the value is a two-byte word, with the least + * significant byte stored first, and the most significant byte stored second, + * e.g. 12 07 is acually 712 (hex) or 1810 (decimal). Most word values in AGI + * are stored like this, but not all. + *

+ * + * + * + * + * + * + * + * + * + * + *
ByteMeaning
0Unknown (always seems to be either 1 or 2)
1Unknown (always seems to be 1)
2Number of loops
3-4Position of description (more on this later) (ls,ms) Both bytes are 0 if there is no description.
5-6Position of first loop (ls,ms)
7-8Position of second loop (if any) (ls,ms)
9-10Position of third loop (if any) (ls,ms)
....
+ *

+ * Note: Two of these loop references CAN point to the same place. This is done + * when you want to use mirroring (more on this later). + *

+ * + * + * + * + * + * + * + *
Loop Header (3+ bytes)
ByteMeaning
0Number of cells in this loop
1-2Position of first cell, relative to start of loop (ls,ms)
3-4Position of second cell (if any), relative to start of loop (ls,ms)
5-6Position of third cell (if any), relative to start of loop (ls,ms)
+ *

+ * + * + * + * + * + * + *
Cell Header (3 bytes)
ByteMeaning
0Width of cell (remember that AGI pixels are 2 normal EGA pixels wide so a cel of width 12 is actually 24 pixels wide on screen)
1Height of cell
2Transparency and cell mirroring
+ *

+ * 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. + *

    + * + * + * + * + * + * + * + *
    ByteMeaning
    0-1Hi and then Lo byte for 'A' offset
    ...
    50-51Hi and then Lo byte for 'Z' offset
    52Words 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). + *

    + * + * + * + * + * + * + * + * + * + *
    ByteMeaning
    0Number of characters to include from start of prevous word
    1Char 1 (xor 0x7F gives the ASCII code for the character)
    2Char 2
    ...
    nLast char
    n + 1Wordnum (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
    0Words are ignored (e.g. the, at)
    1Anyword
    9999ROL (Rest Of Line) -- it does matter what the rest of the input list is
    + *

    + * + * @author Dr. Z, Lance Ewing (Documentation) + * @version 0.00.00.01 + */ +public class Words implements WordsProvider { + protected Map wordHash = new HashMap(800); + + protected Map wordNumToWordMap = new HashMap(); + + /** + * Creates a new Word container. + */ + public Words() { + } + + private static String removeSpaces(String inputString) { + StringBuffer buff = new StringBuffer(inputString.length()); + StringTokenizer token = new StringTokenizer(inputString.trim(), " "); + + while (token.hasMoreTokens()) { + buff.append(token.nextToken()); + + if (token.hasMoreTokens()) { + buff.append(" "); + } + } + + return buff.toString(); + } + + private static int findChar(String str, int begin) { + int ch = str.indexOf(' ', begin); + + if (ch < 0) { + ch = str.length(); + } + + return ch; + } + + public Words loadWords(InputStream stream) throws IOException { + loadWordTable(stream); + return this; + } + + /** + * Read a AGI word table. + * + * @param stream Stream from where to read the words. + * @return Returns the number of words readed. + */ + protected int loadWordTable(InputStream stream) throws IOException { + ByteCasterStream bstream = new ByteCasterStream(stream); + String prev = null; + String curr; + int i, wordNum, wordCount; + + IOUtils.skip(stream, 52); + wordCount = 0; + + while (true) { + i = stream.read(); + + if (i < 0) { + break; + } else if (i > 0) { + curr = prev.substring(0, i); + } else { + curr = ""; + } + + while (true) { + i = stream.read(); + + if (i <= 0) { + break; + } else { + curr += (char) ((i ^ 0x7F) & 0x7F); + + if (i >= 0x7F) { + break; + } + } + } + + if (i <= 0) { + break; + } + + wordNum = bstream.hiloReadUnsignedShort(); + prev = curr; + + addWord(wordNum, curr); + wordCount++; + } + + return wordCount; + } + + private boolean addWord(int wordNum, String word) { + Word w = wordHash.get(word); + + if (w != null) { + return false; + } + + w = new Word(); + w.number = wordNum; + w.text = word; + + // Map of word text to the Word object. + wordHash.put(word, w); + + // Map of word number to the Word object. + wordNumToWordMap.put(wordNum, w); + + return true; + } + + public Word getWordByNumber(int wordNum) { + return wordNumToWordMap.get(wordNum); + } + + public Word findWord(String word) { + return wordHash.get(word); + } + + public int getWordCount() { + return wordHash.size(); + } + + public Collection words() { + return wordHash.values(); + } + + public List parse(String inputString) { + List vector = new ArrayList(5); + int begin, end; + Word word; + + inputString = inputString.toLowerCase(); + inputString = removeSpaces(inputString); + begin = 0; + + while (inputString.length() > 0) { + end = findChar(inputString, begin); + word = findWord(inputString.substring(0, end)); + + if (word != null) { + begin = 0; + + try { + inputString = inputString.substring(end + 1); + } catch (StringIndexOutOfBoundsException sioobex) { + inputString = ""; + } + + if (word.number == 9999) { + return vector; + } + + if (word.number != 0) { + vector.add(word); + } + + continue; + } + + if (end >= inputString.length()) { + begin = 0; + end = findChar(inputString, 0); + + word = new Word(); + word.number = -1; + word.text = inputString.substring(0, end); + vector.add(word); + + if (end >= inputString.length()) { + break; + } + + inputString = inputString.substring(end + 1); + continue; + } + + begin = end + 1; + } + + System.out.println("Words.java = " + vector); + return vector; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/sierra/agi/word/WordsProvider.java b/core/src/main/java/com/sierra/agi/word/WordsProvider.java new file mode 100644 index 0000000..adad4be --- /dev/null +++ b/core/src/main/java/com/sierra/agi/word/WordsProvider.java @@ -0,0 +1,16 @@ +/* + * WordsProvider.java + * Adventure Game Interpreter Word Package + * + * Created by Dr. Z + * Copyright (c) 2001 Dr. Z. All rights reserved. + */ + +package com.sierra.agi.word; + +import java.io.IOException; +import java.io.InputStream; + +public interface WordsProvider { + Words loadWords(InputStream in) throws IOException; +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..95d583e --- /dev/null +++ b/gradle.properties @@ -0,0 +1,9 @@ +org.gradle.daemon=true +org.gradle.jvmargs=-Xms512M -Xmx1G +org.gradle.configureondemand=false +android.enableR8.fullMode=false +graalHelperVersion=2.0.0 +enableGraalNative=false +gwtFrameworkVersion=2.10.0 +gwtPluginVersion=1.1.29 +gdxVersion=1.12.1 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7f93135c49b765f8051ef9d0a6055ff8e46073d8 GIT binary patch literal 63721 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鯊%1Et zwk~hMkhq;MOw^Q5`7oC{CUUyTw9x>^%*FHx^qJw(LB+E0WBX@{Ghw;)6aA-KyYg8p z7XDveQOpEr;B4je@2~usI5BlFadedX^ma{b{ypd|RNYqo#~d*mj&y`^iojR}s%~vF z(H!u`yx68D1Tj(3(m;Q+Ma}s2n#;O~bcB1`lYk%Irx60&-nWIUBr2x&@}@76+*zJ5 ze&4?q8?m%L9c6h=J$WBzbiTf1Z-0Eb5$IZs>lvm$>1n_Mezp*qw_pr8<8$6f)5f<@ zyV#tzMCs51nTv_5ca`x`yfE5YA^*%O_H?;tWYdM_kHPubA%vy47i=9>Bq) zRQ&0UwLQHeswmB1yP)+BiR;S+Vc-5TX84KUA;8VY9}yEj0eESSO`7HQ4lO z4(CyA8y1G7_C;6kd4U3K-aNOK!sHE}KL_-^EDl(vB42P$2Km7$WGqNy=%fqB+ zSLdrlcbEH=T@W8V4(TgoXZ*G1_aq$K^@ek=TVhoKRjw;HyI&coln|uRr5mMOy2GXP zwr*F^Y|!Sjr2YQXX(Fp^*`Wk905K%$bd03R4(igl0&7IIm*#f`A!DCarW9$h$z`kYk9MjjqN&5-DsH@8xh63!fTNPxWsFQhNv z#|3RjnP$Thdb#Ys7M+v|>AHm0BVTw)EH}>x@_f4zca&3tXJhTZ8pO}aN?(dHo)44Z z_5j+YP=jMlFqwvf3lq!57-SAuRV2_gJ*wsR_!Y4Z(trO}0wmB9%f#jNDHPdQGHFR; zZXzS-$`;7DQ5vF~oSgP3bNV$6Z(rwo6W(U07b1n3UHqml>{=6&-4PALATsH@Bh^W? z)ob%oAPaiw{?9HfMzpGb)@Kys^J$CN{uf*HX?)z=g`J(uK1YO^8~s1(ZIbG%Et(|q z$D@_QqltVZu9Py4R0Ld8!U|#`5~^M=b>fnHthzKBRr=i+w@0Vr^l|W;=zFT#PJ?*a zbC}G#It}rQP^Ait^W&aa6B;+0gNvz4cWUMzpv(1gvfw-X4xJ2Sv;mt;zb2Tsn|kSS zo*U9N?I{=-;a-OybL4r;PolCfiaL=y@o9{%`>+&FI#D^uy#>)R@b^1ue&AKKwuI*` zx%+6r48EIX6nF4o;>)zhV_8(IEX})NGU6Vs(yslrx{5fII}o3SMHW7wGtK9oIO4OM&@@ECtXSICLcPXoS|{;=_yj>hh*%hP27yZwOmj4&Lh z*Nd@OMkd!aKReoqNOkp5cW*lC)&C$P?+H3*%8)6HcpBg&IhGP^77XPZpc%WKYLX$T zsSQ$|ntaVVOoRat$6lvZO(G-QM5s#N4j*|N_;8cc2v_k4n6zx9c1L4JL*83F-C1Cn zaJhd;>rHXB%%ZN=3_o3&Qd2YOxrK~&?1=UuN9QhL$~OY-Qyg&})#ez*8NpQW_*a&kD&ANjedxT0Ar z<6r{eaVz3`d~+N~vkMaV8{F?RBVemN(jD@S8qO~L{rUw#=2a$V(7rLE+kGUZ<%pdr z?$DP|Vg#gZ9S}w((O2NbxzQ^zTot=89!0^~hE{|c9q1hVzv0?YC5s42Yx($;hAp*E zyoGuRyphQY{Q2ee0Xx`1&lv(l-SeC$NEyS~8iil3_aNlnqF_G|;zt#F%1;J)jnPT& z@iU0S;wHJ2$f!juqEzPZeZkjcQ+Pa@eERSLKsWf=`{R@yv7AuRh&ALRTAy z8=g&nxsSJCe!QLchJ=}6|LshnXIK)SNd zRkJNiqHwKK{SO;N5m5wdL&qK`v|d?5<4!(FAsDxR>Ky#0#t$8XCMptvNo?|SY?d8b z`*8dVBlXTUanlh6n)!EHf2&PDG8sXNAt6~u-_1EjPI1|<=33T8 zEnA00E!`4Ave0d&VVh0e>)Dc}=FfAFxpsC1u9ATfQ`-Cu;mhc8Z>2;uyXtqpLb7(P zd2F9<3cXS} znMg?{&8_YFTGRQZEPU-XPq55%51}RJpw@LO_|)CFAt62-_!u_Uq$csc+7|3+TV_!h z+2a7Yh^5AA{q^m|=KSJL+w-EWDBc&I_I1vOr^}P8i?cKMhGy$CP0XKrQzCheG$}G# zuglf8*PAFO8%xop7KSwI8||liTaQ9NCAFarr~psQt)g*pC@9bORZ>m`_GA`_K@~&% zijH0z;T$fd;-Liw8%EKZas>BH8nYTqsK7F;>>@YsE=Rqo?_8}UO-S#|6~CAW0Oz1} z3F(1=+#wrBJh4H)9jTQ_$~@#9|Bc1Pd3rAIA_&vOpvvbgDJOM(yNPhJJq2%PCcMaI zrbe~toYzvkZYQ{ea(Wiyu#4WB#RRN%bMe=SOk!CbJZv^m?Flo5p{W8|0i3`hI3Np# zvCZqY%o258CI=SGb+A3yJe~JH^i{uU`#U#fvSC~rWTq+K`E%J@ zasU07&pB6A4w3b?d?q}2=0rA#SA7D`X+zg@&zm^iA*HVi z009#PUH<%lk4z~p^l0S{lCJk1Uxi=F4e_DwlfHA`X`rv(|JqWKAA5nH+u4Da+E_p+ zVmH@lg^n4ixs~*@gm_dgQ&eDmE1mnw5wBz9Yg?QdZwF|an67Xd*x!He)Gc8&2!urh z4_uXzbYz-aX)X1>&iUjGp;P1u8&7TID0bTH-jCL&Xk8b&;;6p2op_=y^m@Nq*0{#o!!A;wNAFG@0%Z9rHo zcJs?Th>Ny6+hI`+1XoU*ED$Yf@9f91m9Y=#N(HJP^Y@ZEYR6I?oM{>&Wq4|v0IB(p zqX#Z<_3X(&{H+{3Tr|sFy}~=bv+l=P;|sBz$wk-n^R`G3p0(p>p=5ahpaD7>r|>pm zv;V`_IR@tvZreIuv2EM7ZQHhO+qUgw#kOs%*ekY^n|=1#x9&c;Ro&I~{rG-#_3ZB1 z?|9}IFdbP}^DneP*T-JaoYHt~r@EfvnPE5EKUwIxjPbsr$% zfWW83pgWST7*B(o=kmo)74$8UU)v0{@4DI+ci&%=#90}!CZz|rnH+Mz=HN~97G3~@ z;v5(9_2%eca(9iu@J@aqaMS6*$TMw!S>H(b z4(*B!|H|8&EuB%mITr~O?vVEf%(Gr)6E=>H~1VR z&1YOXluJSG1!?TnT)_*YmJ*o_Q@om~(GdrhI{$Fsx_zrkupc#y{DK1WOUR>tk>ZE) ziOLoBkhZZ?0Uf}cm>GsA>Rd6V8@JF)J*EQlQ<=JD@m<)hyElXR0`pTku*3MU`HJn| zIf7$)RlK^pW-$87U;431;Ye4Ie+l~_B3*bH1>*yKzn23cH0u(i5pXV! z4K?{3oF7ZavmmtTq((wtml)m6i)8X6ot_mrE-QJCW}Yn!(3~aUHYG=^fA<^~`e3yc z-NWTb{gR;DOUcK#zPbN^D*e=2eR^_!(!RKkiwMW@@yYtEoOp4XjOGgzi`;=8 zi3`Ccw1%L*y(FDj=C7Ro-V?q)-%p?Ob2ZElu`eZ99n14-ZkEV#y5C+{Pq87Gu3&>g zFy~Wk7^6v*)4pF3@F@rE__k3ikx(hzN3@e*^0=KNA6|jC^B5nf(XaoQaZN?Xi}Rn3 z$8&m*KmWvPaUQ(V<#J+S&zO|8P-#!f%7G+n_%sXp9=J%Z4&9OkWXeuZN}ssgQ#Tcj z8p6ErJQJWZ+fXLCco=RN8D{W%+*kko*2-LEb))xcHwNl~Xmir>kmAxW?eW50Osw3# zki8Fl$#fvw*7rqd?%E?}ZX4`c5-R&w!Y0#EBbelVXSng+kUfeUiqofPehl}$ormli zg%r)}?%=?_pHb9`Cq9Z|B`L8b>(!+8HSX?`5+5mm81AFXfnAt1*R3F z%b2RPIacKAddx%JfQ8l{3U|vK@W7KB$CdLqn@wP^?azRks@x8z59#$Q*7q!KilY-P zHUbs(IFYRGG1{~@RF;Lqyho$~7^hNC`NL3kn^Td%A7dRgr_&`2k=t+}D-o9&C!y^? z6MsQ=tc3g0xkK(O%DzR9nbNB(r@L;1zQrs8mzx&4dz}?3KNYozOW5;=w18U6$G4U2 z#2^qRLT*Mo4bV1Oeo1PKQ2WQS2Y-hv&S|C7`xh6=Pj7MNLC5K-zokZ67S)C;(F0Dd zloDK2_o1$Fmza>EMj3X9je7e%Q`$39Dk~GoOj89-6q9|_WJlSl!!+*{R=tGp z8u|MuSwm^t7K^nUe+^0G3dkGZr3@(X+TL5eah)K^Tn zXEtHmR9UIaEYgD5Nhh(s*fcG_lh-mfy5iUF3xxpRZ0q3nZ=1qAtUa?(LnT9I&~uxX z`pV?+=|-Gl(kz?w!zIieXT}o}7@`QO>;u$Z!QB${a08_bW0_o@&9cjJUXzVyNGCm8 zm=W+$H!;_Kzp6WQqxUI;JlPY&`V}9C$8HZ^m?NvI*JT@~BM=()T()Ii#+*$y@lTZBkmMMda>7s#O(1YZR+zTG@&}!EXFG{ zEWPSDI5bFi;NT>Yj*FjH((=oe%t%xYmE~AGaOc4#9K_XsVpl<4SP@E!TgC0qpe1oi zNpxU2b0(lEMcoibQ-G^cxO?ySVW26HoBNa;n0}CWL*{k)oBu1>F18X061$SP{Gu67 z-v-Fa=Fl^u3lnGY^o5v)Bux}bNZ~ z5pL+7F_Esoun8^5>z8NFoIdb$sNS&xT8_|`GTe8zSXQzs4r^g0kZjg(b0bJvz`g<70u9Z3fQILX1Lj@;@+##bP|FAOl)U^9U>0rx zGi)M1(Hce)LAvQO-pW!MN$;#ZMX?VE(22lTlJrk#pB0FJNqVwC+*%${Gt#r_tH9I_ z;+#)#8cWAl?d@R+O+}@1A^hAR1s3UcW{G+>;X4utD2d9X(jF555}!TVN-hByV6t+A zdFR^aE@GNNgSxxixS2p=on4(+*+f<8xrwAObC)D5)4!z7)}mTpb7&ofF3u&9&wPS< zB62WHLGMhmrmOAgmJ+|c>qEWTD#jd~lHNgT0?t-p{T=~#EMcB| z=AoDKOL+qXCfk~F)-Rv**V}}gWFl>liXOl7Uec_8v)(S#av99PX1sQIVZ9eNLkhq$ zt|qu0b?GW_uo}TbU8!jYn8iJeIP)r@;!Ze_7mj{AUV$GEz6bDSDO=D!&C9!M@*S2! zfGyA|EPlXGMjkH6x7OMF?gKL7{GvGfED=Jte^p=91FpCu)#{whAMw`vSLa`K#atdN zThnL+7!ZNmP{rc=Z>%$meH;Qi1=m1E3Lq2D_O1-X5C;!I0L>zur@tPAC9*7Jeh)`;eec}1`nkRP(%iv-`N zZ@ip-g|7l6Hz%j%gcAM}6-nrC8oA$BkOTz^?dakvX?`^=ZkYh%vUE z9+&)K1UTK=ahYiaNn&G5nHUY5niLGus@p5E2@RwZufRvF{@$hW{;{3QhjvEHMvduO z#Wf-@oYU4ht?#uP{N3utVzV49mEc9>*TV_W2TVC`6+oI)zAjy$KJrr=*q##&kobiQ z1vNbya&OVjK`2pdRrM?LuK6BgrLN7H_3m z!qpNKg~87XgCwb#I=Q&0rI*l$wM!qTkXrx1ko5q-f;=R2fImRMwt5Qs{P*p^z@9ex z`2#v(qE&F%MXlHpdO#QEZyZftn4f05ab^f2vjxuFaat2}jke{j?5GrF=WYBR?gS(^ z9SBiNi}anzBDBRc+QqizTTQuJrzm^bNA~A{j%ugXP7McZqJ}65l10({wk++$=e8O{ zxWjG!Qp#5OmI#XRQQM?n6?1ztl6^D40hDJr?4$Wc&O_{*OfMfxe)V0=e{|N?J#fgE>j9jAajze$iN!*yeF%jJU#G1c@@rm zolGW!j?W6Q8pP=lkctNFdfgUMg92wlM4E$aks1??M$~WQfzzzXtS)wKrr2sJeCN4X zY(X^H_c^PzfcO8Bq(Q*p4c_v@F$Y8cHLrH$`pJ2}=#*8%JYdqsqnGqEdBQMpl!Ot04tUGSXTQdsX&GDtjbWD=prcCT9(+ z&UM%lW%Q3yrl1yiYs;LxzIy>2G}EPY6|sBhL&X&RAQrSAV4Tlh2nITR?{6xO9ujGu zr*)^E`>o!c=gT*_@6S&>0POxcXYNQd&HMw6<|#{eSute2C3{&h?Ah|cw56-AP^f8l zT^kvZY$YiH8j)sk7_=;gx)vx-PW`hbSBXJGCTkpt;ap(}G2GY=2bbjABU5)ty%G#x zAi07{Bjhv}>OD#5zh#$0w;-vvC@^}F! z#X$@)zIs1L^E;2xDAwEjaXhTBw2<{&JkF*`;c3<1U@A4MaLPe{M5DGGkL}#{cHL%* zYMG+-Fm0#qzPL#V)TvQVI|?_M>=zVJr9>(6ib*#z8q@mYKXDP`k&A4A};xMK0h=yrMp~JW{L?mE~ph&1Y1a#4%SO)@{ zK2juwynUOC)U*hVlJU17%llUxAJFuKZh3K0gU`aP)pc~bE~mM!i1mi!~LTf>1Wp< zuG+ahp^gH8g8-M$u{HUWh0m^9Rg@cQ{&DAO{PTMudV6c?ka7+AO& z746QylZ&Oj`1aqfu?l&zGtJnpEQOt;OAFq19MXTcI~`ZcoZmyMrIKDFRIDi`FH)w; z8+*8tdevMDv*VtQi|e}CnB_JWs>fhLOH-+Os2Lh!&)Oh2utl{*AwR)QVLS49iTp{6 z;|172Jl!Ml17unF+pd+Ff@jIE-{Oxv)5|pOm@CkHW?{l}b@1>Pe!l}VccX#xp@xgJ zyE<&ep$=*vT=}7vtvif0B?9xw_3Gej7mN*dOHdQPtW5kA5_zGD zpA4tV2*0E^OUimSsV#?Tg#oiQ>%4D@1F5@AHwT8Kgen$bSMHD3sXCkq8^(uo7CWk`mT zuslYq`6Yz;L%wJh$3l1%SZv#QnG3=NZ=BK4yzk#HAPbqXa92;3K5?0kn4TQ`%E%X} z&>Lbt!!QclYKd6+J7Nl@xv!uD%)*bY-;p`y^ZCC<%LEHUi$l5biu!sT3TGGSTPA21 zT8@B&a0lJHVn1I$I3I1I{W9fJAYc+8 zVj8>HvD}&O`TqU2AAb={?eT;0hyL(R{|h23=4fDSZKC32;wWxsVj`P z3J3{M$PwdH!ro*Cn!D&=jnFR>BNGR<<|I8CI@+@658Dy(lhqbhXfPTVecY@L8%`3Q z1Fux2w?2C3th60jI~%OC9BtpNF$QPqcG+Pz96qZJ71_`0o0w_q7|h&O>`6U+^BA&5 zXd5Zp1Xkw~>M%RixTm&OqpNl8Q+ue=92Op_>T~_9UON?ZM2c0aGm=^A4ejrXj3dV9 zhh_bCt-b9`uOX#cFLj!vhZ#lS8Tc47OH>*)y#{O9?AT~KR9LntM|#l#Dlm^8{nZdk zjMl#>ZM%#^nK2TPzLcKxqx24P7R1FPlBy7LSBrRvx>fE$9AJ;7{PQm~^LBX^k#6Zq zw*Z(zJC|`!6_)EFR}8|n8&&Rbj8y028~P~sFXBFRt+tmqH-S3<%N;C&WGH!f3{7cm zy_fCAb9@HqaXa1Y5vFbxWf%#zg6SI$C+Uz5=CTO}e|2fjWkZ;Dx|84Ow~bkI=LW+U zuq;KSv9VMboRvs9)}2PAO|b(JCEC_A0wq{uEj|3x@}*=bOd zwr{TgeCGG>HT<@Zeq8y}vTpwDg#UBvD)BEs@1KP$^3$sh&_joQPn{hjBXmLPJ{tC) z*HS`*2+VtJO{|e$mM^|qv1R*8i(m1`%)}g=SU#T#0KlTM2RSvYUc1fP+va|4;5}Bfz98UvDCpq7}+SMV&;nX zQw~N6qOX{P55{#LQkrZk(e5YGzr|(B;Q;ju;2a`q+S9bsEH@i1{_Y0;hWYn1-79jl z5c&bytD*k)GqrVcHn6t-7kinadiD>B{Tl`ZY@`g|b~pvHh5!gKP4({rp?D0aFd_cN zhHRo4dd5^S6ViN(>(28qZT6E>??aRhc($kP`>@<+lIKS5HdhjVU;>f7<4))E*5|g{ z&d1}D|vpuV^eRj5j|xx9nwaCxXFG?Qbjn~_WSy=N}P0W>MP zG-F%70lX5Xr$a)2i6?i|iMyM|;Jtf*hO?=Jxj12oz&>P=1#h~lf%#fc73M2_(SUM- zf&qnjS80|_Y0lDgl&I?*eMumUklLe_=Td!9G@eR*tcPOgIShJipp3{A10u(4eT~DY zHezEj8V+7m!knn7)W!-5QI3=IvC^as5+TW1@Ern@yX| z7Nn~xVx&fGSr+L%4iohtS3w^{-H1A_5=r&x8}R!YZvp<2T^YFvj8G_vm}5q;^UOJf ztl=X3iL;;^^a#`t{Ae-%5Oq{?M#s6Npj+L(n-*LMI-yMR{)qki!~{5z{&`-iL}lgW zxo+tnvICK=lImjV$Z|O_cYj_PlEYCzu-XBz&XC-JVxUh9;6*z4fuBG+H{voCC;`~GYV|hj%j_&I zDZCj>Q_0RCwFauYoVMiUSB+*Mx`tg)bWmM^SwMA+?lBg12QUF_x2b)b?qb88K-YUd z0dO}3k#QirBV<5%jL$#wlf!60dizu;tsp(7XLdI=eQs?P`tOZYMjVq&jE)qK*6B^$ zBe>VvH5TO>s>izhwJJ$<`a8fakTL!yM^Zfr2hV9`f}}VVUXK39p@G|xYRz{fTI+Yq z20d=)iwjuG9RB$%$^&8#(c0_j0t_C~^|n+c`Apu|x7~;#cS-s=X1|C*YxX3ailhg_|0`g!E&GZJEr?bh#Tpb8siR=JxWKc{#w7g zWznLwi;zLFmM1g8V5-P#RsM@iX>TK$xsWuujcsVR^7TQ@!+vCD<>Bk9tdCo7Mzgq5 zv8d>dK9x8C@Qoh01u@3h0X_`SZluTb@5o;{4{{eF!-4405x8X7hewZWpz z2qEi4UTiXTvsa(0X7kQH{3VMF>W|6;6iTrrYD2fMggFA&-CBEfSqPlQDxqsa>{e2M z(R5PJ7uOooFc|9GU0ELA%m4&4Ja#cQpNw8i8ACAoK6?-px+oBl_yKmenZut#Xumjz zk8p^OV2KY&?5MUwGrBOo?ki`Sxo#?-Q4gw*Sh0k`@ zFTaYK2;}%Zk-68`#5DXU$2#=%YL#S&MTN8bF+!J2VT6x^XBci6O)Q#JfW{YMz) zOBM>t2rSj)n#0a3cjvu}r|k3od6W(SN}V-cL?bi*Iz-8uOcCcsX0L>ZXjLqk zZu2uHq5B|Kt>e+=pPKu=1P@1r9WLgYFq_TNV1p9pu0erHGd!+bBp!qGi+~4A(RsYN@CyXNrC&hxGmW)u5m35OmWwX`I+0yByglO`}HC4nGE^_HUs^&A(uaM zKPj^=qI{&ayOq#z=p&pnx@@k&I1JI>cttJcu@Ihljt?6p^6{|ds`0MoQwp+I{3l6` zB<9S((RpLG^>=Kic`1LnhpW2=Gu!x`m~=y;A`Qk!-w`IN;S8S930#vBVMv2vCKi}u z6<-VPrU0AnE&vzwV(CFC0gnZYcpa-l5T0ZS$P6(?9AM;`Aj~XDvt;Jua=jIgF=Fm? zdp=M$>`phx%+Gu};;-&7T|B1AcC#L4@mW5SV_^1BRbo6;2PWe$r+npRV`yc;T1mo& z+~_?7rA+(Um&o@Tddl zL_hxvWk~a)yY}%j`Y+200D%9$bWHy&;(yj{jpi?Rtz{J66ANw)UyPOm;t6FzY3$hx zcn)Ir79nhFvNa7^a{SHN7XH*|Vlsx`CddPnA&Qvh8aNhEA;mPVv;Ah=k<*u!Zq^7 z<=xs*iQTQOMMcg|(NA_auh@x`3#_LFt=)}%SQppP{E>mu_LgquAWvh<>L7tf9+~rO znwUDS52u)OtY<~!d$;m9+87aO+&`#2ICl@Y>&F{jI=H(K+@3M1$rr=*H^dye#~TyD z!){#Pyfn+|ugUu}G;a~!&&0aqQ59U@UT3|_JuBlYUpT$2+11;}JBJ`{+lQN9T@QFY z5+`t;6(TS0F?OlBTE!@7D`8#URDNqx2t6`GZ{ZgXeS@v%-eJzZOHz18aS|svxII$a zZeFjrJ*$IwX$f-Rzr_G>xbu@euGl)B7pC&S+CmDJBg$BoV~jxSO#>y z33`bupN#LDoW0feZe0%q8un0rYN|eRAnwDHQ6e_)xBTbtoZtTA=Fvk){q}9Os~6mQ zKB80VI_&6iSq`LnK7*kfHZoeX6?WE}8yjuDn=2#JG$+;-TOA1%^=DnXx%w{b=w}tS zQbU3XxtOI8E(!%`64r2`zog;5<0b4i)xBmGP^jiDZ2%HNSxIf3@wKs~uk4%3Mxz;~ zts_S~E4>W+YwI<-*-$U8*^HKDEa8oLbmqGg?3vewnaNg%Mm)W=)lcC_J+1ov^u*N3 zXJ?!BrH-+wGYziJq2Y#vyry6Z>NPgkEk+Ke`^DvNRdb>Q2Nlr#v%O@<5hbflI6EKE z9dWc0-ORk^T}jP!nkJ1imyjdVX@GrjOs%cpgA8-c&FH&$(4od#x6Y&=LiJZPINVyW z0snY$8JW@>tc2}DlrD3StQmA0Twck~@>8dSix9CyQOALcREdxoM$Sw*l!}bXKq9&r zysMWR@%OY24@e`?+#xV2bk{T^C_xSo8v2ZI=lBI*l{RciPwuE>L5@uhz@{!l)rtVlWC>)6(G)1~n=Q|S!{E9~6*fdpa*n z!()-8EpTdj=zr_Lswi;#{TxbtH$8*G=UM`I+icz7sr_SdnHXrv=?iEOF1UL+*6O;% zPw>t^kbW9X@oEXx<97%lBm-9?O_7L!DeD)Me#rwE54t~UBu9VZ zl_I1tBB~>jm@bw0Aljz8! zXBB6ATG6iByKIxs!qr%pz%wgqbg(l{65DP4#v(vqhhL{0b#0C8mq`bnqZ1OwFV z7mlZZJFMACm>h9v^2J9+^_zc1=JjL#qM5ZHaThH&n zXPTsR8(+)cj&>Un{6v*z?@VTLr{TmZ@-fY%*o2G}*G}#!bmqpoo*Ay@U!JI^Q@7gj;Kg-HIrLj4}#ec4~D2~X6vo;ghep-@&yOivYP zC19L0D`jjKy1Yi-SGPAn94(768Tcf$urAf{)1)9W58P`6MA{YG%O?|07!g9(b`8PXG1B1Sh0?HQmeJtP0M$O$hI z{5G`&9XzYhh|y@qsF1GnHN|~^ru~HVf#)lOTSrv=S@DyR$UKQk zjdEPFDz{uHM&UM;=mG!xKvp;xAGHOBo~>_=WFTmh$chpC7c`~7?36h)7$fF~Ii}8q zF|YXxH-Z?d+Q+27Rs3X9S&K3N+)OBxMHn1u(vlrUC6ckBY@@jl+mgr#KQUKo#VeFm zFwNYgv0<%~Wn}KeLeD9e1$S>jhOq&(e*I@L<=I5b(?G(zpqI*WBqf|Zge0&aoDUsC zngMRA_Kt0>La+Erl=Uv_J^p(z=!?XHpenzn$%EA`JIq#yYF?JLDMYiPfM(&Csr#f{ zdd+LJL1by?xz|D8+(fgzRs~(N1k9DSyK@LJygwaYX8dZl0W!I&c^K?7)z{2is;OkE zd$VK-(uH#AUaZrp=1z;O*n=b?QJkxu`Xsw&7yrX0?(CX=I-C#T;yi8a<{E~?vr3W> zQrpPqOW2M+AnZ&p{hqmHZU-;Q(7?- zP8L|Q0RM~sB0w1w53f&Kd*y}ofx@c z5Y6B8qGel+uT1JMot$nT1!Tim6{>oZzJXdyA+4euOLME?5Fd_85Uk%#E*ln%y{u8Q z$|?|R@Hpb~yTVK-Yr_S#%NUy7EBfYGAg>b({J|5b+j-PBpPy$Ns`PaJin4JdRfOaS zE|<HjH%NuJgsd2wOlv>~y=np%=2)$M9LS|>P)zJ+Fei5vYo_N~B0XCn+GM76 z)Xz3tg*FRVFgIl9zpESgdpWAavvVViGlU8|UFY{{gVJskg*I!ZjWyk~OW-Td4(mZ6 zB&SQreAAMqwp}rjy`HsG({l2&q5Y52<@AULVAu~rWI$UbFuZs>Sc*x+XI<+ez%$U)|a^unjpiW0l0 zj1!K0(b6$8LOjzRqQ~K&dfbMIE=TF}XFAi)$+h}5SD3lo z%%Qd>p9se=VtQG{kQ;N`sI)G^u|DN#7{aoEd zkksYP%_X$Rq08);-s6o>CGJ<}v`qs%eYf+J%DQ^2k68C%nvikRsN?$ap--f+vCS`K z#&~)f7!N^;sdUXu54gl3L=LN>FB^tuK=y2e#|hWiWUls__n@L|>xH{%8lIJTd5`w? zSwZbnS;W~DawT4OwSJVdAylbY+u5S+ZH{4hAi2&}Iv~W(UvHg(1GTZRPz`@{SOqzy z(8g&Dz=$PfRV=6FgxN~zo+G8OoPI&d-thcGVR*_^(R8COTM@bq?fDwY{}WhsQS1AK zF6R1t8!RdFmfocpJ6?9Yv~;WYi~XPgs(|>{5})j!AR!voO7y9&cMPo#80A(`za@t>cx<0;qxM@S*m(jYP)dMXr*?q0E`oL;12}VAep179uEr8c<=D zr5?A*C{eJ`z9Ee;E$8)MECqatHkbHH z&Y+ho0B$31MIB-xm&;xyaFCtg<{m~M-QDbY)fQ>Q*Xibb~8ytxZQ?QMf9!%cV zU0_X1@b4d+Pg#R!`OJ~DOrQz3@cpiGy~XSKjZQQ|^4J1puvwKeScrH8o{bscBsowomu z^f12kTvje`yEI3eEXDHJ6L+O{Jv$HVj%IKb|J{IvD*l6IG8WUgDJ*UGz z3!C%>?=dlfSJ>4U88)V+`U-!9r^@AxJBx8R;)J4Fn@`~k>8>v0M9xp90OJElWP&R5 zM#v*vtT}*Gm1^)Bv!s72T3PB0yVIjJW)H7a)ilkAvoaH?)jjb`MP>2z{%Y?}83 zUIwBKn`-MSg)=?R)1Q0z3b>dHE^)D8LFs}6ASG1|daDly_^lOSy&zIIhm*HXm1?VS=_iacG);_I9c zUQH1>i#*?oPIwBMJkzi_*>HoUe}_4o>2(SHWzqQ=;TyhAHS;Enr7!#8;sdlty&(>d zl%5cjri8`2X^Ds`jnw7>A`X|bl=U8n+3LKLy(1dAu8`g@9=5iw$R0qk)w8Vh_Dt^U zIglK}sn^)W7aB(Q>HvrX=rxB z+*L)3DiqpQ_%~|m=44LcD4-bxO3OO*LPjsh%p(k?&jvLp0py57oMH|*IMa(<|{m1(0S|x)?R-mqJ=I;_YUZA>J z62v*eSK;5w!h8J+6Z2~oyGdZ68waWfy09?4fU&m7%u~zi?YPHPgK6LDwphgaYu%0j zurtw)AYOpYKgHBrkX189mlJ`q)w-f|6>IER{5Lk97%P~a-JyCRFjejW@L>n4vt6#hq;!|m;hNE||LK3nw1{bJOy+eBJjK=QqNjI;Q6;Rp5 z&035pZDUZ#%Oa;&_7x0T<7!RW`#YBOj}F380Bq?MjjEhrvlCATPdkCTTl+2efTX$k zH&0zR1n^`C3ef~^sXzJK-)52(T}uTG%OF8yDhT76L~|^+hZ2hiSM*QA9*D5odI1>& z9kV9jC~twA5MwyOx(lsGD_ggYmztXPD`2=_V|ks_FOx!_J8!zM zTzh^cc+=VNZ&(OdN=y4Juw)@8-85lwf_#VMN!Ed(eQiRiLB2^2e`4dp286h@v@`O%_b)Y~A; zv}r6U?zs&@uD_+(_4bwoy7*uozNvp?bXFoB8?l8yG0qsm1JYzIvB_OH4_2G*IIOwT zVl%HX1562vLVcxM_RG*~w_`FbIc!(T=3>r528#%mwwMK}uEhJ()3MEby zQQjzqjWkwfI~;Fuj(Lj=Ug0y`>~C7`w&wzjK(rPw+Hpd~EvQ-ufQOiB4OMpyUKJhw zqEt~jle9d7S~LI~$6Z->J~QJ{Vdn3!c}g9}*KG^Kzr^(7VI5Gk(mHLL{itj_hG?&K4Ws0+T4gLfi3eu$N=`s36geNC?c zm!~}vG6lx9Uf^5M;bWntF<-{p^bruy~f?sk9 zcETAPQZLoJ8JzMMg<-=ju4keY@SY%Wo?u9Gx=j&dfa6LIAB|IrbORLV1-H==Z1zCM zeZcOYpm5>U2fU7V*h;%n`8 zN95QhfD994={1*<2vKLCNF)feKOGk`R#K~G=;rfq}|)s20&MCa65 zUM?xF5!&e0lF%|U!#rD@I{~OsS_?=;s_MQ_b_s=PuWdC)q|UQ&ea)DMRh5>fpQjXe z%9#*x=7{iRCtBKT#H>#v%>77|{4_slZ)XCY{s3j_r{tdpvb#|r|sbS^dU1x70$eJMU!h{Y7Kd{dl}9&vxQl6Jt1a` zHQZrWyY0?!vqf@u-fxU_@+}u(%Wm>0I#KP48tiAPYY!TdW(o|KtVI|EUB9V`CBBNaBLVih7+yMVF|GSoIQD0Jfb{ z!OXq;(>Z?O`1gap(L~bUcp>Lc@Jl-})^=6P%<~~9ywY=$iu8pJ0m*hOPzr~q`23eX zgbs;VOxxENe0UMVeN*>uCn9Gk!4siN-e>x)pIKAbQz!G)TcqIJ0`JBBaX>1-4_XO_-HCS^vr2vjv#7KltDZdyQ{tlWh4$Gm zB>|O1cBDC)yG(sbnc*@w6e%e}r*|IhpXckx&;sQCwGdKH+3oSG-2)Bf#x`@<4ETAr z0My%7RFh6ZLiZ_;X6Mu1YmXx7C$lSZ^}1h;j`EZd6@%JNUe=btBE z%s=Xmo1Ps?8G`}9+6>iaB8bgjUdXT?=trMu|4yLX^m0Dg{m7rpKNJey|EwHI+nN1e zL^>qN%5Fg)dGs4DO~uwIdXImN)QJ*Jhpj7$fq_^`{3fwpztL@WBB}OwQ#Epo-mqMO zsM$UgpFiG&d#)lzEQ{3Q;)&zTw;SzGOah-Dpm{!q7<8*)Ti_;xvV2TYXa}=faXZy? z3y?~GY@kl)>G&EvEijk9y1S`*=zBJSB1iet>0;x1Ai)*`^{pj0JMs)KAM=@UyOGtO z3y0BouW$N&TnwU6!%zS%nIrnANvZF&vB1~P5_d`x-giHuG zPJ;>XkVoghm#kZXRf>qxxEix;2;D1CC~NrbO6NBX!`&_$iXwP~P*c($EVV|669kDO zKoTLZNF4Cskh!Jz5ga9uZ`3o%7Pv`d^;a=cXI|>y;zC3rYPFLQkF*nv(r>SQvD*## z(Vo%^9g`%XwS0t#94zPq;mYGLKu4LU3;txF26?V~A0xZbU4Lmy`)>SoQX^m7fd^*E z+%{R4eN!rIk~K)M&UEzxp9dbY;_I^c} zOc{wlIrN_P(PPqi51k_$>Lt|X6A^|CGYgKAmoI#Li?;Wq%q~q*L7ehZkUrMxW67Jl zhsb~+U?33QS>eqyN{(odAkbopo=Q$Az?L+NZW>j;#~@wCDX?=L5SI|OxI~7!Pli;e zELMFcZtJY3!|=Gr2L4>z8yQ-{To>(f80*#;6`4IAiqUw`=Pg$%C?#1 z_g@hIGerILSU>=P>z{gM|DS91A4cT@PEIB^hSop!uhMo#2G;+tQSpDO_6nOnPWSLU zS;a9m^DFMXR4?*X=}d7l;nXuHk&0|m`NQn%d?8|Ab3A9l9Jh5s120ibWBdB z$5YwsK3;wvp!Kn@)Qae{ef`0#NwlRpQ}k^r>yos_Ne1;xyKLO?4)t_G4eK~wkUS2A&@_;)K0-03XGBzU+5f+uMDxC z(s8!8!RvdC#@`~fx$r)TKdLD6fWEVdEYtV#{ncT-ZMX~eI#UeQ-+H(Z43vVn%Yj9X zLdu9>o%wnWdvzA-#d6Z~vzj-}V3FQ5;axDIZ;i(95IIU=GQ4WuU{tl-{gk!5{l4_d zvvb&uE{%!iFwpymz{wh?bKr1*qzeZb5f6e6m_ozRF&zux2mlK=v_(_s^R6b5lu?_W4W3#<$zeG~Pd)^!4tzhs}-Sx$FJP>)ZGF(hVTH|C3(U zs0PO&*h_ zNA-&qZpTP$$LtIgfiCn07}XDbK#HIXdmv8zdz4TY;ifNIH-0jy(gMSByG2EF~Th#eb_TueZC` zE?3I>UTMpKQ})=C;6p!?G)M6w^u*A57bD?2X`m3X^6;&4%i_m(uGJ3Z5h`nwxM<)H z$I5m?wN>O~8`BGnZ=y^p6;0+%_0K}Dcg|K;+fEi|qoBqvHj(M&aHGqNF48~XqhtU? z^ogwBzRlOfpAJ+Rw7IED8lRbTdBdyEK$gPUpUG}j-M42xDj_&qEAQEtbs>D#dRd7Y z<&TpSZ(quQDHiCFn&0xsrz~4`4tz!CdL8m~HxZM_agu@IrBpyeL1Ft}V$HX_ZqDPm z-f89)pjuEzGdq-PRu`b1m+qBGY{zr_>{6Ss>F|xHZlJj9dt5HD$u`1*WZe)qEIuDSR)%z+|n zatVlhQ?$w#XRS7xUrFE;Y8vMGhQS5*T{ZnY=q1P?w5g$OKJ#M&e??tAmPWHMj3xhS ziGxapy?kn@$~2%ZY;M8Bc@%$pkl%Rvj!?o%agBvpQ-Q61n9kznC4ttrRNQ4%GFR5u zyv%Yo9~yxQJWJSfj z?#HY$y=O~F|2pZs22pu|_&Ajd+D(Mt!nPUG{|1nlvP`=R#kKH zO*s$r_%ss5h1YO7k0bHJ2CXN)Yd6CHn~W!R=SqkWe=&nAZu(Q1G!xgcUilM@YVei@2@a`8he z9@pM`)VB*=e7-MWgLlXlc)t;fF&-AwM{E-EX}pViFn0I0CNw2bNEnN2dj!^4(^zS3 zobUm1uQnpqk_4q{pl*n06=TfK_C>UgurKFjRXsK_LEn};=79`TB12tv6KzwSu*-C8 z;=~ohDLZylHQ|Mpx-?yql>|e=vI1Z!epyUpAcDCp4T|*RV&X`Q$0ogNwy6mFALo^@ z9=&(9txO8V@E!@6^(W0{*~CT>+-MA~vnJULBxCTUW>X5>r7*eXYUT0B6+w@lzw%n> z_VjJ<2qf|(d6jYq2(x$(ZDf!yVkfnbvNmb5c|hhZ^2TV_LBz`9w!e_V*W_(MiA7|= z&EeIIkw*+$Xd!)j8<@_<}A5;~A_>3JT*kX^@}cDoLd>Qj<`Se^wdUa(j0dp+Tl8EptwBm{9OGsdFEq zM`!pjf(Lm(`$e3FLOjqA5LnN5o!}z{ zNf}rJuZh@yUtq&ErjHeGzX4(!luV!jB&;FAP|!R_QHYw#^Z1LwTePAKJ6X&IDNO#; z)#I@Xnnzyij~C@UH~X51JCgQeF0&hTXnuoElz#m{heZRexWc0k4<>0+ClX7%0 zEBqCCld1tD9Zwkr4{?Nor19#E5-YKfB8d?qgR82-Ow2^AuNevly2*tHA|sK!ybYkX zm-sLQH72P&{vEAW6+z~O5d0qd=xW~rua~5a?ymYFSD@8&gV)E5@RNNBAj^C99+Z5Z zR@Pq55mbCQbz+Mn$d_CMW<-+?TU960agEk1J<>d>0K=pF19yN))a~4>m^G&tc*xR+yMD*S=yip-q=H zIlredHpsJV8H(32@Zxc@bX6a21dUV95Th--8pE6C&3F>pk=yv$yd6@Haw;$v4+Fcb zRwn{Qo@0`7aPa2LQOP}j9v>sjOo5Kqvn|`FLizX zB+@-u4Lw|jsvz{p^>n8Vo8H2peIqJJnMN}A)q6%$Tmig7eu^}K2 zrh$X?T|ZMsoh{6pdw1G$_T<`Ds-G=jc;qcGdK4{?dN2-XxjDNbb(7pk|3JUVCU4y; z)?LXR>f+AAu)JEiti_Zy#z5{RgsC}R(@jl%9YZ>zu~hKQ*AxbvhC378-I@{~#%Y`Z zy=a=9YpewPIC+gkEUUwtUL7|RU7=!^Aa}Mk^6uxOgRGA#JXjWLsjFUnix|Mau{hDT z7mn*z1m5g`vP(#tjT0Zy4eAY(br&!RiiXE=ZI!{sE1#^#%x^Z7t1U)b<;%Y}Q9=5v z;wpDCEZ@OE36TWT=|gxigT@VaW9BvHS05;_P(#s z8zI4XFQys}q)<`tkX$WnSarn{3e!s}4(J!=Yf>+Y>cP3f;vr63f2{|S^`_pWc)^5_!R z*(x-fuBxL51@xe!lnDBKi}Br$c$BMZ3%f2Sa6kLabiBS{pq*yj;q|k(86x`PiC{p6 z_bxCW{>Q2BA8~Ggz&0jkrcU+-$ANBsOop*ms>34K9lNYil@}jC;?cYP(m^P}nR6FV zk(M%48Z&%2Rx$A&FhOEirEhY0(dn;-k(qkTU)sFQ`+-ih+s@A8g?r8Pw+}2;35WYf zi}VO`jS`p(tc)$X$a>-#WXoW!phhatC*$}|rk>|wUU71eUJG^$c6_jwX?iSHM@6__ zvV|6%U*$sSXJu9SX?2%M^kK|}a2QJ8AhF{fuXrHZxXsI~O zGKX45!K7p*MCPEQ=gp?eu&#AW*pR{lhQR##P_*{c_DjMGL|3T3-bSJ(o$|M{ytU}> zAV>wq*uE*qFo9KvnA^@juy{x<-u*#2NvkV={Ly}ysKYB-k`K3@K#^S1Bb$8Y#0L0# z`6IkSG&|Z$ODy|VLS+y5pFJx&8tvPmMd8c9FhCyiU8~k6FwkakUd^(_ml8`rnl>JS zZV){9G*)xBqPz^LDqRwyS6w86#D^~xP4($150M)SOZRe9sn=>V#aG0Iy(_^YcPpIz8QYM-#s+n% z@Jd?xQq?Xk6=<3xSY7XYP$$yd&Spu{A#uafiIfy8gRC`o0nk{ezEDjb=q_qRAlR1d zFq^*9Gn)yTG4b}R{!+3hWQ+u3GT~8nwl2S1lpw`s0X_qpxv)g+JIkVKl${sYf_nV~B>Em>M;RlqGb5WVil(89 zs=ld@|#;dq1*vQGz=7--Br-|l) zZ%Xh@v8>B7P?~}?Cg$q9_={59l%m~O&*a6TKsCMAzG&vD>k2WDzJ6!tc!V)+oxF;h zJH;apM=wO?r_+*#;ulohuP=E>^zon}a$NnlcQ{1$SO*i=jnGVcQa^>QOILc)e6;eNTI>os=eaJ{*^DE+~jc zS}TYeOykDmJ=6O%>m`i*>&pO_S;qMySJIyP=}4E&J%#1zju$RpVAkZbEl+p%?ZP^C z*$$2b4t%a(e+%>a>d_f_<JjxI#J1x;=hPd1zFPx=6T$;;X1TD*2(edZ3f46zaAoW>L53vS_J*N8TMB|n+;LD| zC=GkQPpyDY#Am4l49chDv*gojhRj_?63&&8#doW`INATAo(qY#{q}%nf@eTIXmtU< zdB<7YWfyCmBs|c)cK>1)v&M#!yNj#4d$~pVfDWQc_ke1?fw{T1Nce_b`v|Vp5ig(H zJvRD^+ps46^hLX;=e2!2e;w9y1D@!D$c@Jc&%%%IL=+xzw55&2?darw=9g~>P z9>?Kdc$r?6c$m%x2S$sdpPl>GQZ{rC9mPS63*qjCVa?OIBj!fW zm|g?>CVfGXNjOfcyqImXR_(tXS(F{FcoNzKvG5R$IgGaxC@)i(e+$ME}vPVIhd|mx2IIE+f zM?9opQHIVgBWu)^A|RzXw!^??S!x)SZOwZaJkGjc<_}2l^eSBm!eAJG9T>EC6I_sy z?bxzDIAn&K5*mX)$RQzDA?s)-no-XF(g*yl4%+GBf`##bDXJ==AQk*xmnatI;SsLp zP9XTHq5mmS=iWu~9ES>b%Q=1aMa|ya^vj$@qz9S!ih{T8_PD%Sf_QrNKwgrXw9ldm zHRVR98*{C?_XNpJn{abA!oix_mowRMu^2lV-LPi;0+?-F(>^5#OHX-fPED zCu^l7u3E%STI}c4{J2!)9SUlGP_@!d?5W^QJXOI-Ea`hFMKjR7TluLvzC-ozCPn1`Tpy z!vlv@_Z58ILX6>nDjTp-1LlFMx~-%GA`aJvG$?8*Ihn;mH37eK**rmOEwqegf-Ccx zrIX4;{c~RK>XuTXxYo5kMiWMy)!IC{*DHG@E$hx?RwP@+wuad(P1{@%tRkyJRqD)3 zMHHHZ4boqDn>-=DgR5VlhQTpfVy182Gk;A_S8A1-;U1RR>+$62>(MUx@Nox$vTjHq z%QR=j!6Gdyb5wu7y(YUktwMuW5<@jl?m4cv4BODiT5o8qVdC0MBqGr@-YBIwnpZAY znX9(_uQjP}JJ=!~Ve9#5I~rUnN|P_3D$LqZcvBnywYhjlMSFHm`;u9GPla{5QD7(7*6Tb3Svr8;(nuAd81q$*uq6HC_&~je*Ca7hP4sJp0av{M8480wF zxASi7Qv+~@2U%Nu1Ud;s-G4CTVWIPyx!sg&8ZG0Wq zG_}i3C(6_1>q3w!EH7$Kwq8uBp2F2N7}l65mk1p*9v0&+;th=_E-W)E;w}P(j⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3fa8f86 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/html/build.gradle b/html/build.gradle new file mode 100644 index 0000000..e4cdd90 --- /dev/null +++ b/html/build.gradle @@ -0,0 +1,153 @@ + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'org.gretty:gretty:3.1.0' + } +} +apply plugin: "gwt" +apply plugin: "war" +apply plugin: "org.gretty" + +gwt { + gwtVersion = "$gwtFrameworkVersion" // Should match the version used for building the GWT backend. See gradle.properties. + maxHeapSize = '1G' // Default 256m is not enough for the GWT compiler. GWT is HUNGRY. + minHeapSize = '1G' + + src = files(file('src/main/java')) // Needs to be in front of "modules" below. + modules 'com.agifans.agile.GdxDefinition' + devModules 'com.agifans.agile.GdxDefinitionSuperdev' + project.webAppDirName = 'webapp' + + compiler.strict = true + compiler.disableCastChecking = true + //// The next line can be useful to uncomment if you want output that hasn't been obfuscated. +// compiler.style = org.docstr.gradle.plugins.gwt.Style.DETAILED + + sourceLevel = 1.11 +} + +dependencies { + implementation "com.badlogicgames.gdx:gdx:$gdxVersion:sources" + implementation "com.github.tommyettinger:gdx-backend-gwt:1.1210.0" + implementation "com.github.tommyettinger:gdx-backend-gwt:1.1210.0:sources" + implementation "com.google.jsinterop:jsinterop-annotations:2.0.2:sources" + implementation project(':core') + +} + +import org.akhikhl.gretty.AppBeforeIntegrationTestTask +import org.docstr.gradle.plugins.gwt.GwtSuperDev + +gretty.httpPort = 8080 +// The line below will need to be changed only if you change the build directory to something other than "build". +gretty.resourceBase = "${project.layout.buildDirectory.asFile.get().absolutePath}/gwt/draftOut" +gretty.contextPath = "/" +gretty.portPropertiesFileName = "TEMP_PORTS.properties" + +task startHttpServer (dependsOn: [draftCompileGwt]) { + doFirst { + copy { + from "webapp" + into gretty.resourceBase + } + copy { + from "war" + into gretty.resourceBase + } + } +} +task beforeRun(type: AppBeforeIntegrationTestTask, dependsOn: startHttpServer) { + // The next line allows ports to be reused instead of + // needing a process to be manually terminated. + file("build/TEMP_PORTS.properties").delete() + // Somewhat of a hack; uses Gretty's support for wrapping a task in + // a start and then stop of a Jetty server that serves files while + // also running the SuperDev code server. + integrationTestTask 'superDev' + + interactive false +} + +task superDev(type: GwtSuperDev) { + doFirst { + gwt.modules = gwt.devModules + } +} + +//// We delete the (temporary) war/ folder because if any extra files get into it, problems occur. +//// The war/ folder shouldn't be committed to version control. +clean.delete += [file("war")] + +// This next line can be changed if you want to, for instance, always build into the +// docs/ folder of a Git repo, which can be set to automatically publish on GitHub Pages. +// This is relative to the html/ folder. +var outputPath = "build/dist/" + +task dist(dependsOn: [clean, compileGwt]) { + doLast { + // Uncomment the next line if you have changed outputPath and know that its contents + // should be replaced by a new dist build. Some large JS files are not cleaned up by + // default unless the outputPath is inside build/ (then the clean task removes them). + // Do not uncomment the next line if you changed outputPath to a folder that has + // non-generated files that you want to keep! + //delete(file(outputPath)) + + file(outputPath).mkdirs() + copy { + from("build/gwt/out"){ + exclude '**/*.symbolMap' // Not used by a dist, and these can be large. + } + into outputPath + } + copy { + from("webapp") { + exclude 'index.html' // We edit this HTML file later. + exclude 'refresh.png' // We don't need this button; this saves some bytes. + } + into outputPath + } + copy { + from("webapp") { + // These next two lines take the index.html page and remove the superdev refresh button. + include 'index.html' + filter { String line -> line.replaceAll(' + + + + + + + + + + + + + \ No newline at end of file diff --git a/html/src/main/java/com/agifans/agile/GdxDefinitionSuperdev.gwt.xml b/html/src/main/java/com/agifans/agile/GdxDefinitionSuperdev.gwt.xml new file mode 100644 index 0000000..216f2a5 --- /dev/null +++ b/html/src/main/java/com/agifans/agile/GdxDefinitionSuperdev.gwt.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/html/src/main/java/com/agifans/agile/gwt/GwtAgileRunner.java b/html/src/main/java/com/agifans/agile/gwt/GwtAgileRunner.java new file mode 100644 index 0000000..8b39d06 --- /dev/null +++ b/html/src/main/java/com/agifans/agile/gwt/GwtAgileRunner.java @@ -0,0 +1,31 @@ +package com.agifans.agile.gwt; + +import com.agifans.agile.AgileRunner; +import com.agifans.agile.Interpreter; + +public class GwtAgileRunner extends AgileRunner { + + //@Override + //public void init(Interpreter interpreter) { + // // TODO Auto-generated method stub + // + //} + + @Override + public void start() { + // TODO Auto-generated method stub + + } + + @Override + public void stop() { + // TODO Auto-generated method stub + + } + + @Override + public boolean isRunning() { + // TODO Auto-generated method stub + return false; + } +} diff --git a/html/src/main/java/com/agifans/agile/gwt/GwtLauncher.java b/html/src/main/java/com/agifans/agile/gwt/GwtLauncher.java new file mode 100644 index 0000000..be06316 --- /dev/null +++ b/html/src/main/java/com/agifans/agile/gwt/GwtLauncher.java @@ -0,0 +1,28 @@ +package com.agifans.agile.gwt; + +import com.badlogic.gdx.ApplicationListener; +import com.badlogic.gdx.backends.gwt.GwtApplication; +import com.badlogic.gdx.backends.gwt.GwtApplicationConfiguration; +import com.agifans.agile.Agile; + +/** Launches the GWT application. */ +public class GwtLauncher extends GwtApplication { + @Override + public GwtApplicationConfiguration getConfig () { + // Resizable application, uses available space in browser with no padding: + GwtApplicationConfiguration cfg = new GwtApplicationConfiguration(true); + cfg.padVertical = 0; + cfg.padHorizontal = 0; + return cfg; + // If you want a fixed size application, comment out the above resizable section, + // and uncomment below: + //return new GwtApplicationConfiguration(640, 480); + } + + @Override + public ApplicationListener createApplicationListener () { + GwtAgileRunner gwtAgileRunner = new GwtAgileRunner(); + GwtWavePlayer gwtWavePlayer = new GwtWavePlayer(); + return new Agile(gwtAgileRunner, gwtWavePlayer); + } +} diff --git a/html/src/main/java/com/agifans/agile/gwt/GwtWavePlayer.java b/html/src/main/java/com/agifans/agile/gwt/GwtWavePlayer.java new file mode 100644 index 0000000..ee1c8ac --- /dev/null +++ b/html/src/main/java/com/agifans/agile/gwt/GwtWavePlayer.java @@ -0,0 +1,37 @@ +package com.agifans.agile.gwt; + +import com.agifans.agile.WavePlayer; + +public class GwtWavePlayer implements WavePlayer { + + @Override + public void playWaveData(byte[] waveData, Runnable endedCallback) { + // TODO Auto-generated method stub + + } + + @Override + public void stopPlaying(boolean wait) { + // TODO Auto-generated method stub + + } + + @Override + public boolean isPlaying() { + // TODO Auto-generated method stub + return false; + } + + @Override + public void reset() { + // TODO Auto-generated method stub + + } + + @Override + public void dispose() { + // TODO Auto-generated method stub + + } + +} diff --git a/html/webapp/WEB-INF/web.xml b/html/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..4301df2 --- /dev/null +++ b/html/webapp/WEB-INF/web.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/html/webapp/index.html b/html/webapp/index.html new file mode 100644 index 0000000..502088b --- /dev/null +++ b/html/webapp/index.html @@ -0,0 +1,31 @@ + + + + libGDX application + + + + + + + +
    + + + + + diff --git a/html/webapp/refresh.png b/html/webapp/refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..aab1e3872e5fc840acc6e880a17c3e42f5a6cc3a GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~&3?$8t&jUkyQ&PHgbLqiHaA}oCTa8c$9AB-44$rjF6*2UngCFLYV803 literal 0 HcmV?d00001 diff --git a/html/webapp/styles.css b/html/webapp/styles.css new file mode 100644 index 0000000..e768a39 --- /dev/null +++ b/html/webapp/styles.css @@ -0,0 +1,53 @@ +canvas { + cursor: default; + outline: none; +} + +body { + background-color: #222222; +} + +p { + text-align: center; + color: #eeeeee; +} + +a { + text-align: center; + color: #bbbbff; +} + +.superdev { + color: rgb(37,37,37); + text-shadow: 0px 1px 1px rgba(250,250,250,0.1); + font-size: 50pt; + display: block; + position: relative; + text-decoration: none; + background-color: rgb(83,87,93); + box-shadow: 0px 3px 0px 0px rgb(34,34,34), + 0px 7px 10px 0px rgb(17,17,17), + inset 0px 1px 1px 0px rgba(250, 250, 250, .2), + inset 0px -12px 35px 0px rgba(0, 0, 0, .5); + width: 70px; + height: 70px; + border: 0; + border-radius: 35px; + text-align: center; + line-height: 68px; +} + +.superdev:active { + box-shadow: 0px 0px 0px 0px rgb(34,34,34), + 0px 3px 7px 0px rgb(17,17,17), + inset 0px 1px 1px 0px rgba(250, 250, 250, .2), + inset 0px -10px 35px 5px rgba(0, 0, 0, .5); + background-color: rgb(83,87,93); + top: 3px; + color: #fff; + text-shadow: 0px 0px 3px rgb(250,250,250); +} + +.superdev:hover { + background-color: rgb(100,100,100); +} diff --git a/lwjgl3/build.gradle b/lwjgl3/build.gradle new file mode 100644 index 0000000..2062449 --- /dev/null +++ b/lwjgl3/build.gradle @@ -0,0 +1,115 @@ +buildscript { + repositories { + gradlePluginPortal() + } + dependencies { +// using jpackage only works if the JDK version is 14 or higher. +// your JAVA_HOME environment variable may also need to be a JDK with version 14 or higher. + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_14)) { + classpath "org.beryx:badass-runtime-plugin:1.13.0" + } + if(enableGraalNative == 'true') { + classpath "org.graalvm.buildtools.native:org.graalvm.buildtools.native.gradle.plugin:0.9.28" + } + } +} +if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_14)) { + apply plugin: 'org.beryx.runtime' +} +else { + apply plugin: 'application' +} + +sourceSets.main.resources.srcDirs += [ rootProject.file('assets').path ] +mainClassName = 'com.agifans.agile.lwjgl3.Lwjgl3Launcher' +eclipse.project.name = appName + '-lwjgl3' +java.sourceCompatibility = 11 +java.targetCompatibility = 11 + +dependencies { + implementation "com.badlogicgames.gdx:gdx-backend-lwjgl3:$gdxVersion" + implementation "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" + implementation project(':core') +} + +def jarName = "${appName}-${version}.jar" +def os = System.properties['os.name'].toLowerCase() + +run { + workingDir = rootProject.file('assets').path + setIgnoreExitValue(true) + + // This next line could be needed to run LWJGL3 Java apps on macOS, but StartupHelper should make it unnecessary. + //if (os.contains('mac')) jvmArgs += "-XstartOnFirstThread" + // If you encounter issues with the 'lwjgl3:run' task on macOS specifically, try uncommenting the above line, and + // regardless, please report it via the gdx-liftoff issue tracker or just mention it on the libGDX Discord. +} + +jar { +// sets the name of the .jar file this produces to the name of the game or app. + archiveFileName.set(jarName) +// using 'lib' instead of the default 'libs' appears to be needed by jpackageimage. + destinationDirectory = file("${project.layout.buildDirectory.asFile.get().absolutePath}/lib") +// the duplicatesStrategy matters starting in Gradle 7.0; this setting works. + duplicatesStrategy(DuplicatesStrategy.EXCLUDE) + dependsOn configurations.runtimeClasspath + from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } +// these "exclude" lines remove some unnecessary duplicate files in the output JAR. + exclude('META-INF/INDEX.LIST', 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA') + dependencies { + exclude('META-INF/INDEX.LIST', 'META-INF/maven/**') + } +// setting the manifest makes the JAR runnable. + manifest { + attributes 'Main-Class': project.mainClassName + } +// this last step may help on some OSes that need extra instruction to make runnable JARs. + doLast { + file(archiveFile).setExecutable(true, false) + } +} + +if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_14)) { + tasks.jpackageImage.doNotTrackState("This task both reads from and writes to the build folder.") + runtime { + options.set(['--strip-debug', + '--compress', '2', + '--no-header-files', + '--no-man-pages', + '--strip-native-commands', + '--vm', 'server']) +// you could very easily need more modules than this one. +// use the lwjgl3:suggestModules task to see which modules may be needed. + modules.set([ + 'jdk.unsupported' + ]) + distDir.set(file(project.layout.buildDirectory)) + jpackage { + imageName = appName +// you can set this to false if you want to build an installer, or keep it as true to build just an app. + skipInstaller = true +// this may need to be set to a different path if your JAVA_HOME points to a low JDK version. + jpackageHome = javaHome.getOrElse("") + mainJar = jarName + if (os.contains('win')) { + imageOptions = ["--icon", "icons/logo.ico"] + } else if (os.contains('nix') || os.contains('nux') || os.contains('bsd')) { + imageOptions = ["--icon", "icons/logo.png"] + } else if (os.contains('mac')) { +// If you are making a jpackage image on macOS, the below line should work thanks to StartupHelper. + imageOptions = ["--icon", "icons/logo.icns"] +// If the above line doesn't produce a runnable executable, you can try using the below line instead of the above one. +// imageOptions = ["--icon", "icons/logo.icns", "--java-options", "\"-XstartOnFirstThread\""] + } + } + } +} + +// Equivalent to the jar task; here for compatibility with gdx-setup. +tasks.register('dist') { + dependsOn['jar'] +} + +if(enableGraalNative == 'true') { + apply from: file("nativeimage.gradle") +} diff --git a/lwjgl3/icons/logo.icns b/lwjgl3/icons/logo.icns new file mode 100644 index 0000000000000000000000000000000000000000..5e41ad7c1af69bbc1ea082c61fccd833abc5c4ff GIT binary patch literal 201876 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

    KO)MRW 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

    `#4_X4po6zBr>DFqDjW2! z{_P&1LrE2Mfng-Rc+nO76%M4{>N7%t?)?3HcOO5&1?@h|V!{u-A0J5u2pxmpolbIH zWHUG@?w3e8GhcYn-jYE!V@mg*AQrQL=6}BvAbAXJ9-WbiGM2i0jYQK-zjGELd&()Za+UrC@EWFRrO z;4g)&K5*z4-Z-t-wBoKclKPV+fWq^Y03z0VwB{6!4?EFi;rLv0ewTwL0Jt- z<{kafREP&C+~p=P=SxA7!qyVSnxi|(SP{jowkoS`0n&D#KP1_JUj0PYRR9IZ5fYL1 zkIS~HHQeD9orcGmso9a|=VTtZ)OQHYEy3DOW9GseGN;KjjO}k`fVGNr$1qEiz%(i< zi1wFwn4OpHAo{KduGI8nkAGQ?d}0=EH%yapXC(Ocr}7uN<31W0%`LOj z^GYCgD^E&-ct&=Iw9XUrSyl6ByJ$Yw^np0X%8l#53uB@Qwe5>el7