diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d97c25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,128 @@ +## Java + +*.class +*.war +*.ear +hs_err_pid* + +## Robovm +/ios/robovm-build/ + +## GWT +/html/war/ +/html/gwt-unitCache/ +.apt_generated/ +.gwt/ +gwt-unitCache/ +www-test/ +.gwt-tmp/ + +## Android Studio and Intellij and Android in general +/android/libs/armeabi/ +/android/libs/armeabi-v7a/ +/android/libs/arm64-v8a/ +/android/libs/x86/ +/android/libs/x86_64/ +/android/gen/ +.idea/ +*.ipr +*.iws +*.iml +/android/out/ +com_crashlytics_export_strings.xml + +## Eclipse + +.classpath +.project +.metadata/ +/android/bin/ +/core/bin/ +/desktop/bin/ +/html/bin/ +/ios/bin/ +/ios-moe/bin/ +*.tmp +*.bak +*.swp +*~.nib +.settings/ +.loadpath +.externalToolBuilders/ +*.launch + +## NetBeans + +/nbproject/private/ +/android/nbproject/private/ +/core/nbproject/private/ +/desktop/nbproject/private/ +/html/nbproject/private/ +/ios/nbproject/private/ +/ios-moe/nbproject/private/ + +/build/ +/android/build/ +/core/build/ +/desktop/build/ +/html/build/ +/ios/build/ +/ios-moe/build/ + +/nbbuild/ +/android/nbbuild/ +/core/nbbuild/ +/desktop/nbbuild/ +/html/nbbuild/ +/ios/nbbuild/ +/ios-moe/nbbuild/ + +/dist/ +/android/dist/ +/core/dist/ +/desktop/dist/ +/html/dist/ +/ios/dist/ +/ios-moe/dist/ + +/nbdist/ +/android/nbdist/ +/core/nbdist/ +/desktop/nbdist/ +/html/nbdist/ +/ios/nbdist/ +/ios-moe/nbdist/ + +nbactions.xml +nb-configuration.xml + +## Gradle + +/local.properties +.gradle/ +gradle-app.setting +/build/ +/android/build/ +/core/build/ +/desktop/build/ +/html/build/ +/ios/build/ +/ios-moe/build/ + +## OS Specific +.DS_Store +Thumbs.db + +## iOS +/ios/xcode/*.xcodeproj/* +!/ios/xcode/*.xcodeproj/xcshareddata +!/ios/xcode/*.xcodeproj/project.pbxproj +/ios/xcode/native/ + +/ios-moe/xcode/*.xcodeproj/* +!/ios-moe/xcode/*.xcodeproj/xcshareddata +!/ios-moe/xcode/*.xcodeproj/project.pbxproj +/ios-moe/xcode/native/ + +## data +/core/data diff --git a/README.md b/README.md new file mode 100644 index 0000000..e759a51 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Warsmash +Greatest game ever diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..3e9b05e --- /dev/null +++ b/build.gradle @@ -0,0 +1,98 @@ +buildscript { + repositories { + mavenLocal() + flatDir { + dirs "$rootProject.projectDir/jars" + } + mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } + maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } + maven { url "https://maven.nikr.net/" } + jcenter() + google() + } +} + +allprojects { + apply plugin: "eclipse" + apply plugin: "idea" + + version = '1.0' + ext { + appName = "warsmash" + gdxVersion = '1.9.8' + roboVMVersion = '2.3.5' + box2DLightsVersion = '1.4' + ashleyVersion = '1.7.0' + aiVersion = '1.8.0' + antlrVersion = '4.7' + } + + repositories { + mavenLocal() + mavenCentral() + google() + maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } + maven { url "https://oss.sonatype.org/content/repositories/releases/" } + maven { url "https://maven.nikr.net/" } + maven { url 'https://jitpack.io' } + } +} + +project(":desktop") { + apply plugin: "java" + + + dependencies { + compile project(":core") + compile "com.badlogicgames.gdx:gdx-backend-lwjgl:$gdxVersion" + compile "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" + compile "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-desktop" + compile "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-desktop" + compile "com.google.guava:guava:23.5-jre" + compile "net.nikr:dds:1.0.0" + implementation 'com.github.inwc3:wc3libs:-SNAPSHOT' + compile files(fileTree(dir:'../jars', includes: ['*.jar'])) + + } +} + +project(":core") { + apply plugin: "java" + + + dependencies { + compile project(":fdfparser") + compile project(":jassparser") + compile "com.badlogicgames.gdx:gdx:$gdxVersion" + compile "com.badlogicgames.gdx:gdx-box2d:$gdxVersion" + compile "com.badlogicgames.gdx:gdx-freetype:$gdxVersion" + compile "com.google.guava:guava:23.5-jre" + compile "net.nikr:dds:1.0.0" + implementation 'com.github.inwc3:wc3libs:-SNAPSHOT' + compile files(fileTree(dir:'../jars', includes: ['*.jar'])) + + } +} + +project(":fdfparser") { + apply plugin: "antlr" + + + dependencies { + antlr "org.antlr:antlr4:$antlrVersion" // use antlr version 4 + } +} + +project(":jassparser") { + apply plugin: "antlr" + + + dependencies { + antlr "org.antlr:antlr4:$antlrVersion" // use antlr version 4 + } +} + +tasks.eclipse.doLast { + delete ".project" +} \ No newline at end of file diff --git a/core/assets/badlogic.jpg b/core/assets/badlogic.jpg new file mode 100644 index 0000000..4390da6 Binary files /dev/null and b/core/assets/badlogic.jpg differ diff --git a/core/assets/shaders/BoneTexture.vs b/core/assets/shaders/BoneTexture.vs new file mode 100644 index 0000000..5dfa5b7 --- /dev/null +++ b/core/assets/shaders/BoneTexture.vs @@ -0,0 +1,16 @@ +uniform sampler2D u_boneMap; +uniform float u_vectorSize; +uniform float u_rowSize; +mat4 fetchMatrix(float column, float row) { + column *= u_vectorSize * 4.0; + row *= u_rowSize; + // Add in half texel to sample in the middle of the texel. + // Otherwise, since the sample is directly on the boundry, small floating point errors can cause the sample to get the wrong pixel. + // This is mostly noticable with NPOT textures, which the bone maps are. + column += 0.5 * u_vectorSize; + row += 0.5 * u_rowSize; + return mat4(texture2D(u_boneMap, vec2(column, row)), + texture2D(u_boneMap, vec2(column + u_vectorSize, row)), + texture2D(u_boneMap, vec2(column + u_vectorSize * 2.0, row)), + texture2D(u_boneMap, vec2(column + u_vectorSize * 3.0, row))); +} \ No newline at end of file diff --git a/core/assets/warsmash.ini b/core/assets/warsmash.ini new file mode 100644 index 0000000..2f07845 --- /dev/null +++ b/core/assets/warsmash.ini @@ -0,0 +1,53 @@ +[DataSources] +Count=8 +Type00=MPQ +Path00="D:\Games\Warcraft III Patch 1.22\war3.mpq" +Type01=MPQ +Path01="D:\Games\Warcraft III Patch 1.22\War3x.mpq" +Type02=MPQ +Path02="D:\Games\Warcraft III Patch 1.22\War3xlocal.mpq" +Type03=MPQ +Path03="D:\Games\Warcraft III Patch 1.22\War3Patch.mpq" +Type04=Folder +Path04="..\..\resources" +Type05=Folder +Path05="D:\Backups\Warsmash\Data" +Type06=Folder +Path06="D:\Games\Warcraft III Patch 1.22\Maps" +Type07=Folder +Path07="." + +[Map] +//FilePath="CombatUnitTests.w3x" +//FilePath="PitchRoll.w3x" +//FilePath="PeonStartingBase_Simple.w3x" +FilePath="PeonStartingBase_Scythe.w3x" +//FilePath="MyStromguarde.w3m" +//FilePath="ColdArrows.w3m" +//FilePath="DungeonGoldMine.w3m" +//FilePath="PlayerPeasants.w3m" +//FilePath="FireLord.w3x" +//FilePath="Maps\Campaign\NightElf03.w3m" +//FilePath="PhoenixAttack.w3x" +//FilePath="LightEnvironmentTest.w3x" +//FilePath="TorchLight2.w3x" +//FilePath="OrcAssault.w3x" +//FilePath="FrostyVsFarm.w3m" +//FilePath="ModelTest.w3x" +//FilePath="SpinningSample.w3x" +//FilePath="Maps\Campaign\Prologue02.w3m" +//FilePath="Pathing.w3x" +//FilePath="ItemFacing.w3x" +//FilePath=SomeParticleTests.w3x +//FilePath="PeonMiningMultiHall.w3x" +//FilePath="QuadtreeBugs.w3x" +//FilePath="test2.w3x" +//FilePath="FarseerHoldPositionTest.w3x" +//FilePath="Ramps.w3m" +//FilePath="V1\Farm.w3x" +//FilePath="PenguinWorld.w3x" +//FilePath="Maps\FrozenThrone\Campaign\UndeadX09.w3x" +//FilePath="LavellaLagoon.w3x" +//FilePath="WiceOrc.w3x" +//FilePath="NorthrendPathingDoodle.w3x" +//FilePath="Maps\Campaign\Prologue01.w3m" diff --git a/core/assets/warsmash131.ini b/core/assets/warsmash131.ini new file mode 100644 index 0000000..ad07661 --- /dev/null +++ b/core/assets/warsmash131.ini @@ -0,0 +1,26 @@ +[DataSources] +Count=5 +Type00=Folder +Path00="D:\Games\Warcraft III CASC 1.31\war3.w3mod" +Type01=Folder +Path01="D:\Games\Warcraft III CASC 1.31\war3.w3mod\_locales\enus.w3mod" +Type02=Folder +Path02="..\..\resources" +Type03=Folder +Path03="D:\Backups\Warsmash\Data" +Type04=Folder +Path04="." + +[Map] +//FilePath="PitchRoll.w3x" +//FilePath="ReforgedGeorgeVacation.w3x" +//FilePath="Maps\Campaign\NightElf03.w3m" +//FilePath="PrivateDontShare/Cult 8.w3x" +//FilePath="TorchLight2.w3x" +//FilePath="OrcAssault.w3x" +//FilePath="PeonStartingBase.w3x" +//FilePath="PhoenixAttack.w3x" +//FilePath="OperationReforged.w3x" +//FilePath="AzerothRoleplay1.909t03DecoratedV2.w3x" +//FilePath="American Colo EX 1.0 unpro.w3x" +FilePath="TheSheepAttack.w3x" diff --git a/core/assets/warsmash131notworking.ini b/core/assets/warsmash131notworking.ini new file mode 100644 index 0000000..140c51a --- /dev/null +++ b/core/assets/warsmash131notworking.ini @@ -0,0 +1,42 @@ +[DataSources] +Count=5 +Type00=CASC +Path00="D:\Games\Warcraft III Patch 1.31\" +Prefixes00="war3.w3mod","war3.w3mod\_deprecated.w3mod","war3.w3mod\_locales\enus.w3mod" +Type01=Folder +Path01="..\..\resources" +Type02=Folder +Path02="D:\Backups\Warsmash\Data" +Type03=Folder +Path03="D:\Games\Warcraft III Patch 1.22\Maps" +Type04=Folder +Path04="." + +[Map] +//FilePath="CombatUnitTests.w3x" +//FilePath="PitchRoll.w3x" +//FilePath="PeonStartingBase.w3x" +//FilePath="MyStromguarde.w3m" +//FilePath="ColdArrows.w3m" +//FilePath="DungeonGoldMine.w3m" +//FilePath="PlayerPeasants.w3m" +//FilePath="FireLord.w3x" +//FilePath="Maps\Campaign\NightElf03.w3m" +//FilePath="PhoenixAttack.w3x" +//FilePath="LightEnvironmentTest.w3x" +//FilePath="TorchLight2.w3x" +FilePath="OrcAssault.w3x" +//FilePath="FrostyVsFarm.w3m" +//FilePath="ModelTest.w3x" +//FilePath="SpinningSample.w3x" +//FilePath="Maps\Campaign\Prologue02.w3m" +//FilePath="Pathing.w3x" +//FilePath="ItemFacing.w3x" +//FilePath=SomeParticleTests.w3x +//FilePath="PeonMiningMultiHall.w3x" +//FilePath="QuadtreeBugs.w3x" +//FilePath="test2.w3x" +//FilePath="FarseerHoldPositionTest.w3x" +//FilePath="Ramps.w3m" +//FilePath="V1\Farm.w3x" +//FilePath="TheSheepAttack.w3x" \ No newline at end of file diff --git a/core/assets/warsmashPRSCMOD.ini b/core/assets/warsmashPRSCMOD.ini new file mode 100644 index 0000000..0c75d66 --- /dev/null +++ b/core/assets/warsmashPRSCMOD.ini @@ -0,0 +1,26 @@ +// This is the Warsmash INI file for Project Revolution +// PRSCMOD + +[DataSources] +Count=9 +Type00=MPQ +Path00="D:\Games\Warcraft III Project Revolution\War3\The Sheep Attack\war3.mpq" +Type01=MPQ +Path01="D:\Games\Warcraft III Project Revolution\War3\The Sheep Attack\War3x.mpq" +Type02=MPQ +Path02="D:\Games\Warcraft III Project Revolution\War3\The Sheep Attack\War3xlocal.mpq" +Type03=MPQ +Path03="D:\Games\Warcraft III Project Revolution\War3\The Sheep Attack\war3patch.mpq" +Type04=MPQ +Path04="D:\Games\Warcraft III Project Revolution\PRSCMOD\Revolution.mpq" +Type05=MPQ +Path05="D:\Games\Warcraft III Project Revolution\PRSCMOD\Sound.mpq" +Type06=Folder +Path06="D:\Games\Warcraft III Project Revolution\ProjectRevolusmash" +Type07=Folder +Path07="..\..\resources" +Type08=Folder +Path08="D:\Games\Warcraft III Project Revolution\PRSCMOD\PR-Maps" + +[Map] +FilePath="ProjectRevolusmash.w3x" \ No newline at end of file diff --git a/core/assets/warsmashRF.ini b/core/assets/warsmashRF.ini new file mode 100644 index 0000000..9279a95 --- /dev/null +++ b/core/assets/warsmashRF.ini @@ -0,0 +1,41 @@ +[DataSources] +Count=5 +Type00=CASC +Path00="C:\Program Files\Warcraft III" +Prefixes00=war3.w3mod,war3.w3mod\_deprecated.w3mod,war3.w3mod\_locales\enus.w3mod,war3.w3mod\_hd.w3mod,war3.w3mod\_hd.w3mod\_locales\enus.w3mod +Type01=Folder +Path01="..\..\resources" +Type02=Folder +Path02="D:\Backups\Warsmash\Data" +Type03=Folder +Path03="D:\Games\Warcraft III Patch 1.22\Maps" +Type04=Folder +Path04="." + +[Map] +//FilePath="CombatUnitTests.w3x" +//FilePath="PitchRoll.w3x" +FilePath="PeonStartingBase.w3x" +//FilePath="MyStromguarde.w3m" +//FilePath="ColdArrows.w3m" +//FilePath="DungeonGoldMine.w3m" +//FilePath="PlayerPeasants.w3m" +//FilePath="FireLord.w3x" +//FilePath="Maps\Campaign\NightElf03.w3m" +//FilePath="PhoenixAttack.w3x" +//FilePath="LightEnvironmentTest.w3x" +//FilePath="TorchLight2.w3x" +//FilePath="OrcAssault.w3x" +//FilePath="FrostyVsFarm.w3m" +//FilePath="ModelTest.w3x" +//FilePath="SpinningSample.w3x" +//FilePath="Maps\Campaign\Prologue02.w3m" +//FilePath="Pathing.w3x" +//FilePath="ItemFacing.w3x" +//FilePath=SomeParticleTests.w3x +//FilePath="PeonMiningMultiHall.w3x" +//FilePath="QuadtreeBugs.w3x" +//FilePath="test2.w3x" +//FilePath="FarseerHoldPositionTest.w3x" +//FilePath="Ramps.w3m" +//FilePath="V1\Farm.w3x" diff --git a/core/assets/warsmashTTOR.ini b/core/assets/warsmashTTOR.ini new file mode 100644 index 0000000..b1b9c90 --- /dev/null +++ b/core/assets/warsmashTTOR.ini @@ -0,0 +1,22 @@ +// This is the Warsmash INI file for Project Revolution +// PRSCMOD + +[DataSources] +Count=7 +Type00=MPQ +Path00="D:\Games\Warcraft III Project Revolution\War3\The Sheep Attack\war3.mpq" +Type01=MPQ +Path01="D:\Games\Warcraft III Project Revolution\War3\The Sheep Attack\War3x.mpq" +Type02=MPQ +Path02="D:\Games\Warcraft III Project Revolution\War3\The Sheep Attack\War3xlocal.mpq" +Type03=MPQ +Path03="D:\Games\Warcraft III Project Revolution\War3\The Sheep Attack\war3patch_TTOR.mpq" +Type04=Folder +Path04="D:\Games\Warcraft III Project Revolution\ProjectRevolusmash" +Type05=Folder +Path05="..\..\resources" +Type06=Folder +Path06="D:\Games\Warcraft III Project Revolution\War3\The Sheep Attack\Maps" + +[Map] +FilePath="(2)BootyBay.w3m" \ No newline at end of file diff --git a/core/assets/warsmashUF.ini b/core/assets/warsmashUF.ini new file mode 100644 index 0000000..eac283a --- /dev/null +++ b/core/assets/warsmashUF.ini @@ -0,0 +1,22 @@ +// This is the Warsmash INI file for Project Revolution +// PRSCMOD + +[DataSources] +Count=7 +Type00=MPQ +Path00="D:\Games\Warcraft III Patch 1.27 Redownload\Warcraft III\war3.mpq" +Type01=MPQ +Path01="D:\Games\Warcraft III Patch 1.27 Redownload\Warcraft III\War3x.mpq" +Type02=MPQ +Path02="D:\Games\Warcraft III Patch 1.27 Redownload\Warcraft III\War3xlocal.mpq" +Type03=MPQ +Path03="D:\Games\Warcraft III Patch 1.27 Redownload\Warcraft III\war3patch.mpq" +Type04=MPQ +Path04="D:\Games\Warcraft III Patch 1.27 Redownload\Warcraft III\War3Mod.mpq" +Type05=Folder +Path05="..\..\resources" +Type06=Folder +Path06="D:\Games\Warcraft III Patch 1.27 Redownload\Warcraft III\Maps" + +[Map] +FilePath="Maps\Campaign\Prologue01.w3m" \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle new file mode 100644 index 0000000..13c049a --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,11 @@ +apply plugin: "java" + +sourceCompatibility = 1.8 +[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' + +sourceSets.main.java.srcDirs = [ "src/" ] + + +eclipse.project { + name = appName + "-core" +} diff --git a/core/src/com/etheller/warsmash/CodeCounter.java b/core/src/com/etheller/warsmash/CodeCounter.java new file mode 100644 index 0000000..0dda796 --- /dev/null +++ b/core/src/com/etheller/warsmash/CodeCounter.java @@ -0,0 +1,35 @@ +package com.etheller.warsmash; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +public class CodeCounter { + + public static void main(final String[] args) { + final int sourceLines = countFile(new File("src/com")); + System.out.println(sourceLines); + } + + public static int countFile(final File file) { + if (file.isDirectory()) { + int sum = 0; + for (final File subFile : file.listFiles()) { + sum += countFile(subFile); + } + return sum; + } + else { + try { + if (file.getName().toLowerCase().endsWith(".java")) { + return Files.readAllLines(file.toPath()).size(); + } + } + catch (final IOException e) { + e.printStackTrace(); + } + return 0; + } + } + +} diff --git a/core/src/com/etheller/warsmash/MathSpeedBenchmark.java b/core/src/com/etheller/warsmash/MathSpeedBenchmark.java new file mode 100644 index 0000000..9968868 --- /dev/null +++ b/core/src/com/etheller/warsmash/MathSpeedBenchmark.java @@ -0,0 +1,61 @@ +package com.etheller.warsmash; + +public class MathSpeedBenchmark { + private static final int NUMBER_OF_ITERATIONS = 100000000; + + public static void main(final String[] args) { + // Let us solve for Ground Distance two ways. + + long sumCosineTime = 0; + long sumSquareRootTime = 0; + final float[] thrallXs = new float[NUMBER_OF_ITERATIONS]; + final float[] thrallYs = new float[NUMBER_OF_ITERATIONS]; + final float[] murlocXs = new float[NUMBER_OF_ITERATIONS]; + final float[] murlocYs = new float[NUMBER_OF_ITERATIONS]; + for (int i = 0; i < NUMBER_OF_ITERATIONS; i++) { + + thrallXs[i] = getRandomFloat(-25000.0f, 25000.0f); + thrallYs[i] = getRandomFloat(-25000.0f, 25000.0f); + murlocXs[i] = getRandomFloat(-25000.0f, 25000.0f); + murlocYs[i] = getRandomFloat(-25000.0f, 25000.0f); + } + final long clockTime1 = System.currentTimeMillis(); + for (int i = 0; i < NUMBER_OF_ITERATIONS; i++) { + final float distance2 = groundDistanceSqrt(thrallXs[i], thrallYs[i], murlocXs[i], murlocYs[i]); + } + final long clockTime2 = System.currentTimeMillis(); + for (int i = 0; i < NUMBER_OF_ITERATIONS; i++) { + final float distance1 = groundDistanceCos(thrallXs[i], thrallYs[i], murlocXs[i], murlocYs[i]); + } + final long clockTime3 = System.currentTimeMillis(); +// if (Math.abs(distance2 - distance1) > 0.1) { +// System.out.println(thrallX + "," + thrallY); +// System.out.println(murlocX + "," + murlocY); +// System.err.println(distance1 + " != " + distance2); +// throw new RuntimeException("You have failed to do mathematics."); +// } + sumCosineTime = clockTime2 - clockTime1; + sumSquareRootTime = clockTime3 - clockTime2; + System.out.println("Square Root: " + sumCosineTime); + System.out.println("Cosine: " + sumSquareRootTime); + } + + static float getRandomFloat(final float min, final float max) { + final float range = max - min; + return (float) ((Math.random() * range) + min); + } + + static float groundDistanceSqrt(final float thrallX, final float thrallY, final float murlocX, + final float murlocY) { + final float dx = murlocX - thrallX; + final float dy = murlocY - thrallY; + return (float) StrictMath.sqrt((dx * dx) + (dy * dy)); + } + + static float groundDistanceCos(final float thrallX, final float thrallY, final float murlocX, final float murlocY) { + final float dx = murlocX - thrallX; + final float dy = murlocY - thrallY; + final double angle = StrictMath.atan2(dy, dx); + return (float) (dx / StrictMath.cos(angle)); + } +} diff --git a/core/src/com/etheller/warsmash/SingleModelScreen.java b/core/src/com/etheller/warsmash/SingleModelScreen.java new file mode 100644 index 0000000..cf583e1 --- /dev/null +++ b/core/src/com/etheller/warsmash/SingleModelScreen.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash; + +public interface SingleModelScreen { + void setModel(String path); +} diff --git a/core/src/com/etheller/warsmash/TestMain.java b/core/src/com/etheller/warsmash/TestMain.java new file mode 100644 index 0000000..bc30512 --- /dev/null +++ b/core/src/com/etheller/warsmash/TestMain.java @@ -0,0 +1,36 @@ +package com.etheller.warsmash; + +import com.etheller.warsmash.util.War3ID; + +public class TestMain { + public static void main(final String[] args) { + if (true) { + System.out.println(War3ID.fromString("hwat").getValue()); + System.out.println(Integer.toHexString(War3ID.fromString("hwat").getValue())); + System.out.println(new War3ID(0x68776174)); + return; + } +// System.out.println(Integer.parseInt("4294967295")); + for (int i = 1; i <= 30; i++) { +// System.out.println(a(i)); + } + + int checkX = 0; + int checkY = 0; + for (int i = 0; i < 300; i++) { + System.out.println(checkX + "," + checkY); + final double angle = ((((int) Math.floor(Math.sqrt((4 * i) + 1))) % 4) * Math.PI) / 2; + checkX += (int) Math.sin(angle); + checkY += (int) Math.cos(angle); + } + } + + public static int a(final int n) { + if (n == 1) { + return 0; + } + else { + return a(n - 1) - (int) Math.sin(((Math.floor(Math.sqrt((4 * (n - 2)) + 1)) % 4) * Math.PI) / 2); + } + } +} diff --git a/core/src/com/etheller/warsmash/WarsmashGdxGame.java b/core/src/com/etheller/warsmash/WarsmashGdxGame.java new file mode 100644 index 0000000..9ff816f --- /dev/null +++ b/core/src/com/etheller/warsmash/WarsmashGdxGame.java @@ -0,0 +1,535 @@ +package com.etheller.warsmash; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; + +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.audio.Music; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.math.Quaternion; +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.util.DataSourceFileHandle; +import com.etheller.warsmash.viewer5.Camera; +import com.etheller.warsmash.viewer5.CanvasProvider; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.PathSolver; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.SolvedPath; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxHandler; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxViewer; +import com.etheller.warsmash.viewer5.handlers.mdx.Sequence; +import com.etheller.warsmash.viewer5.handlers.mdx.SequenceLoopMode; + +public class WarsmashGdxGame extends ApplicationAdapter implements CanvasProvider { + private static final boolean SPIN = false; + private static final boolean ADVANCE_ANIMS = true; + private DataSource codebase; + private ModelViewer viewer; + private MdxModel model; + private CameraManager cameraManager; + public static int VAO; + private final Rectangle tempRect = new Rectangle(); + + private BitmapFont font; + private SpriteBatch batch; + private final DataTable warsmashIni; + + public WarsmashGdxGame(final DataTable warsmashIni) { + this.warsmashIni = warsmashIni; + } + + @Override + public void create() { + + final ByteBuffer tempByteBuffer = ByteBuffer.allocateDirect(4); + tempByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final IntBuffer temp = tempByteBuffer.asIntBuffer(); +// + Gdx.gl30.glGenVertexArrays(1, temp); + VAO = temp.get(0); + + Gdx.gl30.glBindVertexArray(VAO); + + final String renderer = Gdx.gl.glGetString(GL20.GL_RENDERER); + System.err.println("Renderer: " + renderer); + + this.codebase = WarsmashGdxMapScreen.parseDataSources(this.warsmashIni); + this.viewer = new MdxViewer(this.codebase, this, new Vector3(0.3f, 0.3f, -0.25f)); + + this.viewer.addHandler(new MdxHandler()); + this.viewer.enableAudio(); + + final Scene scene = this.viewer.addSimpleScene(); + scene.enableAudio(); + + this.cameraManager = new CameraManager(); + this.cameraManager.setupCamera(scene); + + final String musicPath = "Sound\\Music\\mp3Music\\Mainscreen.mp3"; + final Music music = Gdx.audio.newMusic(new DataSourceFileHandle(this.viewer.dataSource, musicPath)); +// music.setVolume(0.2f); + music.setLooping(true); + music.play(); + +// this.mainModel = (MdxModel) this.viewer.load("Doodads\\Cinematic\\ArthasIllidanFight\\ArthasIllidanFight.mdx", +// this.mainModel = (MdxModel) this.viewer.load("UI\\Glues\\SinglePlayer\\NightElf_Exp\\NightElf_Exp.mdx", +// this.mainModel = (MdxModel) this.viewer.load("Abilities\\Spells\\Orc\\FeralSpirit\\feralspirittarget.mdx", +// new PathSolver() { +// @Override +// public SolvedPath solve(final String src, final Object solverParams) { +// return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); +// } +// }, null); + +// final EventObjectEmitterObject evt = this.mainModel.getEventObjects().get(1); +// for (final Sequence seq : this.mainModel.getSequences()) { +// System.out.println(seq.getName() + ": " + Arrays.toString(seq.getInterval())); +// } +// System.out.println(Arrays.toString(evt.keyFrames)); +// System.out.println(evt.name); + +// this.mainInstance = (MdxComplexInstance) this.mainModel.addInstance(0); + +// this.mainInstance.setScene(scene); +// +// final int animIndex = 0; +// this.modelCamera = this.mainModel.cameras.get(animIndex); +// this.mainInstance.setSequence(animIndex); +// +// this.mainInstance.setSequenceLoopMode(SequenceLoopMode.LOOP_TO_NEXT_ANIMATION); + +// acolytesHarvestingSceneJoke2(scene); + +// singleModelScene(scene, "Buildings\\Undead\\Necropolis\\Necropolis.mdx", "birth"); +// singleModelScene(scene, "Units\\Orc\\KotoBeast\\KotoBeast.mdx", "spell slam"); + singleModelScene(scene, "UI\\Glues\\MainMenu\\MainMenu3D\\MainMenu3D.mdx", "Stand"); + this.modelCamera = this.mainModel.cameras.get(0); + + System.out.println("Loaded"); + Gdx.gl30.glClearColor(0.5f, 0.5f, 0.5f, 1); // TODO remove white background + + this.font = new BitmapFont(); + this.batch = new SpriteBatch(); + } + + private void makeDruidSquare(final Scene scene) { + final MdxModel model2 = (MdxModel) this.viewer.load("units\\nightelf\\druidoftheclaw\\druidoftheclaw.mdx", + new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + makePerfectSquare(scene, model2, 15); + } + + private void singleAcolyteScene(final Scene scene) { + final MdxModel model2 = (MdxModel) this.viewer.load("units\\undead\\acolyte\\acolyte.mdx", new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + + final MdxComplexInstance instance3 = (MdxComplexInstance) model2.addInstance(0); + + instance3.setScene(scene); + + int animIndex = 0; + for (final Sequence s : model2.getSequences()) { + if (s.getName().toLowerCase().startsWith("stand work")) { + animIndex = model2.getSequences().indexOf(s); + } + } + instance3.setSequence(animIndex); + + instance3.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + } + + private void singleModelScene(final Scene scene, final String path, final String animName) { + final MdxModel model2 = (MdxModel) this.viewer.load(path, new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + + final MdxComplexInstance instance3 = (MdxComplexInstance) model2.addInstance(0); + + instance3.setScene(scene); + + int animIndex = 0; + for (final Sequence s : model2.getSequences()) { + if (s.getName().toLowerCase().startsWith(animName)) { + animIndex = model2.getSequences().indexOf(s); + break; + } + } + instance3.setSequence(animIndex); + + instance3.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + this.mainInstance = instance3; + this.mainModel = model2; + } + + private void acolytesHarvestingScene(final Scene scene) { + + final MdxModel acolyteModel = (MdxModel) this.viewer.load("units\\undead\\acolyte\\acolyte.mdx", + new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + final MdxModel mineEffectModel = (MdxModel) this.viewer + .load("abilities\\spells\\undead\\undeadmine\\undeadminecircle.mdx", new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + for (int i = 0; i < 5; i++) { + final MdxComplexInstance acolyteInstance = (MdxComplexInstance) acolyteModel.addInstance(0); + + acolyteInstance.setScene(scene); + + int animIndex = i % acolyteModel.getSequences().size(); + for (final Sequence s : acolyteModel.getSequences()) { + if (s.getName().toLowerCase().startsWith("stand work")) { + animIndex = acolyteModel.getSequences().indexOf(s); + } + } + acolyteInstance.setSequence(animIndex); + + acolyteInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + + final double angle = ((Math.PI * 2) / 5) * i; + acolyteInstance.localLocation.x = (float) Math.cos(angle) * 256; + acolyteInstance.localLocation.y = (float) Math.sin(angle) * 256; + acolyteInstance.localRotation.setFromAxisRad(0, 0, 1, (float) (angle + Math.PI)); + + final MdxComplexInstance effectInstance = (MdxComplexInstance) mineEffectModel.addInstance(0); + + effectInstance.setScene(scene); + + effectInstance.setSequence(1); + + effectInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + effectInstance.localLocation.x = (float) Math.cos(angle) * 256; + effectInstance.localLocation.y = (float) Math.sin(angle) * 256; + effectInstance.localRotation.setFromAxisRad(0, 0, 1, (float) (angle)); + + } + final MdxModel mineModel = (MdxModel) this.viewer.load("buildings\\undead\\hauntedmine\\hauntedmine.mdx", + new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + final MdxComplexInstance mineInstance = (MdxComplexInstance) mineModel.addInstance(0); + + mineInstance.setScene(scene); + + mineInstance.setSequence(2); + + mineInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + } + + private void acolytesHarvestingSceneJoke2(final Scene scene) { + + final MdxModel acolyteModel = (MdxModel) this.viewer.load("units\\undead\\acolyte\\acolyte.mdx", + new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + final MdxModel mineEffectModel = (MdxModel) this.viewer + .load("abilities\\spells\\undead\\undeadmine\\undeadminecircle.mdx", new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + for (int i = 0; i < 5; i++) { + final MdxComplexInstance acolyteInstance = (MdxComplexInstance) acolyteModel.addInstance(0); + + acolyteInstance.setScene(scene); + + int animIndex = i % acolyteModel.getSequences().size(); + for (final Sequence s : acolyteModel.getSequences()) { + if (s.getName().toLowerCase().startsWith("stand work")) { + animIndex = acolyteModel.getSequences().indexOf(s); + } + } + acolyteInstance.setSequence(animIndex); + + acolyteInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + + final double angle = ((Math.PI * 2) / 5) * i; + acolyteInstance.localLocation.x = (float) Math.cos(angle) * 256; + acolyteInstance.localLocation.y = (float) Math.sin(angle) * 256; + acolyteInstance.localRotation.setFromAxisRad(0, 0, 1, (float) (angle + Math.PI)); + + final MdxComplexInstance effectInstance = (MdxComplexInstance) mineEffectModel.addInstance(0); + + effectInstance.setScene(scene); + + effectInstance.setSequence(1); + + effectInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + effectInstance.localLocation.x = (float) Math.cos(angle) * 256; + effectInstance.localLocation.y = (float) Math.sin(angle) * 256; + effectInstance.localRotation.setFromAxisRad(0, 0, 1, (float) (angle)); + + } + final MdxModel mineModel = (MdxModel) this.viewer.load("units\\orc\\spiritwolf\\spiritwolf.mdx", + new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + final MdxComplexInstance mineInstance = (MdxComplexInstance) mineModel.addInstance(0); + + mineInstance.setScene(scene); + + mineInstance.setSequence(0); + mineInstance.localScale.x = 2; + mineInstance.localScale.y = 2; + mineInstance.localScale.z = 2; + + mineInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + final MdxModel mineModel2 = (MdxModel) this.viewer + .load("abilities\\spells\\undead\\unsummon\\unsummontarget.mdx", new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + final MdxComplexInstance mineInstance2 = (MdxComplexInstance) mineModel2.addInstance(0); + + mineInstance2.setScene(scene); + + mineInstance2.setSequence(0); + + mineInstance2.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + } + + private void makeFourHundred(final Scene scene, final MdxModel model2) { + for (int i = 0; i < 400; i++) { + final MdxComplexInstance instance3 = (MdxComplexInstance) model2.addInstance(0); + instance3.localLocation.x = (((i % 20) - 10) * 128); + instance3.localLocation.y = (((i / 20) - 10) * 128); + + instance3.setScene(scene); + + final int animIndex = i % model2.getSequences().size(); + instance3.setSequence(animIndex); + + instance3.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + } + } + + private void makePerfectSquare(final Scene scene, final MdxModel model2, final int n) { + final int n2 = n * n; + for (int i = 0; i < n2; i++) { + final MdxComplexInstance instance3 = (MdxComplexInstance) model2.addInstance(0); + instance3.localLocation.x = (((i % n) - (n / 2)) * 128); + instance3.localLocation.y = (((i / n) - (n / 2)) * 128); + + instance3.setScene(scene); + + final int animIndex = i % model2.getSequences().size(); + instance3.setSequence(animIndex); + + instance3.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + } + } + + public static void bindDefaultVertexArray() { + Gdx.gl30.glBindVertexArray(VAO); + } + + private int frame = 0; + private MdxComplexInstance mainInstance; + private MdxModel mainModel; + private com.etheller.warsmash.viewer5.handlers.mdx.Camera modelCamera; + private final float[] cameraPositionTemp = new float[3]; + private final float[] cameraTargetTemp = new float[3]; + private final boolean firstFrame = true; + + @Override + public void render() { + Gdx.gl30.glBindVertexArray(VAO); + if (SPIN) { + this.cameraManager.horizontalAngle += 0.0001; + if (this.cameraManager.horizontalAngle > (2 * Math.PI)) { + this.cameraManager.horizontalAngle = 0; + } + } +// this.modelCamera = this.mainModel.cameras.get(this.mainInstance.sequence); + this.cameraManager.updateCamera(); + this.viewer.updateAndRender(); + +// gl.glDrawElements(GL20.GL_TRIANGLES, this.elements, GL20.GL_UNSIGNED_SHORT, this.faceOffset); + +// this.batch.begin(); +// this.font.draw(this.batch, Integer.toString(Gdx.graphics.getFramesPerSecond()), 0, 0); +// this.batch.end(); + + this.frame++; + if ((this.frame % 1000) == 0) { + System.out.println(Integer.toString(Gdx.graphics.getFramesPerSecond())); + } + + if (ADVANCE_ANIMS && this.mainInstance.sequenceEnded) { + final int sequence = (this.mainInstance.sequence + 1) % this.mainModel.getSequences().size(); + this.mainInstance.setSequence(sequence); + this.mainInstance.frame += (int) (Gdx.graphics.getRawDeltaTime() * 1000); + } +// if (this.firstFrame) { +// final Music music = Gdx.audio.newMusic(new DataSourceFileHandle(this.viewer.dataSource, +// "Sound\\Ambient\\DoodadEffects\\FinalCinematic.mp3")); +// music.setVolume(0.2f); +// music.setLooping(true); +// music.play(); +// this.firstFrame = false; +// } + } + + @Override + public void dispose() { + } + + @Override + public float getWidth() { + return Gdx.graphics.getWidth(); + } + + @Override + public float getHeight() { + return Gdx.graphics.getHeight(); + } + + @Override + public void resize(final int width, final int height) { + this.tempRect.width = width; + this.tempRect.height = height; + this.cameraManager.camera.viewport(this.tempRect); + } + + class CameraManager { + private CanvasProvider canvas; + private Camera camera; + private float moveSpeed; + private float rotationSpeed; + private float zoomFactor; + private float horizontalAngle; + private float verticalAngle; + private float distance; + private Vector3 position; + private Vector3 target; + private Vector3 worldUp; + private Vector3 vecHeap; + private Vector3 vecHeap2; + private Quaternion quatHeap; + private Quaternion quatHeap2; + + // An orbit camera setup example. + // Left mouse button controls the orbit itself. + // The right mouse button allows to move the camera and the point it's looking + // at on the XY plane. + // Scrolling zooms in and out. + private void setupCamera(final Scene scene) { + this.canvas = scene.viewer.canvas; + this.camera = scene.camera; + this.moveSpeed = 2; + this.rotationSpeed = (float) (Math.PI / 180); + this.zoomFactor = 0.1f; + this.horizontalAngle = (float) (Math.PI / 2); + this.verticalAngle = (float) (Math.PI / 4); + this.distance = 500; + this.position = new Vector3(); + this.target = new Vector3(0, 0, 50); + this.worldUp = new Vector3(0, 0, 1); + this.vecHeap = new Vector3(); + this.vecHeap2 = new Vector3(); + this.quatHeap = new Quaternion(); + this.quatHeap2 = new Quaternion(); + + updateCamera(); + +// cameraUpdate(); + } + + private void updateCamera() { + // Limit the vertical angle so it doesn't flip. + // Since the camera uses a quaternion, flips don't matter to it, but this feels + // better. + this.verticalAngle = (float) Math.min(Math.max(0.01, this.verticalAngle), Math.PI - 0.01); + + this.quatHeap.idt(); + this.quatHeap.setFromAxisRad(0, 0, 1, this.horizontalAngle); + this.quatHeap2.idt(); + this.quatHeap2.setFromAxisRad(1, 0, 0, this.verticalAngle); + this.quatHeap.mul(this.quatHeap2); + + this.position.set(0, 0, 1); + this.quatHeap.transform(this.position); + this.position.scl(this.distance); + this.position = this.position.add(this.target); + if (WarsmashGdxGame.this.modelCamera != null) { + WarsmashGdxGame.this.modelCamera.getPositionTranslation(WarsmashGdxGame.this.cameraPositionTemp, + WarsmashGdxGame.this.mainInstance.sequence, WarsmashGdxGame.this.mainInstance.frame, + WarsmashGdxGame.this.mainInstance.counter); + WarsmashGdxGame.this.modelCamera.getTargetTranslation(WarsmashGdxGame.this.cameraTargetTemp, + WarsmashGdxGame.this.mainInstance.sequence, WarsmashGdxGame.this.mainInstance.frame, + WarsmashGdxGame.this.mainInstance.counter); + + this.position.set(WarsmashGdxGame.this.modelCamera.position); + this.target.set(WarsmashGdxGame.this.modelCamera.targetPosition); +// this.vecHeap2.set(this.target); +// this.vecHeap2.sub(this.position); +// this.vecHeap.set(this.vecHeap2); +// this.vecHeap.crs(this.worldUp); +// this.vecHeap.crs(this.vecHeap2); +// this.vecHeap.nor(); +// this.vecHeap.scl(this.camera.rect.height / 2f); +// this.position.add(this.vecHeap); + + this.position.add(WarsmashGdxGame.this.cameraPositionTemp[0], + WarsmashGdxGame.this.cameraPositionTemp[1], WarsmashGdxGame.this.cameraPositionTemp[2]); + this.target.add(WarsmashGdxGame.this.cameraTargetTemp[0], WarsmashGdxGame.this.cameraTargetTemp[1], + WarsmashGdxGame.this.cameraTargetTemp[2]); + this.camera.perspective(WarsmashGdxGame.this.modelCamera.fieldOfView * 0.75f, + Gdx.graphics.getWidth() / (float) Gdx.graphics.getHeight(), + WarsmashGdxGame.this.modelCamera.nearClippingPlane, + WarsmashGdxGame.this.modelCamera.farClippingPlane); + } + else { + this.camera.perspective(70, this.camera.getAspect(), 100, 5000); + } + + this.camera.moveToAndFace(this.position, this.target, this.worldUp); + } + +// private void cameraUpdate() { +// +// } + } + + public DataSource getCodebase() { + return this.codebase; + } +} diff --git a/core/src/com/etheller/warsmash/WarsmashGdxMapScreen.java b/core/src/com/etheller/warsmash/WarsmashGdxMapScreen.java new file mode 100644 index 0000000..bc3601c --- /dev/null +++ b/core/src/com/etheller/warsmash/WarsmashGdxMapScreen.java @@ -0,0 +1,517 @@ +package com.etheller.warsmash; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.InputProcessor; +import com.badlogic.gdx.Screen; +import com.badlogic.gdx.audio.Music; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.GL30; +import com.badlogic.gdx.graphics.OrthographicCamera; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.utils.viewport.ExtendViewport; +import com.etheller.warsmash.datasources.CascDataSourceDescriptor; +import com.etheller.warsmash.datasources.CompoundDataSourceDescriptor; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.datasources.DataSourceDescriptor; +import com.etheller.warsmash.datasources.FolderDataSourceDescriptor; +import com.etheller.warsmash.datasources.MpqDataSourceDescriptor; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.jass.Jass2.RootFrameListener; +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.units.Element; +import com.etheller.warsmash.util.DataSourceFileHandle; +import com.etheller.warsmash.util.ImageUtils; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.Model; +import com.etheller.warsmash.viewer5.ModelInstance; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.PathSolver; +import com.etheller.warsmash.viewer5.RenderBatch; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.TextureMapper; +import com.etheller.warsmash.viewer5.handlers.ModelHandler; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; +import com.etheller.warsmash.viewer5.handlers.w3x.camera.CameraPreset; +import com.etheller.warsmash.viewer5.handlers.w3x.camera.CameraRates; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayerUnitOrderExecutor; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.MeleeUI; + +public class WarsmashGdxMapScreen implements InputProcessor, Screen { + public static final boolean ENABLE_AUDIO = true; + private static final boolean ENABLE_MUSIC = false; + private final War3MapViewer viewer; + private final Rectangle tempRect = new Rectangle(); + + // libGDX stuff + private OrthographicCamera uiCamera; + private SpriteBatch batch; + private ExtendViewport uiViewport; + private GlyphLayout glyphLayout; + + private Texture solidGreenTexture; + + private ShapeRenderer shapeRenderer; + + private MdxModel timeIndicator; + + private Scene uiScene; + private MeleeUI meleeUI; + + private Music currentMusic; + private final WarsmashGdxMultiScreenGame screenManager; + private final WarsmashGdxMenuScreen menuScreen; + + public WarsmashGdxMapScreen(final War3MapViewer mapViewer, final WarsmashGdxMultiScreenGame screenManager, + final WarsmashGdxMenuScreen menuScreen) { + this.viewer = mapViewer; + this.screenManager = screenManager; + this.menuScreen = menuScreen; + } + + /* + * (non-Javadoc) + * + * @see com.badlogic.gdx.ApplicationAdapter#create() + */ + @Override + public void show() { + + final ByteBuffer tempByteBuffer = ByteBuffer.allocateDirect(4); + tempByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final IntBuffer temp = tempByteBuffer.asIntBuffer(); + + Gdx.gl30.glGenVertexArrays(1, temp); + WarsmashGdxGame.VAO = temp.get(0); + + Gdx.gl30.glBindVertexArray(WarsmashGdxGame.VAO); + + final String renderer = Gdx.gl.glGetString(GL20.GL_RENDERER); + System.err.println("Renderer: " + renderer); + + final Element cameraData = this.viewer.miscData.get("Camera"); + Element cameraListenerData = this.viewer.miscData.get("Listener"); + if (cameraListenerData == null) { + cameraListenerData = new Element("Listener", new DataTable(null)); + } + final CameraPreset[] cameraPresets = new CameraPreset[6]; + for (int i = 0; i < cameraPresets.length; i++) { + cameraPresets[i] = new CameraPreset(cameraData.getFieldFloatValue("AOA", i), + cameraData.getFieldFloatValue("FOV", i), cameraData.getFieldFloatValue("Rotation", i), + cameraData.getFieldFloatValue("Rotation", i + cameraPresets.length), + cameraData.getFieldFloatValue("Rotation", i + (cameraPresets.length * 2)), + cameraData.getFieldFloatValue("Distance", i), cameraData.getFieldFloatValue("FarZ", i), + cameraData.getFieldFloatValue("NearZ", i), cameraData.getFieldFloatValue("Height", i), + cameraListenerData.getFieldFloatValue("ListenerDistance", i), + cameraListenerData.getFieldFloatValue("ListenerAOA", i)); + } + + System.out.println("Loaded"); + Gdx.gl30.glClearColor(0.0f, 0.0f, 0.0f, 1); // TODO remove white background + Gdx.gl30.glEnable(GL30.GL_SCISSOR_TEST); + + final Scene portraitScene = this.viewer.addSimpleScene(); + this.uiScene = this.viewer.addSimpleScene(); + this.uiScene.alpha = true; + if (ENABLE_AUDIO) { + this.uiScene.enableAudio(); + } + +// this.mainModel = (MdxModel) this.viewer.load("UI\\Glues\\MainMenu\\MainMenu3D_exp\\MainMenu3D_exp.mdx", + + // libGDX stuff + final int width = Gdx.graphics.getWidth(); + final int height = Gdx.graphics.getHeight(); + + this.glyphLayout = new GlyphLayout(); + + // Constructs a new OrthographicCamera, using the given viewport width and + // height + // Height is multiplied by aspect ratio. + this.uiCamera = new OrthographicCamera(); + int aspect3By4Width; + int aspect3By4Height; + if (width < ((height * 4) / 3)) { + aspect3By4Width = width; + aspect3By4Height = (width * 3) / 4; + } + else { + aspect3By4Width = (height * 4) / 3; + aspect3By4Height = height; + } + this.uiViewport = new ExtendViewport(aspect3By4Width, aspect3By4Height, this.uiCamera); + this.uiViewport.update(width, height); + + this.uiCamera.position.set(this.uiViewport.getMinWorldWidth() / 2, this.uiViewport.getMinWorldHeight() / 2, 0); + this.uiCamera.update(); + + this.batch = new SpriteBatch(); + +// this.consoleUITexture = new Texture(new DataSourceFileHandle(this.viewer.dataSource, "AlphaUi.png")); + + this.solidGreenTexture = ImageUtils.getAnyExtensionTexture(this.viewer.dataSource, + "ReplaceableTextures\\TeamColor\\TeamColor06.blp"); + + Gdx.input.setInputProcessor(this); + + this.shapeRenderer = new ShapeRenderer(); + +// Jass2.loadJUI(this.codebase, this.uiViewport, fontGenerator, this.uiScene, this.viewer, +// new RootFrameListener() { +// @Override +// public void onCreate(final GameUI rootFrame) { +// WarsmashGdxMapGame.this.gameUI = rootFrame; +// } +// }, "Scripts\\common.jui", "Scripts\\melee.jui"); + final Element cameraRatesElement = this.viewer.miscData.get("CameraRates"); + final CameraRates cameraRates = new CameraRates(cameraRatesElement.getFieldFloatValue("AOA"), + cameraRatesElement.getFieldFloatValue("FOV"), cameraRatesElement.getFieldFloatValue("Rotation"), + cameraRatesElement.getFieldFloatValue("Distance"), cameraRatesElement.getFieldFloatValue("Forward"), + cameraRatesElement.getFieldFloatValue("Strafe")); + this.meleeUI = new MeleeUI(this.viewer.mapMpq, this.uiViewport, this.uiScene, portraitScene, cameraPresets, + cameraRates, this.viewer, new RootFrameListener() { + @Override + public void onCreate(final GameUI rootFrame) { + WarsmashGdxMapScreen.this.viewer.setGameUI(rootFrame); + + if (ENABLE_MUSIC) { + final String musicField = rootFrame + .getSkinField("Music_V" + WarsmashConstants.GAME_VERSION); + final String[] musics = musicField.split(";"); + String musicPath = musics[(int) (Math.random() * musics.length)]; + if (false) { + musicPath = "Sound\\Music\\mp3Music\\PH1.mp3"; + } + final Music music = Gdx.audio.newMusic( + new DataSourceFileHandle(WarsmashGdxMapScreen.this.viewer.dataSource, musicPath)); + music.setVolume(1.0f); + music.setLooping(true); + music.play(); + WarsmashGdxMapScreen.this.currentMusic = music; + } + } + }, new CPlayerUnitOrderExecutor(this.viewer.simulation, this.viewer.getLocalPlayerIndex(), + this.viewer.getCommandErrorListener()), + new Runnable() { + @Override + public void run() { + WarsmashGdxMapScreen.this.menuScreen.onReturnFromGame(); + WarsmashGdxMapScreen.this.screenManager.setScreen(WarsmashGdxMapScreen.this.menuScreen); + } + }); + this.viewer.getCommandErrorListener().setDelegate(this.meleeUI); + final ModelInstance libgdxContentInstance = new LibGDXContentLayerModel(null, this.viewer, "", + this.viewer.mapPathSolver, "").addInstance(); + libgdxContentInstance.setScene(this.uiScene); + this.meleeUI.main(); + + updateUIScene(); + + resize(width, height); + + try { + this.viewer.loadAfterUI(); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + + public static DataSource parseDataSources(final DataTable warsmashIni) { + final Element dataSourcesConfig = warsmashIni.get("DataSources"); + final int dataSourcesCount = dataSourcesConfig.getFieldValue("Count"); + final List dataSourcesList = new ArrayList<>(); + for (int i = 0; i < dataSourcesCount; i++) { + final String type = dataSourcesConfig.getField("Type" + (i < 10 ? "0" : "") + i); + final String path = dataSourcesConfig.getField("Path" + (i < 10 ? "0" : "") + i); + switch (type) { + case "Folder": { + dataSourcesList.add(new FolderDataSourceDescriptor(path)); + break; + } + case "MPQ": { + dataSourcesList.add(new MpqDataSourceDescriptor(path)); + break; + } + case "CASC": { + final String prefixes = dataSourcesConfig.getField("Prefixes" + (i < 10 ? "0" : "") + i); + dataSourcesList.add(new CascDataSourceDescriptor(path, Arrays.asList(prefixes.split(",")))); + break; + } + default: + throw new RuntimeException("Unknown data source type: " + type); + } + } + return new CompoundDataSourceDescriptor(dataSourcesList).createDataSource(); + } + + private void updateUIScene() { + this.tempRect.x = this.uiViewport.getScreenX(); + this.tempRect.y = this.uiViewport.getScreenY(); + this.tempRect.width = this.uiViewport.getScreenWidth(); + this.tempRect.height = this.uiViewport.getScreenHeight(); + this.uiScene.camera.viewport(this.tempRect); + final float worldWidth = this.uiViewport.getWorldWidth(); + final float worldHeight = this.uiViewport.getWorldHeight(); + final float xScale = worldWidth / this.uiViewport.getMinWorldWidth(); + final float yScale = worldHeight / this.uiViewport.getMinWorldHeight(); + final float uiSceneWidth = 0.8f * xScale; + final float uiSceneHeight = 0.6f * yScale; + final float uiSceneX = ((0.8f - uiSceneWidth) / 2); + final float uiSceneY = ((0.6f - uiSceneHeight) / 2); + this.uiScene.camera.ortho(uiSceneX, uiSceneWidth + uiSceneX, uiSceneY, uiSceneHeight + uiSceneY, -1024f, 1024); + } + + @Override + public void render(final float delta) { + this.uiCamera.update(); + Gdx.gl30.glEnable(GL30.GL_SCISSOR_TEST); + final float deltaTime = Gdx.graphics.getDeltaTime(); + Gdx.gl30.glBindVertexArray(WarsmashGdxGame.VAO); + this.meleeUI.update(deltaTime); + this.viewer.updateAndRender(); + + Gdx.gl30.glDisable(GL30.GL_SCISSOR_TEST); + + Gdx.gl30.glDisable(GL30.GL_CULL_FACE); + + this.viewer.webGL.useShaderProgram(null); + + Gdx.gl30.glActiveTexture(GL30.GL_TEXTURE0); + } + + private void renderLibGDXContent() { + + Gdx.gl30.glDisable(GL30.GL_SCISSOR_TEST); + + Gdx.gl30.glDisable(GL30.GL_CULL_FACE); + + this.viewer.webGL.useShaderProgram(null); + + Gdx.gl30.glActiveTexture(GL30.GL_TEXTURE0); + + this.uiViewport.apply(); + this.batch.setProjectionMatrix(this.uiCamera.combined); + this.batch.begin(); + this.meleeUI.render(this.batch, this.glyphLayout); + this.batch.end(); + + Gdx.gl30.glEnable(GL30.GL_SCISSOR_TEST); + Gdx.gl30.glBindVertexArray(WarsmashGdxGame.VAO); + } + + @Override + public void dispose() { + this.meleeUI.dispose(); + } + + @Override + public void resize(final int width, final int height) { +// super.resize(width, height); + + this.uiViewport.update(width, height); + this.uiCamera.position.set(this.uiViewport.getMinWorldWidth() / 2, this.uiViewport.getMinWorldHeight() / 2, 0); + + this.meleeUI.resize(setupWorldFrameViewport(width, height)); + updateUIScene(); + + } + + private Rectangle setupWorldFrameViewport(final int width, final int height) { + this.tempRect.x = 0; + this.tempRect.width = width; + final float topHeight = 0.02666f * height; + final float bottomHeight = 0.21333f * height; + this.tempRect.y = (int) bottomHeight; + this.tempRect.height = height - (int) (topHeight + bottomHeight); + return this.tempRect; + } + + @Override + public boolean keyDown(final int keycode) { + this.meleeUI.keyDown(keycode); + return true; + } + + @Override + public boolean keyUp(final int keycode) { + this.meleeUI.keyUp(keycode); + return true; + } + + @Override + public boolean keyTyped(final char character) { + if (character == '1') { + Gdx.input.setCursorCatched(!Gdx.input.isCursorCatched()); + } + return false; + } + + @Override + public boolean touchDown(final int screenX, final int screenY, final int pointer, final int button) { + final float worldScreenY = this.viewer.canvas.getHeight() - screenY; + + if (this.meleeUI.touchDown(screenX, screenY, worldScreenY, button)) { + return false; + } + return false; + } + + @Override + public boolean touchUp(final int screenX, final int screenY, final int pointer, final int button) { + final float worldScreenY = this.viewer.canvas.getHeight() - screenY; + + if (this.meleeUI.touchUp(screenX, screenY, worldScreenY, button)) { + return false; + } + return false; + } + + @Override + public boolean touchDragged(final int screenX, final int screenY, final int pointer) { + final float worldScreenY = this.viewer.canvas.getHeight() - screenY; + if (this.meleeUI.touchDragged(screenX, screenY, worldScreenY, pointer)) { + return false; + } + return false; + } + + @Override + public boolean mouseMoved(final int screenX, final int screenY) { + final float worldScreenY = this.viewer.canvas.getHeight() - screenY; + if (this.meleeUI.mouseMoved(screenX, screenY, worldScreenY)) { + return false; + } + return false; + } + + @Override + public boolean scrolled(final int amount) { + this.meleeUI.scrolled(amount); + return true; + } + + private static class Message { + private final float time; + private final String text; + + public Message(final float time, final String text) { + this.time = time; + this.text = text; + } + } + + private class LibGDXContentLayerModelInstance extends ModelInstance { + + public LibGDXContentLayerModelInstance(final Model model) { + super(model); + } + + @Override + public void updateAnimations(final float dt) { + + } + + @Override + public void clearEmittedObjects() { + + } + + @Override + protected void updateLights(final Scene scene2) { + + } + + @Override + public void renderOpaque(final Matrix4 mvp) { + + } + + @Override + public void renderTranslucent() { + renderLibGDXContent(); + } + + @Override + public void load() { + } + + @Override + protected RenderBatch getBatch(final TextureMapper textureMapper2) { + throw new UnsupportedOperationException("NOT API"); + } + + @Override + public void setReplaceableTexture(final int replaceableTextureId, final String replaceableTextureFile) { + + } + + @Override + public boolean isBatched() { + return super.isBatched(); + } + + @Override + protected void removeLights(final Scene scene2) { + // TODO Auto-generated method stub + + } + + } + + private class LibGDXContentLayerModel extends Model { + + public LibGDXContentLayerModel(final ModelHandler handler, final ModelViewer viewer, final String extension, + final PathSolver pathSolver, final String fetchUrl) { + super(handler, viewer, extension, pathSolver, fetchUrl); + this.ok = true; + } + + @Override + protected ModelInstance createInstance(final int type) { + return new LibGDXContentLayerModelInstance(this); + } + + @Override + protected void lateLoad() { + } + + @Override + protected void load(final InputStream src, final Object options) { + } + + @Override + protected void error(final Exception e) { + } + + } + + @Override + public void pause() { + } + + @Override + public void resume() { + } + + @Override + public void hide() { + if (this.currentMusic != null) { + this.currentMusic.stop(); + } + } +} diff --git a/core/src/com/etheller/warsmash/WarsmashGdxMenuScreen.java b/core/src/com/etheller/warsmash/WarsmashGdxMenuScreen.java new file mode 100644 index 0000000..84d7674 --- /dev/null +++ b/core/src/com/etheller/warsmash/WarsmashGdxMenuScreen.java @@ -0,0 +1,882 @@ +package com.etheller.warsmash; + +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.InputProcessor; +import com.badlogic.gdx.Screen; +import com.badlogic.gdx.audio.Music; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.GL30; +import com.badlogic.gdx.graphics.OrthographicCamera; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Quaternion; +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.math.Vector3; +import com.badlogic.gdx.utils.viewport.ExtendViewport; +import com.badlogic.gdx.utils.viewport.FitViewport; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.jass.Jass2.RootFrameListener; +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.util.DataSourceFileHandle; +import com.etheller.warsmash.util.ImageUtils; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.Camera; +import com.etheller.warsmash.viewer5.CanvasProvider; +import com.etheller.warsmash.viewer5.Model; +import com.etheller.warsmash.viewer5.ModelInstance; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.PathSolver; +import com.etheller.warsmash.viewer5.RenderBatch; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.SolvedPath; +import com.etheller.warsmash.viewer5.TextureMapper; +import com.etheller.warsmash.viewer5.handlers.ModelHandler; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxHandler; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxViewer; +import com.etheller.warsmash.viewer5.handlers.mdx.Sequence; +import com.etheller.warsmash.viewer5.handlers.mdx.SequenceLoopMode; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.MenuUI; + +public class WarsmashGdxMenuScreen implements InputProcessor, Screen, SingleModelScreen { + private static final boolean ENABLE_AUDIO = true; + private static final boolean ENABLE_MUSIC = false; + private DataSource codebase; + private MdxViewer viewer; + private MdxModel model; + private CameraManager cameraManager; + private final Rectangle tempRect = new Rectangle(); + + // libGDX stuff + private OrthographicCamera uiCamera; + private SpriteBatch batch; + private Viewport uiViewport; + private GlyphLayout glyphLayout; + + private final DataTable warsmashIni; + private Scene uiScene; + private Texture solidGreenTexture; + private MenuUI menuUI; + private final WarsmashGdxMultiScreenGame game; + private Music currentMusic; + private boolean hasPlayedStandHack = false; + private boolean loaded = false; + + public WarsmashGdxMenuScreen(final DataTable warsmashIni, final WarsmashGdxMultiScreenGame game) { + this.warsmashIni = warsmashIni; + this.game = game; + } + + @Override + public void show() { + if (!this.loaded) { + this.loaded = true; + final ByteBuffer tempByteBuffer = ByteBuffer.allocateDirect(4); + tempByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final IntBuffer temp = tempByteBuffer.asIntBuffer(); +// + Gdx.gl30.glGenVertexArrays(1, temp); + WarsmashGdxGame.VAO = temp.get(0); + + Gdx.gl30.glBindVertexArray(WarsmashGdxGame.VAO); + + final String renderer = Gdx.gl.glGetString(GL20.GL_RENDERER); + System.err.println("Renderer: " + renderer); + + this.codebase = WarsmashGdxMapScreen.parseDataSources(this.warsmashIni); + this.viewer = new MdxViewer(this.codebase, this.game, Vector3.Zero); + + this.viewer.addHandler(new MdxHandler()); + this.viewer.enableAudio(); + + this.scene = this.viewer.addSimpleScene(); + this.scene.enableAudio(); + + this.uiScene = this.viewer.addSimpleScene(); + this.uiScene.alpha = true; + if (ENABLE_AUDIO) { + this.uiScene.enableAudio(); + } + final int width = Gdx.graphics.getWidth(); + final int height = Gdx.graphics.getHeight(); + + this.glyphLayout = new GlyphLayout(); + + // Constructs a new OrthographicCamera, using the given viewport width and + // height + // Height is multiplied by aspect ratio. + this.uiCamera = new OrthographicCamera(); + int aspect3By4Width; + int aspect3By4Height; + if (width < ((height * 4) / 3)) { + aspect3By4Width = width; + aspect3By4Height = (width * 3) / 4; + } + else { + aspect3By4Width = (height * 4) / 3; + aspect3By4Height = height; + } + this.uiViewport = new FitViewport(aspect3By4Width, aspect3By4Height, this.uiCamera); + this.uiViewport.update(width, height); + + this.uiCamera.position.set(getMinWorldWidth() / 2, getMinWorldHeight() / 2, 0); + this.uiCamera.update(); + + this.batch = new SpriteBatch(); + +// this.consoleUITexture = new Texture(new DataSourceFileHandle(this.viewer.dataSource, "AlphaUi.png")); + + this.solidGreenTexture = ImageUtils.getAnyExtensionTexture(this.viewer.dataSource, + "ReplaceableTextures\\TeamColor\\TeamColor06.blp"); + + this.cameraManager = new CameraManager(); + this.cameraManager.setupCamera(this.scene); + +// this.mainModel = (MdxModel) this.viewer.load("Doodads\\Cinematic\\ArthasIllidanFight\\ArthasIllidanFight.mdx", +// this.mainModel = (MdxModel) this.viewer.load("UI\\Glues\\SinglePlayer\\NightElf_Exp\\NightElf_Exp.mdx", +// this.mainModel = (MdxModel) this.viewer.load("Abilities\\Spells\\Orc\\FeralSpirit\\feralspirittarget.mdx", +// new PathSolver() { +// @Override +// public SolvedPath solve(final String src, final Object solverParams) { +// return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); +// } +// }, null); + +// final EventObjectEmitterObject evt = this.mainModel.getEventObjects().get(1); +// for (final Sequence seq : this.mainModel.getSequences()) { +// System.out.println(seq.getName() + ": " + Arrays.toString(seq.getInterval())); +// } +// System.out.println(Arrays.toString(evt.keyFrames)); +// System.out.println(evt.name); + +// this.mainInstance = (MdxComplexInstance) this.mainModel.addInstance(0); + +// this.mainInstance.setScene(scene); +// +// final int animIndex = 0; +// this.modelCamera = this.mainModel.cameras.get(animIndex); +// this.mainInstance.setSequence(animIndex); +// +// this.mainInstance.setSequenceLoopMode(SequenceLoopMode.LOOP_TO_NEXT_ANIMATION); + +// acolytesHarvestingSceneJoke2(scene); + +// singleModelScene(scene, "Buildings\\Undead\\Necropolis\\Necropolis.mdx", "birth"); +// singleModelScene(scene, "Units\\Orc\\KotoBeast\\KotoBeast.mdx", "spell slam"); + + System.out.println("Loaded"); + Gdx.gl30.glClearColor(0.0f, 0.0f, 0.0f, 1); + + this.menuUI = new MenuUI(this.viewer.dataSource, this.uiViewport, this.uiScene, this.viewer, this.game, + this, this.warsmashIni, new RootFrameListener() { + @Override + public void onCreate(final GameUI rootFrame) { +// WarsmashGdxMapGame.this.viewer.setGameUI(rootFrame); + + if (ENABLE_MUSIC) { + final String musicField = rootFrame + .getSkinField("GlueMusic_V" + WarsmashConstants.GAME_VERSION); + final String[] musics = musicField.split(";"); + final String musicPath = musics[(int) (Math.random() * musics.length)]; + final Music music = Gdx.audio.newMusic(new DataSourceFileHandle( + WarsmashGdxMenuScreen.this.viewer.dataSource, musicPath)); +// music.setVolume(0.2f); + music.setLooping(true); + music.play(); + WarsmashGdxMenuScreen.this.currentMusic = music; + } + + singleModelScene(WarsmashGdxMenuScreen.this.scene, War3MapViewer.mdx(rootFrame + .getSkinField("GlueSpriteLayerBackground_V" + WarsmashConstants.GAME_VERSION)), + "Stand"); + WarsmashGdxMenuScreen.this.modelCamera = WarsmashGdxMenuScreen.this.mainModel.cameras + .get(0); + } + }); + + final ModelInstance libgdxContentInstance = new LibGDXContentLayerModel(null, this.viewer, "", + PathSolver.DEFAULT, "").addInstance(); + libgdxContentInstance.setLocation(0f, 0f, -0.5f); + libgdxContentInstance.setScene(this.uiScene); + this.menuUI.main(); + + updateUIScene(); + + resize(width, height); + } + + Gdx.input.setInputProcessor(this); + if (this.currentMusic != null) { + this.currentMusic.play(); + } + + } + + private float getMinWorldWidth() { + if (this.uiViewport instanceof ExtendViewport) { + return ((ExtendViewport) this.uiViewport).getMinWorldWidth(); + } + return this.uiViewport.getWorldWidth(); + } + + private float getMinWorldHeight() { + if (this.uiViewport instanceof ExtendViewport) { + return ((ExtendViewport) this.uiViewport).getMinWorldHeight(); + } + return this.uiViewport.getWorldHeight(); + } + + private void updateUIScene() { + this.tempRect.x = this.uiViewport.getScreenX(); + this.tempRect.y = this.uiViewport.getScreenY(); + this.tempRect.width = this.uiViewport.getScreenWidth(); + this.tempRect.height = this.uiViewport.getScreenHeight(); + this.uiScene.camera.viewport(this.tempRect); + final float worldWidth = this.uiViewport.getWorldWidth(); + final float worldHeight = this.uiViewport.getWorldHeight(); + final float xScale = worldWidth / getMinWorldWidth(); + final float yScale = worldHeight / getMinWorldHeight(); + final float uiSceneWidth = 0.8f * xScale; + final float uiSceneHeight = 0.6f * yScale; + final float uiSceneX = ((0.8f - uiSceneWidth) / 2); + final float uiSceneY = ((0.6f - uiSceneHeight) / 2); + this.uiScene.camera.ortho(uiSceneX, uiSceneWidth + uiSceneX, uiSceneY, uiSceneHeight + uiSceneY, -1024f, 1024); + } + + private void makeDruidSquare(final Scene scene) { + final MdxModel model2 = (MdxModel) this.viewer.load("units\\nightelf\\druidoftheclaw\\druidoftheclaw.mdx", + new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + makePerfectSquare(scene, model2, 15); + } + + private void singleAcolyteScene(final Scene scene) { + final MdxModel model2 = (MdxModel) this.viewer.load("units\\undead\\acolyte\\acolyte.mdx", new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + + final MdxComplexInstance instance3 = (MdxComplexInstance) model2.addInstance(0); + + instance3.setScene(scene); + + int animIndex = 0; + for (final Sequence s : model2.getSequences()) { + if (s.getName().toLowerCase().startsWith("stand work")) { + animIndex = model2.getSequences().indexOf(s); + } + } + instance3.setSequence(animIndex); + + instance3.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + } + + private void singleModelScene(final Scene scene, final String path, final String animName) { + final MdxModel model2 = (MdxModel) this.viewer.load(path, new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + + final MdxComplexInstance instance3 = (MdxComplexInstance) model2.addInstance(0); + + instance3.setScene(scene); + + int animIndex = 0; + for (final Sequence s : model2.getSequences()) { + if (s.getName().toLowerCase().startsWith(animName)) { + animIndex = model2.getSequences().indexOf(s); + break; + } + } + instance3.setSequence(animIndex); + + instance3.setSequenceLoopMode(SequenceLoopMode.NEVER_LOOP); + this.mainInstance = instance3; + this.mainModel = model2; + } + + @Override + public void setModel(final String path) { + if (this.mainInstance != null) { + this.mainInstance.detach(); + } + if (path == null) { + this.modelCamera = null; + this.mainInstance = null; + this.mainModel = null; + } + else { + singleModelScene(this.scene, War3MapViewer.mdx(path), "birth"); + WarsmashGdxMenuScreen.this.modelCamera = WarsmashGdxMenuScreen.this.mainModel.cameras.get(0); + // this hack is because we only have the queued animation system in RenderWidget + // which is stupid and back and needs to get moved to the model instance + // itself... our model instance class is a + // hacky replica of a model viewer tool with a bunch of irrelevant loop type + // settings instead of what it should be + this.hasPlayedStandHack = false; + } + + } + + private void acolytesHarvestingScene(final Scene scene) { + + final MdxModel acolyteModel = (MdxModel) this.viewer.load("units\\undead\\acolyte\\acolyte.mdx", + new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + final MdxModel mineEffectModel = (MdxModel) this.viewer + .load("abilities\\spells\\undead\\undeadmine\\undeadminecircle.mdx", new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + for (int i = 0; i < 5; i++) { + final MdxComplexInstance acolyteInstance = (MdxComplexInstance) acolyteModel.addInstance(0); + + acolyteInstance.setScene(scene); + + int animIndex = i % acolyteModel.getSequences().size(); + for (final Sequence s : acolyteModel.getSequences()) { + if (s.getName().toLowerCase().startsWith("stand work")) { + animIndex = acolyteModel.getSequences().indexOf(s); + } + } + acolyteInstance.setSequence(animIndex); + + acolyteInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + + final double angle = ((Math.PI * 2) / 5) * i; + acolyteInstance.localLocation.x = (float) Math.cos(angle) * 256; + acolyteInstance.localLocation.y = (float) Math.sin(angle) * 256; + acolyteInstance.localRotation.setFromAxisRad(0, 0, 1, (float) (angle + Math.PI)); + + final MdxComplexInstance effectInstance = (MdxComplexInstance) mineEffectModel.addInstance(0); + + effectInstance.setScene(scene); + + effectInstance.setSequence(1); + + effectInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + effectInstance.localLocation.x = (float) Math.cos(angle) * 256; + effectInstance.localLocation.y = (float) Math.sin(angle) * 256; + effectInstance.localRotation.setFromAxisRad(0, 0, 1, (float) (angle)); + + } + final MdxModel mineModel = (MdxModel) this.viewer.load("buildings\\undead\\hauntedmine\\hauntedmine.mdx", + new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + final MdxComplexInstance mineInstance = (MdxComplexInstance) mineModel.addInstance(0); + + mineInstance.setScene(scene); + + mineInstance.setSequence(2); + + mineInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + } + + private void acolytesHarvestingSceneJoke2(final Scene scene) { + + final MdxModel acolyteModel = (MdxModel) this.viewer.load("units\\undead\\acolyte\\acolyte.mdx", + new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + final MdxModel mineEffectModel = (MdxModel) this.viewer + .load("abilities\\spells\\undead\\undeadmine\\undeadminecircle.mdx", new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + for (int i = 0; i < 5; i++) { + final MdxComplexInstance acolyteInstance = (MdxComplexInstance) acolyteModel.addInstance(0); + + acolyteInstance.setScene(scene); + + int animIndex = i % acolyteModel.getSequences().size(); + for (final Sequence s : acolyteModel.getSequences()) { + if (s.getName().toLowerCase().startsWith("stand work")) { + animIndex = acolyteModel.getSequences().indexOf(s); + } + } + acolyteInstance.setSequence(animIndex); + + acolyteInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + + final double angle = ((Math.PI * 2) / 5) * i; + acolyteInstance.localLocation.x = (float) Math.cos(angle) * 256; + acolyteInstance.localLocation.y = (float) Math.sin(angle) * 256; + acolyteInstance.localRotation.setFromAxisRad(0, 0, 1, (float) (angle + Math.PI)); + + final MdxComplexInstance effectInstance = (MdxComplexInstance) mineEffectModel.addInstance(0); + + effectInstance.setScene(scene); + + effectInstance.setSequence(1); + + effectInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + effectInstance.localLocation.x = (float) Math.cos(angle) * 256; + effectInstance.localLocation.y = (float) Math.sin(angle) * 256; + effectInstance.localRotation.setFromAxisRad(0, 0, 1, (float) (angle)); + + } + final MdxModel mineModel = (MdxModel) this.viewer.load("units\\orc\\spiritwolf\\spiritwolf.mdx", + new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + final MdxComplexInstance mineInstance = (MdxComplexInstance) mineModel.addInstance(0); + + mineInstance.setScene(scene); + + mineInstance.setSequence(0); + mineInstance.localScale.x = 2; + mineInstance.localScale.y = 2; + mineInstance.localScale.z = 2; + + mineInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + final MdxModel mineModel2 = (MdxModel) this.viewer + .load("abilities\\spells\\undead\\unsummon\\unsummontarget.mdx", new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + return new SolvedPath(src, src.substring(src.lastIndexOf('.')), true); + } + }, null); + final MdxComplexInstance mineInstance2 = (MdxComplexInstance) mineModel2.addInstance(0); + + mineInstance2.setScene(scene); + + mineInstance2.setSequence(0); + + mineInstance2.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + } + + private void makeFourHundred(final Scene scene, final MdxModel model2) { + for (int i = 0; i < 400; i++) { + final MdxComplexInstance instance3 = (MdxComplexInstance) model2.addInstance(0); + instance3.localLocation.x = (((i % 20) - 10) * 128); + instance3.localLocation.y = (((i / 20) - 10) * 128); + + instance3.setScene(scene); + + final int animIndex = i % model2.getSequences().size(); + instance3.setSequence(animIndex); + + instance3.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + } + } + + private void makePerfectSquare(final Scene scene, final MdxModel model2, final int n) { + final int n2 = n * n; + for (int i = 0; i < n2; i++) { + final MdxComplexInstance instance3 = (MdxComplexInstance) model2.addInstance(0); + instance3.localLocation.x = (((i % n) - (n / 2)) * 128); + instance3.localLocation.y = (((i / n) - (n / 2)) * 128); + + instance3.setScene(scene); + + final int animIndex = i % model2.getSequences().size(); + instance3.setSequence(animIndex); + + instance3.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + } + } + + public static void bindDefaultVertexArray() { + Gdx.gl30.glBindVertexArray(WarsmashGdxGame.VAO); + } + + private final int frame = 0; + private MdxComplexInstance mainInstance; + private MdxModel mainModel; + private com.etheller.warsmash.viewer5.handlers.mdx.Camera modelCamera; + private final float[] cameraPositionTemp = new float[3]; + private final float[] cameraTargetTemp = new float[3]; + private final boolean firstFrame = true; + private Scene scene; + + @Override + public void render(final float delta) { + + Gdx.gl30.glEnable(GL30.GL_SCISSOR_TEST); + final float deltaTime = Gdx.graphics.getDeltaTime(); + Gdx.gl30.glBindVertexArray(WarsmashGdxGame.VAO); + this.cameraManager.updateCamera(); + this.menuUI.update(deltaTime); + if ((this.mainInstance != null) && this.mainInstance.sequenceEnded + && (((this.mainModel.getSequences().get(this.mainInstance.sequence).getFlags() & 0x1) == 0) + || !this.hasPlayedStandHack)) { + SequenceUtils.randomStandSequence(this.mainInstance); + this.hasPlayedStandHack = true; + } + this.viewer.updateAndRender(); + + Gdx.gl30.glDisable(GL30.GL_SCISSOR_TEST); + + Gdx.gl30.glDisable(GL30.GL_CULL_FACE); + + this.viewer.webGL.useShaderProgram(null); + + Gdx.gl30.glActiveTexture(GL30.GL_TEXTURE0); + } + + @Override + public void dispose() { + this.menuUI.dispose(); + } + + @Override + public void resize(final int width, final int height) { + this.tempRect.width = width; + this.tempRect.height = height; + final float fourThirdsHeight = (this.tempRect.height * 4) / 3; + if (fourThirdsHeight < this.tempRect.width) { + final float dx = this.tempRect.width - fourThirdsHeight; + this.tempRect.width = fourThirdsHeight; + this.tempRect.x = dx / 2; + } + else { + final float threeFourthsWidth = (this.tempRect.width * 3) / 4; + if (threeFourthsWidth < this.tempRect.height) { + final float dy = this.tempRect.height - threeFourthsWidth; + this.tempRect.height = threeFourthsWidth; + this.tempRect.y = dy; + } + } + this.cameraManager.camera.viewport(this.tempRect); + +// super.resize(width, height); + + this.uiViewport.update(width, height); + this.uiCamera.position.set(getMinWorldWidth() / 2, getMinWorldHeight() / 2, 0); + + this.menuUI.resize(); + updateUIScene(); + + } + + class CameraManager { + private CanvasProvider canvas; + private Camera camera; + private float moveSpeed; + private float rotationSpeed; + private float zoomFactor; + private float horizontalAngle; + private float verticalAngle; + private float distance; + private Vector3 position; + private Vector3 target; + private Vector3 worldUp; + private Vector3 vecHeap; + private Vector3 vecHeap2; + private Quaternion quatHeap; + private Quaternion quatHeap2; + + // An orbit camera setup example. + // Left mouse button controls the orbit itself. + // The right mouse button allows to move the camera and the point it's looking + // at on the XY plane. + // Scrolling zooms in and out. + private void setupCamera(final Scene scene) { + this.canvas = scene.viewer.canvas; + this.camera = scene.camera; + this.moveSpeed = 2; + this.rotationSpeed = (float) (Math.PI / 180); + this.zoomFactor = 0.1f; + this.horizontalAngle = (float) (Math.PI / 2); + this.verticalAngle = (float) (Math.PI / 4); + this.distance = 500; + this.position = new Vector3(); + this.target = new Vector3(0, 0, 50); + this.worldUp = new Vector3(0, 0, 1); + this.vecHeap = new Vector3(); + this.vecHeap2 = new Vector3(); + this.quatHeap = new Quaternion(); + this.quatHeap2 = new Quaternion(); + + updateCamera(); + +// cameraUpdate(); + } + + private void updateCamera() { + // Limit the vertical angle so it doesn't flip. + // Since the camera uses a quaternion, flips don't matter to it, but this feels + // better. + this.verticalAngle = (float) Math.min(Math.max(0.01, this.verticalAngle), Math.PI - 0.01); + + this.quatHeap.idt(); + this.quatHeap.setFromAxisRad(0, 0, 1, this.horizontalAngle); + this.quatHeap2.idt(); + this.quatHeap2.setFromAxisRad(1, 0, 0, this.verticalAngle); + this.quatHeap.mul(this.quatHeap2); + + this.position.set(0, 0, 1); + this.quatHeap.transform(this.position); + this.position.scl(this.distance); + this.position = this.position.add(this.target); + if (WarsmashGdxMenuScreen.this.modelCamera != null) { + WarsmashGdxMenuScreen.this.modelCamera.getPositionTranslation( + WarsmashGdxMenuScreen.this.cameraPositionTemp, WarsmashGdxMenuScreen.this.mainInstance.sequence, + WarsmashGdxMenuScreen.this.mainInstance.frame, WarsmashGdxMenuScreen.this.mainInstance.counter); + WarsmashGdxMenuScreen.this.modelCamera.getTargetTranslation(WarsmashGdxMenuScreen.this.cameraTargetTemp, + WarsmashGdxMenuScreen.this.mainInstance.sequence, WarsmashGdxMenuScreen.this.mainInstance.frame, + WarsmashGdxMenuScreen.this.mainInstance.counter); + + this.position.set(WarsmashGdxMenuScreen.this.modelCamera.position); + this.target.set(WarsmashGdxMenuScreen.this.modelCamera.targetPosition); +// this.vecHeap2.set(this.target); +// this.vecHeap2.sub(this.position); +// this.vecHeap.set(this.vecHeap2); +// this.vecHeap.crs(this.worldUp); +// this.vecHeap.crs(this.vecHeap2); +// this.vecHeap.nor(); +// this.vecHeap.scl(this.camera.rect.height / 2f); +// this.position.add(this.vecHeap); + + this.position.add(WarsmashGdxMenuScreen.this.cameraPositionTemp[0], + WarsmashGdxMenuScreen.this.cameraPositionTemp[1], + WarsmashGdxMenuScreen.this.cameraPositionTemp[2]); + this.target.add(WarsmashGdxMenuScreen.this.cameraTargetTemp[0], + WarsmashGdxMenuScreen.this.cameraTargetTemp[1], WarsmashGdxMenuScreen.this.cameraTargetTemp[2]); + this.camera.perspective(WarsmashGdxMenuScreen.this.modelCamera.fieldOfView * 0.6f, + this.camera.rect.width / this.camera.rect.height, + WarsmashGdxMenuScreen.this.modelCamera.nearClippingPlane, + WarsmashGdxMenuScreen.this.modelCamera.farClippingPlane); + } + else { + this.camera.perspective(70, this.camera.getAspect(), 100, 5000); + } + + this.camera.moveToAndFace(this.position, this.target, this.worldUp); + } + +// private void cameraUpdate() { +// +// } + } + + public DataSource getCodebase() { + return this.codebase; + } + + @Override + public boolean keyDown(final int keycode) { + return this.menuUI.keyDown(keycode); + } + + @Override + public boolean keyUp(final int keycode) { + return this.menuUI.keyUp(keycode); + } + + @Override + public boolean keyTyped(final char character) { + return this.menuUI.keyTyped(character); + } + + @Override + public boolean touchDown(final int screenX, final int screenY, final int pointer, final int button) { + final float worldScreenY = this.game.getHeight() - screenY; + + if (this.menuUI.touchDown(screenX, screenY, worldScreenY, button)) { + return false; + } + return false; + } + + @Override + public boolean touchUp(final int screenX, final int screenY, final int pointer, final int button) { + final float worldScreenY = this.game.getHeight() - screenY; + + if (this.menuUI.touchUp(screenX, screenY, worldScreenY, button)) { + return false; + } + return false; + } + + @Override + public boolean touchDragged(final int screenX, final int screenY, final int pointer) { + final float worldScreenY = this.game.getHeight() - screenY; + if (this.menuUI.touchDragged(screenX, screenY, worldScreenY, pointer)) { + return false; + } + return false; + } + + @Override + public boolean mouseMoved(final int screenX, final int screenY) { + final float worldScreenY = this.game.getHeight() - screenY; + if (this.menuUI.mouseMoved(screenX, screenY, worldScreenY)) { + return false; + } + return false; + } + + @Override + public boolean scrolled(final int amount) { + // TODO Auto-generated method stub + return false; + } + + private void renderLibGDXContent() { + + Gdx.gl30.glDisable(GL30.GL_SCISSOR_TEST); + + Gdx.gl30.glDisable(GL30.GL_CULL_FACE); + + this.viewer.webGL.useShaderProgram(null); + + Gdx.gl30.glActiveTexture(GL30.GL_TEXTURE0); + + this.uiViewport.apply(); + this.batch.setProjectionMatrix(this.uiCamera.combined); + this.batch.begin(); + this.menuUI.render(this.batch, this.glyphLayout); + this.batch.end(); + + Gdx.gl30.glEnable(GL30.GL_SCISSOR_TEST); + Gdx.gl30.glBindVertexArray(WarsmashGdxGame.VAO); + } + + private class LibGDXContentLayerModelInstance extends ModelInstance { + + public LibGDXContentLayerModelInstance(final Model model) { + super(model); + } + + @Override + public void updateAnimations(final float dt) { + + } + + @Override + public void clearEmittedObjects() { + + } + + @Override + protected void updateLights(final Scene scene2) { + + } + + @Override + public void renderOpaque(final Matrix4 mvp) { + + } + + @Override + public void renderTranslucent() { + renderLibGDXContent(); + } + + @Override + public void load() { + } + + @Override + protected RenderBatch getBatch(final TextureMapper textureMapper2) { + throw new UnsupportedOperationException("NOT API"); + } + + @Override + public void setReplaceableTexture(final int replaceableTextureId, final String replaceableTextureFile) { + + } + + @Override + public boolean isBatched() { + return super.isBatched(); + } + + @Override + protected void removeLights(final Scene scene2) { + // TODO Auto-generated method stub + + } + + } + + private class LibGDXContentLayerModel extends Model { + + public LibGDXContentLayerModel(final ModelHandler handler, final ModelViewer viewer, final String extension, + final PathSolver pathSolver, final String fetchUrl) { + super(handler, viewer, extension, pathSolver, fetchUrl); + this.ok = true; + } + + @Override + protected ModelInstance createInstance(final int type) { + return new LibGDXContentLayerModelInstance(this); + } + + @Override + protected void lateLoad() { + } + + @Override + protected void load(final InputStream src, final Object options) { + } + + @Override + protected void error(final Exception e) { + } + + } + + @Override + public void hide() { + if (this.currentMusic != null) { + this.currentMusic.stop(); + } + this.menuUI.hide(); + } + + @Override + public void pause() { + } + + @Override + public void resume() { + } + + public void startMap(final String finalFileToLoad) { + this.menuUI.startMap(finalFileToLoad); + } + + public void onReturnFromGame() { + this.menuUI.onReturnFromGame(); + } +} diff --git a/core/src/com/etheller/warsmash/WarsmashGdxMultiScreenGame.java b/core/src/com/etheller/warsmash/WarsmashGdxMultiScreenGame.java new file mode 100644 index 0000000..2ba3bb4 --- /dev/null +++ b/core/src/com/etheller/warsmash/WarsmashGdxMultiScreenGame.java @@ -0,0 +1,23 @@ +package com.etheller.warsmash; + +import com.badlogic.gdx.Game; +import com.badlogic.gdx.Gdx; +import com.etheller.warsmash.viewer5.CanvasProvider; + +public class WarsmashGdxMultiScreenGame extends Game implements CanvasProvider { + + @Override + public void create() { + } + + @Override + public float getWidth() { + return Gdx.graphics.getWidth(); + } + + @Override + public float getHeight() { + return Gdx.graphics.getHeight(); + } + +} diff --git a/core/src/com/etheller/warsmash/WarsmashPreviewApplication.java b/core/src/com/etheller/warsmash/WarsmashPreviewApplication.java new file mode 100644 index 0000000..36d628a --- /dev/null +++ b/core/src/com/etheller/warsmash/WarsmashPreviewApplication.java @@ -0,0 +1,177 @@ +package com.etheller.warsmash; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; + +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.viewer5.CanvasProvider; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.PathSolver; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.handlers.ResourceHandlerConstructionParams; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxHandler; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxViewer; +import com.etheller.warsmash.viewer5.handlers.w3x.camera.PortraitCameraManager; +import com.hiveworkshop.rms.parsers.mdlx.MdlxModel; +import com.hiveworkshop.rms.parsers.mdlx.util.MdxUtils; + +public class WarsmashPreviewApplication extends ApplicationAdapter implements CanvasProvider { + private DataSource codebase; + private ModelViewer viewer; + private MdxModel model; + private PortraitCameraManager cameraManager; + public static int VAO; + private final Rectangle tempRect = new Rectangle(); + + private BitmapFont font; + private SpriteBatch batch; + private final DataTable warsmashIni; + + public WarsmashPreviewApplication(final DataTable warsmashIni) { + this.warsmashIni = warsmashIni; + } + + @Override + public void create() { + + final ByteBuffer tempByteBuffer = ByteBuffer.allocateDirect(4); + tempByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final IntBuffer temp = tempByteBuffer.asIntBuffer(); +// + Gdx.gl30.glGenVertexArrays(1, temp); + VAO = temp.get(0); + + Gdx.gl30.glBindVertexArray(VAO); + + final String renderer = Gdx.gl.glGetString(GL20.GL_RENDERER); + System.err.println("Renderer: " + renderer); + + this.codebase = WarsmashGdxMapScreen.parseDataSources(this.warsmashIni); + this.viewer = new MdxViewer(this.codebase, this, new Vector3(0.3f, 0.3f, -0.25f)); + + this.mdxHandler = new MdxHandler(); + this.viewer.addHandler(this.mdxHandler); + this.viewer.enableAudio(); + + this.scene = this.viewer.addSimpleScene(); + this.scene.enableAudio(); + + this.cameraManager = new PortraitCameraManager(); + this.cameraManager.setupCamera(this.scene); + this.cameraManager.distance = 500; + this.cameraManager.horizontalAngle = (float) ((Math.PI) / 2); + + System.out.println("Loaded"); + Gdx.gl30.glClearColor(0.5f, 0.5f, 0.5f, 1); // TODO remove white background + + this.font = new BitmapFont(); + this.batch = new SpriteBatch(); + } + + public static void bindDefaultVertexArray() { + Gdx.gl30.glBindVertexArray(VAO); + } + + private int frame = 0; + private MdxComplexInstance mainInstance; + private MdxModel mainModel; + private com.etheller.warsmash.viewer5.handlers.mdx.Camera modelCamera; + private final float[] cameraPositionTemp = new float[3]; + private final float[] cameraTargetTemp = new float[3]; + private final boolean firstFrame = true; + private Scene scene; + private MdxHandler mdxHandler; + + @Override + public void render() { + Gdx.gl30.glBindVertexArray(VAO); + this.cameraManager.updateCamera(); + this.viewer.updateAndRender(); + + this.frame++; + if ((this.frame % 1000) == 0) { + System.out.println(Integer.toString(Gdx.graphics.getFramesPerSecond())); + } + + } + + @Override + public void dispose() { + } + + @Override + public float getWidth() { + return Gdx.graphics.getWidth(); + } + + @Override + public float getHeight() { + return Gdx.graphics.getHeight(); + } + + @Override + public void resize(final int width, final int height) { + this.tempRect.width = width; + this.tempRect.height = height; + this.cameraManager.camera.viewport(this.tempRect); + } + + public DataSource getCodebase() { + return this.codebase; + } + + public MdlxModel loadCustomModel(final String filename) { + clearMainInstance(); + final MdxModel mdx = (MdxModel) this.mdxHandler.construct(new ResourceHandlerConstructionParams(this.viewer, + this.mdxHandler, ".mdx", PathSolver.DEFAULT, filename)); + final MdlxModel mdlxModel; + try (FileInputStream stream = new FileInputStream(filename)) { + mdlxModel = MdxUtils.loadMdlx(stream); + mdx.load(mdlxModel); + mdx.ok = true; +// mdx.lateLoad(); + } + catch (final FileNotFoundException e) { + throw new RuntimeException(e); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + this.mainModel = mdx; + final MdxComplexInstance instance = (MdxComplexInstance) mdx.addInstance(); +// this.cameraManager.setModelInstance(instance, mdx); + instance.setScene(this.scene); + this.mainInstance = instance; + return mdlxModel; + } + + private void clearMainInstance() { + if (this.mainInstance != null) { + this.mainInstance.detach(); + this.mainInstance = null; + } + this.mainModel = null; + } + + public PortraitCameraManager getCameraManager() { + return this.cameraManager; + } + + public MdxComplexInstance getMainInstance() { + return this.mainInstance; + } +} diff --git a/core/src/com/etheller/warsmash/WarsmashTestGame.java b/core/src/com/etheller/warsmash/WarsmashTestGame.java new file mode 100644 index 0000000..635aac1 --- /dev/null +++ b/core/src/com/etheller/warsmash/WarsmashTestGame.java @@ -0,0 +1,113 @@ +package com.etheller.warsmash; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; + +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; + +public class WarsmashTestGame extends ApplicationAdapter { + private int arrayBuffer; + private int elementBuffer; + private int VAO; + + @Override + public void create() { + Gdx.gl.glClearColor(0.5f, 0.5f, 0.5f, 1.0f); // colour to use when clearing + + final ByteBuffer tempByteBuffer = ByteBuffer.allocateDirect(4); + tempByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final IntBuffer temp = tempByteBuffer.asIntBuffer(); + + Gdx.gl30.glGenVertexArrays(1, temp); + this.VAO = temp.get(0); + + Gdx.gl30.glBindVertexArray(this.VAO); + + this.shaderProgram = new ShaderProgram(vsSimple, fsSimple); + if (!this.shaderProgram.isCompiled()) { + throw new IllegalStateException(this.shaderProgram.getLog()); + } + + this.arrayBuffer = Gdx.gl.glGenBuffer(); + this.elementBuffer = Gdx.gl.glGenBuffer(); + System.out.println("arrayBuffer: " + this.arrayBuffer + ", elementBuffer: " + this.elementBuffer); + + Gdx.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.arrayBuffer); + + this.shaderProgram.enableVertexAttribute("a_position"); + this.shaderProgram.setVertexAttribute("a_position", 3, GL20.GL_FLOAT, false, 0, 0); + + Gdx.gl.glBindBuffer(GL20.GL_ELEMENT_ARRAY_BUFFER, this.elementBuffer); + + final ByteBuffer vertexByteBuffer = ByteBuffer.allocateDirect(4 * 9); + vertexByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + this.vertexBuffer = vertexByteBuffer.asFloatBuffer(); + + this.vertexBuffer.put(0, -1f); + this.vertexBuffer.put(1, -1f); + this.vertexBuffer.put(2, 0); + this.vertexBuffer.put(3, 1f); + this.vertexBuffer.put(4, -1f); + this.vertexBuffer.put(5, 0); + this.vertexBuffer.put(6, 0f); + this.vertexBuffer.put(7, 1f); + this.vertexBuffer.put(8, 0); + + Gdx.gl.glBufferData(GL20.GL_ARRAY_BUFFER, 9 * 4, null, GL20.GL_STATIC_DRAW); + + final ByteBuffer faceByteBuffer = ByteBuffer.allocateDirect(6); + faceByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + this.faceBuffer = faceByteBuffer.asShortBuffer(); + + this.faceBuffer.put(0, (short) 0); + this.faceBuffer.put(1, (short) 1); + this.faceBuffer.put(2, (short) 2); + + Gdx.gl.glBufferData(GL20.GL_ELEMENT_ARRAY_BUFFER, 3 * 2, null, GL20.GL_STATIC_DRAW); + + } + + @Override + public void render() { + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT); + +// Gdx.gl30.glBindVertexArray(this.VAO); + this.shaderProgram.begin(); + Gdx.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.arrayBuffer); + Gdx.gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, 0, 9 * 4, this.vertexBuffer); + Gdx.gl.glBindBuffer(GL20.GL_ELEMENT_ARRAY_BUFFER, this.elementBuffer); + Gdx.gl.glBufferSubData(GL20.GL_ELEMENT_ARRAY_BUFFER, 0, 3 * 2, this.faceBuffer); + Gdx.gl.glDrawElements(GL20.GL_TRIANGLES, 9, GL20.GL_UNSIGNED_SHORT, 0); +// Gdx.gl.glDrawArrays(GL20.GL_TRIANGLES, 0, 3); + this.shaderProgram.end(); + } + + @Override + public void dispose() { + } + + @Override + public void resize(final int width, final int height) { + } + + public static final String vsSimple = "\r\n" + // + " attribute vec3 a_position;\r\n" + // + " void main() {\r\n" + // + " gl_Position = vec4(a_position, 1.0);\r\n" + // + " }\r\n"; + + public static final String fsSimple = "\r\n" + // + " void main() {\r\n" + // + " gl_FragColor = vec4(0.0, 1.0, 1.0, 1.0);\r\n" + // + " }\r\n"; + private ShaderProgram shaderProgram; + private FloatBuffer vertexBuffer; + private ShortBuffer faceBuffer; + +} diff --git a/core/src/com/etheller/warsmash/WarsmashTestGame2.java b/core/src/com/etheller/warsmash/WarsmashTestGame2.java new file mode 100644 index 0000000..8261284 --- /dev/null +++ b/core/src/com/etheller/warsmash/WarsmashTestGame2.java @@ -0,0 +1,138 @@ +package com.etheller.warsmash; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.GL30; + +public class WarsmashTestGame2 extends ApplicationAdapter { + private int VBO; + private int VAO; + + @Override + public void create() { + final ByteBuffer vertexByteBuffer = ByteBuffer.allocateDirect(4 * 9); + vertexByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final FloatBuffer vertexBuffer = vertexByteBuffer.asFloatBuffer(); + + vertexBuffer.put(0, -0.5f); + vertexBuffer.put(1, -0.5f); + vertexBuffer.put(2, 0); + vertexBuffer.put(3, 0.5f); + vertexBuffer.put(4, -0.5f); + vertexBuffer.put(5, 0); + vertexBuffer.put(6, 0f); + vertexBuffer.put(7, 0.5f); + vertexBuffer.put(8, 0); + vertexBuffer.clear(); + + Gdx.gl30.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + Gdx.gl30.glEnable(GL20.GL_DEPTH_TEST); +// Gdx.gl30.glEnable(GL20.GL_CULL_FACE); + + final ByteBuffer tempByteBuffer = ByteBuffer.allocateDirect(4); + System.out.println(tempByteBuffer.order()); + tempByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final IntBuffer temp = tempByteBuffer.asIntBuffer(); + + Gdx.gl30.glGenBuffers(1, temp); + this.VBO = temp.get(0); + + temp.clear(); + Gdx.gl30.glGenVertexArrays(1, temp); + this.VAO = temp.get(0); + + Gdx.gl30.glBindVertexArray(this.VAO); + + Gdx.gl30.glBindBuffer(GL30.GL_ARRAY_BUFFER, this.VBO); + Gdx.gl30.glBufferData(GL30.GL_ARRAY_BUFFER, 9 * 4, vertexByteBuffer, GL30.GL_STATIC_DRAW); + + Gdx.gl30.glVertexAttribPointer(0, 3, GL30.GL_FLOAT, false, 3 * 4, 0); + Gdx.gl30.glEnableVertexAttribArray(0); + + final int vertexShader = Gdx.gl30.glCreateShader(GL30.GL_VERTEX_SHADER); + Gdx.gl30.glShaderSource(vertexShader, vsSimple); + Gdx.gl30.glCompileShader(vertexShader); + + temp.clear(); + Gdx.gl30.glGetShaderiv(vertexShader, GL30.GL_COMPILE_STATUS, temp); + int success = temp.get(0); + if (success == 0) { + final String infoLog = Gdx.gl30.glGetShaderInfoLog(vertexShader); + System.err.println(infoLog); + throw new IllegalStateException("bad vertex shader"); + } + + final int fragmentShader = Gdx.gl30.glCreateShader(GL30.GL_FRAGMENT_SHADER); + Gdx.gl30.glShaderSource(fragmentShader, fsSimple); + Gdx.gl30.glCompileShader(fragmentShader); + + temp.clear(); + Gdx.gl30.glGetShaderiv(fragmentShader, GL30.GL_COMPILE_STATUS, temp); + success = temp.get(0); + if (success == 0) { + final String infoLog = Gdx.gl30.glGetShaderInfoLog(fragmentShader); + System.err.println(infoLog); + throw new IllegalStateException("bad fragment shader"); + } + + this.shaderProgram = Gdx.gl30.glCreateProgram(); + + Gdx.gl30.glAttachShader(this.shaderProgram, vertexShader); + Gdx.gl30.glAttachShader(this.shaderProgram, fragmentShader); + Gdx.gl30.glLinkProgram(this.shaderProgram); + + temp.clear(); + Gdx.gl30.glGetProgramiv(this.shaderProgram, GL30.GL_LINK_STATUS, temp); + success = temp.get(0); + if (success == 0) { + final String infoLog = Gdx.gl30.glGetProgramInfoLog(this.shaderProgram); + System.err.println(infoLog); + throw new IllegalStateException("bad program"); + } + + Gdx.gl30.glDeleteShader(vertexShader); + Gdx.gl30.glDeleteShader(fragmentShader); + } + + @Override + public void render() { + Gdx.gl30.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT); + Gdx.gl30.glUseProgram(this.shaderProgram); + Gdx.gl30.glBindVertexArray(this.VAO); + + Gdx.gl30.glDrawArrays(GL30.GL_TRIANGLES, 0, 3); + } + + @Override + public void dispose() { + } + + @Override + public void resize(final int width, final int height) { + final int side = Math.min(width, height); + Gdx.gl30.glViewport((width - side) / 2, (height - side) / 2, side, side); + + } + + public static final String vsSimple = "\r\n" + // + "#version 450 core\r\n" + // + " layout(location = 0) in vec3 aPos;\r\n" + // + " void main() {\r\n" + // + " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\r\n" + // + " }\r\n"; + + public static final String fsSimple = "\r\n" + // + "#version 450 core\r\n" + // + " out vec4 FragColor;\r\n" + // + " void main() {\r\n" + // + " FragColor = vec4(0.2f, 1.0f, 0.2f, 1.0f);\r\n" + // + " }\r\n"; + private int shaderProgram; + +} diff --git a/core/src/com/etheller/warsmash/WarsmashTestGame3.java b/core/src/com/etheller/warsmash/WarsmashTestGame3.java new file mode 100644 index 0000000..f2b0ee7 --- /dev/null +++ b/core/src/com/etheller/warsmash/WarsmashTestGame3.java @@ -0,0 +1,145 @@ +package com.etheller.warsmash; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.GL30; + +public class WarsmashTestGame3 extends ApplicationAdapter { + private int VBO; + private int VAO; + + @Override + public void create() { + Gdx.gl30.glBindVertexArray(0); + + this.vertexByteBuffer = ByteBuffer.allocateDirect(4 * 9); + this.vertexByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final FloatBuffer vertexBuffer = this.vertexByteBuffer.asFloatBuffer(); + + vertexBuffer.put(0, -0.5f); + vertexBuffer.put(1, -0.5f); + vertexBuffer.put(2, 0); + vertexBuffer.put(3, 0.5f); + vertexBuffer.put(4, -0.5f); + vertexBuffer.put(5, 0); + vertexBuffer.put(6, 0f); + vertexBuffer.put(7, 0.5f); + vertexBuffer.put(8, 0); + vertexBuffer.clear(); + + Gdx.gl30.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + Gdx.gl30.glEnable(GL20.GL_DEPTH_TEST); +// Gdx.gl30.glEnable(GL20.GL_CULL_FACE); + + final ByteBuffer tempByteBuffer = ByteBuffer.allocateDirect(4); + tempByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final IntBuffer temp = tempByteBuffer.asIntBuffer(); + + Gdx.gl30.glGenVertexArrays(1, temp); + this.VAO = temp.get(0); + + Gdx.gl30.glBindVertexArray(this.VAO); + + temp.clear(); + Gdx.gl30.glGenBuffers(1, temp); + this.VBO = temp.get(0); + + Gdx.gl30.glBindBuffer(GL30.GL_ARRAY_BUFFER, this.VBO); + Gdx.gl30.glBufferData(GL30.GL_ARRAY_BUFFER, 9 * 4, this.vertexByteBuffer, GL30.GL_STATIC_DRAW); + + Gdx.gl30.glVertexAttribPointer(0, 3, GL30.GL_FLOAT, false, 3 * 4, 0); + Gdx.gl30.glEnableVertexAttribArray(0); + + final int vertexShader = Gdx.gl30.glCreateShader(GL30.GL_VERTEX_SHADER); + Gdx.gl30.glShaderSource(vertexShader, vsSimple); + Gdx.gl30.glCompileShader(vertexShader); + + temp.clear(); + Gdx.gl30.glGetShaderiv(vertexShader, GL30.GL_COMPILE_STATUS, temp); + int success = temp.get(0); + if (success == 0) { + final String infoLog = Gdx.gl30.glGetShaderInfoLog(vertexShader); + System.err.println(infoLog); + throw new IllegalStateException("bad vertex shader"); + } + + final int fragmentShader = Gdx.gl30.glCreateShader(GL30.GL_FRAGMENT_SHADER); + Gdx.gl30.glShaderSource(fragmentShader, fsSimple); + Gdx.gl30.glCompileShader(fragmentShader); + + temp.clear(); + Gdx.gl30.glGetShaderiv(fragmentShader, GL30.GL_COMPILE_STATUS, temp); + success = temp.get(0); + if (success == 0) { + final String infoLog = Gdx.gl30.glGetShaderInfoLog(fragmentShader); + System.err.println(infoLog); + throw new IllegalStateException("bad fragment shader"); + } + + this.shaderProgram = Gdx.gl30.glCreateProgram(); + + Gdx.gl30.glAttachShader(this.shaderProgram, vertexShader); + Gdx.gl30.glAttachShader(this.shaderProgram, fragmentShader); + Gdx.gl30.glLinkProgram(this.shaderProgram); + + temp.clear(); + Gdx.gl30.glGetProgramiv(this.shaderProgram, GL30.GL_LINK_STATUS, temp); + success = temp.get(0); + if (success == 0) { + final String infoLog = Gdx.gl30.glGetProgramInfoLog(this.shaderProgram); + System.err.println(infoLog); + throw new IllegalStateException("bad program"); + } + + Gdx.gl30.glDeleteShader(vertexShader); + Gdx.gl30.glDeleteShader(fragmentShader); + } + + @Override + public void render() { + + Gdx.gl30.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT); + Gdx.gl30.glBindVertexArray(this.VAO); + Gdx.gl30.glUseProgram(this.shaderProgram); + Gdx.gl30.glBindBuffer(GL30.GL_ARRAY_BUFFER, this.VBO); + Gdx.gl30.glVertexAttribPointer(0, 3, GL30.GL_FLOAT, false, 3 * 4, 0); + Gdx.gl30.glEnableVertexAttribArray(0); + + Gdx.gl30.glDrawArrays(GL30.GL_TRIANGLES, 0, 3); + } + + @Override + public void dispose() { + } + + @Override + public void resize(final int width, final int height) { + final int side = Math.min(width, height); + Gdx.gl30.glViewport((width - side) / 2, (height - side) / 2, side, side); + + } + + public static final String vsSimple = "\r\n" + // + "#version 450 core\r\n" + // + " layout(location = 0) in vec3 aPos;\r\n" + // + " void main() {\r\n" + // + " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\r\n" + // + " }\r\n"; + + public static final String fsSimple = "\r\n" + // + "#version 450 core\r\n" + // + " out vec4 FragColor;\r\n" + // + " void main() {\r\n" + // + " FragColor = vec4(0.2f, 1.0f, 0.2f, 1.0f);\r\n" + // + " }\r\n"; + private int shaderProgram; + + private ByteBuffer vertexByteBuffer; + +} diff --git a/core/src/com/etheller/warsmash/WarsmashTestGameAttributes.java b/core/src/com/etheller/warsmash/WarsmashTestGameAttributes.java new file mode 100644 index 0000000..36e4fa9 --- /dev/null +++ b/core/src/com/etheller/warsmash/WarsmashTestGameAttributes.java @@ -0,0 +1,163 @@ +package com.etheller.warsmash; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; + +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; + +public class WarsmashTestGameAttributes extends ApplicationAdapter { + private int arrayBuffer; + private int elementBuffer; + private int VAO; + + @Override + public void create() { + Gdx.gl.glClearColor(0.5f, 0.5f, 0.5f, 1.0f); // colour to use when clearing + + final ByteBuffer tempByteBuffer = ByteBuffer.allocateDirect(4); + tempByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final IntBuffer temp = tempByteBuffer.asIntBuffer(); + + Gdx.gl30.glGenVertexArrays(1, temp); + this.VAO = temp.get(0); + + Gdx.gl30.glBindVertexArray(this.VAO); + + this.shaderProgram = new ShaderProgram(vsSimple, fsSimple); + if (!this.shaderProgram.isCompiled()) { + throw new IllegalStateException(this.shaderProgram.getLog()); + } + + this.arrayBuffer = Gdx.gl.glGenBuffer(); + this.elementBuffer = Gdx.gl.glGenBuffer(); + System.out.println("arrayBuffer: " + this.arrayBuffer + ", elementBuffer: " + this.elementBuffer); + + Gdx.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.arrayBuffer); + + Gdx.gl.glBindBuffer(GL20.GL_ELEMENT_ARRAY_BUFFER, this.elementBuffer); + + final ByteBuffer vertexByteBuffer = ByteBuffer.allocateDirect(4 * 9); + vertexByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + this.vertexBuffer = vertexByteBuffer.asFloatBuffer(); + + this.vertexBuffer.put(0, -1f); + this.vertexBuffer.put(1, -1f); + this.vertexBuffer.put(2, 0); + this.vertexBuffer.put(3, 1f); + this.vertexBuffer.put(4, -1f); + this.vertexBuffer.put(5, 0); + this.vertexBuffer.put(6, 0f); + this.vertexBuffer.put(7, 1f); + this.vertexBuffer.put(8, 0); + + Gdx.gl.glBufferData(GL20.GL_ARRAY_BUFFER, ((9 * 4) * 2) + 3, null, GL20.GL_STATIC_DRAW); + Gdx.gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, 0, 9 * 4, this.vertexBuffer); + + final ByteBuffer vertex2ByteBuffer = ByteBuffer.allocateDirect(4 * 9); + vertex2ByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final FloatBuffer vertexBuffer2 = vertex2ByteBuffer.asFloatBuffer(); + + vertexBuffer2.put(0, -1f); + vertexBuffer2.put(1, -1f); + vertexBuffer2.put(2, 0); + vertexBuffer2.put(3, 1f); + vertexBuffer2.put(4, -1f); + vertexBuffer2.put(5, 0); + vertexBuffer2.put(6, 0f); + vertexBuffer2.put(7, 1f); + vertexBuffer2.put(8, 0); + + Gdx.gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, 9 * 4, 9 * 4, vertexBuffer2); + + final ByteBuffer skinByteBuffer = ByteBuffer.allocateDirect(3); + skinByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + skinByteBuffer.put((byte) 34); + skinByteBuffer.put((byte) 35); + skinByteBuffer.put((byte) 36); + skinByteBuffer.clear(); + Gdx.gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, 9 * 4 * 2, 3, skinByteBuffer); + +// this.shaderProgram.setVertexAttribute("a_position", 3, GL20.GL_FLOAT, false, 0, 0); +// this.shaderProgram.enableVertexAttribute("a_position"); +// this.shaderProgram.setVertexAttribute("a_position2", 3, GL20.GL_FLOAT, false, 0, 4 * 9); +// this.shaderProgram.enableVertexAttribute("a_position2"); +// this.shaderProgram.setVertexAttribute("a_boneNumber", 1, GL20.GL_UNSIGNED_BYTE, false, 1, 4 * 9 * 2); +// this.shaderProgram.enableVertexAttribute("a_boneNumber"); + + final ByteBuffer faceByteBuffer = ByteBuffer.allocateDirect(6); + faceByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + this.faceBuffer = faceByteBuffer.asShortBuffer(); + + this.faceBuffer.put(0, (short) 0); + this.faceBuffer.put(1, (short) 1); + this.faceBuffer.put(2, (short) 2); + + Gdx.gl.glBufferData(GL20.GL_ELEMENT_ARRAY_BUFFER, 3 * 2, null, GL20.GL_STATIC_DRAW); + + final int glGetError = Gdx.gl.glGetError(); + System.out.println(glGetError); + } + + @Override + public void render() { + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT); + +// Gdx.gl30.glBindVertexArray(this.VAO); + this.shaderProgram.begin(); + + Gdx.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.arrayBuffer); + this.shaderProgram.setVertexAttribute("a_position", 3, GL20.GL_FLOAT, false, 0, 0); + this.shaderProgram.enableVertexAttribute("a_position"); + this.shaderProgram.setVertexAttribute("a_position2", 3, GL20.GL_FLOAT, false, 0, 4 * 9); + this.shaderProgram.enableVertexAttribute("a_position2"); + this.shaderProgram.setVertexAttribute("a_boneNumber", 1, GL20.GL_UNSIGNED_BYTE, false, 1, 4 * 9 * 2); + this.shaderProgram.enableVertexAttribute("a_boneNumber"); + Gdx.gl.glBindBuffer(GL20.GL_ELEMENT_ARRAY_BUFFER, this.elementBuffer); + Gdx.gl.glBufferSubData(GL20.GL_ELEMENT_ARRAY_BUFFER, 0, 3 * 2, this.faceBuffer); + Gdx.gl.glDrawElements(GL20.GL_TRIANGLES, 3, GL20.GL_UNSIGNED_SHORT, 0); +// Gdx.gl.glDrawArrays(GL20.GL_TRIANGLES, 0, 3); + this.shaderProgram.end(); + } + + @Override + public void dispose() { + } + + @Override + public void resize(final int width, final int height) { + } + + public static final String vsSimple = "\r\n" + // + " attribute vec3 a_position;\r\n" + // + " attribute vec3 a_position2;\r\n" + // + " attribute float a_boneNumber;\r\n" + // + " varying float fragNumber;\r\n" + // + " void main() {\r\n" + // + " gl_Position = vec4(a_position2.x, a_position2.y, a_position2.z, 1.0);\r\n" + // + " fragNumber = a_boneNumber;\r\n" + // + " }\r\n"; + + public static final String fsSimple = "\r\n" + // + " varying float fragNumber;\r\n" + // + " void main() {\r\n" + // + " if( fragNumber > 35.5 ) {\r\n" + // + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\r\n" + // + " } else if( fragNumber > 34.5 ) {\r\n" + // + " gl_FragColor = vec4(0.0, 1.0, 1.0, 1.0);\r\n" + // + " } else if( fragNumber > 33.5 ) {\r\n" + // + " gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);\r\n" + // + " } else {\r\n" + // + " gl_FragColor = vec4(fragNumber*100.0, fragNumber, fragNumber, 1.0);\r\n" + // + " }\r\n" + // + " }\r\n"; + private ShaderProgram shaderProgram; + private FloatBuffer vertexBuffer; + private ShortBuffer faceBuffer; + +} diff --git a/core/src/com/etheller/warsmash/WarsmashTestGameAttributes2.java b/core/src/com/etheller/warsmash/WarsmashTestGameAttributes2.java new file mode 100644 index 0000000..bc65a6c --- /dev/null +++ b/core/src/com/etheller/warsmash/WarsmashTestGameAttributes2.java @@ -0,0 +1,213 @@ +package com.etheller.warsmash; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; +import java.util.Arrays; + +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; +import com.etheller.warsmash.datasources.CompoundDataSourceDescriptor; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.datasources.DataSourceDescriptor; +import com.etheller.warsmash.datasources.FolderDataSourceDescriptor; +import com.hiveworkshop.rms.parsers.mdlx.MdlxGeoset; +import com.hiveworkshop.rms.parsers.mdlx.MdlxModel; + +public class WarsmashTestGameAttributes2 extends ApplicationAdapter { + private int arrayBuffer; + private int elementBuffer; + private int VAO; + private DataSource codebase; + + @Override + public void create() { + final FolderDataSourceDescriptor war3mpq = new FolderDataSourceDescriptor( + "D:\\NEEDS_ORGANIZING\\MPQBuild\\War3.mpq\\war3.mpq"); + final FolderDataSourceDescriptor testingFolder = new FolderDataSourceDescriptor( + "D:\\NEEDS_ORGANIZING\\MPQBuild\\Test"); + this.codebase = new CompoundDataSourceDescriptor(Arrays.asList(war3mpq, testingFolder)) + .createDataSource(); + + final MdlxModel model; + try { + model = new MdlxModel(this.codebase.read("Buildings\\Other\\TempArtB\\TempArtB.mdx")); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + + Gdx.gl.glClearColor(0.5f, 0.5f, 0.5f, 1.0f); // colour to use when clearing + + final ByteBuffer tempByteBuffer = ByteBuffer.allocateDirect(4); + tempByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final IntBuffer temp = tempByteBuffer.asIntBuffer(); + + Gdx.gl30.glGenVertexArrays(1, temp); + this.VAO = temp.get(0); + + Gdx.gl30.glBindVertexArray(this.VAO); + + this.shaderProgram = new ShaderProgram(vsSimple, fsSimple); + if (!this.shaderProgram.isCompiled()) { + throw new IllegalStateException(this.shaderProgram.getLog()); + } + + this.arrayBuffer = Gdx.gl.glGenBuffer(); + this.elementBuffer = Gdx.gl.glGenBuffer(); + System.out.println("arrayBuffer: " + this.arrayBuffer + ", elementBuffer: " + this.elementBuffer); + + Gdx.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.arrayBuffer); + + Gdx.gl.glBindBuffer(GL20.GL_ELEMENT_ARRAY_BUFFER, this.elementBuffer); + + final MdlxGeoset geoset0 = model.getGeosets().get(0); + final float[] vertices = geoset0.getVertices(); + final ByteBuffer vertexByteBuffer = ByteBuffer.allocateDirect(4 * 9); + vertexByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + this.vertexBuffer = vertexByteBuffer.asFloatBuffer(); + + this.vertexBuffer.put(0, -1f); + this.vertexBuffer.put(1, -1f); + this.vertexBuffer.put(2, 0); + this.vertexBuffer.put(3, 1f); + this.vertexBuffer.put(4, -1f); + this.vertexBuffer.put(5, 0); + this.vertexBuffer.put(6, 0f); + this.vertexBuffer.put(7, 1f); + this.vertexBuffer.put(8, 0); + + Gdx.gl.glBufferData(GL20.GL_ARRAY_BUFFER, ((9 * 4) * 2) + 3, null, GL20.GL_STATIC_DRAW); + Gdx.gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, 0, 9 * 4, this.vertexBuffer); + + final ByteBuffer vertex2ByteBuffer = ByteBuffer.allocateDirect(4 * 9); + vertex2ByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final FloatBuffer vertexBuffer2 = vertex2ByteBuffer.asFloatBuffer(); + + vertexBuffer2.put(0, -1f); + vertexBuffer2.put(1, -1f); + vertexBuffer2.put(2, 0); + vertexBuffer2.put(3, 1f); + vertexBuffer2.put(4, -1f); + vertexBuffer2.put(5, 0); + vertexBuffer2.put(6, 0f); + vertexBuffer2.put(7, 1f); + vertexBuffer2.put(8, 0); + + Gdx.gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, 9 * 4, 9 * 4, vertexBuffer2); + + final ByteBuffer skinByteBuffer = ByteBuffer.allocateDirect(3); + skinByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + skinByteBuffer.put((byte) 34); + skinByteBuffer.put((byte) 35); + skinByteBuffer.put((byte) 36); + skinByteBuffer.clear(); + Gdx.gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, 9 * 4 * 2, 3, skinByteBuffer); + +// this.shaderProgram.setVertexAttribute("a_position", 3, GL20.GL_FLOAT, false, 0, 0); +// this.shaderProgram.enableVertexAttribute("a_position"); +// this.shaderProgram.setVertexAttribute("a_position2", 3, GL20.GL_FLOAT, false, 0, 4 * 9); +// this.shaderProgram.enableVertexAttribute("a_position2"); +// this.shaderProgram.setVertexAttribute("a_boneNumber", 1, GL20.GL_UNSIGNED_BYTE, false, 1, 4 * 9 * 2); +// this.shaderProgram.enableVertexAttribute("a_boneNumber"); + + final ByteBuffer faceByteBuffer = ByteBuffer.allocateDirect(6); + faceByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + this.faceBuffer = faceByteBuffer.asShortBuffer(); + + this.faceBuffer.put(0, (short) 0); + this.faceBuffer.put(1, (short) 1); + this.faceBuffer.put(2, (short) 2); + + Gdx.gl.glBufferData(GL20.GL_ELEMENT_ARRAY_BUFFER, 3 * 2, null, GL20.GL_STATIC_DRAW); + + final int glGetError = Gdx.gl.glGetError(); + System.out.println(glGetError); + } + + @Override + public void render() { + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT); + +// Gdx.gl30.glBindVertexArray(this.VAO); + this.shaderProgram.begin(); + + Gdx.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.arrayBuffer); + this.shaderProgram.setVertexAttribute("a_position", 3, GL20.GL_FLOAT, false, 0, 0); + this.shaderProgram.enableVertexAttribute("a_position"); + this.shaderProgram.setVertexAttribute("a_position2", 3, GL20.GL_FLOAT, false, 0, 4 * 9); + this.shaderProgram.enableVertexAttribute("a_position2"); + this.shaderProgram.setVertexAttribute("a_boneNumber", 1, GL20.GL_UNSIGNED_BYTE, false, 1, 4 * 9 * 2); + this.shaderProgram.enableVertexAttribute("a_boneNumber"); + Gdx.gl.glBindBuffer(GL20.GL_ELEMENT_ARRAY_BUFFER, this.elementBuffer); + Gdx.gl.glBufferSubData(GL20.GL_ELEMENT_ARRAY_BUFFER, 0, 3 * 2, this.faceBuffer); + Gdx.gl.glDrawElements(GL20.GL_TRIANGLES, 3, GL20.GL_UNSIGNED_SHORT, 0); +// Gdx.gl.glDrawArrays(GL20.GL_TRIANGLES, 0, 3); + this.shaderProgram.end(); + } + + @Override + public void dispose() { + } + + @Override + public void resize(final int width, final int height) { + } + + public static final String vsSimple = "\r\n" + // + " attribute vec3 a_position;\r\n" + // + " attribute vec3 a_position2;\r\n" + // + " attribute float a_boneNumber;\r\n" + // + " varying float fragNumber;\r\n" + // + " void main() {\r\n" + // + " gl_Position = vec4(a_position2.x, a_position2.y, a_position2.z, 1.0);\r\n" + // + " fragNumber = a_boneNumber;\r\n" + // + " }\r\n"; + + public static final String fsSimple = "\r\n" + // + " varying float fragNumber;\r\n" + // + " void main() {\r\n" + // + " if( fragNumber > 35.5 ) {\r\n" + // + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\r\n" + // + " } else if( fragNumber > 34.5 ) {\r\n" + // + " gl_FragColor = vec4(0.0, 1.0, 1.0, 1.0);\r\n" + // + " } else if( fragNumber > 33.5 ) {\r\n" + // + " gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);\r\n" + // + " } else {\r\n" + // + " gl_FragColor = vec4(fragNumber*100.0, fragNumber, fragNumber, 1.0);\r\n" + // + " }\r\n" + // + " }\r\n"; + private ShaderProgram shaderProgram; + private FloatBuffer vertexBuffer; + private ShortBuffer faceBuffer; + + private static ShortBuffer wrapFaces(final int[] faces) { + final ShortBuffer wrapper = ByteBuffer.allocateDirect(faces.length * 2).order(ByteOrder.nativeOrder()) + .asShortBuffer(); + for (final int face : faces) { + wrapper.put((short) face); + } + wrapper.clear(); + return wrapper; + } + + private static ByteBuffer wrap(final byte[] skin) { + final ByteBuffer wrapper = ByteBuffer.allocateDirect(skin.length).order(ByteOrder.nativeOrder()); + wrapper.put(skin); + wrapper.clear(); + return wrapper; + } + + private static FloatBuffer wrap(final float[] positions) { + final FloatBuffer wrapper = ByteBuffer.allocateDirect(positions.length * 4).order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + wrapper.put(positions); + wrapper.clear(); + return wrapper; + } +} diff --git a/core/src/com/etheller/warsmash/WarsmashTestGameTextureBuffer.java b/core/src/com/etheller/warsmash/WarsmashTestGameTextureBuffer.java new file mode 100644 index 0000000..c0f7930 --- /dev/null +++ b/core/src/com/etheller/warsmash/WarsmashTestGameTextureBuffer.java @@ -0,0 +1,244 @@ +package com.etheller.warsmash; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; + +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.GL30; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; +import com.etheller.warsmash.viewer5.Shaders; + +public class WarsmashTestGameTextureBuffer extends ApplicationAdapter { + private int arrayBuffer; + private int elementBuffer; + private int VAO; + private ShaderProgram shaderProgram; + private FloatBuffer vertexBuffer; + private ShortBuffer faceBuffer; + + @Override + public void create() { +// ShaderProgram.pedantic = false; + Gdx.gl.glClearColor(0.5f, 0.5f, 0.5f, 1.0f); // colour to use when clearing + + final ByteBuffer tempByteBuffer = ByteBuffer.allocateDirect(4); + tempByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final IntBuffer temp = tempByteBuffer.asIntBuffer(); + + Gdx.gl30.glGenVertexArrays(1, temp); + this.VAO = temp.get(0); + + Gdx.gl30.glBindVertexArray(this.VAO); + + System.out.println(vsSimple); + this.shaderProgram = new ShaderProgram(vsSimple, fsSimple); + if (!this.shaderProgram.isCompiled()) { + throw new IllegalStateException(this.shaderProgram.getLog()); + } + + this.arrayBuffer = Gdx.gl.glGenBuffer(); + this.elementBuffer = Gdx.gl.glGenBuffer(); + System.out.println("arrayBuffer: " + this.arrayBuffer + ", elementBuffer: " + this.elementBuffer); + + Gdx.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.arrayBuffer); + + Gdx.gl.glBindBuffer(GL20.GL_ELEMENT_ARRAY_BUFFER, this.elementBuffer); + + final ByteBuffer vertexByteBuffer = ByteBuffer.allocateDirect(4 * 9); + vertexByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + this.vertexBuffer = vertexByteBuffer.asFloatBuffer(); + + this.vertexBuffer.put(0, -1f); + this.vertexBuffer.put(1, -1f); + this.vertexBuffer.put(2, 0); + this.vertexBuffer.put(3, 1f); + this.vertexBuffer.put(4, -1f); + this.vertexBuffer.put(5, 0); + this.vertexBuffer.put(6, 0f); + this.vertexBuffer.put(7, 1f); + this.vertexBuffer.put(8, 0); + + Gdx.gl.glBufferData(GL20.GL_ARRAY_BUFFER, ((9 * 4) * 2) + 3, null, GL20.GL_STATIC_DRAW); + Gdx.gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, 0, 9 * 4, this.vertexBuffer); + + final ByteBuffer vertex2ByteBuffer = ByteBuffer.allocateDirect(4 * 9); + vertex2ByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final FloatBuffer vertexBuffer2 = vertex2ByteBuffer.asFloatBuffer(); + + vertexBuffer2.put(0, -1f); + vertexBuffer2.put(1, -1f); + vertexBuffer2.put(2, 0); + vertexBuffer2.put(3, 1f); + vertexBuffer2.put(4, -1f); + vertexBuffer2.put(5, 0); + vertexBuffer2.put(6, 0f); + vertexBuffer2.put(7, 1f); + vertexBuffer2.put(8, 0); + + Gdx.gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, 9 * 4, 9 * 4, vertexBuffer2); + + final ByteBuffer skinByteBuffer = ByteBuffer.allocateDirect(3); + skinByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + skinByteBuffer.put((byte) 34); + skinByteBuffer.put((byte) 35); + skinByteBuffer.put((byte) 36); + skinByteBuffer.clear(); + Gdx.gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, 9 * 4 * 2, 3, skinByteBuffer); + +// this.shaderProgram.setVertexAttribute("a_position", 3, GL20.GL_FLOAT, false, 0, 0); +// this.shaderProgram.enableVertexAttribute("a_position"); +// this.shaderProgram.setVertexAttribute("a_position2", 3, GL20.GL_FLOAT, false, 0, 4 * 9); +// this.shaderProgram.enableVertexAttribute("a_position2"); +// this.shaderProgram.setVertexAttribute("a_boneNumber", 1, GL20.GL_UNSIGNED_BYTE, false, 1, 4 * 9 * 2); +// this.shaderProgram.enableVertexAttribute("a_boneNumber"); +// this.shaderProgram.setUniformi("u_boneMap", 15); + + final ByteBuffer faceByteBuffer = ByteBuffer.allocateDirect(6); + faceByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + this.faceBuffer = faceByteBuffer.asShortBuffer(); + + this.faceBuffer.put(0, (short) 0); + this.faceBuffer.put(1, (short) 1); + this.faceBuffer.put(2, (short) 2); + + Gdx.gl.glBufferData(GL20.GL_ELEMENT_ARRAY_BUFFER, 3 * 2, null, GL20.GL_STATIC_DRAW); + + final int glGetError = Gdx.gl.glGetError(); + System.out.println(glGetError); + + final ByteBuffer vertex3ByteBuffer = ByteBuffer.allocateDirect(4 * 16); + vertex3ByteBuffer.order(ByteOrder.nativeOrder()); + this.vertexTextureBuffer3 = vertex3ByteBuffer.asFloatBuffer(); + + this.vertexTextureBuffer3.put(0, 0.5f); + this.vertexTextureBuffer3.put(1, 0); + this.vertexTextureBuffer3.put(2, 0); + this.vertexTextureBuffer3.put(3, 0); + this.vertexTextureBuffer3.put(4, 0); + this.vertexTextureBuffer3.put(5, 1); + this.vertexTextureBuffer3.put(6, 0); + this.vertexTextureBuffer3.put(7, 0); + this.vertexTextureBuffer3.put(8, 0); + this.vertexTextureBuffer3.put(9, 0); + this.vertexTextureBuffer3.put(10, 1); + this.vertexTextureBuffer3.put(11, 0); + this.vertexTextureBuffer3.put(12, 0.0f); + this.vertexTextureBuffer3.put(13, 0.0f); + this.vertexTextureBuffer3.put(14, 0); + this.vertexTextureBuffer3.put(15, 1); + +// this.vertexTextureBuffer = Gdx.gl.glGenBuffer(); + this.vertexTexture = Gdx.gl.glGenTexture(); + + Gdx.gl.glActiveTexture(GL20.GL_TEXTURE15); +// Gdx.gl.glBindBuffer(GL20.GL_TEXTURE_2D, this.vertexTextureBuffer); +// Gdx.gl.glBindBuffer(GL20.GL_TEXTURE_2D, 0); + + Gdx.gl.glBindTexture(GL20.GL_TEXTURE_2D, this.vertexTexture); + Gdx.gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_S, GL20.GL_CLAMP_TO_EDGE); + Gdx.gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_T, GL20.GL_CLAMP_TO_EDGE); + Gdx.gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MIN_FILTER, GL20.GL_NEAREST); + Gdx.gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MAG_FILTER, GL20.GL_NEAREST); + Gdx.gl.glTexImage2D(GL20.GL_TEXTURE_2D, 0, GL30.GL_RGBA, 4, 1, 0, GL30.GL_RGBA, GL20.GL_FLOAT, + this.vertexTextureBuffer3); + } + + @Override + public void render() { + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT); + +// Gdx.gl30.glBindVertexArray(this.VAO); + this.shaderProgram.begin(); + + Gdx.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.arrayBuffer); + this.shaderProgram.setVertexAttribute("a_position", 3, GL20.GL_FLOAT, false, 0, 0); + this.shaderProgram.enableVertexAttribute("a_position"); + this.shaderProgram.setVertexAttribute("a_position2", 3, GL20.GL_FLOAT, false, 0, 4 * 9); + this.shaderProgram.enableVertexAttribute("a_position2"); + this.shaderProgram.setVertexAttribute("a_boneNumber", 1, GL20.GL_UNSIGNED_BYTE, false, 1, 4 * 9 * 2); + this.shaderProgram.enableVertexAttribute("a_boneNumber"); + + Gdx.gl.glActiveTexture(GL20.GL_TEXTURE15); + Gdx.gl.glBindTexture(GL20.GL_TEXTURE_2D, this.vertexTexture); + Gdx.gl.glTexSubImage2D(GL20.GL_TEXTURE_2D, 0, 0, 0, 4, 1, GL30.GL_RGBA, GL20.GL_FLOAT, + this.vertexTextureBuffer3); + this.shaderProgram.setUniformi("u_boneMap", 15); + this.shaderProgram.setUniformf("u_vectorSize", 1f / 4); + this.shaderProgram.setUniformf("u_rowSize", 1); + + Gdx.gl.glBindBuffer(GL20.GL_ELEMENT_ARRAY_BUFFER, this.elementBuffer); + Gdx.gl.glBufferSubData(GL20.GL_ELEMENT_ARRAY_BUFFER, 0, 3 * 2, this.faceBuffer); + Gdx.gl.glDrawElements(GL20.GL_TRIANGLES, 3, GL20.GL_UNSIGNED_SHORT, 0); +// Gdx.gl.glDrawArrays(GL20.GL_TRIANGLES, 0, 3); + this.shaderProgram.end(); + } + + @Override + public void dispose() { + } + + @Override + public void resize(final int width, final int height) { + } + + public static final String boneTexture = ""// + + " uniform samplerBuffer u_boneMap;\r\n" + // + // " uniform uint u_vectorSize;\r\n" + // + " uniform uint u_rowSize;\r\n" + // + " mat4 fetchMatrix(uint column, uint row) {\r\n" + // + // " column *= u_vectorSize * 4.0;\r\n" + // + // " row *= u_rowSize;\r\n" + // + " // Add in half texel to sample in the middle of the texel.\r\n" + // + " // Otherwise, since the sample is directly on the boundry, small floating point errors can cause the sample to get the wrong pixel.\r\n" + + // + " // This is mostly noticable with NPOT textures, which the bone maps are.\r\n" + // + // " column += 0.5 * u_vectorSize;\r\n" + // + // " row += 0.5 * u_rowSize;\r\n" + // + " return mat4(texelFetch(u_boneMap, row * u_rowSize + column * 4),\r\n" + // + " texelFetch(u_boneMap, row * u_rowSize + column * 4 + 1),\r\n" + // + " texelFetch(u_boneMap, row * u_rowSize + column * 4 + 2),\r\n" + // + " texelFetch(u_boneMap, row * u_rowSize + column * 4 + 3);\r\n" + // + " }"; + + public static final String vsSimple = "\r\n" + // + "\r\n" + // + " attribute vec3 a_position;\r\n" + // + " attribute vec3 a_position2;\r\n" + // + " attribute float a_boneNumber;\r\n" + // + " varying float fragNumber;\r\n" + // + Shaders.boneTexture + "\r\n" + // + " void main() {\r\n" + // + " mat4 bone = fetchMatrix(0.0, 0.0);\r\n" + // + " if( a_boneNumber <= 34.5 ) {\r\n" + // + " gl_Position = vec4(a_position2.x * bone[0][0], a_position2.y, a_position2.z, 1.0);\r\n" + // + " } else {\r\n" + // + " gl_Position = vec4(a_position2.x, a_position2.y, a_position2.z, 1.0);\r\n" + // + " }\r\n" + // + " fragNumber = a_boneNumber;\r\n" + // + " }\r\n"; + + public static final String fsSimple = "\r\n" + // + " varying float fragNumber;\r\n" + // + Shaders.boneTexture + "\r\n" + // + " void main() {\r\n" + // + " mat4 bone = fetchMatrix(0.0, 0.0);\r\n" + // + " if( fragNumber > 35.5 ) {\r\n" + // + " gl_FragColor = bone[0];//vec4(1.0, 0.0, 0.0, 1.0);\r\n" + // + " } else if( fragNumber > 34.5 ) {\r\n" + // + " gl_FragColor = bone[1];//vec4(0.0, 1.0, 1.0, 1.0);\r\n" + // + " } else if( fragNumber > 33.5 ) {\r\n" + // + " gl_FragColor = bone[2];//vec4(1.0, 0.0, 1.0, 1.0);\r\n" + // + " } else {\r\n" + // + " gl_FragColor = vec4(fragNumber*100.0, fragNumber, fragNumber, 1.0);\r\n" + // + " }\r\n" + // + " }\r\n"; + private int vertexTexture; + private int vertexTextureBuffer; + private FloatBuffer vertexTextureBuffer3; + +} diff --git a/core/src/com/etheller/warsmash/WarsmashTestGameTextureBuffer2.java b/core/src/com/etheller/warsmash/WarsmashTestGameTextureBuffer2.java new file mode 100644 index 0000000..5a892c6 --- /dev/null +++ b/core/src/com/etheller/warsmash/WarsmashTestGameTextureBuffer2.java @@ -0,0 +1,203 @@ +package com.etheller.warsmash; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; + +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; +import com.etheller.warsmash.viewer5.Shaders; +import com.etheller.warsmash.viewer5.gl.DataTexture; + +public class WarsmashTestGameTextureBuffer2 extends ApplicationAdapter { + private int arrayBuffer; + private int elementBuffer; + private int VAO; + + @Override + public void create() { +// ShaderProgram.pedantic = false; + Gdx.gl.glClearColor(0.5f, 0.5f, 0.5f, 1.0f); // colour to use when clearing + + final ByteBuffer tempByteBuffer = ByteBuffer.allocateDirect(4); + tempByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final IntBuffer temp = tempByteBuffer.asIntBuffer(); + + Gdx.gl30.glGenVertexArrays(1, temp); + this.VAO = temp.get(0); + + Gdx.gl30.glBindVertexArray(this.VAO); + + System.out.println(vsSimple); + this.shaderProgram = new ShaderProgram(vsSimple, fsSimple); + if (!this.shaderProgram.isCompiled()) { + throw new IllegalStateException(this.shaderProgram.getLog()); + } + + this.arrayBuffer = Gdx.gl.glGenBuffer(); + this.elementBuffer = Gdx.gl.glGenBuffer(); + System.out.println("arrayBuffer: " + this.arrayBuffer + ", elementBuffer: " + this.elementBuffer); + + Gdx.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.arrayBuffer); + + Gdx.gl.glBindBuffer(GL20.GL_ELEMENT_ARRAY_BUFFER, this.elementBuffer); + + final ByteBuffer vertexByteBuffer = ByteBuffer.allocateDirect(4 * 9); + vertexByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + this.vertexBuffer = vertexByteBuffer.asFloatBuffer(); + + this.vertexBuffer.put(0, -1f); + this.vertexBuffer.put(1, -1f); + this.vertexBuffer.put(2, 0); + this.vertexBuffer.put(3, 1f); + this.vertexBuffer.put(4, -1f); + this.vertexBuffer.put(5, 0); + this.vertexBuffer.put(6, 0f); + this.vertexBuffer.put(7, 1f); + this.vertexBuffer.put(8, 0); + + Gdx.gl.glBufferData(GL20.GL_ARRAY_BUFFER, ((9 * 4) * 2) + 3, null, GL20.GL_STATIC_DRAW); + Gdx.gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, 0, 9 * 4, this.vertexBuffer); + + final ByteBuffer vertex2ByteBuffer = ByteBuffer.allocateDirect(4 * 9); + vertex2ByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final FloatBuffer vertexBuffer2 = vertex2ByteBuffer.asFloatBuffer(); + + vertexBuffer2.put(0, -1f); + vertexBuffer2.put(1, -1f); + vertexBuffer2.put(2, 0); + vertexBuffer2.put(3, 1f); + vertexBuffer2.put(4, -1f); + vertexBuffer2.put(5, 0); + vertexBuffer2.put(6, 0f); + vertexBuffer2.put(7, 1f); + vertexBuffer2.put(8, 0); + + Gdx.gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, 9 * 4, 9 * 4, vertexBuffer2); + + final ByteBuffer skinByteBuffer = ByteBuffer.allocateDirect(3); + skinByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + skinByteBuffer.put((byte) 34); + skinByteBuffer.put((byte) 35); + skinByteBuffer.put((byte) 36); + skinByteBuffer.clear(); + Gdx.gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, 9 * 4 * 2, 3, skinByteBuffer); + +// this.shaderProgram.setVertexAttribute("a_position", 3, GL20.GL_FLOAT, false, 0, 0); +// this.shaderProgram.enableVertexAttribute("a_position"); +// this.shaderProgram.setVertexAttribute("a_position2", 3, GL20.GL_FLOAT, false, 0, 4 * 9); +// this.shaderProgram.enableVertexAttribute("a_position2"); +// this.shaderProgram.setVertexAttribute("a_boneNumber", 1, GL20.GL_UNSIGNED_BYTE, false, 1, 4 * 9 * 2); +// this.shaderProgram.enableVertexAttribute("a_boneNumber"); +// this.shaderProgram.setUniformi("u_boneMap", 15); + + final ByteBuffer faceByteBuffer = ByteBuffer.allocateDirect(6); + faceByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + this.faceBuffer = faceByteBuffer.asShortBuffer(); + + this.faceBuffer.put(0, (short) 0); + this.faceBuffer.put(1, (short) 1); + this.faceBuffer.put(2, (short) 2); + + Gdx.gl.glBufferData(GL20.GL_ELEMENT_ARRAY_BUFFER, 3 * 2, null, GL20.GL_STATIC_DRAW); + + final int glGetError = Gdx.gl.glGetError(); + System.out.println(glGetError); + + final ByteBuffer vertex3ByteBuffer = ByteBuffer.allocateDirect(4 * 16); + vertex3ByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + final FloatBuffer vertexBuffer3 = vertex3ByteBuffer.asFloatBuffer(); + + vertexBuffer3.put(0, 1); + vertexBuffer3.put(1, 0); + vertexBuffer3.put(2, 0); + vertexBuffer3.put(3, 0); + vertexBuffer3.put(4, 0); + vertexBuffer3.put(5, 1); + vertexBuffer3.put(6, 0); + vertexBuffer3.put(7, 0); + vertexBuffer3.put(8, 0); + vertexBuffer3.put(9, 0); + vertexBuffer3.put(10, 1); + vertexBuffer3.put(11, 0); + vertexBuffer3.put(12, 0.0f); + vertexBuffer3.put(13, 0.0f); + vertexBuffer3.put(14, 0); + vertexBuffer3.put(15, 1); + + this.dataTexture = new DataTexture(Gdx.gl, 4, 4, 1); + this.dataTexture.bindAndUpdate(vertexBuffer3); + } + + @Override + public void render() { + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT); + +// Gdx.gl30.glBindVertexArray(this.VAO); + this.shaderProgram.begin(); + + this.dataTexture.bind(15); + + Gdx.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.arrayBuffer); + this.shaderProgram.setVertexAttribute("a_position", 3, GL20.GL_FLOAT, false, 0, 0); + this.shaderProgram.enableVertexAttribute("a_position"); + this.shaderProgram.setVertexAttribute("a_position2", 3, GL20.GL_FLOAT, false, 0, 4 * 9); + this.shaderProgram.enableVertexAttribute("a_position2"); + this.shaderProgram.setVertexAttribute("a_boneNumber", 1, GL20.GL_UNSIGNED_BYTE, false, 1, 4 * 9 * 2); + this.shaderProgram.enableVertexAttribute("a_boneNumber"); + + this.shaderProgram.setUniformi("u_boneMap", 15); + this.shaderProgram.setUniformf("u_vectorSize", 1f / this.dataTexture.getWidth()); + this.shaderProgram.setUniformf("u_rowSize", 1); + + Gdx.gl.glBindBuffer(GL20.GL_ELEMENT_ARRAY_BUFFER, this.elementBuffer); + Gdx.gl.glBufferSubData(GL20.GL_ELEMENT_ARRAY_BUFFER, 0, 3 * 2, this.faceBuffer); + Gdx.gl.glDrawElements(GL20.GL_TRIANGLES, 3, GL20.GL_UNSIGNED_SHORT, 0); +// Gdx.gl.glDrawArrays(GL20.GL_TRIANGLES, 0, 3); + this.shaderProgram.end(); + } + + @Override + public void dispose() { + } + + @Override + public void resize(final int width, final int height) { + } + + public static final String vsSimple = "\r\n" + // + "\r\n" + // + " attribute vec3 a_position;\r\n" + // + " attribute vec3 a_position2;\r\n" + // + " attribute float a_boneNumber;\r\n" + // + " varying float fragNumber;\r\n" + // + Shaders.boneTexture + "\r\n" + // + " void main() {\r\n" + // + " mat4 bone = fetchMatrix(0.0, 0.0);\r\n" + // + " gl_Position = bone * vec4(a_position2.x, a_position2.y, a_position2.z, 1.0);\r\n" + // + " fragNumber = a_boneNumber;\r\n" + // + " }\r\n"; + + public static final String fsSimple = "\r\n" + // + " varying float fragNumber;\r\n" + // + " void main() {\r\n" + // + " if( fragNumber > 35.5 ) {\r\n" + // + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\r\n" + // + " } else if( fragNumber > 34.5 ) {\r\n" + // + " gl_FragColor = vec4(0.0, 1.0, 1.0, 1.0);\r\n" + // + " } else if( fragNumber > 33.5 ) {\r\n" + // + " gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);\r\n" + // + " } else {\r\n" + // + " gl_FragColor = vec4(fragNumber*100.0, fragNumber, fragNumber, 1.0);\r\n" + // + " }\r\n" + // + " }\r\n"; + private ShaderProgram shaderProgram; + private FloatBuffer vertexBuffer; + private ShortBuffer faceBuffer; + private DataTexture dataTexture; + +} diff --git a/core/src/com/etheller/warsmash/WarsmashTestMyTextureGame.java b/core/src/com/etheller/warsmash/WarsmashTestMyTextureGame.java new file mode 100644 index 0000000..edc1cec --- /dev/null +++ b/core/src/com/etheller/warsmash/WarsmashTestMyTextureGame.java @@ -0,0 +1,47 @@ +package com.etheller.warsmash; + +import java.util.Arrays; + +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.etheller.warsmash.datasources.CompoundDataSourceDescriptor; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.datasources.DataSourceDescriptor; +import com.etheller.warsmash.datasources.FolderDataSourceDescriptor; +import com.etheller.warsmash.util.ImageUtils; + +public class WarsmashTestMyTextureGame extends ApplicationAdapter { + + private DataSource codebase; + private Texture texture; + private SpriteBatch batch; + + @Override + public void create() { + + final FolderDataSourceDescriptor war3mpq = new FolderDataSourceDescriptor("E:\\Backups\\Warcraft\\Data\\127"); + final FolderDataSourceDescriptor testingFolder = new FolderDataSourceDescriptor( + "D:\\NEEDS_ORGANIZING\\MPQBuild\\Test"); + final FolderDataSourceDescriptor currentFolder = new FolderDataSourceDescriptor("."); + this.codebase = new CompoundDataSourceDescriptor( + Arrays.asList(war3mpq, testingFolder, currentFolder)).createDataSource(); + + this.texture = ImageUtils.getAnyExtensionTexture(this.codebase, "Textures\\Dust3x.blp"); + Gdx.gl.glClearColor(0, 0, 0, 1); + this.batch = new SpriteBatch(); + this.batch.enableBlending(); + } + + @Override + public void render() { + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT); + + this.batch.begin(); + this.batch.draw(this.texture, 20, 20, 256, 256); + this.batch.end(); + } + +} diff --git a/core/src/com/etheller/warsmash/common/FetchDataTypeName.java b/core/src/com/etheller/warsmash/common/FetchDataTypeName.java new file mode 100644 index 0000000..1d4b35b --- /dev/null +++ b/core/src/com/etheller/warsmash/common/FetchDataTypeName.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.common; + +/** + * These will probably change the further I get from the source material I am + * copying from. + */ +public enum FetchDataTypeName { + IMAGE, + TEXT, + SLK, + ARRAY_BUFFER, + BLOB; +} diff --git a/core/src/com/etheller/warsmash/common/LoadGenericCallback.java b/core/src/com/etheller/warsmash/common/LoadGenericCallback.java new file mode 100644 index 0000000..d7c4c29 --- /dev/null +++ b/core/src/com/etheller/warsmash/common/LoadGenericCallback.java @@ -0,0 +1,7 @@ +package com.etheller.warsmash.common; + +import java.io.InputStream; + +public interface LoadGenericCallback { + Object call(InputStream data); // TODO typing +} diff --git a/core/src/com/etheller/warsmash/datasources/CascDataSource.java b/core/src/com/etheller/warsmash/datasources/CascDataSource.java new file mode 100644 index 0000000..5f92f16 --- /dev/null +++ b/core/src/com/etheller/warsmash/datasources/CascDataSource.java @@ -0,0 +1,225 @@ +package com.etheller.warsmash.datasources; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import com.hiveworkshop.blizzard.casc.io.WarcraftIIICASC; +import com.hiveworkshop.blizzard.casc.io.WarcraftIIICASC.FileSystem; +import com.hiveworkshop.json.JSONArray; +import com.hiveworkshop.json.JSONObject; +import com.hiveworkshop.json.JSONTokener; + +public class CascDataSource implements DataSource { + private final String[] prefixes; + private WarcraftIIICASC warcraftIIICASC; + private FileSystem rootFileSystem; + private List listFile; + private Map fileAliases; + + public CascDataSource(final String warcraft3InstallPath, final String[] prefixes) { + this.prefixes = prefixes; + for (int i = 0; i < (prefixes.length / 2); i++) { + final String temp = prefixes[i]; + prefixes[i] = prefixes[prefixes.length - i - 1]; + prefixes[prefixes.length - i - 1] = temp; + } + + try { + this.warcraftIIICASC = new WarcraftIIICASC(Paths.get(warcraft3InstallPath), true); + this.rootFileSystem = this.warcraftIIICASC.getRootFileSystem(); + this.listFile = this.rootFileSystem.enumerateFiles(); + this.fileAliases = new HashMap<>(); + if (this.has("filealiases.json")) { + try (InputStream stream = this.getResourceAsStream("filealiases.json")) { + stream.mark(4); + if ('\ufeff' != stream.read()) { + stream.reset(); // not the BOM marker + } + final JSONArray jsonObject = new JSONArray(new JSONTokener(stream)); + for (int i = 0; i < jsonObject.length(); i++) { + final JSONObject alias = jsonObject.getJSONObject(i); + final String src = alias.getString("src"); + final String dest = alias.getString("dest"); + this.fileAliases.put(src.toLowerCase(Locale.US).replace('/', '\\'), + dest.toLowerCase(Locale.US).replace('/', '\\')); + if ((src.toLowerCase(Locale.US).contains(".blp") + || dest.toLowerCase(Locale.US).contains(".blp")) + && (!alias.has("assetType") || "Texture".equals(alias.getString("assetType")))) { + // This case: I saw a texture that resolves in game but was failing in our code + // here, because of this entry: + // {"src":"Units/Human/WarWagon/SiegeEngine.blp", + // "dest":"Textures/Steamtank.blp", "assetType": "Texture"}, + // Our repo here checks BLP then DDS at a high-up application level thing, and + // the problem is that this entry is written using .BLP but we must be able to + // resolve .DDS when we go to look it up. The actual model is .BLP so maybe + // that's how the game does it, but my alias mapping is happening after the + // .BLP->.DDS dynamic fix, and not before. + this.fileAliases.put(src.toLowerCase(Locale.US).replace('/', '\\').replace(".blp", ".dds"), + dest.toLowerCase(Locale.US).replace('/', '\\').replace(".blp", ".dds")); + } + } + } + } + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public InputStream getResourceAsStream(String filepath) { + filepath = filepath.toLowerCase(Locale.US).replace('/', '\\').replace(':', '\\'); + final String resolvedAlias = this.fileAliases.get(filepath); + if (resolvedAlias != null) { + filepath = resolvedAlias; + } + for (final String prefix : this.prefixes) { + final String tempFilepath = prefix + "\\" + filepath; + final InputStream stream = internalGetResourceAsStream(tempFilepath); + if (stream != null) { + return stream; + } + } + return internalGetResourceAsStream(filepath); + } + + private InputStream internalGetResourceAsStream(final String tempFilepath) { + try { + if (this.rootFileSystem.isFile(tempFilepath) && this.rootFileSystem.isFileAvailable(tempFilepath)) { + final ByteBuffer buffer = this.rootFileSystem.readFileData(tempFilepath); + if (buffer.hasArray()) { + return new ByteArrayInputStream(buffer.array()); + } + final byte[] data = new byte[buffer.remaining()]; + buffer.clear(); + buffer.get(data); + return new ByteArrayInputStream(data); + } + } + catch (final IOException e) { + throw new RuntimeException("CASC parser error for: " + tempFilepath, e); + } + return null; + } + + @Override + public ByteBuffer read(String path) { + path = path.toLowerCase(Locale.US).replace('/', '\\').replace(':', '\\'); + final String resolvedAlias = this.fileAliases.get(path); + if (resolvedAlias != null) { + path = resolvedAlias; + } + for (final String prefix : this.prefixes) { + final String tempFilepath = prefix + "\\" + path; + final ByteBuffer stream = internalRead(tempFilepath); + if (stream != null) { + return stream; + } + } + return internalRead(path); + } + + private ByteBuffer internalRead(final String tempFilepath) { + try { + if (this.rootFileSystem.isFile(tempFilepath) && this.rootFileSystem.isFileAvailable(tempFilepath)) { + final ByteBuffer buffer = this.rootFileSystem.readFileData(tempFilepath); + return buffer; + } + } + catch (final IOException e) { + throw new RuntimeException("CASC parser error for: " + tempFilepath, e); + } + return null; + } + + @Override + public File getFile(String filepath) { + filepath = filepath.toLowerCase(Locale.US).replace('/', '\\').replace(':', '\\'); + final String resolvedAlias = this.fileAliases.get(filepath); + if (resolvedAlias != null) { + filepath = resolvedAlias; + } + for (final String prefix : this.prefixes) { + final String tempFilepath = prefix + "\\" + filepath; + final File file = internalGetFile(tempFilepath); + if (file != null) { + return file; + } + } + return internalGetFile(filepath); + } + + private File internalGetFile(final String tempFilepath) { + try { + if (this.rootFileSystem.isFile(tempFilepath) && this.rootFileSystem.isFileAvailable(tempFilepath)) { + final ByteBuffer buffer = this.rootFileSystem.readFileData(tempFilepath); + String tmpdir = System.getProperty("java.io.tmpdir"); + if (!tmpdir.endsWith(File.separator)) { + tmpdir += File.separator; + } + final String tempDir = tmpdir + "MatrixEaterExtract/"; + final File tempProduct = new File(tempDir + tempFilepath.replace('\\', File.separatorChar)); + tempProduct.delete(); + tempProduct.getParentFile().mkdirs(); + try (final FileChannel fileChannel = FileChannel.open(tempProduct.toPath(), StandardOpenOption.CREATE, + StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.TRUNCATE_EXISTING)) { + fileChannel.write(buffer); + } + tempProduct.deleteOnExit(); + return tempProduct; + } + } + catch (final IOException e) { + throw new RuntimeException("CASC parser error for: " + tempFilepath, e); + } + return null; + } + + @Override + public boolean has(String filepath) { + filepath = filepath.toLowerCase(Locale.US).replace('/', '\\').replace(':', '\\'); + final String resolvedAlias = this.fileAliases.get(filepath); + if (resolvedAlias != null) { + filepath = resolvedAlias; + } + for (final String prefix : this.prefixes) { + final String tempFilepath = prefix + "\\" + filepath; + try { + if (this.rootFileSystem.isFile(tempFilepath)) { + return true; + } + } + catch (final IOException e) { + throw new RuntimeException("CASC parser error for: " + tempFilepath, e); + } + } + try { + return this.rootFileSystem.isFile(filepath); + } + catch (final IOException e) { + throw new RuntimeException("CASC parser error for: " + filepath, e); + } + } + + @Override + public Collection getListfile() { + return this.listFile; + } + + @Override + public void close() throws IOException { + this.warcraftIIICASC.close(); + } + +} diff --git a/core/src/com/etheller/warsmash/datasources/CascDataSourceDescriptor.java b/core/src/com/etheller/warsmash/datasources/CascDataSourceDescriptor.java new file mode 100644 index 0000000..4c590a4 --- /dev/null +++ b/core/src/com/etheller/warsmash/datasources/CascDataSourceDescriptor.java @@ -0,0 +1,96 @@ +package com.etheller.warsmash.datasources; + +import java.util.Collections; +import java.util.List; + +public class CascDataSourceDescriptor implements DataSourceDescriptor { + /** + * Generated serial id + */ + private static final long serialVersionUID = 832549098549298820L; + private final String gameInstallPath; + private final List prefixes; + + public CascDataSourceDescriptor(final String gameInstallPath, final List prefixes) { + this.gameInstallPath = gameInstallPath; + this.prefixes = prefixes; + } + + @Override + public DataSource createDataSource() { + return new CascDataSource(this.gameInstallPath, this.prefixes.toArray(new String[this.prefixes.size()])); + } + + @Override + public String getDisplayName() { + return "CASC: " + this.gameInstallPath; + } + + public void addPrefix(final String prefix) { + this.prefixes.add(prefix); + } + + public void deletePrefix(final int index) { + this.prefixes.remove(index); + } + + public void movePrefixUp(final int index) { + if (index > 0) { + Collections.swap(this.prefixes, index, index - 1); + } + } + + public void movePrefixDown(final int index) { + if (index < (this.prefixes.size() - 1)) { + Collections.swap(this.prefixes, index, index + 1); + } + } + + public String getGameInstallPath() { + return this.gameInstallPath; + } + + public List getPrefixes() { + return this.prefixes; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + ((this.gameInstallPath == null) ? 0 : this.gameInstallPath.hashCode()); + result = (prime * result) + ((this.prefixes == null) ? 0 : this.prefixes.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final CascDataSourceDescriptor other = (CascDataSourceDescriptor) obj; + if (this.gameInstallPath == null) { + if (other.gameInstallPath != null) { + return false; + } + } + else if (!this.gameInstallPath.equals(other.gameInstallPath)) { + return false; + } + if (this.prefixes == null) { + if (other.prefixes != null) { + return false; + } + } + else if (!this.prefixes.equals(other.prefixes)) { + return false; + } + return true; + } +} diff --git a/core/src/com/etheller/warsmash/datasources/CompoundDataSource.java b/core/src/com/etheller/warsmash/datasources/CompoundDataSource.java new file mode 100644 index 0000000..c427302 --- /dev/null +++ b/core/src/com/etheller/warsmash/datasources/CompoundDataSource.java @@ -0,0 +1,153 @@ +package com.etheller.warsmash.datasources; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class CompoundDataSource implements DataSource { + private final List mpqList = new ArrayList<>(); + + public CompoundDataSource(final List dataSources) { + if (dataSources != null) { + for (final DataSource dataSource : dataSources) { + this.mpqList.add(dataSource); + } + } + } + + Map cache = new HashMap<>(); + + @Override + public File getFile(final String filepath) { + if (this.cache.containsKey(filepath)) { + return this.cache.get(filepath); + } + try { + for (int i = this.mpqList.size() - 1; i >= 0; i--) { + final DataSource mpq = this.mpqList.get(i); + final File tempProduct = mpq.getFile(filepath); + if (tempProduct != null) { + this.cache.put(filepath, tempProduct); + return tempProduct; + } + } + } + catch (final IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return null; + } + + @Override + public ByteBuffer read(final String path) throws IOException { + try { + for (int i = this.mpqList.size() - 1; i >= 0; i--) { + final DataSource mpq = this.mpqList.get(i); + final ByteBuffer buffer = mpq.read(path); + if (buffer != null) { + return buffer; + } + } + } + catch (final IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return null; + } + + @Override + public InputStream getResourceAsStream(final String filepath) { + try { + for (int i = this.mpqList.size() - 1; i >= 0; i--) { + final DataSource mpq = this.mpqList.get(i); + final InputStream resourceAsStream = mpq.getResourceAsStream(filepath); + if (resourceAsStream != null) { + return resourceAsStream; + } + } + } + catch (final IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return null; + } + + @Override + public boolean has(final String filepath) { + if (this.cache.containsKey(filepath)) { + return true; + } + for (int i = this.mpqList.size() - 1; i >= 0; i--) { + final DataSource mpq = this.mpqList.get(i); + if (mpq.has(filepath)) { + return true; + } + } + return false; + } + + public void refresh(final List dataSourceDescriptors) { + for (final DataSource dataSource : this.mpqList) { + try { + dataSource.close(); + } + catch (final NullPointerException e) { + e.printStackTrace(); + } + catch (final IOException e) { + e.printStackTrace(); + } + } + this.cache.clear(); + this.mpqList.clear(); + if (dataSourceDescriptors != null) { + for (final DataSourceDescriptor descriptor : dataSourceDescriptors) { + this.mpqList.add(descriptor.createDataSource()); + } + } + } + + public interface LoadedMPQ { + void unload(); + + boolean hasListfile(); + + boolean has(String path); + } + + public Set getMergedListfile() { + final Set listfile = new HashSet<>(); + for (final DataSource mpqGuy : this.mpqList) { + final Collection dataSourceListfile = mpqGuy.getListfile(); + if (dataSourceListfile != null) { + for (final String element : dataSourceListfile) { + listfile.add(element); + } + } + } + return listfile; + } + + @Override + public Collection getListfile() { + return getMergedListfile(); + } + + @Override + public void close() throws IOException { + for (final DataSource mpqGuy : this.mpqList) { + mpqGuy.close(); + } + } +} diff --git a/core/src/com/etheller/warsmash/datasources/CompoundDataSourceDescriptor.java b/core/src/com/etheller/warsmash/datasources/CompoundDataSourceDescriptor.java new file mode 100644 index 0000000..0c1a957 --- /dev/null +++ b/core/src/com/etheller/warsmash/datasources/CompoundDataSourceDescriptor.java @@ -0,0 +1,27 @@ +package com.etheller.warsmash.datasources; + +import java.util.ArrayList; +import java.util.List; + +public class CompoundDataSourceDescriptor implements DataSourceDescriptor { + private final List dataSourceDescriptors; + + public CompoundDataSourceDescriptor(final List dataSourceDescriptors) { + this.dataSourceDescriptors = dataSourceDescriptors; + } + + @Override + public DataSource createDataSource() { + final List dataSources = new ArrayList<>(); + for (final DataSourceDescriptor descriptor : this.dataSourceDescriptors) { + dataSources.add(descriptor.createDataSource()); + } + return new CompoundDataSource(dataSources); + } + + @Override + public String getDisplayName() { + return "CompoundDataSourceDescriptor"; + } + +} diff --git a/core/src/com/etheller/warsmash/datasources/DataSource.java b/core/src/com/etheller/warsmash/datasources/DataSource.java new file mode 100644 index 0000000..bb82b22 --- /dev/null +++ b/core/src/com/etheller/warsmash/datasources/DataSource.java @@ -0,0 +1,49 @@ +package com.etheller.warsmash.datasources; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Collection; + +public interface DataSource { + /** + * Efficiently return a stream instance that will read the data source file's + * contents directly from the data source. For example, this will read a file + * within an MPQ or CASC storage without extracting it. + * + * @param filepath + * @return + * @throws IOException + */ + InputStream getResourceAsStream(String filepath) throws IOException; + + /** + * Inefficiently copy a file from the data source onto the Hard Drive of the + * computer, and then return a java File instance pointed at the file. + * + * @param filepath + * @return + * @throws IOException + */ + File getFile(String filepath) throws IOException; + + ByteBuffer read(String path) throws IOException; + + /** + * Returns true if the data source contains a valid entry for a particular file. + * Some data sources (MPQs) may contain files for which this returns true, even + * though they cannot list the file in their Listfile. + * + * @param filepath + * @return + */ + boolean has(String filepath); + + /** + * @return a list of data source contents, or null if no list is provided + */ + Collection getListfile(); + + void close() throws IOException; +} diff --git a/core/src/com/etheller/warsmash/datasources/DataSourceDescriptor.java b/core/src/com/etheller/warsmash/datasources/DataSourceDescriptor.java new file mode 100644 index 0000000..2b4fd30 --- /dev/null +++ b/core/src/com/etheller/warsmash/datasources/DataSourceDescriptor.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.datasources; + +import java.io.Serializable; + +public interface DataSourceDescriptor extends Serializable { + DataSource createDataSource(); + + String getDisplayName(); +} diff --git a/core/src/com/etheller/warsmash/datasources/FolderDataSource.java b/core/src/com/etheller/warsmash/datasources/FolderDataSource.java new file mode 100644 index 0000000..356eb7e --- /dev/null +++ b/core/src/com/etheller/warsmash/datasources/FolderDataSource.java @@ -0,0 +1,87 @@ +package com.etheller.warsmash.datasources; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +public class FolderDataSource implements DataSource { + + private final Path folderPath; + private final Set listfile; + + public FolderDataSource(final Path folderPath) { + this.folderPath = folderPath; + this.listfile = new HashSet<>(); + try { + Files.walk(folderPath).filter(Files::isRegularFile).forEach(new Consumer() { + @Override + public void accept(final Path t) { + FolderDataSource.this.listfile.add(folderPath.relativize(t).toString()); + } + }); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public InputStream getResourceAsStream(String filepath) throws IOException { + filepath = fixFilepath(filepath); + if (!has(filepath)) { + return null; + } + return Files.newInputStream(this.folderPath.resolve(filepath), StandardOpenOption.READ); + } + + @Override + public File getFile(String filepath) throws IOException { + filepath = fixFilepath(filepath); + if (!has(filepath)) { + return null; + } + return new File(this.folderPath.toString() + File.separatorChar + filepath); + } + + @Override + public ByteBuffer read(String path) throws IOException { + path = fixFilepath(path); + if (!has(path)) { + return null; + } + return ByteBuffer.wrap(Files.readAllBytes(Paths.get(this.folderPath.toString(), path))); + } + + @Override + public boolean has(String filepath) { + filepath = fixFilepath(filepath); + if ("".equals(filepath)) { + return false; // special case for folder data source, dont do this + } + final Path resolvedPath = this.folderPath.resolve(filepath); + return Files.exists(resolvedPath) && !Files.isDirectory(resolvedPath); + } + + @Override + public Collection getListfile() { + return this.listfile; + } + + @Override + public void close() { + } + + private static String fixFilepath(final String filepath) { + return filepath.replace('\\', File.separatorChar).replace('/', File.separatorChar).replace(':', + File.separatorChar); + } +} diff --git a/core/src/com/etheller/warsmash/datasources/FolderDataSourceDescriptor.java b/core/src/com/etheller/warsmash/datasources/FolderDataSourceDescriptor.java new file mode 100644 index 0000000..174aad3 --- /dev/null +++ b/core/src/com/etheller/warsmash/datasources/FolderDataSourceDescriptor.java @@ -0,0 +1,57 @@ +package com.etheller.warsmash.datasources; + +import java.nio.file.Paths; + +public class FolderDataSourceDescriptor implements DataSourceDescriptor { + /** + * Generated serial id + */ + private static final long serialVersionUID = -476724730967709309L; + private final String folderPath; + + public FolderDataSourceDescriptor(final String folderPath) { + this.folderPath = folderPath; + } + + @Override + public DataSource createDataSource() { + return new FolderDataSource(Paths.get(this.folderPath)); + } + + @Override + public String getDisplayName() { + return "Folder: " + this.folderPath; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + ((this.folderPath == null) ? 0 : this.folderPath.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final FolderDataSourceDescriptor other = (FolderDataSourceDescriptor) obj; + if (this.folderPath == null) { + if (other.folderPath != null) { + return false; + } + } + else if (!this.folderPath.equals(other.folderPath)) { + return false; + } + return true; + } + +} diff --git a/core/src/com/etheller/warsmash/datasources/MpqDataSource.java b/core/src/com/etheller/warsmash/datasources/MpqDataSource.java new file mode 100644 index 0000000..9bdfbbd --- /dev/null +++ b/core/src/com/etheller/warsmash/datasources/MpqDataSource.java @@ -0,0 +1,166 @@ +package com.etheller.warsmash.datasources; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import mpq.ArchivedFile; +import mpq.ArchivedFileExtractor; +import mpq.ArchivedFileStream; +import mpq.HashLookup; +import mpq.MPQArchive; +import mpq.MPQException; + +public class MpqDataSource implements DataSource { + + private final MPQArchive archive; + private final SeekableByteChannel inputChannel; + private final ArchivedFileExtractor extractor = new ArchivedFileExtractor(); + + public MpqDataSource(final MPQArchive archive, final SeekableByteChannel inputChannel) { + this.archive = archive; + this.inputChannel = inputChannel; + } + + public MPQArchive getArchive() { + return this.archive; + } + + public SeekableByteChannel getInputChannel() { + return this.inputChannel; + } + + @Override + public InputStream getResourceAsStream(final String filepath) throws IOException { + ArchivedFile file = null; + try { + file = this.archive.lookupHash2(new HashLookup(filepath)); + } + catch (final MPQException exc) { + if (exc.getMessage().equals("lookup not found")) { + return null; + } + else { + throw new IOException(exc); + } + } + final ArchivedFileStream stream = new ArchivedFileStream(this.inputChannel, this.extractor, file); + final InputStream newInputStream = Channels.newInputStream(stream); + return newInputStream; + } + + @Override + public ByteBuffer read(final String path) throws IOException { + ArchivedFile file = null; + try { + file = this.archive.lookupHash2(new HashLookup(path)); + } + catch (final MPQException exc) { + if (exc.getMessage().equals("lookup not found")) { + return null; + } + else { + throw new IOException(exc); + } + } + try (final ArchivedFileStream stream = new ArchivedFileStream(this.inputChannel, this.extractor, file)) { + final long size = stream.size(); + final ByteBuffer buffer = ByteBuffer.allocate((int) size); + stream.read(buffer); + return buffer; + } + } + + @Override + public File getFile(final String filepath) throws IOException { + // TODO Auto-generated method stub + // System.out.println("getting it from the outside: " + + // filepath); + ArchivedFile file = null; + try { + file = this.archive.lookupHash2(new HashLookup(filepath)); + } + catch (final MPQException exc) { + if (exc.getMessage().equals("lookup not found")) { + return null; + } + else { + throw new IOException(exc); + } + } + final ArchivedFileStream stream = new ArchivedFileStream(this.inputChannel, this.extractor, file); + final InputStream newInputStream = Channels.newInputStream(stream); + String tmpdir = System.getProperty("java.io.tmpdir"); + if (!tmpdir.endsWith(File.separator)) { + tmpdir += File.separator; + } + final String tempDir = tmpdir + "RMSExtract/"; + final File tempProduct = new File(tempDir + filepath.replace('\\', File.separatorChar)); + tempProduct.delete(); + tempProduct.getParentFile().mkdirs(); + Files.copy(newInputStream, tempProduct.toPath()); + tempProduct.deleteOnExit(); + return tempProduct; + } + + @Override + public boolean has(final String filepath) { + try { + this.archive.lookupPath(filepath); + return true; + } + catch (final MPQException exc) { + if (exc.getMessage().equals("lookup not found")) { + return false; + } + else { + throw new RuntimeException(exc); + } + } + } + + @Override + public Collection getListfile() { + try { + final Set listfile = new HashSet<>(); + ArchivedFile listfileContents; + listfileContents = this.archive.lookupHash2(new HashLookup("(listfile)")); + final ArchivedFileStream stream = new ArchivedFileStream(this.inputChannel, this.extractor, + listfileContents); + final InputStream newInputStream = Channels.newInputStream(stream); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(newInputStream))) { + String line; + while ((line = reader.readLine()) != null) { + listfile.add(line); + } + } + catch (final IOException exc) { + throw new RuntimeException(exc); + } + return listfile; + } + catch (final MPQException exc) { + if (exc.getMessage().equals("lookup not found")) { + return null; + } + else { + throw new RuntimeException(exc); + } + } + } + + @Override + public void close() throws IOException { + this.inputChannel.close(); + } + +} diff --git a/core/src/com/etheller/warsmash/datasources/MpqDataSourceDescriptor.java b/core/src/com/etheller/warsmash/datasources/MpqDataSourceDescriptor.java new file mode 100644 index 0000000..276a666 --- /dev/null +++ b/core/src/com/etheller/warsmash/datasources/MpqDataSourceDescriptor.java @@ -0,0 +1,78 @@ +package com.etheller.warsmash.datasources; + +import java.io.IOException; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.EnumSet; + +import mpq.MPQArchive; +import mpq.MPQException; + +public class MpqDataSourceDescriptor implements DataSourceDescriptor { + /** + * Generated serial id + */ + private static final long serialVersionUID = 8424254987711783598L; + private final String mpqFilePath; + + public MpqDataSourceDescriptor(final String mpqFilePath) { + this.mpqFilePath = mpqFilePath; + } + + @Override + public DataSource createDataSource() { + try { + SeekableByteChannel sbc; + sbc = Files.newByteChannel(Paths.get(this.mpqFilePath), EnumSet.of(StandardOpenOption.READ)); + return new MpqDataSource(new MPQArchive(sbc), sbc); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + catch (final MPQException e) { + throw new RuntimeException(e); + } + } + + @Override + public String getDisplayName() { + return "MPQ Archive: " + this.mpqFilePath; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + ((this.mpqFilePath == null) ? 0 : this.mpqFilePath.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final MpqDataSourceDescriptor other = (MpqDataSourceDescriptor) obj; + if (this.mpqFilePath == null) { + if (other.mpqFilePath != null) { + return false; + } + } + else if (!this.mpqFilePath.equals(other.mpqFilePath)) { + return false; + } + return true; + } + + public String getMpqFilePath() { + return this.mpqFilePath; + } +} diff --git a/core/src/com/etheller/warsmash/datasources/SubdirDataSource.java b/core/src/com/etheller/warsmash/datasources/SubdirDataSource.java new file mode 100644 index 0000000..708d340 --- /dev/null +++ b/core/src/com/etheller/warsmash/datasources/SubdirDataSource.java @@ -0,0 +1,55 @@ +package com.etheller.warsmash.datasources; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class SubdirDataSource implements DataSource { + private final DataSource dataSource; + private final String subdir; + + public SubdirDataSource(final DataSource dataSource, final String subdir) { + this.dataSource = dataSource; + this.subdir = subdir; + } + + @Override + public File getFile(final String filepath) throws IOException { + return this.dataSource.getFile(this.subdir + filepath); + } + + @Override + public ByteBuffer read(final String path) throws IOException { + return this.dataSource.read(this.subdir + path); + } + + @Override + public InputStream getResourceAsStream(final String filepath) throws IOException { + return this.dataSource.getResourceAsStream(this.subdir + filepath); + } + + @Override + public boolean has(final String filepath) { + return this.dataSource.has(this.subdir + filepath); + } + + @Override + public Collection getListfile() { + final List results = new ArrayList<>(); + for (final String x : this.dataSource.getListfile()) { + if (x.startsWith(this.subdir)) { + results.add(x.substring(this.subdir.length())); + } + } + return results; + } + + @Override + public void close() throws IOException { + this.dataSource.close(); + } +} diff --git a/core/src/com/etheller/warsmash/networking/TestClient.java b/core/src/com/etheller/warsmash/networking/TestClient.java new file mode 100644 index 0000000..6d534f2 --- /dev/null +++ b/core/src/com/etheller/warsmash/networking/TestClient.java @@ -0,0 +1,32 @@ +package com.etheller.warsmash.networking; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; + +import com.etheller.warsmash.util.WarsmashConstants; + +public class TestClient { + + public static void main(final String[] args) { + try { + try (final DatagramChannel channel = DatagramChannel.open() + .connect(new InetSocketAddress(InetAddress.getLocalHost(), WarsmashConstants.PORT_NUMBER))) { + final ByteBuffer buffer = ByteBuffer.allocate(256); + buffer.clear(); + buffer.put((byte) 3); + buffer.put((byte) 2); + buffer.put((byte) 4); + buffer.put((byte) 1); + buffer.flip(); + channel.write(buffer); + } + } + catch (final IOException e) { + e.printStackTrace(); + } + } + +} diff --git a/core/src/com/etheller/warsmash/networking/WarsmashGameServer.java b/core/src/com/etheller/warsmash/networking/WarsmashGameServer.java new file mode 100644 index 0000000..c10be23 --- /dev/null +++ b/core/src/com/etheller/warsmash/networking/WarsmashGameServer.java @@ -0,0 +1,82 @@ +package com.etheller.warsmash.networking; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.Iterator; +import java.util.Set; + +import com.etheller.warsmash.util.WarsmashConstants; + +public class WarsmashGameServer implements Runnable { + + private final Selector selector; + private boolean running; + private final SelectionKey key; + private final ByteBuffer readBuffer; + + public WarsmashGameServer() throws IOException { + this.selector = Selector.open(); + this.running = true; + final DatagramChannel channel = DatagramChannel.open() + .bind(new InetSocketAddress(WarsmashConstants.PORT_NUMBER)); + channel.configureBlocking(false); + this.key = channel.register(this.selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); + this.readBuffer = ByteBuffer.allocate(256); + } + + @Override + public void run() { + while (this.running) { + try { + final int selectedKeyCount = this.selector.select(); + if (selectedKeyCount > 0) { + final Set selectedKeys = this.selector.selectedKeys(); + + final Iterator keyIterator = selectedKeys.iterator(); + + while (keyIterator.hasNext()) { + final SelectionKey key = keyIterator.next(); + + if (key.isReadable()) { + final DatagramChannel channel = (DatagramChannel) key.channel(); + this.readBuffer.clear(); + final SocketAddress receiveAddr = channel.receive(this.readBuffer); + this.readBuffer.flip(); + System.out.println("NOTE - Received from: " + receiveAddr); + while (this.readBuffer.hasRemaining()) { + System.out.println("NOTE - Received: " + this.readBuffer.get()); + } + } + + keyIterator.remove(); + } + } + } + catch (final IOException e) { + System.err.println("Error reading from channel:"); + e.printStackTrace(); + } + } + } + + public void setRunning(final boolean running) { + this.running = running; + } + + public static void main(final String[] args) { + WarsmashGameServer warsmashGameServer; + try { + warsmashGameServer = new WarsmashGameServer(); + new Thread(warsmashGameServer).start(); + } + catch (final IOException e) { + e.printStackTrace(); + } + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/DataSourceFDFParserBuilder.java b/core/src/com/etheller/warsmash/parsers/fdf/DataSourceFDFParserBuilder.java new file mode 100644 index 0000000..a2bec5b --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/DataSourceFDFParserBuilder.java @@ -0,0 +1,48 @@ +package com.etheller.warsmash.parsers.fdf; + +import java.io.IOException; + +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; + +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.fdfparser.FDFLexer; +import com.etheller.warsmash.fdfparser.FDFParser; +import com.etheller.warsmash.fdfparser.FDFParserBuilder; + +public class DataSourceFDFParserBuilder implements FDFParserBuilder { + private final DataSource dataSource; + + public DataSourceFDFParserBuilder(final DataSource dataSource) { + this.dataSource = dataSource; + } + + @Override + public FDFParser build(final String path) { + FDFLexer lexer; + try { + lexer = new FDFLexer(CharStreams.fromStream(this.dataSource.getResourceAsStream(path))); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + final FDFParser fdfParser = new FDFParser(new CommonTokenStream(lexer)); + final BaseErrorListener errorListener = new BaseErrorListener() { + @Override + public void syntaxError(final Recognizer recognizer, final Object offendingSymbol, final int line, + final int charPositionInLine, final String msg, final RecognitionException e) { + String sourceName = path; + if (!sourceName.isEmpty()) { + sourceName = String.format("%s:%d:%d: ", sourceName, line, charPositionInLine); + } + + System.err.println(sourceName + "line " + line + ":" + charPositionInLine + " " + msg); + } + }; + fdfParser.addErrorListener(errorListener); + return fdfParser; + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/parsers/fdf/DynamicFontGeneratorHolder.java b/core/src/com/etheller/warsmash/parsers/fdf/DynamicFontGeneratorHolder.java new file mode 100644 index 0000000..feb4a4a --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/DynamicFontGeneratorHolder.java @@ -0,0 +1,44 @@ +package com.etheller.warsmash.parsers.fdf; + +import java.util.HashMap; +import java.util.Map; + +import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.units.Element; +import com.etheller.warsmash.util.DataSourceFileHandle; + +public class DynamicFontGeneratorHolder { + private final DataSource dataSource; + private final Element skin; + private final Map fontNameToGenerator; + + public DynamicFontGeneratorHolder(final DataSource dataSource, final Element skin) { + this.dataSource = dataSource; + this.skin = skin; + this.fontNameToGenerator = new HashMap<>(); + } + + public FreeTypeFontGenerator getFontGenerator(final String font) { + FreeTypeFontGenerator fontGenerator = this.fontNameToGenerator.get(font); + if (fontGenerator == null) { + final String fontName = this.skin.getField(font); + if (fontName == null) { + throw new IllegalStateException("No such font: " + font); + } + if (!this.dataSource.has(fontName)) { + throw new IllegalStateException("No such font file: " + fontName + " (for \"" + font + "\")"); + } + fontGenerator = new FreeTypeFontGenerator(new DataSourceFileHandle(this.dataSource, fontName)); + this.fontNameToGenerator.put(font, fontGenerator); + } + return fontGenerator; + } + + public void dispose() { + for (final FreeTypeFontGenerator generator : this.fontNameToGenerator.values()) { + generator.dispose(); + } + this.fontNameToGenerator.clear(); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/GameSkin.java b/core/src/com/etheller/warsmash/parsers/fdf/GameSkin.java new file mode 100644 index 0000000..ccbcf44 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/GameSkin.java @@ -0,0 +1,22 @@ +package com.etheller.warsmash.parsers.fdf; + +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.units.Element; + +public class GameSkin { + private final Element skin; + private final DataTable skinsTable; + + public GameSkin(final Element skin, final DataTable skinsTable) { + this.skin = skin; + this.skinsTable = skinsTable; + } + + public Element getSkin() { + return this.skin; + } + + public DataTable getSkinsTable() { + return this.skinsTable; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/GameUI.java b/core/src/com/etheller/warsmash/parsers/fdf/GameUI.java new file mode 100644 index 0000000..dd5fd4c --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/GameUI.java @@ -0,0 +1,1092 @@ +package com.etheller.warsmash.parsers.fdf; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator; +import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator.FreeTypeFontParameter; +import com.badlogic.gdx.utils.viewport.ExtendViewport; +import com.badlogic.gdx.utils.viewport.FitViewport; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.fdfparser.FDFParser; +import com.etheller.warsmash.fdfparser.FrameDefinitionVisitor; +import com.etheller.warsmash.parsers.fdf.datamodel.AnchorDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.BackdropCornerFlags; +import com.etheller.warsmash.parsers.fdf.datamodel.ControlStyle; +import com.etheller.warsmash.parsers.fdf.datamodel.FontDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.FrameClass; +import com.etheller.warsmash.parsers.fdf.datamodel.FrameDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.FramePoint; +import com.etheller.warsmash.parsers.fdf.datamodel.FrameTemplateEnvironment; +import com.etheller.warsmash.parsers.fdf.datamodel.SetPointDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.TextJustify; +import com.etheller.warsmash.parsers.fdf.datamodel.Vector2Definition; +import com.etheller.warsmash.parsers.fdf.datamodel.Vector4Definition; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringPairFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.frames.AbstractUIFrame; +import com.etheller.warsmash.parsers.fdf.frames.BackdropFrame; +import com.etheller.warsmash.parsers.fdf.frames.ControlFrame; +import com.etheller.warsmash.parsers.fdf.frames.EditBoxFrame; +import com.etheller.warsmash.parsers.fdf.frames.FilterModeTextureFrame; +import com.etheller.warsmash.parsers.fdf.frames.GlueButtonFrame; +import com.etheller.warsmash.parsers.fdf.frames.GlueTextButtonFrame; +import com.etheller.warsmash.parsers.fdf.frames.ListBoxFrame; +import com.etheller.warsmash.parsers.fdf.frames.SetPoint; +import com.etheller.warsmash.parsers.fdf.frames.SimpleButtonFrame; +import com.etheller.warsmash.parsers.fdf.frames.SimpleFrame; +import com.etheller.warsmash.parsers.fdf.frames.SimpleStatusBarFrame; +import com.etheller.warsmash.parsers.fdf.frames.SpriteFrame; +import com.etheller.warsmash.parsers.fdf.frames.StringFrame; +import com.etheller.warsmash.parsers.fdf.frames.TextButtonFrame; +import com.etheller.warsmash.parsers.fdf.frames.TextureFrame; +import com.etheller.warsmash.parsers.fdf.frames.UIFrame; +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.units.Element; +import com.etheller.warsmash.units.custom.WTS; +import com.etheller.warsmash.util.ImageUtils; +import com.etheller.warsmash.util.StringBundle; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.handlers.AbstractMdxModelViewer; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.FocusableFrame; +import com.hiveworkshop.rms.parsers.mdlx.MdlxLayer.FilterMode; + +public final class GameUI extends AbstractUIFrame implements UIFrame { + public static final boolean DEBUG = false; + private static final boolean PIN_FAIL_IS_FATAL = true; + private final DataSource dataSource; + private final Element skin; + private final Viewport viewport; + private final Scene uiScene; + private final AbstractMdxModelViewer modelViewer; + private final int racialCommandIndex; + private final FrameTemplateEnvironment templates; + private final Map pathToTexture = new HashMap<>(); + private final boolean autoPosition = false; + private final FreeTypeFontGenerator fontGenerator; + private final FreeTypeFontParameter fontParam; + private final Map nameToFrame = new HashMap<>(); + private final Viewport fdfCoordinateResolutionDummyViewport; + private final DataTable skinData; + private final Element errorStrings; + private final GlyphLayout glyphLayout; + private final WTS mapStrings; + private final BitmapFont font; + private final BitmapFont font20; + private final DynamicFontGeneratorHolder dynamicFontGeneratorHolder; + + public GameUI(final DataSource dataSource, final GameSkin skin, final Viewport viewport, final Scene uiScene, + final AbstractMdxModelViewer modelViewer, final int racialCommandIndex, final WTS mapStrings) { + super("GameUI", null); + this.dataSource = dataSource; + this.skin = skin.getSkin(); + this.viewport = viewport; + this.uiScene = uiScene; + this.modelViewer = modelViewer; + this.racialCommandIndex = racialCommandIndex; + if (viewport instanceof ExtendViewport) { + this.renderBounds.set(0, 0, ((ExtendViewport) viewport).getMinWorldWidth(), + ((ExtendViewport) viewport).getMinWorldHeight()); + } + else { + this.renderBounds.set(0, 0, viewport.getWorldWidth(), viewport.getWorldHeight()); + } + this.templates = new FrameTemplateEnvironment(); + + this.dynamicFontGeneratorHolder = new DynamicFontGeneratorHolder(this.modelViewer.dataSource, this.skin); + this.fontGenerator = this.dynamicFontGeneratorHolder.getFontGenerator("MasterFont"); + final FreeTypeFontParameter fontParam = new FreeTypeFontParameter(); + fontParam.size = 32; + this.font = this.fontGenerator.generateFont(fontParam); + fontParam.size = 20; + this.font20 = this.fontGenerator.generateFont(fontParam); + this.fontParam = new FreeTypeFontParameter(); + this.fdfCoordinateResolutionDummyViewport = new FitViewport(0.8f, 0.6f); + this.skinData = skin.getSkinsTable(); + this.errorStrings = this.skinData.get("Errors"); + this.glyphLayout = new GlyphLayout(); + this.mapStrings = mapStrings; + } + + public static GameSkin loadSkin(final DataSource dataSource, final String skin) { + final DataTable skinsTable = new DataTable(StringBundle.EMPTY); + try (InputStream stream = dataSource.getResourceAsStream("UI\\war3skins.txt")) { + skinsTable.readTXT(stream, true); + try (InputStream miscDataTxtStream = dataSource.getResourceAsStream("Units\\CommandFunc.txt")) { + skinsTable.readTXT(miscDataTxtStream, true); + } + try (InputStream miscDataTxtStream = dataSource.getResourceAsStream("Units\\CommandStrings.txt")) { + skinsTable.readTXT(miscDataTxtStream, true); + } + } + catch (final IOException e) { + throw new RuntimeException(e); + } + // TODO eliminate duplicate read of skin TXT!! + if (dataSource.has("war3mapSkin.txt")) { + try (InputStream miscDataTxtStream = dataSource.getResourceAsStream("war3mapSkin.txt")) { + skinsTable.readTXT(miscDataTxtStream, true); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } +// final Element main = skinsTable.get("Main"); +// final String skinsField = main.getField("Skins"); +// final String[] skins = skinsField.split(","); + final Element defaultSkin = skinsTable.get("Default"); + final Element userSkin = skinsTable.get(skin); + final Element customSkin = skinsTable.get("CustomSkin"); + for (final String key : defaultSkin.keySet()) { + if (!userSkin.hasField(key)) { + userSkin.setField(key, defaultSkin.getField(key)); + } + } + if (customSkin != null) { + for (final String key : customSkin.keySet()) { + userSkin.setField(key, customSkin.getField(key)); + } + } + return new GameSkin(userSkin, skinsTable); + } + + public static GameSkin loadSkin(final DataSource dataSource, final int skinIndex) { + final DataTable skinsTable = new DataTable(StringBundle.EMPTY); + try (InputStream stream = dataSource.getResourceAsStream("UI\\war3skins.txt")) { + skinsTable.readTXT(stream, true); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + // TODO eliminate duplicate read of skin TXT!! + if (dataSource.has("war3mapSkin.txt")) { + try (InputStream miscDataTxtStream = dataSource.getResourceAsStream("war3mapSkin.txt")) { + skinsTable.readTXT(miscDataTxtStream, true); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + final Element main = skinsTable.get("Main"); + final String skinsField = main.getField("Skins"); + final String[] skins = skinsField.split(","); + final Element defaultSkin = skinsTable.get("Default"); + final Element userSkin; + if ((skinIndex >= 0) && (skinIndex < skins.length)) { + userSkin = skinsTable.get(skins[skinIndex]); + } + else { + userSkin = new Element("UserSkin", skinsTable); + } + final Element customSkin = skinsTable.get("CustomSkin"); + for (final String key : defaultSkin.keySet()) { + if (!userSkin.hasField(key)) { + userSkin.setField(key, defaultSkin.getField(key)); + } + } + if (customSkin != null) { + for (final String key : customSkin.keySet()) { + userSkin.setField(key, customSkin.getField(key)); + } + } + return new GameSkin(userSkin, skinsTable); + } + + public void loadTOCFile(final String tocFilePath) throws IOException { + final DataSourceFDFParserBuilder dataSourceFDFParserBuilder = new DataSourceFDFParserBuilder(this.dataSource); + final FrameDefinitionVisitor fdfVisitor = new FrameDefinitionVisitor(this.templates, + dataSourceFDFParserBuilder); + System.err.println("Loading TOC file: " + tocFilePath); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(this.dataSource.getResourceAsStream(tocFilePath)))) { + String line; + int tocLines = 0; + while ((line = reader.readLine()) != null) { + final FDFParser firstFileParser = dataSourceFDFParserBuilder.build(line); + fdfVisitor.visit(firstFileParser.program()); + tocLines++; + } + System.out.println("TOC file loaded " + tocLines + " lines"); + } + } + + public String getSkinField(String file) { + if ((file != null) && this.skin.hasField(file)) { + file = this.skin.getField(file); + } + else { + throw new IllegalStateException("Decorated file name lookup not available: " + file); + } + return file; + } + + public String trySkinField(String file) { + if ((file != null) && this.skin.hasField(file)) { + file = this.skin.getField(file); + } + return file; + } + + public DataTable getSkinData() { + return this.skinData; + } + + public UIFrame createFrame(final String name, final UIFrame owner, final int priority, final int createContext) { + final FrameDefinition frameDefinition = this.templates.getFrame(name); + final UIFrame inflatedFrame = inflate(frameDefinition, owner, null, frameDefinition.has("DecorateFileNames")); + if (owner == this) { + add(inflatedFrame); + } + return inflatedFrame; + } + + public UIFrame createSimpleFrame(final String name, final UIFrame owner, final int createContext) { + final FrameDefinition frameDefinition = this.templates.getFrame(name); + if (frameDefinition == null) { + final SimpleFrame simpleFrame = new SimpleFrame(name, owner); + add(simpleFrame); + return simpleFrame; + } + else if (frameDefinition.getFrameClass() == FrameClass.Frame) { + final UIFrame inflated = inflate(frameDefinition, owner, null, frameDefinition.has("DecorateFileNames")); + if (this.autoPosition) { + inflated.positionBounds(this, this.viewport); + } + add(inflated); + return inflated; + } + return null; + } + + public TextureFrame createTextureFrame(final String name, final UIFrame parent, final boolean decorateFileNames, + final Vector4Definition texCoord) { + final TextureFrame textureFrame = new TextureFrame(name, parent, decorateFileNames, texCoord); + this.nameToFrame.put(name, textureFrame); + add(textureFrame); + return textureFrame; + } + + public TextureFrame createTextureFrame(final String name, final UIFrame parent, final boolean decorateFileNames, + final Vector4Definition texCoord, final FilterMode filterMode) { + final FilterModeTextureFrame textureFrame = new FilterModeTextureFrame(name, parent, decorateFileNames, + texCoord); + textureFrame.setFilterMode(filterMode); + this.nameToFrame.put(name, textureFrame); + add(textureFrame); + return textureFrame; + } + + public StringFrame createStringFrame(final String name, final UIFrame parent, final Color color, + final TextJustify justifyH, final TextJustify justifyV, final float fdfFontSize) { + this.fontParam.size = (int) convertY(this.viewport, fdfFontSize); + if (this.fontParam.size == 0) { + this.fontParam.size = 128; + } + final BitmapFont frameFont = this.fontGenerator.generateFont(this.fontParam); + final StringFrame stringFrame = new StringFrame(name, parent, color, justifyH, justifyV, frameFont, name, null, + null); + this.nameToFrame.put(name, stringFrame); + add(stringFrame); + return stringFrame; + } + + public UIFrame inflate(final FrameDefinition frameDefinition, final UIFrame parent, + final FrameDefinition parentDefinitionIfAvailable, final boolean inDecorateFileNames) { + UIFrame inflatedFrame = null; + BitmapFont frameFont = null; + Viewport viewport2 = this.viewport; + switch (frameDefinition.getFrameClass()) { + case Frame: + if ("SIMPLEFRAME".equals(frameDefinition.getFrameType())) { + final SimpleFrame simpleFrame = new SimpleFrame(frameDefinition.getName(), parent); + // TODO: we should not need to put ourselves in this map 2x, but we do + // since there are nested inflate calls happening before the general case + // mapping + this.nameToFrame.put(frameDefinition.getName(), simpleFrame); + for (final FrameDefinition childDefinition : frameDefinition.getInnerFrames()) { + simpleFrame.add(inflate(childDefinition, simpleFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames"))); + } + inflatedFrame = simpleFrame; + } + else if ("SIMPLESTATUSBAR".equals(frameDefinition.getFrameType())) { + final boolean decorateFileNames = frameDefinition.has("DecorateFileNames") + || ((parentDefinitionIfAvailable != null) + && parentDefinitionIfAvailable.has("DecorateFileNames")); + final SimpleStatusBarFrame simpleStatusBarFrame = new SimpleStatusBarFrame(frameDefinition.getName(), + parent, decorateFileNames); + for (final FrameDefinition childDefinition : frameDefinition.getInnerFrames()) { + simpleStatusBarFrame.add(inflate(childDefinition, simpleStatusBarFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames"))); + } + final String barTexture = frameDefinition.getString("BarTexture"); + if (barTexture != null) { + simpleStatusBarFrame.getBarFrame().setTexture(barTexture, this); + simpleStatusBarFrame.getBorderFrame().setTexture(barTexture + "Border", this); + } + inflatedFrame = simpleStatusBarFrame; + } + else if ("SPRITE".equals(frameDefinition.getFrameType())) { + final SpriteFrame spriteFrame = new SpriteFrame(frameDefinition.getName(), parent, this.uiScene, + viewport2); + String backgroundArt = frameDefinition.getString("BackgroundArt"); + if (frameDefinition.has("DecorateFileNames") || inDecorateFileNames) { + if (backgroundArt != null) { + if (this.skin.hasField(backgroundArt)) { + backgroundArt = this.skin.getField(backgroundArt); + } + } + } + if (backgroundArt != null) { + setSpriteFrameModel(spriteFrame, backgroundArt); + } + viewport2 = this.viewport; // TODO was fdfCoordinateResolutionDummyViewport here previously, but is that + // a good idea? + this.nameToFrame.put(frameDefinition.getName(), spriteFrame); + for (final FrameDefinition childDefinition : frameDefinition.getInnerFrames()) { + spriteFrame.add(inflate(childDefinition, spriteFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames"))); + } + inflatedFrame = spriteFrame; + } + else if ("FRAME".equals(frameDefinition.getFrameType()) + || "DIALOG".equals(frameDefinition.getFrameType())) { + final SimpleFrame simpleFrame = new SimpleFrame(frameDefinition.getName(), parent); + // TODO: we should not need to put ourselves in this map 2x, but we do + // since there are nested inflate calls happening before the general case + // mapping + this.nameToFrame.put(frameDefinition.getName(), simpleFrame); + for (final FrameDefinition childDefinition : frameDefinition.getInnerFrames()) { + simpleFrame.add(inflate(childDefinition, simpleFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames"))); + } + inflatedFrame = simpleFrame; + } + else if ("TEXT".equals(frameDefinition.getFrameType())) { + final Float textLength = frameDefinition.getFloat("TextLength"); + TextJustify justifyH = frameDefinition.getTextJustify("FontJustificationH"); + if (justifyH == null) { + justifyH = TextJustify.LEFT; + } + TextJustify justifyV = frameDefinition.getTextJustify("FontJustificationV"); + if (justifyV == null) { + justifyV = TextJustify.MIDDLE; + } + for (final SetPointDefinition setPoint : frameDefinition.getSetPoints()) { + if (((setPoint.getMyPoint() == FramePoint.TOP) && (setPoint.getOtherPoint() == FramePoint.TOP)) + || ((setPoint.getMyPoint() == FramePoint.BOTTOM) + && (setPoint.getOtherPoint() == FramePoint.BOTTOM))) { + justifyH = TextJustify.CENTER; + } + } + + Color fontColor; + final Vector4Definition fontColorDefinition = frameDefinition.getVector4("FontColor"); + if (fontColorDefinition == null) { + fontColor = Color.WHITE; + } + else { + fontColor = new Color(fontColorDefinition.getX(), fontColorDefinition.getY(), + fontColorDefinition.getZ(), fontColorDefinition.getW()); + } + + Color fontHighlightColor; + final Vector4Definition fontHighlightColorDefinition = frameDefinition.getVector4("FontHighlightColor"); + if (fontHighlightColorDefinition == null) { + fontHighlightColor = null; + } + else { + fontHighlightColor = new Color(fontHighlightColorDefinition.getX(), + fontHighlightColorDefinition.getY(), fontHighlightColorDefinition.getZ(), + fontHighlightColorDefinition.getW()); + } + + Color fontDisabledColor; + final Vector4Definition fontDisabledColorDefinition = frameDefinition.getVector4("FontDisabledColor"); + if (fontDisabledColorDefinition == null) { + fontDisabledColor = null; + } + else { + fontDisabledColor = new Color(fontDisabledColorDefinition.getX(), + fontDisabledColorDefinition.getY(), fontDisabledColorDefinition.getZ(), + fontDisabledColorDefinition.getW()); + } + + Color fontShadowColor; + final Vector4Definition fontShadowColorDefinition = frameDefinition.getVector4("FontShadowColor"); + if (fontShadowColorDefinition == null) { + fontShadowColor = null; + } + else { + fontShadowColor = new Color(fontShadowColorDefinition.getX(), fontShadowColorDefinition.getY(), + fontShadowColorDefinition.getZ(), fontShadowColorDefinition.getW()); + } + final FontDefinition font = frameDefinition.getFont("FrameFont"); + final Float height = frameDefinition.getFloat("Height"); + this.fontParam.size = (int) convertY(viewport2, + (font == null ? (height == null ? 0.06f : height) : font.getFontSize())); + if (this.fontParam.size == 0) { + this.fontParam.size = 24; + } + frameFont = this.dynamicFontGeneratorHolder.getFontGenerator(font.getFontName()) + .generateFont(this.fontParam); + String textString = frameDefinition.getName(); + String text = frameDefinition.getString("Text"); + if (text != null) { + final String decoratedString = this.templates.getDecoratedString(text); + if (decoratedString != text) { + text = decoratedString; + } + textString = text; + } + final StringFrame stringFrame = new StringFrame(frameDefinition.getName(), parent, fontColor, justifyH, + justifyV, frameFont, textString, fontHighlightColor, fontDisabledColor); + if (fontShadowColor != null) { + final Vector2Definition shadowOffset = frameDefinition.getVector2("FontShadowOffset"); + stringFrame.setFontShadowColor(fontShadowColor); + stringFrame.setFontShadowOffsetX(convertX(viewport2, shadowOffset.getX())); + stringFrame.setFontShadowOffsetY(convertY(viewport2, shadowOffset.getY())); + } + inflatedFrame = stringFrame; + } + else if ("GLUETEXTBUTTON".equals(frameDefinition.getFrameType())) { + // ButtonText & ControlBackdrop + final GlueTextButtonFrame glueButtonFrame = new GlueTextButtonFrame(frameDefinition.getName(), parent); + // TODO: we should not need to put ourselves in this map 2x, but we do + // since there are nested inflate calls happening before the general case + // mapping + this.nameToFrame.put(frameDefinition.getName(), glueButtonFrame); + final String controlBackdropKey = frameDefinition.getString("ControlBackdrop"); + final String controlPushedBackdropKey = frameDefinition.getString("ControlPushedBackdrop"); + final String controlDisabledBackdropKey = frameDefinition.getString("ControlDisabledBackdrop"); + final String controlMouseOverHighlightKey = frameDefinition.getString("ControlMouseOverHighlight"); + final String buttonTextKey = frameDefinition.getString("ButtonText"); + for (final FrameDefinition childDefinition : frameDefinition.getInnerFrames()) { + if (childDefinition.getName().equals(controlBackdropKey)) { + final UIFrame inflatedChild = inflate(childDefinition, glueButtonFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + inflatedChild.setSetAllPoints(true); + glueButtonFrame.setControlBackdrop(inflatedChild); + } + else if (childDefinition.getName().equals(controlPushedBackdropKey)) { + final UIFrame inflatedChild = inflate(childDefinition, glueButtonFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + inflatedChild.setSetAllPoints(true); + glueButtonFrame.setControlPushedBackdrop(inflatedChild); + } + else if (childDefinition.getName().equals(controlDisabledBackdropKey)) { + final UIFrame inflatedChild = inflate(childDefinition, glueButtonFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + inflatedChild.setSetAllPoints(true); + glueButtonFrame.setControlDisabledBackdrop(inflatedChild); + } + else if (childDefinition.getName().equals(controlMouseOverHighlightKey)) { + final UIFrame inflatedChild = inflate(childDefinition, glueButtonFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + inflatedChild.setSetAllPoints(true); + glueButtonFrame.setControlMouseOverHighlight(inflatedChild); + } + else if (childDefinition.getName().equals(buttonTextKey)) { + final UIFrame inflatedChild = inflate(childDefinition, glueButtonFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + inflatedChild.setSetAllPoints(true); + glueButtonFrame.setButtonText(inflatedChild); + } + } + final EnumSet controlStyle = ControlStyle + .parseControlStyle(frameDefinition.getString("ControlStyle")); + if (controlStyle.contains(ControlStyle.AUTOTRACK) + && controlStyle.contains(ControlStyle.HIGHLIGHTONMOUSEOVER)) { + glueButtonFrame.setHighlightOnMouseOver(true); + } + inflatedFrame = glueButtonFrame; + } + else if ("SIMPLEBUTTON".equals(frameDefinition.getFrameType())) { + // ButtonText & ControlBackdrop + final SimpleButtonFrame simpleButtonFrame = new SimpleButtonFrame(frameDefinition.getName(), parent); + // TODO: we should not need to put ourselves in this map 2x, but we do + // since there are nested inflate calls happening before the general case + // mapping + this.nameToFrame.put(frameDefinition.getName(), simpleButtonFrame); + final StringPairFrameDefinitionField normalTextDefinition = frameDefinition.getStringPair("NormalText"); + final StringPairFrameDefinitionField disabledTextDefinition = frameDefinition + .getStringPair("DisabledText"); + final StringPairFrameDefinitionField highlightTextDefinition = frameDefinition + .getStringPair("HighlightText"); + final String normalTextureDefinition = frameDefinition.getString("NormalTexture"); + final String pushedTextureDefinition = frameDefinition.getString("PushedTexture"); + final String disabledTextureDefinition = frameDefinition.getString("DisabledTexture"); + final String useHighlightDefinition = frameDefinition.getString("UseHighlight"); + + final boolean decorateFileNamesOnThisFrame = frameDefinition.has("DecorateFileNames") + || inDecorateFileNames; + final UIFrame normalText = inflate(this.templates.getFrame(normalTextDefinition.getFirst()), + simpleButtonFrame, frameDefinition, decorateFileNamesOnThisFrame); + setDecoratedText((StringFrame) normalText, normalTextDefinition.getSecond()); + normalText.setSetAllPoints(true); + final UIFrame disabledText = inflate(this.templates.getFrame(disabledTextDefinition.getFirst()), + simpleButtonFrame, frameDefinition, decorateFileNamesOnThisFrame); + setDecoratedText((StringFrame) disabledText, disabledTextDefinition.getSecond()); + disabledText.setSetAllPoints(true); + final UIFrame highlightText = inflate(this.templates.getFrame(highlightTextDefinition.getFirst()), + simpleButtonFrame, frameDefinition, decorateFileNamesOnThisFrame); + setDecoratedText((StringFrame) highlightText, highlightTextDefinition.getSecond()); + highlightText.setSetAllPoints(true); + final UIFrame normalTexture = inflate(this.templates.getFrame(normalTextureDefinition), + simpleButtonFrame, frameDefinition, decorateFileNamesOnThisFrame); + normalTexture.setSetAllPoints(true); + final UIFrame pushedTexture = inflate(this.templates.getFrame(pushedTextureDefinition), + simpleButtonFrame, frameDefinition, decorateFileNamesOnThisFrame); + pushedTexture.setSetAllPoints(true); + final UIFrame disabledTexture = inflate(this.templates.getFrame(disabledTextureDefinition), + simpleButtonFrame, frameDefinition, decorateFileNamesOnThisFrame); + disabledTexture.setSetAllPoints(true); + final UIFrame useHighlight = inflate(this.templates.getFrame(useHighlightDefinition), simpleButtonFrame, + frameDefinition, decorateFileNamesOnThisFrame); + useHighlight.setSetAllPoints(true); + simpleButtonFrame.setButtonText(normalText); + simpleButtonFrame.setDisabledText(disabledText); + simpleButtonFrame.setHighlightText(highlightText); + simpleButtonFrame.setControlBackdrop(normalTexture); + simpleButtonFrame.setControlDisabledBackdrop(disabledTexture); + simpleButtonFrame.setControlMouseOverHighlight(useHighlight); + simpleButtonFrame.setControlPushedBackdrop(pushedTexture); + + final Vector2Definition pushedTextOffset = frameDefinition.getVector2("ButtonPushedTextOffset"); + if (pushedTextOffset != null) { + final UIFrame pushedNormalText = inflate(this.templates.getFrame(normalTextDefinition.getFirst()), + simpleButtonFrame, frameDefinition, decorateFileNamesOnThisFrame); + setDecoratedText((StringFrame) pushedNormalText, normalTextDefinition.getSecond()); + final UIFrame pushedHighlightText = inflate( + this.templates.getFrame(highlightTextDefinition.getFirst()), simpleButtonFrame, + frameDefinition, decorateFileNamesOnThisFrame); + setDecoratedText((StringFrame) pushedHighlightText, highlightTextDefinition.getSecond()); + pushedNormalText.addSetPoint(new SetPoint(FramePoint.TOPLEFT, simpleButtonFrame, FramePoint.TOPLEFT, + GameUI.convertX(viewport2, pushedTextOffset.getX()), + GameUI.convertY(viewport2, pushedTextOffset.getY()))); + pushedNormalText.addSetPoint(new SetPoint(FramePoint.BOTTOMRIGHT, simpleButtonFrame, + FramePoint.BOTTOMRIGHT, GameUI.convertX(viewport2, pushedTextOffset.getX()), + GameUI.convertY(viewport2, pushedTextOffset.getY()))); + pushedHighlightText.addSetPoint(new SetPoint(FramePoint.TOPLEFT, simpleButtonFrame, + FramePoint.TOPLEFT, GameUI.convertX(viewport2, pushedTextOffset.getX()), + GameUI.convertY(viewport2, pushedTextOffset.getY()))); + pushedHighlightText.addSetPoint(new SetPoint(FramePoint.BOTTOMRIGHT, simpleButtonFrame, + FramePoint.BOTTOMRIGHT, GameUI.convertX(viewport2, pushedTextOffset.getX()), + GameUI.convertY(viewport2, pushedTextOffset.getY()))); + simpleButtonFrame.setPushedText(pushedNormalText); + simpleButtonFrame.setPushedHighlightText(pushedHighlightText); + } + else { + simpleButtonFrame.setPushedText(normalText); + simpleButtonFrame.setPushedHighlightText(highlightText); + } + + inflatedFrame = simpleButtonFrame; + } + else if ("GLUEBUTTON".equals(frameDefinition.getFrameType())) { + // ButtonText & ControlBackdrop + final GlueButtonFrame glueButtonFrame = new GlueButtonFrame(frameDefinition.getName(), parent); + // TODO: we should not need to put ourselves in this map 2x, but we do + // since there are nested inflate calls happening before the general case + // mapping + this.nameToFrame.put(frameDefinition.getName(), glueButtonFrame); + final String controlBackdropKey = frameDefinition.getString("ControlBackdrop"); + final String controlPushedBackdropKey = frameDefinition.getString("ControlPushedBackdrop"); + final String controlDisabledBackdropKey = frameDefinition.getString("ControlDisabledBackdrop"); + final String controlMouseOverHighlightKey = frameDefinition.getString("ControlMouseOverHighlight"); + for (final FrameDefinition childDefinition : frameDefinition.getInnerFrames()) { + if (childDefinition.getName().equals(controlBackdropKey)) { + final UIFrame inflatedChild = inflate(childDefinition, glueButtonFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + inflatedChild.setSetAllPoints(true); + glueButtonFrame.setControlBackdrop(inflatedChild); + } + else if (childDefinition.getName().equals(controlPushedBackdropKey)) { + final UIFrame inflatedChild = inflate(childDefinition, glueButtonFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + inflatedChild.setSetAllPoints(true); + glueButtonFrame.setControlPushedBackdrop(inflatedChild); + } + else if (childDefinition.getName().equals(controlDisabledBackdropKey)) { + final UIFrame inflatedChild = inflate(childDefinition, glueButtonFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + inflatedChild.setSetAllPoints(true); + glueButtonFrame.setControlDisabledBackdrop(inflatedChild); + } + else if (childDefinition.getName().equals(controlMouseOverHighlightKey)) { + final UIFrame inflatedChild = inflate(childDefinition, glueButtonFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + inflatedChild.setSetAllPoints(true); + glueButtonFrame.setControlMouseOverHighlight(inflatedChild); + } + } + final EnumSet controlStyle = ControlStyle + .parseControlStyle(frameDefinition.getString("ControlStyle")); + if (controlStyle.contains(ControlStyle.AUTOTRACK) + && controlStyle.contains(ControlStyle.HIGHLIGHTONMOUSEOVER)) { + glueButtonFrame.setHighlightOnMouseOver(true); + } + inflatedFrame = glueButtonFrame; + } + else if ("TEXTBUTTON".equals(frameDefinition.getFrameType())) { + // ButtonText & ControlBackdrop + final TextButtonFrame glueButtonFrame = new TextButtonFrame(frameDefinition.getName(), parent); + // TODO: we should not need to put ourselves in this map 2x, but we do + // since there are nested inflate calls happening before the general case + // mapping + this.nameToFrame.put(frameDefinition.getName(), glueButtonFrame); + final String controlBackdropKey = frameDefinition.getString("ControlBackdrop"); + final String controlPushedBackdropKey = frameDefinition.getString("ControlPushedBackdrop"); + final String controlDisabledBackdropKey = frameDefinition.getString("ControlDisabledBackdrop"); + final String controlMouseOverHighlightKey = frameDefinition.getString("ControlMouseOverHighlight"); + final Vector2Definition pushedTextOffset = frameDefinition.getVector2("ButtonPushedTextOffset"); + if (pushedTextOffset != null) { + glueButtonFrame.setButtonPushedTextOffsetX(pushedTextOffset.getX()); + glueButtonFrame.setButtonPushedTextOffsetY(pushedTextOffset.getY()); + } + for (final FrameDefinition childDefinition : frameDefinition.getInnerFrames()) { + if (childDefinition.getName().equals(controlBackdropKey)) { + final UIFrame inflatedChild = inflate(childDefinition, glueButtonFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + inflatedChild.setSetAllPoints(true); + glueButtonFrame.setControlBackdrop(inflatedChild); + } + else if (childDefinition.getName().equals(controlPushedBackdropKey)) { + final UIFrame inflatedChild = inflate(childDefinition, glueButtonFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + inflatedChild.setSetAllPoints(true); + glueButtonFrame.setControlPushedBackdrop(inflatedChild); + } + else if (childDefinition.getName().equals(controlDisabledBackdropKey)) { + final UIFrame inflatedChild = inflate(childDefinition, glueButtonFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + inflatedChild.setSetAllPoints(true); + glueButtonFrame.setControlDisabledBackdrop(inflatedChild); + } + else if (childDefinition.getName().equals(controlMouseOverHighlightKey)) { + final UIFrame inflatedChild = inflate(childDefinition, glueButtonFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + inflatedChild.setSetAllPoints(true); + glueButtonFrame.setControlMouseOverHighlight(inflatedChild); + } + } + final EnumSet controlStyle = ControlStyle + .parseControlStyle(frameDefinition.getString("ControlStyle")); + if (controlStyle.contains(ControlStyle.AUTOTRACK) + && controlStyle.contains(ControlStyle.HIGHLIGHTONMOUSEOVER)) { + glueButtonFrame.setHighlightOnMouseOver(true); + } + inflatedFrame = glueButtonFrame; + } + else if ("EDITBOX".equals(frameDefinition.getFrameType())) { + final float editBorderSize = convertX(viewport2, frameDefinition.getFloat("EditBorderSize")); + final Vector4Definition editCursorColorDefinition = frameDefinition.getVector4("EditCursorColor"); + Color editCursorColor; + if (editCursorColorDefinition == null) { + editCursorColor = Color.WHITE; + } + else { + editCursorColor = new Color(editCursorColorDefinition.getX(), editCursorColorDefinition.getY(), + editCursorColorDefinition.getZ(), editCursorColorDefinition.getW()); + } + final EditBoxFrame editBoxFrame = new EditBoxFrame(frameDefinition.getName(), parent, editBorderSize, + editCursorColor); + // TODO: we should not need to put ourselves in this map 2x, but we do + // since there are nested inflate calls happening before the general case + // mapping + this.nameToFrame.put(frameDefinition.getName(), editBoxFrame); + final String controlBackdropKey = frameDefinition.getString("ControlBackdrop"); + final String editTextFrameKey = frameDefinition.getString("EditTextFrame"); + for (final FrameDefinition childDefinition : frameDefinition.getInnerFrames()) { + if (childDefinition.getName().equals(controlBackdropKey)) { + final UIFrame inflatedChild = inflate(childDefinition, editBoxFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + inflatedChild.setSetAllPoints(true); + editBoxFrame.setControlBackdrop(inflatedChild); + } + else if (childDefinition.getName().equals(editTextFrameKey)) { + final UIFrame inflatedChild = inflate(childDefinition, editBoxFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + final StringFrame editTextFrame = (StringFrame) inflatedChild; + inflatedChild.setSetAllPoints(true, editBorderSize); + editBoxFrame.setEditTextFrame(editTextFrame); + setText(editTextFrame, ""); + } + } + inflatedFrame = editBoxFrame; + } + else if ("CONTROL".equals(frameDefinition.getFrameType())) { + final ControlFrame controlFrame = new ControlFrame(frameDefinition.getName(), parent); + // TODO: we should not need to put ourselves in this map 2x, but we do + // since there are nested inflate calls happening before the general case + // mapping + this.nameToFrame.put(frameDefinition.getName(), controlFrame); + final String controlBackdropKey = frameDefinition.getString("ControlBackdrop"); + for (final FrameDefinition childDefinition : frameDefinition.getInnerFrames()) { + if (childDefinition.getName().equals(controlBackdropKey)) { + final UIFrame inflatedChild = inflate(childDefinition, controlFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + inflatedChild.setSetAllPoints(true); + controlFrame.setControlBackdrop(inflatedChild); + } + } + inflatedFrame = controlFrame; + } + else if ("LISTBOX".equals(frameDefinition.getFrameType())) { + // TODO advanced components here + final ListBoxFrame controlFrame = new ListBoxFrame(frameDefinition.getName(), parent, viewport2); + // TODO: we should not need to put ourselves in this map 2x, but we do + // since there are nested inflate calls happening before the general case + // mapping + this.nameToFrame.put(frameDefinition.getName(), controlFrame); + final String controlBackdropKey = frameDefinition.getString("ControlBackdrop"); + for (final FrameDefinition childDefinition : frameDefinition.getInnerFrames()) { + if (childDefinition.getName().equals(controlBackdropKey)) { + final UIFrame inflatedChild = inflate(childDefinition, controlFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames")); + inflatedChild.setSetAllPoints(true); + controlFrame.setControlBackdrop(inflatedChild); + } + } + inflatedFrame = controlFrame; + } + else if ("HIGHLIGHT".equals(frameDefinition.getFrameType())) { + final String highlightType = frameDefinition.getString("HighlightType"); + if (!"FILETEXTURE".equals(highlightType)) { + throw new IllegalStateException( + "Our engine does not know how to handle a non-FILETEXTURE highlight"); + } + final String highlightAlphaFile = frameDefinition.getString("HighlightAlphaFile"); + final String highlightAlphaMode = frameDefinition.getString("HighlightAlphaMode"); + final FilterModeTextureFrame textureFrame = new FilterModeTextureFrame(frameDefinition.getName(), + parent, inDecorateFileNames || frameDefinition.has("DecorateFileNames"), null); + textureFrame.setTexture(highlightAlphaFile, this); + if ("ADD".equals(highlightAlphaMode)) { + textureFrame.setFilterMode(FilterMode.ADDALPHA); + } + return textureFrame; + } + else if ("BACKDROP".equals(frameDefinition.getFrameType())) { + final boolean tileBackground = frameDefinition.has("BackdropTileBackground"); + final boolean mirrored = frameDefinition.has("BackdropMirrored"); + String backgroundString = frameDefinition.getString("BackdropBackground"); + String cornerFlagsString = frameDefinition.getString("BackdropCornerFlags"); + if (cornerFlagsString == null) { + cornerFlagsString = ""; + } + final EnumSet cornerFlags = BackdropCornerFlags + .parseCornerFlags(cornerFlagsString); + final Float cornerSizeNullable = frameDefinition.getFloat("BackdropCornerSize"); + final float cornerSize = GameUI.convertX(viewport2, + cornerSizeNullable == null ? 0.0f : cornerSizeNullable); + final Float backgroundSizeNullable = frameDefinition.getFloat("BackdropBackgroundSize"); + final float backgroundSize = GameUI.convertX(viewport2, + backgroundSizeNullable == null ? 0.0f : backgroundSizeNullable); + Vector4Definition backgroundInsets = frameDefinition.getVector4("BackdropBackgroundInsets"); + if (backgroundInsets != null) { + backgroundInsets = new Vector4Definition(GameUI.convertX(viewport2, backgroundInsets.getX()), + GameUI.convertY(viewport2, backgroundInsets.getY()), + GameUI.convertX(viewport2, backgroundInsets.getZ()), + GameUI.convertY(viewport2, backgroundInsets.getW())); + } + else { + backgroundInsets = new Vector4Definition(0, 0, 0, 0); + } + final boolean decorateFileNames = frameDefinition.has("DecorateFileNames") || inDecorateFileNames; + String edgeFileString = frameDefinition.getString("BackdropEdgeFile"); + System.out.println(frameDefinition.getName() + " wants edge file: " + edgeFileString); + if (decorateFileNames && (edgeFileString != null)) { + edgeFileString = trySkinField(edgeFileString); + } + if (decorateFileNames && (edgeFileString != null)) { + backgroundString = trySkinField(backgroundString); + } + final Texture background = backgroundString == null ? null : loadTexture(backgroundString); + final Texture edgeFile = edgeFileString == null ? null : loadTexture(edgeFileString); + System.out.println(frameDefinition.getName() + " got edge file: " + edgeFile); + + final BackdropFrame backdropFrame = new BackdropFrame(frameDefinition.getName(), parent, + decorateFileNames, tileBackground, background, cornerFlags, cornerSize, backgroundSize, + backgroundInsets, edgeFile, mirrored); + this.nameToFrame.put(frameDefinition.getName(), backdropFrame); + for (final FrameDefinition childDefinition : frameDefinition.getInnerFrames()) { + backdropFrame.add(inflate(childDefinition, backdropFrame, frameDefinition, decorateFileNames)); + } + inflatedFrame = backdropFrame; + } + break; + case Layer: + final SimpleFrame simpleFrame = new SimpleFrame(frameDefinition.getName(), parent); + simpleFrame.setSetAllPoints(true); + this.nameToFrame.put(frameDefinition.getName(), simpleFrame); + for (final FrameDefinition childDefinition : frameDefinition.getInnerFrames()) { + simpleFrame.add(inflate(childDefinition, simpleFrame, frameDefinition, + inDecorateFileNames || childDefinition.has("DecorateFileNames"))); + } + inflatedFrame = simpleFrame; + break; + case String: + final Float textLength = frameDefinition.getFloat("TextLength"); + TextJustify justifyH = frameDefinition.getTextJustify("FontJustificationH"); + if (justifyH == null) { + justifyH = TextJustify.CENTER; + } + TextJustify justifyV = frameDefinition.getTextJustify("FontJustificationV"); + if (justifyV == null) { + justifyV = TextJustify.MIDDLE; + } + + Color fontColor; + final Vector4Definition fontColorDefinition = frameDefinition.getVector4("FontColor"); + if (fontColorDefinition == null) { + fontColor = Color.WHITE; + } + else { + fontColor = new Color(fontColorDefinition.getX(), fontColorDefinition.getY(), + fontColorDefinition.getZ(), fontColorDefinition.getW()); + } + final FontDefinition font = frameDefinition.getFont("Font"); + this.fontParam.size = (int) convertY(viewport2, font.getFontSize()); + if (this.fontParam.size == 0) { + this.fontParam.size = 24; + } + frameFont = this.dynamicFontGeneratorHolder.getFontGenerator(font.getFontName()) + .generateFont(this.fontParam); + String textString = frameDefinition.getName(); + String text = frameDefinition.getString("Text"); + if (text != null) { + final String decoratedString = this.templates.getDecoratedString(text); + if (decoratedString != text) { + text = decoratedString; + } + textString = text; + } + final StringFrame stringFrame = new StringFrame(frameDefinition.getName(), parent, fontColor, justifyH, + justifyV, frameFont, textString, null, null); + inflatedFrame = stringFrame; + break; + case Texture: + final String file = frameDefinition.getString("File"); + final boolean decorateFileNames = frameDefinition.has("DecorateFileNames") || inDecorateFileNames; + final Vector4Definition texCoord = frameDefinition.getVector4("TexCoord"); + TextureFrame textureFrame; + final String alphaMode = frameDefinition.getString("AlphaMode"); + if ((alphaMode != null) && alphaMode.equals("ADD")) { + final FilterModeTextureFrame filterModeTextureFrame = new FilterModeTextureFrame( + frameDefinition.getName(), parent, decorateFileNames, texCoord); + filterModeTextureFrame.setFilterMode(FilterMode.ADDALPHA); + textureFrame = filterModeTextureFrame; + } + else { + textureFrame = new TextureFrame(frameDefinition.getName(), parent, decorateFileNames, texCoord); + } + textureFrame.setTexture(file, this); + inflatedFrame = textureFrame; + break; + default: + break; + } + if (inflatedFrame != null) { + if (frameDefinition.has("SetAllPoints")) { + inflatedFrame.setSetAllPoints(true); + } + Float width = frameDefinition.getFloat("Width"); + if (width != null) { + inflatedFrame.setWidth(convertX(viewport2, width)); + } + else { + width = frameDefinition.getFloat("TextLength"); + if (width != null) { + if (frameFont != null) { + inflatedFrame.setWidth(convertX(viewport2, width * frameFont.getSpaceWidth())); + } + } + } + final Float height = frameDefinition.getFloat("Height"); + if (height != null) { + inflatedFrame.setHeight(convertY(viewport2, height)); + } + for (final AnchorDefinition anchor : frameDefinition.getAnchors()) { + inflatedFrame.addAnchor(new AnchorDefinition(anchor.getMyPoint(), + convertX(this.viewport, anchor.getX()), convertY(this.viewport, anchor.getY()))); + } + for (final SetPointDefinition setPointDefinition : frameDefinition.getSetPoints()) { + final UIFrame otherFrameByName = getFrameByName(setPointDefinition.getOther(), + 0 /* TODO: createContext */); + if (otherFrameByName == null) { + System.err.println("Failing to pin " + frameDefinition.getName() + " to " + + setPointDefinition.getOther() + " because it was null!"); + if (PIN_FAIL_IS_FATAL) { + throw new IllegalStateException("Failing to pin " + frameDefinition.getName() + " to " + + setPointDefinition.getOther() + " because it was null!"); + } + } + else { + inflatedFrame.addSetPoint(new SetPoint(setPointDefinition.getMyPoint(), otherFrameByName, + setPointDefinition.getOtherPoint(), convertX(this.viewport, setPointDefinition.getX()), + convertY(this.viewport, setPointDefinition.getY()))); + } + } + this.nameToFrame.put(frameDefinition.getName(), inflatedFrame); + } + else { + // TODO in production throw some kind of exception here + } + return inflatedFrame; + } + + public void setSpriteFrameModel(final SpriteFrame spriteFrame, String backgroundArt) { + if (backgroundArt.toLowerCase().endsWith(".mdl") || backgroundArt.toLowerCase().endsWith(".mdx")) { + backgroundArt = backgroundArt.substring(0, backgroundArt.length() - 4); + } + backgroundArt += ".mdx"; + final MdxModel model = (MdxModel) this.modelViewer.load(backgroundArt, this.modelViewer.mapPathSolver, + this.modelViewer.solverParams); + spriteFrame.setModel(model); + } + + public UIFrame createFrameByType(final String typeName, final String name, final UIFrame owner, + final String inherits, final int createContext) { + // TODO idk what inherits is doing yet, and I didn't implement createContext yet + // even though it looked like just mapping/indexing on int + final FrameDefinition baseTemplateDef = this.templates.getFrame(name); + final FrameDefinition frameDefinition = new FrameDefinition(FrameClass.Frame, typeName, name); + if (baseTemplateDef != null) { + frameDefinition.inheritFrom(baseTemplateDef, "WITHCHILDREN".equals(inherits)); + } + final UIFrame inflatedFrame = inflate(frameDefinition, owner, null, frameDefinition.has("DecorateFileNames")); + if (this == owner) { + add(inflatedFrame); + } + return inflatedFrame; + } + + public UIFrame getFrameByName(final String name, final int createContext) { + return this.nameToFrame.get(name); + } + + public static float convertX(final Viewport viewport, final float fdfX) { + if (viewport instanceof ExtendViewport) { + return (fdfX / 0.8f) * ((ExtendViewport) viewport).getMinWorldWidth(); + } + return (fdfX / 0.8f) * viewport.getWorldWidth(); + } + + public static float convertY(final Viewport viewport, final float fdfY) { + if (viewport instanceof ExtendViewport) { + return (fdfY / 0.6f) * ((ExtendViewport) viewport).getMinWorldHeight(); + } + return (fdfY / 0.6f) * viewport.getWorldHeight(); + } + + public static float unconvertX(final Viewport viewport, final float nonFdfX) { + if (viewport instanceof ExtendViewport) { + return (nonFdfX / ((ExtendViewport) viewport).getMinWorldWidth()) * 0.8f; + } + return (nonFdfX / viewport.getWorldWidth()) * 0.8f; + } + + public static float unconvertY(final Viewport viewport, final float nonFdfY) { + if (viewport instanceof ExtendViewport) { + return (nonFdfY / ((ExtendViewport) viewport).getMinWorldHeight()) * 0.6f; + } + return (nonFdfY / viewport.getWorldHeight()) * 0.6f; + } + + public Texture loadTexture(String path) { + final int lastDotIndex = path.lastIndexOf('.'); + if (lastDotIndex == -1) { + path = path + ".blp"; + } + else { + path = path.substring(0, lastDotIndex) + ".blp"; + } + Texture texture = this.pathToTexture.get(path); + if (texture == null) { + try { + texture = ImageUtils.getAnyExtensionTexture(this.dataSource, path); + this.pathToTexture.put(path, texture); + } + catch (final Exception exc) { + + } + } + return texture; + } + + @Override + public final void positionBounds(final GameUI gameUI, final Viewport viewport) { + innerPositionBounds(this, viewport); + } + + @Override + protected void internalRender(final SpriteBatch batch, final BitmapFont baseFont, final GlyphLayout glyphLayout) { + super.internalRender(batch, baseFont, glyphLayout); + } + + @Override + public void add(final UIFrame childFrame) { + super.add(childFrame); + this.nameToFrame.put(childFrame.getName(), childFrame); + } + + public Scene getUiScene() { + return this.uiScene; + } + + public FrameTemplateEnvironment getTemplates() { + return this.templates; + } + + public String getErrorString(final String key) { + String errorString = this.errorStrings.getField(key, this.racialCommandIndex); + if (errorString.startsWith("TRIGSTR_")) { + errorString = this.mapStrings.get(Integer.parseInt(errorString.substring(8))); + } + return errorString; + } + + public GlyphLayout getGlyphLayout() { + return this.glyphLayout; + } + + public void setText(final StringFrame stringFrame, final String text) { + stringFrame.setText(text, this, this.viewport); + } + + public void setDecoratedText(final StringFrame stringFrame, final String text) { + stringFrame.setText(this.templates.getDecoratedString(text), this, this.viewport); + } + + public BitmapFont getFont() { + return this.font; + } + + public BitmapFont getFont20() { + return this.font20; + } + + public FreeTypeFontGenerator getFontGenerator() { + return this.fontGenerator; + } + + public void dispose() { + this.dynamicFontGeneratorHolder.dispose(); + } + + public FocusableFrame getNextFocusFrame() { + // TODO to support tabbing thru menus and stuff, we will have to implement this + return null; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/Main.java b/core/src/com/etheller/warsmash/parsers/fdf/Main.java new file mode 100644 index 0000000..02a6fc1 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/Main.java @@ -0,0 +1,55 @@ +package com.etheller.warsmash.parsers.fdf; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.Arrays; + +import com.etheller.warsmash.datasources.CompoundDataSourceDescriptor; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.datasources.DataSourceDescriptor; +import com.etheller.warsmash.datasources.FolderDataSourceDescriptor; +import com.etheller.warsmash.fdfparser.FDFParser; +import com.etheller.warsmash.fdfparser.FrameDefinitionVisitor; +import com.etheller.warsmash.parsers.fdf.datamodel.FrameDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.FrameTemplateEnvironment; + +public class Main { + public static final boolean REPORT_SYNTAX_ERRORS = true; + + public static void main(final String[] args) { + if (args.length < 1) { + System.err.println("Usage: "); + return; + } + try { + + final FolderDataSourceDescriptor war3mpq = new FolderDataSourceDescriptor( + "E:\\Backups\\Warcraft\\Data\\127"); + final FolderDataSourceDescriptor testingFolder = new FolderDataSourceDescriptor( + "E:\\Backups\\Warsmash\\Data"); + final FolderDataSourceDescriptor currentFolder = new FolderDataSourceDescriptor("."); + final DataSource dataSource = new CompoundDataSourceDescriptor( + Arrays.asList(war3mpq, testingFolder, currentFolder)).createDataSource(); + + final FrameTemplateEnvironment templates = new FrameTemplateEnvironment(); + final DataSourceFDFParserBuilder dataSourceFDFParserBuilder = new DataSourceFDFParserBuilder(dataSource); + final FrameDefinitionVisitor fdfVisitor = new FrameDefinitionVisitor(templates, dataSourceFDFParserBuilder); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(dataSource.getResourceAsStream("UI\\FrameDef\\FrameDef.toc")))) { + String line; + while ((line = reader.readLine()) != null) { + final FDFParser firstFileParser = dataSourceFDFParserBuilder.build(line); + fdfVisitor.visit(firstFileParser.program()); + } + } + + final FrameDefinition bnetChat = templates.getFrame("ConsoleUI"); + System.out.println("Value of ConsoleUI: " + bnetChat); + } + catch (final Exception exc) { + exc.printStackTrace(); + System.err.println(exc.getMessage()); + } + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/ModelExport.java b/core/src/com/etheller/warsmash/parsers/fdf/ModelExport.java new file mode 100644 index 0000000..243a455 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/ModelExport.java @@ -0,0 +1,38 @@ +package com.etheller.warsmash.parsers.fdf; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +import com.etheller.warsmash.datasources.CompoundDataSourceDescriptor; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.datasources.DataSourceDescriptor; +import com.etheller.warsmash.datasources.FolderDataSourceDescriptor; +import com.hiveworkshop.rms.parsers.mdlx.MdlxModel; +import com.hiveworkshop.rms.parsers.mdlx.util.MdxUtils; + +public class ModelExport { + + public static void main(final String[] args) { + + final FolderDataSourceDescriptor war3mpq = new FolderDataSourceDescriptor("E:\\Backups\\Warcraft\\Data\\127"); + final FolderDataSourceDescriptor testingFolder = new FolderDataSourceDescriptor("E:\\Backups\\Warsmash\\Data"); + final FolderDataSourceDescriptor currentFolder = new FolderDataSourceDescriptor("."); + final DataSource dataSource = new CompoundDataSourceDescriptor( + Arrays.asList(war3mpq, testingFolder, currentFolder)).createDataSource(); + + try (InputStream modelStream = dataSource + .getResourceAsStream("UI\\Glues\\MainMenu\\MainMenu3D\\MainMenu3D.mdx")) { + final MdlxModel model = new MdlxModel(dataSource.read("UI\\Glues\\MainMenu\\MainMenu3D\\MainMenu3D.mdx")); + try (FileOutputStream fos = new FileOutputStream(new File("C:\\Temp\\MainMenu3D.mdl"))) { + MdxUtils.saveMdl(model, fos); + } + } + catch (final IOException e) { + e.printStackTrace(); + } + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/AbstractRenderableFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/AbstractRenderableFrame.java new file mode 100644 index 0000000..418840f --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/AbstractRenderableFrame.java @@ -0,0 +1,397 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import java.util.EnumMap; + +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.datamodel.AnchorDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.FramePoint; + +public abstract class AbstractRenderableFrame implements UIFrame { + private static final FramePoint[] LEFT_ANCHOR_PRIORITY = { FramePoint.LEFT, FramePoint.TOPLEFT, + FramePoint.BOTTOMLEFT }; + private static final FramePoint[] RIGHT_ANCHOR_PRIORITY = { FramePoint.RIGHT, FramePoint.TOPRIGHT, + FramePoint.BOTTOMRIGHT }; + private static final FramePoint[] CENTER_HORIZ_ANCHOR_PRIORITY = { FramePoint.CENTER, FramePoint.TOP, + FramePoint.BOTTOM }; + private static final FramePoint[] CENTER_VERT_ANCHOR_PRIORITY = { FramePoint.CENTER, FramePoint.LEFT, + FramePoint.RIGHT }; + private static final FramePoint[] TOP_ANCHOR_PRIORITY = { FramePoint.TOP, FramePoint.TOPLEFT, FramePoint.TOPRIGHT }; + private static final FramePoint[] BOTTOM_ANCHOR_PRIORITY = { FramePoint.BOTTOM, FramePoint.BOTTOMLEFT, + FramePoint.BOTTOMRIGHT }; + private static final boolean DEBUG_LOG = false; + protected String name; + protected UIFrame parent; + protected boolean visible = true; + protected int level; + protected final Rectangle renderBounds = new Rectangle(0, 0, 0, 0); // in libgdx rendering space + private final EnumMap framePointToAssignment = new EnumMap<>(FramePoint.class); + protected float assignedHeight; + protected float assignedWidth; + + public AbstractRenderableFrame(final String name, final UIFrame parent) { + this.name = name; + this.parent = parent; + } + + @Override + public void setSetAllPoints(final boolean setAllPoints) { + for (final FramePoint framePoint : FramePoint.values()) { + if (!this.framePointToAssignment.containsKey(framePoint)) { + this.framePointToAssignment.put(framePoint, new SetPoint(framePoint, this.parent, framePoint, 0, 0)); + } + } + } + + @Override + public void setSetAllPoints(final boolean setAllPoints, final float inset) { + this.framePointToAssignment.put(FramePoint.TOPLEFT, + new SetPoint(FramePoint.TOPLEFT, this.parent, FramePoint.TOPLEFT, inset, -inset)); + this.framePointToAssignment.put(FramePoint.LEFT, + new SetPoint(FramePoint.LEFT, this.parent, FramePoint.LEFT, inset, 0)); + this.framePointToAssignment.put(FramePoint.BOTTOMLEFT, + new SetPoint(FramePoint.BOTTOMLEFT, this.parent, FramePoint.BOTTOMLEFT, inset, inset)); + this.framePointToAssignment.put(FramePoint.BOTTOM, + new SetPoint(FramePoint.BOTTOM, this.parent, FramePoint.BOTTOM, 0, inset)); + this.framePointToAssignment.put(FramePoint.BOTTOMRIGHT, + new SetPoint(FramePoint.BOTTOMRIGHT, this.parent, FramePoint.BOTTOMRIGHT, -inset, inset)); + this.framePointToAssignment.put(FramePoint.RIGHT, + new SetPoint(FramePoint.RIGHT, this.parent, FramePoint.RIGHT, -inset, 0)); + this.framePointToAssignment.put(FramePoint.TOPRIGHT, + new SetPoint(FramePoint.TOPRIGHT, this.parent, FramePoint.TOPRIGHT, -inset, -inset)); + this.framePointToAssignment.put(FramePoint.TOP, + new SetPoint(FramePoint.TOP, this.parent, FramePoint.TOP, 0, -inset)); + this.framePointToAssignment.put(FramePoint.CENTER, + new SetPoint(FramePoint.CENTER, this.parent, FramePoint.CENTER, 0, 0)); + } + + @Override + public void setWidth(final float width) { + this.assignedWidth = width; + this.renderBounds.width = width; + } + + @Override + public float getAssignedWidth() { + return this.assignedWidth; + } + + @Override + public float getAssignedHeight() { + return this.assignedHeight; + } + + @Override + public void setHeight(final float height) { + this.assignedHeight = height; + this.renderBounds.height = height; + } + + private FramePointAssignment getByPriority(final FramePoint[] priorities) { + for (final FramePoint priorityFramePoint : priorities) { + final FramePointAssignment framePointAssignment = this.framePointToAssignment.get(priorityFramePoint); + if (framePointAssignment != null) { + return framePointAssignment; + } + } + return null; + } + + public void clearFramePointAssignments() { + this.framePointToAssignment.clear(); + } + + private FramePointAssignment getLeftAnchor() { + return getByPriority(LEFT_ANCHOR_PRIORITY); + } + + private FramePointAssignment getRightAnchor() { + return getByPriority(RIGHT_ANCHOR_PRIORITY); + } + + private FramePointAssignment getTopAnchor() { + return getByPriority(TOP_ANCHOR_PRIORITY); + } + + private FramePointAssignment getBottomAnchor() { + return getByPriority(BOTTOM_ANCHOR_PRIORITY); + } + + private FramePointAssignment getCenterHorizontalAnchor() { + return getByPriority(CENTER_HORIZ_ANCHOR_PRIORITY); + } + + private FramePointAssignment getCenterVerticalAnchor() { + return getByPriority(CENTER_VERT_ANCHOR_PRIORITY); + } + + @Override + public float getFramePointX(final FramePoint framePoint) { + switch (framePoint) { + case CENTER: + case BOTTOM: + case TOP: + return this.renderBounds.x + (this.renderBounds.width / 2); + case BOTTOMLEFT: + case LEFT: + case TOPLEFT: + return this.renderBounds.x; + case BOTTOMRIGHT: + case RIGHT: + case TOPRIGHT: + return this.renderBounds.x + this.renderBounds.width; + default: + return 0; + } + } + + @Override + public void setFramePointX(final FramePoint framePoint, final float x) { + if (this.renderBounds.width == 0) { + this.renderBounds.x = x; + return; + } + switch (framePoint) { + case CENTER: + case BOTTOM: + case TOP: + this.renderBounds.x = x - (this.renderBounds.width / 2); + return; + case BOTTOMLEFT: + case LEFT: + case TOPLEFT: + if (getRightAnchor() != null) { + final float oldRightX = this.renderBounds.x + this.renderBounds.width; + this.renderBounds.x = x; + this.renderBounds.width = oldRightX - x; + } + else { + // no right anchor, keep width + this.renderBounds.x = x; + } + return; + case BOTTOMRIGHT: + case RIGHT: + case TOPRIGHT: + if (getLeftAnchor() != null) { + this.renderBounds.width = x - this.renderBounds.x; + } + else { + this.renderBounds.x = x - this.renderBounds.width; + } + return; + default: + return; + } + } + + @Override + public float getFramePointY(final FramePoint framePoint) { + switch (framePoint) { + case LEFT: + case CENTER: + case RIGHT: + return this.renderBounds.y + (this.renderBounds.height / 2); + case BOTTOMLEFT: + case BOTTOM: + case BOTTOMRIGHT: + return this.renderBounds.y; + case TOPLEFT: + case TOP: + case TOPRIGHT: + return this.renderBounds.y + this.renderBounds.height; + default: + return 0; + } + } + + @Override + public void setFramePointY(final FramePoint framePoint, final float y) { + if (this.renderBounds.height == 0) { + this.renderBounds.y = y; + return; + } + switch (framePoint) { + case LEFT: + case CENTER: + case RIGHT: + this.renderBounds.y = y - (this.renderBounds.height / 2); + return; + case TOPLEFT: + case TOP: + case TOPRIGHT: + if (getBottomAnchor() != null) { + this.renderBounds.height = y - this.renderBounds.y; + } + else { + this.renderBounds.y = y - this.renderBounds.height; + } + return; + case BOTTOMLEFT: + case BOTTOM: + case BOTTOMRIGHT: + if (getTopAnchor() != null) { + final float oldBottomY = this.renderBounds.y + this.renderBounds.height; + this.renderBounds.y = y; + this.renderBounds.height = oldBottomY - y; + } + else { + this.renderBounds.y = y; + } + return; + default: + return; + } + } + + @Override + public void addAnchor(final AnchorDefinition anchorDefinition) { + this.framePointToAssignment.put(anchorDefinition.getMyPoint(), new SetPoint(anchorDefinition.getMyPoint(), + this.parent, anchorDefinition.getMyPoint(), anchorDefinition.getX(), anchorDefinition.getY())); + } + + @Override + public void addSetPoint(final SetPoint setPointDefinition) { + this.framePointToAssignment.put(setPointDefinition.getMyPoint(), setPointDefinition); + } + + @Override + public void positionBounds(final GameUI gameUI, final Viewport viewport) { + if (this.parent == null) { + // TODO this is a bit of a hack, remove later + return; + } + if (this.framePointToAssignment.isEmpty()) { + this.renderBounds.x = this.parent.getFramePointX(FramePoint.LEFT); + this.renderBounds.y = this.parent.getFramePointY(FramePoint.BOTTOM); + } + else { + final FramePointAssignment leftAnchor = getLeftAnchor(); + final FramePointAssignment rightAnchor = getRightAnchor(); + final FramePointAssignment topAnchor = getTopAnchor(); + final FramePointAssignment bottomAnchor = getBottomAnchor(); + final FramePointAssignment centerHorizontalAnchor = getCenterHorizontalAnchor(); + final FramePointAssignment centerVerticalAnchor = getCenterVerticalAnchor(); + if (leftAnchor != null) { + this.renderBounds.x = leftAnchor.getX(gameUI, viewport); + if (this.assignedWidth == 0) { + if (rightAnchor != null) { + this.renderBounds.width = rightAnchor.getX(gameUI, viewport) - this.renderBounds.x; + } + else if (centerHorizontalAnchor != null) { + this.renderBounds.width = (centerHorizontalAnchor.getX(gameUI, viewport) - this.renderBounds.x) + * 2; + } + } + } + else if (rightAnchor != null) { + this.renderBounds.x = rightAnchor.getX(gameUI, viewport) - this.renderBounds.width; + if (centerHorizontalAnchor != null) { + this.renderBounds.width = (this.renderBounds.x - centerHorizontalAnchor.getX(gameUI, viewport)) * 2; + } + } + else if (centerHorizontalAnchor != null) { + this.renderBounds.x = centerHorizontalAnchor.getX(gameUI, viewport) - (this.renderBounds.width / 2); + } + if (bottomAnchor != null) { + this.renderBounds.y = bottomAnchor.getY(gameUI, viewport); + if (this.assignedHeight == 0) { + if (topAnchor != null) { + this.renderBounds.height = topAnchor.getY(gameUI, viewport) - this.renderBounds.y; + } + else if (centerVerticalAnchor != null) { + this.renderBounds.height = (centerVerticalAnchor.getY(gameUI, viewport) - this.renderBounds.y) + * 2; + } + } + } + else if (topAnchor != null) { + this.renderBounds.y = topAnchor.getY(gameUI, viewport) - this.renderBounds.height; + if (centerVerticalAnchor != null) { + this.renderBounds.height = (this.renderBounds.y - centerVerticalAnchor.getY(gameUI, viewport)) * 2; + } + } + else if (centerVerticalAnchor != null) { + this.renderBounds.y = centerVerticalAnchor.getY(gameUI, viewport) - (this.renderBounds.height / 2); + } + } + if (DEBUG_LOG) { + System.out.println(getClass().getSimpleName() + ":" + this.name + ":" + hashCode() + + " finishing position bounds: " + this.renderBounds); + } + innerPositionBounds(gameUI, viewport); + } + + protected abstract void innerPositionBounds(GameUI gameUI, final Viewport viewport); + + @Override + public boolean isVisible() { + return this.visible; + } + + public int getLevel() { + return this.level; + } + + @Override + public void setVisible(final boolean visible) { + this.visible = visible; + } + + public void setLevel(final int level) { + this.level = level; + } + + @Override + public final void render(final SpriteBatch batch, final BitmapFont font20, final GlyphLayout glyphLayout) { + if (this.visible) { + internalRender(batch, font20, glyphLayout); + } + } + + @Override + public UIFrame getParent() { + return this.parent; + } + + @Override + public boolean isVisibleOnScreen() { + boolean visibleOnScreen = this.visible; + UIFrame ancestor = this.parent; + while (visibleOnScreen && (ancestor != null)) { + visibleOnScreen &= ancestor.isVisible(); + ancestor = ancestor.getParent(); + } + return visibleOnScreen; + } + + protected abstract void internalRender(SpriteBatch batch, BitmapFont baseFont, GlyphLayout glyphLayout); + + @Override + public UIFrame touchDown(final float screenX, final float screenY, final int button) { + return null; + } + + @Override + public UIFrame touchUp(final float screenX, final float screenY, final int button) { + return null; + } + + @Override + public UIFrame getFrameChildUnderMouse(final float screenX, final float screenY) { + return null; + } + + @Override + public String getName() { + return this.name; + } + + public Rectangle getRenderBounds() { + return this.renderBounds; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/AbstractUIFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/AbstractUIFrame.java new file mode 100644 index 0000000..5727e28 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/AbstractUIFrame.java @@ -0,0 +1,90 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import java.util.ArrayList; +import java.util.List; + +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; + +public abstract class AbstractUIFrame extends AbstractRenderableFrame implements UIFrame { + private final List childFrames = new ArrayList<>(); + + public void add(final UIFrame childFrame) { + if (childFrame == null) { + return; + } + this.childFrames.add(childFrame); + } + + public void remove(final UIFrame childFrame) { + if (childFrame == null) { + return; + } + this.childFrames.remove(childFrame); + } + + public AbstractUIFrame(final String name, final UIFrame parent) { + super(name, parent); + } + + @Override + public void setVisible(final boolean visible) { + super.setVisible(visible); + } + + @Override + protected void internalRender(final SpriteBatch batch, final BitmapFont baseFont, final GlyphLayout glyphLayout) { + for (final UIFrame childFrame : this.childFrames) { + childFrame.render(batch, baseFont, glyphLayout); + } + } + + @Override + protected void innerPositionBounds(final GameUI gameUI, final Viewport viewport) { + for (final UIFrame childFrame : this.childFrames) { + childFrame.positionBounds(gameUI, viewport); + } + } + + @Override + public UIFrame touchDown(final float screenX, final float screenY, final int button) { + if (isVisible()) { + for (final UIFrame childFrame : this.childFrames) { + final UIFrame clickedChild = childFrame.touchDown(screenX, screenY, button); + if (clickedChild != null) { + return clickedChild; + } + } + } + return super.touchDown(screenX, screenY, button); + } + + @Override + public UIFrame touchUp(final float screenX, final float screenY, final int button) { + if (isVisible()) { + for (final UIFrame childFrame : this.childFrames) { + final UIFrame clickedChild = childFrame.touchUp(screenX, screenY, button); + if (clickedChild != null) { + return clickedChild; + } + } + } + return super.touchUp(screenX, screenY, button); + } + + @Override + public UIFrame getFrameChildUnderMouse(final float screenX, final float screenY) { + if (isVisible()) { + for (final UIFrame childFrame : this.childFrames) { + final UIFrame clickedChild = childFrame.getFrameChildUnderMouse(screenX, screenY); + if (clickedChild != null) { + return clickedChild; + } + } + } + return super.getFrameChildUnderMouse(screenX, screenY); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/AnchorPoint.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/AnchorPoint.java new file mode 100644 index 0000000..01566c9 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/AnchorPoint.java @@ -0,0 +1,36 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.datamodel.FramePoint; + +public class AnchorPoint implements FramePointAssignment { + private final FramePoint framePoint; + private final float x; + private final float y; + + public AnchorPoint(final FramePoint framePoint, final float x, final float y) { + this.framePoint = framePoint; + this.x = x; + this.y = y; + } + + public float getX() { + return this.x; + } + + public float getY() { + return this.y; + } + + @Override + public float getX(final GameUI gameUI, final Viewport uiViewport) { + return gameUI.getFramePointX(this.framePoint) + this.x; + } + + @Override + public float getY(final GameUI gameUI, final Viewport uiViewport) { + return gameUI.getFramePointY(this.framePoint) + this.y; + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/BackdropFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/BackdropFrame.java new file mode 100644 index 0000000..9e59928 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/BackdropFrame.java @@ -0,0 +1,197 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import java.util.EnumSet; + +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.etheller.warsmash.parsers.fdf.datamodel.BackdropCornerFlags; +import com.etheller.warsmash.parsers.fdf.datamodel.Vector4Definition; + +public class BackdropFrame extends AbstractUIFrame { + private final boolean decorateFileNames; + private final boolean tileBackground; + private final Texture background; + private final EnumSet cornerFlags; + private final float cornerSize; + private float backgroundSize; + private final Vector4Definition backgroundInsets; + private final Texture edgeFile; + private final float edgeFileWidth; + private final float edgeUVWidth; + private final float edgeFileHeight; + private final float edgeUVHeight; + private final boolean mirrored; + + public BackdropFrame(final String name, final UIFrame parent, final boolean decorateFileNames, + final boolean tileBackground, final Texture background, final EnumSet cornerFlags, + final float cornerSize, final float backgroundSize, final Vector4Definition backgroundInsets, + final Texture edgeFile, final boolean mirrored) { + super(name, parent); + this.decorateFileNames = decorateFileNames; + this.tileBackground = tileBackground && (backgroundSize > 0); + this.background = background; + this.cornerFlags = cornerFlags; + this.cornerSize = cornerSize; + this.backgroundSize = backgroundSize; + this.backgroundInsets = backgroundInsets; + this.edgeFile = edgeFile; + this.edgeFileWidth = edgeFile == null ? 0.0f : edgeFile.getWidth(); + this.edgeFileHeight = edgeFile == null ? 0.0f : edgeFile.getHeight(); + this.edgeUVWidth = 1f / 8f; + this.edgeUVHeight = 1f; + this.mirrored = mirrored; + this.backgroundSize -= (backgroundInsets.getX() + backgroundInsets.getY() + backgroundInsets.getZ() + + backgroundInsets.getW()) / 2f; + } + + @Override + protected void internalRender(final SpriteBatch batch, final BitmapFont baseFont, final GlyphLayout glyphLayout) { + if (this.background != null) { + final float backgroundX = this.renderBounds.x + this.backgroundInsets.getX(); + final float backgroundY = this.renderBounds.y + this.backgroundInsets.getY(); + final float backgroundWidth = this.renderBounds.width - this.backgroundInsets.getX() + - this.backgroundInsets.getZ(); + final float backgroundHeight = this.renderBounds.height - this.backgroundInsets.getY() + - this.backgroundInsets.getW(); + if (this.tileBackground) { + final float backgroundVerticalRepeatCount = (backgroundHeight / this.backgroundSize); + final float backgroundHorizontalRepeatCount = backgroundWidth / this.backgroundSize; + final float backgroundHeightRemainder = backgroundHeight % this.backgroundSize; + final float backgroundHeightRemainderRatio = backgroundHeightRemainder / this.backgroundSize; + final float backgroundWidthRemainder = backgroundWidth % this.backgroundSize; + final float backgroundWidthRemainderRatio = backgroundWidthRemainder / this.backgroundSize; + final int backgroundVerticalFloorRepeatCount = (int) Math.floor(backgroundVerticalRepeatCount); + final int backgroundHorizontalFloorRepeatCount = (int) Math.floor(backgroundHorizontalRepeatCount); + for (int j = 0; j < backgroundVerticalFloorRepeatCount; j++) { + for (int i = 0; i < backgroundHorizontalFloorRepeatCount; i++) { + batch.draw(this.background, backgroundX + (i * this.backgroundSize), + backgroundY + (j * this.backgroundSize), this.backgroundSize, this.backgroundSize); + } + batch.draw(this.background, + backgroundX + ((backgroundHorizontalFloorRepeatCount) * this.backgroundSize), + backgroundY + (j * this.backgroundSize), backgroundWidthRemainder, this.backgroundSize, 0, + 1.0f, backgroundWidthRemainderRatio, 0); + } + for (int i = 0; i < backgroundHorizontalFloorRepeatCount; i++) { + batch.draw(this.background, backgroundX + (i * this.backgroundSize), + backgroundY + (backgroundVerticalFloorRepeatCount * this.backgroundSize), + this.backgroundSize, backgroundHeightRemainder, 0, 1.0f, 1.0f, + backgroundHeightRemainderRatio); + } + batch.draw(this.background, + backgroundX + ((backgroundHorizontalFloorRepeatCount) * this.backgroundSize), + backgroundY + (backgroundVerticalFloorRepeatCount * this.backgroundSize), + backgroundWidthRemainder, backgroundHeightRemainder, 0, 1.0f, backgroundWidthRemainderRatio, + backgroundHeightRemainderRatio); + } + else { + if (this.mirrored) { + batch.draw(this.background, backgroundX, backgroundY, backgroundWidth, backgroundHeight, 0, 0, + this.background.getWidth(), this.background.getHeight(), true, false); + } + else { + batch.draw(this.background, backgroundX, backgroundY, backgroundWidth, backgroundHeight); + } + } + } + if (this.edgeFile != null) { + if (this.cornerFlags.contains(BackdropCornerFlags.BL)) { + batch.draw(this.edgeFile, this.renderBounds.x, this.renderBounds.y, this.cornerSize, this.cornerSize, + this.edgeUVWidth * 6, this.edgeUVHeight, this.edgeUVWidth * 7, 0); + } + if (this.cornerFlags.contains(BackdropCornerFlags.BR)) { + batch.draw(this.edgeFile, (this.renderBounds.x + this.renderBounds.width) - this.cornerSize, + this.renderBounds.y, this.cornerSize, this.cornerSize, this.edgeUVWidth * 7, this.edgeUVHeight, + this.edgeUVWidth * 8, 0); + } + if (this.cornerFlags.contains(BackdropCornerFlags.UL)) { + batch.draw(this.edgeFile, this.renderBounds.x, + this.renderBounds.y + (this.renderBounds.height - this.cornerSize), this.cornerSize, + this.cornerSize, this.edgeUVWidth * 4, this.edgeUVHeight, this.edgeUVWidth * 5, 0); + } + if (this.cornerFlags.contains(BackdropCornerFlags.UR)) { + batch.draw(this.edgeFile, (this.renderBounds.x + this.renderBounds.width) - this.cornerSize, + this.renderBounds.y + (this.renderBounds.height - this.cornerSize), this.cornerSize, + this.cornerSize, this.edgeUVWidth * 5, this.edgeUVHeight, this.edgeUVWidth * 6, 0); + } + final float borderVerticalRepeatCount = (this.renderBounds.height / this.cornerSize); + final float heightRemainder = this.renderBounds.height % this.cornerSize; + final float heightRemainderRatio = heightRemainder / this.cornerSize; + final int borderVerticalRepeatCountLessOne = (int) (borderVerticalRepeatCount - 1); + if (this.cornerFlags.contains(BackdropCornerFlags.L)) { + for (int i = 1; i < borderVerticalRepeatCountLessOne; i++) { + batch.draw(this.edgeFile, this.renderBounds.x, this.renderBounds.y + (this.cornerSize * i), + this.cornerSize, this.cornerSize, this.edgeUVWidth * 0, this.edgeUVHeight, + this.edgeUVWidth * 1, 0); + } + if (borderVerticalRepeatCountLessOne > 0) { + batch.draw(this.edgeFile, this.renderBounds.x, + this.renderBounds.y + (this.cornerSize * borderVerticalRepeatCountLessOne), this.cornerSize, + heightRemainder, this.edgeUVWidth * 0, heightRemainderRatio, this.edgeUVWidth * 1, 0); + } + } + if (this.cornerFlags.contains(BackdropCornerFlags.R)) { + for (int i = 1; i < borderVerticalRepeatCountLessOne; i++) { + batch.draw(this.edgeFile, (this.renderBounds.x + this.renderBounds.width) - this.cornerSize, + this.renderBounds.y + (this.cornerSize * i), this.cornerSize, this.cornerSize, + this.edgeUVWidth * 1, this.edgeUVHeight, this.edgeUVWidth * 2, 0); + } + if (borderVerticalRepeatCountLessOne > 0) { + batch.draw(this.edgeFile, (this.renderBounds.x + this.renderBounds.width) - this.cornerSize, + this.renderBounds.y + (this.cornerSize * borderVerticalRepeatCountLessOne), this.cornerSize, + heightRemainder, this.edgeUVWidth * 1, heightRemainderRatio, this.edgeUVWidth * 2, 0); + } + } + + final float borderHorizontalRepeatCount = this.renderBounds.width / this.cornerSize; + final float widthRemainder = (this.renderBounds.width % this.cornerSize) / this.cornerSize; + final int widthRemainderByHeight = (int) (widthRemainder * this.edgeFileHeight); + final int widthRemainderByCornerSize = (int) (widthRemainder * this.cornerSize); + final int borderHorizontalRepeatCountLessOne = (int) (borderHorizontalRepeatCount - 1); + final float halfPi = 270; + if (this.cornerFlags.contains(BackdropCornerFlags.B)) { + for (int i = 1; i < borderHorizontalRepeatCountLessOne; i++) { + batch.draw(this.edgeFile, this.renderBounds.x + (this.cornerSize * i), this.renderBounds.y, + this.cornerSize / 2, this.cornerSize / 2, this.cornerSize, this.cornerSize, 1.0f, 1.0f, + halfPi, (int) ((this.edgeFileWidth * 3f) / 8f), 0, (int) (this.edgeFileWidth / 8), + (int) this.edgeFileHeight, false, false); + } + if (borderHorizontalRepeatCountLessOne > 0) { + batch.draw(this.edgeFile, + (this.renderBounds.x + (this.cornerSize * borderHorizontalRepeatCountLessOne)) + - ((this.cornerSize - widthRemainderByCornerSize) / 2), + this.renderBounds.y + ((this.cornerSize - widthRemainderByCornerSize) / 2), + this.cornerSize / 2, widthRemainderByCornerSize / 2, this.cornerSize, + widthRemainderByCornerSize, 1.0f, 1.0f, halfPi, (int) ((this.edgeFileWidth * 3f) / 8f), 0, + (int) (this.edgeFileWidth / 8), widthRemainderByHeight, false, false); + } + } + if (this.cornerFlags.contains(BackdropCornerFlags.T)) { + for (int i = 1; i < borderHorizontalRepeatCountLessOne; i++) { + batch.draw(this.edgeFile, this.renderBounds.x + (this.cornerSize * i), + this.renderBounds.y + (this.renderBounds.height - this.cornerSize), this.cornerSize / 2, + this.cornerSize / 2, this.cornerSize, this.cornerSize, 1.0f, 1.0f, halfPi, + (int) ((this.edgeFileWidth * 2f) / 8f), 0, (int) (this.edgeFileWidth / 8), + (int) this.edgeFileHeight, false, false); + } + if (borderHorizontalRepeatCountLessOne > 0) { + batch.draw(this.edgeFile, + (this.renderBounds.x + (this.cornerSize * borderHorizontalRepeatCountLessOne)) + - ((this.cornerSize - widthRemainderByCornerSize) / 2), + this.renderBounds.y + + (this.renderBounds.height - ((this.cornerSize + widthRemainderByCornerSize) / 2)), + this.cornerSize / 2, widthRemainderByCornerSize / 2, this.cornerSize, + widthRemainderByCornerSize, 1.0f, 1.0f, halfPi, (int) ((this.edgeFileWidth * 2f) / 8f), 0, + (int) (this.edgeFileWidth / 8), widthRemainderByHeight, false, false); + } + } + } + super.internalRender(batch, baseFont, glyphLayout); + } + + public float getCornerSize() { + return this.cornerSize; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/ControlFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/ControlFrame.java new file mode 100644 index 0000000..dd6a309 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/ControlFrame.java @@ -0,0 +1,35 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; + +public class ControlFrame extends AbstractRenderableFrame { + + private UIFrame controlBackdrop; + + public ControlFrame(final String name, final UIFrame parent) { + super(name, parent); + } + + public void setControlBackdrop(final UIFrame controlBackdrop) { + this.controlBackdrop = controlBackdrop; + } + + public UIFrame getControlBackdrop() { + return this.controlBackdrop; + } + + @Override + protected void innerPositionBounds(final GameUI gameUI, final Viewport viewport) { + this.controlBackdrop.positionBounds(gameUI, viewport); + } + + @Override + protected void internalRender(final SpriteBatch batch, final BitmapFont baseFont, final GlyphLayout glyphLayout) { + this.controlBackdrop.render(batch, baseFont, glyphLayout); + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/EditBoxFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/EditBoxFrame.java new file mode 100644 index 0000000..db1fbcd --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/EditBoxFrame.java @@ -0,0 +1,179 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import com.badlogic.gdx.Input; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.utils.TimeUtils; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.datamodel.FramePoint; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.FocusableFrame; + +public class EditBoxFrame extends AbstractRenderableFrame implements FocusableFrame { + + private UIFrame controlBackdrop; + private final float editBorderSize; + private final Color editCursorColor; + private StringFrame editTextFrame; + private boolean focused = false; + private int cursorIndex; + + // TODO design in such a way that references to these are not held !! very bad + // code design here + private GameUI gameUI; + private Viewport viewport; + private GlyphLayout glyphLayout; + private Runnable onChange; + + public EditBoxFrame(final String name, final UIFrame parent, final float editBorderSize, + final Color editCursorColor) { + super(name, parent); + this.editBorderSize = editBorderSize; + this.editCursorColor = editCursorColor; + } + + public void setControlBackdrop(final UIFrame controlBackdrop) { + this.controlBackdrop = controlBackdrop; + } + + @Override + protected void innerPositionBounds(final GameUI gameUI, final Viewport viewport) { + this.gameUI = gameUI; + this.viewport = viewport; + this.controlBackdrop.positionBounds(gameUI, viewport); + this.editTextFrame.positionBounds(gameUI, viewport); + } + + @Override + protected void internalRender(final SpriteBatch batch, final BitmapFont baseFont, final GlyphLayout glyphLayout) { + this.glyphLayout = glyphLayout; + this.controlBackdrop.render(batch, baseFont, glyphLayout); + this.editTextFrame.render(batch, baseFont, glyphLayout); + if (this.focused) { + final long time = TimeUtils.millis(); + if ((time % 500) > 250) { + final BitmapFont frameFont = this.editTextFrame.getFrameFont(); + frameFont.setColor(this.editCursorColor); + final int cursorRenderPosition = Math.min(this.cursorIndex, this.editTextFrame.getText().length()); + this.cursorIndex = cursorRenderPosition; + glyphLayout.setText(frameFont, this.editTextFrame.getText().substring(0, cursorRenderPosition)); + final float cursorXOffset = glyphLayout.width; + glyphLayout.setText(frameFont, "|"); + frameFont.draw(batch, "|", + (this.editTextFrame.getFramePointX(FramePoint.LEFT) + cursorXOffset) - (glyphLayout.width / 2), + this.editTextFrame.getFramePointY(FramePoint.LEFT) + ((frameFont.getCapHeight()) / 2)); + } + } + } + + public void setEditTextFrame(final StringFrame editTextFrame) { + this.editTextFrame = editTextFrame; + } + + @Override + public boolean isFocusable() { + return true; + } + + @Override + public void onFocusGained() { + this.focused = true; + } + + @Override + public void onFocusLost() { + this.focused = false; + } + + @Override + public boolean keyDown(final int keycode) { + switch (keycode) { + case Input.Keys.LEFT: { + this.cursorIndex = Math.max(0, this.cursorIndex - 1); + break; + } + case Input.Keys.RIGHT: { + final String text = this.editTextFrame.getText(); + this.cursorIndex = Math.min(text.length(), this.cursorIndex + 1); + break; + } + case Input.Keys.BACKSPACE: { + final String prevText = this.editTextFrame.getText(); + final int prevTextLength = prevText.length(); + final int cursorIndex = Math.min(this.cursorIndex, prevTextLength); + if (cursorIndex >= 1) { + this.cursorIndex = cursorIndex - 1; + final String newText = prevText.substring(0, cursorIndex - 1) + + prevText.substring(cursorIndex, prevTextLength); + this.editTextFrame.setText(newText, this.gameUI, this.viewport); + if (this.onChange != null) { + this.onChange.run(); + } + } + break; + } + } + return false; + } + + @Override + public boolean keyUp(final int keycode) { + return false; + } + + @Override + public boolean keyTyped(final char character) { + if (Character.isAlphabetic(character) || Character.isDigit(character)) { + final String prevText = this.editTextFrame.getText(); + final int prevTextLength = prevText.length(); + final int cursorIndex = Math.min(this.cursorIndex, prevTextLength); + final String newText = prevText.substring(0, cursorIndex) + character + + prevText.substring(cursorIndex, prevTextLength); + this.editTextFrame.setText(newText, this.gameUI, this.viewport); + this.cursorIndex++; + if (this.onChange != null) { + this.onChange.run(); + } + } + return false; + } + + @Override + public UIFrame touchDown(final float screenX, final float screenY, final int button) { + if (isVisible() && this.renderBounds.contains(screenX, screenY)) { + + final String text = this.editTextFrame.getText(); + int indexFound = -1; + final float fpXOfEditText = this.editTextFrame.getFramePointX(FramePoint.LEFT); + float lastX = 0; + for (int i = 0; i < text.length(); i++) { + final BitmapFont frameFont = this.editTextFrame.getFrameFont(); + this.glyphLayout.setText(frameFont, this.editTextFrame.getText().substring(0, i)); + final float x = fpXOfEditText + this.glyphLayout.width; + if (((x + lastX) / 2) > screenX) { + indexFound = i - 1; + break; + } + lastX = x; + } + if (indexFound == -1) { + indexFound = text.length(); + } + this.cursorIndex = indexFound; + + return this; + } + return super.touchDown(screenX, screenY, button); + } + + public String getText() { + return this.editTextFrame.getText(); + } + + public void setOnChange(final Runnable onChange) { + this.onChange = onChange; + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/FilterModeTextureFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/FilterModeTextureFrame.java new file mode 100644 index 0000000..3b72c3a --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/FilterModeTextureFrame.java @@ -0,0 +1,33 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.etheller.warsmash.parsers.fdf.datamodel.Vector4Definition; +import com.hiveworkshop.rms.parsers.mdlx.MdlxLayer.FilterMode; + +public class FilterModeTextureFrame extends TextureFrame { + private int blendSrc; + private int blendDst; + + public FilterModeTextureFrame(final String name, final UIFrame parent, final boolean decorateFileNames, + final Vector4Definition texCoord) { + super(name, parent, decorateFileNames, texCoord); + } + + @Override + protected void internalRender(final SpriteBatch batch, final BitmapFont baseFont, final GlyphLayout glyphLayout) { + final int blendDstFunc = batch.getBlendDstFunc(); + final int blendSrcFunc = batch.getBlendSrcFunc(); + batch.setBlendFunction(this.blendSrc, this.blendDst); + super.internalRender(batch, baseFont, glyphLayout); + batch.setBlendFunction(blendSrcFunc, blendDstFunc); + } + + public void setFilterMode(final FilterMode filterMode) { + final int[] layerFilterMode = com.etheller.warsmash.viewer5.handlers.mdx.FilterMode.layerFilterMode(filterMode); + this.blendSrc = layerFilterMode[0]; + this.blendDst = layerFilterMode[1]; + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/FramePointAssignment.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/FramePointAssignment.java new file mode 100644 index 0000000..66706a7 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/FramePointAssignment.java @@ -0,0 +1,10 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; + +public interface FramePointAssignment { + float getX(GameUI gameUI, Viewport uiViewport); + + float getY(GameUI gameUI, Viewport uiViewport); +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/GlueButtonFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/GlueButtonFrame.java new file mode 100644 index 0000000..f71be58 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/GlueButtonFrame.java @@ -0,0 +1,166 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.ClickableFrame; + +public class GlueButtonFrame extends AbstractRenderableFrame implements ClickableFrame { + private UIFrame controlBackdrop; + private UIFrame controlPushedBackdrop; + private UIFrame controlDisabledBackdrop; + private UIFrame controlMouseOverHighlight; + + private boolean enabled = true; + private boolean highlightOnMouseOver; + private boolean mouseOver = false; + + private UIFrame activeChild; + + private Runnable onClick; + + public GlueButtonFrame(final String name, final UIFrame parent) { + super(name, parent); + } + + public void setControlBackdrop(final UIFrame controlBackdrop) { + this.controlBackdrop = controlBackdrop; + if (this.activeChild == null) { + this.activeChild = controlBackdrop; + } + } + + public void setControlPushedBackdrop(final UIFrame controlPushedBackdrop) { + this.controlPushedBackdrop = controlPushedBackdrop; + } + + public void setControlDisabledBackdrop(final UIFrame controlDisabledBackdrop) { + this.controlDisabledBackdrop = controlDisabledBackdrop; + } + + public void setControlMouseOverHighlight(final UIFrame controlMouseOverHighlight) { + this.controlMouseOverHighlight = controlMouseOverHighlight; + } + + public void setEnabled(final boolean enabled) { + this.enabled = enabled; + if (this.enabled) { + this.activeChild = this.controlBackdrop; + } + else { + this.activeChild = this.controlDisabledBackdrop; + } + } + + public boolean isEnabled() { + return this.enabled; + } + + public void setHighlightOnMouseOver(final boolean highlightOnMouseOver) { + this.highlightOnMouseOver = highlightOnMouseOver; + } + + public void setOnClick(final Runnable onClick) { + this.onClick = onClick; + } + + @Override + protected void innerPositionBounds(final GameUI gameUI, final Viewport viewport) { + if (this.controlBackdrop != null) { + this.controlBackdrop.positionBounds(gameUI, viewport); + } + if (this.controlPushedBackdrop != null) { + this.controlPushedBackdrop.positionBounds(gameUI, viewport); + } + if (this.controlDisabledBackdrop != null) { + this.controlDisabledBackdrop.positionBounds(gameUI, viewport); + } + if (this.controlMouseOverHighlight != null) { + this.controlMouseOverHighlight.positionBounds(gameUI, viewport); + } + if (this.enabled) { + this.activeChild = this.controlBackdrop; + } + else { + this.activeChild = this.controlDisabledBackdrop; + } + } + + @Override + protected void internalRender(final SpriteBatch batch, final BitmapFont baseFont, final GlyphLayout glyphLayout) { + if (this.activeChild != null) { + this.activeChild.render(batch, baseFont, glyphLayout); + } + if (this.mouseOver) { + this.controlMouseOverHighlight.render(batch, baseFont, glyphLayout); + } + } + + @Override + public void mouseDown(final GameUI gameUI, final Viewport uiViewport) { + if (this.enabled) { + this.activeChild = this.controlPushedBackdrop; + } + } + + @Override + public void mouseUp(final GameUI gameUI, final Viewport uiViewport) { + if (this.enabled) { + this.activeChild = this.controlBackdrop; + } + } + + @Override + public void mouseEnter(final GameUI gameUI, final Viewport uiViewport) { + if (this.highlightOnMouseOver) { + this.mouseOver = true; + onMouseEnter(); + } + } + + protected void onMouseEnter() { + } + + @Override + public void mouseExit(final GameUI gameUI, final Viewport uiViewport) { + this.mouseOver = false; + onMouseExit(); + } + + protected void onMouseExit() { + } + + @Override + public void onClick(final int button) { + if (this.onClick != null) { + this.onClick.run(); + } + } + + @Override + public UIFrame touchUp(final float screenX, final float screenY, final int button) { + if (isVisible() && this.enabled && this.renderBounds.contains(screenX, screenY)) { + return this; + } + return super.touchUp(screenX, screenY, button); + } + + @Override + public UIFrame touchDown(final float screenX, final float screenY, final int button) { + if (isVisible() && this.enabled && this.renderBounds.contains(screenX, screenY)) { + return this; + } + return super.touchDown(screenX, screenY, button); + } + + @Override + public UIFrame getFrameChildUnderMouse(final float screenX, final float screenY) { + if (isVisible() && this.enabled && this.renderBounds.contains(screenX, screenY)) { + return this; + } + return super.getFrameChildUnderMouse(screenX, screenY); + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/GlueTextButtonFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/GlueTextButtonFrame.java new file mode 100644 index 0000000..fc0ba43 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/GlueTextButtonFrame.java @@ -0,0 +1,77 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; + +public class GlueTextButtonFrame extends GlueButtonFrame { + private UIFrame buttonText; + + public GlueTextButtonFrame(final String name, final UIFrame parent) { + super(name, parent); + } + + public void setButtonText(final UIFrame buttonText) { + this.buttonText = buttonText; + } + + @Override + protected void innerPositionBounds(final GameUI gameUI, final Viewport viewport) { + super.innerPositionBounds(gameUI, viewport); + if (this.buttonText != null) { + this.buttonText.positionBounds(gameUI, viewport); + } + } + + @Override + protected void internalRender(final SpriteBatch batch, final BitmapFont baseFont, final GlyphLayout glyphLayout) { + super.internalRender(batch, baseFont, glyphLayout); + if (this.buttonText != null) { + this.buttonText.render(batch, baseFont, glyphLayout); + } + } + + @Override + public void setEnabled(final boolean enabled) { + super.setEnabled(enabled); + if (this.buttonText instanceof StringFrame) { + final StringFrame stringButtonText = (StringFrame) this.buttonText; + final Color fontColor = enabled ? stringButtonText.getFontOriginalColor() + : stringButtonText.getFontDisabledColor(); + if (fontColor != null) { + stringButtonText.setColor(fontColor); + } + } + } + + @Override + protected void onMouseEnter() { + super.onMouseEnter(); + if (isEnabled()) { + if (this.buttonText instanceof StringFrame) { + final StringFrame stringFrame = (StringFrame) this.buttonText; + final Color fontHighlightColor = stringFrame.getFontHighlightColor(); + if (fontHighlightColor != null) { + stringFrame.setColor(fontHighlightColor); + } + } + } + } + + @Override + protected void onMouseExit() { + super.onMouseExit(); + if (isEnabled()) { + if (this.buttonText instanceof StringFrame) { + final StringFrame stringFrame = (StringFrame) this.buttonText; + final Color fontOriginalColor = stringFrame.getFontOriginalColor(); + if (fontOriginalColor != null) { + stringFrame.setColor(fontOriginalColor); + } + } + } + } +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/ListBoxFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/ListBoxFrame.java new file mode 100644 index 0000000..3816045 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/ListBoxFrame.java @@ -0,0 +1,202 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import java.util.ArrayList; +import java.util.List; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.Pixmap; +import com.badlogic.gdx.graphics.Pixmap.Format; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.datamodel.FramePoint; +import com.etheller.warsmash.parsers.fdf.datamodel.TextJustify; + +public class ListBoxFrame extends ControlFrame { + // TODO where are these colors in the UI definition files? + private static final Color SELECT_COLOR = Color.BLUE; + private static final Color MOUSE_OVER_HIGHLIGHT_COLOR = new Color(0.3f, 0.3f, 1.0f, 0.25f); + + private final List listItems = new ArrayList<>(); + private final List stringFrames = new ArrayList<>(); + private BitmapFont frameFont; + private float listBoxBorder; + private int selectedIndex = -1; + private int mouseOverIndex = -1; + + private final TextureFrame selectionFrame; + private final TextureFrame mouseHighlightFrame; + private GameUI gameUI; + private Viewport viewport; + private Runnable onSelect; + + public ListBoxFrame(final String name, final UIFrame parent, final Viewport viewport) { + super(name, parent); + this.listBoxBorder = GameUI.convertX(viewport, 0.01f); + this.selectionFrame = new TextureFrame(null, this, false, null); + this.mouseHighlightFrame = new TextureFrame(null, this, false, null); + final Pixmap pixmap = new Pixmap(1, 1, Format.RGBA8888); + pixmap.setColor(SELECT_COLOR); + pixmap.fill(); + this.selectionFrame.setTexture(new Texture(pixmap)); + final Pixmap mousePixmap = new Pixmap(1, 1, Format.RGBA8888); + mousePixmap.setColor(MOUSE_OVER_HIGHLIGHT_COLOR); + mousePixmap.fill(); + this.mouseHighlightFrame.setTexture(new Texture(mousePixmap)); + } + + public void setListBoxBorder(final float listBoxBorder) { + this.listBoxBorder = listBoxBorder; + } + + public float getListBoxBorder() { + return this.listBoxBorder; + } + + public void setFrameFont(final BitmapFont frameFont) { + this.frameFont = frameFont; + } + + @Override + protected void innerPositionBounds(final GameUI gameUI, final Viewport viewport) { + this.gameUI = gameUI; + this.viewport = viewport; + super.innerPositionBounds(gameUI, viewport); + updateUI(gameUI, viewport); + } + + private void positionChildren(final GameUI gameUI, final Viewport viewport) { + for (final SingleStringFrame frame : this.stringFrames) { + frame.positionBounds(gameUI, viewport); + } + this.selectionFrame.positionBounds(gameUI, viewport); + this.mouseHighlightFrame.positionBounds(gameUI, viewport); + } + + @Override + protected void internalRender(final SpriteBatch batch, final BitmapFont baseFont, final GlyphLayout glyphLayout) { + super.internalRender(batch, baseFont, glyphLayout); + this.selectionFrame.render(batch, baseFont, glyphLayout); + this.mouseHighlightFrame.render(batch, baseFont, glyphLayout); + for (final SingleStringFrame frame : this.stringFrames) { + frame.render(batch, baseFont, glyphLayout); + } + } + + public void addItem(final String item, final GameUI gameUI, final Viewport viewport) { + this.listItems.add(item); + updateUI(gameUI, viewport); + } + + public void setItems(final List items, final GameUI gameUI, final Viewport viewport) { + this.listItems.clear(); + this.listItems.addAll(items); + updateUI(gameUI, viewport); + } + + public void removeItem(final String item, final GameUI gameUI, final Viewport viewport) { + this.listItems.remove(item); + updateUI(gameUI, viewport); + } + + public void removeItem(final int index, final GameUI gameUI, final Viewport viewport) { + this.listItems.remove(index); + updateUI(gameUI, viewport); + } + + public void setSelectedIndex(final int selectedIndex) { + this.selectedIndex = selectedIndex; + } + + public int getSelectedIndex() { + return this.selectedIndex; + } + + private void updateUI(final GameUI gameUI, final Viewport viewport) { + this.stringFrames.clear(); + SingleStringFrame prev = null; + int i = 0; + boolean foundSelected = false; + boolean foundMouseOver = false; + for (final String string : this.listItems) { + final boolean selected = (i == this.selectedIndex); + final boolean mousedOver = (i == this.mouseOverIndex); + final SingleStringFrame stringFrame = new SingleStringFrame("LISTY" + i++, this, Color.WHITE, + TextJustify.LEFT, TextJustify.MIDDLE, this.frameFont); + stringFrame.setText(string); + stringFrame.setWidth(this.renderBounds.width - (this.listBoxBorder * 2)); + stringFrame.setHeight(this.frameFont.getLineHeight()); + if (prev != null) { + stringFrame.addSetPoint(new SetPoint(FramePoint.TOPLEFT, prev, FramePoint.BOTTOMLEFT, 0, 0)); + } + else { + stringFrame.addSetPoint(new SetPoint(FramePoint.TOPLEFT, this, FramePoint.TOPLEFT, this.listBoxBorder, + -this.listBoxBorder)); + } + this.stringFrames.add(stringFrame); + prev = stringFrame; + if (selected) { + this.selectionFrame + .addSetPoint(new SetPoint(FramePoint.TOPLEFT, stringFrame, FramePoint.TOPLEFT, 0, 0)); + this.selectionFrame + .addSetPoint(new SetPoint(FramePoint.BOTTOMRIGHT, stringFrame, FramePoint.BOTTOMRIGHT, 0, 0)); + foundSelected = true; + } + else if (mousedOver) { + this.mouseHighlightFrame + .addSetPoint(new SetPoint(FramePoint.TOPLEFT, stringFrame, FramePoint.TOPLEFT, 0, 0)); + this.mouseHighlightFrame + .addSetPoint(new SetPoint(FramePoint.BOTTOMRIGHT, stringFrame, FramePoint.BOTTOMRIGHT, 0, 0)); + foundMouseOver = true; + } + } + this.selectionFrame.setVisible(foundSelected); + this.mouseHighlightFrame.setVisible(foundMouseOver); + positionChildren(gameUI, viewport); + } + + @Override + public UIFrame touchDown(final float screenX, final float screenY, final int button) { + if (isVisible() && this.renderBounds.contains(screenX, screenY)) { + int index = 0; + for (final SingleStringFrame stringFrame : this.stringFrames) { + if (stringFrame.getRenderBounds().contains(screenX, screenY)) { + this.selectedIndex = index; + } + index++; + } + updateUI(this.gameUI, this.viewport); + if (this.onSelect != null) { + this.onSelect.run(); + } + return this; + } + return super.touchDown(screenX, screenY, button); + } + + @Override + public UIFrame getFrameChildUnderMouse(final float screenX, final float screenY) { + if (isVisible() && this.renderBounds.contains(screenX, screenY)) { + int index = 0; + int mouseOverIndex = -1; + for (final SingleStringFrame stringFrame : this.stringFrames) { + if (stringFrame.getRenderBounds().contains(screenX, screenY)) { + mouseOverIndex = index; + } + index++; + } + if (this.mouseOverIndex != mouseOverIndex) { + this.mouseOverIndex = mouseOverIndex; + updateUI(this.gameUI, this.viewport); + } + } + return super.getFrameChildUnderMouse(screenX, screenY); + } + + public void setOnSelect(final Runnable onSelect) { + this.onSelect = onSelect; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/SetPoint.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/SetPoint.java new file mode 100644 index 0000000..f8ff31e --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/SetPoint.java @@ -0,0 +1,58 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.datamodel.FramePoint; + +public class SetPoint implements FramePointAssignment { + private final FramePoint myPoint; + private final UIFrame other; + private final FramePoint otherPoint; + private final float x; + private final float y; + + public SetPoint(final FramePoint myPoint, final UIFrame other, final FramePoint otherPoint, final float x, + final float y) { + this.myPoint = myPoint; + this.other = other; + this.otherPoint = otherPoint; + this.x = x; + this.y = y; + } + + public FramePoint getMyPoint() { + return this.myPoint; + } + + public UIFrame getOther() { + return this.other; + } + + public FramePoint getOtherPoint() { + return this.otherPoint; + } + + public float getX() { + return this.x; + } + + public float getY() { + return this.y; + } + + @Override + public float getX(final GameUI gameUI, final Viewport uiViewport) { + return this.other.getFramePointX(this.otherPoint) + this.x; + } + + @Override + public float getY(final GameUI gameUI, final Viewport uiViewport) { + return this.other.getFramePointY(this.otherPoint) + this.y; + } + + @Override + public String toString() { + return "SetPoint [myPoint=" + this.myPoint + ", other=" + this.other + ", otherPoint=" + this.otherPoint + + ", x=" + this.x + ", y=" + this.y + "]"; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/SimpleButtonFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/SimpleButtonFrame.java new file mode 100644 index 0000000..610a8c8 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/SimpleButtonFrame.java @@ -0,0 +1,217 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.ClickableFrame; + +public class SimpleButtonFrame extends AbstractRenderableFrame implements ClickableFrame { + + private UIFrame controlBackdrop; + private UIFrame controlPushedBackdrop; + private UIFrame controlDisabledBackdrop; + private UIFrame controlMouseOverHighlight; + + private boolean enabled = true; + private boolean highlightOnMouseOver; + private boolean mouseOver = false; + private boolean pushed = false; + + private UIFrame activeChild; + private UIFrame activeTextChild; + + private UIFrame buttonText; + private UIFrame disabledText; + private UIFrame highlightText; + + private UIFrame pushedText; + private UIFrame pushedHighlightText; + + private Runnable onClick; + + public SimpleButtonFrame(final String name, final UIFrame parent) { + super(name, parent); + } + + public void setControlBackdrop(final UIFrame controlBackdrop) { + this.controlBackdrop = controlBackdrop; + if (this.activeChild == null) { + this.activeChild = controlBackdrop; + } + } + + public void setControlPushedBackdrop(final UIFrame controlPushedBackdrop) { + this.controlPushedBackdrop = controlPushedBackdrop; + } + + public void setControlDisabledBackdrop(final UIFrame controlDisabledBackdrop) { + this.controlDisabledBackdrop = controlDisabledBackdrop; + } + + public void setControlMouseOverHighlight(final UIFrame controlMouseOverHighlight) { + this.controlMouseOverHighlight = controlMouseOverHighlight; + this.highlightOnMouseOver |= controlMouseOverHighlight != null; + } + + public void setEnabled(final boolean enabled) { + this.enabled = enabled; + if (this.enabled) { + this.activeChild = this.controlBackdrop; + this.activeTextChild = this.buttonText; + } + else { + this.activeChild = this.controlDisabledBackdrop; + this.activeTextChild = this.disabledText; + } + } + + public void setHighlightOnMouseOver(final boolean highlightOnMouseOver) { + this.highlightOnMouseOver = highlightOnMouseOver; + } + + public void setOnClick(final Runnable onClick) { + this.onClick = onClick; + } + + @Override + protected void innerPositionBounds(final GameUI gameUI, final Viewport viewport) { + if (this.controlBackdrop != null) { + this.controlBackdrop.positionBounds(gameUI, viewport); + } + if (this.controlPushedBackdrop != null) { + this.controlPushedBackdrop.positionBounds(gameUI, viewport); + } + if (this.controlDisabledBackdrop != null) { + this.controlDisabledBackdrop.positionBounds(gameUI, viewport); + } + if (this.controlMouseOverHighlight != null) { + this.controlMouseOverHighlight.positionBounds(gameUI, viewport); + } + if (this.buttonText != null) { + this.buttonText.positionBounds(gameUI, viewport); + } + if (this.pushedText != null) { + this.pushedText.positionBounds(gameUI, viewport); + } + if (this.disabledText != null) { + this.disabledText.positionBounds(gameUI, viewport); + } + if (this.highlightText != null) { + this.highlightText.positionBounds(gameUI, viewport); + } + if (this.pushedHighlightText != null) { + this.pushedHighlightText.positionBounds(gameUI, viewport); + } + if (this.enabled) { + this.activeChild = this.controlBackdrop; + this.activeTextChild = this.buttonText; + } + else { + this.activeChild = this.controlDisabledBackdrop; + this.activeTextChild = this.disabledText; + } + } + + @Override + protected void internalRender(final SpriteBatch batch, final BitmapFont baseFont, final GlyphLayout glyphLayout) { + if (this.activeChild != null) { + this.activeChild.render(batch, baseFont, glyphLayout); + } + if (this.activeTextChild != null) { + this.activeTextChild.render(batch, baseFont, glyphLayout); + } + if (this.mouseOver) { + this.controlMouseOverHighlight.render(batch, baseFont, glyphLayout); + } + } + + @Override + public void mouseDown(final GameUI gameUI, final Viewport uiViewport) { + if (this.enabled) { + this.activeChild = this.controlPushedBackdrop; + this.pushed = true; + this.activeTextChild = this.mouseOver ? this.pushedHighlightText : this.pushedText; + } + } + + @Override + public void mouseUp(final GameUI gameUI, final Viewport uiViewport) { + if (this.enabled) { + this.activeChild = this.controlBackdrop; + this.activeTextChild = this.mouseOver ? this.highlightText : this.buttonText; + } + this.pushed = false; + } + + @Override + public void mouseEnter(final GameUI gameUI, final Viewport uiViewport) { + if (this.highlightOnMouseOver) { + this.mouseOver = true; + if (this.enabled) { + this.activeTextChild = this.pushed ? this.pushedHighlightText : this.highlightText; + } + } + } + + @Override + public void mouseExit(final GameUI gameUI, final Viewport uiViewport) { + this.mouseOver = false; + if (this.enabled) { + this.activeTextChild = this.pushed ? this.pushedText : this.buttonText; + } + } + + @Override + public void onClick(final int button) { + if (this.onClick != null) { + this.onClick.run(); + } + } + + @Override + public UIFrame touchUp(final float screenX, final float screenY, final int button) { + if (isVisible() && this.enabled && this.renderBounds.contains(screenX, screenY)) { + return this; + } + return super.touchUp(screenX, screenY, button); + } + + @Override + public UIFrame touchDown(final float screenX, final float screenY, final int button) { + if (isVisible() && this.enabled && this.renderBounds.contains(screenX, screenY)) { + return this; + } + return super.touchDown(screenX, screenY, button); + } + + @Override + public UIFrame getFrameChildUnderMouse(final float screenX, final float screenY) { + if (isVisible() && this.enabled && this.renderBounds.contains(screenX, screenY)) { + return this; + } + return super.getFrameChildUnderMouse(screenX, screenY); + } + + public void setButtonText(final UIFrame buttonText) { + this.buttonText = buttonText; + } + + public void setPushedHighlightText(final UIFrame pushedHighlightText) { + this.pushedHighlightText = pushedHighlightText; + } + + public void setPushedText(final UIFrame pushedText) { + this.pushedText = pushedText; + } + + public void setHighlightText(final UIFrame highlightText) { + this.highlightText = highlightText; + this.highlightOnMouseOver |= highlightText != null; + } + + public void setDisabledText(final UIFrame disabledText) { + this.disabledText = disabledText; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/SimpleFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/SimpleFrame.java new file mode 100644 index 0000000..b90dd09 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/SimpleFrame.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +public class SimpleFrame extends AbstractUIFrame { + + public SimpleFrame(final String name, final UIFrame parent) { + super(name, parent); + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/SimpleStatusBarFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/SimpleStatusBarFrame.java new file mode 100644 index 0000000..a7ea544 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/SimpleStatusBarFrame.java @@ -0,0 +1,41 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import com.etheller.warsmash.parsers.fdf.datamodel.FramePoint; +import com.etheller.warsmash.parsers.fdf.datamodel.Vector4Definition; + +public class SimpleStatusBarFrame extends AbstractUIFrame { + private final boolean decorateFileNames; + private final TextureFrame barFrame; + private final TextureFrame borderFrame; + + public SimpleStatusBarFrame(final String name, final UIFrame parent, final boolean decorateFileNames) { + super(name, parent); + this.decorateFileNames = decorateFileNames; + this.barFrame = new TextureFrame(name + "Bar", this, decorateFileNames, new Vector4Definition(0, 1, 0, 1)); + this.borderFrame = new TextureFrame(name + "Border", this, decorateFileNames, + new Vector4Definition(0, 1, 0, 1)); + this.borderFrame.setSetAllPoints(true); + this.barFrame.addSetPoint(new SetPoint(FramePoint.TOPLEFT, this, FramePoint.TOPLEFT, 0, 0)); + this.barFrame.addSetPoint(new SetPoint(FramePoint.BOTTOMLEFT, this, FramePoint.BOTTOMLEFT, 0, 0)); + this.barFrame.setSetAllPoints(true); + add(this.barFrame); + add(this.borderFrame); + } + + public boolean isDecorateFileNames() { + return this.decorateFileNames; + } + + public void setValue(final float value) { + this.barFrame.setTexCoord(0, value, 0, 1); + this.barFrame.setWidth(this.renderBounds.width * value); + } + + public TextureFrame getBarFrame() { + return this.barFrame; + } + + public TextureFrame getBorderFrame() { + return this.borderFrame; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/SingleStringFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/SingleStringFrame.java new file mode 100644 index 0000000..b6968d4 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/SingleStringFrame.java @@ -0,0 +1,106 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.datamodel.TextJustify; + +public class SingleStringFrame extends AbstractRenderableFrame { + private Color color; + private String text = "Default string"; + private final TextJustify justifyH; + private final TextJustify justifyV; + private final BitmapFont frameFont; + private Color fontShadowColor; + private float fontShadowOffsetX; + private float fontShadowOffsetY; + private float alpha = 1.0f; + + public SingleStringFrame(final String name, final UIFrame parent, final Color color, final TextJustify justifyH, + final TextJustify justifyV, final BitmapFont frameFont) { + super(name, parent); + this.color = color; + this.justifyH = justifyH; + this.justifyV = justifyV; + this.frameFont = frameFont; + this.text = name; + } + + public void setText(final String text) { + if (text == null) { + throw new IllegalArgumentException(); + } + this.text = text; + } + + public void setColor(final Color color) { + this.color = color; + } + + public Color getColor() { + return this.color; + } + + public void setFontShadowColor(final Color fontShadowColor) { + this.fontShadowColor = fontShadowColor; + } + + public void setFontShadowOffsetX(final float fontShadowOffsetX) { + this.fontShadowOffsetX = fontShadowOffsetX; + } + + public void setFontShadowOffsetY(final float fontShadowOffsetY) { + this.fontShadowOffsetY = fontShadowOffsetY; + } + + @Override + protected void internalRender(final SpriteBatch batch, final BitmapFont baseFont, final GlyphLayout glyphLayout) { + glyphLayout.setText(this.frameFont, this.text); + final float x; + switch (this.justifyH) { + case CENTER: + x = this.renderBounds.x + ((this.renderBounds.width - glyphLayout.width) / 2); + break; + case RIGHT: + x = (this.renderBounds.x + this.renderBounds.width) - glyphLayout.width; + break; + case LEFT: + default: + x = this.renderBounds.x; + break; + } + final float y; + switch (this.justifyV) { + case MIDDLE: + y = this.renderBounds.y + ((this.renderBounds.height + this.frameFont.getLineHeight()) / 2); + break; + case TOP: + y = (this.renderBounds.y + this.renderBounds.height); + break; + case BOTTOM: + default: + y = this.renderBounds.y + this.frameFont.getLineHeight(); + break; + } + if (this.fontShadowColor != null) { + this.frameFont.setColor(this.fontShadowColor.r, this.fontShadowColor.g, this.fontShadowColor.b, + this.fontShadowColor.a * this.alpha); + this.frameFont.draw(batch, this.text, x + this.fontShadowOffsetX, y + this.fontShadowOffsetY); + } + this.frameFont.setColor(this.color.r, this.color.g, this.color.b, this.color.a * this.alpha); + this.frameFont.draw(batch, this.text, x, y); + } + + @Override + protected void innerPositionBounds(final GameUI gameUI, final Viewport viewport) { + } + + public void setAlpha(final float alpha) { + this.alpha = alpha; + + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/SmartBackdropFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/SmartBackdropFrame.java new file mode 100644 index 0000000..d7ea575 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/SmartBackdropFrame.java @@ -0,0 +1,77 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import java.util.EnumSet; + +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.datamodel.BackdropCornerFlags; +import com.etheller.warsmash.parsers.fdf.datamodel.Vector4Definition; +import com.etheller.warsmash.viewer5.Scene; +import com.hiveworkshop.rms.parsers.mdlx.MdlxGeoset; +import com.hiveworkshop.rms.parsers.mdlx.MdlxLayer; +import com.hiveworkshop.rms.parsers.mdlx.MdlxLayer.FilterMode; +import com.hiveworkshop.rms.parsers.mdlx.MdlxMaterial; +import com.hiveworkshop.rms.parsers.mdlx.MdlxModel; +import com.hiveworkshop.rms.parsers.mdlx.MdlxTexture; +import com.hiveworkshop.rms.parsers.mdlx.MdlxTexture.WrapMode; + +public class SmartBackdropFrame extends SpriteFrame { + private final boolean decorateFileNames; + private final boolean tileBackground; + private final String backgroundString; + private final EnumSet cornerFlags; + private final float cornerSize; + private final float backgroundSize; + private final Vector4Definition backgroundInsets; + private final String edgeFileString; + + public SmartBackdropFrame(final String name, final UIFrame parent, final Scene scene, final Viewport uiViewport, + final boolean decorateFileNames, final boolean tileBackground, final String backgroundString, + final EnumSet cornerFlags, final float cornerSize, final float backgroundSize, + final Vector4Definition backgroundInsets, final String edgeFileString) { + super(name, parent, scene, uiViewport); + this.decorateFileNames = decorateFileNames; + this.tileBackground = tileBackground; + this.backgroundString = backgroundString; + this.cornerFlags = cornerFlags; + this.cornerSize = cornerSize; + this.backgroundSize = backgroundSize; + this.backgroundInsets = backgroundInsets; + this.edgeFileString = edgeFileString; + } + + @Override + protected void innerPositionBounds(final GameUI gameUI, final Viewport viewport) { + generateBackdropModel(); + super.innerPositionBounds(gameUI, viewport); + } + + private MdlxModel generateBackdropModel() { + final MdlxModel model = new MdlxModel(); + final int edgeFileMaterialId = generateMaterial(model, this.edgeFileString, true); + final int backgroundMaterialId = generateMaterial(model, this.backgroundString, this.tileBackground); + final MdlxGeoset edgeGeoset = new MdlxGeoset(); + final float[] edgeGeosetVertices = new float[32 * 4]; + return model; + } + + private int generateMaterial(final MdlxModel model, final String path, final boolean wrap) { + final MdlxTexture edgeFileReference = new MdlxTexture(); + if (wrap) { + edgeFileReference.setWrapMode(WrapMode.REPEAT_BOTH); + } + edgeFileReference.setPath(path); + final int textureId = model.getTextures().size(); + model.getTextures().add(edgeFileReference); + final MdlxMaterial edgeFileMaterial = new MdlxMaterial(); + final MdlxLayer edgeFileMaterialLayer = new MdlxLayer(); + edgeFileMaterialLayer.setAlpha(1.0f); + edgeFileMaterialLayer.setFilterMode(FilterMode.BLEND); + edgeFileMaterialLayer.setTextureId(textureId); + edgeFileMaterial.getLayers().add(edgeFileMaterialLayer); + final int materialId = model.getMaterials().size(); + model.getMaterials().add(edgeFileMaterial); + return materialId; + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/SpriteFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/SpriteFrame.java new file mode 100644 index 0000000..79e32f2 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/SpriteFrame.java @@ -0,0 +1,138 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.datamodel.FramePoint; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.mdx.SequenceLoopMode; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; + +public class SpriteFrame extends AbstractUIFrame { + + protected final Scene scene; + protected final Viewport uiViewport; + private MdxComplexInstance instance; + private float zDepth; + + public SpriteFrame(final String name, final UIFrame parent, final Scene scene, final Viewport uiViewport) { + super(name, parent); + this.scene = scene; + this.uiViewport = uiViewport; + } + + public void setModel(final MdxModel model) { + if (this.instance != null) { + this.scene.removeInstance(this.instance); + } + if (model != null) { + this.instance = (MdxComplexInstance) model.addInstance(); + this.instance.setSequenceLoopMode(SequenceLoopMode.MODEL_LOOP); + this.instance.setScene(this.scene); + this.instance.setLocation(this.renderBounds.x, this.renderBounds.y, this.zDepth); + } + else { + this.instance = null; + } + } + + @Override + public void setVisible(final boolean visible) { + super.setVisible(visible); + updateInstanceLocation(this.uiViewport); + } + + @Override + protected void internalRender(final SpriteBatch batch, final BitmapFont baseFont, final GlyphLayout glyphLayout) { + super.internalRender(batch, baseFont, glyphLayout); + } + + @Override + public void setFramePointX(final FramePoint framePoint, final float x) { + super.setFramePointX(framePoint, x); + updateInstanceLocation(this.uiViewport); + } + + @Override + public void setFramePointY(final FramePoint framePoint, final float y) { + super.setFramePointY(framePoint, y); + updateInstanceLocation(this.uiViewport); + } + + public void setZDepth(final float depth) { + this.zDepth = depth; + updateInstanceLocation(this.uiViewport); + } + + @Override + protected void innerPositionBounds(final GameUI gameUI, final Viewport viewport) { + super.innerPositionBounds(gameUI, viewport); + updateInstanceLocation(viewport); + } + + public void setSequence(final int index) { + if (this.instance != null) { + this.instance.setSequence(index); + } + } + + public void setSequence(final String animationName) { + if (this.instance != null) { + SequenceUtils.randomSequence(this.instance, animationName.toLowerCase()); + } + } + + public void setSequence(final PrimaryTag animationName) { + if (this.instance != null) { + SequenceUtils.randomSequence(this.instance, animationName); + } + } + + public void setAnimationSpeed(final float speedRatio) { + if (this.instance != null) { + this.instance.setAnimationSpeed(speedRatio); + } + } + + public void setFrame(final int animationFrame) { + if (this.instance != null) { + this.instance.setFrame(animationFrame); + } + } + + public void setFrameByRatio(final float ratioOfAnimationCompleted) { + if (this.instance != null) { + this.instance.setFrameByRatio(ratioOfAnimationCompleted); + } + } + + private void updateInstanceLocation(final Viewport viewport) { + if (this.instance != null) { + this.instance.setLocation(GameUI.unconvertX(viewport, this.renderBounds.x), + GameUI.unconvertY(viewport, this.renderBounds.y), this.zDepth); + if (isVisible()) { + this.instance.show(); + } + else { + this.instance.hide(); + } + } + } + + public boolean isSequenceEnded() { + return this.instance.sequenceEnded; + } + + public void setReplaceableId(final int replaceableId, final String blpPath) { + if (this.instance != null) { + this.instance.setReplaceableTexture(replaceableId, blpPath); + } + + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/StringFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/StringFrame.java new file mode 100644 index 0000000..11cfeb1 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/StringFrame.java @@ -0,0 +1,496 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import java.util.ArrayList; +import java.util.List; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.datamodel.AnchorDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.FramePoint; +import com.etheller.warsmash.parsers.fdf.datamodel.TextJustify; + +public class StringFrame extends AbstractRenderableFrame { + private final List internalFrames = new ArrayList<>(); + private Color color; + private String text = "Default string"; + private final TextJustify justifyH; + private final TextJustify justifyV; + private final BitmapFont frameFont; + private Color fontShadowColor; + private float fontShadowOffsetX; + private float fontShadowOffsetY; + private float alpha = 1.0f; + private final SimpleFrame internalFramesContainer; + private float predictedViewportHeight; + + static ShapeRenderer shapeRenderer = new ShapeRenderer(); + private final Color fontHighlightColor; + private final Color fontDisabledColor; + private final Color fontColor; + + public StringFrame(final String name, final UIFrame parent, final Color color, final TextJustify justifyH, + final TextJustify justifyV, final BitmapFont frameFont, final String text, final Color fontHighlightColor, + final Color fontDisabledColor) { + super(name, parent); + this.fontColor = color; + this.color = color; + this.justifyH = justifyH; + this.justifyV = justifyV; + this.frameFont = frameFont; + this.text = text; + this.fontHighlightColor = fontHighlightColor; + this.fontDisabledColor = fontDisabledColor; + this.internalFramesContainer = new SimpleFrame(null, this); + } + + public String getText() { + return this.text; + } + + public void setText(final String text, final GameUI gameUI, final Viewport viewport) { + if (text == null) { + throw new IllegalArgumentException(); + } + this.text = text; + positionBounds(gameUI, viewport); + } + + public void setColor(final Color color) { + for (final SingleStringFrame internalFrame : this.internalFrames) { + if (internalFrame.getColor() == this.color) { + internalFrame.setColor(color); + } + } + this.color = color; + } + + public Color getColor() { + return this.color; + } + + public Color getFontOriginalColor() { + return this.fontColor; + } + + public Color getFontDisabledColor() { + return this.fontDisabledColor; + } + + public Color getFontHighlightColor() { + return this.fontHighlightColor; + } + + public void setFontShadowColor(final Color fontShadowColor) { + this.fontShadowColor = fontShadowColor; + for (final SingleStringFrame internalFrame : this.internalFrames) { + internalFrame.setFontShadowColor(fontShadowColor); + } + } + + public void setFontShadowOffsetX(final float fontShadowOffsetX) { + this.fontShadowOffsetX = fontShadowOffsetX; + for (final SingleStringFrame internalFrame : this.internalFrames) { + internalFrame.setFontShadowOffsetX(fontShadowOffsetX); + } + } + + public void setFontShadowOffsetY(final float fontShadowOffsetY) { + this.fontShadowOffsetY = fontShadowOffsetY; + for (final SingleStringFrame internalFrame : this.internalFrames) { + internalFrame.setFontShadowOffsetY(fontShadowOffsetY); + } + } + + @Override + protected void internalRender(final SpriteBatch batch, final BitmapFont baseFont, final GlyphLayout glyphLayout) { + this.internalFramesContainer.render(batch, baseFont, glyphLayout); + + if (GameUI.DEBUG) { + batch.end(); + shapeRenderer.setProjectionMatrix(batch.getProjectionMatrix()); + shapeRenderer.setColor(1f, 1f, 1f, 1f); + shapeRenderer.begin(ShapeType.Line); + shapeRenderer.rect(this.renderBounds.x, this.renderBounds.y, this.renderBounds.width, + this.renderBounds.height); + + shapeRenderer.end(); + + batch.begin(); + } + } + + @Override + public void positionBounds(final GameUI gameUI, final Viewport viewport) { + createInternalFrames(gameUI.getGlyphLayout()); + if (this.renderBounds.height == 0) { + this.renderBounds.height = getPredictedViewportHeight(); + } + super.positionBounds(gameUI, viewport); + } + + @Override + protected void innerPositionBounds(final GameUI gameUI, final Viewport viewport) { + this.internalFramesContainer.positionBounds(gameUI, viewport); + } + + private void createInternalFrames(final GlyphLayout glyphLayout) { + for (final SingleStringFrame internalFrame : this.internalFrames) { + this.internalFramesContainer.remove(internalFrame); + } + this.internalFrames.clear(); + final StringBuilder currentLine = new StringBuilder(); + final StringBuilder currentWord = new StringBuilder(); + float currentXCoordForWord = 0; + float currentXCoordForFrames = 0; + final float usedWidth = 0; + float usedHeight = 0; + float usedWidthMax = 0; + final float startingBoundsWidth = this.renderBounds.width; + final boolean firstInLine = false; + Color currentColor = this.color; + for (int i = 0; i < this.text.length(); i++) { + final char c = this.text.charAt(i); + switch (c) { + case '|': { + // special control character + if ((i + 1) < this.text.length()) { + final char escapedCharacter = this.text.charAt(i + 1); + switch (escapedCharacter) { + case 'c': + case 'C': + if ((i + 9) < this.text.length()) { + int colorInt; + try { + final String upperCase = this.text.substring(i + 2, i + 10).toUpperCase(); + colorInt = (int) Long.parseLong(upperCase, 16); + } + catch (final NumberFormatException exc) { + currentWord.append(c); + break; + } + i += 9; + { + final String wordString = currentWord.toString(); + currentWord.setLength(0); + glyphLayout.setText(this.frameFont, wordString); + final float wordWidth = glyphLayout.width; + if ((startingBoundsWidth > 0) + && ((currentXCoordForWord + wordWidth) >= startingBoundsWidth)) { + final String currentLineString = currentLine.toString(); + currentLine.setLength(0); + glyphLayout.setText(this.frameFont, currentLineString); + usedWidthMax = Math.max(currentXCoordForFrames + glyphLayout.width, usedWidthMax); + final SingleStringFrame singleStringFrame = new SingleStringFrame(currentLineString, + this.internalFramesContainer, currentColor, TextJustify.LEFT, + TextJustify.TOP, this.frameFont); + singleStringFrame.setHeight(this.frameFont.getLineHeight()); + singleStringFrame.setWidth(glyphLayout.width); + singleStringFrame.setAlpha(this.alpha); + singleStringFrame.setFontShadowColor(this.fontShadowColor); + singleStringFrame.setFontShadowOffsetX(this.fontShadowOffsetX); + singleStringFrame.setFontShadowOffsetY(this.fontShadowOffsetY); + singleStringFrame.addAnchor(new AnchorDefinition(FramePoint.TOPLEFT, + currentXCoordForFrames, usedHeight)); + this.internalFrames.add(singleStringFrame); + usedHeight += this.frameFont.getLineHeight(); + currentXCoordForWord = 0; + currentXCoordForFrames = 0; + } + currentXCoordForWord += wordWidth; + currentLine.append(wordString); + + final String currentLineString = currentLine.toString(); + currentLine.setLength(0); + glyphLayout.setText(this.frameFont, currentLineString); + usedWidthMax = Math.max(currentXCoordForFrames + glyphLayout.width, usedWidthMax); + final SingleStringFrame singleStringFrame = new SingleStringFrame(currentLineString, + this.internalFramesContainer, currentColor, TextJustify.LEFT, TextJustify.TOP, + this.frameFont); + singleStringFrame.setHeight(this.frameFont.getLineHeight()); + singleStringFrame.setWidth(glyphLayout.width); + singleStringFrame.setAlpha(this.alpha); + singleStringFrame.setFontShadowColor(this.fontShadowColor); + singleStringFrame.setFontShadowOffsetX(this.fontShadowOffsetX); + singleStringFrame.setFontShadowOffsetY(this.fontShadowOffsetY); + singleStringFrame.addAnchor( + new AnchorDefinition(FramePoint.TOPLEFT, currentXCoordForFrames, -usedHeight)); + this.internalFrames.add(singleStringFrame); + currentXCoordForFrames = currentXCoordForWord; + + currentColor = new Color((colorInt << 8) | (colorInt >>> 24)); + } + } + break; + case 'r': + case 'R': + i++; { + final String wordString = currentWord.toString(); + currentWord.setLength(0); + glyphLayout.setText(this.frameFont, wordString); + final float wordWidth = glyphLayout.width; + if ((startingBoundsWidth > 0) && ((currentXCoordForWord + wordWidth) >= startingBoundsWidth)) { + final String currentLineString = currentLine.toString(); + currentLine.setLength(0); + glyphLayout.setText(this.frameFont, currentLineString); + usedWidthMax = Math.max(currentXCoordForFrames + glyphLayout.width, usedWidthMax); + final SingleStringFrame singleStringFrame = new SingleStringFrame(currentLineString, + this.internalFramesContainer, currentColor, TextJustify.LEFT, TextJustify.TOP, + this.frameFont); + singleStringFrame.setHeight(this.frameFont.getLineHeight()); + singleStringFrame.setWidth(glyphLayout.width); + singleStringFrame.setAlpha(this.alpha); + singleStringFrame.setFontShadowColor(this.fontShadowColor); + singleStringFrame.setFontShadowOffsetX(this.fontShadowOffsetX); + singleStringFrame.setFontShadowOffsetY(this.fontShadowOffsetY); + singleStringFrame.addAnchor( + new AnchorDefinition(FramePoint.TOPLEFT, currentXCoordForFrames, -usedHeight)); + this.internalFrames.add(singleStringFrame); + usedHeight += this.frameFont.getLineHeight(); + currentXCoordForWord = 0; + currentXCoordForFrames = 0; + } + currentXCoordForWord += wordWidth; + currentLine.append(wordString); + + final String currentLineString = currentLine.toString(); + currentLine.setLength(0); + glyphLayout.setText(this.frameFont, currentLineString); + usedWidthMax = Math.max(currentXCoordForFrames + glyphLayout.width, usedWidthMax); + final SingleStringFrame singleStringFrame = new SingleStringFrame(currentLineString, + this.internalFramesContainer, currentColor, TextJustify.LEFT, TextJustify.TOP, + this.frameFont); + singleStringFrame.setHeight(this.frameFont.getLineHeight()); + singleStringFrame.setWidth(glyphLayout.width); + singleStringFrame.setAlpha(this.alpha); + singleStringFrame.setFontShadowColor(this.fontShadowColor); + singleStringFrame.setFontShadowOffsetX(this.fontShadowOffsetX); + singleStringFrame.setFontShadowOffsetY(this.fontShadowOffsetY); + singleStringFrame.addAnchor( + new AnchorDefinition(FramePoint.TOPLEFT, currentXCoordForFrames, -usedHeight)); + this.internalFrames.add(singleStringFrame); + currentXCoordForFrames = currentXCoordForWord; + } + currentColor = this.color; + break; + case 'n': + case 'N': { + + final String wordString = currentWord.toString(); + currentWord.setLength(0); + glyphLayout.setText(this.frameFont, wordString); + final float wordWidth = glyphLayout.width; + if ((startingBoundsWidth > 0) && ((currentXCoordForWord + wordWidth) >= startingBoundsWidth)) { + final String currentLineString = currentLine.toString(); + currentLine.setLength(0); + glyphLayout.setText(this.frameFont, currentLineString); + usedWidthMax = Math.max(currentXCoordForFrames + glyphLayout.width, usedWidthMax); + final SingleStringFrame singleStringFrame = new SingleStringFrame(currentLineString, + this.internalFramesContainer, currentColor, TextJustify.LEFT, TextJustify.TOP, + this.frameFont); + singleStringFrame.setHeight(this.frameFont.getLineHeight()); + singleStringFrame.setWidth(glyphLayout.width); + singleStringFrame.setAlpha(this.alpha); + singleStringFrame.setFontShadowColor(this.fontShadowColor); + singleStringFrame.setFontShadowOffsetX(this.fontShadowOffsetX); + singleStringFrame.setFontShadowOffsetY(this.fontShadowOffsetY); + singleStringFrame.addAnchor( + new AnchorDefinition(FramePoint.TOPLEFT, currentXCoordForFrames, -usedHeight)); + this.internalFrames.add(singleStringFrame); + usedHeight += this.frameFont.getLineHeight(); + currentXCoordForWord = 0; + currentXCoordForFrames = 0; + } + currentXCoordForWord += wordWidth; + currentLine.append(wordString); + + final String currentLineString = currentLine.toString(); + currentLine.setLength(0); + glyphLayout.setText(this.frameFont, currentLineString); + usedWidthMax = Math.max(currentXCoordForFrames + glyphLayout.width, usedWidthMax); + final SingleStringFrame singleStringFrame = new SingleStringFrame(currentLineString, + this.internalFramesContainer, currentColor, TextJustify.LEFT, TextJustify.TOP, + this.frameFont); + singleStringFrame.setHeight(this.frameFont.getLineHeight()); + singleStringFrame.setWidth(glyphLayout.width); + singleStringFrame.setAlpha(this.alpha); + singleStringFrame.setFontShadowColor(this.fontShadowColor); + singleStringFrame.setFontShadowOffsetX(this.fontShadowOffsetX); + singleStringFrame.setFontShadowOffsetY(this.fontShadowOffsetY); + singleStringFrame.addAnchor( + new AnchorDefinition(FramePoint.TOPLEFT, currentXCoordForFrames, -usedHeight)); + this.internalFrames.add(singleStringFrame); + usedHeight += this.frameFont.getLineHeight(); + currentXCoordForWord = 0; + currentXCoordForFrames = 0; + + } + i++; + break; + default: + currentWord.append(c); + break; + } + } + } + break; + case ' ': + currentWord.append(' '); + final String wordString = currentWord.toString(); + currentWord.setLength(0); + glyphLayout.setText(this.frameFont, wordString); + final float wordWidth = glyphLayout.width; + if ((startingBoundsWidth > 0) && ((currentXCoordForWord + wordWidth) >= startingBoundsWidth)) { + final String currentLineString = currentLine.toString(); + currentLine.setLength(0); + glyphLayout.setText(this.frameFont, currentLineString); + usedWidthMax = Math.max(currentXCoordForFrames + glyphLayout.width, usedWidthMax); + final SingleStringFrame singleStringFrame = new SingleStringFrame(currentLineString, + this.internalFramesContainer, currentColor, TextJustify.LEFT, TextJustify.TOP, + this.frameFont); + singleStringFrame.setHeight(this.frameFont.getLineHeight()); + singleStringFrame.setWidth(glyphLayout.width); + singleStringFrame.setAlpha(this.alpha); + singleStringFrame.setFontShadowColor(this.fontShadowColor); + singleStringFrame.setFontShadowOffsetX(this.fontShadowOffsetX); + singleStringFrame.setFontShadowOffsetY(this.fontShadowOffsetY); + singleStringFrame + .addAnchor(new AnchorDefinition(FramePoint.TOPLEFT, currentXCoordForFrames, -usedHeight)); + this.internalFrames.add(singleStringFrame); + usedHeight += this.frameFont.getLineHeight(); + currentXCoordForWord = 0; + currentXCoordForFrames = 0; + } + currentXCoordForWord += wordWidth; + currentLine.append(wordString); + break; + default: + currentWord.append(c); + break; + } + } + + { + + final String wordString = currentWord.toString(); + currentWord.setLength(0); + glyphLayout.setText(this.frameFont, wordString); + final float wordWidth = glyphLayout.width; + if ((startingBoundsWidth > 0) && ((currentXCoordForWord + wordWidth) >= startingBoundsWidth)) { + final String currentLineString = currentLine.toString(); + currentLine.setLength(0); + glyphLayout.setText(this.frameFont, currentLineString); + usedWidthMax = Math.max(currentXCoordForFrames + glyphLayout.width, usedWidthMax); + final SingleStringFrame singleStringFrame = new SingleStringFrame(currentLineString, + this.internalFramesContainer, currentColor, TextJustify.LEFT, TextJustify.TOP, this.frameFont); + singleStringFrame.setHeight(this.frameFont.getLineHeight()); + singleStringFrame.setWidth(glyphLayout.width); + singleStringFrame.setAlpha(this.alpha); + singleStringFrame.setFontShadowColor(this.fontShadowColor); + singleStringFrame.setFontShadowOffsetX(this.fontShadowOffsetX); + singleStringFrame.setFontShadowOffsetY(this.fontShadowOffsetY); + singleStringFrame + .addAnchor(new AnchorDefinition(FramePoint.TOPLEFT, currentXCoordForFrames, -usedHeight)); + this.internalFrames.add(singleStringFrame); + usedHeight += this.frameFont.getLineHeight(); + currentXCoordForWord = 0; + currentXCoordForFrames = 0; + } + currentXCoordForWord += wordWidth; + currentLine.append(wordString); + + final String currentLineString = currentLine.toString(); + currentLine.setLength(0); + glyphLayout.setText(this.frameFont, currentLineString); + usedWidthMax = Math.max(currentXCoordForFrames + glyphLayout.width, usedWidthMax); + final SingleStringFrame singleStringFrame = new SingleStringFrame(currentLineString, + this.internalFramesContainer, currentColor, TextJustify.LEFT, TextJustify.TOP, this.frameFont); + singleStringFrame.setHeight(this.frameFont.getLineHeight()); + singleStringFrame.setWidth(glyphLayout.width); + singleStringFrame.setAlpha(this.alpha); + singleStringFrame.setFontShadowColor(this.fontShadowColor); + singleStringFrame.setFontShadowOffsetX(this.fontShadowOffsetX); + singleStringFrame.setFontShadowOffsetY(this.fontShadowOffsetY); + singleStringFrame.addAnchor(new AnchorDefinition(FramePoint.TOPLEFT, currentXCoordForFrames, -usedHeight)); + this.internalFrames.add(singleStringFrame); + currentXCoordForFrames = currentXCoordForWord; + usedHeight += this.frameFont.getCapHeight(); + } + + this.internalFramesContainer.setWidth(usedWidthMax); + this.internalFramesContainer.setHeight(usedHeight); + this.predictedViewportHeight = (usedHeight - this.frameFont.getCapHeight()) + this.frameFont.getLineHeight(); + + this.internalFramesContainer.clearFramePointAssignments(); + switch (this.justifyH) { + case CENTER: + switch (this.justifyV) { + case MIDDLE: + this.internalFramesContainer.addAnchor(new AnchorDefinition(FramePoint.CENTER, 0, 0)); + break; + case BOTTOM: + this.internalFramesContainer.addAnchor(new AnchorDefinition(FramePoint.BOTTOM, 0, 0)); + break; + case TOP: + default: + this.internalFramesContainer.addAnchor(new AnchorDefinition(FramePoint.TOP, 0, 0)); + break; + } + break; + case RIGHT: + switch (this.justifyV) { + case MIDDLE: + this.internalFramesContainer.addAnchor(new AnchorDefinition(FramePoint.RIGHT, 0, 0)); + break; + case BOTTOM: + this.internalFramesContainer.addAnchor(new AnchorDefinition(FramePoint.BOTTOMRIGHT, 0, 0)); + break; + case TOP: + default: + this.internalFramesContainer.addAnchor(new AnchorDefinition(FramePoint.TOPRIGHT, 0, 0)); + break; + } + break; + case LEFT: + default: + switch (this.justifyV) { + case MIDDLE: + this.internalFramesContainer.addAnchor(new AnchorDefinition(FramePoint.LEFT, 0, 0)); + break; + case BOTTOM: + this.internalFramesContainer.addAnchor(new AnchorDefinition(FramePoint.BOTTOMLEFT, 0, 0)); + break; + case TOP: + default: + this.internalFramesContainer.addAnchor(new AnchorDefinition(FramePoint.TOPLEFT, 0, 0)); + break; + } + break; + } + + for (final SingleStringFrame internalFrame : this.internalFrames) { + this.internalFramesContainer.add(internalFrame); + } + } + + public void setAlpha(final float alpha) { + this.alpha = alpha; + for (final SingleStringFrame internalFrame : this.internalFrames) { + internalFrame.setAlpha(alpha); + } + + } + + public float getPredictedViewportHeight() { + return this.predictedViewportHeight; + } + + public BitmapFont getFrameFont() { + return this.frameFont; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/TextButtonFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/TextButtonFrame.java new file mode 100644 index 0000000..6e08134 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/TextButtonFrame.java @@ -0,0 +1,18 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +public class TextButtonFrame extends GlueTextButtonFrame { + private float buttonPushedTextOffsetX; + private float buttonPushedTextOffsetY; + + public TextButtonFrame(final String name, final UIFrame parent) { + super(name, parent); + } + + public void setButtonPushedTextOffsetX(final float buttonPushedTextOffsetX) { + this.buttonPushedTextOffsetX = buttonPushedTextOffsetX; + } + + public void setButtonPushedTextOffsetY(final float buttonPushedTextOffsetY) { + this.buttonPushedTextOffsetY = buttonPushedTextOffsetY; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/TextureFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/TextureFrame.java new file mode 100644 index 0000000..22b77a6 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/TextureFrame.java @@ -0,0 +1,86 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.datamodel.Vector4Definition; + +public class TextureFrame extends AbstractRenderableFrame { + private TextureRegion texture; + private final boolean decorateFileNames; + private final Vector4Definition texCoord; + private Color color; + + public TextureFrame(final String name, final UIFrame parent, final boolean decorateFileNames, + final Vector4Definition texCoord) { + super(name, parent); + this.decorateFileNames = decorateFileNames; + this.texCoord = texCoord; + } + + @Override + protected void internalRender(final SpriteBatch batch, final BitmapFont baseFont, final GlyphLayout glyphLayout) { + if (this.texture == null) { + return; + } + if (this.color != null) { + batch.setColor(this.color); + } + batch.draw(this.texture, this.renderBounds.x, this.renderBounds.y, this.renderBounds.width, + this.renderBounds.height); + if (this.color != null) { + batch.setColor(1f, 1f, 1f, 1f); + } + } + + @Override + protected void innerPositionBounds(final GameUI gameUI, final Viewport viewport) { + } + + public void setColor(final Color color) { + this.color = color; + } + + public void setTexture(String file, final GameUI gameUI) { + if (this.decorateFileNames) { + file = gameUI.trySkinField(file); + } + final Texture texture = gameUI.loadTexture(file); + if (texture != null) { + setTexture(texture); + } + } + + public void setTexCoord(final float x, final float y, final float z, final float w) { + this.texCoord.set(x, y, z, w); + if (this.texture != null) { + this.texture.setRegion(this.texCoord.getX(), this.texCoord.getZ(), this.texCoord.getY(), + this.texCoord.getW()); + } + } + + public void setTexture(final Texture texture) { + if (texture == null) { + this.texture = null; + return; + } + final TextureRegion texRegion; + if (this.texCoord != null) { + texRegion = new TextureRegion(texture, this.texCoord.getX(), this.texCoord.getZ(), this.texCoord.getY(), + this.texCoord.getW()); + } + else { + texRegion = new TextureRegion(texture); + } + this.texture = texRegion; + } + + public void setTexture(final TextureRegion texture) { + this.texture = texture; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/fdf/frames/UIFrame.java b/core/src/com/etheller/warsmash/parsers/fdf/frames/UIFrame.java new file mode 100644 index 0000000..3bd83b1 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/fdf/frames/UIFrame.java @@ -0,0 +1,55 @@ +package com.etheller.warsmash.parsers.fdf.frames; + +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.datamodel.AnchorDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.FramePoint; + +public interface UIFrame { + public void render(SpriteBatch batch, BitmapFont baseFont, GlyphLayout glyphLayout); + + public float getFramePointX(FramePoint framePoint); + + public float getFramePointY(FramePoint framePoint); + + void setFramePointX(final FramePoint framePoint, final float x); + + void setFramePointY(final FramePoint framePoint, final float y); + + void positionBounds(GameUI gameUI, final Viewport viewport); + + void addAnchor(final AnchorDefinition anchorDefinition); + + void addSetPoint(SetPoint setPointDefinition); + + void setWidth(final float width); + + void setHeight(final float height); + + float getAssignedWidth(); + + float getAssignedHeight(); + + void setSetAllPoints(boolean setAllPoints); + + void setSetAllPoints(boolean setAllPoints, float inset); + + void setVisible(boolean visible); + + UIFrame getParent(); + + boolean isVisible(); + + boolean isVisibleOnScreen(); + + UIFrame touchDown(float screenX, float screenY, int button); + + UIFrame touchUp(float screenX, float screenY, int button); + + UIFrame getFrameChildUnderMouse(float screenX, float screenY); + + String getName(); +} diff --git a/core/src/com/etheller/warsmash/parsers/jass/Jass2.java b/core/src/com/etheller/warsmash/parsers/jass/Jass2.java new file mode 100644 index 0000000..6246602 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/jass/Jass2.java @@ -0,0 +1,2864 @@ +package com.etheller.warsmash.parsers.jass; + +import java.awt.geom.Point2D; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.interpreter.JassLexer; +import com.etheller.interpreter.JassParser; +import com.etheller.interpreter.ast.function.JassFunction; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.scope.trigger.Trigger; +import com.etheller.interpreter.ast.scope.trigger.TriggerBooleanExpression; +import com.etheller.interpreter.ast.value.BooleanJassValue; +import com.etheller.interpreter.ast.value.HandleJassType; +import com.etheller.interpreter.ast.value.HandleJassValue; +import com.etheller.interpreter.ast.value.IntegerJassValue; +import com.etheller.interpreter.ast.value.JassValue; +import com.etheller.interpreter.ast.value.RealJassValue; +import com.etheller.interpreter.ast.value.StringJassValue; +import com.etheller.interpreter.ast.value.visitor.BooleanJassValueVisitor; +import com.etheller.interpreter.ast.value.visitor.IntegerJassValueVisitor; +import com.etheller.interpreter.ast.value.visitor.JassFunctionJassValueVisitor; +import com.etheller.interpreter.ast.value.visitor.ObjectJassValueVisitor; +import com.etheller.interpreter.ast.value.visitor.RealJassValueVisitor; +import com.etheller.interpreter.ast.value.visitor.StringJassValueVisitor; +import com.etheller.interpreter.ast.visitors.JassProgramVisitor; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.parsers.fdf.GameSkin; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.datamodel.AnchorDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.FramePoint; +import com.etheller.warsmash.parsers.fdf.frames.SetPoint; +import com.etheller.warsmash.parsers.fdf.frames.StringFrame; +import com.etheller.warsmash.parsers.fdf.frames.UIFrame; +import com.etheller.warsmash.parsers.jass.scope.CommonTriggerExecutionScope; +import com.etheller.warsmash.parsers.jass.triggers.BoolExprAnd; +import com.etheller.warsmash.parsers.jass.triggers.BoolExprCondition; +import com.etheller.warsmash.parsers.jass.triggers.BoolExprFilter; +import com.etheller.warsmash.parsers.jass.triggers.BoolExprNot; +import com.etheller.warsmash.parsers.jass.triggers.BoolExprOr; +import com.etheller.warsmash.parsers.jass.triggers.TriggerAction; +import com.etheller.warsmash.parsers.jass.triggers.TriggerCondition; +import com.etheller.warsmash.units.Element; +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.ability.ItemUI; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CDestructableType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitEnumFunction; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.ai.AIDifficulty; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.config.CPlayerAPI; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.config.War3MapConfig; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.item.CItemTypeJass; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIdUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CAllianceType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CMapControl; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CMapFlag; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CMapPlacement; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayerColor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayerGameResult; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayerJass; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayerScore; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayerState; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CRace; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CRacePreference; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CStartLocPrio; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.region.CRegion; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.state.CGameState; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.state.CUnitState; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.timers.CTimerJass; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.JassGameEventsWar3; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CAttackTypeJass; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CBlendMode; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CCameraField; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CDamageType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CEffectType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CFogState; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CGameSpeed; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CGameType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CLimitOp; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CMapDensity; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CMapDifficulty; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CPathingTypeJass; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CPlayerSlotState; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CRarityControl; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CSoundType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CSoundVolumeGroup; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CTexMapFlags; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CVersion; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CWeaponSoundTypeJass; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.unit.CUnitTypeJass; + +public class Jass2 { + public static final boolean REPORT_SYNTAX_ERRORS = true; + + public static CommonEnvironment loadCommon(final DataSource dataSource, final Viewport uiViewport, + final Scene uiScene, final War3MapViewer war3MapViewer, final RootFrameListener rootFrameListener, + final String... files) { + + final JassProgramVisitor jassProgramVisitor = new JassProgramVisitor(); + final CommonEnvironment environment = new CommonEnvironment(jassProgramVisitor, dataSource, uiViewport, uiScene, + war3MapViewer, rootFrameListener); + for (final String jassFile : files) { + try { + JassLexer lexer; + try { + lexer = new JassLexer(CharStreams.fromStream(dataSource.getResourceAsStream(jassFile))); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + final JassParser parser = new JassParser(new CommonTokenStream(lexer)); +// parser.removeErrorListener(ConsoleErrorListener.INSTANCE); + parser.addErrorListener(new BaseErrorListener() { + @Override + public void syntaxError(final Recognizer recognizer, final Object offendingSymbol, + final int line, final int charPositionInLine, final String msg, + final RecognitionException e) { + if (!REPORT_SYNTAX_ERRORS) { + return; + } + + final String sourceName = String.format("%s:%d:%d: ", jassFile, line, charPositionInLine); + + System.err.println(sourceName + "line " + line + ":" + charPositionInLine + " " + msg); + } + }); + jassProgramVisitor.visit(parser.program()); + } + catch (final Exception e) { + e.printStackTrace(); + } + } + jassProgramVisitor.getJassNativeManager().checkUnregisteredNatives(); + return environment; + } + + public static JUIEnvironment loadJUI(final DataSource dataSource, final Viewport uiViewport, final Scene uiScene, + final War3MapViewer war3MapViewer, final RootFrameListener rootFrameListener, final String... files) { + + final JassProgramVisitor jassProgramVisitor = new JassProgramVisitor(); + final JUIEnvironment environment = new JUIEnvironment(jassProgramVisitor, dataSource, uiViewport, uiScene, + war3MapViewer, rootFrameListener); + for (final String jassFile : files) { + try { + JassLexer lexer; + try { + lexer = new JassLexer(CharStreams.fromStream(dataSource.getResourceAsStream(jassFile))); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + final JassParser parser = new JassParser(new CommonTokenStream(lexer)); +// parser.removeErrorListener(ConsoleErrorListener.INSTANCE); + parser.addErrorListener(new BaseErrorListener() { + @Override + public void syntaxError(final Recognizer recognizer, final Object offendingSymbol, + final int line, final int charPositionInLine, final String msg, + final RecognitionException e) { + if (!REPORT_SYNTAX_ERRORS) { + return; + } + + final String sourceName = String.format("%s:%d:%d: ", jassFile, line, charPositionInLine); + + System.err.println(sourceName + "line " + line + ":" + charPositionInLine + " " + msg); + } + }); + jassProgramVisitor.visit(parser.program()); + } + catch (final Exception e) { + e.printStackTrace(); + } + } + jassProgramVisitor.getJassNativeManager().checkUnregisteredNatives(); + return environment; + } + + public static interface RootFrameListener { + void onCreate(GameUI rootFrame); + } + + private static final class JUIEnvironment { + private GameUI gameUI; + private Element skin; + + public JUIEnvironment(final JassProgramVisitor jassProgramVisitor, final DataSource dataSource, + final Viewport uiViewport, final Scene uiScene, final War3MapViewer war3MapViewer, + final RootFrameListener rootFrameListener) { + final GlobalScope globals = jassProgramVisitor.getGlobals(); + final HandleJassType frameHandleType = globals.registerHandleType("framehandle"); + final HandleJassType framePointType = globals.registerHandleType("framepointtype"); + final HandleJassType triggerType = globals.registerHandleType("trigger"); + final HandleJassType triggerActionType = globals.registerHandleType("triggeraction"); + final HandleJassType triggerConditionType = globals.registerHandleType("triggercondition"); + final HandleJassType boolExprType = globals.registerHandleType("boolexpr"); + final HandleJassType conditionFuncType = globals.registerHandleType("conditionfunc"); + final HandleJassType filterType = globals.registerHandleType("filterfunc"); + jassProgramVisitor.getJassNativeManager().createNative("LogError", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String stringValue = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + System.err.println(stringValue); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertFramePointType", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int value = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(framePointType, FramePoint.values()[value]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("CreateRootFrame", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String skinArg = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + final GameSkin skin = GameUI.loadSkin(dataSource, skinArg); + final GameUI gameUI = new GameUI(dataSource, skin, uiViewport, uiScene, war3MapViewer, 0, + war3MapViewer.getAllObjectData().getWts()); + JUIEnvironment.this.gameUI = gameUI; + JUIEnvironment.this.skin = skin.getSkin(); + rootFrameListener.onCreate(gameUI); + return new HandleJassValue(frameHandleType, gameUI); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("LoadTOCFile", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String tocFileName = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + try { + JUIEnvironment.this.gameUI.loadTOCFile(tocFileName); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + return BooleanJassValue.TRUE; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("CreateSimpleFrame", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String templateName = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + final UIFrame ownerFrame = arguments.get(1).visit(ObjectJassValueVisitor.getInstance()); + final int createContext = arguments.get(2).visit(IntegerJassValueVisitor.getInstance()); + + final UIFrame simpleFrame = JUIEnvironment.this.gameUI.createSimpleFrame(templateName, ownerFrame, + createContext); + + return new HandleJassValue(frameHandleType, simpleFrame); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("CreateFrame", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String templateName = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + final UIFrame ownerFrame = arguments.get(1).visit(ObjectJassValueVisitor.getInstance()); + final int priority = arguments.get(2).visit(IntegerJassValueVisitor.getInstance()); + final int createContext = arguments.get(3).visit(IntegerJassValueVisitor.getInstance()); + + final UIFrame simpleFrame = JUIEnvironment.this.gameUI.createFrame(templateName, ownerFrame, + priority, createContext); + + return new HandleJassValue(frameHandleType, simpleFrame); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetFrameByName", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String templateName = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + final int createContext = arguments.get(1).visit(IntegerJassValueVisitor.getInstance()); + + final UIFrame simpleFrame = JUIEnvironment.this.gameUI.getFrameByName(templateName, createContext); + return new HandleJassValue(frameHandleType, simpleFrame); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("FrameSetAnchor", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final UIFrame frame = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final FramePoint framePoint = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + final double x = arguments.get(2).visit(RealJassValueVisitor.getInstance()); + final double y = arguments.get(3).visit(RealJassValueVisitor.getInstance()); + + frame.addAnchor(new AnchorDefinition(framePoint, GameUI.convertX(uiViewport, (float) x), + GameUI.convertY(uiViewport, (float) y))); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("FrameSetAbsPoint", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final UIFrame frame = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final FramePoint framePoint = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + final double x = arguments.get(2).visit(RealJassValueVisitor.getInstance()); + final double y = arguments.get(3).visit(RealJassValueVisitor.getInstance()); + + frame.setFramePointX(framePoint, GameUI.convertX(uiViewport, (float) x)); + frame.setFramePointY(framePoint, GameUI.convertY(uiViewport, (float) y)); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("FrameSetPoint", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final UIFrame frame = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final FramePoint framePoint = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + final UIFrame otherFrame = arguments.get(2).visit(ObjectJassValueVisitor.getInstance()); + final FramePoint otherPoint = arguments.get(3) + .visit(ObjectJassValueVisitor.getInstance()); + final double x = arguments.get(2).visit(RealJassValueVisitor.getInstance()); + final double y = arguments.get(3).visit(RealJassValueVisitor.getInstance()); + + frame.addSetPoint(new SetPoint(framePoint, otherFrame, otherPoint, + GameUI.convertX(uiViewport, (float) x), GameUI.convertY(uiViewport, (float) y))); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("FrameSetText", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final StringFrame frame = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final String text = arguments.get(1).visit(StringJassValueVisitor.getInstance()); + + JUIEnvironment.this.gameUI.setText(frame, text); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("FrameSetTextColor", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final StringFrame frame = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final int colorInt = arguments.get(1).visit(IntegerJassValueVisitor.getInstance()); + frame.setColor(new Color(colorInt)); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertColor", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int a = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + final int r = arguments.get(1).visit(IntegerJassValueVisitor.getInstance()); + final int g = arguments.get(2).visit(IntegerJassValueVisitor.getInstance()); + final int b = arguments.get(3).visit(IntegerJassValueVisitor.getInstance()); + return new IntegerJassValue(a | (b << 8) | (g << 16) | (r << 24)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("FramePositionBounds", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final UIFrame frame = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + frame.positionBounds(JUIEnvironment.this.gameUI, uiViewport); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SkinGetField", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String fieldName = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + return new StringJassValue(JUIEnvironment.this.skin.getField(fieldName)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("CreateTrigger", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new HandleJassValue(triggerType, new Trigger()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("DestroyTrigger", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Trigger trigger = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + trigger.destroy(); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("EnableTrigger", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Trigger trigger = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + trigger.setEnabled(true); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("DisableTrigger", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Trigger trigger = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + trigger.setEnabled(false); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("IsTriggerEnabled", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Trigger trigger = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + return BooleanJassValue.of(trigger.isEnabled()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("Condition", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final JassFunction func = arguments.get(0).visit(JassFunctionJassValueVisitor.getInstance()); + return new HandleJassValue(conditionFuncType, new BoolExprCondition(func)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("Filter", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final JassFunction func = arguments.get(0).visit(JassFunctionJassValueVisitor.getInstance()); + return new HandleJassValue(filterType, new BoolExprFilter(func)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("DestroyCondition", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final BoolExprCondition trigger = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + System.err.println( + "DestroyCondition called but in Java we don't have a destructor, so we need to unregister later when that is implemented"); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("DestroyFilter", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final BoolExprFilter trigger = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + System.err.println( + "DestroyFilter called but in Java we don't have a destructor, so we need to unregister later when that is implemented"); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("DestroyBoolExpr", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final TriggerBooleanExpression trigger = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + System.err.println( + "DestroyBoolExpr called but in Java we don't have a destructor, so we need to unregister later when that is implemented"); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("And", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final TriggerBooleanExpression operandA = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final TriggerBooleanExpression operandB = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + return new HandleJassValue(boolExprType, new BoolExprAnd(operandA, operandB)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("Or", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final TriggerBooleanExpression operandA = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final TriggerBooleanExpression operandB = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + return new HandleJassValue(boolExprType, new BoolExprOr(operandA, operandB)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("Not", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final TriggerBooleanExpression operand = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + return new HandleJassValue(boolExprType, new BoolExprNot(operand)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("TriggerAddCondition", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Trigger whichTrigger = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final TriggerBooleanExpression condition = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + final int index = whichTrigger.addCondition(condition); + return new HandleJassValue(triggerConditionType, + new TriggerCondition(condition, whichTrigger, index)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("TriggerRemoveCondition", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Trigger whichTrigger = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final TriggerCondition condition = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + if (condition.getTrigger() != whichTrigger) { + throw new IllegalArgumentException("Unable to remove condition, wrong trigger"); + } + whichTrigger.removeConditionAtIndex(condition.getConditionIndex()); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("TriggerAddAction", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Trigger whichTrigger = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final JassFunction actionFunc = arguments.get(1).visit(JassFunctionJassValueVisitor.getInstance()); + final int actionIndex = whichTrigger.addAction(actionFunc); + return new HandleJassValue(triggerActionType, + new TriggerAction(whichTrigger, actionFunc, actionIndex)); + } + }); + } + } + + private static final class CommonEnvironment { + private GameUI gameUI; + private Element skin; + + public CommonEnvironment(final JassProgramVisitor jassProgramVisitor, final DataSource dataSource, + final Viewport uiViewport, final Scene uiScene, final War3MapViewer war3MapViewer, + final RootFrameListener rootFrameListener) { + final Rectangle tempRect = new Rectangle(); + final CSimulation simulation = war3MapViewer.simulation; + final GlobalScope globals = jassProgramVisitor.getGlobals(); + final HandleJassType agentType = globals.registerHandleType("agent"); + final HandleJassType eventType = globals.registerHandleType("event"); + final HandleJassType playerType = globals.registerHandleType("player"); + final HandleJassType widgetType = globals.registerHandleType("widget"); + final HandleJassType unitType = globals.registerHandleType("unit"); + final HandleJassType destructableType = globals.registerHandleType("destructable"); + final HandleJassType itemType = globals.registerHandleType("item"); + final HandleJassType abilityType = globals.registerHandleType("ability"); + final HandleJassType buffType = globals.registerHandleType("buff"); + final HandleJassType forceType = globals.registerHandleType("force"); + final HandleJassType groupType = globals.registerHandleType("group"); + final HandleJassType triggerType = globals.registerHandleType("trigger"); + final HandleJassType triggerconditionType = globals.registerHandleType("triggercondition"); + final HandleJassType triggeractionType = globals.registerHandleType("triggeraction"); + final HandleJassType timerType = globals.registerHandleType("timer"); + final HandleJassType locationType = globals.registerHandleType("location"); + final HandleJassType regionType = globals.registerHandleType("region"); + final HandleJassType rectType = globals.registerHandleType("rect"); + final HandleJassType boolexprType = globals.registerHandleType("boolexpr"); + final HandleJassType soundType = globals.registerHandleType("sound"); + final HandleJassType conditionfuncType = globals.registerHandleType("conditionfunc"); + final HandleJassType filterfuncType = globals.registerHandleType("filterfunc"); + final HandleJassType unitpoolType = globals.registerHandleType("unitpool"); + final HandleJassType itempoolType = globals.registerHandleType("itempool"); + final HandleJassType raceType = globals.registerHandleType("race"); + final HandleJassType alliancetypeType = globals.registerHandleType("alliancetype"); + final HandleJassType racepreferenceType = globals.registerHandleType("racepreference"); + final HandleJassType gamestateType = globals.registerHandleType("gamestate"); + final HandleJassType igamestateType = globals.registerHandleType("igamestate"); + final HandleJassType fgamestateType = globals.registerHandleType("fgamestate"); + final HandleJassType playerstateType = globals.registerHandleType("playerstate"); + final HandleJassType playerscoreType = globals.registerHandleType("playerscore"); + final HandleJassType playergameresultType = globals.registerHandleType("playergameresult"); + final HandleJassType unitstateType = globals.registerHandleType("unitstate"); + final HandleJassType aidifficultyType = globals.registerHandleType("aidifficulty"); + final HandleJassType eventidType = globals.registerHandleType("eventid"); + final HandleJassType gameeventType = globals.registerHandleType("gameevent"); + final HandleJassType playereventType = globals.registerHandleType("playerevent"); + final HandleJassType playeruniteventType = globals.registerHandleType("playerunitevent"); + final HandleJassType uniteventType = globals.registerHandleType("unitevent"); + final HandleJassType limitopType = globals.registerHandleType("limitop"); + final HandleJassType widgeteventType = globals.registerHandleType("widgetevent"); + final HandleJassType dialogeventType = globals.registerHandleType("dialogevent"); + final HandleJassType unittypeType = globals.registerHandleType("unittype"); + final HandleJassType gamespeedType = globals.registerHandleType("gamespeed"); + final HandleJassType gamedifficultyType = globals.registerHandleType("gamedifficulty"); + final HandleJassType gametypeType = globals.registerHandleType("gametype"); + final HandleJassType mapflagType = globals.registerHandleType("mapflag"); + final HandleJassType mapvisibilityType = globals.registerHandleType("mapvisibility"); + final HandleJassType mapsettingType = globals.registerHandleType("mapsetting"); + final HandleJassType mapdensityType = globals.registerHandleType("mapdensity"); + final HandleJassType mapcontrolType = globals.registerHandleType("mapcontrol"); + final HandleJassType playerslotstateType = globals.registerHandleType("playerslotstate"); + final HandleJassType volumegroupType = globals.registerHandleType("volumegroup"); + final HandleJassType camerafieldType = globals.registerHandleType("camerafield"); + final HandleJassType camerasetupType = globals.registerHandleType("camerasetup"); + final HandleJassType playercolorType = globals.registerHandleType("playercolor"); + final HandleJassType placementType = globals.registerHandleType("placement"); + final HandleJassType startlocprioType = globals.registerHandleType("startlocprio"); + final HandleJassType raritycontrolType = globals.registerHandleType("raritycontrol"); + final HandleJassType blendmodeType = globals.registerHandleType("blendmode"); + final HandleJassType texmapflagsType = globals.registerHandleType("texmapflags"); + final HandleJassType effectType = globals.registerHandleType("effect"); + final HandleJassType effecttypeType = globals.registerHandleType("effecttype"); + final HandleJassType weathereffectType = globals.registerHandleType("weathereffect"); + final HandleJassType terraindeformationType = globals.registerHandleType("terraindeformation"); + final HandleJassType fogstateType = globals.registerHandleType("fogstate"); + final HandleJassType fogmodifierType = globals.registerHandleType("fogmodifier"); + final HandleJassType dialogType = globals.registerHandleType("dialog"); + final HandleJassType buttonType = globals.registerHandleType("button"); + final HandleJassType questType = globals.registerHandleType("quest"); + final HandleJassType questitemType = globals.registerHandleType("questitem"); + final HandleJassType defeatconditionType = globals.registerHandleType("defeatcondition"); + final HandleJassType timerdialogType = globals.registerHandleType("timerdialog"); + final HandleJassType leaderboardType = globals.registerHandleType("leaderboard"); + final HandleJassType multiboardType = globals.registerHandleType("multiboard"); + final HandleJassType multiboarditemType = globals.registerHandleType("multiboarditem"); + final HandleJassType trackableType = globals.registerHandleType("trackable"); + final HandleJassType gamecacheType = globals.registerHandleType("gamecache"); + final HandleJassType versionType = globals.registerHandleType("version"); + final HandleJassType itemtypeType = globals.registerHandleType("itemtype"); + final HandleJassType texttagType = globals.registerHandleType("texttag"); + final HandleJassType attacktypeType = globals.registerHandleType("attacktype"); + final HandleJassType damagetypeType = globals.registerHandleType("damagetype"); + final HandleJassType weapontypeType = globals.registerHandleType("weapontype"); + final HandleJassType soundtypeType = globals.registerHandleType("soundtype"); + final HandleJassType lightningType = globals.registerHandleType("lightning"); + final HandleJassType pathingtypeType = globals.registerHandleType("pathingtype"); + final HandleJassType imageType = globals.registerHandleType("image"); + final HandleJassType ubersplatType = globals.registerHandleType("ubersplat"); + final HandleJassType hashtableType = globals.registerHandleType("hashtable"); + + jassProgramVisitor.getJassNativeManager().createNative("ConvertRace", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(raceType, CRace.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertAllianceType", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(alliancetypeType, CAllianceType.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertRacePref", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(racepreferenceType, CRacePreference.getById(i)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertIGameState", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(igamestateType, CGameState.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertFGameState", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(fgamestateType, CGameState.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertPlayerState", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(playerstateType, CPlayerState.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertPlayerScore", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(playerscoreType, CPlayerScore.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertGameResult", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(playergameresultType, CPlayerGameResult.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertUnitState", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(unitstateType, CUnitState.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertAIDifficulty", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(aidifficultyType, AIDifficulty.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertGameEvent", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(gameeventType, JassGameEventsWar3.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertPlayerEvent", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(playereventType, JassGameEventsWar3.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertPlayerUnitEvent", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(playeruniteventType, JassGameEventsWar3.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertWidgetEvent", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(widgeteventType, JassGameEventsWar3.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertDialogEvent", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(dialogeventType, JassGameEventsWar3.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertUnitEvent", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(uniteventType, JassGameEventsWar3.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertLimitOp", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(limitopType, CLimitOp.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertUnitType", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(unittypeType, CUnitTypeJass.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertGameSpeed", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(gamespeedType, CGameSpeed.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertPlacement", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(placementType, CMapPlacement.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertStartLocPrio", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(startlocprioType, CStartLocPrio.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertGameDifficulty", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(gamedifficultyType, CMapDifficulty.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertGameType", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(gametypeType, CGameType.getById(i)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertMapFlag", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(gametypeType, CMapFlag.getById(i)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertMapVisibility", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new HandleJassValue(mapvisibilityType, null); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertMapSetting", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new HandleJassValue(mapsettingType, null); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertMapDensity", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(mapdensityType, CMapDensity.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertMapControl", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(mapcontrolType, CMapControl.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertPlayerColor", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(playercolorType, CMapControl.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertPlayerSlotState", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(playerslotstateType, CPlayerSlotState.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertVolumeGroup", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(volumegroupType, CSoundVolumeGroup.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertCameraField", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(camerafieldType, CCameraField.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertBlendMode", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(blendmodeType, CBlendMode.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertRarityControl", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(raritycontrolType, CRarityControl.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertTexMapFlags", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(texmapflagsType, CTexMapFlags.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertFogState", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(fogstateType, CFogState.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertEffectType", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(effecttypeType, CEffectType.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertVersion", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(versionType, CVersion.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertItemType", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(itemtypeType, CItemTypeJass.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertAttackType", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(attacktypeType, CAttackTypeJass.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertDamageType", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(attacktypeType, CDamageType.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertWeaponType", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(weapontypeType, CWeaponSoundTypeJass.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertSoundType", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(soundtypeType, CSoundType.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ConvertPathingType", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final int i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(pathingtypeType, CPathingTypeJass.VALUES[i]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("OrderId", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String idString = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + final int orderId = OrderIdUtils.getOrderId(idString); + return new IntegerJassValue(orderId); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("OrderId2String", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Integer id = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new StringJassValue(OrderIdUtils.getStringFromOrderId(id)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("UnitId", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String idString = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + final CUnitType unitType = simulation.getUnitData().getUnitTypeByJassLegacyName(idString); + if (unitType == null) { + return new IntegerJassValue(0); + } + return new IntegerJassValue(unitType.getTypeId().getValue()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("UnitId2String", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Integer id = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + final War3ID war3id = new War3ID(id); + return new StringJassValue(simulation.getUnitData().getUnitType(war3id).getLegacyName()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("AbilityId", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new IntegerJassValue(0); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("AbilityId2String", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new StringJassValue(""); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetObjectName", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Integer id = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + final War3ID war3id = new War3ID(id); + final CUnitType unitType = simulation.getUnitData().getUnitType(war3id); + if (unitType != null) { + return new StringJassValue(unitType.getName()); + } + // TODO for now this looks in the ability editor data, not the fast symbol table + // layer on top, because the layer on top forgot to have a name value... + final MutableGameObject abilityEditorData = war3MapViewer.getAllObjectData().getAbilities() + .get(war3id); + if (abilityEditorData != null) { + return new StringJassValue(abilityEditorData.getName()); + } + final ItemUI itemUI = war3MapViewer.getAbilityDataUI().getItemUI(war3id); + if (itemUI != null) { + return new StringJassValue(itemUI.getName()); + } + final CDestructableType destructableType = simulation.getDestructableData().getUnitType(war3id); + if (destructableType != null) { + return new StringJassValue(destructableType.getName()); + } + return new StringJassValue(""); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("Deg2Rad", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Double value = arguments.get(0).visit(RealJassValueVisitor.getInstance()); + return new RealJassValue(StrictMath.toRadians(value)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("Rad2Deg", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Double value = arguments.get(0).visit(RealJassValueVisitor.getInstance()); + return new RealJassValue(StrictMath.toDegrees(value)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("Sin", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Double value = arguments.get(0).visit(RealJassValueVisitor.getInstance()); + return new RealJassValue(StrictMath.sin(value)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("Cos", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Double value = arguments.get(0).visit(RealJassValueVisitor.getInstance()); + return new RealJassValue(StrictMath.cos(value)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("Tan", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Double value = arguments.get(0).visit(RealJassValueVisitor.getInstance()); + return new RealJassValue(StrictMath.tan(value)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("Asin", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Double value = arguments.get(0).visit(RealJassValueVisitor.getInstance()); + final double result = StrictMath.asin(value); + if (Double.isNaN(result)) { + return new RealJassValue(0); + } + return new RealJassValue(result); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("Acos", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Double value = arguments.get(0).visit(RealJassValueVisitor.getInstance()); + final double result = StrictMath.acos(value); + if (Double.isNaN(result)) { + return new RealJassValue(0); + } + return new RealJassValue(result); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("Atan", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Double value = arguments.get(0).visit(RealJassValueVisitor.getInstance()); + final double result = StrictMath.atan(value); + if (Double.isNaN(result)) { + return new RealJassValue(0); + } + return new RealJassValue(result); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("Atan2", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Double y = arguments.get(0).visit(RealJassValueVisitor.getInstance()); + final Double x = arguments.get(1).visit(RealJassValueVisitor.getInstance()); + final double result = StrictMath.atan2(y, x); + if (Double.isNaN(result)) { + return new RealJassValue(0); + } + return new RealJassValue(result); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SquareRoot", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Double value = arguments.get(0).visit(RealJassValueVisitor.getInstance()); + final double result = StrictMath.sqrt(value); + return new RealJassValue(result); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("Pow", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Double y = arguments.get(0).visit(RealJassValueVisitor.getInstance()); + final Double x = arguments.get(1).visit(RealJassValueVisitor.getInstance()); + final double result = StrictMath.pow(y, x); + if (Double.isNaN(result)) { + return new RealJassValue(0); + } + return new RealJassValue(result); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("I2R", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Integer i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new RealJassValue(i.doubleValue()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("R2I", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Double r = arguments.get(0).visit(RealJassValueVisitor.getInstance()); + return new IntegerJassValue(r.intValue()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("I2S", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Integer i = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new StringJassValue(i.toString()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("R2S", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Double r = arguments.get(0).visit(RealJassValueVisitor.getInstance()); + return new StringJassValue(r.toString()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("R2SW", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Double r = arguments.get(0).visit(RealJassValueVisitor.getInstance()); + final int width = arguments.get(1).visit(IntegerJassValueVisitor.getInstance()); + final int precision = arguments.get(2).visit(IntegerJassValueVisitor.getInstance()); + return new StringJassValue(String.format("%" + precision + "." + width + "f", r)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("S2I", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String s = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + try { + final int intValue = Integer.parseInt(s); + return new IntegerJassValue(intValue); + } + catch (final Exception exc) { + return new IntegerJassValue(0); + } + } + }); + jassProgramVisitor.getJassNativeManager().createNative("S2R", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String s = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + try { + final double parsedValue = Double.parseDouble(s); + return new RealJassValue(parsedValue); + } + catch (final Exception exc) { + return new RealJassValue(0); + } + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SubString", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String s = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + final int start = arguments.get(1).visit(IntegerJassValueVisitor.getInstance()); + final int end = arguments.get(2).visit(IntegerJassValueVisitor.getInstance()); + return new StringJassValue(s.substring(start, end)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("StringLength", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String s = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + return new IntegerJassValue(s.length()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("StringCase", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String s = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + final boolean upper = arguments.get(1).visit(BooleanJassValueVisitor.getInstance()); + return new StringJassValue(upper ? s.toUpperCase(Locale.US) : s.toLowerCase(Locale.US)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("StringHash", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String s = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + return new IntegerJassValue(s.hashCode()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetLocalizedString", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String key = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + // TODO this might be wrong, or a subset of the needed return values + final String decoratedString = war3MapViewer.getGameUI().getTemplates().getDecoratedString(key); + if (key.equals(decoratedString)) { + System.err.println("GetLocalizedString: NOT FOUND: " + key); + } + return new StringJassValue(decoratedString); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetLocalizedHotkey", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String key = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + // TODO this might be wrong, or a subset of the needed return values + final String decoratedString = war3MapViewer.getGameUI().getTemplates().getDecoratedString(key); + if (key.equals(decoratedString)) { + System.err.println("GetLocalizedHotkey: NOT FOUND: " + key); + } + return new IntegerJassValue(decoratedString.charAt(0)); + } + }); + final War3MapConfig mapConfig = war3MapViewer.getMapConfig(); + registerConfigNatives(jassProgramVisitor, mapConfig, startlocprioType, gametypeType, placementType, + gamespeedType, gamedifficultyType, mapdensityType, locationType, playerType, playercolorType, + mapcontrolType, playerslotstateType, simulation); + + // ============================================================================ + // Timer API + // + jassProgramVisitor.getJassNativeManager().createNative("CreateTimer", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new HandleJassValue(timerType, new CTimerJass(globalScope)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("DestroyTimer", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CTimerJass timer = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + simulation.unregisterTimer(timer); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("TimerStart", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CTimerJass timer = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final Double timeout = arguments.get(1).visit(RealJassValueVisitor.getInstance()); + final boolean periodic = arguments.get(2).visit(BooleanJassValueVisitor.getInstance()); + final JassFunction handlerFunc = arguments.get(3).visit(JassFunctionJassValueVisitor.getInstance()); + if (!timer.isRunning()) { + timer.setTimeoutTime(timeout.floatValue()); + timer.setRepeats(periodic); + timer.setHandlerFunc(handlerFunc); + timer.start(simulation); + } + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("TimerGetElapsed", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CTimerJass timer = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + return new RealJassValue(timer.getElapsed(simulation)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("TimerGetRemaining", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CTimerJass timer = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + return new RealJassValue(timer.getRemaining(simulation)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("TimerGetTimeout", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CTimerJass timer = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + return new RealJassValue(timer.getTimeoutTime()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("PauseTimer", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CTimerJass timer = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + timer.pause(simulation); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ResumeTimer", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CTimerJass timer = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + timer.resume(simulation); + return null; + } + }); + + // ============================================================================ + // Group API + // + jassProgramVisitor.getJassNativeManager().createNative("CreateGroup", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new HandleJassValue(groupType, new ArrayList()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("DestroyGroup", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + System.err.println( + "DestroyGroup called but in Java we don't have a destructor, so we need to unregister later when that is implemented"); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupAddUnit", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final CUnit whichUnit = arguments.get(1).visit(ObjectJassValueVisitor.getInstance()); + group.add(whichUnit); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupRemoveUnit", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final CUnit whichUnit = arguments.get(1).visit(ObjectJassValueVisitor.getInstance()); + group.remove(whichUnit); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupClear", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + group.clear(); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupEnumUnitsOfType", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final String unitname = arguments.get(1).visit(StringJassValueVisitor.getInstance()); + final TriggerBooleanExpression filter = arguments.get(2) + .visit(ObjectJassValueVisitor.getInstance()); + for (final CUnit unit : simulation.getUnits()) { + if (unitname.equals(unit.getUnitType().getLegacyName())) { + if (filter.evaluate(globalScope, + CommonTriggerExecutionScope.filterScope(triggerScope, unit))) { + // TODO the trigger scope for evaluation here might need to be a clean one? + group.add(unit); + } + } + } + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupEnumUnitsOfPlayer", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final CPlayerJass player = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + final TriggerBooleanExpression filter = arguments.get(2) + .visit(ObjectJassValueVisitor.getInstance()); + for (final CUnit unit : simulation.getUnits()) { + if (unit.getPlayerIndex() == player.getId()) { + if (filter.evaluate(globalScope, + CommonTriggerExecutionScope.filterScope(triggerScope, unit))) { + // TODO the trigger scope for evaluation here might need to be a clean one? + group.add(unit); + } + } + } + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupEnumUnitsOfTypeCounted", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final String unitname = arguments.get(1).visit(StringJassValueVisitor.getInstance()); + final TriggerBooleanExpression filter = arguments.get(2) + .visit(ObjectJassValueVisitor.getInstance()); + final Integer countLimit = arguments.get(3).visit(IntegerJassValueVisitor.getInstance()); + int count = 0; + for (final CUnit unit : simulation.getUnits()) { + if (unitname.equals(unit.getUnitType().getLegacyName())) { + if (filter.evaluate(globalScope, + CommonTriggerExecutionScope.filterScope(triggerScope, unit))) { + // TODO the trigger scope for evaluation here might need to be a clean one? + group.add(unit); + count++; + if (count >= countLimit) { + break; + } + } + } + } + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupEnumUnitsInRect", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final Rectangle rect = arguments.get(1).visit(ObjectJassValueVisitor.getInstance()); + final TriggerBooleanExpression filter = arguments.get(2) + .visit(ObjectJassValueVisitor.getInstance()); + simulation.getWorldCollision().enumUnitsInRect(rect, new CUnitEnumFunction() { + @Override + public boolean call(final CUnit unit) { + if (filter.evaluate(globalScope, + CommonTriggerExecutionScope.filterScope(triggerScope, unit))) { + // TODO the trigger scope for evaluation here might need to be a clean one? + group.add(unit); + } + return false; + } + }); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupEnumUnitsInRectCounted", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final Rectangle rect = arguments.get(1).visit(ObjectJassValueVisitor.getInstance()); + final TriggerBooleanExpression filter = arguments.get(2) + .visit(ObjectJassValueVisitor.getInstance()); + final Integer countLimit = arguments.get(3).visit(IntegerJassValueVisitor.getInstance()); + simulation.getWorldCollision().enumUnitsInRect(rect, new CUnitEnumFunction() { + int count = 0; + + @Override + public boolean call(final CUnit unit) { + if (filter.evaluate(globalScope, + CommonTriggerExecutionScope.filterScope(triggerScope, unit))) { + // TODO the trigger scope for evaluation here might need to be a clean one? + group.add(unit); + this.count++; + if (this.count >= countLimit) { + return true; + } + } + return false; + } + }); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupEnumUnitsInRange", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final float x = arguments.get(1).visit(RealJassValueVisitor.getInstance()).floatValue(); + final float y = arguments.get(2).visit(RealJassValueVisitor.getInstance()).floatValue(); + final float radius = arguments.get(3).visit(RealJassValueVisitor.getInstance()).floatValue(); + final TriggerBooleanExpression filter = arguments.get(4) + .visit(ObjectJassValueVisitor.getInstance()); + simulation.getWorldCollision().enumUnitsInRect(tempRect.set(x - radius, y - radius, radius, radius), + new CUnitEnumFunction() { + + @Override + public boolean call(final CUnit unit) { + if (unit.distance(x, y) <= radius) { + if (filter.evaluate(globalScope, + CommonTriggerExecutionScope.filterScope(triggerScope, unit))) { + // TODO the trigger scope for evaluation here might need to be a clean one? + group.add(unit); + } + } + return false; + } + }); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupEnumUnitsInRangeOfLoc", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final Point2D.Double whichLocation = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + final float x = (float) whichLocation.x; + final float y = (float) whichLocation.y; + final float radius = arguments.get(2).visit(RealJassValueVisitor.getInstance()).floatValue(); + final TriggerBooleanExpression filter = arguments.get(3) + .visit(ObjectJassValueVisitor.getInstance()); + simulation.getWorldCollision().enumUnitsInRect(tempRect.set(x - radius, y - radius, radius, radius), + new CUnitEnumFunction() { + + @Override + public boolean call(final CUnit unit) { + if (unit.distance(x, y) <= radius) { + if (filter.evaluate(globalScope, + CommonTriggerExecutionScope.filterScope(triggerScope, unit))) { + // TODO the trigger scope for evaluation here might need to be a clean one? + group.add(unit); + } + } + return false; + } + }); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupEnumUnitsInRangeCounted", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final float x = arguments.get(1).visit(RealJassValueVisitor.getInstance()).floatValue(); + final float y = arguments.get(2).visit(RealJassValueVisitor.getInstance()).floatValue(); + final float radius = arguments.get(3).visit(RealJassValueVisitor.getInstance()).floatValue(); + final TriggerBooleanExpression filter = arguments.get(4) + .visit(ObjectJassValueVisitor.getInstance()); + final Integer countLimit = arguments.get(5).visit(IntegerJassValueVisitor.getInstance()); + simulation.getWorldCollision().enumUnitsInRect(tempRect.set(x - radius, y - radius, radius, radius), + new CUnitEnumFunction() { + int count = 0; + + @Override + public boolean call(final CUnit unit) { + if (unit.distance(x, y) <= radius) { + if (filter.evaluate(globalScope, + CommonTriggerExecutionScope.filterScope(triggerScope, unit))) { + // TODO the trigger scope for evaluation here might need to be a clean one? + group.add(unit); + this.count++; + if (this.count >= countLimit) { + return true; + } + } + } + return false; + } + }); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupEnumUnitsInRangeOfLocCounted", + new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0) + .visit(ObjectJassValueVisitor.>getInstance()); + final Point2D.Double whichLocation = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + final float x = (float) whichLocation.x; + final float y = (float) whichLocation.y; + final float radius = arguments.get(2).visit(RealJassValueVisitor.getInstance()) + .floatValue(); + final TriggerBooleanExpression filter = arguments.get(3) + .visit(ObjectJassValueVisitor.getInstance()); + final Integer countLimit = arguments.get(4).visit(IntegerJassValueVisitor.getInstance()); + simulation.getWorldCollision().enumUnitsInRect( + tempRect.set(x - radius, y - radius, radius, radius), new CUnitEnumFunction() { + int count = 0; + + @Override + public boolean call(final CUnit unit) { + if (unit.distance(x, y) <= radius) { + if (filter.evaluate(globalScope, + CommonTriggerExecutionScope.filterScope(triggerScope, unit))) { + // TODO the trigger scope for evaluation here might need to be a + // clean one? + group.add(unit); + this.count++; + if (this.count >= countLimit) { + return true; + } + } + } + return false; + } + }); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupEnumUnitsSelected", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final CPlayerJass whyichPlayer = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + final TriggerBooleanExpression filter = arguments.get(2) + .visit(ObjectJassValueVisitor.getInstance()); + throw new UnsupportedOperationException("GroupEnumUnitsSelected not supported yet."); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupImmediateOrder", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final String order = arguments.get(1).visit(StringJassValueVisitor.getInstance()); + final int orderId = OrderIdUtils.getOrderId(order); + boolean success = true; + for (final CUnit unit : group) { + success &= unit.order(simulation, orderId, null); + } + return BooleanJassValue.of(success); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupImmediateOrderById", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final int order = arguments.get(1).visit(IntegerJassValueVisitor.getInstance()); + boolean success = true; + for (final CUnit unit : group) { + success &= unit.order(simulation, order, null); + } + return BooleanJassValue.of(success); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupPointOrder", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final String order = arguments.get(1).visit(StringJassValueVisitor.getInstance()); + final Double x = arguments.get(2).visit(RealJassValueVisitor.getInstance()); + final Double y = arguments.get(3).visit(RealJassValueVisitor.getInstance()); + final AbilityPointTarget target = new AbilityPointTarget(x.floatValue(), y.floatValue()); + final int orderId = OrderIdUtils.getOrderId(order); + boolean success = true; + for (final CUnit unit : group) { + success &= unit.order(simulation, orderId, target); + } + return BooleanJassValue.of(success); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupPointOrderLoc", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final String order = arguments.get(1).visit(StringJassValueVisitor.getInstance()); + final Point2D.Double whichLocation = arguments.get(2) + .visit(ObjectJassValueVisitor.getInstance()); + final AbilityPointTarget target = new AbilityPointTarget((float) whichLocation.x, + (float) whichLocation.y); + final int orderId = OrderIdUtils.getOrderId(order); + boolean success = true; + for (final CUnit unit : group) { + success &= unit.order(simulation, orderId, target); + } + return BooleanJassValue.of(success); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupPointOrderById", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final int orderId = arguments.get(1).visit(IntegerJassValueVisitor.getInstance()); + final Double x = arguments.get(2).visit(RealJassValueVisitor.getInstance()); + final Double y = arguments.get(3).visit(RealJassValueVisitor.getInstance()); + final AbilityPointTarget target = new AbilityPointTarget(x.floatValue(), y.floatValue()); + boolean success = true; + for (final CUnit unit : group) { + success &= unit.order(simulation, orderId, target); + } + return BooleanJassValue.of(success); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupPointOrderByIdLoc", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final int orderId = arguments.get(1).visit(IntegerJassValueVisitor.getInstance()); + final Point2D.Double whichLocation = arguments.get(2) + .visit(ObjectJassValueVisitor.getInstance()); + final AbilityPointTarget target = new AbilityPointTarget((float) whichLocation.x, + (float) whichLocation.y); + boolean success = true; + for (final CUnit unit : group) { + success &= unit.order(simulation, orderId, target); + } + return BooleanJassValue.of(success); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupTargetOrder", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final String order = arguments.get(1).visit(StringJassValueVisitor.getInstance()); + final CWidget target = arguments.get(2).visit(ObjectJassValueVisitor.getInstance()); + final int orderId = OrderIdUtils.getOrderId(order); + boolean success = true; + for (final CUnit unit : group) { + success &= unit.order(simulation, orderId, target); + } + return BooleanJassValue.of(success); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GroupTargetOrderById", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final int orderId = arguments.get(1).visit(IntegerJassValueVisitor.getInstance()); + final CWidget target = arguments.get(2).visit(ObjectJassValueVisitor.getInstance()); + boolean success = true; + for (final CUnit unit : group) { + success &= unit.order(simulation, orderId, target); + } + return BooleanJassValue.of(success); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ForGroup", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + final JassFunction callback = arguments.get(1).visit(JassFunctionJassValueVisitor.getInstance()); + for (final CUnit unit : group) { + callback.call(Collections.emptyList(), globalScope, + CommonTriggerExecutionScope.enumScope(triggerScope, unit)); + } + return new HandleJassValue(unitType, group.get(0)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("FirstOfGroup", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List group = arguments.get(0).visit(ObjectJassValueVisitor.>getInstance()); + return new HandleJassValue(unitType, group.get(0)); + } + }); + // ============================================================================ + // Force API + // + jassProgramVisitor.getJassNativeManager().createNative("CreateForce", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new HandleJassValue(forceType, new ArrayList()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("DestroyForce", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List force = arguments.get(0) + .visit(ObjectJassValueVisitor.>getInstance()); + System.err.println( + "DestroyForce called but in Java we don't have a destructor, so we need to unregister later when that is implemented"); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ForceAddPlayer", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List force = arguments.get(0) + .visit(ObjectJassValueVisitor.>getInstance()); + final CPlayerJass player = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + force.add(player); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ForceRemovePlayer", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List force = arguments.get(0) + .visit(ObjectJassValueVisitor.>getInstance()); + final CPlayerJass player = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + force.remove(player); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ForceClear", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List force = arguments.get(0) + .visit(ObjectJassValueVisitor.>getInstance()); + force.clear(); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ForceEnumPlayers", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List force = arguments.get(0) + .visit(ObjectJassValueVisitor.>getInstance()); + final TriggerBooleanExpression filter = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + for (int i = 0; i < WarsmashConstants.MAX_PLAYERS; i++) { + final CPlayerJass jassPlayer = simulation.getPlayer(i); + if (filter.evaluate(globalScope, + CommonTriggerExecutionScope.filterScope(triggerScope, jassPlayer))) { + force.add(jassPlayer); + } + } + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ForceEnumPlayersCounted", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List force = arguments.get(0) + .visit(ObjectJassValueVisitor.>getInstance()); + final TriggerBooleanExpression filter = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + final Integer countLimit = arguments.get(2).visit(IntegerJassValueVisitor.getInstance()); + int count = 0; + for (int i = 0; (i < WarsmashConstants.MAX_PLAYERS) && (count < countLimit); i++) { + final CPlayerJass jassPlayer = simulation.getPlayer(i); + if (filter.evaluate(globalScope, + CommonTriggerExecutionScope.filterScope(triggerScope, jassPlayer))) { + force.add(jassPlayer); + count++; + } + } + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ForceEnumAllies", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List force = arguments.get(0) + .visit(ObjectJassValueVisitor.>getInstance()); + final CPlayerJass player = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + final TriggerBooleanExpression filter = arguments.get(2) + .visit(ObjectJassValueVisitor.getInstance()); + for (int i = 0; i < WarsmashConstants.MAX_PLAYERS; i++) { + final CPlayerJass jassPlayer = simulation.getPlayer(i); + if (player.hasAlliance(i, CAllianceType.PASSIVE)) { + if (filter.evaluate(globalScope, + CommonTriggerExecutionScope.filterScope(triggerScope, jassPlayer))) { + force.add(jassPlayer); + } + } + } + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ForceEnumEnemies", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List force = arguments.get(0) + .visit(ObjectJassValueVisitor.>getInstance()); + final CPlayerJass player = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + final TriggerBooleanExpression filter = arguments.get(2) + .visit(ObjectJassValueVisitor.getInstance()); + for (int i = 0; i < WarsmashConstants.MAX_PLAYERS; i++) { + final CPlayerJass jassPlayer = simulation.getPlayer(i); + if (!player.hasAlliance(i, CAllianceType.PASSIVE)) { + if (filter.evaluate(globalScope, + CommonTriggerExecutionScope.filterScope(triggerScope, jassPlayer))) { + force.add(jassPlayer); + } + } + } + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ForForce", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final List force = arguments.get(0) + .visit(ObjectJassValueVisitor.>getInstance()); + final JassFunction callback = arguments.get(1).visit(JassFunctionJassValueVisitor.getInstance()); + for (final CPlayerJass player : force) { + callback.call(Collections.emptyList(), globalScope, + CommonTriggerExecutionScope.enumScope(triggerScope, player)); + } + return null; + } + }); + // ============================================================================ + // Region and Location API + // + jassProgramVisitor.getJassNativeManager().createNative("Rect", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final float minx = arguments.get(0).visit(RealJassValueVisitor.getInstance()).floatValue(); + final float miny = arguments.get(1).visit(RealJassValueVisitor.getInstance()).floatValue(); + final float maxx = arguments.get(2).visit(RealJassValueVisitor.getInstance()).floatValue(); + final float maxy = arguments.get(3).visit(RealJassValueVisitor.getInstance()).floatValue(); + return new HandleJassValue(rectType, new Rectangle(minx, miny, maxx - minx, maxy - miny)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("RectFromLoc", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Point2D.Double min = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final Point2D.Double max = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + final float minx = (float) min.x; + final float miny = (float) min.y; + final float maxx = (float) max.x; + final float maxy = (float) max.y; + return new HandleJassValue(rectType, new Rectangle(minx, miny, maxx - minx, maxy - miny)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("RemoveRect", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Rectangle rect = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + System.err.println( + "RemoveRect called but in Java we don't have a destructor, so we need to unregister later when that is implemented"); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetRect", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Rectangle rect = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final float minx = arguments.get(1).visit(RealJassValueVisitor.getInstance()).floatValue(); + final float miny = arguments.get(2).visit(RealJassValueVisitor.getInstance()).floatValue(); + final float maxx = arguments.get(3).visit(RealJassValueVisitor.getInstance()).floatValue(); + final float maxy = arguments.get(4).visit(RealJassValueVisitor.getInstance()).floatValue(); + rect.set(minx, miny, maxx - minx, maxy - miny); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetRectFromLoc", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Rectangle rect = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final Point2D.Double min = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + final Point2D.Double max = arguments.get(2) + .visit(ObjectJassValueVisitor.getInstance()); + final float minx = (float) min.x; + final float miny = (float) min.y; + final float maxx = (float) max.x; + final float maxy = (float) max.y; + rect.set(minx, miny, maxx - minx, maxy - miny); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("MoveRectTo", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Rectangle rect = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final float newCenterX = arguments.get(1).visit(RealJassValueVisitor.getInstance()).floatValue(); + final float newCenterY = arguments.get(2).visit(RealJassValueVisitor.getInstance()).floatValue(); + rect.setCenter(newCenterX, newCenterY); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("MoveRectToLoc", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Rectangle rect = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final Point2D.Double newCenterLoc = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + rect.setCenter((float) newCenterLoc.x, (float) newCenterLoc.y); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetRectCenterX", new JassFunction() { + Vector2 centerHeap = new Vector2(); + + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Rectangle rect = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + return new RealJassValue(rect.getCenter(this.centerHeap).x); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetRectCenterY", new JassFunction() { + Vector2 centerHeap = new Vector2(); + + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Rectangle rect = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + return new RealJassValue(rect.getCenter(this.centerHeap).y); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetRectMinX", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Rectangle rect = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + return new RealJassValue(rect.getX()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetRectMinY", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Rectangle rect = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + return new RealJassValue(rect.getY()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetRectMaxX", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Rectangle rect = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + return new RealJassValue(rect.getX() + rect.getWidth()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetRectMaxY", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Rectangle rect = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + return new RealJassValue(rect.getY() + rect.getHeight()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("CreateRegion", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new HandleJassValue(regionType, new CRegion()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("RemoveRegion", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CRegion region = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + region.remove(simulation.getRegionManager()); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("RegionAddRect", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CRegion region = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final Rectangle rect = arguments.get(1).visit(ObjectJassValueVisitor.getInstance()); + region.addRect(rect, simulation.getRegionManager()); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("RegionClearRect", new JassFunction() { + + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CRegion region = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final Rectangle rect = arguments.get(1).visit(ObjectJassValueVisitor.getInstance()); + region.clearRect(rect, simulation.getRegionManager()); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("RegionAddCell", new JassFunction() { + + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CRegion region = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final float x = arguments.get(1).visit(RealJassValueVisitor.getInstance()).floatValue(); + final float y = arguments.get(2).visit(RealJassValueVisitor.getInstance()).floatValue(); + region.addCell(x, y, simulation.getRegionManager()); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("RegionAddCellAtLoc", new JassFunction() { + + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CRegion region = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final Point2D.Double whichLocation = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + region.addCell((float) whichLocation.x, (float) whichLocation.y, simulation.getRegionManager()); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("RegionClearCell", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CRegion region = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final float x = arguments.get(1).visit(RealJassValueVisitor.getInstance()).floatValue(); + final float y = arguments.get(2).visit(RealJassValueVisitor.getInstance()).floatValue(); + region.clearCell(x, y, simulation.getRegionManager()); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("RegionClearCellAtLoc", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CRegion region = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final Point2D.Double whichLocation = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + region.clearCell((float) whichLocation.x, (float) whichLocation.y, simulation.getRegionManager()); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("Location", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final float x = arguments.get(0).visit(RealJassValueVisitor.getInstance()).floatValue(); + final float y = arguments.get(1).visit(RealJassValueVisitor.getInstance()).floatValue(); + return new HandleJassValue(locationType, new Point2D.Double(x, y)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("RemoveLocation", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Point2D.Double whichLocation = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + System.err.println( + "RemoveRect called but in Java we don't have a destructor, so we need to unregister later when that is implemented"); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("MoveLocation", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Point2D.Double whichLocation = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final float x = arguments.get(1).visit(RealJassValueVisitor.getInstance()).floatValue(); + final float y = arguments.get(2).visit(RealJassValueVisitor.getInstance()).floatValue(); + whichLocation.x = x; + whichLocation.y = y; + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetLocationX", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Point2D.Double whichLocation = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + return new RealJassValue(whichLocation.x); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetLocationY", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Point2D.Double whichLocation = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + return new RealJassValue(whichLocation.y); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetLocationZ", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Point2D.Double whichLocation = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + return new RealJassValue( + war3MapViewer.terrain.getGroundHeight((float) whichLocation.x, (float) whichLocation.y)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("IsUnitInRegion", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CRegion whichRegion = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final CUnit whichUnit = arguments.get(1).visit(ObjectJassValueVisitor.getInstance()); + return BooleanJassValue.of(whichUnit.isInRegion(whichRegion)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("IsPointInRegion", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CRegion whichRegion = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final float x = arguments.get(1).visit(RealJassValueVisitor.getInstance()).floatValue(); + final float y = arguments.get(2).visit(RealJassValueVisitor.getInstance()).floatValue(); + return BooleanJassValue.of(whichRegion.contains(x, y, simulation.getRegionManager())); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("IsLocationInRegion", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CRegion whichRegion = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final Point2D.Double whichLocation = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + return BooleanJassValue.of(whichRegion.contains((float) whichLocation.x, (float) whichLocation.y, + simulation.getRegionManager())); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetWorldBounds", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final float worldMinX = simulation.getPathingGrid().getWorldX(0) - 16f; + final float worldMinY = simulation.getPathingGrid().getWorldY(0) - 16f; + final float worldMaxX = simulation.getPathingGrid() + .getWorldX(simulation.getPathingGrid().getWidth() - 1) + 16f; + final float worldMaxY = simulation.getPathingGrid() + .getWorldY(simulation.getPathingGrid().getHeight() - 1) + 16f; + return new HandleJassValue(rectType, + new Rectangle(worldMinX, worldMinY, worldMaxX - worldMinX, worldMaxY - worldMinY)); + } + }); + + } + + private void registerConfigNatives(final JassProgramVisitor jassProgramVisitor, final War3MapConfig mapConfig, + final HandleJassType startlocprioType, final HandleJassType gametypeType, + final HandleJassType placementType, final HandleJassType gamespeedType, + final HandleJassType gamedifficultyType, final HandleJassType mapdensityType, + final HandleJassType locationType, final HandleJassType playerType, + final HandleJassType playercolorType, final HandleJassType mapcontrolType, + final HandleJassType playerslotstateType, final CPlayerAPI playerAPI) { + jassProgramVisitor.getJassNativeManager().createNative("SetMapName", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String name = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + mapConfig.setMapName(name); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetMapDescription", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final String name = arguments.get(0).visit(StringJassValueVisitor.getInstance()); + mapConfig.setMapDescription(name); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetTeams", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Integer teamCount = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + mapConfig.setTeamCount(teamCount); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetPlayers", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Integer playerCount = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + mapConfig.setPlayerCount(playerCount); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("DefineStartLocation", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Integer whichStartLoc = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + final Double x = arguments.get(1).visit(RealJassValueVisitor.getInstance()); + final Double y = arguments.get(2).visit(RealJassValueVisitor.getInstance()); + mapConfig.getStartLoc(whichStartLoc).setX(x.floatValue()); + mapConfig.getStartLoc(whichStartLoc).setY(y.floatValue()); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("DefineStartLocationLoc", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Integer whichStartLoc = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + final Point2D.Double whichLocation = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + mapConfig.getStartLoc(whichStartLoc).setX((float) whichLocation.x); + mapConfig.getStartLoc(whichStartLoc).setY((float) whichLocation.y); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetStartLocPrioCount", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Integer whichStartLoc = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + final Integer prioSlotCount = arguments.get(1).visit(IntegerJassValueVisitor.getInstance()); + mapConfig.getStartLoc(whichStartLoc).setStartLocPrioCount(prioSlotCount); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetStartLocPrio", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Integer whichStartLoc = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + final Integer prioSlotIndex = arguments.get(1).visit(IntegerJassValueVisitor.getInstance()); + final Integer otherStartLocIndex = arguments.get(2).visit(IntegerJassValueVisitor.getInstance()); + final CStartLocPrio priority = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + mapConfig.getStartLoc(whichStartLoc).setStartLocPrio(prioSlotIndex, otherStartLocIndex, priority); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetStartLocPrioSlot", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Integer whichStartLoc = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + final Integer prioSlotIndex = arguments.get(1).visit(IntegerJassValueVisitor.getInstance()); + return new IntegerJassValue( + mapConfig.getStartLoc(whichStartLoc).getOtherStartIndices()[prioSlotIndex]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetStartLocPrio", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Integer whichStartLoc = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + final Integer prioSlotIndex = arguments.get(1).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(startlocprioType, + mapConfig.getStartLoc(whichStartLoc).getOtherStartLocPriorities()[prioSlotIndex]); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetGameTypeSupported", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CGameType gameType = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final Boolean value = arguments.get(1).visit(BooleanJassValueVisitor.getInstance()); + mapConfig.setGameTypeSupported(gameType, value); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetMapFlag", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CMapFlag mapFlag = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + final Boolean value = arguments.get(1).visit(BooleanJassValueVisitor.getInstance()); + mapConfig.setMapFlag(mapFlag, value); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetGamePlacement", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CMapPlacement placement = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + mapConfig.setPlacement(placement); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetGameSpeed", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CGameSpeed gameSpeed = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + mapConfig.setGameSpeed(gameSpeed); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetGameDifficulty", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CMapDifficulty gameDifficulty = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + mapConfig.setGameDifficulty(gameDifficulty); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetResourceDensity", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CMapDensity resourceDensity = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + mapConfig.setResourceDensity(resourceDensity); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetCreatureDensity", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CMapDensity creatureDensity = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + mapConfig.setCreatureDensity(creatureDensity); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetTeams", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new IntegerJassValue(mapConfig.getTeamCount()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetPlayers", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new IntegerJassValue(mapConfig.getPlayerCount()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("IsGameTypeSupported", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CGameType gameType = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + return new BooleanJassValue(mapConfig.isGameTypeSupported(gameType)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetGameTypeSelected", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new HandleJassValue(gametypeType, mapConfig.getGameTypeSelected()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("IsMapFlagSet", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CMapFlag mapFlag = arguments.get(0).visit(ObjectJassValueVisitor.getInstance()); + return new BooleanJassValue(mapConfig.isMapFlagSet(mapFlag)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetGamePlacement", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new HandleJassValue(placementType, mapConfig.getPlacement()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetGameSpeed", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new HandleJassValue(gamespeedType, mapConfig.getGameSpeed()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetGameDifficulty", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new HandleJassValue(gamedifficultyType, mapConfig.getGameDifficulty()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetResourceDensity", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new HandleJassValue(mapdensityType, mapConfig.getResourceDensity()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetCreatureDensity", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + return new HandleJassValue(mapdensityType, mapConfig.getCreatureDensity()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetStartLocationX", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Integer whichStartLoc = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new RealJassValue(mapConfig.getStartLoc(whichStartLoc).getX()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetStartLocationY", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Integer whichStartLoc = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new RealJassValue(mapConfig.getStartLoc(whichStartLoc).getY()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetStartLocationLoc", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final Integer whichStartLoc = arguments.get(0).visit(IntegerJassValueVisitor.getInstance()); + return new HandleJassValue(locationType, new Point2D.Double( + mapConfig.getStartLoc(whichStartLoc).getX(), mapConfig.getStartLoc(whichStartLoc).getY())); + } + }); + // PlayerAPI + + jassProgramVisitor.getJassNativeManager().createNative("SetPlayerTeam", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final Integer whichTeam = arguments.get(1).visit(IntegerJassValueVisitor.getInstance()); + player.setTeam(whichTeam); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetPlayerStartLocation", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final Integer startLocIndex = arguments.get(1).visit(IntegerJassValueVisitor.getInstance()); + player.setStartLocationIndex(startLocIndex); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("ForcePlayerStartLocation", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final Integer startLocIndex = arguments.get(1).visit(IntegerJassValueVisitor.getInstance()); + player.forceStartLocation(startLocIndex); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetPlayerColor", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final CPlayerColor playerColor = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + player.setColor(playerColor.ordinal()); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetPlayerAlliance", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final CPlayerJass otherPlayer = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + final CAllianceType whichAllianceSetting = arguments.get(2) + .visit(ObjectJassValueVisitor.getInstance()); + final Boolean value = arguments.get(3).visit(BooleanJassValueVisitor.getInstance()); + player.setAlliance(otherPlayer.getId(), whichAllianceSetting, value); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetPlayerTaxRate", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final CPlayerJass otherPlayer = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + final CPlayerState whichResource = arguments.get(2) + .visit(ObjectJassValueVisitor.getInstance()); + final int taxRate = arguments.get(3).visit(IntegerJassValueVisitor.getInstance()); + player.setTaxRate(otherPlayer.getId(), whichResource, taxRate); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetPlayerRacePreference", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final CRacePreference whichRacePreference = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + player.setRacePref(whichRacePreference); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetPlayerRaceSelectable", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final Boolean value = arguments.get(1).visit(BooleanJassValueVisitor.getInstance()); + player.setRaceSelectable(value); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetPlayerController", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final CMapControl controlType = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + player.setController(controlType); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetPlayerName", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final String name = arguments.get(1).visit(StringJassValueVisitor.getInstance()); + player.setName(name); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("SetPlayerOnScoreScreen", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final Boolean value = arguments.get(1).visit(BooleanJassValueVisitor.getInstance()); + player.setOnScoreScreen(value); + return null; + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetPlayerTeam", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + return new IntegerJassValue(player.getTeam()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetPlayerStartLocation", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + return new IntegerJassValue(player.getStartLocationIndex()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetPlayerColor", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + return new HandleJassValue(playercolorType, CPlayerColor.getColorByIndex(player.getColor())); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetPlayerSelectable", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + return new BooleanJassValue(player.isSelectable()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetPlayerController", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + return new HandleJassValue(mapcontrolType, player.getController()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetPlayerSlotState", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + return new HandleJassValue(playerslotstateType, player.getController()); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetPlayerTaxRate", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final CPlayerJass otherPlayer = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + final CPlayerState whichResource = arguments.get(2) + .visit(ObjectJassValueVisitor.getInstance()); + return new IntegerJassValue(player.getTaxRate(otherPlayer.getId(), whichResource)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("IsPlayerRacePrefSet", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + final CRacePreference racePref = arguments.get(1) + .visit(ObjectJassValueVisitor.getInstance()); + return new BooleanJassValue(player.isRacePrefSet(racePref)); + } + }); + jassProgramVisitor.getJassNativeManager().createNative("GetPlayerName", new JassFunction() { + @Override + public JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + final CPlayerJass player = arguments.get(0) + .visit(ObjectJassValueVisitor.getInstance()); + return new StringJassValue(player.getName()); + } + }); + } + } +} diff --git a/core/src/com/etheller/warsmash/parsers/jass/JassTest.java b/core/src/com/etheller/warsmash/parsers/jass/JassTest.java new file mode 100644 index 0000000..629dd51 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/jass/JassTest.java @@ -0,0 +1,64 @@ +package com.etheller.warsmash.parsers.jass; + +import java.io.IOException; +import java.util.Arrays; + +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; + +import com.etheller.interpreter.JassLexer; +import com.etheller.interpreter.JassParser; +import com.etheller.interpreter.ast.visitors.JassProgramVisitor; +import com.etheller.warsmash.datasources.CompoundDataSourceDescriptor; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.datasources.DataSourceDescriptor; +import com.etheller.warsmash.datasources.FolderDataSourceDescriptor; + +public class JassTest { + public static final boolean REPORT_SYNTAX_ERRORS = true; + + public static void main(final String[] args) { + final JassProgramVisitor jassProgramVisitor = new JassProgramVisitor(); + try { + final FolderDataSourceDescriptor war3mpq = new FolderDataSourceDescriptor( + "E:\\Backups\\Warcraft\\Data\\127"); + final FolderDataSourceDescriptor testingFolder = new FolderDataSourceDescriptor( + "E:\\Backups\\Warsmash\\Data"); + final FolderDataSourceDescriptor currentFolder = new FolderDataSourceDescriptor("."); + final DataSource dataSource = new CompoundDataSourceDescriptor( + Arrays.asList(war3mpq, testingFolder, currentFolder)).createDataSource(); + JassLexer lexer; + try { + lexer = new JassLexer(CharStreams.fromStream(dataSource.getResourceAsStream("Scripts\\common.j"))); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + final JassParser parser = new JassParser(new CommonTokenStream(lexer)); + parser.addErrorListener(new BaseErrorListener() { + @Override + public void syntaxError(final Recognizer recognizer, final Object offendingSymbol, final int line, + final int charPositionInLine, final String msg, final RecognitionException e) { + if (!REPORT_SYNTAX_ERRORS) { + return; + } + + String sourceName = recognizer.getInputStream().getSourceName(); + if (!sourceName.isEmpty()) { + sourceName = String.format("%s:%d:%d: ", sourceName, line, charPositionInLine); + } + + System.err.println(sourceName + "line " + line + ":" + charPositionInLine + " " + msg); + } + }); + jassProgramVisitor.visit(parser.program()); + } + catch (final Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/jass/Tmpgen.java b/core/src/com/etheller/warsmash/parsers/jass/Tmpgen.java new file mode 100644 index 0000000..7d32790 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/jass/Tmpgen.java @@ -0,0 +1,22 @@ +package com.etheller.warsmash.parsers.jass; + +import java.util.Scanner; + +public class Tmpgen { + + public static void main(final String[] args) { + // final HandleJassType eventType = globals.registerHandleType("event"); + + final Scanner scanner = new Scanner(System.in); + while (scanner.hasNextLine()) { + final String line = scanner.nextLine(); + if (line.startsWith("type ")) { + final String[] splitLine = line.split("\\s+"); + System.out.println("final HandleJassType " + splitLine[1] + "Type = globals.registerHandleType(\"" + + splitLine[1] + "\");"); + } + } + + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/jass/Tmpgen2.java b/core/src/com/etheller/warsmash/parsers/jass/Tmpgen2.java new file mode 100644 index 0000000..b9124c1 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/jass/Tmpgen2.java @@ -0,0 +1,29 @@ +package com.etheller.warsmash.parsers.jass; + +import java.util.Scanner; + +public class Tmpgen2 { + + public static void main(final String[] args) { + // final HandleJassType eventType = globals.registerHandleType("event"); + + final Scanner scanner = new Scanner(System.in); + while (scanner.hasNextLine()) { + final String line = scanner.nextLine(); + final String[] splitLine = line.trim().split("\\s+"); + if (line.trim().startsWith("//")) { + System.out.println(line); + } + else { + if (splitLine.length > 3) { + System.out.println(splitLine[2] + ","); + } + else { + System.out.println(); + } + } + } + + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/jass/scope/CommonTriggerExecutionScope.java b/core/src/com/etheller/warsmash/parsers/jass/scope/CommonTriggerExecutionScope.java new file mode 100644 index 0000000..c6d6564 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/jass/scope/CommonTriggerExecutionScope.java @@ -0,0 +1,64 @@ +package com.etheller.warsmash.parsers.jass.scope; + +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayerJass; + +public class CommonTriggerExecutionScope extends TriggerExecutionScope { + private final CUnit triggeringUnit; + private final CUnit filterUnit; + private final CUnit enumUnit; + private final CPlayerJass filterPlayer; + private final CPlayerJass enumPlayer; + + public CommonTriggerExecutionScope(final TriggerExecutionScope parentScope, final CUnit triggeringUnit, + final CUnit filterUnit, final CUnit enumUnit, final CPlayerJass filterPlayer, + final CPlayerJass enumPlayer) { + super(parentScope.getTriggeringTrigger()); + this.triggeringUnit = triggeringUnit; + this.filterUnit = filterUnit; + this.enumUnit = enumUnit; + this.filterPlayer = filterPlayer; + this.enumPlayer = enumPlayer; + } + + public CUnit getEnumUnit() { + return this.enumUnit; + } + + public CUnit getTriggeringUnit() { + return this.triggeringUnit; + } + + public CUnit getFilterUnit() { + return this.filterUnit; + } + + public CPlayerJass getFilterPlayer() { + return this.filterPlayer; + } + + public CPlayerJass getEnumPlayer() { + return this.enumPlayer; + } + + public static CommonTriggerExecutionScope filterScope(final TriggerExecutionScope parentScope, + final CUnit filterUnit) { + return new CommonTriggerExecutionScope(parentScope, null, filterUnit, null, null, null); + } + + public static CommonTriggerExecutionScope enumScope(final TriggerExecutionScope parentScope, final CUnit enumUnit) { + return new CommonTriggerExecutionScope(parentScope, null, null, enumUnit, null, null); + } + + public static CommonTriggerExecutionScope filterScope(final TriggerExecutionScope parentScope, + final CPlayerJass filterPlayer) { + return new CommonTriggerExecutionScope(parentScope, null, null, null, filterPlayer, null); + } + + public static CommonTriggerExecutionScope enumScope(final TriggerExecutionScope parentScope, + final CPlayerJass enumPlayer) { + return new CommonTriggerExecutionScope(parentScope, null, null, null, null, enumPlayer); + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/jass/triggers/BoolExprAnd.java b/core/src/com/etheller/warsmash/parsers/jass/triggers/BoolExprAnd.java new file mode 100644 index 0000000..26deb4e --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/jass/triggers/BoolExprAnd.java @@ -0,0 +1,21 @@ +package com.etheller.warsmash.parsers.jass.triggers; + +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.scope.trigger.TriggerBooleanExpression; + +public class BoolExprAnd implements TriggerBooleanExpression { + private final TriggerBooleanExpression operandA; + private final TriggerBooleanExpression operandB; + + public BoolExprAnd(final TriggerBooleanExpression operandA, final TriggerBooleanExpression operandB) { + this.operandA = operandA; + this.operandB = operandB; + } + + @Override + public boolean evaluate(final GlobalScope globalScope, final TriggerExecutionScope triggerScope) { + return this.operandA.evaluate(globalScope, triggerScope) && this.operandB.evaluate(globalScope, triggerScope); + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/jass/triggers/BoolExprCondition.java b/core/src/com/etheller/warsmash/parsers/jass/triggers/BoolExprCondition.java new file mode 100644 index 0000000..34ef292 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/jass/triggers/BoolExprCondition.java @@ -0,0 +1,27 @@ +package com.etheller.warsmash.parsers.jass.triggers; + +import java.util.Collections; + +import com.etheller.interpreter.ast.function.JassFunction; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.scope.trigger.TriggerBooleanExpression; +import com.etheller.interpreter.ast.value.JassValue; +import com.etheller.interpreter.ast.value.visitor.BooleanJassValueVisitor; + +public class BoolExprCondition implements TriggerBooleanExpression { + private final JassFunction takesNothingReturnsBooleanFunction; + + public BoolExprCondition(final JassFunction returnsBooleanFunction) { + this.takesNothingReturnsBooleanFunction = returnsBooleanFunction; + } + + @Override + public boolean evaluate(final GlobalScope globalScope, final TriggerExecutionScope triggerScope) { + final JassValue booleanJassReturnValue = this.takesNothingReturnsBooleanFunction.call(Collections.EMPTY_LIST, + globalScope, triggerScope); + final Boolean booleanReturnValue = booleanJassReturnValue.visit(BooleanJassValueVisitor.getInstance()); + return booleanReturnValue.booleanValue(); + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/jass/triggers/BoolExprFilter.java b/core/src/com/etheller/warsmash/parsers/jass/triggers/BoolExprFilter.java new file mode 100644 index 0000000..b3a6e27 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/jass/triggers/BoolExprFilter.java @@ -0,0 +1,27 @@ +package com.etheller.warsmash.parsers.jass.triggers; + +import java.util.Collections; + +import com.etheller.interpreter.ast.function.JassFunction; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.scope.trigger.TriggerBooleanExpression; +import com.etheller.interpreter.ast.value.JassValue; +import com.etheller.interpreter.ast.value.visitor.BooleanJassValueVisitor; + +public class BoolExprFilter implements TriggerBooleanExpression { + private final JassFunction takesNothingReturnsBooleanFunction; + + public BoolExprFilter(final JassFunction returnsBooleanFunction) { + this.takesNothingReturnsBooleanFunction = returnsBooleanFunction; + } + + @Override + public boolean evaluate(final GlobalScope globalScope, final TriggerExecutionScope triggerScope) { + final JassValue booleanJassReturnValue = this.takesNothingReturnsBooleanFunction.call(Collections.EMPTY_LIST, + globalScope, triggerScope); + final Boolean booleanReturnValue = booleanJassReturnValue.visit(BooleanJassValueVisitor.getInstance()); + return booleanReturnValue.booleanValue(); + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/jass/triggers/BoolExprNot.java b/core/src/com/etheller/warsmash/parsers/jass/triggers/BoolExprNot.java new file mode 100644 index 0000000..0f2cc52 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/jass/triggers/BoolExprNot.java @@ -0,0 +1,19 @@ +package com.etheller.warsmash.parsers.jass.triggers; + +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.scope.trigger.TriggerBooleanExpression; + +public class BoolExprNot implements TriggerBooleanExpression { + private final TriggerBooleanExpression operand; + + public BoolExprNot(final TriggerBooleanExpression operand) { + this.operand = operand; + } + + @Override + public boolean evaluate(final GlobalScope globalScope, final TriggerExecutionScope triggerScope) { + return this.operand.evaluate(globalScope, triggerScope); + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/jass/triggers/BoolExprOr.java b/core/src/com/etheller/warsmash/parsers/jass/triggers/BoolExprOr.java new file mode 100644 index 0000000..a59cd83 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/jass/triggers/BoolExprOr.java @@ -0,0 +1,21 @@ +package com.etheller.warsmash.parsers.jass.triggers; + +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.scope.trigger.TriggerBooleanExpression; + +public class BoolExprOr implements TriggerBooleanExpression { + private final TriggerBooleanExpression operandA; + private final TriggerBooleanExpression operandB; + + public BoolExprOr(final TriggerBooleanExpression operandA, final TriggerBooleanExpression operandB) { + this.operandA = operandA; + this.operandB = operandB; + } + + @Override + public boolean evaluate(final GlobalScope globalScope, final TriggerExecutionScope triggerScope) { + return this.operandA.evaluate(globalScope, triggerScope) || this.operandB.evaluate(globalScope, triggerScope); + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/jass/triggers/TriggerAction.java b/core/src/com/etheller/warsmash/parsers/jass/triggers/TriggerAction.java new file mode 100644 index 0000000..7620f13 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/jass/triggers/TriggerAction.java @@ -0,0 +1,28 @@ +package com.etheller.warsmash.parsers.jass.triggers; + +import com.etheller.interpreter.ast.function.JassFunction; +import com.etheller.interpreter.ast.scope.trigger.Trigger; + +public class TriggerAction { + private final Trigger trigger; + private final JassFunction actionFunc; + private final int actionIndex; + + public TriggerAction(final Trigger trigger, final JassFunction actionFunc, final int actionIndex) { + this.trigger = trigger; + this.actionFunc = actionFunc; + this.actionIndex = actionIndex; + } + + public Trigger getTrigger() { + return this.trigger; + } + + public JassFunction getActionFunc() { + return this.actionFunc; + } + + public int getActionIndex() { + return this.actionIndex; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/jass/triggers/TriggerCondition.java b/core/src/com/etheller/warsmash/parsers/jass/triggers/TriggerCondition.java new file mode 100644 index 0000000..8ee32d7 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/jass/triggers/TriggerCondition.java @@ -0,0 +1,28 @@ +package com.etheller.warsmash.parsers.jass.triggers; + +import com.etheller.interpreter.ast.scope.trigger.Trigger; +import com.etheller.interpreter.ast.scope.trigger.TriggerBooleanExpression; + +public class TriggerCondition { + private final TriggerBooleanExpression boolexpr; + private final Trigger trigger; + private final int conditionIndex; + + public TriggerCondition(final TriggerBooleanExpression boolexpr, final Trigger trigger, final int index) { + this.boolexpr = boolexpr; + this.trigger = trigger; + this.conditionIndex = index; + } + + public TriggerBooleanExpression getBoolexpr() { + return this.boolexpr; + } + + public Trigger getTrigger() { + return this.trigger; + } + + public int getConditionIndex() { + return this.conditionIndex; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/terrain/Corner.java b/core/src/com/etheller/warsmash/parsers/terrain/Corner.java new file mode 100644 index 0000000..f3f71f2 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/terrain/Corner.java @@ -0,0 +1,27 @@ +package com.etheller.warsmash.parsers.terrain; + +public class Corner { + boolean mapEdge; + + int groundTexture; + float height; + float waterHeight; + boolean ramp; + boolean blight; + boolean water; + boolean boundary; + boolean cliff; + boolean romp; + int groundVariation; + int cliffVariation; + int cliffTexture; + int layerHeight; + + public float finalGroundHeight() { + return 0; + } + + public float finalWaterHeight() { + return 0; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/terrain/Terrain.java b/core/src/com/etheller/warsmash/parsers/terrain/Terrain.java new file mode 100644 index 0000000..950474b --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/terrain/Terrain.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.parsers.terrain; + +import com.badlogic.gdx.utils.FloatArray; + +public class Terrain { + private FloatArray groundHeights; + private FloatArray groundCornerHeights; + + private FloatArray waterHeights; + + public Terrain() { + } +} diff --git a/core/src/com/etheller/warsmash/parsers/terrain/TilePathing.java b/core/src/com/etheller/warsmash/parsers/terrain/TilePathing.java new file mode 100644 index 0000000..1c2d661 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/terrain/TilePathing.java @@ -0,0 +1,15 @@ +package com.etheller.warsmash.parsers.terrain; + +public class TilePathing { + boolean unwalkable = false; + boolean unflyable = false; + boolean unbuildable = false; + + public byte mask() { + byte mask = 0; + mask |= this.unwalkable ? 0b00000010 : 0; + mask |= this.unflyable ? 0b00000100 : 0; + mask |= this.unbuildable ? 0b00001000 : 0; + return mask; + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/parsers/w3x/War3Map.java b/core/src/com/etheller/warsmash/parsers/w3x/War3Map.java new file mode 100644 index 0000000..409d53a --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/War3Map.java @@ -0,0 +1,147 @@ +package com.etheller.warsmash.parsers.w3x; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.util.Arrays; +import java.util.Collection; + +import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; + +import com.etheller.warsmash.datasources.CompoundDataSource; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.datasources.MpqDataSource; +import com.etheller.warsmash.parsers.w3x.doo.War3MapDoo; +import com.etheller.warsmash.parsers.w3x.objectdata.Warcraft3MapObjectData; +import com.etheller.warsmash.parsers.w3x.unitsdoo.War3MapUnitsDoo; +import com.etheller.warsmash.parsers.w3x.w3e.War3MapW3e; +import com.etheller.warsmash.parsers.w3x.w3i.War3MapW3i; +import com.etheller.warsmash.parsers.w3x.wpm.War3MapWpm; +import com.etheller.warsmash.units.custom.WTS; +import com.google.common.io.LittleEndianDataInputStream; + +import mpq.MPQArchive; +import mpq.MPQException; + +/** + * Warcraft 3 map (W3X and W3M). + */ +public class War3Map implements DataSource { + + private CompoundDataSource dataSource; + private MpqDataSource internalMpqContentsDataSource; + + public War3Map(final DataSource dataSource, final String mapFileName) { + try { + // Slightly complex. Here's the theory: + // 1.) Copy map into RAM + // 2.) Setup a Data Source that will read assets + // from either the map or the game, giving the map priority. + SeekableByteChannel sbc; + try (InputStream mapStream = dataSource.getResourceAsStream(mapFileName)) { + final byte[] mapData = IOUtils.toByteArray(mapStream); + sbc = new SeekableInMemoryByteChannel(mapData); + this.internalMpqContentsDataSource = new MpqDataSource(new MPQArchive(sbc), sbc); + this.dataSource = new CompoundDataSource(Arrays.asList(dataSource, this.internalMpqContentsDataSource)); + } + } + catch (final IOException e) { + throw new RuntimeException(e); + } + catch (final MPQException e) { + throw new RuntimeException(e); + } + } + + public War3MapW3i readMapInformation() throws IOException { + War3MapW3i mapInfo; + try (LittleEndianDataInputStream stream = new LittleEndianDataInputStream( + this.dataSource.getResourceAsStream("war3map.w3i"))) { + mapInfo = new War3MapW3i(stream); + } + return mapInfo; + } + + public War3MapW3e readEnvironment() throws IOException { + War3MapW3e environment; + try (LittleEndianDataInputStream stream = new LittleEndianDataInputStream( + this.dataSource.getResourceAsStream("war3map.w3e"))) { + environment = new War3MapW3e(stream); + } + return environment; + } + + public War3MapWpm readPathing() throws IOException { + War3MapWpm pathingMap; + try (LittleEndianDataInputStream stream = new LittleEndianDataInputStream( + this.dataSource.getResourceAsStream("war3map.wpm"))) { + pathingMap = new War3MapWpm(stream); + } + return pathingMap; + } + + public War3MapDoo readDoodads() throws IOException { + War3MapDoo doodadsFile; + try (LittleEndianDataInputStream stream = new LittleEndianDataInputStream( + this.dataSource.getResourceAsStream("war3map.doo"))) { + doodadsFile = new War3MapDoo(stream); + } + return doodadsFile; + } + + public War3MapUnitsDoo readUnits() throws IOException { + War3MapUnitsDoo unitsFile; + try (LittleEndianDataInputStream stream = new LittleEndianDataInputStream( + this.dataSource.getResourceAsStream("war3mapUnits.doo"))) { + unitsFile = new War3MapUnitsDoo(stream); + } + return unitsFile; + } + + public Warcraft3MapObjectData readModifications() throws IOException { + final Warcraft3MapObjectData changes = Warcraft3MapObjectData.load(this.dataSource, true); + return changes; + } + + public Warcraft3MapObjectData readModifications(final WTS preloadedWTS) throws IOException { + final Warcraft3MapObjectData changes = Warcraft3MapObjectData.load(this.dataSource, true, preloadedWTS); + return changes; + } + + @Override + public InputStream getResourceAsStream(final String filepath) throws IOException { + return this.dataSource.getResourceAsStream(filepath); + } + + @Override + public File getFile(final String filepath) throws IOException { + return this.dataSource.getFile(filepath); + } + + @Override + public boolean has(final String filepath) { + return this.dataSource.has(filepath); + } + + @Override + public ByteBuffer read(final String path) throws IOException { + return this.dataSource.read(path); + } + + @Override + public Collection getListfile() { + return this.internalMpqContentsDataSource.getListfile(); + } + + @Override + public void close() throws IOException { + this.dataSource.close(); + } + + public CompoundDataSource getCompoundDataSource() { + return this.dataSource; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/doo/Doodad.java b/core/src/com/etheller/warsmash/parsers/w3x/doo/Doodad.java new file mode 100644 index 0000000..913e388 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/doo/Doodad.java @@ -0,0 +1,157 @@ +package com.etheller.warsmash.parsers.w3x.doo; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class Doodad { + private War3ID id; + private int variation; + private final float[] location = new float[3]; + private float angle; + private final float[] scale = { 1f, 1f, 1f }; + private short flags; // short to store unsigned byte, java problem + private short life; // short to store unsigned byte, java problem + private long itemTable = -1; // long to store unsigned int32, java problem + private final List itemSets = new ArrayList<>(); + private int editorId; + private final short[] u1 = new short[8]; // short to store unsigned byte, java problem + + public void load(final LittleEndianDataInputStream stream, final int version) throws IOException { + this.id = ParseUtils.readWar3ID(stream); + this.variation = stream.readInt(); + ParseUtils.readFloatArray(stream, this.location); + this.angle = stream.readFloat(); + ParseUtils.readFloatArray(stream, this.scale); + this.flags = ParseUtils.readUInt8(stream); + this.life = ParseUtils.readUInt8(stream); + + if (version > 7) { + this.itemTable = ParseUtils.readUInt32(stream); + + for (long i = 0, l = ParseUtils.readUInt32(stream); i < l; i++) { + final RandomItemSet itemSet = new RandomItemSet(); + + itemSet.load(stream); + + this.itemSets.add(itemSet); + } + } + + this.editorId = stream.readInt(); + } + + public void save(final LittleEndianDataOutputStream stream, final int version) throws IOException { + ParseUtils.writeWar3ID(stream, this.id); + ; + stream.writeInt(this.variation); + ParseUtils.writeFloatArray(stream, this.location); + stream.writeFloat(this.angle); + ParseUtils.writeFloatArray(stream, this.scale); + ParseUtils.writeUInt8(stream, this.flags); + ParseUtils.writeUInt8(stream, this.life); + + if (version > 7) { + ParseUtils.writeUInt32(stream, this.itemTable); + ParseUtils.writeUInt32(stream, this.itemSets.size()); + + for (final RandomItemSet itemSet : this.itemSets) { + itemSet.save(stream); + } + } + + stream.writeInt(this.editorId); + } + + public int getByteLength(final int version) { + int size = 42; + + if (version > 7) { + size += 8; + + for (final RandomItemSet itemSet : this.itemSets) { + size += itemSet.getByteLength(); + } + } + + return size; + } + + public War3ID getId() { + return this.id; + } + + public void setId(final War3ID id) { + this.id = id; + } + + public int getVariation() { + return this.variation; + } + + public void setVariation(final int variation) { + this.variation = variation; + } + + public float getAngle() { + return this.angle; + } + + public void setAngle(final float angle) { + this.angle = angle; + } + + public short getFlags() { + return this.flags; + } + + public void setFlags(final short flags) { + this.flags = flags; + } + + public short getLife() { + return this.life; + } + + public void setLife(final short life) { + this.life = life; + } + + public long getItemTable() { + return this.itemTable; + } + + public void setItemTable(final long itemTable) { + this.itemTable = itemTable; + } + + public int getEditorId() { + return this.editorId; + } + + public void setEditorId(final int editorId) { + this.editorId = editorId; + } + + public float[] getLocation() { + return this.location; + } + + public float[] getScale() { + return this.scale; + } + + public List getItemSets() { + return this.itemSets; + } + + public short[] getU1() { + return this.u1; + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/doo/RandomItem.java b/core/src/com/etheller/warsmash/parsers/w3x/doo/RandomItem.java new file mode 100644 index 0000000..26fd3c2 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/doo/RandomItem.java @@ -0,0 +1,23 @@ +package com.etheller.warsmash.parsers.w3x.doo; + +import java.io.IOException; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class RandomItem { + private War3ID id; + private int chance; + + public void load(final LittleEndianDataInputStream stream) throws IOException { + this.id = ParseUtils.readWar3ID(stream); + this.chance = stream.readInt(); + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeWar3ID(stream, this.id); + stream.writeInt(this.chance); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/doo/RandomItemSet.java b/core/src/com/etheller/warsmash/parsers/w3x/doo/RandomItemSet.java new file mode 100644 index 0000000..f115b8e --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/doo/RandomItemSet.java @@ -0,0 +1,34 @@ +package com.etheller.warsmash.parsers.w3x.doo; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class RandomItemSet { + private final List items = new ArrayList<>(); + + public void load(final LittleEndianDataInputStream stream) throws IOException { + for (long i = 0, l = ParseUtils.readUInt32(stream); i < l; i++) { + final RandomItem item = new RandomItem(); + + item.load(stream); + + this.items.add(item); + } + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, this.items.size()); + for (final RandomItem item : this.items) { + item.save(stream); + } + } + + public int getByteLength() { + return 4 + (this.items.size() * 8); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/doo/TerrainDoodad.java b/core/src/com/etheller/warsmash/parsers/w3x/doo/TerrainDoodad.java new file mode 100644 index 0000000..5390166 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/doo/TerrainDoodad.java @@ -0,0 +1,54 @@ +package com.etheller.warsmash.parsers.w3x.doo; + +import java.io.IOException; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +/** + * A terrain doodad. + * + * This type of doodad works much like cliffs. It uses the height of the + * terrain, and gets affected by the ground heightmap. It cannot be manipulated + * in any way in the World Editor once placed. Indeed, the only way to change it + * is to remove it by changing cliffs around it. + */ +public class TerrainDoodad { + private War3ID id; + private long u1; + private final long[] location = new long[2]; + + public void load(final LittleEndianDataInputStream stream, final int version) throws IOException { + this.id = ParseUtils.readWar3ID(stream); + this.u1 = ParseUtils.readUInt32(stream); + ParseUtils.readUInt32Array(stream, this.location); + } + + public void save(final LittleEndianDataOutputStream stream, final int version) throws IOException { + ParseUtils.writeWar3ID(stream, this.id); + ParseUtils.writeUInt32(stream, this.u1); + ParseUtils.writeUInt32Array(stream, this.location); + } + + public War3ID getId() { + return this.id; + } + + public long getU1() { + return this.u1; + } + + public long[] getLocation() { + return this.location; + } + + public void setId(final War3ID id) { + this.id = id; + } + + public void setU1(final long u1) { + this.u1 = u1; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/doo/War3MapDoo.java b/core/src/com/etheller/warsmash/parsers/w3x/doo/War3MapDoo.java new file mode 100644 index 0000000..828fca2 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/doo/War3MapDoo.java @@ -0,0 +1,108 @@ +package com.etheller.warsmash.parsers.w3x.doo; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +/** + * war3map.doo - the doodad and destructible file. + */ +public class War3MapDoo { + private static final War3ID MAGIC_NUMBER = War3ID.fromString("W3do"); + private int version = 0; + private final short[] u1 = new short[4]; + private final List doodads = new ArrayList<>(); + private final short[] u2 = new short[4]; + private final List terrainDoodads = new ArrayList<>(); + + public War3MapDoo(final LittleEndianDataInputStream stream) throws IOException { + if (stream != null) { + this.load(stream); + } + } + + private boolean load(final LittleEndianDataInputStream stream) throws IOException { + final War3ID firstId = ParseUtils.readWar3ID(stream); + if (!MAGIC_NUMBER.equals(firstId)) { + return false; + } + + this.version = stream.readInt(); + ParseUtils.readUInt8Array(stream, this.u1); + + for (int i = 0, l = stream.readInt(); i < l; i++) { + final Doodad doodad = new Doodad(); + + doodad.load(stream, this.version); + + this.doodads.add(doodad); + } + + ParseUtils.readUInt8Array(stream, this.u2); + + for (int i = 0, l = stream.readInt(); i < l; i++) { + final TerrainDoodad terrainDoodad = new TerrainDoodad(); + + terrainDoodad.load(stream, this.version); + + this.terrainDoodads.add(terrainDoodad); + } + + return true; + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + + ParseUtils.writeWar3ID(stream, MAGIC_NUMBER); + stream.writeInt(this.version); + ParseUtils.writeUInt8Array(stream, this.u1); + ParseUtils.writeUInt32(stream, this.doodads.size()); + + for (final Doodad doodad : this.doodads) { + doodad.save(stream, this.version); + } + + ParseUtils.writeUInt8Array(stream, this.u2); + ParseUtils.writeUInt32(stream, this.terrainDoodads.size()); + + for (final TerrainDoodad terrainDoodad : this.terrainDoodads) { + terrainDoodad.save(stream, this.version); + } + } + + public int getByteLength() { + int size = 24 + (this.terrainDoodads.size() * 16); + + for (final Doodad doodad : this.doodads) { + size += doodad.getByteLength(this.version); + } + + return size; + } + + public int getVersion() { + return this.version; + } + + public short[] getU1() { + return this.u1; + } + + public List getDoodads() { + return this.doodads; + } + + public short[] getU2() { + return this.u2; + } + + public List getTerrainDoodads() { + return this.terrainDoodads; + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/objectdata/MakeMeTFTBeROC.java b/core/src/com/etheller/warsmash/parsers/w3x/objectdata/MakeMeTFTBeROC.java new file mode 100644 index 0000000..92a14de --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/objectdata/MakeMeTFTBeROC.java @@ -0,0 +1,98 @@ +package com.etheller.warsmash.parsers.w3x.objectdata; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Arrays; + +import com.etheller.warsmash.datasources.CompoundDataSourceDescriptor; +import com.etheller.warsmash.datasources.FolderDataSourceDescriptor; +import com.etheller.warsmash.datasources.MpqDataSourceDescriptor; +import com.etheller.warsmash.units.GameObject; +import com.etheller.warsmash.units.ObjectData; +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataOutputStream; + +public class MakeMeTFTBeROC { + + public static void main(final String[] args) { + + try { + final MpqDataSourceDescriptor reignOfChaosData = new MpqDataSourceDescriptor( + "E:\\Games\\Warcraft III Patch 1.14\\war3.mpq"); + final Warcraft3MapObjectData reignOfChaosUnitData = Warcraft3MapObjectData + .load(reignOfChaosData.createDataSource(), true); + + final FolderDataSourceDescriptor tftDesc1 = new FolderDataSourceDescriptor( + "D:\\NEEDS_ORGANIZING\\Reforged Beta 13991\\war3.w3mod"); + final FolderDataSourceDescriptor tftDesc2 = new FolderDataSourceDescriptor( + "D:\\NEEDS_ORGANIZING\\Reforged Beta 13991\\war3.w3mod\\_balance\\custom_v1.w3mod"); + final FolderDataSourceDescriptor tftDesc3 = new FolderDataSourceDescriptor( + "D:\\NEEDS_ORGANIZING\\Reforged Beta 13991\\war3.w3mod\\_locales\\enus.w3mod"); + final CompoundDataSourceDescriptor frozenThroneData = new CompoundDataSourceDescriptor( + Arrays.asList(tftDesc1, tftDesc2, tftDesc3)); + + final Warcraft3MapObjectData frozenThroneUnitData = Warcraft3MapObjectData + .load(frozenThroneData.createDataSource(), true); + for (final War3ID unitId : reignOfChaosUnitData.getUnits().keySet()) { + final MutableGameObject reignOfChaosUnit = reignOfChaosUnitData.getUnits().get(unitId); + final MutableGameObject frozenThroneEquivalentUnit = frozenThroneUnitData.getUnits().get(unitId); + if (frozenThroneEquivalentUnit == null) { + System.err.println("No TFT equivalent for: " + reignOfChaosUnit.getName()); + continue; + } + final ObjectData metaDataSlk = reignOfChaosUnitData.getUnits().getSourceSLKMetaData(); + for (final String fieldTypeId : metaDataSlk.keySet()) { + final War3ID fieldTypeIdCode = War3ID.fromString(fieldTypeId); + final GameObject unitFieldInformation = metaDataSlk.get(fieldTypeId); + if (unitFieldInformation.getFieldValue("useItem") == 1) { + continue; + } + final String fieldType = unitFieldInformation.getField("type"); + switch (fieldType) { + case "int": + frozenThroneEquivalentUnit.setField(fieldTypeIdCode, 0, + reignOfChaosUnit.getFieldAsInteger(fieldTypeIdCode, 0)); + break; + case "real": + case "unreal": + frozenThroneEquivalentUnit.setField(fieldTypeIdCode, 0, + reignOfChaosUnit.getFieldAsFloat(fieldTypeIdCode, 0)); + break; + case "bool": + frozenThroneEquivalentUnit.setField(fieldTypeIdCode, 0, + reignOfChaosUnit.getFieldAsBoolean(fieldTypeIdCode, 0)); + break; + case "string": + case "abilityList": + case "stringList": + case "soundLabel": + case "unitList": + case "itemList": + case "techList": + case "intList": + case "model": + case "char": + case "icon": + frozenThroneEquivalentUnit.setField(fieldTypeIdCode, 0, + reignOfChaosUnit.getFieldAsString(fieldTypeIdCode, 0)); + break; + default: // treat as string + break; + + } + } + } + + try (LittleEndianDataOutputStream outputStream = new LittleEndianDataOutputStream( + new FileOutputStream("C:\\Users\\micro\\OneDrive\\Documents\\Warcraft III\\Data\\roc.w3u"))) { + frozenThroneUnitData.getUnits().getEditorData().save(outputStream, false); + } + + } + catch (final IOException e) { + e.printStackTrace(); + } + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/objectdata/Warcraft3MapObjectData.java b/core/src/com/etheller/warsmash/parsers/w3x/objectdata/Warcraft3MapObjectData.java new file mode 100644 index 0000000..8236c53 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/objectdata/Warcraft3MapObjectData.java @@ -0,0 +1,184 @@ +package com.etheller.warsmash.parsers.w3x.objectdata; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.units.StandardObjectData; +import com.etheller.warsmash.units.StandardObjectData.WarcraftData; +import com.etheller.warsmash.units.custom.WTS; +import com.etheller.warsmash.units.custom.WTSFile; +import com.etheller.warsmash.units.custom.War3ObjectDataChangeset; +import com.etheller.warsmash.units.manager.MutableObjectData; +import com.etheller.warsmash.units.manager.MutableObjectData.WorldEditorDataType; +import com.etheller.warsmash.util.WorldEditStrings; +import com.google.common.io.LittleEndianDataInputStream; + +public final class Warcraft3MapObjectData { + private final MutableObjectData units; + private final MutableObjectData items; + private final MutableObjectData destructibles; + private final MutableObjectData doodads; + private final MutableObjectData abilities; + private final MutableObjectData buffs; + private final MutableObjectData upgrades; + private final List datas; + private transient Map typeToData = new HashMap<>(); + private final WTS wts; + + public Warcraft3MapObjectData(final MutableObjectData units, final MutableObjectData items, + final MutableObjectData destructibles, final MutableObjectData doodads, final MutableObjectData abilities, + final MutableObjectData buffs, final MutableObjectData upgrades, final WTS wts) { + this.units = units; + this.items = items; + this.destructibles = destructibles; + this.doodads = doodads; + this.abilities = abilities; + this.buffs = buffs; + this.upgrades = upgrades; + this.datas = new ArrayList<>(); + this.datas.add(units); + this.datas.add(items); + this.datas.add(destructibles); + this.datas.add(doodads); + this.datas.add(abilities); + this.datas.add(buffs); + this.datas.add(upgrades); + for (final MutableObjectData data : this.datas) { + this.typeToData.put(data.getWorldEditorDataType(), data); + } + this.wts = wts; + } + + public MutableObjectData getDataByType(final WorldEditorDataType type) { + return this.typeToData.get(type); + } + + public MutableObjectData getUnits() { + return this.units; + } + + public MutableObjectData getItems() { + return this.items; + } + + public MutableObjectData getDestructibles() { + return this.destructibles; + } + + public MutableObjectData getDoodads() { + return this.doodads; + } + + public MutableObjectData getAbilities() { + return this.abilities; + } + + public MutableObjectData getBuffs() { + return this.buffs; + } + + public MutableObjectData getUpgrades() { + return this.upgrades; + } + + public List getDatas() { + return this.datas; + } + + public WTS getWts() { + return this.wts; + } + + public static WTS loadWTS(final DataSource dataSource) throws IOException { + final WTS wts = dataSource.has("war3map.wts") ? new WTSFile(dataSource.getResourceAsStream("war3map.wts")) + : WTS.DO_NOTHING; + return wts; + } + + public static Warcraft3MapObjectData load(final DataSource dataSource, final boolean inlineWTS) throws IOException { + final WTS wts = loadWTS(dataSource); + return load(dataSource, inlineWTS, wts); + } + + public static Warcraft3MapObjectData load(final DataSource dataSource, final boolean inlineWTS, final WTS wts) + throws IOException { + + final StandardObjectData standardObjectData = new StandardObjectData(dataSource); + final WarcraftData standardUnits = standardObjectData.getStandardUnits(); + final WarcraftData standardItems = standardObjectData.getStandardItems(); + final WarcraftData standardDoodads = standardObjectData.getStandardDoodads(); + final WarcraftData standardDestructables = standardObjectData.getStandardDestructables(); + final WarcraftData abilities = standardObjectData.getStandardAbilities(); + final WarcraftData standardAbilityBuffs = standardObjectData.getStandardAbilityBuffs(); + final WarcraftData standardUpgrades = standardObjectData.getStandardUpgrades(); + + final DataTable standardUnitMeta = standardObjectData.getStandardUnitMeta(); + final DataTable standardDoodadMeta = standardObjectData.getStandardDoodadMeta(); + final DataTable standardDestructableMeta = standardObjectData.getStandardDestructableMeta(); + final DataTable abilityMeta = standardObjectData.getStandardAbilityMeta(); + final DataTable standardAbilityBuffMeta = standardObjectData.getStandardAbilityBuffMeta(); + final DataTable standardUpgradeMeta = standardObjectData.getStandardUpgradeMeta(); + + final War3ObjectDataChangeset unitChangeset = new War3ObjectDataChangeset('u'); + final War3ObjectDataChangeset itemChangeset = new War3ObjectDataChangeset('t'); + final War3ObjectDataChangeset doodadChangeset = new War3ObjectDataChangeset('d'); + final War3ObjectDataChangeset destructableChangeset = new War3ObjectDataChangeset('b'); + final War3ObjectDataChangeset abilityChangeset = new War3ObjectDataChangeset('a'); + final War3ObjectDataChangeset buffChangeset = new War3ObjectDataChangeset('h'); + final War3ObjectDataChangeset upgradeChangeset = new War3ObjectDataChangeset('q'); + + if (dataSource.has("war3map.w3u")) { + unitChangeset.load(new LittleEndianDataInputStream(dataSource.getResourceAsStream("war3map.w3u")), wts, + inlineWTS); + } + if (dataSource.has("war3map.w3t")) { + itemChangeset.load(new LittleEndianDataInputStream(dataSource.getResourceAsStream("war3map.w3t")), wts, + inlineWTS); + } + if (dataSource.has("war3map.w3d")) { + doodadChangeset.load(new LittleEndianDataInputStream(dataSource.getResourceAsStream("war3map.w3d")), wts, + inlineWTS); + } + if (dataSource.has("war3map.w3b")) { + destructableChangeset.load(new LittleEndianDataInputStream(dataSource.getResourceAsStream("war3map.w3b")), + wts, inlineWTS); + } + if (dataSource.has("war3map.w3a")) { + abilityChangeset.load(new LittleEndianDataInputStream(dataSource.getResourceAsStream("war3map.w3a")), wts, + inlineWTS); + } + if (dataSource.has("war3map.w3h")) { + buffChangeset.load(new LittleEndianDataInputStream(dataSource.getResourceAsStream("war3map.w3h")), wts, + inlineWTS); + } + if (dataSource.has("war3map.w3q")) { + upgradeChangeset.load(new LittleEndianDataInputStream(dataSource.getResourceAsStream("war3map.w3q")), wts, + inlineWTS); + } + + final WorldEditStrings worldEditStrings = standardObjectData.getWorldEditStrings(); + final MutableObjectData unitData = new MutableObjectData(worldEditStrings, WorldEditorDataType.UNITS, + standardUnits, standardUnitMeta, unitChangeset); + final MutableObjectData itemData = new MutableObjectData(worldEditStrings, WorldEditorDataType.ITEM, + standardItems, standardUnitMeta, itemChangeset); + final MutableObjectData doodadData = new MutableObjectData(worldEditStrings, WorldEditorDataType.DOODADS, + standardDoodads, standardDoodadMeta, doodadChangeset); + final MutableObjectData destructableData = new MutableObjectData(worldEditStrings, + WorldEditorDataType.DESTRUCTIBLES, standardDestructables, standardDestructableMeta, + destructableChangeset); + final MutableObjectData abilityData = new MutableObjectData(worldEditStrings, WorldEditorDataType.ABILITIES, + abilities, abilityMeta, abilityChangeset); + final MutableObjectData buffData = new MutableObjectData(worldEditStrings, WorldEditorDataType.BUFFS_EFFECTS, + standardAbilityBuffs, standardAbilityBuffMeta, buffChangeset); + final MutableObjectData upgradeData = new MutableObjectData(worldEditStrings, WorldEditorDataType.UPGRADES, + standardUpgrades, standardUpgradeMeta, upgradeChangeset); + + return new Warcraft3MapObjectData(unitData, itemData, destructableData, doodadData, abilityData, buffData, + upgradeData, wts); + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/DroppedItem.java b/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/DroppedItem.java new file mode 100644 index 0000000..28dfa43 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/DroppedItem.java @@ -0,0 +1,26 @@ +package com.etheller.warsmash.parsers.w3x.unitsdoo; + +import java.io.IOException; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +/** + * A dropped item. + */ +public class DroppedItem { + private War3ID id; + private int chance; + + public void load(final LittleEndianDataInputStream stream) throws IOException { + this.id = ParseUtils.readWar3ID(stream); + this.chance = stream.readInt(); + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeWar3ID(stream, this.id); + stream.writeInt(this.chance); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/DroppedItemSet.java b/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/DroppedItemSet.java new file mode 100644 index 0000000..d70b57f --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/DroppedItemSet.java @@ -0,0 +1,38 @@ +package com.etheller.warsmash.parsers.w3x.unitsdoo; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +/** + * A dropped item set. + */ +public class DroppedItemSet { + private final List items = new ArrayList<>(); + + public void load(final LittleEndianDataInputStream stream) throws IOException { + for (long i = 0, l = ParseUtils.readUInt32(stream); i < l; i++) { + final DroppedItem item = new DroppedItem(); + + item.load(stream); + + this.items.add(item); + } + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, this.items.size()); + + for (final DroppedItem item : this.items) { + item.save(stream); + } + } + + public int getByteLength() { + return 4 + (this.items.size() * 8); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/InventoryItem.java b/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/InventoryItem.java new file mode 100644 index 0000000..7912c25 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/InventoryItem.java @@ -0,0 +1,26 @@ +package com.etheller.warsmash.parsers.w3x.unitsdoo; + +import java.io.IOException; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +/** + * An inventory item. + */ +public class InventoryItem { + private int slot; + private War3ID id; + + public void load(final LittleEndianDataInputStream stream) throws IOException { + this.slot = stream.readInt(); + this.id = ParseUtils.readWar3ID(stream); + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + stream.writeInt(this.slot); + ParseUtils.writeWar3ID(stream, this.id); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/ModifiedAbility.java b/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/ModifiedAbility.java new file mode 100644 index 0000000..1a89ca5 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/ModifiedAbility.java @@ -0,0 +1,26 @@ +package com.etheller.warsmash.parsers.w3x.unitsdoo; + +import java.io.IOException; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class ModifiedAbility { + private War3ID id; + private int activeForAutocast = 0; + private int heroLevel = 1; + + public void load(final LittleEndianDataInputStream stream) throws IOException { + this.id = ParseUtils.readWar3ID(stream); + this.activeForAutocast = stream.readInt(); + this.heroLevel = stream.readInt(); + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeWar3ID(stream, this.id); + stream.writeInt(this.activeForAutocast); + stream.writeInt(this.heroLevel); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/RandomUnit.java b/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/RandomUnit.java new file mode 100644 index 0000000..ef0852e --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/RandomUnit.java @@ -0,0 +1,23 @@ +package com.etheller.warsmash.parsers.w3x.unitsdoo; + +import java.io.IOException; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class RandomUnit { + private War3ID id; + private int chance; + + public void load(final LittleEndianDataInputStream stream) throws IOException { + this.id = ParseUtils.readWar3ID(stream); + this.chance = stream.readInt(); + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeWar3ID(stream, this.id); + stream.writeInt(this.chance); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/Unit.java b/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/Unit.java new file mode 100644 index 0000000..1ce7c1b --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/Unit.java @@ -0,0 +1,428 @@ +package com.etheller.warsmash.parsers.w3x.unitsdoo; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class Unit { + private War3ID id; + private int variation; + private final float[] location = new float[3]; + private float angle; + private final float[] scale = new float[3]; + private short flags; + private int player; + private int unknown; + private int hitpoints = -1; + private int mana = -1; + /** + * @since 8 + */ + private int droppedItemTable = 0; + private final List droppedItemSets = new ArrayList<>(); + private int goldAmount; + private float targetAcquisition; + private int heroLevel; + /** + * @since 8 + */ + private int heroStrength; + /** + * @since 8 + */ + private int heroAgility; + /** + * @since 8 + */ + private int heroIntelligence; + private final List itemsInInventory = new ArrayList<>(); + private final List modifiedAbilities = new ArrayList<>(); + private int randomFlag; + private final short[] level = new short[3]; + private short itemClass; + private long unitGroup; + private long positionInGroup; + private final List randomUnitTables = new ArrayList<>(); + private int customTeamColor; + private int waygate; + private int creationNumber; + + public void load(final LittleEndianDataInputStream stream, final int version) throws IOException { + this.id = ParseUtils.readWar3ID(stream); + this.variation = stream.readInt(); + ParseUtils.readFloatArray(stream, this.location); + this.angle = stream.readFloat(); + ParseUtils.readFloatArray(stream, this.scale); + this.flags = ParseUtils.readUInt8(stream); + this.player = stream.readInt(); + this.unknown = ParseUtils.readUInt16(stream); + this.hitpoints = stream.readInt(); + this.mana = stream.readInt(); + + if (version > 7) { + this.droppedItemTable = stream.readInt(); + } + + for (int i = 0, l = stream.readInt(); i < l; i++) { + final DroppedItemSet set = new DroppedItemSet(); + + set.load(stream); + + this.droppedItemSets.add(set); + } + + this.goldAmount = stream.readInt(); + this.targetAcquisition = stream.readFloat(); + this.heroLevel = stream.readInt(); + + if (version > 7) { + this.heroStrength = stream.readInt(); + this.heroAgility = stream.readInt(); + this.heroIntelligence = stream.readInt(); + } + + for (int i = 0, l = stream.readInt(); i < l; i++) { + final InventoryItem item = new InventoryItem(); + + item.load(stream); + + this.itemsInInventory.add(item); + } + + for (int i = 0, l = stream.readInt(); i < l; i++) { + final ModifiedAbility modifiedAbility = new ModifiedAbility(); + + modifiedAbility.load(stream); + + this.modifiedAbilities.add(modifiedAbility); + } + + this.randomFlag = stream.readInt(); + + if (this.randomFlag == 0) { + ParseUtils.readUInt8Array(stream, this.level); + this.itemClass = ParseUtils.readUInt8(stream); + } + else if (this.randomFlag == 1) { + this.unitGroup = ParseUtils.readUInt32(stream); + this.positionInGroup = ParseUtils.readUInt32(stream); + } + else if (this.randomFlag == 2) { + for (int i = 0, l = stream.readInt(); i < l; i++) { + final RandomUnit randomUnit = new RandomUnit(); + + randomUnit.load(stream); + + this.randomUnitTables.add(randomUnit); + } + } + + this.customTeamColor = stream.readInt(); + this.waygate = stream.readInt(); + this.creationNumber = stream.readInt(); + } + + public void save(final LittleEndianDataOutputStream stream, final int version) throws IOException { + ParseUtils.writeWar3ID(stream, this.id); + stream.writeInt(this.variation); + ParseUtils.writeFloatArray(stream, this.location); + stream.writeFloat(this.angle); + ParseUtils.writeFloatArray(stream, this.scale); + ParseUtils.writeUInt8(stream, this.flags); + stream.writeInt(this.player); + ParseUtils.writeUInt16(stream, this.unknown); + stream.writeInt(this.hitpoints); + stream.writeInt(this.mana); + + if (version > 7) { + stream.writeInt(this.droppedItemTable); + } + + stream.writeInt(this.droppedItemSets.size()); + + for (final DroppedItemSet droppedItemSet : this.droppedItemSets) { + droppedItemSet.save(stream); + } + + stream.writeInt(this.goldAmount); + stream.writeFloat(this.targetAcquisition); + stream.writeInt(this.heroLevel); + + if (version > 7) { + stream.writeInt(this.heroStrength); + stream.writeInt(this.heroAgility); + stream.writeInt(this.heroIntelligence); + } + + stream.writeInt(this.itemsInInventory.size()); + + for (final InventoryItem itemInInventory : this.itemsInInventory) { + itemInInventory.save(stream); + } + + stream.writeInt(this.modifiedAbilities.size()); + + for (final ModifiedAbility modifiedAbility : this.modifiedAbilities) { + modifiedAbility.save(stream); + } + + stream.writeInt(this.randomFlag); + + if (this.randomFlag == 0) { + ParseUtils.writeUInt8Array(stream, this.level); + ParseUtils.writeUInt8(stream, this.itemClass); + } + else if (this.randomFlag == 1) { + ParseUtils.writeUInt32(stream, this.unitGroup); + ParseUtils.writeUInt32(stream, this.positionInGroup); + } + else if (this.randomFlag == 2) { + stream.writeInt(this.randomUnitTables.size()); + + for (final RandomUnit randomUnitTable : this.randomUnitTables) { + randomUnitTable.save(stream); + } + } + + stream.writeInt(this.customTeamColor); + stream.writeInt(this.waygate); + stream.writeInt(this.creationNumber); + } + + public int getByteLength(final int version) { + int size = 91; + + if (version > 7) { + size += 16; + } + + for (final DroppedItemSet droppedItemSet : this.droppedItemSets) { + size += droppedItemSet.getByteLength(); + } + + size += this.itemsInInventory.size() * 8; + + size += this.modifiedAbilities.size() * 12; + + if (this.randomFlag == 0) { + size += 4; + } + else if (this.randomFlag == 1) { + size += 8; + } + else if (this.randomFlag == 2) { + size += 4 + (this.randomUnitTables.size() * 8); + } + + return size; + } + + public War3ID getId() { + return this.id; + } + + public int getVariation() { + return this.variation; + } + + public float[] getLocation() { + return this.location; + } + + public float getAngle() { + return this.angle; + } + + public float[] getScale() { + return this.scale; + } + + public short getFlags() { + return this.flags; + } + + public int getPlayer() { + return this.player; + } + + public int getUnknown() { + return this.unknown; + } + + public int getHitpoints() { + return this.hitpoints; + } + + public int getMana() { + return this.mana; + } + + public int getDroppedItemTable() { + return this.droppedItemTable; + } + + public List getDroppedItemSets() { + return this.droppedItemSets; + } + + public int getGoldAmount() { + return this.goldAmount; + } + + public float getTargetAcquisition() { + return this.targetAcquisition; + } + + public int getHeroLevel() { + return this.heroLevel; + } + + public int getHeroStrength() { + return this.heroStrength; + } + + public int getHeroAgility() { + return this.heroAgility; + } + + public int getHeroIntelligence() { + return this.heroIntelligence; + } + + public List getItemsInInventory() { + return this.itemsInInventory; + } + + public List getModifiedAbilities() { + return this.modifiedAbilities; + } + + public int getRandomFlag() { + return this.randomFlag; + } + + public short[] getLevel() { + return this.level; + } + + public short getItemClass() { + return this.itemClass; + } + + public long getUnitGroup() { + return this.unitGroup; + } + + public long getPositionInGroup() { + return this.positionInGroup; + } + + public List getRandomUnitTables() { + return this.randomUnitTables; + } + + public int getCustomTeamColor() { + return this.customTeamColor; + } + + public int getWaygate() { + return this.waygate; + } + + public int getCreationNumber() { + return this.creationNumber; + } + + public void setId(final War3ID id) { + this.id = id; + } + + public void setVariation(final int variation) { + this.variation = variation; + } + + public void setAngle(final float angle) { + this.angle = angle; + } + + public void setFlags(final short flags) { + this.flags = flags; + } + + public void setPlayer(final int player) { + this.player = player; + } + + public void setUnknown(final int unknown) { + this.unknown = unknown; + } + + public void setHitpoints(final int hitpoints) { + this.hitpoints = hitpoints; + } + + public void setMana(final int mana) { + this.mana = mana; + } + + public void setDroppedItemTable(final int droppedItemTable) { + this.droppedItemTable = droppedItemTable; + } + + public void setGoldAmount(final int goldAmount) { + this.goldAmount = goldAmount; + } + + public void setTargetAcquisition(final float targetAcquisition) { + this.targetAcquisition = targetAcquisition; + } + + public void setHeroLevel(final int heroLevel) { + this.heroLevel = heroLevel; + } + + public void setHeroStrength(final int heroStrength) { + this.heroStrength = heroStrength; + } + + public void setHeroAgility(final int heroAgility) { + this.heroAgility = heroAgility; + } + + public void setHeroIntelligence(final int heroIntelligence) { + this.heroIntelligence = heroIntelligence; + } + + public void setRandomFlag(final int randomFlag) { + this.randomFlag = randomFlag; + } + + public void setItemClass(final short itemClass) { + this.itemClass = itemClass; + } + + public void setUnitGroup(final long unitGroup) { + this.unitGroup = unitGroup; + } + + public void setPositionInGroup(final long positionInGroup) { + this.positionInGroup = positionInGroup; + } + + public void setCustomTeamColor(final int customTeamColor) { + this.customTeamColor = customTeamColor; + } + + public void setWaygate(final int waygate) { + this.waygate = waygate; + } + + public void setCreationNumber(final int creationNumber) { + this.creationNumber = creationNumber; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/War3MapUnitsDoo.java b/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/War3MapUnitsDoo.java new file mode 100644 index 0000000..d8f6bfe --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/unitsdoo/War3MapUnitsDoo.java @@ -0,0 +1,76 @@ +package com.etheller.warsmash.parsers.w3x.unitsdoo; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class War3MapUnitsDoo { + private static final War3ID MAGIC_NUMBER = War3ID.fromString("W3do"); + private int version = 8; + private long unknown = 11; + private final List units = new ArrayList<>(); + + public War3MapUnitsDoo(final LittleEndianDataInputStream stream) throws IOException { + if (stream != null) { + this.load(stream); + } + } + + private boolean load(final LittleEndianDataInputStream stream) throws IOException { + final War3ID firstId = ParseUtils.readWar3ID(stream); + if (!MAGIC_NUMBER.equals(firstId)) { + return false; + } + + this.version = stream.readInt(); + this.unknown = ParseUtils.readUInt32(stream); + + for (int i = 0, l = stream.readInt(); i < l; i++) { + final Unit unit = new Unit(); + + unit.load(stream, this.version); + + this.units.add(unit); + } + + return true; + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeWar3ID(stream, MAGIC_NUMBER); + stream.writeInt(this.version); + ParseUtils.writeUInt32(stream, this.unknown); + stream.writeInt(this.units.size()); + + for (final Unit unit : this.units) { + unit.save(stream, this.version); + } + } + + public int getByteLength() { + int size = 16; + + for (final Unit unit : this.units) { + size += unit.getByteLength(this.version); + } + + return size; + } + + public List getUnits() { + return this.units; + } + + public int getVersion() { + return this.version; + } + + public void setVersion(final int version) { + this.version = version; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/w3e/Corner.java b/core/src/com/etheller/warsmash/parsers/w3x/w3e/Corner.java new file mode 100644 index 0000000..ff64e7c --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/w3e/Corner.java @@ -0,0 +1,149 @@ +package com.etheller.warsmash.parsers.w3x.w3e; + +import java.io.IOException; + +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +/** + * A tile corner. + */ +public class Corner { + private float groundHeight; + private float waterHeight; + private int mapEdge; + private int ramp; + private int blight; + private int water; + private int boundary; + private int groundTexture; + private int cliffVariation; + private int groundVariation; + private int cliffTexture; + private int layerHeight; + + public Corner() { + // TODO Auto-generated constructor stub + } + + public Corner(final Corner other) { + this.groundHeight = other.groundHeight; + this.waterHeight = other.waterHeight; + this.mapEdge = other.mapEdge; + this.ramp = other.ramp; + this.blight = other.blight; + this.water = other.water; + this.boundary = other.boundary; + this.groundTexture = other.groundTexture; + this.cliffVariation = other.cliffVariation; + this.groundVariation = other.groundVariation; + this.cliffTexture = other.cliffTexture; + this.layerHeight = other.layerHeight; + } + + public void load(final LittleEndianDataInputStream stream) throws IOException { + this.groundHeight = (stream.readShort() - 8192) / (float) 512; + + final short waterAndEdge = stream.readShort(); + this.waterHeight = ((waterAndEdge & 0x3FFF) - 8192) / (float) 512; + this.mapEdge = waterAndEdge & 0x4000; + + final short textureAndFlags = ParseUtils.readUInt8(stream); + + this.ramp = textureAndFlags & 0b00010000; + this.blight = textureAndFlags & 0b00100000; + this.water = textureAndFlags & 0b01000000; + this.boundary = textureAndFlags & 0b10000000; + + this.groundTexture = textureAndFlags & 0b00001111; + + final short variation = ParseUtils.readUInt8(stream); + + this.cliffVariation = (variation & 0b11100000) >>> 5; + this.groundVariation = variation & 0b00011111; + + final short cliffTextureAndLayer = ParseUtils.readUInt8(stream); + + this.cliffTexture = (cliffTextureAndLayer & 0b11110000) >>> 4; + this.layerHeight = cliffTextureAndLayer & 0b00001111; + + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + stream.writeShort((short) ((this.groundHeight * 512f) + 8192f)); + stream.writeShort((short) ((this.waterHeight * 512f) + 8192f + (this.mapEdge << 14))); + ParseUtils.writeUInt8(stream, (short) ((this.ramp << 4) | (this.blight << 5) | (this.water << 6) + | (this.boundary << 7) | this.groundTexture)); + ParseUtils.writeUInt8(stream, (short) ((this.cliffVariation << 5) | this.groundVariation)); + ParseUtils.writeUInt8(stream, (short) ((this.cliffTexture << 4) + this.layerHeight)); + } + + public float getGroundHeight() { + return this.groundHeight; + } + + public float getWaterHeight() { + return this.waterHeight; + } + + public int getMapEdge() { + return this.mapEdge; + } + + public int getRamp() { + return this.ramp; + } + + public boolean isRamp() { + return this.ramp != 0; + } + + public void setRamp(final int ramp) { + this.ramp = ramp; + } + + public int getBlight() { + return this.blight; + } + + public int getWater() { + return this.water; + } + + public int getBoundary() { + return this.boundary; + } + + public int getGroundTexture() { + return this.groundTexture; + } + + public int getCliffVariation() { + return this.cliffVariation; + } + + public int getGroundVariation() { + return this.groundVariation; + } + + public int getCliffTexture() { + return this.cliffTexture; + } + + public void setCliffTexture(final int cliffTexture) { + this.cliffTexture = cliffTexture; + } + + public int getLayerHeight() { + return this.layerHeight; + } + + public float computeFinalGroundHeight() { + return (this.groundHeight + this.layerHeight) - 2.0f; + } + + public float computeFinalWaterHeight(final float waterOffset) { + return this.waterHeight + waterOffset; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/w3e/War3MapW3e.java b/core/src/com/etheller/warsmash/parsers/w3x/w3e/War3MapW3e.java new file mode 100644 index 0000000..8494df7 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/w3e/War3MapW3e.java @@ -0,0 +1,148 @@ +package com.etheller.warsmash.parsers.w3x.w3e; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +/** + * war3map.w3e - the environment file. + */ +public class War3MapW3e { + private static final War3ID MAGIC_NUMBER = War3ID.fromString("W3E!"); + private int version; + private char tileset = 'A'; + private int hasCustomTileset; + private final List groundTiles = new ArrayList<>(); + private final List cliffTiles = new ArrayList<>(); + private final int[] mapSize = new int[2]; + private final float[] centerOffset = new float[2]; + private Corner[][] corners; + + public War3MapW3e(final LittleEndianDataInputStream stream) throws IOException { + if (stream != null) { + this.load(stream); + } + } + + private boolean load(final LittleEndianDataInputStream stream) throws IOException { + final War3ID firstId = ParseUtils.readWar3ID(stream); + if (!MAGIC_NUMBER.equals(firstId)) { + return false; + } + + this.version = stream.readInt(); + this.tileset = (char) stream.read(); + this.hasCustomTileset = stream.readInt(); + + for (int i = 0, l = stream.readInt(); i < l; i++) { + this.groundTiles.add(ParseUtils.readWar3ID(stream)); + } + + for (int i = 0, l = stream.readInt(); i < l; i++) { + this.cliffTiles.add(ParseUtils.readWar3ID(stream)); + } + + ParseUtils.readInt32Array(stream, this.mapSize); + ParseUtils.readFloatArray(stream, this.centerOffset); + + this.corners = new Corner[this.mapSize[1]][]; + for (int row = 0, rows = this.mapSize[1]; row < rows; row++) { + this.corners[row] = new Corner[this.mapSize[0]]; + + for (int column = 0, columns = this.mapSize[0]; column < columns; column++) { + final Corner corner = new Corner(); + + corner.load(stream); + + this.corners[row][column] = corner; + } + } + + return true; + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeWar3ID(stream, MAGIC_NUMBER); + stream.writeInt(this.version); + stream.write(this.tileset); + stream.writeInt(this.hasCustomTileset); + ParseUtils.writeUInt32(stream, this.groundTiles.size()); + + for (final War3ID groundTile : this.groundTiles) { + ParseUtils.writeWar3ID(stream, groundTile); + } + + ParseUtils.writeUInt32(stream, this.cliffTiles.size()); + + for (final War3ID cliffTile : this.cliffTiles) { + ParseUtils.writeWar3ID(stream, cliffTile); + } + + ParseUtils.writeInt32Array(stream, this.mapSize); + ParseUtils.writeFloatArray(stream, this.centerOffset); + + for (final Corner[] row : this.corners) { + for (final Corner corner : row) { + corner.save(stream); + } + } + } + + public int getByteLength() { + return 37 + (this.groundTiles.size() * 4) + (this.cliffTiles.size() * 4) + + (this.mapSize[0] * this.mapSize[1] * 7); + } + + public int getVersion() { + return this.version; + } + + public char getTileset() { + return this.tileset; + } + + public int getHasCustomTileset() { + return this.hasCustomTileset; + } + + public List getGroundTiles() { + return this.groundTiles; + } + + public List getCliffTiles() { + return this.cliffTiles; + } + + public int[] getMapSize() { + return this.mapSize; + } + + public float[] getCenterOffset() { + return this.centerOffset; + } + + public Corner[][] getCorners() { + return this.corners; + } + + public void setVersion(final int version) { + this.version = version; + } + + public void setTileset(final char tileset) { + this.tileset = tileset; + } + + public void setHasCustomTileset(final int hasCustomTileset) { + this.hasCustomTileset = hasCustomTileset; + } + + public void setCorners(final Corner[][] corners) { + this.corners = corners; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/w3i/Force.java b/core/src/com/etheller/warsmash/parsers/w3x/w3i/Force.java new file mode 100644 index 0000000..0d3f8dd --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/w3i/Force.java @@ -0,0 +1,29 @@ +package com.etheller.warsmash.parsers.w3x.w3i; + +import java.io.IOException; + +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class Force { + private long flags; + private long playerMasks; + private String name; + + public void load(final LittleEndianDataInputStream stream) throws IOException { + this.flags = ParseUtils.readUInt32(stream); + this.playerMasks = ParseUtils.readUInt32(stream); + this.name = ParseUtils.readUntilNull(stream); + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, this.flags); + ParseUtils.writeUInt32(stream, this.playerMasks); + ParseUtils.writeWithNullTerminator(stream, this.name); + } + + public int getByteLength() { + return 9 + this.name.length(); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/w3i/Player.java b/core/src/com/etheller/warsmash/parsers/w3x/w3i/Player.java new file mode 100644 index 0000000..5ab3d0a --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/w3i/Player.java @@ -0,0 +1,93 @@ +package com.etheller.warsmash.parsers.w3x.w3i; + +import java.io.IOException; + +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +/** + * A player. + */ +public class Player { + private int id; + private int type; + private int race; + private int isFixedStartPosition; + private String name; + private final float[] startLocation = new float[2]; + private long allyLowPriorities; + private long allyHighPriorities; + private long enemyLowPrioritiesFlags; + private long enemyHighPrioritiesFlags; + + public void load(final LittleEndianDataInputStream stream, final int version) throws IOException { + this.id = (int) ParseUtils.readUInt32(stream); + this.type = stream.readInt(); + this.race = stream.readInt(); + this.isFixedStartPosition = stream.readInt(); + this.name = ParseUtils.readUntilNull(stream); + ParseUtils.readFloatArray(stream, this.startLocation); + this.allyLowPriorities = ParseUtils.readUInt32(stream); + this.allyHighPriorities = ParseUtils.readUInt32(stream); + if (version > 30) { + this.enemyLowPrioritiesFlags = ParseUtils.readUInt32(stream); + this.enemyHighPrioritiesFlags = ParseUtils.readUInt32(stream); + } + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, this.id); + stream.writeInt(this.type); + stream.writeInt(this.race); + stream.writeInt(this.isFixedStartPosition); + ParseUtils.writeWithNullTerminator(stream, this.name); + ParseUtils.writeFloatArray(stream, this.startLocation); + ParseUtils.writeUInt32(stream, this.allyLowPriorities); + ParseUtils.writeUInt32(stream, this.allyHighPriorities); + } + + public int getByteLength() { + return 33 + this.name.length(); + } + + public int getId() { + return this.id; + } + + public int getType() { + return this.type; + } + + public int getRace() { + return this.race; + } + + public int getIsFixedStartPosition() { + return this.isFixedStartPosition; + } + + public String getName() { + return this.name; + } + + public float[] getStartLocation() { + return this.startLocation; + } + + public long getAllyLowPriorities() { + return this.allyLowPriorities; + } + + public long getAllyHighPriorities() { + return this.allyHighPriorities; + } + + public long getEnemyLowPrioritiesFlags() { + return this.enemyLowPrioritiesFlags; + } + + public long getEnemyHighPrioritiesFlags() { + return this.enemyHighPrioritiesFlags; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/w3i/RandomItem.java b/core/src/com/etheller/warsmash/parsers/w3x/w3i/RandomItem.java new file mode 100644 index 0000000..cd332ef --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/w3i/RandomItem.java @@ -0,0 +1,39 @@ +package com.etheller.warsmash.parsers.w3x.w3i; + +import java.io.IOException; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class RandomItem { + private int chance; + private War3ID id; + + public void load(final LittleEndianDataInputStream stream) throws IOException { + this.chance = stream.readInt(); + this.id = ParseUtils.readWar3ID(stream); + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + stream.writeInt(this.chance); + ParseUtils.writeWar3ID(stream, this.id); + } + + public int getChance() { + return this.chance; + } + + public War3ID getId() { + return this.id; + } + + public void setChance(final int chance) { + this.chance = chance; + } + + public void setId(final War3ID id) { + this.id = id; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/w3i/RandomItemSet.java b/core/src/com/etheller/warsmash/parsers/w3x/w3i/RandomItemSet.java new file mode 100644 index 0000000..4b3e2ea --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/w3i/RandomItemSet.java @@ -0,0 +1,39 @@ +package com.etheller.warsmash.parsers.w3x.w3i; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class RandomItemSet { + private final List items = new ArrayList<>(); + + public void load(final LittleEndianDataInputStream stream) throws IOException { + for (long i = 0, l = ParseUtils.readUInt32(stream); i < l; i++) { + final RandomItem item = new RandomItem(); + + item.load(stream); + + this.items.add(item); + } + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, this.items.size()); + + for (final RandomItem item : this.items) { + item.save(stream); + } + } + + public int getByteLength() { + return 4 + (this.items.size() * 8); + } + + public List getItems() { + return this.items; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/w3i/RandomItemTable.java b/core/src/com/etheller/warsmash/parsers/w3x/w3i/RandomItemTable.java new file mode 100644 index 0000000..634ff0c --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/w3i/RandomItemTable.java @@ -0,0 +1,69 @@ +package com.etheller.warsmash.parsers.w3x.w3i; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class RandomItemTable { + private War3ID id; + private String name; + private final List sets = new ArrayList<>(); + + public void load(final LittleEndianDataInputStream stream) throws IOException { + this.id = ParseUtils.readWar3ID(stream); + this.name = ParseUtils.readUntilNull(stream); + + for (long i = 0, l = ParseUtils.readUInt32(stream); i < l; i++) { + final RandomItemSet set = new RandomItemSet(); + + set.load(stream); + + this.sets.add(set); + } + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeWar3ID(stream, this.id); + ParseUtils.writeWithNullTerminator(stream, this.name); + ParseUtils.writeUInt32(stream, this.sets.size()); + + for (final RandomItemSet set : this.sets) { + set.save(stream); + } + } + + public int getByteLength() { + int size = 9 + this.name.length(); + + for (final RandomItemSet set : this.sets) { + size += set.getByteLength(); + } + + return size; + } + + public War3ID getId() { + return this.id; + } + + public String getName() { + return this.name; + } + + public List getSets() { + return this.sets; + } + + public void setId(final War3ID id) { + this.id = id; + } + + public void setName(final String name) { + this.name = name; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/w3i/RandomUnit.java b/core/src/com/etheller/warsmash/parsers/w3x/w3i/RandomUnit.java new file mode 100644 index 0000000..5ad694d --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/w3i/RandomUnit.java @@ -0,0 +1,31 @@ +package com.etheller.warsmash.parsers.w3x.w3i; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class RandomUnit { + private int chance; + private final List ids = new ArrayList<>(); + + public void load(final LittleEndianDataInputStream stream, final int positions) throws IOException { + this.chance = stream.readInt(); + + for (int i = 0; i < positions; i++) { + this.ids.add(ParseUtils.readWar3ID(stream)); + } + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + stream.writeInt(this.chance); + + for (final War3ID id : this.ids) { + ParseUtils.writeWar3ID(stream, id); + } + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/w3i/RandomUnitTable.java b/core/src/com/etheller/warsmash/parsers/w3x/w3i/RandomUnitTable.java new file mode 100644 index 0000000..3295681 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/w3i/RandomUnitTable.java @@ -0,0 +1,48 @@ +package com.etheller.warsmash.parsers.w3x.w3i; + +import java.io.IOException; +import java.util.List; + +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class RandomUnitTable { + private int id; + private String name; + private int positions; + private int[] columnTypes; + private List units; + + public void load(final LittleEndianDataInputStream stream) throws IOException { + this.id = stream.readInt(); // TODO is this a War3ID? + this.name = ParseUtils.readUntilNull(stream); + this.positions = stream.readInt(); + this.columnTypes = ParseUtils.readInt32Array(stream, this.positions); + + for (long i = 0, l = ParseUtils.readUInt32(stream); i < l; i++) { + final RandomUnit unit = new RandomUnit(); + + unit.load(stream, this.positions); + + this.units.add(unit); + } + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + stream.writeInt(this.id); + ParseUtils.writeWithNullTerminator(stream, this.name); + stream.writeInt(this.positions); + ParseUtils.writeInt32Array(stream, this.columnTypes); + ParseUtils.writeUInt32(stream, this.units.size()); + + for (final RandomUnit unit : this.units) { + unit.save(stream); + } + } + + public int getByteLength() { + return 13 + this.name.length() + (this.columnTypes.length * 4) + + (this.units.size() * (4 + (4 * this.positions))); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/w3i/TechAvailabilityChange.java b/core/src/com/etheller/warsmash/parsers/w3x/w3i/TechAvailabilityChange.java new file mode 100644 index 0000000..9c37f84 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/w3i/TechAvailabilityChange.java @@ -0,0 +1,24 @@ +package com.etheller.warsmash.parsers.w3x.w3i; + +import java.io.IOException; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class TechAvailabilityChange { + private long playerFlags; + private War3ID id; + + public void load(final LittleEndianDataInputStream stream) throws IOException { + this.playerFlags = ParseUtils.readUInt32(stream); + this.id = ParseUtils.readWar3ID(stream); + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, this.playerFlags); + ParseUtils.writeWar3ID(stream, this.id); + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/w3i/UpgradeAvailabilityChange.java b/core/src/com/etheller/warsmash/parsers/w3x/w3i/UpgradeAvailabilityChange.java new file mode 100644 index 0000000..ed922c7 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/w3i/UpgradeAvailabilityChange.java @@ -0,0 +1,30 @@ +package com.etheller.warsmash.parsers.w3x.w3i; + +import java.io.IOException; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class UpgradeAvailabilityChange { + private long playerFlags; + private War3ID id; + private int levelAffected; + private int availability; + + public void load(final LittleEndianDataInputStream stream) throws IOException { + this.playerFlags = ParseUtils.readUInt32(stream); + this.id = ParseUtils.readWar3ID(stream); + this.levelAffected = stream.readInt(); + this.availability = stream.readInt(); + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, this.playerFlags); + ParseUtils.writeWar3ID(stream, this.id); + stream.writeInt(this.levelAffected); + stream.writeInt(this.availability); + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/w3i/War3MapW3i.java b/core/src/com/etheller/warsmash/parsers/w3x/w3i/War3MapW3i.java new file mode 100644 index 0000000..32c6df7 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/w3i/War3MapW3i.java @@ -0,0 +1,456 @@ +package com.etheller.warsmash.parsers.w3x.w3i; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +/** + * war3map.w3i - the general map information file. + */ +public class War3MapW3i { + private int version; + private int saves; + private int editorVersion; + private final short[] unknown1 = new short[16]; + private String name; + private String author; + private String description; + private String recommendedPlayers; + private final float[] cameraBounds = new float[8]; + private final int[] cameraBoundsComplements = new int[4]; + private final int[] playableSize = new int[2]; + private long flags; + private char tileset = 'A'; + private int campaignBackground; + private String loadingScreenModel; + private String loadingScreenText; + private String loadingScreenTitle; + private String loadingScreenSubtitle; + private int loadingScreen; + private String prologueScreenModel; + private String prologueScreenText; + private String prologueScreenTitle; + private String prologueScreenSubtitle; + private int useTerrainFog; + private final float[] fogHeight = new float[2]; + private float fogDensity; + private final short[] fogColor = new short[4]; + private int globalWeather; + private String soundEnvironment; + private char lightEnvironmentTileset; + private final short[] waterVertexColor = new short[4]; + private final short[] unknown2ProbablyLua = new short[4]; + private final List players = new ArrayList<>(); + private final List forces = new ArrayList<>(); + private final List upgradeAvailabilityChanges = new ArrayList<>(); + private final List techAvailabilityChanges = new ArrayList<>(); + private final List randomUnitTables = new ArrayList<>(); + private final List randomItemTables = new ArrayList<>(); + + public War3MapW3i(final LittleEndianDataInputStream stream) throws IOException { + if (stream != null) { + load(stream); + } + } + + private void load(final LittleEndianDataInputStream stream) throws IOException { + this.version = stream.readInt(); + this.saves = stream.readInt(); + this.editorVersion = stream.readInt(); + + if (this.version > 27) { + ParseUtils.readUInt8Array(stream, this.unknown1); + } + + this.name = ParseUtils.readUntilNull(stream); + this.author = ParseUtils.readUntilNull(stream); + this.description = ParseUtils.readUntilNull(stream); + this.recommendedPlayers = ParseUtils.readUntilNull(stream); + ParseUtils.readFloatArray(stream, this.cameraBounds); + ParseUtils.readInt32Array(stream, this.cameraBoundsComplements); + ParseUtils.readInt32Array(stream, this.playableSize); + this.flags = ParseUtils.readUInt32(stream); + this.tileset = (char) stream.read(); + this.campaignBackground = stream.readInt(); + + if (this.version > 24) { + this.loadingScreenModel = ParseUtils.readUntilNull(stream); + } + + this.loadingScreenText = ParseUtils.readUntilNull(stream); + this.loadingScreenTitle = ParseUtils.readUntilNull(stream); + this.loadingScreenSubtitle = ParseUtils.readUntilNull(stream); + this.loadingScreen = stream.readInt(); + + if (this.version > 24) { + this.prologueScreenModel = ParseUtils.readUntilNull(stream); + } + + this.prologueScreenText = ParseUtils.readUntilNull(stream); + this.prologueScreenTitle = ParseUtils.readUntilNull(stream); + this.prologueScreenSubtitle = ParseUtils.readUntilNull(stream); + + if (this.version > 24) { + this.useTerrainFog = stream.readInt(); + ParseUtils.readFloatArray(stream, this.fogHeight); + this.fogDensity = stream.readFloat(); + ParseUtils.readUInt8Array(stream, this.fogColor); + this.globalWeather = stream.readInt(); // TODO probably war3id, right? + this.soundEnvironment = ParseUtils.readUntilNull(stream); + this.lightEnvironmentTileset = (char) stream.read(); + ParseUtils.readUInt8Array(stream, this.waterVertexColor); + } + + if (this.version > 27) { + ParseUtils.readUInt8Array(stream, this.unknown2ProbablyLua); + } + if (this.version > 30) { + final long supportedModes = ParseUtils.readUInt32(stream); + final long gameDataVersion = ParseUtils.readUInt32(stream); + } + + for (int i = 0, l = stream.readInt(); i < l; i++) { + final Player player = new Player(); + + player.load(stream, this.version); + + this.players.add(player); + } + + for (int i = 0, l = stream.readInt(); i < l; i++) { + final Force force = new Force(); + + force.load(stream); + + this.forces.add(force); + } + + if (stream.available() == 1) { + // some kind of really stupid protected map??? + return; + } + if (stream.available() > 0) { + for (int i = 0, l = stream.readInt(); i < l; i++) { + final UpgradeAvailabilityChange upgradeAvailabilityChange = new UpgradeAvailabilityChange(); + + upgradeAvailabilityChange.load(stream); + + this.upgradeAvailabilityChanges.add(upgradeAvailabilityChange); + } + } + + if (stream.available() > 0) { + for (int i = 0, l = stream.readInt(); i < l; i++) { + final TechAvailabilityChange techAvailabilityChange = new TechAvailabilityChange(); + + techAvailabilityChange.load(stream); + + this.techAvailabilityChanges.add(techAvailabilityChange); + } + } + + if (stream.available() > 0) { + for (int i = 0, l = stream.readInt(); i < l; i++) { + final RandomUnitTable randomUnitTable = new RandomUnitTable(); + + randomUnitTable.load(stream); + + this.randomUnitTables.add(randomUnitTable); + } + } + + if (this.version > 24) { + if (stream.available() > 0) { + for (int i = 0, l = stream.readInt(); i < l; i++) { + final RandomItemTable randomItemTable = new RandomItemTable(); + + randomItemTable.load(stream); + + this.randomItemTables.add(randomItemTable); + } + } + } + } + + public void save(final LittleEndianDataOutputStream stream) throws IOException { + stream.writeInt(this.version); + stream.writeInt(this.saves); + stream.writeInt(this.editorVersion); + + if (this.version > 27) { + ParseUtils.writeUInt8Array(stream, this.unknown1); + } + + ParseUtils.writeWithNullTerminator(stream, this.name); + ParseUtils.writeWithNullTerminator(stream, this.author); + ParseUtils.writeWithNullTerminator(stream, this.description); + ParseUtils.writeWithNullTerminator(stream, this.recommendedPlayers); + ParseUtils.writeFloatArray(stream, this.cameraBounds); + ParseUtils.writeInt32Array(stream, this.cameraBoundsComplements); + ParseUtils.writeInt32Array(stream, this.playableSize); + ParseUtils.writeUInt32(stream, this.flags); + stream.write((byte) this.tileset); + stream.writeInt(this.campaignBackground); + + if (this.version > 24) { + ParseUtils.writeWithNullTerminator(stream, this.loadingScreenModel); + } + + ParseUtils.writeWithNullTerminator(stream, this.loadingScreenText); + ParseUtils.writeWithNullTerminator(stream, this.loadingScreenTitle); + ParseUtils.writeWithNullTerminator(stream, this.loadingScreenSubtitle); + stream.writeInt(this.loadingScreen); + + if (this.version > 24) { + ParseUtils.writeWithNullTerminator(stream, this.prologueScreenModel); + } + + ParseUtils.writeWithNullTerminator(stream, this.prologueScreenText); + ParseUtils.writeWithNullTerminator(stream, this.prologueScreenTitle); + ParseUtils.writeWithNullTerminator(stream, this.prologueScreenSubtitle); + + if (this.version > 24) { + stream.writeInt(this.useTerrainFog); + ParseUtils.writeFloatArray(stream, this.fogHeight); + stream.writeFloat(this.fogDensity); + ParseUtils.writeUInt8Array(stream, this.fogColor); + stream.writeInt(this.globalWeather); // TODO War3ID??? + ParseUtils.writeWithNullTerminator(stream, this.soundEnvironment); + stream.write((byte) this.lightEnvironmentTileset); + ParseUtils.writeUInt8Array(stream, this.waterVertexColor); + } + + if (this.version > 27) { + ParseUtils.writeUInt8Array(stream, this.unknown2ProbablyLua); + } + + ParseUtils.writeUInt32(stream, this.players.size()); + + for (final Player player : this.players) { + player.save(stream); + } + + ParseUtils.writeUInt32(stream, this.forces.size()); + + for (final Force force : this.forces) { + force.save(stream); + } + + ParseUtils.writeUInt32(stream, this.upgradeAvailabilityChanges.size()); + + for (final UpgradeAvailabilityChange change : this.upgradeAvailabilityChanges) { + change.save(stream); + } + + ParseUtils.writeUInt32(stream, this.techAvailabilityChanges.size()); + + for (final TechAvailabilityChange change : this.techAvailabilityChanges) { + change.save(stream); + } + + ParseUtils.writeUInt32(stream, this.randomUnitTables.size()); + + for (final RandomUnitTable table : this.randomUnitTables) { + table.save(stream); + } + + if (this.version > 24) { + ParseUtils.writeUInt32(stream, this.randomItemTables.size()); + + for (final RandomItemTable table : this.randomItemTables) { + table.save(stream); + } + } + + } + + public int getByteLength() { + int size = 111 + this.name.length() + this.author.length() + this.description.length() + + this.recommendedPlayers.length() + this.loadingScreenText.length() + this.loadingScreenTitle.length() + + this.loadingScreenSubtitle.length() + this.prologueScreenText.length() + + this.prologueScreenTitle.length() + this.prologueScreenSubtitle.length(); + + for (final Player player : this.players) { + size += player.getByteLength(); + } + + for (final Force force : this.forces) { + size += force.getByteLength(); + } + + size += this.upgradeAvailabilityChanges.size() * 16; + + size += this.techAvailabilityChanges.size() * 8; + + for (final RandomUnitTable table : this.randomUnitTables) { + size += table.getByteLength(); + } + + if (this.version > 24) { + size += 36 + this.loadingScreenModel.length() + this.prologueScreenModel.length() + + this.soundEnvironment.length(); + + for (final RandomItemTable table : this.randomItemTables) { + size += table.getByteLength(); + } + } + + return size; + } + + public int getVersion() { + return this.version; + } + + public int getSaves() { + return this.saves; + } + + public int getEditorVersion() { + return this.editorVersion; + } + + public short[] getUnknown1() { + return this.unknown1; + } + + public String getName() { + return this.name; + } + + public String getAuthor() { + return this.author; + } + + public String getDescription() { + return this.description; + } + + public String getRecommendedPlayers() { + return this.recommendedPlayers; + } + + public float[] getCameraBounds() { + return this.cameraBounds; + } + + public int[] getCameraBoundsComplements() { + return this.cameraBoundsComplements; + } + + public int[] getPlayableSize() { + return this.playableSize; + } + + public long getFlags() { + return this.flags; + } + + public char getTileset() { + return this.tileset; + } + + public int getCampaignBackground() { + return this.campaignBackground; + } + + public String getLoadingScreenModel() { + return this.loadingScreenModel; + } + + public String getLoadingScreenText() { + return this.loadingScreenText; + } + + public String getLoadingScreenTitle() { + return this.loadingScreenTitle; + } + + public String getLoadingScreenSubtitle() { + return this.loadingScreenSubtitle; + } + + public int getLoadingScreen() { + return this.loadingScreen; + } + + public String getPrologueScreenModel() { + return this.prologueScreenModel; + } + + public String getPrologueScreenText() { + return this.prologueScreenText; + } + + public String getPrologueScreenTitle() { + return this.prologueScreenTitle; + } + + public String getPrologueScreenSubtitle() { + return this.prologueScreenSubtitle; + } + + public int getUseTerrainFog() { + return this.useTerrainFog; + } + + public float[] getFogHeight() { + return this.fogHeight; + } + + public float getFogDensity() { + return this.fogDensity; + } + + public short[] getFogColor() { + return this.fogColor; + } + + public int getGlobalWeather() { + return this.globalWeather; + } + + public String getSoundEnvironment() { + return this.soundEnvironment; + } + + public char getLightEnvironmentTileset() { + return this.lightEnvironmentTileset; + } + + public short[] getWaterVertexColor() { + return this.waterVertexColor; + } + + public short[] getUnknown2() { + return this.unknown2ProbablyLua; + } + + public List getPlayers() { + return this.players; + } + + public List getForces() { + return this.forces; + } + + public List getUpgradeAvailabilityChanges() { + return this.upgradeAvailabilityChanges; + } + + public List getTechAvailabilityChanges() { + return this.techAvailabilityChanges; + } + + public List getRandomUnitTables() { + return this.randomUnitTables; + } + + public List getRandomItemTables() { + return this.randomItemTables; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/w3x/wpm/War3MapWpm.java b/core/src/com/etheller/warsmash/parsers/w3x/wpm/War3MapWpm.java new file mode 100644 index 0000000..78b436f --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/w3x/wpm/War3MapWpm.java @@ -0,0 +1,54 @@ +package com.etheller.warsmash.parsers.w3x.wpm; + +import java.io.IOException; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; + +public class War3MapWpm { + private static final War3ID MAGIC_NUMBER = War3ID.fromString("MP3W"); + private int version; + private final int[] size = new int[2]; + private short[] pathing; + + public War3MapWpm(final LittleEndianDataInputStream stream) throws IOException { + if (stream != null) { + this.load(stream); + } + } + + private boolean load(final LittleEndianDataInputStream stream) throws IOException { + final War3ID firstId = ParseUtils.readWar3ID(stream); + if (!MAGIC_NUMBER.equals(firstId)) { + return false; + } + + this.version = stream.readInt(); + ParseUtils.readInt32Array(stream, this.size); + this.pathing = ParseUtils.readUInt8Array(stream, this.size[0] * this.size[1]); + + return true; + } + + public int getVersion() { + return this.version; + } + + public int[] getSize() { + return this.size; + } + + public short[] getPathing() { + return this.pathing; + } + + public void setVersion(final int version) { + this.version = version; + } + + public void setPathing(final short[] pathing) { + this.pathing = pathing; + } + +} diff --git a/core/src/com/etheller/warsmash/units/DataTable.java b/core/src/com/etheller/warsmash/units/DataTable.java new file mode 100644 index 0000000..d9c33e3 --- /dev/null +++ b/core/src/com/etheller/warsmash/units/DataTable.java @@ -0,0 +1,326 @@ +package com.etheller.warsmash.units; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.etheller.warsmash.util.StringBundle; + +public class DataTable implements ObjectData { + public static boolean DEBUG = false; + + Map dataTable = new LinkedHashMap<>(); + + private final StringBundle worldEditStrings; + + public DataTable(final StringBundle worldEditStrings) { + this.worldEditStrings = worldEditStrings; + } + + @Override + public String getLocalizedString(final String key) { + return this.worldEditStrings.getString(key); + } + + @Override + public Set keySet() { + final Set outputKeySet = new HashSet<>(); + final Set internalKeySet = this.dataTable.keySet(); + for (final StringKey key : internalKeySet) { + outputKeySet.add(key.getString()); + } + return outputKeySet; + } + + public void readTXT(final InputStream inputStream) { + try { + readTXT(inputStream, false); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + + public void readTXT(final File f) { + readTXT(f, false); + } + + public void readTXT(final File f, final boolean canProduce) { + try { + readTXT(new FileInputStream(f), canProduce); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + + public void readSLK(final File f) { + try { + readSLK(new FileInputStream(f)); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + + public void readTXT(final InputStream txt, final boolean canProduce) throws IOException { + if (txt == null) { + return; + } + final BufferedReader reader = new BufferedReader(new InputStreamReader(txt, "utf-8")); + // BOM marker will only appear on the very beginning + reader.mark(4); + if ('\ufeff' != reader.read()) { + reader.reset(); // not the BOM marker + } + + String input = ""; + Element currentUnit = null; + final boolean first = true; + while ((input = reader.readLine()) != null) { + if (DEBUG) { + System.out.println(input); + } + if (input.startsWith("//")) { + continue; + } + if (input.startsWith("[") && input.contains("]")) { + final int start = input.indexOf("[") + 1; + final int end = input.indexOf("]"); + final String newKey = input.substring(start, end); + final String newKeyBase = newKey; + currentUnit = this.dataTable.get(new StringKey(newKey)); + if (currentUnit == null) { + currentUnit = new Element(newKey, this); + if (canProduce) { + currentUnit = new LMUnit(newKey, this); + this.dataTable.put(new StringKey(newKey), currentUnit); + } + } + } + else if (input.contains("=")) { + final int eIndex = input.indexOf("="); + final String fieldValue = input.substring(eIndex + 1); + final StringBuilder builder = new StringBuilder(); + boolean withinQuotedString = false; + final String fieldName = input.substring(0, eIndex); + boolean wasSlash = false; + final List values = new ArrayList<>(); + for (int i = 0; i < fieldValue.length(); i++) { + final char c = fieldValue.charAt(i); + final boolean isSlash = c == '/'; + if (isSlash && wasSlash && !withinQuotedString) { + builder.setLength(builder.length() - 1); + break; // comment starts here + } + if (c == '\"') { + withinQuotedString = !withinQuotedString; + } + else if (!withinQuotedString && (c == ',')) { + values.add(builder.toString().trim()); + builder.setLength(0); // empty buffer + } + else { + builder.append(c); + } + wasSlash = isSlash; + } + if (builder.length() > 0) { + if (currentUnit == null) { + System.out.println("null for " + input); + } + values.add(builder.toString().trim()); + } + currentUnit.setField(fieldName, values); + } + } + + reader.close(); + } + + public void readSLK(final InputStream txt) throws IOException { + if (txt == null) { + return; + } + final BufferedReader reader = new BufferedReader(new InputStreamReader(txt, "utf-8")); + + String input = ""; + Element currentUnit = null; + input = reader.readLine(); + if (!input.contains("ID")) { + System.err.println("Formatting of SLK is unusual."); + } + input = reader.readLine(); + while (input.startsWith("P;") || input.startsWith("F;")) { + input = reader.readLine(); + } + final int yIndex = input.indexOf("Y") + 1; + final int xIndex = input.indexOf("X") + 1; + int colCount = 0; + int rowCount = 0; + boolean flipMode = false; + if (xIndex > yIndex) { + colCount = Integer.parseInt(input.substring(xIndex, input.lastIndexOf(";"))); + rowCount = Integer.parseInt(input.substring(yIndex, xIndex - 2)); + } + else { + rowCount = Integer.parseInt(input.substring(yIndex, input.lastIndexOf(";"))); + colCount = Integer.parseInt(input.substring(xIndex, yIndex - 2)); + flipMode = true; + } + int rowStartCount = 0; + String[] dataNames = new String[colCount]; + int col = 0; + int lastFieldId = 0; + while ((input = reader.readLine()) != null) { + if (DEBUG) { + System.out.println(input); + } + if (input.startsWith("E")) { + break; + } + if (input.startsWith("O;")) { + continue; + } + if (input.contains("X1;") || input.endsWith(";X1")) { + rowStartCount++; + col = 0; + } + else { + col++; + } + String kInput; + if (input.startsWith("F;")) { + kInput = reader.readLine(); + if (DEBUG) { + System.out.println(kInput); + } + } + else { + kInput = input; + } + if (rowStartCount <= 1) { + final int subXIndex = input.indexOf("X"); + final int subYIndex = input.indexOf("Y"); + if ((subYIndex >= 0) && (subYIndex < subXIndex)) { + final int eIndex = kInput.indexOf("K"); + final int fieldIdEndIndex = kInput != input ? input.length() : eIndex - 1; + if ((eIndex == -1) || (kInput.charAt(eIndex - 1) != ';')) { + continue; + } + final int fieldId; + if (subXIndex < 0) { + if (lastFieldId == 0) { + rowStartCount++; + } + fieldId = lastFieldId + 1; + } + else { + fieldId = Integer.parseInt(input.substring(subXIndex + 1, fieldIdEndIndex)); + } + + final int quotationIndex = kInput.indexOf("\""); + if ((fieldId - 1) >= dataNames.length) { + dataNames = Arrays.copyOf(dataNames, fieldId); + } + if (quotationIndex == -1) { + dataNames[fieldId - 1] = kInput.substring(eIndex + 1); + } + else { + dataNames[fieldId - 1] = kInput.substring(quotationIndex + 1, kInput.lastIndexOf("\"")); + } + lastFieldId = fieldId; + continue; + } + else { + int eIndex = kInput.indexOf("K"); + if ((eIndex == -1) || (kInput.charAt(eIndex - 1) != ';')) { + continue; + } + final int fieldId; + if (subXIndex < 0) { + if (lastFieldId == 0) { + rowStartCount++; + } + fieldId = lastFieldId + 1; + } + else { + if (flipMode && input.contains("Y") && (input == kInput)) { + eIndex = Math.min(subYIndex, eIndex); + } + final int fieldIdEndIndex = kInput != input ? input.length() : eIndex - 1; + fieldId = Integer.parseInt(input.substring(subXIndex + 1, fieldIdEndIndex)); + } + + final int quotationIndex = kInput.indexOf("\""); + if ((fieldId - 1) >= dataNames.length) { + dataNames = Arrays.copyOf(dataNames, fieldId); + } + if (quotationIndex == -1) { + dataNames[fieldId - 1] = kInput.substring(eIndex + 1, kInput.length()); + } + else { + dataNames[fieldId - 1] = kInput.substring(quotationIndex + 1, kInput.lastIndexOf("\"")); + } + lastFieldId = fieldId; + continue; + } + } + if (input.contains("X1;") || ((input != kInput) && input.endsWith("X1"))) { + final int start = kInput.indexOf("\"") + 1; + final int end = kInput.lastIndexOf("\""); + if ((start - 1) != end) { + final String newKey = kInput.substring(start, end); + currentUnit = this.dataTable.get(new StringKey(newKey)); + if (currentUnit == null) { + currentUnit = new Element(newKey, this); + this.dataTable.put(new StringKey(newKey), currentUnit); + } + } + } + else if (kInput.contains("K")) { + final int subXIndex = input.indexOf("X"); + int eIndex = kInput.indexOf("K"); + if (flipMode && kInput.contains("Y")) { + eIndex = Math.min(kInput.indexOf("Y"), eIndex); + } + final int fieldIdEndIndex = kInput != input ? input.length() : eIndex - 1; + final int fieldId = (subXIndex == -1) || (subXIndex > fieldIdEndIndex) ? 1 + : Integer.parseInt(input.substring(subXIndex + 1, fieldIdEndIndex)); + String fieldValue = kInput.substring(eIndex + 1); + if ((fieldValue.length() > 1) && fieldValue.startsWith("\"") && fieldValue.endsWith("\"")) { + fieldValue = fieldValue.substring(1, fieldValue.length() - 1); + } + if (dataNames[fieldId - 1] != null) { + currentUnit.setField(dataNames[fieldId - 1], fieldValue); + } + } + } + + reader.close(); + } + + @Override + public Element get(final String id) { + return this.dataTable.get(new StringKey(id)); + } + + @Override + public void setValue(final String id, final String field, final String value) { + get(id).setField(field, value); + } + + public void put(final String id, final Element e) { + this.dataTable.put(new StringKey(id), e); + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/units/Element.java b/core/src/com/etheller/warsmash/units/Element.java new file mode 100644 index 0000000..15de6fd --- /dev/null +++ b/core/src/com/etheller/warsmash/units/Element.java @@ -0,0 +1,217 @@ +package com.etheller.warsmash.units; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +public class Element extends HashedGameObject { + + public Element(final String id, final DataTable table) { + super(id, table); + } + + public List builds() { + return getFieldAsList("Builds", this.parentTable); + } + + public List requires() { + final List requirements = getFieldAsList("Requires", this.parentTable); + final List reqLvls = requiresLevels(); + return requirements; + } + + public List requiresLevels() { + final String stringList = getField("Requiresamount"); + final String[] listAsArray = stringList.split(","); + final LinkedList output = new LinkedList<>(); + if ((listAsArray != null) && (listAsArray.length > 0) && !listAsArray[0].equals("")) { + for (final String levelString : listAsArray) { + final Integer level = Integer.parseInt(levelString); + if (level != null) { + output.add(level); + } + } + } + return output; + } + + public List parents() { + return getFieldAsList("Parents", this.parentTable); + } + + public List children() { + return getFieldAsList("Children", this.parentTable); + } + + public List requiredBy() { + return getFieldAsList("RequiredBy", this.parentTable); + } + + public List trains() { + return getFieldAsList("Trains", this.parentTable); + } + + public List upgrades() { + return getFieldAsList("Upgrade", this.parentTable); + } + + public List researches() { + return getFieldAsList("Researches", this.parentTable); + } + + public List dependencyOr() { + return getFieldAsList("DependencyOr", this.parentTable); + } + + public List abilities() { + return getFieldAsList("abilList", this.parentTable); + } + + HashMap> hashedLists = new HashMap<>(); + + @Override + public String toString() { + return getField("Name"); + } + + public int getTechTier() { + final String tier = getField("Custom Field: TechTier"); + if (tier == null) { + return -1; + } + return Integer.parseInt(tier); + } + + public void setTechTier(final int i) { + setField("Custom Field: TechTier", i + ""); + } + + public int getTechDepth() { + final String tier = getField("Custom Field: TechDepth"); + if (tier == null) { + return -1; + } + return Integer.parseInt(tier); + } + + public void setTechDepth(final int i) { + setField("Custom Field: TechDepth", i + ""); + } + + public String getIconPath() { + String artField = getField("Art"); + if (artField.indexOf(',') != -1) { + artField = artField.substring(0, artField.indexOf(',')); + } + return artField; + } + + public String getUnitId() { + return this.id; + } + + @Override + public String getName() { + String name = getField("Name"); + boolean nameKnown = name.length() >= 1; + if (!nameKnown && !getField("code").equals(this.id) && (getField("code").length() >= 4)) { + final Element other = (Element) this.parentTable.get(getField("code").substring(0, 4)); + if (other != null) { + name = other.getName(); + nameKnown = true; + } + } + if (!nameKnown && (getField("EditorName").length() > 1)) { + name = getField("EditorName"); + nameKnown = true; + } + if (!nameKnown && (getField("Editorname").length() > 1)) { + name = getField("Editorname"); + nameKnown = true; + } + if (!nameKnown && (getField("BuffTip").length() > 1)) { + name = getField("BuffTip"); + nameKnown = true; + } + if (!nameKnown && (getField("Bufftip").length() > 1)) { + name = getField("Bufftip"); + nameKnown = true; + } + if (nameKnown && name.startsWith("WESTRING")) { + if (!name.contains(" ")) { + name = this.parentTable.getLocalizedString(name); + } + else { + final String[] names = name.split(" "); + name = ""; + for (final String subName : names) { + if (name.length() > 0) { + name += " "; + } + if (subName.startsWith("WESTRING")) { + name += this.parentTable.getLocalizedString(subName); + } + else { + name += subName; + } + } + } + if (name.startsWith("\"") && name.endsWith("\"")) { + name = name.substring(1, name.length() - 1); + } + setField("Name", name); + } + if (!nameKnown) { + name = this.parentTable.getLocalizedString("WESTRING_UNKNOWN") + " '" + getUnitId() + "'"; + } + if (getField("campaign").startsWith("1") && Character.isUpperCase(getUnitId().charAt(0))) { + name = getField("Propernames"); + if (name.contains(",")) { + name = name.split(",")[0]; + } + } + String suf = getField("EditorSuffix"); + if ((suf.length() > 0) && !suf.equals("_")) { + if (suf.startsWith("WESTRING")) { + suf = this.parentTable.getLocalizedString(suf); + } + if (!suf.startsWith(" ")) { + name += " "; + } + name += suf; + } + return name; + } + + public void addParent(final String parentId) { + String parentField = getField("Parents"); + if (!parentField.contains(parentId)) { + parentField = parentField + "," + parentId; + setField("Parents", parentField); + } + } + + public void addChild(final String parentId) { + String parentField = getField("Children"); + if (!parentField.contains(parentId)) { + parentField = parentField + "," + parentId; + setField("Children", parentField); + } + } + + public void addRequiredBy(final String parentId) { + String parentField = getField("RequiredBy"); + if (!parentField.contains(parentId)) { + parentField = parentField + "," + parentId; + setField("RequiredBy", parentField); + } + } + + public void addResearches(final String parentId) { + String parentField = getField("Researches"); + if (!parentField.contains(parentId)) { + parentField = parentField + "," + parentId; + setField("Researches", parentField); + } + } +} diff --git a/core/src/com/etheller/warsmash/units/GameObject.java b/core/src/com/etheller/warsmash/units/GameObject.java new file mode 100644 index 0000000..bc5f5d0 --- /dev/null +++ b/core/src/com/etheller/warsmash/units/GameObject.java @@ -0,0 +1,101 @@ +package com.etheller.warsmash.units; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +public interface GameObject { + + public void setField(String field, String value); + + public void setField(String field, String value, int index); + + public String getField(String field); + + public String getField(String field, int index); + + public int getFieldValue(String field); + + public int getFieldValue(String field, int index); + + public float getFieldFloatValue(String field); + + public float getFieldFloatValue(String field, int index); + + public List getFieldAsList(String field, ObjectData objectData); + + public String getId(); + + public ObjectData getTable(); + + public String getName(); + + public Set keySet(); + + GameObject EMPTY = new GameObject() { + + @Override + public void setField(final String field, final String value, final int index) { + } + + @Override + public void setField(final String field, final String value) { + } + + @Override + public Set keySet() { + return Collections.emptySet(); + } + + @Override + public ObjectData getTable() { + return null; + } + + @Override + public String getName() { + return ""; + } + + @Override + public String getId() { + return "0000"; + } + + @Override + public int getFieldValue(final String field, final int index) { + return 0; + } + + @Override + public int getFieldValue(final String field) { + return 0; + } + + @Override + public float getFieldFloatValue(final String field) { + return 0; + } + + @Override + public float getFieldFloatValue(final String field, final int index) { + return 0; + } + + @Override + public List getFieldAsList(final String field, final ObjectData objectData) { + return Collections.emptyList(); + } + + @Override + public String getField(final String field, final int index) { + return ""; + } + + @Override + public String getField(final String field) { + return ""; + } + }; + +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/units/HashedGameObject.java b/core/src/com/etheller/warsmash/units/HashedGameObject.java new file mode 100644 index 0000000..0768c93 --- /dev/null +++ b/core/src/com/etheller/warsmash/units/HashedGameObject.java @@ -0,0 +1,278 @@ +package com.etheller.warsmash.units; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public abstract class HashedGameObject implements GameObject { + HashMap> fields = new HashMap<>(); + String id; + ObjectData parentTable; + + transient HashMap> hashedLists = new HashMap<>(); + + public HashedGameObject(final String id, final ObjectData table) { + this.id = id; + this.parentTable = table; + } + + @Override + public void setField(final String field, final String value) { + final StringKey key = new StringKey(field); + List list = this.fields.get(key); + if (list == null) { + list = new ArrayList<>(); + this.fields.put(key, list); + list.add(value); + } + else { + list.set(0, value); + } + } + + public void setField(final String field, final List value) { + final StringKey key = new StringKey(field); + if (value.isEmpty()) { + this.fields.remove(key); + } + else { + this.fields.put(key, value); + } + } + + @Override + public String getField(final String field) { + final String value = ""; + if (this.fields.get(new StringKey(field)) != null) { + final List list = this.fields.get(new StringKey(field)); + final StringBuilder sb = new StringBuilder(); + if (list != null) { + for (final String str : list) { + if (sb.length() != 0) { + sb.append(','); + } + sb.append(str); + } + return sb.toString(); + } + } + return value; + } + + public boolean hasField(final String field) { + return this.fields.containsKey(new StringKey(field)); + } + + @Override + public int getFieldValue(final String field) { + int i = 0; + try { + i = Integer.parseInt(getField(field).trim()); + } + catch (final NumberFormatException e) { + + } + return i; + } + + @Override + public float getFieldFloatValue(final String field) { + float i = 0; + try { + i = Float.parseFloat(getField(field).trim()); + } + catch (final NumberFormatException e) { + + } + return i; + } + + @Override + public float getFieldFloatValue(final String field, final int index) { + float i = 0; + try { + i = Float.parseFloat(getField(field, index).trim()); + } + catch (final NumberFormatException e) { + + } + return i; + } + + @Override + public void setField(final String field, final String value, final int index) { + final StringKey key = new StringKey(field); + List list = this.fields.get(key); + if (list == null) { + if (index == 0) { + list = new ArrayList<>(); + this.fields.put(key, list); + list.add(value); + } + else { + throw new IndexOutOfBoundsException(); + } + } + else { + if (list.size() == index) { + list.add(value); + } + else { + list.set(index, value); + } + } + } + + @Override + public String getField(final String field, final int index) { + String value = ""; + if (this.fields.get(new StringKey(field)) != null) { + final List list = this.fields.get(new StringKey(field)); + if (list != null) { + if (list.size() > index) { + value = list.get(index); + } + else if (list.size() > 0) { + value = list.get(list.size() - 1); + } + } + } + return value; + } + + @Override + public int getFieldValue(final String field, final int index) { + int i = 0; + try { + i = Integer.parseInt(getField(field, index).trim()); + } + catch (final NumberFormatException e) { + + } + return i; + } + + @Override + public List getFieldAsList(final String field, final ObjectData parentTable) { + List fieldAsList; + fieldAsList = new ArrayList<>(); + final String stringList = getField(field); + final String[] listAsArray = stringList.split(","); + if ((listAsArray != null) && (listAsArray.length > 0)) { + for (final String buildingId : listAsArray) { + final GameObject referencedUnit = parentTable.get(buildingId); + if (referencedUnit != null) { + fieldAsList.add(referencedUnit); + } + } + } + return fieldAsList; + } + + @Override + public String toString() { + return getField("Name"); + } + + @Override + public String getId() { + return this.id; + } + + @Override + public String getName() { + String name = getField("Name"); + boolean nameKnown = name.length() >= 1; + if (!nameKnown && !getField("code").equals(this.id) && (getField("code").length() >= 4)) { + final GameObject other = this.parentTable.get(getField("code").substring(0, 4)); + if (other != null) { + name = other.getName(); + nameKnown = true; + } + } + if (!nameKnown && (getField("EditorName").length() > 1)) { + name = getField("EditorName"); + nameKnown = true; + } + if (!nameKnown && (getField("Editorname").length() > 1)) { + name = getField("Editorname"); + nameKnown = true; + } + if (!nameKnown && (getField("BuffTip").length() > 1)) { + name = getField("BuffTip"); + nameKnown = true; + } + if (!nameKnown && (getField("Bufftip").length() > 1)) { + name = getField("Bufftip"); + nameKnown = true; + } + if (nameKnown && name.startsWith("WESTRING")) { + if (!name.contains(" ")) { + name = this.parentTable.getLocalizedString(name); + } + else { + final String[] names = name.split(" "); + name = ""; + for (final String subName : names) { + if (name.length() > 0) { + name += " "; + } + if (subName.startsWith("WESTRING")) { + name += this.parentTable.getLocalizedString(subName); + } + else { + name += subName; + } + } + } + if (name.startsWith("\"") && name.endsWith("\"")) { + name = name.substring(1, name.length() - 1); + } + setField("Name", name); + } + if (!nameKnown) { + name = this.parentTable.getLocalizedString("WESTRING_UNKNOWN") + " '" + getId() + "'"; + } + if (getField("campaign").startsWith("1") && Character.isUpperCase(getId().charAt(0))) { + name = getField("Propernames"); + if (name.contains(",")) { + name = name.split(",")[0]; + } + } + String suf = getField("EditorSuffix"); + if ((suf.length() > 0) && !suf.equals("_")) { + if (suf.startsWith("WESTRING")) { + suf = this.parentTable.getLocalizedString(suf); + } + if (!suf.startsWith(" ")) { + name += " "; + } + name += suf; + } + return name; + } + + public void addToList(final String parentId, final String list) { + String parentField = getField(list); + if (!parentField.contains(parentId)) { + parentField = parentField + "," + parentId; + setField(list, parentField); + } + } + + @Override + public ObjectData getTable() { + return this.parentTable; + } + + @Override + public Set keySet() { + final Set keySet = new HashSet<>(); + for (final StringKey key : this.fields.keySet()) { + keySet.add(key.getString()); + } + return keySet; + } +} diff --git a/core/src/com/etheller/warsmash/units/LMUnit.java b/core/src/com/etheller/warsmash/units/LMUnit.java new file mode 100644 index 0000000..39d29a1 --- /dev/null +++ b/core/src/com/etheller/warsmash/units/LMUnit.java @@ -0,0 +1,12 @@ +package com.etheller.warsmash.units; + +import java.util.LinkedHashMap; + +public class LMUnit extends Element { + + public LMUnit(final String id, final DataTable table) { + super(id, table); + this.fields = new LinkedHashMap<>(); + } + +} diff --git a/core/src/com/etheller/warsmash/units/ObjectData.java b/core/src/com/etheller/warsmash/units/ObjectData.java new file mode 100644 index 0000000..0ff7c61 --- /dev/null +++ b/core/src/com/etheller/warsmash/units/ObjectData.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.units; + +import java.util.Set; + +public interface ObjectData { + GameObject get(String id); + + void setValue(String id, String field, String value); + + Set keySet(); + + String getLocalizedString(String key); +} diff --git a/core/src/com/etheller/warsmash/units/StandardObjectData.java b/core/src/com/etheller/warsmash/units/StandardObjectData.java new file mode 100644 index 0000000..6cd2129 --- /dev/null +++ b/core/src/com/etheller/warsmash/units/StandardObjectData.java @@ -0,0 +1,645 @@ +package com.etheller.warsmash.units; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.util.WorldEditStrings; + +public class StandardObjectData { + private WorldEditStrings worldEditStrings; + private DataSource source; + + public StandardObjectData(final DataSource dataSource) { + this.source = dataSource; + this.worldEditStrings = new WorldEditStrings(dataSource); + } + + public WarcraftData getStandardUnits() { + + final DataTable profile = new DataTable(this.worldEditStrings); + final DataTable unitAbilities = new DataTable(this.worldEditStrings); + final DataTable unitBalance = new DataTable(this.worldEditStrings); + final DataTable unitData = new DataTable(this.worldEditStrings); + final DataTable unitUI = new DataTable(this.worldEditStrings); + final DataTable unitWeapons = new DataTable(this.worldEditStrings); + + try { + profile.readTXT(this.source.getResourceAsStream("Units\\CampaignUnitFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\CampaignUnitStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\HumanUnitFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\HumanUnitStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\NeutralUnitFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\NeutralUnitStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\NightElfUnitFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\NightElfUnitStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\OrcUnitFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\OrcUnitStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\UndeadUnitFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\UndeadUnitStrings.txt"), true); + + unitAbilities.readSLK(this.source.getResourceAsStream("Units\\UnitAbilities.slk")); + + unitBalance.readSLK(this.source.getResourceAsStream("Units\\UnitBalance.slk")); + + unitData.readSLK(this.source.getResourceAsStream("Units\\UnitData.slk")); + + unitUI.readSLK(this.source.getResourceAsStream("Units\\UnitUI.slk")); + + unitWeapons.readSLK(this.source.getResourceAsStream("Units\\UnitWeapons.slk")); + final InputStream unitSkin = this.source.getResourceAsStream("Units\\UnitSkin.txt"); + if (unitSkin != null) { + profile.readTXT(unitSkin, true); + } + } + catch (final IOException e) { + throw new RuntimeException(e); + } + + final WarcraftData units = new WarcraftData(); + + units.add(profile, "Profile", false); + units.add(unitAbilities, "UnitAbilities", true); + units.add(unitBalance, "UnitBalance", true); + units.add(unitData, "UnitData", true); + units.add(unitUI, "UnitUI", true); + units.add(unitWeapons, "UnitWeapons", true); + + return units; + } + + public WarcraftData getStandardItems() { + final DataTable profile = new DataTable(this.worldEditStrings); + final DataTable itemData = new DataTable(this.worldEditStrings); + + try { + profile.readTXT(this.source.getResourceAsStream("Units\\ItemFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\ItemStrings.txt"), true); + itemData.readSLK(this.source.getResourceAsStream("Units\\ItemData.slk")); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + + final WarcraftData units = new WarcraftData(); + + units.add(profile, "Profile", false); + units.add(itemData, "ItemData", true); + + return units; + } + + public WarcraftData getStandardDestructables() { + final DataTable destructableData = new DataTable(this.worldEditStrings); + + try { + destructableData.readSLK(this.source.getResourceAsStream("Units\\DestructableData.slk")); + final InputStream unitSkin = this.source.getResourceAsStream("Units\\DestructableSkin.txt"); + if (unitSkin != null) { + destructableData.readTXT(unitSkin, true); + } + } + catch (final IOException e) { + throw new RuntimeException(e); + } + + final WarcraftData units = new WarcraftData(); + + units.add(destructableData, "DestructableData", true); + + return units; + } + + public WarcraftData getStandardDoodads() { + + final DataTable destructableData = new DataTable(this.worldEditStrings); + + try { + destructableData.readSLK(this.source.getResourceAsStream("Doodads\\Doodads.slk")); + final InputStream unitSkin = this.source.getResourceAsStream("Doodads\\DoodadSkins.txt"); + if (unitSkin != null) { + destructableData.readTXT(unitSkin, true); + } + } + catch (final IOException e) { + throw new RuntimeException(e); + } + + final WarcraftData units = new WarcraftData(); + + units.add(destructableData, "DoodadData", true); + + return units; + } + + public DataTable getStandardUnitMeta() { + final DataTable unitMetaData = new DataTable(this.worldEditStrings); + try { + unitMetaData.readSLK(this.source.getResourceAsStream("Units\\UnitMetaData.slk")); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + return unitMetaData; + } + + public DataTable getStandardDestructableMeta() { + final DataTable unitMetaData = new DataTable(this.worldEditStrings); + try { + unitMetaData.readSLK(this.source.getResourceAsStream("Units\\DestructableMetaData.slk")); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + return unitMetaData; + } + + public DataTable getStandardDoodadMeta() { + final DataTable unitMetaData = new DataTable(this.worldEditStrings); + try { + unitMetaData.readSLK(this.source.getResourceAsStream("Doodads\\DoodadMetaData.slk")); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + return unitMetaData; + } + + public WarcraftData getStandardAbilities() { + + final DataTable profile = new DataTable(this.worldEditStrings); + final DataTable abilityData = new DataTable(this.worldEditStrings); + + try { + profile.readTXT(this.source.getResourceAsStream("Units\\CampaignAbilityFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\CampaignAbilityStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\CommonAbilityFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\CommonAbilityStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\HumanAbilityFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\HumanAbilityStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\NeutralAbilityFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\NeutralAbilityStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\NightElfAbilityFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\NightElfAbilityStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\OrcAbilityFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\OrcAbilityStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\UndeadAbilityFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\UndeadAbilityStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\ItemAbilityFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\ItemAbilityStrings.txt"), true); + + final InputStream unitSkin = this.source.getResourceAsStream("Units\\AbilitySkin.txt"); + if (unitSkin != null) { + profile.readTXT(unitSkin, true); + } + + abilityData.readSLK(this.source.getResourceAsStream("Units\\AbilityData.slk")); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + + final WarcraftData abilities = new WarcraftData(); + + abilities.add(profile, "Profile", false); + abilities.add(abilityData, "AbilityData", true); + + return abilities; + } + + public WarcraftData getStandardAbilityBuffs() { + final DataTable profile = new DataTable(this.worldEditStrings); + final DataTable abilityData = new DataTable(this.worldEditStrings); + + try { + profile.readTXT(this.source.getResourceAsStream("Units\\CampaignAbilityFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\CampaignAbilityStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\CommonAbilityFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\CommonAbilityStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\HumanAbilityFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\HumanAbilityStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\NeutralAbilityFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\NeutralAbilityStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\NightElfAbilityFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\NightElfAbilityStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\OrcAbilityFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\OrcAbilityStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\UndeadAbilityFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\UndeadAbilityStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\ItemAbilityFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\ItemAbilityStrings.txt"), true); + + abilityData.readSLK(this.source.getResourceAsStream("Units\\AbilityBuffData.slk")); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + + final WarcraftData abilities = new WarcraftData(); + + abilities.add(profile, "Profile", false); + abilities.add(abilityData, "AbilityData", true); + + return abilities; + } + + public WarcraftData getStandardUpgrades() { + final DataTable profile = new DataTable(this.worldEditStrings); + final DataTable upgradeData = new DataTable(this.worldEditStrings); + + try { + profile.readTXT(this.source.getResourceAsStream("Units\\CampaignUpgradeFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\CampaignUpgradeStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\HumanUpgradeFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\HumanUpgradeStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\NeutralUpgradeFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\NeutralUpgradeStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\NightElfUpgradeFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\NightElfUpgradeStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\OrcUpgradeFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\OrcUpgradeStrings.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\UndeadUpgradeFunc.txt"), true); + profile.readTXT(this.source.getResourceAsStream("Units\\UndeadUpgradeStrings.txt"), true); + + upgradeData.readSLK(this.source.getResourceAsStream("Units\\UpgradeData.slk")); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + + final WarcraftData units = new WarcraftData(); + + units.add(profile, "Profile", false); + units.add(upgradeData, "UpgradeData", true); + + return units; + } + + public DataTable getStandardUpgradeMeta() { + final DataTable unitMetaData = new DataTable(this.worldEditStrings); + try { + unitMetaData.readSLK(this.source.getResourceAsStream("Units\\UpgradeMetaData.slk")); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + return unitMetaData; + } + + public DataTable getStandardUpgradeEffectMeta() { + final DataTable unitMetaData = new DataTable(this.worldEditStrings); + try { + unitMetaData.readSLK(this.source.getResourceAsStream("Units\\UpgradeEffectMetaData.slk")); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + return unitMetaData; + } + + public DataTable getStandardAbilityMeta() { + final DataTable unitMetaData = new DataTable(this.worldEditStrings); + try { + unitMetaData.readSLK(this.source.getResourceAsStream("Units\\AbilityMetaData.slk")); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + return unitMetaData; + } + + public DataTable getStandardAbilityBuffMeta() { + final DataTable unitMetaData = new DataTable(this.worldEditStrings); + try { + unitMetaData.readSLK(this.source.getResourceAsStream("Units\\AbilityBuffMetaData.slk")); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + return unitMetaData; + } + + public DataTable getUnitEditorData() { + final DataTable unitMetaData = new DataTable(this.worldEditStrings); + try { + unitMetaData.readTXT(this.source.getResourceAsStream("UI\\UnitEditorData.txt"), true); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + return unitMetaData; + } + + public DataTable getWorldEditData() { + final DataTable unitMetaData = new DataTable(this.worldEditStrings); + try { + unitMetaData.readTXT(this.source.getResourceAsStream("UI\\WorldEditData.txt"), true); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + return unitMetaData; + } + + public WorldEditStrings getWorldEditStrings() { + return this.worldEditStrings; + } + + public static class WarcraftData implements ObjectData { + WorldEditStrings worldEditStrings; + List tables = new ArrayList<>(); + Map tableMap = new HashMap<>(); + Map units = new HashMap<>(); + + public WarcraftData(final WorldEditStrings worldEditStrings) { + this.worldEditStrings = worldEditStrings; + } + + @Override + public String getLocalizedString(final String key) { + return this.worldEditStrings.getString(key); + } + + public void add(final DataTable data, final String name, final boolean canMake) { + this.tableMap.put(new StringKey(name), data); + this.tables.add(data); + if (canMake) { + for (final String id : data.keySet()) { + if (!this.units.containsKey(new StringKey(id))) { + this.units.put(new StringKey(id), new WarcraftObject(data.get(id).getId(), this)); + } + } + } + } + + public WarcraftData() { + } + + public List getTables() { + return this.tables; + } + + public void setTables(final List tables) { + this.tables = tables; + } + + public DataTable getTable(final String tableName) { + return this.tableMap.get(new StringKey(tableName)); + } + + @Override + public GameObject get(final String id) { + return this.units.get(new StringKey(id)); + } + + @Override + public void setValue(final String id, final String field, final String value) { + get(id).setField(field, value); + } + + @Override + public Set keySet() { + final Set keySet = new HashSet<>(); + for (final StringKey key : this.units.keySet()) { + keySet.add(key.getString()); + } + return keySet; + } + + public void cloneUnit(final String parentId, final String cloneId) { + for (final DataTable table : this.tables) { + final Element parentEntry = table.get(parentId); + final LMUnit cloneUnit = new LMUnit(cloneId, table); + for (final String key : parentEntry.keySet()) { + cloneUnit.setField(key, parentEntry.getField(key)); + } + table.put(cloneId, cloneUnit); + } + this.units.put(new StringKey(cloneId), new WarcraftObject(cloneId, this)); + } + } + + public static class WarcraftObject implements GameObject { + String id; + WarcraftData dataSource; + + public WarcraftObject(final String id, final WarcraftData dataSource) { + this.id = id; + this.dataSource = dataSource; + } + + @Override + public void setField(final String field, final String value, final int index) { + for (final DataTable table : this.dataSource.getTables()) { + final Element element = table.get(this.id); + if ((element != null) && element.hasField(field)) { + element.setField(field, value, index); + return; + } + } + } + + @Override + public String getField(final String field, final int index) { + for (final DataTable table : this.dataSource.getTables()) { + final Element element = table.get(this.id); + if ((element != null) && element.hasField(field)) { + return element.getField(field, index); + } + } + return ""; + } + + @Override + public int getFieldValue(final String field, final int index) { + for (final DataTable table : this.dataSource.getTables()) { + final Element element = table.get(this.id); + if ((element != null) && element.hasField(field)) { + return element.getFieldValue(field, index); + } + } + return 0; + } + + @Override + public void setField(final String field, final String value) { + for (final DataTable table : this.dataSource.getTables()) { + final Element element = table.get(this.id); + if ((element != null) && element.hasField(field)) { + element.setField(field, value); + return; + } + } + throw new IllegalArgumentException("no field"); + } + + @Override + public String getField(final String field) { + for (final DataTable table : this.dataSource.getTables()) { + final Element element = table.get(this.id); + if ((element != null) && element.hasField(field)) { + return element.getField(field); + } + } + return ""; + } + + @Override + public int getFieldValue(final String field) { + for (final DataTable table : this.dataSource.getTables()) { + final Element element = table.get(this.id); + if ((element != null) && element.hasField(field)) { + return element.getFieldValue(field); + } + } + return 0; + } + + @Override + public float getFieldFloatValue(final String field) { + for (final DataTable table : this.dataSource.getTables()) { + final Element element = table.get(this.id); + if ((element != null) && element.hasField(field)) { + return element.getFieldFloatValue(field); + } + } + return 0f; + } + + @Override + public float getFieldFloatValue(final String field, final int index) { + for (final DataTable table : this.dataSource.getTables()) { + final Element element = table.get(this.id); + if ((element != null) && element.hasField(field)) { + return element.getFieldFloatValue(field, index); + } + } + return 0f; + } + + /* + * (non-Javadoc) I'm not entirely sure this is still safe to use + * + * @see com.hiveworkshop.wc3.units.GameObject#getFieldAsList(java.lang. String) + */ + @Override + public List getFieldAsList(final String field, final ObjectData objectData) { + for (final DataTable table : this.dataSource.getTables()) { + final Element element = table.get(this.id); + if ((element != null) && element.hasField(field)) { + return element.getFieldAsList(field, objectData); + } + } + return new ArrayList<>();// empty list if not found + } + + @Override + public String getId() { + return this.id; + } + + @Override + public ObjectData getTable() { + return this.dataSource; + } + + // @Override + // public String getName() { + // return dataSource.profile.get(id).getName(); + // } + @Override + public String getName() { + String name = getField("Name"); + boolean nameKnown = name.length() >= 1; + if (!nameKnown && !getField("code").equals(this.id) && (getField("code").length() >= 4)) { + final WarcraftObject other = (WarcraftObject) this.dataSource.get(getField("code").substring(0, 4)); + if (other != null) { + name = other.getName(); + nameKnown = true; + } + } + if (!nameKnown && (getField("EditorName").length() > 1)) { + name = getField("EditorName"); + nameKnown = true; + } + if (!nameKnown && (getField("Editorname").length() > 1)) { + name = getField("Editorname"); + nameKnown = true; + } + if (!nameKnown && (getField("BuffTip").length() > 1)) { + name = getField("BuffTip"); + nameKnown = true; + } + if (!nameKnown && (getField("Bufftip").length() > 1)) { + name = getField("Bufftip"); + nameKnown = true; + } + if (nameKnown && name.startsWith("WESTRING")) { + if (!name.contains(" ")) { + name = this.dataSource.getLocalizedString(name); + } + else { + final String[] names = name.split(" "); + name = ""; + for (final String subName : names) { + if (name.length() > 0) { + name += " "; + } + if (subName.startsWith("WESTRING")) { + name += this.dataSource.getLocalizedString(subName); + } + else { + name += subName; + } + } + } + if (name.startsWith("\"") && name.endsWith("\"")) { + name = name.substring(1, name.length() - 1); + } + setField("Name", name); + } + if (!nameKnown) { + name = this.dataSource.getLocalizedString("WESTRING_UNKNOWN") + " '" + getId() + "'"; + } + if (getField("campaign").startsWith("1") && Character.isUpperCase(getId().charAt(0))) { + name = getField("Propernames"); + if (name.contains(",")) { + name = name.split(",")[0]; + } + } + String suf = getField("EditorSuffix"); + if ((suf.length() > 0) && !suf.equals("_")) { + if (suf.startsWith("WESTRING")) { + suf = this.dataSource.getLocalizedString(suf); + } + if (!suf.startsWith(" ")) { + name += " "; + } + name += suf; + } + return name; + } + + BufferedImage storedImage = null; + String storedImagePath = null; + + @Override + public Set keySet() { + final Set keySet = new HashSet<>(); + for (final DataTable table : this.dataSource.tables) { + keySet.addAll(table.get(this.id).keySet()); + } + return keySet; + } + } + + private StandardObjectData() { + } +} diff --git a/core/src/com/etheller/warsmash/units/StringKey.java b/core/src/com/etheller/warsmash/units/StringKey.java new file mode 100644 index 0000000..c78e16e --- /dev/null +++ b/core/src/com/etheller/warsmash/units/StringKey.java @@ -0,0 +1,54 @@ +package com.etheller.warsmash.units; + +/** + * A hashable wrapper object for a String that can be used as the key in a + * hashtable, but which disregards case as a key -- except that it will remember + * case if directly asked for its value. The game needs this to be able to show + * the original case of a string to the user in the editor, while still doing + * map lookups in a case insensitive way. + * + * @author Eric + * + */ +public final class StringKey { + private final String string; + + public StringKey(final String string) { + this.string = string; + } + + public String getString() { + return this.string; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + ((this.string.toLowerCase() == null) ? 0 : this.string.toLowerCase().hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final StringKey other = (StringKey) obj; + if (this.string == null) { + if (other.string != null) { + return false; + } + } + else if (!this.string.equalsIgnoreCase(other.string)) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/units/custom/Change.java b/core/src/com/etheller/warsmash/units/custom/Change.java new file mode 100644 index 0000000..e185128 --- /dev/null +++ b/core/src/com/etheller/warsmash/units/custom/Change.java @@ -0,0 +1,105 @@ +package com.etheller.warsmash.units.custom; + +import com.etheller.warsmash.util.War3ID; + +public final class Change { + private War3ID id; + private int vartype, level, dataptr; + private int longval; + private float realval; + private String strval; + + private boolean boolval; + private War3ID junkDNA; + + public War3ID getId() { + return this.id; + } + + public void setId(final War3ID id) { + this.id = id; + } + + public int getVartype() { + return this.vartype; + } + + public void setVartype(final int vartype) { + this.vartype = vartype; + } + + public int getLevel() { + return this.level; + } + + public void setLevel(final int level) { + this.level = level; + } + + public int getDataptr() { + return this.dataptr; + } + + public void setDataptr(final int dataptr) { + this.dataptr = dataptr; + } + + public int getLongval() { + return this.longval; + } + + public void setLongval(final int longval) { + this.longval = longval; + } + + public float getRealval() { + return this.realval; + } + + public void setRealval(final float realval) { + this.realval = realval; + } + + public String getStrval() { + return this.strval; + } + + public void setStrval(final String strval) { + this.strval = strval; + } + + public boolean isBoolval() { + return this.boolval; + } + + public void setBoolval(final boolean boolval) { + this.boolval = boolval; + } + + public void setJunkDNA(final War3ID junkDNA) { + this.junkDNA = junkDNA; + } + + public War3ID getJunkDNA() { + return this.junkDNA; + } + + public void copyFrom(final Change other) { + this.id = other.id; + this.level = other.level; + this.dataptr = other.dataptr; + this.vartype = other.vartype; + this.longval = other.longval; + this.realval = other.realval; + this.strval = other.strval; + this.boolval = other.boolval; + this.junkDNA = other.junkDNA; + } + + @Override + public Change clone() { + final Change copy = new Change(); + copy.copyFrom(this); + return copy; + } +} diff --git a/core/src/com/etheller/warsmash/units/custom/ChangeMap.java b/core/src/com/etheller/warsmash/units/custom/ChangeMap.java new file mode 100644 index 0000000..5442421 --- /dev/null +++ b/core/src/com/etheller/warsmash/units/custom/ChangeMap.java @@ -0,0 +1,55 @@ +package com.etheller.warsmash.units.custom; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.etheller.warsmash.util.War3ID; + +public final class ChangeMap implements Iterable>> { + private final Map> idToChanges = new LinkedHashMap<>(); + + public void add(final War3ID war3Id, final Change change) { + List list = this.idToChanges.get(war3Id); + if (list == null) { + list = new ArrayList<>(); + this.idToChanges.put(war3Id, list); + } + list.add(change); + } + + public void add(final War3ID war3Id, final List changes) { + for (final Change change : changes) { + add(war3Id, change); + } + } + + public List get(final War3ID war3ID) { + return this.idToChanges.get(war3ID); + } + + public void delete(final War3ID war3ID, final Change change) { + if (this.idToChanges.containsKey(war3ID)) { + final List changeList = this.idToChanges.get(war3ID); + changeList.remove(change); + if (changeList.isEmpty()) { + this.idToChanges.remove(war3ID); + } + } + } + + @Override + public Iterator>> iterator() { + return this.idToChanges.entrySet().iterator(); + } + + public int size() { + return this.idToChanges.size(); + } + + public void clear() { + this.idToChanges.clear(); + } +} diff --git a/core/src/com/etheller/warsmash/units/custom/ObjectDataChangeEntry.java b/core/src/com/etheller/warsmash/units/custom/ObjectDataChangeEntry.java new file mode 100644 index 0000000..1acda99 --- /dev/null +++ b/core/src/com/etheller/warsmash/units/custom/ObjectDataChangeEntry.java @@ -0,0 +1,47 @@ +package com.etheller.warsmash.units.custom; + +import java.util.List; +import java.util.Map; + +import com.etheller.warsmash.util.War3ID; + +public final class ObjectDataChangeEntry { + private War3ID oldId; + private War3ID newId; + private final ChangeMap changes; + + public ObjectDataChangeEntry(final War3ID oldId, final War3ID newId) { + this.oldId = oldId; + this.newId = newId; + this.changes = new ChangeMap(); + } + + @Override + public ObjectDataChangeEntry clone() { + final ObjectDataChangeEntry objectDataChangeEntry = new ObjectDataChangeEntry(this.oldId, this.newId); + for (final Map.Entry> entry : this.changes) { + objectDataChangeEntry.getChanges().add(entry.getKey(), entry.getValue()); + } + return objectDataChangeEntry; + } + + public ChangeMap getChanges() { + return this.changes; + } + + public War3ID getOldId() { + return this.oldId; + } + + public void setOldId(final War3ID oldId) { + this.oldId = oldId; + } + + public War3ID getNewId() { + return this.newId; + } + + public void setNewId(final War3ID newId) { + this.newId = newId; + } +} diff --git a/core/src/com/etheller/warsmash/units/custom/ObjectMap.java b/core/src/com/etheller/warsmash/units/custom/ObjectMap.java new file mode 100644 index 0000000..bbece53 --- /dev/null +++ b/core/src/com/etheller/warsmash/units/custom/ObjectMap.java @@ -0,0 +1,89 @@ +package com.etheller.warsmash.units.custom; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; + +import com.etheller.warsmash.util.War3ID; + +public final class ObjectMap implements Iterable> { + private final Map idToDataChangeEntry; + private final Set lowerCaseKeySet; + + public ObjectMap() { + this.idToDataChangeEntry = new LinkedHashMap<>(); + this.lowerCaseKeySet = new HashSet<>(); + } + + public void clear() { + this.idToDataChangeEntry.clear(); + this.lowerCaseKeySet.clear(); + } + + public ObjectDataChangeEntry remove(final War3ID key) { + this.lowerCaseKeySet.remove(War3ID.fromString(key.toString().toLowerCase())); + return this.idToDataChangeEntry.remove(key); + } + + public Set keySet() { + return this.idToDataChangeEntry.keySet(); + } + + public ObjectDataChangeEntry put(final War3ID key, final ObjectDataChangeEntry value) { + this.lowerCaseKeySet.add(War3ID.fromString(key.toString().toLowerCase())); + return this.idToDataChangeEntry.put(key, value); + } + + public Set> entrySet() { + return this.idToDataChangeEntry.entrySet(); + } + + public ObjectDataChangeEntry get(final War3ID key) { + return this.idToDataChangeEntry.get(key); + } + + public boolean containsKey(final War3ID key) { + return this.idToDataChangeEntry.containsKey(key); + } + + public boolean containsKeyCaseInsensitive(final War3ID key) { + return this.lowerCaseKeySet.contains(War3ID.fromString(key.toString().toLowerCase())); + } + + public boolean containsValue(final ObjectDataChangeEntry value) { + return this.idToDataChangeEntry.containsValue(value); + } + + public Collection values() { + return this.idToDataChangeEntry.values(); + } + + public int size() { + return this.idToDataChangeEntry.size(); + } + + public void forEach(final BiConsumer forEach) { + this.idToDataChangeEntry.forEach(forEach); + } + + @Override + public Iterator> iterator() { + return this.idToDataChangeEntry.entrySet().iterator(); + } + + @Override + public ObjectMap clone() { + final ObjectMap clone = new ObjectMap(); + forEach(new BiConsumer() { + @Override + public void accept(final War3ID key, final ObjectDataChangeEntry value) { + clone.put(key, value); + } + }); + return clone; + } +} diff --git a/core/src/com/etheller/warsmash/units/custom/WTS.java b/core/src/com/etheller/warsmash/units/custom/WTS.java new file mode 100644 index 0000000..52cc2c0 --- /dev/null +++ b/core/src/com/etheller/warsmash/units/custom/WTS.java @@ -0,0 +1,12 @@ +package com.etheller.warsmash.units.custom; + +public interface WTS { + String get(int key); + + WTS DO_NOTHING = new WTS() { + @Override + public String get(final int key) { + return "TRIGSTR_" + key; + } + }; +} diff --git a/core/src/com/etheller/warsmash/units/custom/WTSFile.java b/core/src/com/etheller/warsmash/units/custom/WTSFile.java new file mode 100644 index 0000000..99b7ab3 --- /dev/null +++ b/core/src/com/etheller/warsmash/units/custom/WTSFile.java @@ -0,0 +1,94 @@ +package com.etheller.warsmash.units.custom; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Hashtable; +import java.util.Map; + +/** + * + * @author Deaod + * + */ +public class WTSFile implements WTS { + private final InputStream source; + private final Map trigStrings = new Hashtable<>(); + + private static enum ParseState { + NEXT_TRIGSTR, + START_OF_DATA, + END_OF_DATA; + } + + private void parse() throws IOException { + final BufferedReader sourceReader = new BufferedReader( + new InputStreamReader(this.source, Charset.forName("utf-8"))); + ParseState state = ParseState.NEXT_TRIGSTR; + + // WTS files may start with a Byte Order Mark, which we will have to skip. + sourceReader.mark(4); + if (sourceReader.read() != 0xFEFF) { + // first character not a BOM, unread the character. + sourceReader.reset(); + } + + String currentLine = sourceReader.readLine(); + int id = 0; + StringBuffer data = new StringBuffer(); + + while (currentLine != null) { + switch (state) { + case NEXT_TRIGSTR: + if (currentLine.startsWith("STRING ")) { + id = Integer.parseInt(currentLine.substring(7)); + state = ParseState.START_OF_DATA; + } + break; + + case START_OF_DATA: + if (currentLine.startsWith("{")) { + state = ParseState.END_OF_DATA; + } + break; + + case END_OF_DATA: + if (currentLine.startsWith("}")) { + this.trigStrings.put(id, data.toString()); + data = new StringBuffer(); + state = ParseState.NEXT_TRIGSTR; + } + else { + data.append(currentLine); + } + break; + } + currentLine = sourceReader.readLine(); + } + sourceReader.close(); + } + + public WTSFile(final InputStream inputStream) throws IOException { + this.source = inputStream; + parse(); + } + + public WTSFile(final Path source) throws IOException { + this(Files.newInputStream(source)); + } + + public WTSFile(final String sourcePath) throws IOException { + this(Paths.get(sourcePath)); + } + + @Override + public String get(final int index) { + return this.trigStrings.get(index); + } + +} diff --git a/core/src/com/etheller/warsmash/units/custom/War3ObjectDataChangeset.java b/core/src/com/etheller/warsmash/units/custom/War3ObjectDataChangeset.java new file mode 100644 index 0000000..1f215dd --- /dev/null +++ b/core/src/com/etheller/warsmash/units/custom/War3ObjectDataChangeset.java @@ -0,0 +1,802 @@ +package com.etheller.warsmash.units.custom; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CodingErrorAction; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +/** + * Inspired by PitzerMike's obj.h, without a lot of immediate focus on Java + * conventions. I will probably get it converted over to Java conventions once I + * have a working replica of his obj.h code. + * + * @author Eric + * + */ +public final class War3ObjectDataChangeset { + public static final int VAR_TYPE_INT = 0; + public static final int VAR_TYPE_REAL = 1; + public static final int VAR_TYPE_UNREAL = 2; + public static final int VAR_TYPE_STRING = 3; + public static final int VAR_TYPE_BOOLEAN = 4; + public static final int MAX_STR_LEN = 1024; + private static final Set UNIT_ID_SET; + private static final Set ABILITY_ID_SET; + static { + final HashSet unitHashSet = new HashSet<>(); + unitHashSet.add(War3ID.fromString("ubpx")); + unitHashSet.add(War3ID.fromString("ubpy")); + unitHashSet.add(War3ID.fromString("ides")); + unitHashSet.add(War3ID.fromString("uhot")); + unitHashSet.add(War3ID.fromString("unam")); + unitHashSet.add(War3ID.fromString("ureq")); + unitHashSet.add(War3ID.fromString("urqa")); + unitHashSet.add(War3ID.fromString("utip")); + unitHashSet.add(War3ID.fromString("utub")); + UNIT_ID_SET = unitHashSet; + final HashSet abilHashSet = new HashSet<>(); + abilHashSet.add(War3ID.fromString("irc2")); + abilHashSet.add(War3ID.fromString("irc3")); + abilHashSet.add(War3ID.fromString("bsk1")); + abilHashSet.add(War3ID.fromString("bsk2")); + abilHashSet.add(War3ID.fromString("bsk3")); + abilHashSet.add(War3ID.fromString("coau")); + abilHashSet.add(War3ID.fromString("coa1")); + abilHashSet.add(War3ID.fromString("coa2")); + abilHashSet.add(War3ID.fromString("cyc1")); + abilHashSet.add(War3ID.fromString("dcp1")); + abilHashSet.add(War3ID.fromString("dcp2")); + abilHashSet.add(War3ID.fromString("dvm1")); + abilHashSet.add(War3ID.fromString("dvm2")); + abilHashSet.add(War3ID.fromString("dvm3")); + abilHashSet.add(War3ID.fromString("dvm4")); + abilHashSet.add(War3ID.fromString("dvm5")); + abilHashSet.add(War3ID.fromString("exh1")); + abilHashSet.add(War3ID.fromString("exhu")); + abilHashSet.add(War3ID.fromString("fak1")); + abilHashSet.add(War3ID.fromString("fak2")); + abilHashSet.add(War3ID.fromString("fak3")); + abilHashSet.add(War3ID.fromString("hwdu")); + abilHashSet.add(War3ID.fromString("inv1")); + abilHashSet.add(War3ID.fromString("inv2")); + abilHashSet.add(War3ID.fromString("inv3")); + abilHashSet.add(War3ID.fromString("inv4")); + abilHashSet.add(War3ID.fromString("inv5")); + abilHashSet.add(War3ID.fromString("liq1")); + abilHashSet.add(War3ID.fromString("liq2")); + abilHashSet.add(War3ID.fromString("liq3")); + abilHashSet.add(War3ID.fromString("liq4")); + abilHashSet.add(War3ID.fromString("mim1")); + abilHashSet.add(War3ID.fromString("mfl1")); + abilHashSet.add(War3ID.fromString("mfl2")); + abilHashSet.add(War3ID.fromString("mfl3")); + abilHashSet.add(War3ID.fromString("mfl4")); + abilHashSet.add(War3ID.fromString("mfl5")); + abilHashSet.add(War3ID.fromString("tpi1")); + abilHashSet.add(War3ID.fromString("tpi2")); + abilHashSet.add(War3ID.fromString("spl1")); + abilHashSet.add(War3ID.fromString("spl2")); + abilHashSet.add(War3ID.fromString("irl1")); + abilHashSet.add(War3ID.fromString("irl2")); + abilHashSet.add(War3ID.fromString("irl3")); + abilHashSet.add(War3ID.fromString("irl4")); + abilHashSet.add(War3ID.fromString("irl5")); + abilHashSet.add(War3ID.fromString("idc1")); + abilHashSet.add(War3ID.fromString("idc2")); + abilHashSet.add(War3ID.fromString("idc3")); + abilHashSet.add(War3ID.fromString("imo1")); + abilHashSet.add(War3ID.fromString("imo2")); + abilHashSet.add(War3ID.fromString("imo3")); + abilHashSet.add(War3ID.fromString("imou")); + abilHashSet.add(War3ID.fromString("ict1")); + abilHashSet.add(War3ID.fromString("ict2")); + abilHashSet.add(War3ID.fromString("isr1")); + abilHashSet.add(War3ID.fromString("isr2")); + abilHashSet.add(War3ID.fromString("ipv1")); + abilHashSet.add(War3ID.fromString("ipv2")); + abilHashSet.add(War3ID.fromString("ipv3")); + abilHashSet.add(War3ID.fromString("mec1")); + abilHashSet.add(War3ID.fromString("spb1")); + abilHashSet.add(War3ID.fromString("spb2")); + abilHashSet.add(War3ID.fromString("spb3")); + abilHashSet.add(War3ID.fromString("spb4")); + abilHashSet.add(War3ID.fromString("spb5")); + abilHashSet.add(War3ID.fromString("gra1")); + abilHashSet.add(War3ID.fromString("gra2")); + abilHashSet.add(War3ID.fromString("gra3")); + abilHashSet.add(War3ID.fromString("gra4")); + abilHashSet.add(War3ID.fromString("gra5")); + abilHashSet.add(War3ID.fromString("ipmu")); + abilHashSet.add(War3ID.fromString("flk1")); + abilHashSet.add(War3ID.fromString("flk2")); + abilHashSet.add(War3ID.fromString("flk3")); + abilHashSet.add(War3ID.fromString("flk4")); + abilHashSet.add(War3ID.fromString("flk5")); + abilHashSet.add(War3ID.fromString("fbk1")); + abilHashSet.add(War3ID.fromString("fbk2")); + abilHashSet.add(War3ID.fromString("fbk3")); + abilHashSet.add(War3ID.fromString("fbk4")); + abilHashSet.add(War3ID.fromString("nca1")); + abilHashSet.add(War3ID.fromString("pxf1")); + abilHashSet.add(War3ID.fromString("pxf2")); + abilHashSet.add(War3ID.fromString("mls1")); + abilHashSet.add(War3ID.fromString("sla1")); + abilHashSet.add(War3ID.fromString("sla2")); + ABILITY_ID_SET = abilHashSet; + } + + private int version; + private ObjectMap original = new ObjectMap(); + private final ObjectMap custom = new ObjectMap(); + private char expected; + private War3ID lastused; + + public char kind; + public boolean detected; + + public War3ID nameField; + + public War3ObjectDataChangeset() { + this.version = 2; + this.kind = 'u'; + this.expected = 'u'; + this.detected = false; + this.lastused = War3ID.fromString("u~~~"); + } + + public War3ObjectDataChangeset(final char expectedkind) { + this.version = 2; + this.kind = 'u'; + this.expected = expectedkind; + this.detected = false; + this.lastused = War3ID.fromString("u~~~"); + } + + public boolean detectKind(final War3ID chid) { + if (UNIT_ID_SET.contains(chid)) { + this.kind = 'u'; + return false; + } + else if (ABILITY_ID_SET.contains(chid)) { + this.kind = 'a'; + } + else { + switch (chid.asStringValue().charAt(0)) { + case 'f': + this.kind = 'h'; + break; + case 'i': + this.kind = 't'; + break; + case 'g': + this.kind = 'q'; + break; + case 'a': + case 'u': + case 'b': + case 'd': + this.kind = chid.asStringValue().charAt(0); + break; + default: + this.kind = 'a'; + } + } + return true; + } + + public char getExpectedKind() { + return this.expected; + } + + public War3ID getNameField() { + final War3ID field = War3ID.fromString("unam"); + char cmp = this.kind; + if (!this.detected) { + cmp = this.expected; + } + switch (cmp) { + case 'h': + this.nameField = field.set(0, 'f'); + break; + case 't': + this.nameField = field.set(0, 'u'); + break; + case 'q': + this.nameField = field.set(0, 'g'); + break; + default: + this.nameField = field.set(0, cmp); + break; + } + return this.nameField; + } + + public boolean extended() { + char cmp = this.kind; + if (!this.detected) { + cmp = this.expected; + } + switch (cmp) { + case 'u': + case 'h': + case 'b': + case 't': + return false; + } + return true; + } + + public void renameids(final ObjectMap map, final boolean isOriginal) { + final War3ID nameId = getNameField(); + final List idsToRemoveFromMap = new ArrayList<>(); + final Map idsToObjectsForAddingToMap = new HashMap<>(); + for (final Iterator> iterator = map.iterator(); iterator.hasNext();) { + final Map.Entry entry = iterator.next(); + final ObjectDataChangeEntry current = entry.getValue(); + final List nameEntry = current.getChanges().get(nameId); + if ((nameEntry != null) && !nameEntry.isEmpty()) { + final Change firstNameChange = nameEntry.get(0); + int pos = firstNameChange.getStrval().lastIndexOf("::"); + if ((pos != -1) && (firstNameChange.getStrval().length() > (pos + 2))) { + String rest = firstNameChange.getStrval().substring(pos + 2); + if (rest.length() == 4) { + final War3ID newId = War3ID.fromString(rest); + final ObjectDataChangeEntry existingObjectWithMatchingId = map.get(newId); + if (isOriginal) {// obj.cpp: update id and name + current.setOldId(newId); + } + else { + current.setNewId(newId); + } + firstNameChange.setStrval(firstNameChange.getStrval().substring(0, pos)); + if (existingObjectWithMatchingId != null) { + // obj.cpp: carry over all changes + final Iterator>> changeIterator = current.getChanges() + .iterator(); + while (changeIterator.hasNext()) { + final Map.Entry> changeIteratorNext = changeIterator.next(); + final War3ID copiedChangeId = changeIteratorNext.getKey(); + List changeListForFieldToOverwrite = existingObjectWithMatchingId.getChanges() + .get(copiedChangeId); + if (changeListForFieldToOverwrite == null) { + changeListForFieldToOverwrite = new ArrayList<>(); + } + for (final Change changeToCopy : changeIteratorNext.getValue()) { + final Iterator replaceIterator = changeListForFieldToOverwrite.iterator(); + boolean didOverwrite = false; + while (replaceIterator.hasNext()) { + final Change changeToOverwrite = replaceIterator.next(); + if (changeToOverwrite.getLevel() != changeToCopy.getLevel()) { + // obj.cpp: we can only replace + // changes with the same + // level/variation + continue; + } + if (copiedChangeId.equals(nameId)) { + // obj.cpp: carry over further + // references + pos = changeToOverwrite.getStrval().lastIndexOf("::"); + if ((pos != -1) && (changeToOverwrite.getStrval().length() > (pos + 2))) { + rest = changeToOverwrite.getStrval().substring(pos + 2); + if ((rest.length() == 4) || "REMOVE".equals(rest)) { + changeToCopy.setStrval(changeToCopy.getStrval() + "::" + rest); + // so if this is a peasant, whose name was "Peasant::hfoo" + // and when we copied his data onto the footman, we found + // that the footman was named "Footman::hkni", then at that + // point we set the peasant's name to be "Peasant::hkni" + // because we are about to copy it onto the footman. + // And, we already set it to just "Peasant", so + // appending the "::" and the 'rest' variable is enough. + // Then, on a further loop iteration, in theory + // we will copoy the footman who is named Peasant + // onto the knight. + // + // TODO but what if we already copied the footman onto the knight? + // did PitzerMike consider this in obj.cpp? + } + } + } + changeToOverwrite.copyFrom(changeToCopy); + didOverwrite = true; + break; + } + if (!didOverwrite) { + changeListForFieldToOverwrite.add(changeToCopy); + if (changeListForFieldToOverwrite.size() == 1) { + existingObjectWithMatchingId.getChanges().add(copiedChangeId, + changeListForFieldToOverwrite); + } + } + } + } + } + else { // obj.cpp: an object with that id didn't exist + idsToRemoveFromMap.add(entry.getKey()); + idsToObjectsForAddingToMap.put(newId, current.clone()); + } + } + else if ("REMOVE".equals(rest)) { // obj.cpp: want to remove the object + idsToRemoveFromMap.add(entry.getKey()); + } // obj.cpp: in all other cases keep it untouched + } + } + + } + for (final War3ID id : idsToRemoveFromMap) { + map.remove(id); + } + for (final Map.Entry entry : idsToObjectsForAddingToMap.entrySet()) { + map.put(entry.getKey(), entry.getValue()); + } + } + + public void renameIds() { + renameids(this.original, true); + renameids(this.custom, false); + } + + // ' ' - '/' + // ':' - '@' + // '[' - '`' + // '{' - '~' + public char nextchar(final char cur) { + switch (cur) { + case '&': // skip ' because often jass parsers don't handle escaped rawcodes like '\'' + return '('; + case '/': // skip digits + return ':'; + case '@': // skip capital letters + return '['; // skip \ for the sam reason like ' ('\\') + case '[': + return ']'; + case '_': // skip � and lower case letters (� can't be seen very well) + return '{'; + case '~': // close circle and restart at ! + return '!'; + default: + return (char) ((short) cur + 1); + } + } + + // we use only special characters to avoid collisions with existing objects + // the first character must remain unchanged though because it can have a + // special meaning + public War3ID getunusedid(final War3ID substitutefor) { + this.lastused = this.lastused.set(0, substitutefor.charAt(0)); + this.lastused = this.lastused.set(3, nextchar(substitutefor.charAt(3))); + if (this.lastused.charAt(3) == '!') { + this.lastused = this.lastused.set(2, nextchar(substitutefor.charAt(2))); + if (this.lastused.charAt(2) == '!') { + this.lastused = this.lastused.set(1, nextchar(substitutefor.charAt(1))); + } + } + return this.lastused; + } + + public void mergetable(final ObjectMap target, final ObjectMap targetCustom, final ObjectMap source, + final CollisionHandling collisionHandling) { + final Iterator> sourceObjectIterator = source.iterator(); + while (sourceObjectIterator.hasNext()) { + final Map.Entry sourceObject = sourceObjectIterator.next(); + if (target.containsKey(sourceObject.getKey())) { + // obj.cpp: we have a collision + War3ID oldId; + War3ID replacementId; + + switch (collisionHandling) { + case CREATE_NEW_ID: + oldId = sourceObject.getKey(); + // obj.cpp: get new id until we finally have one that isn't used yet, or we're + // out of ids + replacementId = getunusedid(oldId); + while (!((oldId.charAt(1) == '~') && (oldId.charAt(2) == '~') && (oldId.charAt(3) == '~')) + && targetCustom.containsKey(replacementId)) { + oldId = replacementId; + replacementId = getunusedid(oldId); + } + if (!((oldId.charAt(1) == '~') && (oldId.charAt(2) == '~') && (oldId.charAt(3) == '~'))) { + sourceObject.getValue().setNewId(replacementId); + targetCustom.put(replacementId, sourceObject.getValue().clone()); + } + break; + case REPLACE: + // final ObjectDataChangeEntry deleteObject = target.get(sourceObject.getKey()); + target.put(sourceObject.getKey(), sourceObject.getValue().clone()); + break; + default:// merge + final ObjectDataChangeEntry targetObject = target.get(sourceObject.getKey()); + for (final Map.Entry> sourceUnitField : sourceObject.getValue().getChanges()) { + for (final Change sourceChange : sourceUnitField.getValue()) { + List targetChanges = targetObject.getChanges().get(sourceUnitField.getKey()); + if (targetChanges == null) { + targetChanges = new ArrayList<>(); + } + Change bestTargetChange = null; + for (final Change targetChange : targetChanges) { + if (targetChange.getLevel() == sourceChange.getLevel()) { + bestTargetChange = targetChange; + break; + } + } + if (bestTargetChange != null) { + bestTargetChange.copyFrom(sourceChange); + } + else { + targetChanges.add(sourceChange.clone()); + if (targetChanges.size() == 1) { + targetObject.getChanges().add(sourceUnitField.getKey(), targetChanges); + } + } + } + } + break; + } + } + else { + targetCustom.put(sourceObject.getKey(), sourceObject.getValue().clone()); + } + } + } + + public static enum CollisionHandling { + CREATE_NEW_ID, + REPLACE, + MERGE; + } + + public void merge(final War3ObjectDataChangeset obj, final CollisionHandling collisionHandling) { + mergetable(this.original, this.custom, obj.original, collisionHandling); + mergetable(this.original, this.custom, obj.custom, collisionHandling); + } + + public int getvartype(final String name) { + if ("int".equals(name) || "bool".equals(name)) { + return 0; + } + else if ("real".equals(name)) { + return 1; + } + else if ("unreal".equals(name)) { + return 2; + } + return 3; // string + } + + public boolean loadtable(final LittleEndianDataInputStream stream, final ObjectMap map, final boolean isOriginal, + final WTS wts, final boolean inlineWTS) throws IOException { + final War3ID noid = new War3ID(0); + final ByteBuffer stringByteBuffer = ByteBuffer.allocate(1024); // TODO check max len? + final CharsetDecoder decoder = Charset.forName("utf-8").newDecoder().onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + int ptr; + final int count = stream.readInt(); + for (int i = 0; i < count; i++) { + final long nanoTime = System.nanoTime(); + War3ID origid; + War3ID newid = null; + origid = readWar3ID(stream); + ObjectDataChangeEntry existingObject; + if (isOriginal) { + if (noid.equals(origid)) { + throw new IOException("the input stream might be screwed"); + } + existingObject = map.get(origid); + if (existingObject == null) { + existingObject = new ObjectDataChangeEntry(origid, noid); + } + existingObject.setNewId(readWar3ID(stream)); + } + else { + newid = readWar3ID(stream); + if (noid.equals(origid) || noid.equals(newid)) { + throw new IOException("the input stream might be screwed"); + } + existingObject = map.get(newid); + if (existingObject == null) { + existingObject = new ObjectDataChangeEntry(origid, newid); + } + } + final int ccount = stream.readInt();// Retera: I assume this is change count? + if ((ccount == 0) && isOriginal) { + // throw new IOException("we seem to have reached the end of the stream and get + // zeroes"); + System.err.println("we seem to have reached the end of the stream and get zeroes"); + } + if (isOriginal) { + debugprint("StandardUnit \"" + origid + "\" " + ccount + " {"); + } + else { + debugprint("CustomUnit \"" + origid + ":" + newid + "\" " + ccount + " {"); + } + for (int j = 0; j < ccount; j++) { + final War3ID chid = readWar3ID(stream); + if (noid.equals(chid)) { + throw new IOException("the input stream might be screwed"); + } + if (!this.detected) { + this.detected = detectKind(chid); + } + + final Change newlyReadChange = new Change(); + newlyReadChange.setId(chid); + newlyReadChange.setVartype(stream.readInt()); + debugprint("\t\"" + chid + "\" {"); + debugprint("\t\tType " + newlyReadChange.getVartype() + ","); + if (extended()) { + newlyReadChange.setLevel(stream.readInt()); + newlyReadChange.setDataptr(stream.readInt()); + debugprint("\t\tLevel " + newlyReadChange.getLevel() + ","); + debugprint("\t\tData " + newlyReadChange.getDataptr() + ","); + } + + switch (newlyReadChange.getVartype()) { + case 0: + newlyReadChange.setLongval(stream.readInt()); + debugprint("\t\tValue " + newlyReadChange.getLongval() + ","); + break; + case 3: + ptr = 0; + stringByteBuffer.clear(); + byte charRead; + while ((charRead = (byte) stream.read()) != 0) { + stringByteBuffer.put(charRead); + } + stringByteBuffer.flip(); + newlyReadChange.setStrval(decoder.decode(stringByteBuffer).toString()); + if (inlineWTS && (newlyReadChange.getStrval().length() > 8) + && "TRIGSTR_".equals(newlyReadChange.getStrval().substring(0, 8))) { + final int key = getWTSValue(newlyReadChange); + newlyReadChange.setStrval(wts.get(key)); + if ((newlyReadChange.getStrval() != null) + && (newlyReadChange.getStrval().length() > MAX_STR_LEN)) { + newlyReadChange.setStrval(newlyReadChange.getStrval().substring(0, MAX_STR_LEN - 1)); + } + } + debugprint("\t\tValue \"" + newlyReadChange.getStrval() + "\","); + break; + case 4: + newlyReadChange.setBoolval(stream.readInt() == 1); + debugprint("\t\tValue " + newlyReadChange.isBoolval() + ","); + break; + default: + newlyReadChange.setRealval(stream.readFloat()); + debugprint("\t\tValue " + newlyReadChange.getRealval() + ","); + break; + } + final War3ID crap = readWar3ID(stream); + debugprint("\t\tExtra \"" + crap + "\","); + newlyReadChange.setJunkDNA(crap); + List existingChanges = existingObject.getChanges().get(chid); + if (existingChanges == null) { + existingChanges = new ArrayList<>(); + } + Change bestTargetChange = null; + for (final Change targetChange : existingChanges) { + if (targetChange.getLevel() == newlyReadChange.getLevel()) { + bestTargetChange = targetChange; + break; + } + } + if (bestTargetChange != null) { + bestTargetChange.copyFrom(newlyReadChange); + } + else { + existingChanges.add(newlyReadChange.clone()); + if (existingChanges.size() == 1) { + existingObject.getChanges().add(chid, existingChanges); + } + } + if (!crap.equals(existingObject.getOldId()) && !crap.equals(existingObject.getNewId()) + && !crap.equals(noid)) { + for (int charIndex = 0; charIndex < 4; charIndex++) { + if ((crap.charAt(charIndex) < 32) || (crap.charAt(charIndex) > 126)) { + return false; + } + } + } + debugprint("\t}"); + } + debugprint("}"); + if ((newid == null) && !isOriginal) { + throw new IllegalStateException("custom unit has no ID!"); + } + map.put(isOriginal ? origid : newid, existingObject); + final long endNanoTime = System.nanoTime(); + final long deltaNanoTime = endNanoTime - nanoTime; + } + return true; + } + + private War3ID readWar3ID(final LittleEndianDataInputStream stream) throws IOException { + return new War3ID(Integer.reverseBytes(stream.readInt())); + } + + private static int getWTSValue(final Change change) { + String numberAsText = change.getStrval().substring(8); + while ((numberAsText.length() > 0) && (numberAsText.charAt(0) == '0')) { + numberAsText = numberAsText.substring(1); + } + if (numberAsText.length() == 0) { + return 0; + } + while (!Character.isDigit(numberAsText.charAt(numberAsText.length() - 1))) { + numberAsText = numberAsText.substring(0, numberAsText.length() - 1); + } + return Integer.parseInt(numberAsText); + } + + public boolean load(final LittleEndianDataInputStream stream, final WTS wts, final boolean inlineWTS) + throws IOException { + this.detected = false; + this.version = stream.readInt(); + if ((this.version != 1) && (this.version != 2)) { + return false; + } + ObjectMap backup = this.original.clone(); + if (!loadtable(stream, this.original, true, wts, inlineWTS)) { + this.original = backup; + return false; + } + backup = this.custom.clone(); + if (!loadtable(stream, this.custom, false, wts, inlineWTS)) { + this.original = backup; + return false; + } + return true; + } + + public boolean load(final File file, final WTS wts, final boolean inlineWTS) throws IOException { + try (LittleEndianDataInputStream inputStream = new LittleEndianDataInputStream(new FileInputStream(file))) { + final boolean result = load(inputStream, wts, inlineWTS); + return result; + } + } + + public static void inlineWTSTable(final ObjectMap map, final WTS wts) { + for (final Map.Entry entry : map.entrySet()) { + for (final Map.Entry> changes : entry.getValue().getChanges()) { + for (final Change change : changes.getValue()) { + if ((change.getStrval().length() > 8) && "TRIGSTR_".equals(change.getStrval().substring(0, 8))) { + final int key = getWTSValue(change); + change.setStrval(wts.get(key)); + if (change.getStrval().length() > MAX_STR_LEN) { + change.setStrval(change.getStrval().substring(0, MAX_STR_LEN - 1)); + } + } + } + } + } + } + + public void inlineWTS(final WTS wts) { + inlineWTSTable(this.original, wts); + inlineWTSTable(this.custom, wts); + } + + public void reset() { + reset('u'); + } + + public void reset(final char expectedkind) { + this.detected = false; + this.kind = 'u'; + this.lastused = War3ID.fromString("u~~~"); + this.expected = expectedkind; + this.original.clear(); + this.custom.clear(); + } + + public boolean saveTable(final LittleEndianDataOutputStream outputStream, final ObjectMap map, + final boolean isOriginal) throws IOException { + final CharsetEncoder encoder = Charset.forName("utf-8").newEncoder().onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + final CharBuffer charBuffer = CharBuffer.allocate(1024); + final ByteBuffer byteBuffer = ByteBuffer.allocate(1024); + final War3ID noid = new War3ID(0); + int count; + count = map.size(); + outputStream.writeInt(count); + for (final Map.Entry entry : map) { + final ObjectDataChangeEntry cl = entry.getValue(); + int totalSize = 0; + for (final Map.Entry> changeEntry : cl.getChanges()) { + totalSize += changeEntry.getValue().size(); + } + if ((totalSize > 0) || !isOriginal) { + ParseUtils.writeWar3ID(outputStream, cl.getOldId()); + ParseUtils.writeWar3ID(outputStream, cl.getNewId()); + count = totalSize;// cl.getChanges().size(); + outputStream.writeInt(count); + for (final Map.Entry> changes : entry.getValue().getChanges()) { + for (final Change change : changes.getValue()) { + ParseUtils.writeWar3ID(outputStream, change.getId()); + outputStream.writeInt(change.getVartype()); + if (extended()) { + outputStream.writeInt(change.getLevel()); + outputStream.writeInt(change.getDataptr()); + } + switch (change.getVartype()) { + case 0: + outputStream.writeInt(change.getLongval()); + break; + case 3: + charBuffer.clear(); + byteBuffer.clear(); + charBuffer.put(change.getStrval()); + charBuffer.flip(); + encoder.encode(charBuffer, byteBuffer, false); + byteBuffer.flip(); + final byte[] stringBytes = new byte[byteBuffer.remaining() + 1]; + int i = 0; + while (byteBuffer.hasRemaining()) { + stringBytes[i++] = byteBuffer.get(); + } + stringBytes[i] = 0; + outputStream.write(stringBytes); + break; + case 4: + outputStream.writeInt(change.isBoolval() ? 1 : 0); + break; + default: + outputStream.writeFloat(change.getRealval()); + break; + } + // if (change.getJunkDNA() == null) { + // saveWriteChars(outputStream, cl.getNewId().asStringValue().toCharArray()); + // } else { + // saveWriteChars(outputStream, + // change.getJunkDNA().asStringValue().toCharArray()); + // } + // saveWriteChars(outputStream, cl.getNewId().asStringValue().toCharArray()); + ParseUtils.writeWar3ID(outputStream, noid); + } + } + } + } + return true; + } + + public boolean save(final LittleEndianDataOutputStream outputStream, final boolean generateWTS) throws IOException { + if (generateWTS) { + throw new UnsupportedOperationException("FAIL cannot generate WTS, needs more code"); + } + this.version = 2; + outputStream.writeInt(this.version); + if (!saveTable(outputStream, this.original, true)) { + throw new RuntimeException("Failed to save standard unit custom data"); + } + if (!saveTable(outputStream, this.custom, false)) { + throw new RuntimeException("Failed to save custom unit custom data"); + } + return true; + } + + public ObjectMap getOriginal() { + return this.original; + } + + public ObjectMap getCustom() { + return this.custom; + } + + private static void debugprint(final String s) { + + } +} diff --git a/core/src/com/etheller/warsmash/units/manager/MutableObjectData.java b/core/src/com/etheller/warsmash/units/manager/MutableObjectData.java new file mode 100644 index 0000000..a343452 --- /dev/null +++ b/core/src/com/etheller/warsmash/units/manager/MutableObjectData.java @@ -0,0 +1,955 @@ +package com.etheller.warsmash.units.manager; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.etheller.warsmash.units.GameObject; +import com.etheller.warsmash.units.ObjectData; +import com.etheller.warsmash.units.custom.Change; +import com.etheller.warsmash.units.custom.ChangeMap; +import com.etheller.warsmash.units.custom.ObjectDataChangeEntry; +import com.etheller.warsmash.units.custom.War3ObjectDataChangeset; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.util.WorldEditStrings; + +public final class MutableObjectData { + private static final War3ID ROC_SUPPORT_URAC = War3ID.fromString("urac"); + private static final War3ID ROC_SUPPORT_UCAM = War3ID.fromString("ucam"); + private static final War3ID ROC_SUPPORT_USPE = War3ID.fromString("uspe"); + private static final War3ID ROC_SUPPORT_UBDG = War3ID.fromString("ubdg"); + + private final WorldEditorDataType worldEditorDataType; + private final ObjectData sourceSLKData; + private final ObjectData sourceSLKMetaData; + private final War3ObjectDataChangeset editorData; + private Set cachedKeySet; + private final Map metaNameToMetaId; + private final Map cachedKeyToGameObject; + private final MutableObjectDataChangeNotifier changeNotifier; + private final WorldEditStrings worldEditStrings; + + public MutableObjectData(final WorldEditStrings worldEditStrings, final WorldEditorDataType worldEditorDataType, + final ObjectData sourceSLKData, final ObjectData sourceSLKMetaData, + final War3ObjectDataChangeset editorData) { + this.worldEditStrings = worldEditStrings; + this.worldEditorDataType = worldEditorDataType; + resolveStringReferencesInNames(sourceSLKData); + this.sourceSLKData = sourceSLKData; + this.sourceSLKMetaData = sourceSLKMetaData; + this.editorData = editorData; + this.metaNameToMetaId = new HashMap<>(); + for (final String metaKeyString : sourceSLKMetaData.keySet()) { + final War3ID metaKey = War3ID.fromString(metaKeyString); + this.metaNameToMetaId.put(sourceSLKMetaData.get(metaKeyString).getField("field"), metaKey); + } + this.cachedKeyToGameObject = new HashMap<>(); + this.changeNotifier = new MutableObjectDataChangeNotifier(); + } + + // TODO remove this hack + public War3ObjectDataChangeset getEditorData() { + return this.editorData; + } + + private void resolveStringReferencesInNames(final ObjectData sourceSLKData) { + for (final String key : sourceSLKData.keySet()) { + final GameObject gameObject = sourceSLKData.get(key); + String name = gameObject.getField("Name"); + final String suffix = gameObject.getField("EditorSuffix"); + if (name.startsWith("WESTRING")) { + if (!name.contains(" ")) { + name = this.worldEditStrings.getString(name); + } + else { + final String[] names = name.split(" "); + name = ""; + for (final String subName : names) { + if (name.length() > 0) { + name += " "; + } + if (subName.startsWith("WESTRING")) { + name += this.worldEditStrings.getString(subName); + } + else { + name += subName; + } + } + } + if (name.startsWith("\"") && name.endsWith("\"")) { + name = name.substring(1, name.length() - 1); + } + gameObject.setField("Name", name); + } + if (suffix.startsWith("WESTRING")) { + gameObject.setField("EditorSuffix", this.worldEditStrings.getString(suffix)); + } + } + } + + public void mergeChangset(final War3ObjectDataChangeset changeset) { + final List newObjects = new ArrayList<>(); + final Map previousAliasToNewAlias = new HashMap<>(); + for (final Map.Entry entry : changeset.getCustom()) { + +// final String newId = JOptionPane.showInputDialog("Choose UNIT ID"); + final War3ID nextDefaultEditorId = /* War3ID.fromString(newId); */getNextDefaultEditorId( + War3ID.fromString(entry.getKey().charAt(0) + "000")); + ; + System.out.println("Merging " + nextDefaultEditorId + " for " + entry.getKey()); + // createNew API will notifier the changeNotifier + final MutableGameObject newObject = createNew(nextDefaultEditorId, entry.getValue().getOldId(), false); + for (final Map.Entry> changeList : entry.getValue().getChanges()) { + newObject.customUnitData.getChanges().add(changeList.getKey(), changeList.getValue()); + } + newObjects.add(nextDefaultEditorId); + previousAliasToNewAlias.put(entry.getKey(), nextDefaultEditorId); + } + final War3ID[] fieldsToCheck = this.worldEditorDataType == WorldEditorDataType.UNITS + ? new War3ID[] { War3ID.fromString("utra"), War3ID.fromString("uupt"), War3ID.fromString("ubui") } + : new War3ID[] {}; + for (final War3ID unitId : newObjects) { + final MutableGameObject unit = get(unitId); + for (final War3ID field : fieldsToCheck) { + final String techtreeString = unit.getFieldAsString(field, 0); + final java.util.List techList = Arrays.asList(techtreeString.split(",")); + final ArrayList resultingTechList = new ArrayList<>(); + for (final String tech : techList) { + if (tech.length() == 4) { + final War3ID newTechId = previousAliasToNewAlias.get(War3ID.fromString(tech)); + if (newTechId != null) { + resultingTechList.add(newTechId.toString()); + } + else { + resultingTechList.add(tech); + } + } + else { + resultingTechList.add(tech); + } + } + final StringBuilder sb = new StringBuilder(); + for (final String tech : resultingTechList) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(tech); + } + unit.setField(field, 0, sb.toString()); + } + } + this.changeNotifier.objectsCreated(newObjects.toArray(new War3ID[newObjects.size()])); + } + + public War3ObjectDataChangeset copySelectedObjects(final List objectsToCopy) { + final War3ObjectDataChangeset changeset = new War3ObjectDataChangeset(this.editorData.getExpectedKind()); + final War3ID[] fieldsToCheck = this.worldEditorDataType == WorldEditorDataType.UNITS + ? new War3ID[] { War3ID.fromString("utra"), War3ID.fromString("uupt"), War3ID.fromString("ubui") } + : new War3ID[] {}; + final Map previousAliasToNewAlias = new HashMap<>(); + for (final MutableGameObject gameObject : objectsToCopy) { + final ObjectDataChangeEntry gameObjectUserDataToCopy; + final ObjectDataChangeEntry gameObjectUserData; + final War3ID alias = gameObject.getAlias(); + if (this.editorData.getOriginal().containsKey(alias)) { + gameObjectUserDataToCopy = this.editorData.getOriginal().get(alias); + final War3ID newAlias = getNextDefaultEditorId( + War3ID.fromString(gameObject.getCode().charAt(0) + "000"), changeset, this.sourceSLKData); + gameObjectUserData = new ObjectDataChangeEntry(gameObjectUserDataToCopy.getOldId(), newAlias); + } + else if (this.editorData.getCustom().containsKey(alias)) { + gameObjectUserDataToCopy = this.editorData.getCustom().get(alias); + gameObjectUserData = new ObjectDataChangeEntry(gameObjectUserDataToCopy.getOldId(), + gameObjectUserDataToCopy.getNewId()); + } + else { + gameObjectUserDataToCopy = null; + final War3ID newAlias = getNextDefaultEditorId( + War3ID.fromString(gameObject.getCode().charAt(0) + "000"), changeset, this.sourceSLKData); + gameObjectUserData = new ObjectDataChangeEntry( + gameObject.isCustom() ? gameObject.getCode() : gameObject.getAlias(), newAlias); + } + if (gameObjectUserDataToCopy != null) { + for (final Map.Entry> changeEntry : gameObjectUserDataToCopy.getChanges()) { + for (final Change change : changeEntry.getValue()) { + final Change newChange = new Change(); + newChange.copyFrom(change); + gameObjectUserData.getChanges().add(change.getId(), newChange); + } + } + } + previousAliasToNewAlias.put(gameObject.getAlias(), gameObjectUserData.getNewId()); + changeset.getCustom().put(gameObjectUserData.getNewId(), gameObjectUserData); + } + final MutableObjectData changeEditManager = new MutableObjectData(this.worldEditStrings, + this.worldEditorDataType, this.sourceSLKData, this.sourceSLKMetaData, changeset); + for (final War3ID unitId : changeEditManager.keySet()) { + final MutableGameObject unit = changeEditManager.get(unitId); + for (final War3ID field : fieldsToCheck) { + final String techtreeString = unit.getFieldAsString(field, 0); + final java.util.List techList = Arrays.asList(techtreeString.split(",")); + final ArrayList resultingTechList = new ArrayList<>(); + for (final String tech : techList) { + if (tech.length() == 4) { + final War3ID newTechId = previousAliasToNewAlias.get(War3ID.fromString(tech)); + if (newTechId != null) { + resultingTechList.add(newTechId.toString()); + } + else { + resultingTechList.add(tech); + } + } + else { + resultingTechList.add(tech); + } + } + final StringBuilder sb = new StringBuilder(); + for (final String tech : resultingTechList) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(tech); + } + unit.setField(field, 0, sb.toString()); + } + } + return changeset; + + } + + public WorldEditorDataType getWorldEditorDataType() { + return this.worldEditorDataType; + } + + public ObjectData getSourceSLKMetaData() { + return this.sourceSLKMetaData; + } + + public void addChangeListener(final MutableObjectDataChangeListener listener) { + this.changeNotifier.subscribe(listener); + } + + public void removeChangeListener(final MutableObjectDataChangeListener listener) { + this.changeNotifier.unsubscribe(listener); + } + + /** + * Returns the set of all Unit IDs in the map, at the cost of a lot of time to + * go find them all. + * + * @return + */ + + public Set keySet() { + if (this.cachedKeySet == null) { + final Set customUnitKeys = this.editorData.getCustom().keySet(); + final Set customKeys = new HashSet<>(customUnitKeys); + for (final String standardUnitKey : this.sourceSLKData.keySet()) { + customKeys.add(War3ID.fromString(standardUnitKey)); + } + this.cachedKeySet = customKeys; + } + return this.cachedKeySet; + } + + public void dropCachesHack() { + this.cachedKeySet = null; + this.cachedKeyToGameObject.clear(); + } + + public MutableGameObject get(final War3ID id) { + MutableGameObject mutableGameObject = this.cachedKeyToGameObject.get(id); + if (mutableGameObject == null) { + if (this.editorData.getCustom().containsKey(id)) { + final ObjectDataChangeEntry customUnitData = this.editorData.getCustom().get(id); + GameObject parentWC3Object = this.sourceSLKData.get(customUnitData.getOldId().asStringValue()); + if (parentWC3Object == null) { + System.err.println("Error parsing unit data: custom unit inherits from unknown id '" + + customUnitData.getOldId().asStringValue() + "'"); + parentWC3Object = GameObject.EMPTY; + } + mutableGameObject = new MutableGameObject(parentWC3Object, customUnitData); + this.cachedKeyToGameObject.put(id, mutableGameObject); + } + else if (this.editorData.getOriginal().containsKey(id)) { + final ObjectDataChangeEntry customUnitData = this.editorData.getOriginal().get(id); + GameObject parentWC3Object = this.sourceSLKData.get(customUnitData.getOldId().asStringValue()); + if (parentWC3Object == null) { + System.err.println("Error parsing unit data: standard unit modifies unknown id '" + + customUnitData.getOldId().asStringValue() + "'"); + parentWC3Object = GameObject.EMPTY; + } + mutableGameObject = new MutableGameObject(parentWC3Object, this.editorData.getOriginal().get(id)); + this.cachedKeyToGameObject.put(id, mutableGameObject); + } + else if (this.sourceSLKData.get(id.asStringValue()) != null) { + GameObject parentWC3Object = this.sourceSLKData.get(id.asStringValue()); + if (parentWC3Object == null) { + System.err.println("Error parsing unit data: id does not exist: '" + id.asStringValue() + "'"); + parentWC3Object = GameObject.EMPTY; + } + mutableGameObject = new MutableGameObject(parentWC3Object, null); + this.cachedKeyToGameObject.put(id, mutableGameObject); + } + } + return mutableGameObject; + } + + public MutableGameObject createNew(final War3ID id, final War3ID parent) { + return createNew(id, parent, true); + } + + private MutableGameObject createNew(final War3ID id, final War3ID parent, final boolean fireListeners) { + this.editorData.getCustom().put(id, new ObjectDataChangeEntry(parent, id)); + if (this.cachedKeySet != null) { + this.cachedKeySet.add(id); + } + if (fireListeners) { + this.changeNotifier.objectCreated(id); + } + return get(id); + } + + public void remove(final War3ID id) { + remove(id, true); + } + + public void remove(final List objects) { + final List removedIds = new ArrayList<>(); + for (final MutableGameObject object : objects) { + if (object.isCustom()) { + remove(object.getAlias(), false); + removedIds.add(object.getAlias()); + } + } + this.changeNotifier.objectsRemoved(removedIds.toArray(new War3ID[removedIds.size()])); + } + + private MutableGameObject remove(final War3ID id, final boolean fireListeners) { + final ObjectDataChangeEntry removedObject = this.editorData.getCustom().remove(id); + final MutableGameObject removedMutableObj = this.cachedKeyToGameObject.remove(id); + if (this.cachedKeySet != null) { + this.cachedKeySet.remove(id); + } + if (fireListeners) { + this.changeNotifier.objectRemoved(id); + } + return removedMutableObj /* might be null based on cache, don't use */; + } + + private static boolean goodForId(final char c) { + return Character.isDigit(c) || ((c >= 'A') && (c <= 'Z')); + } + + public War3ID getNextDefaultEditorId(final War3ID startingId) { + War3ID newId = startingId; + while (this.editorData.getCustom().containsKeyCaseInsensitive(newId) + || (this.sourceSLKData.get(newId.toString()) != null) || !goodForId(newId.charAt(1)) + || !goodForId(newId.charAt(2)) || !goodForId(newId.charAt(3))) { + // TODO good code general solution + if (newId.charAt(3) == 'Z') { + if (newId.charAt(2) == 'Z') { + if (newId.charAt(1) == 'Z') { + newId = new War3ID(((newId.getValue() / (256 * 256 * 256)) * 256 * 256 * 256) + + (256 * 256 * 256) + '0' + ('0' * 256) + ('0' * 256 * 256)); + } + else { + newId = new War3ID( + ((newId.getValue() / (256 * 256)) * 256 * 256) + (256 * 256) + '0' + ('0' * 256)); + } + } + else { + newId = new War3ID(((newId.getValue() / 256) * 256) + 256 + '0'); + } + } + else { + newId = new War3ID(newId.getValue() + 1); + } + } + return newId; + } + + public static War3ID getNextDefaultEditorId(final War3ID startingId, final War3ObjectDataChangeset editorData, + final ObjectData sourceSLKData) { + War3ID newId = startingId; + while (editorData.getCustom().containsKeyCaseInsensitive(newId) || (sourceSLKData.get(newId.toString()) != null) + || !goodForId(newId.charAt(1)) || !goodForId(newId.charAt(2)) || !goodForId(newId.charAt(3))) { + newId = new War3ID(newId.getValue() + 1); + } + return newId; + } + + private static final War3ID BUFF_EDITOR_NAME = War3ID.fromString("fnam"); + private static final War3ID BUFF_BUFFTIP = War3ID.fromString("ftip"); + private static final War3ID UNIT_CAMPAIGN = War3ID.fromString("ucam"); + private static final War3ID UNIT_EDITOR_SUFFIX = War3ID.fromString("unsf"); + private static final War3ID ABIL_EDITOR_SUFFIX = War3ID.fromString("ansf"); + private static final War3ID DESTRUCTABLE_EDITOR_SUFFIX = War3ID.fromString("bsuf"); + private static final War3ID BUFF_EDITOR_SUFFIX = War3ID.fromString("fnsf"); + private static final War3ID UPGRADE_EDITOR_SUFFIX = War3ID.fromString("gnsf"); + private static final War3ID HERO_PROPER_NAMES = War3ID.fromString("upro"); + + private static final Set CATEGORY_FIELDS = new HashSet<>(); + private static final Set TEXT_FIELDS = new HashSet<>(); + private static final Set ICON_FIELDS = new HashSet<>(); + private static final Set FIELD_SETTINGS_FIELDS = new HashSet<>(); + + static { + // categorizing - I thought these would be changeFlags value "c", but no luck + CATEGORY_FIELDS.add(War3ID.fromString("ubdg")); // is a building + CATEGORY_FIELDS.add(War3ID.fromString("uspe")); // categorize special + CATEGORY_FIELDS.add(War3ID.fromString("ucam")); // categorize campaign + CATEGORY_FIELDS.add(War3ID.fromString("urac")); // race + CATEGORY_FIELDS.add(War3ID.fromString("uine")); // in editor + CATEGORY_FIELDS.add(War3ID.fromString("ucls")); // sort string (not a real field, fanmade) + + CATEGORY_FIELDS.add(War3ID.fromString("icla")); // item class + + CATEGORY_FIELDS.add(War3ID.fromString("bcat")); // destructible category + + CATEGORY_FIELDS.add(War3ID.fromString("dcat")); // doodad category + + CATEGORY_FIELDS.add(War3ID.fromString("aher")); // hero ability + CATEGORY_FIELDS.add(War3ID.fromString("aite")); // item ability + CATEGORY_FIELDS.add(War3ID.fromString("arac")); // ability race + + CATEGORY_FIELDS.add(War3ID.fromString("frac")); // buff race + CATEGORY_FIELDS.add(War3ID.fromString("feff")); // is effect + + CATEGORY_FIELDS.add(War3ID.fromString("grac")); // upgrade race + // field structure fields - doesn't seem to be changeFlags 's' like you might + // hope + FIELD_SETTINGS_FIELDS.add(War3ID.fromString("ubdg")); // unit is a builder + FIELD_SETTINGS_FIELDS.add(War3ID.fromString("dvar")); // doodad variations + FIELD_SETTINGS_FIELDS.add(War3ID.fromString("alev")); // ability level + FIELD_SETTINGS_FIELDS.add(War3ID.fromString("glvl")); // upgrade max level + } + + public final class MutableGameObject { + private final GameObject parentWC3Object; + private ObjectDataChangeEntry customUnitData; + + private void fireChangedEvent(final War3ID field, final int level) { + final String changeFlags = MutableObjectData.this.sourceSLKMetaData.get(field.toString()) + .getField("changeFlags"); + if (CATEGORY_FIELDS.contains(field)) { + MutableObjectData.this.changeNotifier.categoriesChanged(getAlias()); + } + else if (changeFlags.contains("t")) { + MutableObjectData.this.changeNotifier.textChanged(getAlias()); + } + else if (changeFlags.contains("m")) { + MutableObjectData.this.changeNotifier.modelChanged(getAlias()); + } + else if (changeFlags.contains("i")) { + MutableObjectData.this.changeNotifier.iconsChanged(getAlias()); + } + else if (FIELD_SETTINGS_FIELDS.contains(field)) { + MutableObjectData.this.changeNotifier.fieldsChanged(getAlias()); + } + } + + public MutableGameObject(final GameObject parentWC3Object, final ObjectDataChangeEntry customUnitData) { + this.parentWC3Object = parentWC3Object; + if (parentWC3Object == null) { + System.err.println( + "Parent object is null for " + customUnitData.getNewId() + ":" + customUnitData.getOldId()); + throw new AssertionError("parentWC3Object cannot be null"); +// this.parentWC3Object = new Element("", new DataTable()); + } + this.customUnitData = customUnitData; + } + + public boolean hasCustomField(final War3ID field, final int level) { + return getMatchingChange(field, level) != null; + } + + public boolean hasEditorData() { + return (this.customUnitData != null) && (this.customUnitData.getChanges().size() > 0); + } + + public boolean isCustom() { + return MutableObjectData.this.editorData.getCustom().containsKey(getAlias()); + } + + public void setField(final War3ID field, final int level, final String value) { + if (value.equals(getFieldStringFromSLKs(field, level))) { + if (!value.equals(getFieldAsString(field, level))) { + fireChangedEvent(field, level); + } + else { + } + resetFieldToDefaults(field, level); + return; + } + final Change matchingChange = getOrCreateMatchingChange(field, level); + matchingChange.setStrval(value); + matchingChange.setVartype(War3ObjectDataChangeset.VAR_TYPE_STRING); + fireChangedEvent(field, level); + } + + public void setField(final War3ID field, final int level, final boolean value) { + if (value == (asInt(getFieldStringFromSLKs(field, level).trim()) == 1)) { + if (value != getFieldAsBoolean(field, level)) { + fireChangedEvent(field, level); + } + resetFieldToDefaults(field, level); + return; + } + final Change matchingChange = getOrCreateMatchingChange(field, level); + matchingChange.setBoolval(value); + matchingChange.setVartype(War3ObjectDataChangeset.VAR_TYPE_BOOLEAN); + fireChangedEvent(field, level); + } + + public void setField(final War3ID field, final int level, final int value) { + if (value == asInt(getFieldStringFromSLKs(field, level).trim())) { + if (value != getFieldAsInteger(field, level)) { + fireChangedEvent(field, level); + } + resetFieldToDefaults(field, level); + return; + } + final Change matchingChange = getOrCreateMatchingChange(field, level); + matchingChange.setLongval(value); + matchingChange.setVartype(War3ObjectDataChangeset.VAR_TYPE_INT); + fireChangedEvent(field, level); + } + + public void resetFieldToDefaults(final War3ID field, final int level) { + final Change existingChange = getMatchingChange(field, level); + if ((existingChange != null) && (this.customUnitData != null)) { + this.customUnitData.getChanges().delete(field, existingChange); + fireChangedEvent(field, level); + } + return; + } + + public void setField(final War3ID field, final int level, final float value) { + if (Math.abs(value - asFloat(getFieldStringFromSLKs(field, level).trim())) < 0.00001f) { + if (Math.abs(value - getFieldAsFloat(field, level)) > 0.00001f) { + fireChangedEvent(field, level); + } + resetFieldToDefaults(field, level); + return; + } + final Change matchingChange = getOrCreateMatchingChange(field, level); + matchingChange.setRealval(value); + final boolean unsigned = MutableObjectData.this.sourceSLKMetaData.get(field.asStringValue()) + .getField("type").equals("unreal"); + matchingChange.setVartype( + unsigned ? War3ObjectDataChangeset.VAR_TYPE_UNREAL : War3ObjectDataChangeset.VAR_TYPE_REAL); + fireChangedEvent(field, level); + } + + private Change getOrCreateMatchingChange(final War3ID field, final int level) { + if (this.customUnitData == null) { + final War3ID war3Id = War3ID.fromString(this.parentWC3Object.getId()); + final ObjectDataChangeEntry newCustomUnitData = new ObjectDataChangeEntry(war3Id, War3ID.NONE); + MutableObjectData.this.editorData.getOriginal().put(war3Id, newCustomUnitData); + this.customUnitData = newCustomUnitData; + } + Change matchingChange = getMatchingChange(field, level); + if (matchingChange == null) { + final ChangeMap changeMap = this.customUnitData.getChanges(); + final List changeList = changeMap.get(field); + matchingChange = new Change(); + matchingChange.setId(field); + matchingChange.setLevel(level); + if (MutableObjectData.this.editorData.extended()) { + // dunno why, but Blizzard sure likes those dataptrs in the ability data + // my code should grab 0 when the metadata lacks this field + matchingChange.setDataptr( + MutableObjectData.this.sourceSLKMetaData.get(field.asStringValue()).getFieldValue("data")); + } + if (changeList == null) { + changeMap.add(field, matchingChange); + } + else { + boolean insertedChange = false; + for (int i = 0; i < changeList.size(); i++) { + if (changeList.get(i).getLevel() > level) { + insertedChange = true; + changeList.add(i, matchingChange); + break; + } + } + if (!insertedChange) { + changeList.add(changeList.size(), matchingChange); + } + } + } + return matchingChange; + } + + public String getFieldAsString(final War3ID field, final int level) { + final Change matchingChange = getMatchingChange(field, level); + if (matchingChange != null) { + if (matchingChange.getVartype() != War3ObjectDataChangeset.VAR_TYPE_STRING) { + throw new IllegalStateException( + "Requested string value of '" + field + "' from '" + this.parentWC3Object.getId() + + "', but this field was not a string! vartype=" + matchingChange.getVartype()); + } + return matchingChange.getStrval(); + } + // no luck with custom data, look at the standard data + int slkLevel = level; + if (MutableObjectData.this.worldEditorDataType == WorldEditorDataType.UPGRADES) { + slkLevel -= 1; + } + return getFieldStringFromSLKs(field, slkLevel); + } + + private Change getMatchingChange(final War3ID field, final int level) { + Change matchingChange = null; + if (this.customUnitData == null) { + return null; + } + final List changeList = this.customUnitData.getChanges().get(field); + if (changeList != null) { + for (final Change change : changeList) { + if (change.getLevel() == level) { + matchingChange = change; + break; + } + } + } + return matchingChange; + } + + public String readSLKTag(final String key) { + if (MutableObjectData.this.metaNameToMetaId.containsKey(key)) { + return getFieldAsString(MutableObjectData.this.metaNameToMetaId.get(key), 0); + } + return this.parentWC3Object.getField(key); + } + + public boolean readSLKTagBoolean(final String key) { + if (MutableObjectData.this.metaNameToMetaId.containsKey(key)) { + return getFieldAsBoolean(MutableObjectData.this.metaNameToMetaId.get(key), 0); + } + return this.parentWC3Object.getFieldValue(key) == 1; + } + + public int readSLKTagInt(final String key) { + if (MutableObjectData.this.metaNameToMetaId.containsKey(key)) { + return getFieldAsInteger(MutableObjectData.this.metaNameToMetaId.get(key), 0); + } + return this.parentWC3Object.getFieldValue(key); + } + + public float readSLKTagFloat(final String key) { + if (MutableObjectData.this.metaNameToMetaId.containsKey(key)) { + return getFieldAsFloat(MutableObjectData.this.metaNameToMetaId.get(key), 0); + } + try { + return Float.parseFloat(this.parentWC3Object.getField(key)); + } + catch (final NumberFormatException exc) { + return Float.NaN; + } + } + + public String getName() { + String name = getFieldAsString(MutableObjectData.this.editorData.getNameField(), + MutableObjectData.this.worldEditorDataType == WorldEditorDataType.UPGRADES ? 1 : 0); + boolean nameKnown = name.length() >= 1; + if (!nameKnown && !readSLKTag("code").equals(getAlias().toString()) && (readSLKTag("code").length() >= 4) + && !isCustom()) { + final MutableGameObject codeObject = get(War3ID.fromString(readSLKTag("code").substring(0, 4))); + if (codeObject != null) { + name = codeObject.getName(); + nameKnown = true; + } + } + String suf = ""; + switch (MutableObjectData.this.worldEditorDataType) { + case ABILITIES: + suf = getFieldAsString(ABIL_EDITOR_SUFFIX, 0); + break; + case BUFFS_EFFECTS: + final String editorName = getFieldAsString(BUFF_EDITOR_NAME, 0); + if (!nameKnown && (editorName.length() > 1)) { + name = editorName; + nameKnown = true; + } + final String buffTip = getFieldAsString(BUFF_BUFFTIP, 0); + if (!nameKnown && (buffTip.length() > 1)) { + name = buffTip; + nameKnown = true; + } + suf = getFieldAsString(BUFF_EDITOR_SUFFIX, 0); + break; + case DESTRUCTIBLES: + suf = getFieldAsString(DESTRUCTABLE_EDITOR_SUFFIX, 0); + break; + case DOODADS: + break; + case ITEM: + break; + case UNITS: + if (getFieldAsBoolean(UNIT_CAMPAIGN, 0) && Character.isUpperCase(getAlias().charAt(0))) { + name = getFieldAsString(HERO_PROPER_NAMES, 0); + if (name.contains(",")) { + name = name.split(",")[0]; + } + } + suf = getFieldAsString(UNIT_EDITOR_SUFFIX, 0); + break; + case UPGRADES: + suf = getFieldAsString(UPGRADE_EDITOR_SUFFIX, 1); + break; + } + if (nameKnown/* && name.startsWith("WESTRING") */) { + if (!name.contains(" ")) { + // name = WEString.getString(name); + } + else { + final String[] names = name.split(" "); + name = ""; + for (final String subName : names) { + if (name.length() > 0) { + name += " "; + } + // if (subName.startsWith("WESTRING")) { + // name += WEString.getString(subName); + // } else { + name += subName; + // } + } + } + if (name.startsWith("\"") && name.endsWith("\"")) { + name = name.substring(1, name.length() - 1); + } + } + if (!nameKnown) { + name = MutableObjectData.this.worldEditStrings.getString("WESTRING_UNKNOWN") + " '" + + getAlias().toString() + "'"; + } + if ((suf.length() > 0) && !suf.equals("_")) { + // if (suf.startsWith("WESTRING")) { + // suf = WEString.getString(suf); + // } + if (!suf.startsWith(" ")) { + name += " "; + } + name += suf; + } + return name; + } + + private String getFieldStringFromSLKs(final War3ID field, final int level) { + final GameObject metaData = MutableObjectData.this.sourceSLKMetaData.get(field.asStringValue()); + if (metaData == null) { + if (MutableObjectData.this.worldEditorDataType == WorldEditorDataType.UNITS) { + if (ROC_SUPPORT_URAC.equals(field)) { + return this.parentWC3Object.getField("race"); + } + else if (ROC_SUPPORT_UCAM.equals(field)) { + return "0"; + } + else if (ROC_SUPPORT_USPE.equals(field)) { + return this.parentWC3Object.getField("special"); + } + else if (ROC_SUPPORT_UBDG.equals(field)) { + return this.parentWC3Object.getField("isbldg"); + } + } + throw new IllegalStateException("Program requested " + field.toString() + " from " + + MutableObjectData.this.worldEditorDataType); + } + if (this.parentWC3Object == null) { + throw new IllegalStateException("corrupted unit, no parent unit id"); + } + int index = metaData.getFieldValue("index"); + final String upgradeHack = metaData.getField("appendIndex"); + if ("0".equals(upgradeHack)) { + // Engage magic upgrade hack to replace index with level + if (!field.toString().equals("gbpx") && !field.toString().equals("gbpy")) { + index = level; + } + } + else if ((index != -1) && (level > 0)) { + index = level - 1; + } + if (index != -1) { + final String fieldStringValue = this.parentWC3Object + .getField(getEditorMetaDataDisplayKey(level, metaData), index); + return fieldStringValue; + } + final String fieldStringValue = this.parentWC3Object.getField(getEditorMetaDataDisplayKey(level, metaData)); + return fieldStringValue; + } + + public int getFieldAsInteger(final War3ID field, final int level) { + final Change matchingChange = getMatchingChange(field, level); + if (matchingChange != null) { + if (matchingChange.getVartype() != War3ObjectDataChangeset.VAR_TYPE_INT) { + throw new IllegalStateException( + "Requested integer value of '" + field + "' from '" + this.parentWC3Object.getId() + + "', but this field was not an int! vartype=" + matchingChange.getVartype()); + } + return matchingChange.getLongval(); + } + // no luck with custom data, look at the standard data + try { + return Integer.parseInt(getFieldStringFromSLKs(field, level)); + } + catch (final NumberFormatException e) { + return 0; + } + } + + public boolean getFieldAsBoolean(final War3ID field, final int level) { + final Change matchingChange = getMatchingChange(field, level); + if (matchingChange != null) { + if (matchingChange.getVartype() != War3ObjectDataChangeset.VAR_TYPE_BOOLEAN) { + if (matchingChange.getVartype() == War3ObjectDataChangeset.VAR_TYPE_INT) { + return matchingChange.getLongval() == 1; + } + else { + throw new IllegalStateException( + "Requested boolean value of '" + field + "' from '" + this.parentWC3Object.getId() + + "', but this field was not a bool! vartype=" + matchingChange.getVartype()); + } + } + return matchingChange.isBoolval(); + } + // no luck with custom data, look at the standard data + try { + return Integer.parseInt(getFieldStringFromSLKs(field, level)) == 1; + } + catch (final NumberFormatException e) { + return false; + } + } + + public float getFieldAsFloat(final War3ID field, final int level) { + final Change matchingChange = getMatchingChange(field, level); + if (matchingChange != null) { + if ((matchingChange.getVartype() != War3ObjectDataChangeset.VAR_TYPE_REAL) + && (matchingChange.getVartype() != War3ObjectDataChangeset.VAR_TYPE_UNREAL)) { + throw new IllegalStateException( + "Requested float value of '" + field + "' from '" + this.parentWC3Object.getId() + + "', but this field was not a float! vartype=" + matchingChange.getVartype()); + } + return matchingChange.getRealval(); + } + // no luck with custom data, look at the standard data + try { + return Float.parseFloat(getFieldStringFromSLKs(field, level)); + } + catch (final NumberFormatException e) { + return 0; + } + } + + public War3ID getAlias() { + if (this.customUnitData == null) { + return War3ID.fromString(this.parentWC3Object.getId()); + } + if (War3ID.NONE.equals(this.customUnitData.getNewId())) { + return this.customUnitData.getOldId(); + } + return this.customUnitData.getNewId(); + } + + public War3ID getCode() { + if (this.customUnitData == null) { + if ((MutableObjectData.this.worldEditorDataType == WorldEditorDataType.ABILITIES) + || (MutableObjectData.this.worldEditorDataType == WorldEditorDataType.BUFFS_EFFECTS)) { + return War3ID.fromString(this.parentWC3Object.getField("code")); + } + else { + return War3ID.fromString(this.parentWC3Object.getId()); + } + } + if (War3ID.NONE.equals(this.customUnitData.getNewId())) { + if ((MutableObjectData.this.worldEditorDataType == WorldEditorDataType.ABILITIES) + || (MutableObjectData.this.worldEditorDataType == WorldEditorDataType.BUFFS_EFFECTS)) { + return War3ID.fromString(this.parentWC3Object.getField("code")); + } + else { + return this.customUnitData.getOldId(); + } + } + return this.customUnitData.getOldId(); + } + + } + + private static int asInt(final String text) { + if ("#VALUE!".equals(text)) { + return 0; + } + return text == null ? 0 + : "".equals(text) ? 0 : "-".equals(text) ? 0 : "_".equals(text) ? 0 : Integer.parseInt(text); + } + + private static float asFloat(final String text) { + return text == null ? 0 + : "".equals(text) ? 0 : "-".equals(text) ? 0 : "_".equals(text) ? 0 : Float.parseFloat(text); + } + + public enum WorldEditorDataType { + UNITS("w3u"), + ITEM("w3t"), + DESTRUCTIBLES("w3b"), + DOODADS("w3d"), + ABILITIES("w3a"), + BUFFS_EFFECTS("w3h"), + UPGRADES("w3q"); + + private String extension; + + private WorldEditorDataType(final String extension) { + this.extension = extension; + } + + public String getExtension() { + return this.extension; + } + } + + public static String getEditorMetaDataDisplayKey(int level, final GameObject metaData) { + final int index = metaData.getFieldValue("index"); + String metaDataName = metaData.getField("field"); + final int repeatCount = metaData.getFieldValue("repeat"); + final String upgradeHack = metaData.getField("appendIndex"); + final boolean repeats = (repeatCount > 0) && !"0".equals(upgradeHack); + final int data = metaData.getFieldValue("data"); + if (data > 0) { + metaDataName += (char) ('A' + (data - 1)); + } + if ("1".equals(upgradeHack)) { + final int upgradeExtensionLevel = level - 1; + if (upgradeExtensionLevel > 0) { + metaDataName += Integer.toString(upgradeExtensionLevel); + } + } + else if (repeats && (index == -1)) { + if (level == 0) { + level = 1; + } + if (repeatCount >= 10) { + metaDataName += String.format("%2d", level).replace(' ', '0'); + } + else { + metaDataName += Integer.toString(level); + } + } + return metaDataName; + } + + public static String getDisplayAsRawDataName(final MutableGameObject gameObject) { + String aliasString = gameObject.getAlias().toString(); + if (!gameObject.getAlias().equals(gameObject.getCode())) { + aliasString += ":" + gameObject.getCode().toString(); + } + return aliasString + " (" + gameObject.getName() + ")"; + } +} diff --git a/core/src/com/etheller/warsmash/units/manager/MutableObjectDataChangeListener.java b/core/src/com/etheller/warsmash/units/manager/MutableObjectDataChangeListener.java new file mode 100644 index 0000000..359863a --- /dev/null +++ b/core/src/com/etheller/warsmash/units/manager/MutableObjectDataChangeListener.java @@ -0,0 +1,23 @@ +package com.etheller.warsmash.units.manager; + +import com.etheller.warsmash.util.War3ID; + +public interface MutableObjectDataChangeListener { + void textChanged(War3ID changedObject); + + void iconsChanged(War3ID changedObject); + + void categoriesChanged(War3ID changedObject); + + void fieldsChanged(War3ID changedObject); + + void modelChanged(War3ID changedObject); + + void objectCreated(War3ID newObject); + + void objectsCreated(War3ID[] newObject); + + void objectRemoved(War3ID removedObject); + + void objectsRemoved(War3ID[] removedObject); +} diff --git a/core/src/com/etheller/warsmash/units/manager/MutableObjectDataChangeNotifier.java b/core/src/com/etheller/warsmash/units/manager/MutableObjectDataChangeNotifier.java new file mode 100644 index 0000000..1ea3dde --- /dev/null +++ b/core/src/com/etheller/warsmash/units/manager/MutableObjectDataChangeNotifier.java @@ -0,0 +1,72 @@ +package com.etheller.warsmash.units.manager; + +import com.etheller.warsmash.util.SubscriberSetNotifier; +import com.etheller.warsmash.util.War3ID; + +public final class MutableObjectDataChangeNotifier extends SubscriberSetNotifier + implements MutableObjectDataChangeListener { + + @Override + public void textChanged(final War3ID changedObject) { + for (final MutableObjectDataChangeListener listener : this.set) { + listener.textChanged(changedObject); + } + } + + @Override + public void categoriesChanged(final War3ID changedObject) { + for (final MutableObjectDataChangeListener listener : this.set) { + listener.categoriesChanged(changedObject); + } + } + + @Override + public void iconsChanged(final War3ID changedObject) { + for (final MutableObjectDataChangeListener listener : this.set) { + listener.iconsChanged(changedObject); + } + } + + @Override + public void fieldsChanged(final War3ID changedObject) { + for (final MutableObjectDataChangeListener listener : this.set) { + listener.fieldsChanged(changedObject); + } + } + + @Override + public void modelChanged(final War3ID changedObject) { + for (final MutableObjectDataChangeListener listener : this.set) { + listener.modelChanged(changedObject); + } + } + + @Override + public void objectCreated(final War3ID newObject) { + for (final MutableObjectDataChangeListener listener : this.set) { + listener.objectCreated(newObject); + } + } + + @Override + public void objectsCreated(final War3ID[] newObject) { + for (final MutableObjectDataChangeListener listener : this.set) { + listener.objectsCreated(newObject); + } + } + + @Override + public void objectRemoved(final War3ID newObject) { + for (final MutableObjectDataChangeListener listener : this.set) { + listener.objectRemoved(newObject); + } + } + + @Override + public void objectsRemoved(final War3ID[] newObject) { + for (final MutableObjectDataChangeListener listener : this.set) { + listener.objectsRemoved(newObject); + } + } + +} diff --git a/core/src/com/etheller/warsmash/util/DataSourceFileHandle.java b/core/src/com/etheller/warsmash/util/DataSourceFileHandle.java new file mode 100644 index 0000000..4bc4a5d --- /dev/null +++ b/core/src/com/etheller/warsmash/util/DataSourceFileHandle.java @@ -0,0 +1,41 @@ +package com.etheller.warsmash.util; + +import java.io.IOException; +import java.io.InputStream; + +import com.badlogic.gdx.files.FileHandle; +import com.etheller.warsmash.datasources.DataSource; + +public class DataSourceFileHandle extends FileHandle { + private final DataSource dataSource; + + public DataSourceFileHandle(final DataSource dataSource, final String path) { + super(fixPath(dataSource, path)); + this.dataSource = dataSource; + } + + @Override + public String path() { + return file().getPath(); + } + + @Override + public InputStream read() { + try { + return this.dataSource.getResourceAsStream(path()); + } + catch (final IOException e) { + throw new RuntimeException("Failed to load FileHandle from DataSource: " + path()); + } + } + + private static String fixPath(final DataSource dataSource, String path) { + if (!dataSource.has(path) && (path.toLowerCase().endsWith(".wav") || path.toLowerCase().endsWith(".mp3"))) { + final String otherPossiblePath = path.substring(0, path.lastIndexOf('.')) + ".flac"; + if (dataSource.has(otherPossiblePath)) { + path = otherPossiblePath; + } + } + return path; + } +} diff --git a/core/src/com/etheller/warsmash/util/Descriptor.java b/core/src/com/etheller/warsmash/util/Descriptor.java new file mode 100644 index 0000000..ff608fe --- /dev/null +++ b/core/src/com/etheller/warsmash/util/Descriptor.java @@ -0,0 +1,6 @@ +package com.etheller.warsmash.util; + +public interface Descriptor { + E create(); + +} diff --git a/core/src/com/etheller/warsmash/util/FastNumberFormat.java b/core/src/com/etheller/warsmash/util/FastNumberFormat.java new file mode 100644 index 0000000..8741b4d --- /dev/null +++ b/core/src/com/etheller/warsmash/util/FastNumberFormat.java @@ -0,0 +1,24 @@ +package com.etheller.warsmash.util; + +public class FastNumberFormat { + private static final StringBuilder RECYCLE_STRING_BUILDER = new StringBuilder(); + + public static String formatWholeNumber(final float value) { + int intValue = (int) value; + RECYCLE_STRING_BUILDER.setLength(0); + do { + RECYCLE_STRING_BUILDER.append(intValue % 10); + intValue /= 10; + } + while (intValue > 0); + final int len = RECYCLE_STRING_BUILDER.length(); + final int halfLength = len / 2; + for (int i = 0; i < halfLength; i++) { + final char swapCharA = RECYCLE_STRING_BUILDER.charAt(i); + final char swapCharB = RECYCLE_STRING_BUILDER.charAt(len - 1 - i); + RECYCLE_STRING_BUILDER.setCharAt(len - 1 - i, swapCharA); + RECYCLE_STRING_BUILDER.setCharAt(i, swapCharB); + } + return RECYCLE_STRING_BUILDER.toString(); + } +} diff --git a/core/src/com/etheller/warsmash/util/FixedIntersector.java b/core/src/com/etheller/warsmash/util/FixedIntersector.java new file mode 100644 index 0000000..0ecbcd8 --- /dev/null +++ b/core/src/com/etheller/warsmash/util/FixedIntersector.java @@ -0,0 +1,82 @@ +package com.etheller.warsmash.util; + +import com.badlogic.gdx.math.Intersector; +import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.math.Plane; +import com.badlogic.gdx.math.Plane.PlaneSide; +import com.badlogic.gdx.math.Vector3; +import com.badlogic.gdx.math.collision.Ray; + +public class FixedIntersector { + + private final static Vector3 v0 = new Vector3(); + private final static Vector3 v1 = new Vector3(); + private final static Vector3 v2 = new Vector3(); + + private static final Plane p = new Plane(new Vector3(), 0); + private static final Vector3 i = new Vector3(); + + /** + * Intersect a {@link Ray} and a triangle, returning the intersection point in + * intersection. + * + * @param ray The ray + * @param t1 The first vertex of the triangle + * @param t2 The second vertex of the triangle + * @param t3 The third vertex of the triangle + * @param intersection The intersection point (optional) + * @return True in case an intersection is present. + */ + public static boolean intersectRayTriangle(final Ray ray, final Vector3 t1, final Vector3 t2, final Vector3 t3, + final Vector3 intersection) { + if (t2.epsilonEquals(t3)) { + return false; + } + final Vector3 edge1 = v0.set(t2).sub(t1); + final Vector3 edge2 = v1.set(t3).sub(t1); + + final Vector3 pvec = v2.set(ray.direction).crs(edge2); + float det = edge1.dot(pvec); + if (MathUtils.isZero(det)) { + p.set(t1, t2, t3); + if ((p.testPoint(ray.origin) == PlaneSide.OnPlane) + && Intersector.isPointInTriangle(ray.origin, t1, t2, t3)) { + if (intersection != null) { + intersection.set(ray.origin); + } + return true; + } + return false; + } + + det = 1.0f / det; + + final Vector3 tvec = i.set(ray.origin).sub(t1); + final float u = tvec.dot(pvec) * det; + if ((u < 0.0f) || (u > 1.0f)) { + return false; + } + + final Vector3 qvec = tvec.crs(edge1); + final float v = ray.direction.dot(qvec) * det; + if ((v < 0.0f) || ((u + v) > 1.0f)) { + return false; + } + + final float t = edge2.dot(qvec) * det; + if (t < 0) { + return false; + } + + if (intersection != null) { + if (t <= MathUtils.FLOAT_ROUNDING_ERROR) { + intersection.set(ray.origin); + } + else { + ray.getEndPoint(intersection, t); + } + } + + return true; + } +} diff --git a/core/src/com/etheller/warsmash/util/ImageUtils.java b/core/src/com/etheller/warsmash/util/ImageUtils.java new file mode 100644 index 0000000..80358b1 --- /dev/null +++ b/core/src/com/etheller/warsmash/util/ImageUtils.java @@ -0,0 +1,255 @@ +package com.etheller.warsmash.util; + +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; +import java.io.IOException; +import java.io.InputStream; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import javax.imageio.ImageIO; + +import com.badlogic.gdx.graphics.GL30; +import com.badlogic.gdx.graphics.Pixmap; +import com.badlogic.gdx.graphics.Pixmap.Format; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.Texture.TextureFilter; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.viewer5.handlers.tga.TgaFile; + +/** + * Uses AWT stuff + * + */ +public final class ImageUtils { + private static final int BYTES_PER_PIXEL = 4; + public static final String DEFAULT_ICON_PATH = "ReplaceableTextures\\CommandButtons\\BTNTemp.blp"; + + public static Texture getAnyExtensionTexture(final DataSource dataSource, final String path) { + BufferedImage image; + try { + final AnyExtensionImage imageInfo = getAnyExtensionImageFixRGB(dataSource, path, "texture"); + image = imageInfo.getImageData(); + if (image != null) { + return ImageUtils.getTexture(image, imageInfo.isNeedsSRGBFix()); + } + } + catch (final IOException e) { + return null; + } + return null; + } + + public static AnyExtensionImage getAnyExtensionImageFixRGB(final DataSource dataSource, final String path, + final String errorType) throws IOException { + if (path.toLowerCase().endsWith(".blp")) { + try (InputStream stream = dataSource.getResourceAsStream(path)) { + if (stream == null) { + final String tgaPath = path.substring(0, path.length() - 4) + ".tga"; + try (final InputStream tgaStream = dataSource.getResourceAsStream(tgaPath)) { + if (tgaStream != null) { + final BufferedImage tgaData = TgaFile.readTGA(tgaPath, tgaStream); + return new AnyExtensionImage(false, tgaData); + } + else { + final String ddsPath = path.substring(0, path.length() - 4) + ".dds"; + try (final InputStream ddsStream = dataSource.getResourceAsStream(ddsPath)) { + if (ddsStream != null) { + final BufferedImage image = ImageIO.read(ddsStream); + return new AnyExtensionImage(false, image); + } + else { + throw new IllegalStateException("Missing " + errorType + ": " + path); + } + } + } + } + } + else { + final BufferedImage image = ImageIO.read(stream); + return new AnyExtensionImage(true, image); + } + } + } + else { + throw new IllegalStateException("Missing " + errorType + ": " + path); + } + } + + public static final class AnyExtensionImage { + private final boolean needsSRGBFix; + private final BufferedImage imageData; + + public AnyExtensionImage(final boolean needsSRGBFix, final BufferedImage imageData) { + this.needsSRGBFix = needsSRGBFix; + this.imageData = imageData; + } + + public BufferedImage getImageData() { + return this.imageData; + } + + public BufferedImage getRGBCorrectImageData() { + return this.needsSRGBFix ? forceBufferedImagesRGB(this.imageData) : this.imageData; + } + + public boolean isNeedsSRGBFix() { + return this.needsSRGBFix; + } + } + + public static BufferedImage getBLPImage(final DataSource dataSource, final String path) { + try { + try (final InputStream resourceAsStream = dataSource.getResourceAsStream(path)) { + if ((resourceAsStream == null) || path.endsWith(".tga")) { + final String tgaPath = path.substring(0, path.length() - 4) + ".tga"; + try (final InputStream tgaStream = dataSource.getResourceAsStream(tgaPath)) { + if (tgaStream == null) { + throw new IllegalStateException("missing resource: " + path); + } + else { + return TgaFile.readTGA(tgaPath, tgaStream); + } + } + } + final BufferedImage image = ImageIO.read(resourceAsStream); + if (image == null) { + throw new IllegalStateException("corrupt resource: " + path); + } + return image; + } + + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + + public static Texture getTexture(final BufferedImage image, final boolean sRGBFix) { + final int[] pixels = new int[image.getWidth() * image.getHeight()]; + image.getRGB(0, 0, image.getWidth(), image.getHeight(), pixels, 0, image.getWidth()); + + // 4 + // for + // RGBA, + // 3 + // for + // RGB + + final Pixmap pixmap = sRGBFix ? new Pixmap(image.getWidth(), image.getHeight(), Format.RGBA8888) { + @Override + public int getGLInternalFormat() { + return GL30.GL_SRGB8_ALPHA8; + } + } : new Pixmap(image.getWidth(), image.getHeight(), Format.RGBA8888); + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + final int pixel = pixels[(y * image.getWidth()) + x]; + pixmap.drawPixel(x, y, (pixel << 8) | (pixel >>> 24)); + } + } + final Texture texture = new Texture(pixmap); + texture.setFilter(TextureFilter.Linear, TextureFilter.Linear); + return texture; + } + + public static Texture getTextureNoColorCorrection(final BufferedImage image) { + final int[] pixels = new int[image.getWidth() * image.getHeight()]; + image.getRGB(0, 0, image.getWidth(), image.getHeight(), pixels, 0, image.getWidth()); + + // 4 + // for + // RGBA, + // 3 + // for + // RGB + + final Pixmap pixmap = new Pixmap(image.getWidth(), image.getHeight(), Format.RGBA8888); + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + final int pixel = pixels[(y * image.getWidth()) + x]; + pixmap.drawPixel(x, y, (pixel << 8) | (pixel >>> 24)); + } + } + final Texture texture = new Texture(pixmap); + texture.setFilter(TextureFilter.Linear, TextureFilter.Linear); + return texture; + } + + public static Buffer getTextureBuffer(final BufferedImage image) { + + final int imageWidth = image.getWidth(); + final int imageHeight = image.getHeight(); + final int[] pixels = new int[imageWidth * imageHeight]; + image.getRGB(0, 0, imageWidth, imageHeight, pixels, 0, imageWidth); + + final ByteBuffer buffer = ByteBuffer.allocateDirect(imageWidth * imageHeight * BYTES_PER_PIXEL) + .order(ByteOrder.nativeOrder()); + // 4 + // for + // RGBA, + // 3 + // for + // RGB + + for (int y = 0; y < imageHeight; y++) { + for (int x = 0; x < imageWidth; x++) { + final int pixel = pixels[(y * imageWidth) + x]; + buffer.put((byte) ((pixel >> 16) & 0xFF)); // Red component + buffer.put((byte) ((pixel >> 8) & 0xFF)); // Green component + buffer.put((byte) (pixel & 0xFF)); // Blue component + buffer.put((byte) ((pixel >> 24) & 0xFF)); // Alpha component. + // Only for RGBA + } + } + + buffer.flip(); + return buffer; + } + + /** + * Convert an input buffered image into sRGB color space using component values + * directly instead of performing a color space conversion. + * + * @param in Input image to be converted. + * @return Resulting sRGB image. + */ + public static BufferedImage forceBufferedImagesRGB(final BufferedImage in) { + // Resolve input ColorSpace. + final ColorSpace inCS = in.getColorModel().getColorSpace(); + final ColorSpace sRGBCS = ColorSpace.getInstance(ColorSpace.CS_sRGB); + if (inCS == sRGBCS) { + // Already is sRGB. + return in; + } + if (inCS.getNumComponents() != sRGBCS.getNumComponents()) { + throw new IllegalArgumentException("Input color space has different number of components from sRGB."); + } + + // Draw input. + final ColorModel lRGBModel = new ComponentColorModel(inCS, true, false, Transparency.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + final ColorModel sRGBModel = new ComponentColorModel(sRGBCS, true, false, Transparency.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + final BufferedImage lRGB = new BufferedImage(lRGBModel, + lRGBModel.createCompatibleWritableRaster(in.getWidth(), in.getHeight()), false, null); + for (int i = 0; i < in.getWidth(); i++) { + for (int j = 0; j < in.getHeight(); j++) { + lRGB.setRGB(i, j, in.getRGB(i, j)); + } + } + + // Convert to sRGB. + final BufferedImage sRGB = new BufferedImage(sRGBModel, lRGB.getRaster(), false, null); + + return sRGB; + } + + private ImageUtils() { + } +} diff --git a/core/src/com/etheller/warsmash/util/IniFile.java b/core/src/com/etheller/warsmash/util/IniFile.java new file mode 100644 index 0000000..5c6fcff --- /dev/null +++ b/core/src/com/etheller/warsmash/util/IniFile.java @@ -0,0 +1,84 @@ +package com.etheller.warsmash.util; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class IniFile { + private static final Pattern NAME_PATTERN = Pattern.compile("^\\[(.+?)\\].*"); + private static final Pattern DATA_PATTERN = Pattern.compile("^(.+?)=(.*?)$"); + public final Map properties = new HashMap<>(); + public final Map> sections = new HashMap<>(); + + public IniFile(final String buffer) { + if (buffer != null) { + this.load(buffer); + } + } + + public void load(final String buffer) { + // All properties added until a section is reached are added to the properties + // map. + // Once a section is reached, any further properties will be added to it until + // matching another section, etc. + Map section = this.properties; + + // Below: using \n instead of \r\n because its not reading directly from the + // actual file, but instead from a Java translated thing + for (final String line : buffer.split("\n")) { + // INI defines comments as starting with a semicolon ';'. + // However, Warcraft 3 INI files use normal C comments '//'. + // In addition, Warcraft 3 files have empty lines. + // Therefore, ignore any line matching any of these conditions. + + if ((line.length() != 0) && !line.startsWith("//") && !line.startsWith(";")) { + final Matcher matcher = NAME_PATTERN.matcher(line); + + if (matcher.matches()) { + final String name = matcher.group(1).trim().toLowerCase(); + + section = this.sections.get(name); + + if (section == null) { + section = new HashMap<>(); + + this.sections.put(name, section); + } + } + else { + final Matcher dataMatcher = DATA_PATTERN.matcher(line); + if (dataMatcher.matches()) { + section.put(dataMatcher.group(1).toLowerCase(), dataMatcher.group(2)); + } + } + } + + } + + } + + public String save() { + final List lines = new ArrayList<>(); + + for (final Map.Entry entry : this.properties.entrySet()) { + lines.add(entry.getKey() + "=" + entry.getValue()); + } + + for (final Map.Entry> sectionData : this.sections.entrySet()) { + lines.add("[" + sectionData.getKey() + "]"); + + for (final Map.Entry entry : sectionData.getValue().entrySet()) { + lines.add(entry.getKey() + "=" + entry.getValue()); + } + } + + return String.join("\r\n", lines); + } + + public Map getSection(final String name) { + return this.sections.get(name.toLowerCase()); + } +} diff --git a/core/src/com/etheller/warsmash/util/Interpolator.java b/core/src/com/etheller/warsmash/util/Interpolator.java new file mode 100644 index 0000000..ab6dd42 --- /dev/null +++ b/core/src/com/etheller/warsmash/util/Interpolator.java @@ -0,0 +1,72 @@ +package com.etheller.warsmash.util; + +public class Interpolator { + public static void interpolateScalar(final float[] out, final float[] a, final float[] b, final float[] c, + final float[] d, final float t, final int type) { + switch (type) { + case 0: { + out[0] = a[0]; + break; + } + case 1: { + out[0] = RenderMathUtils.lerp(a[0], d[0], t); + break; + } + case 2: { + out[0] = RenderMathUtils.hermite(a[0], b[0], c[0], d[0], t); + break; + } + case 3: { + out[0] = RenderMathUtils.bezier(a[0], b[0], c[0], d[0], t); + break; + } + } + } + + public static void interpolateVector(final float[] out, final float[] a, final float[] b, final float[] c, + final float[] d, final float t, final int type) { + switch (type) { + case 0: { + System.arraycopy(a, 0, out, 0, a.length); + break; + } + case 1: { + out[0] = RenderMathUtils.lerp(a[0], d[0], t); + out[1] = RenderMathUtils.lerp(a[1], d[1], t); + out[2] = RenderMathUtils.lerp(a[2], d[2], t); + break; + } + case 2: { + out[0] = RenderMathUtils.hermite(a[0], b[0], c[0], d[0], t); + out[1] = RenderMathUtils.hermite(a[1], b[1], c[1], d[1], t); + out[2] = RenderMathUtils.hermite(a[2], b[2], c[2], d[2], t); + break; + } + case 3: { + out[0] = RenderMathUtils.bezier(a[0], b[0], c[0], d[0], t); + out[1] = RenderMathUtils.bezier(a[1], b[1], c[1], d[1], t); + out[2] = RenderMathUtils.bezier(a[2], b[2], c[2], d[2], t); + break; + } + } + } + + public static void interpolateQuaternion(final float[] out, final float[] a, final float[] b, final float[] c, + final float[] d, final float t, final int type) { + switch (type) { + case 0: { + System.arraycopy(a, 0, out, 0, a.length); + break; + } + case 1: { + RenderMathUtils.slerp(out, a, d, t); + break; + } + case 2: + case 3: { + RenderMathUtils.sqlerp(out, a, b, c, d, t); + break; + } + } + } +} diff --git a/core/src/com/etheller/warsmash/util/MappedData.java b/core/src/com/etheller/warsmash/util/MappedData.java new file mode 100644 index 0000000..ed92769 --- /dev/null +++ b/core/src/com/etheller/warsmash/util/MappedData.java @@ -0,0 +1,97 @@ +package com.etheller.warsmash.util; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A structure that holds mapped data from INI and SLK files. + * + * In the case of SLK files, the first row is expected to hold the names of the + * columns. + */ +public class MappedData { + private final Map map = new HashMap<>(); + + public MappedData() { + this(null); + } + + public MappedData(final String buffer) { + if (buffer != null) { + this.load(buffer); + } + } + + /** + * Load data from an SLK file or an INI file. + * + * Note that this may override previous properties! + */ + public void load(final String buffer) { + if (buffer.startsWith("ID;")) { + final SlkFile file = new SlkFile(buffer); + final List> rows = file.rows; + final List header = rows.get(0); + + for (int i = 1, l = rows.size(); i < l; i++) { + final List row = rows.get(i); + if (row != null) { + String name = (String) row.get(0); + + if (name != null) { + name = name.toLowerCase(); + + if (!this.map.containsKey(name)) { + this.map.put(name, new MappedDataRow()); + } + + final MappedDataRow mapped = this.map.get(name); + + for (int j = 0, k = header.size(); j < k; j++) { + final Object headerObj = header.get(j); + String key = headerObj == null ? null : headerObj.toString(); + + // UnitBalance.slk doesn't define the name of one row. + if (key == null) { + key = "column" + j; + } + + mapped.put(key, j < row.size() ? row.get(j) : null); + } + } + } + } + } + else { + final IniFile file = new IniFile(buffer); + final Map> sections = file.sections; + + for (final Map.Entry> rowAndProperties : sections.entrySet()) { + final String row = rowAndProperties.getKey(); + + if (!this.map.containsKey(row)) { + this.map.put(row, new MappedDataRow()); + } + + final MappedDataRow mapped = this.map.get(row); + + for (final Map.Entry nameAndProperty : rowAndProperties.getValue().entrySet()) { + mapped.put(nameAndProperty.getKey(), nameAndProperty.getValue()); + } + } + } + } + + public MappedDataRow getRow(final String key) { + return this.map.get(key.toLowerCase()); + } + + public Object getProperty(final String key, final String name) { + return this.map.get(key.toLowerCase()).get(name); + } + + public void setRow(final String key, final MappedDataRow values) { + this.map.put(key.toLowerCase(), values); + } +} diff --git a/core/src/com/etheller/warsmash/util/MappedDataRow.java b/core/src/com/etheller/warsmash/util/MappedDataRow.java new file mode 100644 index 0000000..f22a20f --- /dev/null +++ b/core/src/com/etheller/warsmash/util/MappedDataRow.java @@ -0,0 +1,7 @@ +package com.etheller.warsmash.util; + +import java.util.HashMap; + +public class MappedDataRow extends HashMap { + +} diff --git a/core/src/com/etheller/warsmash/util/MdlUtils.java b/core/src/com/etheller/warsmash/util/MdlUtils.java new file mode 100644 index 0000000..3e3623f --- /dev/null +++ b/core/src/com/etheller/warsmash/util/MdlUtils.java @@ -0,0 +1,198 @@ +package com.etheller.warsmash.util; + +/** + * Constants for the tokens were used to prevent typos in token literals. It + * would be very easy for me to type "Interval" in one place and "Intreval" in + * another by mistake. With this paradigm, that mistake causes a compile error, + * since TOKEN_INTREVAL does not exist. + */ +public class MdlUtils { + public static final String TOKEN_VERSION = "Version"; + + public static final String TOKEN_MODEL = "Model"; + + public static final String TOKEN_SEQUENCES = "Sequences"; + + public static final String TOKEN_GLOBAL_SEQUENCES = "GlobalSequences"; + + public static final String TOKEN_INTERVAL = "Interval"; + public static final String TOKEN_NONLOOPING = "NonLooping"; + public static final String TOKEN_MOVESPEED = "MoveSpeed"; + public static final String TOKEN_RARITY = "Rarity"; + + public static final String TOKEN_FORMAT_VERSION = "FormatVersion"; + public static final String TOKEN_BLEND_TIME = "BlendTime"; + public static final String TOKEN_DURATION = "Duration"; + + public static final String TOKEN_IMAGE = "Image"; + public static final String TOKEN_WRAP_WIDTH = "WrapWidth"; + public static final String TOKEN_WRAP_HEIGHT = "WrapHeight"; + public static final String TOKEN_BITMAP = "Bitmap"; + + public static final String TOKEN_TVERTEX_ANIM_SPACE = "TVertexAnim "; + public static final String TOKEN_TVERTEX_ANIM = "TVertexAnim"; + + public static final String TOKEN_DONT_INTERP = "DontInterp"; + public static final String TOKEN_LINEAR = "Linear"; + public static final String TOKEN_HERMITE = "Hermite"; + public static final String TOKEN_BEZIER = "Bezier"; + public static final String TOKEN_GLOBAL_SEQ_ID = "GlobalSeqId"; + + public static final String TOKEN_PLANE = "Plane"; + public static final String TOKEN_BOX = "Box"; + public static final String TOKEN_SPHERE = "Sphere"; + public static final String TOKEN_CYLINDER = "Cylinder"; + + public static final String TOKEN_GEOSETID = "GeosetId"; + public static final String TOKEN_MULTIPLE = "Multiple"; + public static final String TOKEN_GEOSETANIMID = "GeosetAnimId"; + public static final String TOKEN_NONE = "None"; + public static final String TOKEN_OBJECTID = "ObjectId"; + public static final String TOKEN_PARENT = "Parent"; + public static final String TOKEN_BILLBOARDED_LOCK_Z = "BillboardedLockZ"; + public static final String TOKEN_BILLBOARDED_LOCK_Y = "BillboardedLockY"; + public static final String TOKEN_BILLBOARDED_LOCK_X = "BillboardedLockX"; + public static final String TOKEN_BILLBOARDED = "Billboarded"; + public static final String TOKEN_CAMERA_ANCHORED = "CameraAnchored"; + public static final String TOKEN_DONT_INHERIT = "DontInherit"; + public static final String TOKEN_ROTATION = "Rotation"; + public static final String TOKEN_TRANSLATION = "Translation"; + public static final String TOKEN_SCALING = "Scaling"; + public static final String TOKEN_STATIC = "static"; + public static final String TOKEN_ATTACHMENT_ID = "AttachmentID"; + public static final String TOKEN_PATH = "Path"; + public static final String TOKEN_VISIBILITY = "Visibility"; + public static final String TOKEN_POSITION = "Position"; + public static final String TOKEN_FIELDOFVIEW = "FieldOfView"; + public static final String TOKEN_FARCLIP = "FarClip"; + public static final String TOKEN_NEARCLIP = "NearClip"; + public static final String TOKEN_TARGET = "Target"; + public static final String TOKEN_VERTICES = "Vertices"; + public static final String TOKEN_BOUNDSRADIUS = "BoundsRadius"; + public static final String TOKEN_EVENT_TRACK = "EventTrack"; + public static final String TOKEN_MAXIMUM_EXTENT = "MaximumExtent"; + public static final String TOKEN_MINIMUM_EXTENT = "MinimumExtent"; + public static final String TOKEN_NORMALS = "Normals"; + public static final String TOKEN_TVERTICES = "TVertices"; + public static final String TOKEN_VERTEX_GROUP = "VertexGroup"; + public static final String TOKEN_FACES = "Faces"; + public static final String TOKEN_GROUPS = "Groups"; + public static final String TOKEN_ANIM = "Anim"; + public static final String TOKEN_MATERIAL_ID = "MaterialID"; + public static final String TOKEN_SELECTION_GROUP = "SelectionGroup"; + public static final String TOKEN_UNSELECTABLE = "Unselectable"; + public static final String TOKEN_TRIANGLES = "Triangles"; + public static final String TOKEN_MATRICES = "Matrices"; + public static final String TOKEN_DROP_SHADOW = "DropShadow"; + public static final String TOKEN_ALPHA = "Alpha"; + public static final String TOKEN_COLOR = "Color"; + public static final String TOKEN_STATIC_ALPHA = TOKEN_STATIC + " " + TOKEN_ALPHA; + public static final String TOKEN_STATIC_COLOR = TOKEN_STATIC + " " + TOKEN_COLOR; + public static final String TOKEN_FILTER_MODE = "FilterMode"; + public static final String TOKEN_UNSHADED = "Unshaded"; + public static final String TOKEN_SPHERE_ENV_MAP = "SphereEnvMap"; + public static final String TOKEN_TWO_SIDED = "TwoSided"; + public static final String TOKEN_UNFOGGED = "Unfogged"; + public static final String TOKEN_NO_DEPTH_TEST = "NoDepthTest"; + public static final String TOKEN_NO_DEPTH_SET = "NoDepthSet"; + public static final String TOKEN_TEXTURE_ID = "TextureID"; + public static final String TOKEN_STATIC_TEXTURE_ID = TOKEN_STATIC + " " + TOKEN_TEXTURE_ID; + public static final String TOKEN_TVERTEX_ANIM_ID = "TVertexAnimId"; + public static final String TOKEN_COORD_ID = "CoordId"; + + public static final String TOKEN_OMNIDIRECTIONAL = "Omnidirectional"; + public static final String TOKEN_DIRECTIONAL = "Directional"; + public static final String TOKEN_AMBIENT = "Ambient"; + public static final String TOKEN_ATTENUATION_START = "AttenuationStart"; + public static final String TOKEN_STATIC_ATTENUATION_START = TOKEN_STATIC + " " + TOKEN_ATTENUATION_START; + public static final String TOKEN_ATTENUATION_END = "AttenuationEnd"; + public static final String TOKEN_STATIC_ATTENUATION_END = TOKEN_STATIC + " " + TOKEN_ATTENUATION_END; + public static final String TOKEN_INTENSITY = "Intensity"; + public static final String TOKEN_STATIC_INTENSITY = TOKEN_STATIC + " " + TOKEN_INTENSITY; + public static final String TOKEN_AMB_INTENSITY = "AmbIntensity"; + public static final String TOKEN_STATIC_AMB_INTENSITY = TOKEN_STATIC + " " + TOKEN_AMB_INTENSITY; + public static final String TOKEN_AMB_COLOR = "AmbColor"; + public static final String TOKEN_STATIC_AMB_COLOR = TOKEN_STATIC + " " + TOKEN_AMB_COLOR; + + public static final String TOKEN_CONSTANT_COLOR = "ConstantColor"; + public static final String TOKEN_SORT_PRIMS_NEAR_Z = "SortPrimsNearZ"; + public static final String TOKEN_SORT_PRIMS_FAR_Z = "SortPrimsFarZ"; + public static final String TOKEN_FULL_RESOLUTION = "FullResolution"; + public static final String TOKEN_PRIORITY_PLANE = "PriorityPlane"; + + public static final String TOKEN_EMITTER_USES_MDL = "EmitterUsesMDL"; + public static final String TOKEN_EMITTER_USES_TGA = "EmitterUsesTGA"; + public static final String TOKEN_EMISSION_RATE = "EmissionRate"; + public static final String TOKEN_STATIC_EMISSION_RATE = TOKEN_STATIC + " " + TOKEN_EMISSION_RATE; + public static final String TOKEN_GRAVITY = "Gravity"; + public static final String TOKEN_STATIC_GRAVITY = TOKEN_STATIC + " " + TOKEN_GRAVITY; + public static final String TOKEN_LONGITUDE = "Longitude"; + public static final String TOKEN_STATIC_LONGITUDE = TOKEN_STATIC + " " + TOKEN_LONGITUDE; + public static final String TOKEN_LATITUDE = "Latitude"; + public static final String TOKEN_STATIC_LATITUDE = TOKEN_STATIC + " " + TOKEN_LATITUDE; + public static final String TOKEN_PARTICLE = "Particle"; + public static final String TOKEN_LIFE_SPAN = "LifeSpan"; + public static final String TOKEN_STATIC_LIFE_SPAN = TOKEN_STATIC + " " + TOKEN_LIFE_SPAN; + public static final String TOKEN_INIT_VELOCITY = "InitVelocity"; + public static final String TOKEN_STATIC_INIT_VELOCITY = TOKEN_STATIC + " " + TOKEN_INIT_VELOCITY; + + public static final String TOKEN_LINE_EMITTER = "LineEmitter"; + public static final String TOKEN_MODEL_SPACE = "ModelSpace"; + public static final String TOKEN_XY_QUAD = "XYQuad"; + public static final String TOKEN_SPEED = "Speed"; + public static final String TOKEN_STATIC_SPEED = TOKEN_STATIC + " " + TOKEN_SPEED; + public static final String TOKEN_VARIATION = "Variation"; + public static final String TOKEN_STATIC_VARIATION = TOKEN_STATIC + " " + TOKEN_VARIATION; + public static final String TOKEN_SQUIRT = "Squirt"; + public static final String TOKEN_WIDTH = "Width"; + public static final String TOKEN_STATIC_WIDTH = TOKEN_STATIC + " " + TOKEN_WIDTH; + public static final String TOKEN_LENGTH = "Length"; + public static final String TOKEN_STATIC_LENGTH = TOKEN_STATIC + " " + TOKEN_LENGTH; + public static final String TOKEN_ROWS = "Rows"; + public static final String TOKEN_COLUMNS = "Columns"; + public static final String TOKEN_HEAD = "Head"; + public static final String TOKEN_TAIL = "Tail"; + public static final String TOKEN_BOTH = "Both"; + public static final String TOKEN_TAIL_LENGTH = "TailLength"; + public static final String TOKEN_TIME = "Time"; + public static final String TOKEN_SEGMENT_COLOR = "SegmentColor"; + public static final String TOKEN_PARTICLE_SCALING = "ParticleScaling"; + public static final String TOKEN_LIFE_SPAN_UV_ANIM = "LifeSpanUVAnim"; + public static final String TOKEN_DECAY_UV_ANIM = "DecayUVAnim"; + public static final String TOKEN_TAIL_UV_ANIM = "TailUVAnim"; + public static final String TOKEN_TAIL_DECAY_UV_ANIM = "TailDecayUVAnim"; + public static final String TOKEN_REPLACEABLE_ID = "ReplaceableId"; + public static final String TOKEN_BLEND = "Blend";// ParticleEmitter2.FilterMode.BLEND.getMdlText(); + public static final String TOKEN_ADDITIVE = "Additive";// ParticleEmitter2.FilterMode.ADDITIVE.getMdlText(); + public static final String TOKEN_MODULATE = "Modulate";// ParticleEmitter2.FilterMode.MODULATE.getMdlText(); + public static final String TOKEN_MODULATE2X = "Modulate2x";// ParticleEmitter2.FilterMode.MODULATE2X.getMdlText(); + public static final String TOKEN_ALPHAKEY = "AlphaKey";// ParticleEmitter2.FilterMode.ALPHAKEY.getMdlText(); + + public static final String TOKEN_HEIGHT_ABOVE = "HeightAbove"; + public static final String TOKEN_STATIC_HEIGHT_ABOVE = TOKEN_STATIC + " " + TOKEN_HEIGHT_ABOVE; + public static final String TOKEN_HEIGHT_BELOW = "HeightBelow"; + public static final String TOKEN_STATIC_HEIGHT_BELOW = TOKEN_STATIC + " " + TOKEN_HEIGHT_BELOW; + public static final String TOKEN_TEXTURE_SLOT = "TextureSlot"; + public static final String TOKEN_STATIC_TEXTURE_SLOT = TOKEN_STATIC + " " + TOKEN_TEXTURE_SLOT; + + public static final String TOKEN_TEXTURES = "Textures"; + public static final String TOKEN_MATERIALS = "Materials"; + public static final String TOKEN_TEXTURE_ANIMS = "TextureAnims"; + public static final String TOKEN_PIVOT_POINTS = "PivotPoints"; + + public static final String TOKEN_ATTACHMENT = "Attachment"; + public static final String TOKEN_BONE = "Bone"; + public static final String TOKEN_CAMERA = "Camera"; + public static final String TOKEN_COLLISION_SHAPE = "CollisionShape"; + public static final String TOKEN_EVENT_OBJECT = "EventObject"; + public static final String TOKEN_GEOSET = "Geoset"; + public static final String TOKEN_GEOSETANIM = "GeosetAnim"; + public static final String TOKEN_HELPER = "Helper"; + public static final String TOKEN_LAYER = "Layer"; + public static final String TOKEN_LIGHT = "Light"; + public static final String TOKEN_MATERIAL = "Material"; + public static final String TOKEN_PARTICLE_EMITTER = "ParticleEmitter"; + public static final String TOKEN_PARTICLE_EMITTER2 = "ParticleEmitter2"; + public static final String TOKEN_RIBBON_EMITTER = "RibbonEmitter"; + +} diff --git a/core/src/com/etheller/warsmash/util/ParseUtils.java b/core/src/com/etheller/warsmash/util/ParseUtils.java new file mode 100644 index 0000000..89737fd --- /dev/null +++ b/core/src/com/etheller/warsmash/util/ParseUtils.java @@ -0,0 +1,189 @@ +package com.etheller.warsmash.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; + +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class ParseUtils { + public static final Charset UTF8 = Charset.forName("utf-8"); + + public static long readUInt32(final LittleEndianDataInputStream stream) throws IOException { + return stream.readInt() & 0xFFFFFFFFL; + } + + public static int readUInt16(final LittleEndianDataInputStream stream) throws IOException { + return stream.readShort() & 0xFFFF; + } + + public static short readUInt8(final LittleEndianDataInputStream stream) throws IOException { + return (short) (stream.readByte() & (short) 0xFF); + } + + public static void readFloatArray(final LittleEndianDataInputStream stream, final float[] array) + throws IOException { + for (int i = 0; i < array.length; i++) { + array[i] = stream.readFloat(); + } + } + + public static float[] readFloatArray(final LittleEndianDataInputStream stream, final int length) + throws IOException { + final float[] array = new float[length]; + readFloatArray(stream, array); + return array; + } + + public static void readUInt32Array(final LittleEndianDataInputStream stream, final long[] array) + throws IOException { + for (int i = 0; i < array.length; i++) { + array[i] = readUInt32(stream); + } + } + + public static long[] readUInt32Array(final LittleEndianDataInputStream stream, final int length) + throws IOException { + final long[] array = new long[length]; + readUInt32Array(stream, array); + return array; + } + + public static void readInt32Array(final LittleEndianDataInputStream stream, final int[] array) throws IOException { + for (int i = 0; i < array.length; i++) { + array[i] = stream.readInt(); + } + } + + public static int[] readInt32Array(final LittleEndianDataInputStream stream, final int length) throws IOException { + final int[] array = new int[length]; + readInt32Array(stream, array); + return array; + } + + public static void readUInt16Array(final LittleEndianDataInputStream stream, final int[] array) throws IOException { + for (int i = 0; i < array.length; i++) { + array[i] = readUInt16(stream); + } + } + + public static int[] readUInt16Array(final LittleEndianDataInputStream stream, final int length) throws IOException { + final int[] array = new int[length]; + readUInt16Array(stream, array); + return array; + } + + public static void readUInt8Array(final LittleEndianDataInputStream stream, final short[] array) + throws IOException { + for (int i = 0; i < array.length; i++) { + array[i] = readUInt8(stream); + } + } + + public static short[] readUInt8Array(final LittleEndianDataInputStream stream, final int length) + throws IOException { + final short[] array = new short[length]; + readUInt8Array(stream, array); + return array; + } + + public static War3ID readWar3ID(final LittleEndianDataInputStream stream) throws IOException { +// final int value = stream.readInt(); +// return new War3ID(((value & 0xFF000000) >>> 24) | ((value & 0x00FF0000) >>> 8) | ((value & 0x0000FF00) << 8) +// | ((value & 0x000000FF) << 24)); + return new War3ID(Integer.reverseBytes(stream.readInt())); + } + + public static void writeWar3ID(final LittleEndianDataOutputStream stream, final War3ID id) throws IOException { +// final int value = id.getValue(); +// stream.writeInt(((value & 0xFF000000) >>> 24) | ((value & 0x00FF0000) >>> 8) | ((value & 0x0000FF00) << 8) +// | ((value & 0x000000FF) << 24)); + stream.writeInt(Integer.reverseBytes(id.getValue())); + } + + public static void writeFloatArray(final LittleEndianDataOutputStream stream, final float[] array) + throws IOException { + for (int i = 0; i < array.length; i++) { + stream.writeFloat(array[i]); + } + } + + public static void writeUInt32(final LittleEndianDataOutputStream stream, final long uInt) throws IOException { + stream.writeInt((int) (uInt & 0xFFFFFFFF)); + } + + public static void writeUInt16(final LittleEndianDataOutputStream stream, final int uInt) throws IOException { + stream.writeShort((short) (uInt & 0xFFFF)); + } + + public static void writeUInt8(final LittleEndianDataOutputStream stream, final short uInt) throws IOException { + stream.writeByte((byte) (uInt & 0xFF)); + } + + public static void writeUInt32Array(final LittleEndianDataOutputStream stream, final long[] array) + throws IOException { + for (int i = 0; i < array.length; i++) { + writeUInt32(stream, array[i]); + } + } + + public static void writeInt32Array(final LittleEndianDataOutputStream stream, final int[] array) + throws IOException { + for (int i = 0; i < array.length; i++) { + stream.writeInt(array[i]); + } + } + + public static void writeUInt16Array(final LittleEndianDataOutputStream stream, final int[] array) + throws IOException { + for (int i = 0; i < array.length; i++) { + writeUInt16(stream, array[i]); + } + } + + public static void writeUInt8Array(final LittleEndianDataOutputStream stream, final short[] array) + throws IOException { + for (int i = 0; i < array.length; i++) { + writeUInt8(stream, array[i]); + } + } + + public static String readString(final LittleEndianDataInputStream stream, final byte[] recycleByteArray) + throws IOException { + stream.read(recycleByteArray); + int i; + for (i = 0; (i < recycleByteArray.length) && (recycleByteArray[i] != 0); i++) { + } + final String name = new String(recycleByteArray, 0, i, ParseUtils.UTF8); + return name; + } + + public static String readUntilNull(final LittleEndianDataInputStream stream) throws IOException { + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int c; + while (((c = stream.read()) != 0) && (c != -1)) { + baos.write(c); + } + return new String(baos.toByteArray(), ParseUtils.UTF8); + } + + public static void writeWithNullTerminator(final LittleEndianDataOutputStream stream, final String name) + throws IOException { + final byte[] nameBytes = name.getBytes(ParseUtils.UTF8); + stream.write(nameBytes); + stream.write(0); + } + + public static float[] flipRGB(final float[] color) { + final float r = color[0]; + color[0] = color[2]; + color[2] = r; + return color; + } + + public static float[] newFlippedRGB(final float[] color) { + return new float[] { color[2], color[1], color[0] }; + } +} diff --git a/core/src/com/etheller/warsmash/util/Quadtree.java b/core/src/com/etheller/warsmash/util/Quadtree.java new file mode 100644 index 0000000..81a9308 --- /dev/null +++ b/core/src/com/etheller/warsmash/util/Quadtree.java @@ -0,0 +1,253 @@ +package com.etheller.warsmash.util; + +import java.util.function.Consumer; + +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.utils.Array; + +public class Quadtree { + private static final int MAX_DEPTH = 9; // 2^9 = 512, and 512 is the biggest map size... + private static final int SPLIT_THRESHOLD = 6; + + private final Rectangle bounds; + private Quadtree northeast; + private Quadtree northwest; + private Quadtree southwest; + private Quadtree southeast; + private final Array> nodes = new Array<>(); + private boolean leaf = true; + private final NodeAdder nodeAdder = new NodeAdder(); + private final UniqueNodeAdder uniqueNodeAdder = new UniqueNodeAdder(); + + public Quadtree(final Rectangle bounds) { + this.bounds = bounds; + } + + public void add(final T object, final Rectangle bounds) { + final Node node = new Node(object, bounds); + add(node, 0); + } + + public void remove(final T object, final Rectangle bounds) { + remove(object, bounds, null); + } + + public void translate(final T object, final Rectangle prevBoundsToUpdate, final float xShift, final float yShift) { + final Node node = remove(object, prevBoundsToUpdate, null); + prevBoundsToUpdate.x += xShift; + prevBoundsToUpdate.y += yShift; + add(node, 0); + } + + public boolean intersect(final Rectangle bounds, final QuadtreeIntersector intersector) { + if (this.leaf) { + for (int i = 0; i < this.nodes.size; i++) { + final Node node = this.nodes.get(i); + if (node.bounds.overlaps(bounds)) { + if (intersector.onIntersect(node.object)) { + return true; + } + } + } + return false; + } + else { + if (this.northeast.bounds.overlaps(bounds)) { + if (this.northeast.intersect(bounds, intersector)) { + return true; + } + } + if (this.northwest.bounds.overlaps(bounds)) { + if (this.northwest.intersect(bounds, intersector)) { + return true; + } + } + if (this.southwest.bounds.overlaps(bounds)) { + if (this.southwest.intersect(bounds, intersector)) { + return true; + } + } + if (this.southeast.bounds.overlaps(bounds)) { + if (this.southeast.intersect(bounds, intersector)) { + return true; + } + } + return false; + } + } + + public boolean intersect(final float x, final float y, final QuadtreeIntersector intersector) { + if (this.leaf) { + for (int i = 0; i < this.nodes.size; i++) { + final Node node = this.nodes.get(i); + if (node.bounds.contains(x, y)) { + if (intersector.onIntersect(node.object)) { + return true; + } + } + } + return false; + } + else { + if (this.northeast.bounds.contains(x, y)) { + if (this.northeast.intersect(x, y, intersector)) { + return true; + } + } + if (this.northwest.bounds.contains(x, y)) { + if (this.northwest.intersect(x, y, intersector)) { + return true; + } + } + if (this.southwest.bounds.contains(x, y)) { + if (this.southwest.intersect(x, y, intersector)) { + return true; + } + } + if (this.southeast.bounds.contains(x, y)) { + if (this.southeast.intersect(x, y, intersector)) { + return true; + } + } + return false; + } + } + + private void add(final Node node, final int depth) { + if (this.leaf) { + if ((this.nodes.size >= SPLIT_THRESHOLD) && (depth < MAX_DEPTH)) { + split(depth); + // then dont return and add as a nonleaf + } + else { + this.nodes.add(node); + return; + } + } + boolean overlapsAny = false; + if (this.northeast.bounds.overlaps(node.bounds)) { + this.northeast.add(node, depth + 1); + overlapsAny = true; + } + if (this.northwest.bounds.overlaps(node.bounds)) { + this.northwest.add(node, depth + 1); + overlapsAny = true; + } + if (this.southwest.bounds.overlaps(node.bounds)) { + this.southwest.add(node, depth + 1); + overlapsAny = true; + } + if (this.southeast.bounds.overlaps(node.bounds)) { + this.southeast.add(node, depth + 1); + overlapsAny = true; + } + if (!overlapsAny) { + throw new IllegalStateException("Does not overlap anything!"); + } + } + + private void split(final int depth) { + final int splitDepth = depth + 1; + final float halfWidth = this.bounds.width / 2; + final float x = this.bounds.x; + final float xMidpoint = x + halfWidth; + final float halfHeight = this.bounds.height / 2; + final float y = this.bounds.y; + final float yMidpoint = y + halfHeight; + this.northeast = new Quadtree<>(new Rectangle(xMidpoint, yMidpoint, halfWidth, halfHeight)); + this.northwest = new Quadtree<>(new Rectangle(x, yMidpoint, halfWidth, halfHeight)); + this.southwest = new Quadtree<>(new Rectangle(x, y, halfWidth, halfHeight)); + this.southeast = new Quadtree<>(new Rectangle(xMidpoint, y, halfWidth, halfHeight)); + this.leaf = false; + this.nodes.forEach(this.nodeAdder.reset(splitDepth)); + this.nodes.clear(); + } + + private Node remove(final T object, final Rectangle bounds, final Quadtree parent) { + Node returnValue = null; + if (this.leaf) { + for (int i = 0; i < this.nodes.size; i++) { + if (this.nodes.get(i).object == object) { + returnValue = this.nodes.removeIndex(i); + break; + } + } + } + else { + if (this.northeast.bounds.overlaps(bounds)) { + returnValue = this.northeast.remove(object, bounds, this); + } + if (this.northwest.bounds.overlaps(bounds)) { + returnValue = this.northwest.remove(object, bounds, this); + } + if (this.southwest.bounds.overlaps(bounds)) { + returnValue = this.southwest.remove(object, bounds, this); + } + if (this.southeast.bounds.overlaps(bounds)) { + returnValue = this.southeast.remove(object, bounds, this); + } + mergeIfNecessary(); + } + return returnValue; + } + + private void mergeIfNecessary() { + if (this.northeast.leaf && this.northwest.leaf && this.southwest.leaf && this.southeast.leaf) { + final int children = this.northeast.nodes.size + this.northwest.nodes.size + this.southwest.nodes.size + + this.southeast.nodes.size; // might include duplicates + if (children <= SPLIT_THRESHOLD) { + this.leaf = true; + addAllUnique(this.northeast.nodes); + addAllUnique(this.northwest.nodes); + addAllUnique(this.southwest.nodes); + addAllUnique(this.southeast.nodes); + this.northeast = this.northwest = this.southwest = this.southeast = null; + } + } + } + + private void addAllUnique(final Array> nodes) { + nodes.forEach(this.uniqueNodeAdder); + } + + private static final class Node { + private final T object; + private final Rectangle bounds; + + public Node(final T object, final Rectangle bounds) { + this.object = object; + this.bounds = bounds; + } + } + + private final class NodeAdder implements Consumer> { + private int splitDepth; + + private NodeAdder reset(final int splitDepth) { + this.splitDepth = splitDepth; + return this; + } + + @Override + public void accept(final Node node) { + add(node, this.splitDepth); + } + } + + private final class UniqueNodeAdder implements Consumer> { + + private UniqueNodeAdder reset() { + return this; + } + + @Override + public void accept(final Node node) { + for (int i = 0; i < Quadtree.this.nodes.size; i++) { + if (Quadtree.this.nodes.get(i) == node) { + return; + } + } + Quadtree.this.nodes.add(node); + } + } +} diff --git a/core/src/com/etheller/warsmash/util/QuadtreeIntersector.java b/core/src/com/etheller/warsmash/util/QuadtreeIntersector.java new file mode 100644 index 0000000..fa7a890 --- /dev/null +++ b/core/src/com/etheller/warsmash/util/QuadtreeIntersector.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.util; + +public interface QuadtreeIntersector { + /** + * Handles what to do when the intersector finds an intersecting object, + * returning true if we should stop the intersection test and process no more + * objects. + * + * @param intersectingObject + * @return + */ + boolean onIntersect(T intersectingObject); +} diff --git a/core/src/com/etheller/warsmash/util/RawcodeUtils.java b/core/src/com/etheller/warsmash/util/RawcodeUtils.java new file mode 100644 index 0000000..4a85638 --- /dev/null +++ b/core/src/com/etheller/warsmash/util/RawcodeUtils.java @@ -0,0 +1,62 @@ +package com.etheller.warsmash.util; + +import java.nio.charset.StandardCharsets; + +public final class RawcodeUtils { + private RawcodeUtils() { + } + + /** + * Convert a four character string into an integer.
+ * Do not use characters outside the ASCII character set. + * + * @param id the string to convert + * @return integer representation of the string. + */ + public final static int toInt(final String id) { + final byte[] bytes = id.getBytes(StandardCharsets.US_ASCII); + int result = 0; + if (bytes.length >= 4) { + result |= (bytes[3] << 0) & 0x000000FF; + result |= (bytes[2] << 8) & 0x0000FF00; + result |= (bytes[1] << 16) & 0x00FF0000; + result |= (bytes[0] << 24) & 0xFF000000; + } + else { + for (int i = 0; i < bytes.length; i++) { + result |= (bytes[i] << (24 - (i << 3))); + } + } + + return result; + } + + /** + * Convert an integer into a four character string. + * + * @param id the integer to convert + * @return four character string representing the integer. + */ + public final static String toString(final int id) { + final StringBuffer result = new StringBuffer(4); + + return toString(id, result); + } + + /** + * Convert an integer into a four character string using an existing + * StringBuffer. + * + * @param id + * @param result + * @return + */ + public static String toString(final int id, final StringBuffer result) { + result.append((char) ((id >> 24) & 0xFF)); + result.append((char) ((id >> 16) & 0xFF)); + result.append((char) ((id >> 8) & 0xFF)); + result.append((char) ((id >> 0) & 0xFF)); + + return result.toString(); + } +} diff --git a/core/src/com/etheller/warsmash/util/RenderMathUtils.java b/core/src/com/etheller/warsmash/util/RenderMathUtils.java new file mode 100644 index 0000000..f5a3941 --- /dev/null +++ b/core/src/com/etheller/warsmash/util/RenderMathUtils.java @@ -0,0 +1,699 @@ +package com.etheller.warsmash.util; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; +import java.util.List; + +import com.badlogic.gdx.math.Intersector; +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Quaternion; +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.math.Vector3; +import com.badlogic.gdx.math.collision.Ray; + +public enum RenderMathUtils { + ; + public static final float[] EMPTY_FLOAT_ARRAY = new float[0]; + public static final long[] EMPTY_LONG_ARRAY = new long[0]; + public static final Quaternion QUAT_DEFAULT = new Quaternion(0, 0, 0, 1); + public static final Vector3 VEC3_ONE = new Vector3(1, 1, 1); + public static final Vector3 VEC3_UNIT_X = new Vector3(1, 0, 0); + public static final Vector3 VEC3_UNIT_Y = new Vector3(0, 1, 0); + public static final Vector3 VEC3_UNIT_Z = new Vector3(0, 0, 1); + public static final float[] FLOAT_VEC3_ZERO = new float[] { 0, 0, 0 }; + public static final float[] FLOAT_QUAT_DEFAULT = new float[] { 0, 0, 0, 1 }; + public static final float[] FLOAT_VEC3_ONE = new float[] { 1, 1, 1 }; + public static final float HALF_PI = (float) (Math.PI / 2.0); + + // copied from ghostwolf and + // https://www.blend4web.com/api_doc/libs_gl-matrix2.js.html + public static void fromRotationTranslationScaleOrigin(final Quaternion q, final Vector3 v, final Vector3 s, + final Matrix4 out, final Vector3 pivot) { + final float x = q.x; + final float y = q.y; + final float z = q.z; + final float w = q.w; + final float x2 = x + x; + final float y2 = y + y; + final float z2 = z + z; + final float xx = x * x2; + final float xy = x * y2; + final float xz = x * z2; + final float yy = y * y2; + final float yz = y * z2; + final float zz = z * z2; + final float wx = w * x2; + final float wy = w * y2; + final float wz = w * z2; + final float sx = s.x; + final float sy = s.y; + final float sz = s.z; + out.val[Matrix4.M00] = (1 - (yy + zz)) * sx; + out.val[Matrix4.M10] = (xy + wz) * sx; + out.val[Matrix4.M20] = (xz - wy) * sx; + out.val[Matrix4.M30] = 0; + out.val[Matrix4.M01] = (xy - wz) * sy; + out.val[Matrix4.M11] = (1 - (xx + zz)) * sy; + out.val[Matrix4.M21] = (yz + wx) * sy; + out.val[Matrix4.M31] = 0; + out.val[Matrix4.M02] = (xz + wy) * sz; + out.val[Matrix4.M12] = (yz - wx) * sz; + out.val[Matrix4.M22] = (1 - (xx + yy)) * sz; + out.val[Matrix4.M32] = 0; + out.val[Matrix4.M03] = (v.x + pivot.x) - ((out.val[Matrix4.M00] * pivot.x) + (out.val[Matrix4.M01] * pivot.y) + + (out.val[Matrix4.M02] * pivot.z)); + out.val[Matrix4.M13] = (v.y + pivot.y) - ((out.val[Matrix4.M10] * pivot.x) + (out.val[Matrix4.M11] * pivot.y) + + (out.val[Matrix4.M12] * pivot.z)); + out.val[Matrix4.M23] = (v.z + pivot.z) - ((out.val[Matrix4.M20] * pivot.x) + (out.val[Matrix4.M21] * pivot.y) + + (out.val[Matrix4.M22] * pivot.z)); + out.val[Matrix4.M33] = 1; + } + + // copied from + // https://www.blend4web.com/api_doc/libs_gl-matrix2.js.html + public static void fromRotationTranslationScale(final Quaternion q, final Vector3 v, final Vector3 s, + final Matrix4 out) { + final float x = q.x; + final float y = q.y; + final float z = q.z; + final float w = q.w; + final float x2 = x + x; + final float y2 = y + y; + final float z2 = z + z; + final float xx = x * x2; + final float xy = x * y2; + final float xz = x * z2; + final float yy = y * y2; + final float yz = y * z2; + final float zz = z * z2; + final float wx = w * x2; + final float wy = w * y2; + final float wz = w * z2; + final float sx = s.x; + final float sy = s.y; + final float sz = s.z; + out.val[Matrix4.M00] = (1 - (yy + zz)) * sx; + out.val[Matrix4.M10] = (xy + wz) * sx; + out.val[Matrix4.M20] = (xz - wy) * sx; + out.val[Matrix4.M30] = 0; + out.val[Matrix4.M01] = (xy - wz) * sy; + out.val[Matrix4.M11] = (1 - (xx + zz)) * sy; + out.val[Matrix4.M21] = (yz + wx) * sy; + out.val[Matrix4.M31] = 0; + out.val[Matrix4.M02] = (xz + wy) * sz; + out.val[Matrix4.M12] = (yz - wx) * sz; + out.val[Matrix4.M22] = (1 - (xx + yy)) * sz; + out.val[Matrix4.M32] = 0; + out.val[Matrix4.M03] = v.x; + out.val[Matrix4.M13] = v.y; + out.val[Matrix4.M23] = v.z; + out.val[Matrix4.M33] = 1; + } + + public static void mul(final Matrix4 dest, final Matrix4 left, final Matrix4 right) { + dest.set(left); // TODO better performance here, remove the extra + // copying + dest.mul(right); + } + + public static void mul(final Quaternion dest, final Quaternion left, final Quaternion right) { + dest.set(left); // TODO better performance here, remove the extra + // copying + dest.mul(right); + } + + public static Quaternion rotateX(final Quaternion out, final Quaternion a, float rad) { + rad *= 0.5; + + final float ax = a.x, ay = a.y, az = a.z, aw = a.w; + final float bx = (float) Math.sin(rad), bw = (float) Math.cos(rad); + + out.x = (ax * bw) + (aw * bx); + out.y = (ay * bw) + (az * bx); + out.z = (az * bw) - (ay * bx); + out.w = (aw * bw) - (ax * bx); + return out; + } + + public static Quaternion rotateY(final Quaternion out, final Quaternion a, float rad) { + rad *= 0.5; + + final float ax = a.x, ay = a.y, az = a.z, aw = a.w; + final float by = (float) Math.sin(rad), bw = (float) Math.cos(rad); + + out.x = (ax * bw) - (az * by); + out.y = (ay * bw) + (aw * by); + out.z = (az * bw) + (ax * by); + out.w = (aw * bw) - (ay * by); + return out; + } + + public static Quaternion rotateZ(final Quaternion out, final Quaternion a, float rad) { + rad *= 0.5; + + final float ax = a.x, ay = a.y, az = a.z, aw = a.w; + final float bz = (float) Math.sin(rad), bw = (float) Math.cos(rad); + + out.x = (ax * bw) + (ay * bz); + out.y = (ay * bw) - (ax * bz); + out.z = (az * bw) + (aw * bz); + out.w = (aw * bw) - (az * bz); + return out; + } + + public static Matrix4 perspective(final Matrix4 out, final float fovy, final float aspect, final float near, + final float far) { + final float f = 1.0f / (float) Math.tan(fovy / 2), nf; + out.val[Matrix4.M00] = f / aspect; + out.val[Matrix4.M10] = 0; + out.val[Matrix4.M20] = 0; + out.val[Matrix4.M30] = 0; + out.val[Matrix4.M01] = 0; + out.val[Matrix4.M11] = f; + out.val[Matrix4.M21] = 0; + out.val[Matrix4.M31] = 0; + out.val[Matrix4.M02] = 0; + out.val[Matrix4.M12] = 0; + out.val[Matrix4.M32] = -1; + out.val[Matrix4.M03] = 0; + out.val[Matrix4.M13] = 0; + out.val[Matrix4.M33] = 0; + if (!Double.isNaN(far) && !Double.isInfinite(far)) { + nf = 1 / (near - far); + out.val[Matrix4.M22] = (far + near) * nf; + out.val[Matrix4.M23] = (2 * far * near) * nf; + } + else { + out.val[Matrix4.M22] = -1; + out.val[Matrix4.M23] = -2 * near; + } + return out; + } + + public static Matrix4 ortho(final Matrix4 out, final float left, final float right, final float bottom, + final float top, final float near, final float far) { + final float lr = 1 / (left - right); + final float bt = 1 / (bottom - top); + final float nf = 1 / (near - far); + out.val[Matrix4.M00] = -2 * lr; + out.val[Matrix4.M10] = 0; + out.val[Matrix4.M20] = 0; + out.val[Matrix4.M30] = 0; + + out.val[Matrix4.M01] = 0; + out.val[Matrix4.M11] = -2 * bt; + out.val[Matrix4.M21] = 0; + out.val[Matrix4.M31] = 0; + + out.val[Matrix4.M02] = 0; + out.val[Matrix4.M12] = 0; + out.val[Matrix4.M22] = 2 * nf; + out.val[Matrix4.M32] = 0; + + out.val[Matrix4.M03] = (left + right) * lr; + out.val[Matrix4.M13] = (top + bottom) * bt; + out.val[Matrix4.M23] = (far + near) * nf; + out.val[Matrix4.M33] = 1; + return out; + } + + public static void unpackPlanes(final Vector4[] planes, final Matrix4 m) { + final float a00 = m.val[Matrix4.M00], a01 = m.val[Matrix4.M01], a02 = m.val[Matrix4.M02], + a03 = m.val[Matrix4.M03], a10 = m.val[Matrix4.M10], a11 = m.val[Matrix4.M11], a12 = m.val[Matrix4.M12], + a13 = m.val[Matrix4.M13], a20 = m.val[Matrix4.M20], a21 = m.val[Matrix4.M21], a22 = m.val[Matrix4.M22], + a23 = m.val[Matrix4.M23], a30 = m.val[Matrix4.M30], a31 = m.val[Matrix4.M31], a32 = m.val[Matrix4.M32], + a33 = m.val[Matrix4.M33]; + + // Left clipping plane + Vector4 plane = planes[0]; + plane.x = a30 + a00; + plane.y = a31 + a01; + plane.z = a32 + a02; + plane.w = a33 + a03; + + // Right clipping plane + plane = planes[1]; + plane.x = a30 - a00; + plane.y = a31 - a01; + plane.z = a32 - a02; + plane.w = a33 - a03; + + // Top clipping plane + plane = planes[2]; + plane.x = a30 - a10; + plane.y = a31 - a11; + plane.z = a32 - a12; + plane.w = a33 - a13; + + // Bottom clipping plane + plane = planes[3]; + plane.x = a30 + a10; + plane.y = a31 + a11; + plane.z = a32 + a12; + plane.w = a33 + a13; + + // Near clipping plane + plane = planes[4]; + plane.x = a30 + a20; + plane.y = a31 + a21; + plane.z = a32 + a22; + plane.w = a33 + a23; + + // Far clipping plane + plane = planes[5]; + plane.x = a30 - a20; + plane.y = a31 - a21; + plane.z = a32 - a22; + plane.w = a33 - a23; + + normalizePlane(planes[0], planes[0]); + normalizePlane(planes[1], planes[1]); + normalizePlane(planes[2], planes[2]); + normalizePlane(planes[3], planes[3]); + normalizePlane(planes[4], planes[4]); + normalizePlane(planes[5], planes[5]); + } + + public static void normalizePlane(final Vector4 out, final Vector4 plane) { + final float len = Vector3.len(plane.x, plane.y, plane.z); + + out.x = plane.x / len; + out.y = plane.y / len; + out.z = plane.z / len; + out.w = plane.w / len; + } + + public static float distanceToPlane(final Vector4 plane, final Vector3 point) { + return (plane.x * point.x) + (plane.y * point.y) + (plane.z * point.z) + plane.w; + } + + private static final Vector4 heap = new Vector4(); + + public static Vector3 unproject(final Vector3 out, final Vector3 v, final Matrix4 inverseMatrix, + final Rectangle viewport) { + final float x = ((2 * (v.x - viewport.x)) / viewport.width) - 1; + final float y = ((2 * (v.y - viewport.y)) / viewport.height) - 1; + final float z = (2 * v.z) - 1; + + heap.set(x, y, z, 1); + Vector4.transformMat4(heap, heap, inverseMatrix); + out.set(heap.x / heap.w, heap.y / heap.w, heap.z / heap.w); + + return out; + } + + public static int testCell(final List planes, final int left, final int right, final int bottom, + final int top, int first) { + if (first == -1) { + first = 0; + } + + for (int i = 0; i < 6; i++) { + final int index = (first + i) % 6; + final Vector4 plane = planes.get(index); + + if ((distance2Plane2(plane, left, bottom) < 0) && (distance2Plane2(plane, left, top) < 0) + && (distance2Plane2(plane, right, top) < 0) && (distance2Plane2(plane, right, bottom) < 0)) { + return index; + } + } + + return -1; + } + + public static int testCell(final Vector4[] planes, final float left, final float right, final float bottom, + final float top, int first) { + if (first == -1) { + first = 0; + } + + for (int i = 0; i < 6; i++) { + final int index = (first + i) % 6; + final Vector4 plane = planes[index]; + + if ((distance2Plane2(plane, left, bottom) < 0) && (distance2Plane2(plane, left, top) < 0) + && (distance2Plane2(plane, right, top) < 0) && (distance2Plane2(plane, right, bottom) < 0)) { + return index; + } + } + + return -1; + } + + public static float distance2Plane2(final Vector4 plane, final float px, final float py) { + return (plane.x * px) + (plane.y * py) + plane.w; + } + + public static int testSphere(final Vector4[] planes, final float x, final float y, final float z, final float r, + int first) { + if (first == -1) { + first = 0; + } + + for (int i = 0; i < 6; i++) { + final int index = (first + i) % 6; + + if (distanceToPlane3(planes[index], x, y, z) < -r) { + return index; + } + } + + return -1; + } + + public static float distanceToPlane3(final Vector4 plane, final float px, final float py, final float pz) { + return (plane.x * px) + (plane.y * py) + (plane.z * pz) + plane.w; + } + + public static float randomInRange(final float a, final float b) { + return (float) (a + (Math.random() * (b - a))); + } + + public static float clamp(final float x, final float minVal, final float maxVal) { + return Math.min(Math.max(x, minVal), maxVal); + } + + public static float lerp(final float a, final float b, final float t) { + return a + (t * (b - a)); + } + + public static float hermite(final float a, final float b, final float c, final float d, final float t) { + final float factorTimes2 = t * t; + final float factor1 = (factorTimes2 * ((2 * t) - 3)) + 1; + final float factor2 = (factorTimes2 * (t - 2)) + t; + final float factor3 = factorTimes2 * (t - 1); + final float factor4 = factorTimes2 * (3 - (2 * t)); + return (a * factor1) + (b * factor2) + (c * factor3) + (d * factor4); + } + + public static float bezier(final float a, final float b, final float c, final float d, final float t) { + final float invt = 1 - t; + final float factorTimes2 = t * t; + final float inverseFactorTimesTwo = invt * invt; + final float factor1 = inverseFactorTimesTwo * invt; + final float factor2 = 3 * t * inverseFactorTimesTwo; + final float factor3 = 3 * factorTimes2 * invt; + final float factor4 = factorTimes2 * t; + + return (a * factor1) + (b * factor2) + (c * factor3) + (d * factor4); + } + + public static final float EPSILON = 0.000001f; + + public static float[] slerp(final float[] out, final float[] a, final float[] b, final float t) { + final float ax = a[0], ay = a[1], az = a[2], aw = a[3]; + float bx = b[0], by = b[1], bz = b[2], bw = b[3]; + + float omega, cosom, sinom, scale0, scale1; + + // calc cosine + cosom = (ax * bx) + (ay * by) + (az * bz) + (aw * bw); + // adjust signs (if necessary) + if (cosom < 0.0) { + cosom = -cosom; + bx = -bx; + by = -by; + bz = -bz; + bw = -bw; + } + // calculate coefficients + if ((1.0 - cosom) > EPSILON) { + // standard case (slerp) + omega = (float) Math.acos(cosom); + sinom = (float) Math.sin(omega); + scale0 = (float) (Math.sin((1.0 - t) * omega) / sinom); + scale1 = (float) (Math.sin(t * omega) / sinom); + } + else { + // "from" and "to" quaternions are very close + // ... so we can do a linear interpolation + scale0 = 1.0f - t; + scale1 = t; + } + // calculate final values + out[0] = (scale0 * ax) + (scale1 * bx); + out[1] = (scale0 * ay) + (scale1 * by); + out[2] = (scale0 * az) + (scale1 * bz); + out[3] = (scale0 * aw) + (scale1 * bw); + + return out; + } + + private static final float[] sqlerpHeap1 = new float[4]; + private static final float[] sqlerpHeap2 = new float[4]; + + public static float[] sqlerp(final float[] out, final float[] a, final float[] b, final float[] c, final float[] d, + final float t) { + slerp(sqlerpHeap1, a, d, t); + slerp(sqlerpHeap2, b, c, t); + slerp(out, sqlerpHeap1, sqlerpHeap2, 2 * t * (1 - t)); + return out; + } + + static Vector3 best = new Vector3(); + static Vector3 tmp = new Vector3(); + static Vector3 tmp1 = new Vector3(); + static Vector3 tmp2 = new Vector3(); + static Vector3 tmp3 = new Vector3(); + + /** + * Intersects the given ray with list of triangles. Returns the nearest + * intersection point in intersection + * + * @param ray The ray + * @param vertices the vertices + * @param indices the indices, each successive 3 shorts index the 3 + * vertices of a triangle + * @param vertexSize the size of a vertex in floats + * @param intersection The nearest intersection point (optional) + * @return Whether the ray and the triangles intersect. + */ + public static boolean intersectRayTriangles(final Ray ray, final float[] vertices, final int[] indices, + final int vertexSize, final Vector3 intersection) { + float min_dist = Float.MAX_VALUE; + boolean hit = false; + + if ((indices.length % 3) != 0) { + throw new RuntimeException("triangle list size is not a multiple of 3"); + } + + for (int i = 0; i < indices.length; i += 3) { + final int i1 = indices[i] * vertexSize; + final int i2 = indices[i + 1] * vertexSize; + final int i3 = indices[i + 2] * vertexSize; + + final boolean result = FixedIntersector.intersectRayTriangle(ray, + tmp1.set(vertices[i1], vertices[i1 + 1], vertices[i1 + 2]), + tmp2.set(vertices[i2], vertices[i2 + 1], vertices[i2 + 2]), + tmp3.set(vertices[i3], vertices[i3 + 1], vertices[i3 + 2]), tmp); + + if (result == true) { + final float dist = ray.origin.dst2(tmp); + if (dist < min_dist) { + min_dist = dist; + best.set(tmp); + hit = true; + } + } + } + + if (hit == false) { + return false; + } + else { + if (intersection != null) { + intersection.set(best); + } + return true; + } + } + + /** + * Intersects the given ray with list of triangles. Returns the nearest + * intersection point in intersection + * + * @param ray The ray + * @param vertices the vertices + * @param indices the indices, each successive 3 shorts index the 3 + * vertices of a triangle + * @param vertexSize the size of a vertex in floats + * @param intersection The nearest intersection point (optional) + * @return Whether the ray and the triangles intersect. + */ + public static boolean intersectRayTriangles(final Ray ray, final float[] vertices, final int[] indices, + final int vertexSize, final Vector3 worldLocation, final float facingRadians, final Vector3 intersection) { + float min_dist = Float.MAX_VALUE; + boolean hit = false; + + if ((indices.length % 3) != 0) { + throw new RuntimeException("triangle list size is not a multiple of 3"); + } + + final float facingX_X = (float) Math.cos(facingRadians); + final float facingX_Y = (float) Math.sin(facingRadians); + final double halfPi = Math.PI / 2; + final float facingY_X = (float) Math.cos(facingRadians + halfPi); + final float facingY_Y = (float) Math.sin(facingRadians + halfPi); + for (int i = 0; i < indices.length; i += 3) { + final int i1 = indices[i] * vertexSize; + final int i2 = indices[i + 1] * vertexSize; + final int i3 = indices[i + 2] * vertexSize; + + final boolean result = Intersector.intersectRayTriangle(ray, + tmp1.set((vertices[i1] * facingX_X) + (vertices[i1 + 1] * facingY_X) + worldLocation.x, + (vertices[i1] * facingX_Y) + (vertices[i1 + 1] * facingY_Y) + worldLocation.y, + vertices[i1 + 2] + worldLocation.z), + tmp2.set((vertices[i2] * facingX_X) + (vertices[i2 + 1] * facingY_X) + worldLocation.x, + (vertices[i2] * facingX_Y) + (vertices[i2 + 1] * facingY_Y) + worldLocation.y, + vertices[i2 + 2] + worldLocation.z), + tmp3.set((vertices[i3] * facingX_X) + (vertices[i3 + 1] * facingY_X) + worldLocation.x, + (vertices[i3] * facingX_Y) + (vertices[i3 + 1] * facingY_Y) + worldLocation.y, + vertices[i3 + 2] + worldLocation.z), + tmp); + + if (result == true) { + final float dist = ray.origin.dst2(tmp); + if (dist < min_dist) { + min_dist = dist; + best.set(tmp); + hit = true; + } + } + } + + if (hit == false) { + return false; + } + else { + if (intersection != null) { + intersection.set(best); + } + return true; + } + } + + // ==== All of the following "wrap" calls are horribly inefficient. Eventually + // they should be removed entirely with better design. + // Until that happens, be sure to only call them during setup and not while the + // simulation is live. Otherwise you'll probably get some + // bad lag (and memory leaks?). + public static ShortBuffer wrapFaces(final int[] faces) { + final ShortBuffer wrapper = ByteBuffer.allocateDirect(faces.length * 2).order(ByteOrder.nativeOrder()) + .asShortBuffer(); + for (final int face : faces) { + wrapper.put((short) face); + } + wrapper.clear(); + return wrapper; + } + + public static ByteBuffer wrap(final byte[] skin) { + final ByteBuffer wrapper = ByteBuffer.allocateDirect(skin.length).order(ByteOrder.nativeOrder()); + wrapper.put(skin); + wrapper.clear(); + return wrapper; + } + + public static FloatBuffer wrap(final float[] positions) { + final FloatBuffer wrapper = ByteBuffer.allocateDirect(positions.length * 4).order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + wrapper.put(positions); + wrapper.clear(); + return wrapper; + } + + public static IntBuffer wrap(final int[] positions) { + final IntBuffer wrapper = ByteBuffer.allocateDirect(positions.length * 4).order(ByteOrder.nativeOrder()) + .asIntBuffer(); + wrapper.put(positions); + wrapper.clear(); + return wrapper; + } + + public static ByteBuffer wrapAsBytes(final int[] positions) { + final ByteBuffer wrapper = ByteBuffer.allocateDirect(positions.length).order(ByteOrder.nativeOrder()); + for (final int face : positions) { + wrapper.put((byte) face); + } + wrapper.clear(); + return wrapper; + } + + public static Buffer wrap(final short[] cornerTextures) { + final ByteBuffer wrapper = ByteBuffer.allocateDirect(cornerTextures.length).order(ByteOrder.nativeOrder()); + for (final short face : cornerTextures) { + wrapper.put((byte) face); + } + wrapper.clear(); + return wrapper; + } + + public static Buffer wrapShort(final short[] cornerTextures) { + final ByteBuffer wrapper = ByteBuffer.allocateDirect(cornerTextures.length * 2).order(ByteOrder.nativeOrder()); + for (final short face : cornerTextures) { + wrapper.putShort(face); + } + wrapper.clear(); + return wrapper; + } + + public static Buffer wrapPairs(final float[][] quadVertices) { + final FloatBuffer wrapper = ByteBuffer.allocateDirect(quadVertices.length * 8).order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + for (int i = 0; i < quadVertices.length; i++) { + for (int j = 0; j < 2; j++) { + wrapper.put(quadVertices[i][j]); + } + } + wrapper.clear(); + return wrapper; + } + + public static Buffer wrap(final int[][] quadIndices) { + final IntBuffer wrapper = ByteBuffer.allocateDirect(quadIndices.length * 3 * 4).order(ByteOrder.nativeOrder()) + .asIntBuffer(); + for (int i = 0; i < quadIndices.length; i++) { + for (int j = 0; j < 3; j++) { + wrapper.put(quadIndices[i][j]); + } + } + wrapper.clear(); + return wrapper; + } + + public static Buffer wrap(final List vertices) { + if (vertices.isEmpty()) { + return null; + } + final int expectedNumberOfFloats = vertices.get(0).length; + final FloatBuffer wrapper = ByteBuffer.allocateDirect(vertices.size() * expectedNumberOfFloats * 4) + .order(ByteOrder.nativeOrder()).asFloatBuffer(); + for (final float[] subArray : vertices) { + for (final float f : subArray) { + wrapper.put(f); + } + } + wrapper.clear(); + return wrapper; + } + + public static Buffer wrapFaces(final List indices) { + if (indices.isEmpty()) { + return null; + } + final int expectedNumberOfValues = indices.get(0).length; + final ShortBuffer wrapper = ByteBuffer.allocateDirect(indices.size() * expectedNumberOfValues * 2) + .order(ByteOrder.nativeOrder()).asShortBuffer(); + for (final int[] subArray : indices) { + for (final int value : subArray) { + wrapper.put((short) value); + } + } + wrapper.clear(); + return wrapper; + } +} diff --git a/core/src/com/etheller/warsmash/util/SlkFile.java b/core/src/com/etheller/warsmash/util/SlkFile.java new file mode 100644 index 0000000..bebac99 --- /dev/null +++ b/core/src/com/etheller/warsmash/util/SlkFile.java @@ -0,0 +1,73 @@ +package com.etheller.warsmash.util; + +import java.util.ArrayList; +import java.util.List; + +public class SlkFile { + public List> rows = new ArrayList<>(); + + public SlkFile(final String buffer) { + if (buffer != null) { + this.load(buffer); + } + } + + public void load(final String buffer) { + if (!buffer.startsWith("ID")) { + throw new RuntimeException("WrongMagicNumber"); + } + + int x = 0; + int y = 0; + for (final String line : buffer.split("\n")) { + // The B command is supposed to define the total number of columns and rows, + // however in UbetSplatData.slk it gives wrong information + // Therefore, just ignore it, since JavaScript arrays grow as they want either + // way + if (line.charAt(0) != 'B') { + for (final String token : line.split(";")) { + if (token.isEmpty()) { + continue; + } + final char op = token.charAt(0); + final String valueString = token.substring(1).trim(); + final Object value; + + if (op == 'X') { + x = Integer.parseInt(valueString, 10) - 1; + } + else if (op == 'Y') { + y = Integer.parseInt(valueString, 10) - 1; + } + else if (op == 'K') { + while (y >= this.rows.size()) { + this.rows.add(null); + } + if (this.rows.get(y) == null) { + this.rows.set(y, new ArrayList<>()); + } + + if (valueString.charAt(0) == '"') { + value = valueString.substring(1, valueString.length() - 1); + } + else if ("TRUE".equals(valueString)) { + value = true; + } + else if ("FALSE".equals(valueString)) { + value = false; + } + else { + value = Float.parseFloat(valueString); + } + + final List row = this.rows.get(y); + while (x >= row.size()) { + row.add(null); + } + row.set(x, value); + } + } + } + } + } +} diff --git a/core/src/com/etheller/warsmash/util/StringBundle.java b/core/src/com/etheller/warsmash/util/StringBundle.java new file mode 100644 index 0000000..d9d1150 --- /dev/null +++ b/core/src/com/etheller/warsmash/util/StringBundle.java @@ -0,0 +1,19 @@ +package com.etheller.warsmash.util; + +public interface StringBundle { + String getString(String string); + + String getStringCaseSensitive(final String key); + + StringBundle EMPTY = new StringBundle() { + @Override + public String getStringCaseSensitive(final String key) { + return key; + } + + @Override + public String getString(final String string) { + return string; + } + }; +} diff --git a/core/src/com/etheller/warsmash/util/SubscriberSetNotifier.java b/core/src/com/etheller/warsmash/util/SubscriberSetNotifier.java new file mode 100644 index 0000000..8151abe --- /dev/null +++ b/core/src/com/etheller/warsmash/util/SubscriberSetNotifier.java @@ -0,0 +1,22 @@ +package com.etheller.warsmash.util; + +import java.util.HashSet; +import java.util.Set; + +public abstract class SubscriberSetNotifier { + protected final Set set; // bad for iteration but there + // should never be a dude subscribed + // 2x + + public SubscriberSetNotifier() { + this.set = new HashSet<>(); + } + + public final void subscribe(final LISTENER_TYPE listener) { + this.set.add(listener); + } + + public final void unsubscribe(final LISTENER_TYPE listener) { + this.set.remove(listener); + } +} diff --git a/core/src/com/etheller/warsmash/util/Test.java b/core/src/com/etheller/warsmash/util/Test.java new file mode 100644 index 0000000..9d88d96 --- /dev/null +++ b/core/src/com/etheller/warsmash/util/Test.java @@ -0,0 +1,22 @@ +package com.etheller.warsmash.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Test { + + public static void main(final String[] args) { + final Pattern pattern = Pattern.compile("^\\[(.+?)\\]"); + final Matcher matcher = pattern.matcher("[boat] // ocean"); + if (matcher.matches()) { + final String name = matcher.group(1).trim().toLowerCase(); + System.out.println(name); + } + else { + System.out.println("no match"); + } + +// Quadtree myQT = new Quadtree<>(new Rectangle(-, y, width, height)) + } + +} diff --git a/core/src/com/etheller/warsmash/util/Vector4.java b/core/src/com/etheller/warsmash/util/Vector4.java new file mode 100644 index 0000000..e6f42c1 --- /dev/null +++ b/core/src/com/etheller/warsmash/util/Vector4.java @@ -0,0 +1,573 @@ +package com.etheller.warsmash.util; + +import java.io.Serializable; + +import com.badlogic.gdx.math.Interpolation; +import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Vector; +import com.badlogic.gdx.utils.NumberUtils; + +/** + * Encapsulates a 4D vector. Allows chaining operations by returning a reference + * to itself in all modification methods. + * + * @author intrigus + */ +public class Vector4 implements Serializable, Vector { + + /** the x-component of this vector **/ + public float x; + /** the y-component of this vector **/ + public float y; + /** the z-component of this vector **/ + public float z; + /** the w-component of this vector **/ + public float w; + + public final static Vector4 X = new Vector4(1, 0, 0, 0); + public final static Vector4 Y = new Vector4(0, 1, 0, 0); + public final static Vector4 Z = new Vector4(0, 0, 1, 0); + public final static Vector4 W = new Vector4(0, 0, 0, 1); + public final static Vector4 Zero = new Vector4(0, 0, 0, 0); + + private final static Matrix4 tmpMat = new Matrix4(); + + /** Constructs a vector at (0,0,0,0) */ + public Vector4() { + } + + /** + * Creates a vector with the given components + * + * @param x The x-component + * @param y The y-component + * @param z The z-component + * @param w The w-component + */ + public Vector4(final float x, final float y, final float z, final float w) { + this.set(x, y, z, w); + } + + /** + * Creates a vector from the given vector + * + * @param vector The vector + */ + public Vector4(final Vector4 vector) { + this.set(vector); + } + + /** + * Creates a vector from the given array. The array must have at least 4 + * elements. + * + * @param values The array + */ + public Vector4(final float[] values) { + this.set(values[0], values[1], values[2], values[3]); + } + + /** + * Sets the vector to the given components + * + * @param x The x-component + * @param y The y-component + * @param z The z-component + * @param w The w-component + * @return this vector for chaining + */ + public Vector4 set(final float x, final float y, final float z, final float w) { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + return this; + } + + @Override + public Vector4 set(final Vector4 vector) { + return this.set(vector.x, vector.y, vector.z, vector.w); + } + + /** + * Sets the components from the array. The array must have at least 4 elements + * + * @param values The array + * @return this vector for chaining + */ + public Vector4 set(final float[] values) { + return this.set(values[0], values[1], values[2], values[3]); + } + + @Override + public Vector4 cpy() { + return new Vector4(this); + } + + @Override + public Vector4 add(final Vector4 vector) { + return this.add(vector.x, vector.y, vector.z, vector.w); + } + + /** + * Adds the given vector to this component + * + * @param x The x-component of the other vector + * @param y The y-component of the other vector + * @param z The z-component of the other vector + * @param w The w-component of the other vector + * @return This vector for chaining. + */ + public Vector4 add(final float x, final float y, final float z, final float w) { + return this.set(this.x + x, this.y + y, this.z + z, this.w + w); + } + + /** + * Adds the given value to all four components of the vector. + * + * @param values The value + * @return This vector for chaining + */ + public Vector4 add(final float values) { + return this.set(this.x + values, this.y + values, this.z + values, this.w + values); + } + + @Override + public Vector4 sub(final Vector4 a_vec) { + return this.sub(a_vec.x, a_vec.y, a_vec.z, a_vec.w); + } + + /** + * Subtracts the other vector from this vector. + * + * @param x The x-component of the other vector + * @param y The y-component of the other vector + * @param z The z-component of the other vector + * @param w The w-component of the other vector + * @return This vector for chaining + */ + public Vector4 sub(final float x, final float y, final float z, final float w) { + return this.set(this.x - x, this.y - y, this.z - z, this.w - w); + } + + /** + * Subtracts the given value from all components of this vector + * + * @param value The value + * @return This vector for chaining + */ + public Vector4 sub(final float value) { + return this.set(this.x - value, this.y - value, this.z - value, this.w - value); + } + + @Override + public Vector4 scl(final float scalar) { + return this.set(this.x * scalar, this.y * scalar, this.z * scalar, this.w * scalar); + } + + @Override + public Vector4 scl(final Vector4 other) { + return this.set(this.x * other.x, this.y * other.y, this.z * other.z, this.w * other.w); + } + + /** + * Scales this vector by the given values + * + * @param vx X value + * @param vy Y value + * @param vz Z value + * @param vw W value + * @return This vector for chaining + */ + public Vector4 scl(final float vx, final float vy, final float vz, final float vw) { + return this.set(this.x * vx, this.y * vy, this.z * vz, this.z * vw); + } + + @Override + public Vector4 mulAdd(final Vector4 vec, final float scalar) { + this.x += vec.x * scalar; + this.y += vec.y * scalar; + this.z += vec.z * scalar; + this.w += vec.w * scalar; + return this; + } + + @Override + public Vector4 mulAdd(final Vector4 vec, final Vector4 mulVec) { + this.x += vec.x * mulVec.x; + this.y += vec.y * mulVec.y; + this.z += vec.z * mulVec.z; + this.w += vec.w * mulVec.w; + return this; + } + + /** @return The euclidian length */ + public static float len(final float x, final float y, final float z, final float w) { + return (float) Math.sqrt((x * x) + (y * y) + (z * z) + (w * w)); + } + + @Override + public float len() { + return (float) Math.sqrt((this.x * this.x) + (this.y * this.y) + (this.z * this.z) + (this.w * this.w)); + } + + /** @return The squared euclidian length */ + public static float len2(final float x, final float y, final float z, final float w) { + return (x * x) + (y * y) + (z * z) + (w * w); + } + + @Override + public float len2() { + return (this.x * this.x) + (this.y * this.y) + (this.z * this.z) + (this.w * this.w); + } + + /** + * @param vector The other vector + * @return Whether this and the other vector are equal + */ + public boolean idt(final Vector4 vector) { + return (this.x == vector.x) && (this.y == vector.y) && (this.z == vector.z) && (this.w == vector.w); + } + + /** @return The euclidian distance between the two specified vectors */ + public static float dst(final float x1, final float y1, final float z1, final float w1, final float x2, + final float y2, final float z2, final float w2) { + final float a = x2 - x1; + final float b = y2 - y1; + final float c = z2 - z1; + final float d = w2 - w1; + return (float) Math.sqrt((a * a) + (b * b) + (c * c) + (d * d)); + } + + @Override + public float dst(final Vector4 vector) { + final float a = vector.x - this.x; + final float b = vector.y - this.y; + final float c = vector.z - this.z; + final float d = vector.w - this.w; + return (float) Math.sqrt((a * a) + (b * b) + (c * c) + (d * d)); + } + + /** @return the distance between this point and the given point */ + public float dst(final float x, final float y, final float z, final float w) { + final float a = x - this.x; + final float b = y - this.y; + final float c = z - this.z; + final float d = w - this.w; + return (float) Math.sqrt((a * a) + (b * b) + (c * c) + (d * d)); + } + + /** @return the squared distance between the given points */ + public static float dst2(final float x1, final float y1, final float z1, final float w1, final float x2, + final float y2, final float z2, final float w2) { + final float a = x2 - x1; + final float b = y2 - y1; + final float c = z2 - z1; + final float d = w2 - w1; + return (a * a) + (b * b) + (c * c) + (d * d); + } + + @Override + public float dst2(final Vector4 point) { + final float a = point.x - this.x; + final float b = point.y - this.y; + final float c = point.z - this.z; + final float d = point.w - this.w; + return (a * a) + (b * b) + (c * c) + (d * d); + } + + /** + * Returns the squared distance between this point and the given point + * + * @param x The x-component of the other point + * @param y The y-component of the other point + * @param z The z-component of the other point + * @param w The w-component of the other point + * @return The squared distance + */ + public float dst2(final float x, final float y, final float z, final float w) { + final float a = x - this.x; + final float b = y - this.y; + final float c = z - this.z; + final float d = w - this.w; + return (a * a) + (b * b) + (c * c) + (d * d); + } + + @Override + public Vector4 nor() { + final float len2 = this.len2(); + if ((len2 == 0f) || (len2 == 1f)) { + return this; + } + return this.scl(1f / (float) Math.sqrt(len2)); + } + + /** @return The dot product between the two vectors */ + public static float dot(final float x1, final float y1, final float z1, final float w1, final float x2, + final float y2, final float z2, final float w2) { + return (x1 * x2) + (y1 * y2) + (z1 * z2) + (w1 * w2); + } + + @Override + public float dot(final Vector4 vector) { + return (this.x * vector.x) + (this.y * vector.y) + (this.z * vector.z) + (this.w * vector.w); + } + + /** + * Returns the dot product between this and the given vector. + * + * @param x The x-component of the other vector + * @param y The y-component of the other vector + * @param z The z-component of the other vector + * @param w The w-component of the other vector + * @return The dot product + */ + public float dot(final float x, final float y, final float z, final float w) { + return (this.x * x) + (this.y * y) + (this.z * z) + (this.w * w); + } + + @Override + public boolean isUnit() { + return isUnit(0.000000001f); + } + + @Override + public boolean isUnit(final float margin) { + return Math.abs(len2() - 1f) < margin; + } + + @Override + public boolean isZero() { + return (this.x == 0) && (this.y == 0) && (this.z == 0); + } + + @Override + public boolean isZero(final float margin) { + return len2() < margin; + } + + @Override + public boolean isOnLine(final Vector4 other, final float epsilon) { + throw new UnsupportedOperationException(); + // TODO +// return len2((this.y * other.z) - (this.z * other.y), (this.z * other.x) - (this.x * other.z), +// (this.x * other.y) - (this.y * other.x)) <= epsilon; + } + + @Override + public boolean isOnLine(final Vector4 other) { + throw new UnsupportedOperationException(); + // TODO +// return len2((this.y * other.z) - (this.z * other.y), (this.z * other.x) - (this.x * other.z), +// (this.x * other.y) - (this.y * other.x)) <= MathUtils.FLOAT_ROUNDING_ERROR; + } + + @Override + public boolean isCollinear(final Vector4 other, final float epsilon) { + return isOnLine(other, epsilon) && hasSameDirection(other); + } + + @Override + public boolean isCollinear(final Vector4 other) { + return isOnLine(other) && hasSameDirection(other); + } + + @Override + public boolean isCollinearOpposite(final Vector4 other, final float epsilon) { + return isOnLine(other, epsilon) && hasOppositeDirection(other); + } + + @Override + public boolean isCollinearOpposite(final Vector4 other) { + return isOnLine(other) && hasOppositeDirection(other); + } + + @Override + public boolean isPerpendicular(final Vector4 vector) { + return MathUtils.isZero(dot(vector)); + } + + @Override + public boolean isPerpendicular(final Vector4 vector, final float epsilon) { + return MathUtils.isZero(dot(vector), epsilon); + } + + @Override + public boolean hasSameDirection(final Vector4 vector) { + return dot(vector) > 0; + } + + @Override + public boolean hasOppositeDirection(final Vector4 vector) { + return dot(vector) < 0; + } + + @Override + public Vector4 lerp(final Vector4 target, final float alpha) { + // TODO + this.x += alpha * (target.x - this.x); + this.y += alpha * (target.y - this.y); + this.z += alpha * (target.z - this.z); + return this; + } + + @Override + public Vector4 interpolate(final Vector4 target, final float alpha, final Interpolation interpolator) { + // TODO + return lerp(target, interpolator.apply(0f, 1f, alpha)); + } + + @Override + public String toString() { + return "[" + this.x + ", " + this.y + ", " + this.z + ", " + this.w + "]"; + } + + @Override + public Vector4 limit(final float limit) { +// TODO + return limit2(limit * limit); + } + + @Override + public Vector4 limit2(final float limit2) { + // TODO + final float len2 = len2(); + if (len2 > limit2) { + scl((float) Math.sqrt(limit2 / len2)); + } + return this; + } + + @Override + public Vector4 setLength(final float len) { + return setLength2(len * len); + } + + @Override + public Vector4 setLength2(final float len2) { + final float oldLen2 = len2(); + return ((oldLen2 == 0) || (oldLen2 == len2)) ? this : scl((float) Math.sqrt(len2 / oldLen2)); + } + + @Override + public Vector4 clamp(final float min, final float max) { + final float len2 = len2(); + if (len2 == 0f) { + return this; + } + final float max2 = max * max; + if (len2 > max2) { + return scl((float) Math.sqrt(max2 / len2)); + } + final float min2 = min * min; + if (len2 < min2) { + return scl((float) Math.sqrt(min2 / len2)); + } + return this; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + NumberUtils.floatToIntBits(this.x); + result = (prime * result) + NumberUtils.floatToIntBits(this.y); + result = (prime * result) + NumberUtils.floatToIntBits(this.z); + result = (prime * result) + NumberUtils.floatToIntBits(this.w); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Vector4 other = (Vector4) obj; + if (NumberUtils.floatToIntBits(this.x) != NumberUtils.floatToIntBits(other.x)) { + return false; + } + if (NumberUtils.floatToIntBits(this.y) != NumberUtils.floatToIntBits(other.y)) { + return false; + } + if (NumberUtils.floatToIntBits(this.z) != NumberUtils.floatToIntBits(other.z)) { + return false; + } + if (NumberUtils.floatToIntBits(this.w) != NumberUtils.floatToIntBits(other.w)) { + return false; + } + return true; + } + + @Override + public boolean epsilonEquals(final Vector4 other, final float epsilon) { + if (other == null) { + return false; + } + if (Math.abs(other.x - this.x) > epsilon) { + return false; + } + if (Math.abs(other.y - this.y) > epsilon) { + return false; + } + if (Math.abs(other.z - this.z) > epsilon) { + return false; + } + if (Math.abs(other.w - this.w) > epsilon) { + return false; + } + return true; + } + + /** + * Compares this vector with the other vector, using the supplied epsilon for + * fuzzy equality testing. + * + * @return whether the vectors are the same. + */ + public boolean epsilonEquals(final float x, final float y, final float z, final float w, final float epsilon) { + if (Math.abs(x - this.x) > epsilon) { + return false; + } + if (Math.abs(y - this.y) > epsilon) { + return false; + } + if (Math.abs(z - this.z) > epsilon) { + return false; + } + if (Math.abs(w - this.w) > epsilon) { + return false; + } + return true; + } + + @Override + public Vector4 setZero() { + this.x = 0; + this.y = 0; + this.z = 0; + this.w = 0; + return this; + } + + @Override + public Vector4 setToRandomDirection() { + throw new UnsupportedOperationException(); + } + + public static Vector4 transformMat4(final Vector4 out, final Vector4 a, final Matrix4 matrix) { + final float x = a.x, y = a.y, z = a.z, w = a.w; + final float[] m = matrix.val; + out.x = (m[Matrix4.M00] * x) + (m[Matrix4.M01] * y) + (m[Matrix4.M02] * z) + (m[Matrix4.M03] * w); + out.y = (m[Matrix4.M10] * x) + (m[Matrix4.M11] * y) + (m[Matrix4.M12] * z) + (m[Matrix4.M13] * w); + out.z = (m[Matrix4.M20] * x) + (m[Matrix4.M21] * y) + (m[Matrix4.M22] * z) + (m[Matrix4.M23] * w); + out.w = (m[Matrix4.M30] * x) + (m[Matrix4.M31] * y) + (m[Matrix4.M32] * z) + (m[Matrix4.M33] * w); + return out; + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/util/War3ID.java b/core/src/com/etheller/warsmash/util/War3ID.java new file mode 100644 index 0000000..e42e3cb --- /dev/null +++ b/core/src/com/etheller/warsmash/util/War3ID.java @@ -0,0 +1,83 @@ +package com.etheller.warsmash.util; + +public final class War3ID implements Comparable { + public static final War3ID NONE = new War3ID(0); + private final int value; + + public War3ID(final int value) { + this.value = value; + } + + public int getValue() { + return this.value; + } + + public static War3ID fromString(String idString) { + if (idString.length() == 3) { + System.err.println( + "Loaded custom data for the ability CURSE whose MetaData field, 'Crs', is the only 3 letter War3ID in the game. This might cause unexpected errors, so watch your % chance to miss in custom curse abilities carefully."); + idString += '\0'; + } + if (idString.length() != 4) { + throw new IllegalArgumentException( + "A War3ID must be 4 ascii characters in length (got " + idString.length() + ") '" + idString + "'"); + } + return new War3ID(RawcodeUtils.toInt(idString)); + } + + public String asStringValue() { + String string = RawcodeUtils.toString(this.value); + if (((string.charAt(3) == '\0') || (string.charAt(3) == ' ')) && (string.charAt(2) != '\0')) { + string = string.substring(0, 3); + } + return string; + } + + public War3ID set(final int index, final char c) { + final String asStringValue = asStringValue(); + String result = asStringValue.substring(0, index); + result += c; + result += asStringValue.substring(index + 1, asStringValue.length()); + return War3ID.fromString(result); + } + + public char charAt(final int index) { + return (char) ((this.value >>> ((3 - index) * 8)) & 0xFF); + } + + @Override + public String toString() { + return asStringValue(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + this.value; + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final War3ID other = (War3ID) obj; + if (this.value != other.value) { + return false; + } + return true; + } + + @Override + public int compareTo(final War3ID o) { + return Integer.compare(this.value, o.value); + } +} diff --git a/core/src/com/etheller/warsmash/util/WarsmashConstants.java b/core/src/com/etheller/warsmash/util/WarsmashConstants.java new file mode 100644 index 0000000..6b2e4ab --- /dev/null +++ b/core/src/com/etheller/warsmash/util/WarsmashConstants.java @@ -0,0 +1,26 @@ +package com.etheller.warsmash.util; + +public class WarsmashConstants { + public static final int MAX_PLAYERS = 16; + /* + * With version, we use 0 for RoC, 1 for TFT emulation, and probably 2+ or + * whatever for custom mods and other stuff + */ + public static int GAME_VERSION = 1; + public static final int REPLACEABLE_TEXTURE_LIMIT = 64; + public static final float SIMULATION_STEP_TIME = 1 / 20f; + public static final int PORT_NUMBER = 6115; + public static final float BUILDING_CONSTRUCT_START_LIFE = 0.1f; + public static final int BUILD_QUEUE_SIZE = 7; + // It looks like in Patch 1.22, "Particle" in video settings will change this + // factor: + // Low - unknown ? + // Medium - 1.0f + // High - 1.5f + public static final float MODEL_DETAIL_PARTICLE_FACTOR = 1.5f; + public static final float MODEL_DETAIL_PARTICLE_FACTOR_INVERSE = 1f / MODEL_DETAIL_PARTICLE_FACTOR; + + // I know this default string is from somewhere, maybe a language file? Didn't + // find it yet so I used this + public static final String DEFAULT_STRING = "Default string"; +} diff --git a/core/src/com/etheller/warsmash/util/WorldEditStrings.java b/core/src/com/etheller/warsmash/util/WorldEditStrings.java new file mode 100644 index 0000000..40c746e --- /dev/null +++ b/core/src/com/etheller/warsmash/util/WorldEditStrings.java @@ -0,0 +1,81 @@ +package com.etheller.warsmash.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.MissingResourceException; +import java.util.PropertyResourceBundle; +import java.util.ResourceBundle; + +import com.etheller.warsmash.datasources.DataSource; + +public class WorldEditStrings implements StringBundle { + private ResourceBundle bundle; + private ResourceBundle bundlegs; + + public WorldEditStrings(final DataSource dataSource) { + if (dataSource.has("UI\\WorldEditStrings.txt")) { + try (InputStream fis = dataSource.getResourceAsStream("UI\\WorldEditStrings.txt"); + InputStreamReader reader = new InputStreamReader(fis, "utf-8")) { + this.bundle = new PropertyResourceBundle(reader); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + try (InputStream fis = dataSource.getResourceAsStream("UI\\WorldEditGameStrings.txt"); + InputStreamReader reader = new InputStreamReader(fis, "utf-8")) { + this.bundlegs = new PropertyResourceBundle(reader); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public String getString(String string) { + try { + while (string.toUpperCase().startsWith("WESTRING")) { + string = internalGetString(string); + } + return string; + } + catch (final MissingResourceException exc) { + try { + return this.bundlegs.getString(string.toUpperCase()); + } + catch (final MissingResourceException exc2) { + return string; + } + } + } + + private String internalGetString(final String key) { + if (this.bundle == null) { + return this.bundlegs.getString(key.toUpperCase()); + } + try { + String string = this.bundle.getString(key.toUpperCase()); + if ((string.charAt(0) == '"') && (string.length() >= 2) && (string.charAt(string.length() - 1) == '"')) { + string = string.substring(1, string.length() - 1); + } + return string; + } + catch (final MissingResourceException exc) { + return this.bundlegs.getString(key.toUpperCase()); + } + } + + @Override + public String getStringCaseSensitive(final String key) { + if (this.bundle == null) { + return this.bundlegs.getString(key); + } + try { + return this.bundle.getString(key); + } + catch (final MissingResourceException exc) { + return this.bundlegs.getString(key); + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/AudioBufferSource.java b/core/src/com/etheller/warsmash/viewer5/AudioBufferSource.java new file mode 100644 index 0000000..245b844 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/AudioBufferSource.java @@ -0,0 +1,23 @@ +package com.etheller.warsmash.viewer5; + +import com.badlogic.gdx.audio.Sound; +import com.etheller.warsmash.viewer5.gl.Extensions; + +public class AudioBufferSource { + public Sound buffer; + private AudioPanner panner; + + public void connect(final AudioPanner panner) { + this.panner = panner; + } + + public void start(final int value, final float volume, final float pitch, final boolean looping) { + if (this.buffer != null) { + if (!this.panner.listener.is3DSupported() || this.panner.isWithinListenerDistance()) { + Extensions.audio.play(this.buffer, volume, pitch, this.panner.x, this.panner.y, this.panner.z, + this.panner.listener.is3DSupported(), this.panner.maxDistance, this.panner.refDistance, + looping); + } + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/AudioContext.java b/core/src/com/etheller/warsmash/viewer5/AudioContext.java new file mode 100644 index 0000000..68383f2 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/AudioContext.java @@ -0,0 +1,92 @@ +package com.etheller.warsmash.viewer5; + +public class AudioContext { + private boolean running = false; + public Listener listener; + public AudioDestination destination; + + public AudioContext(final Listener listener, final AudioDestination destination) { + this.listener = listener; + this.destination = destination; + } + + public void suspend() { + this.running = false; + } + + public boolean isRunning() { + return this.running; + } + + public void resume() { + this.running = true; + } + + public static interface Listener { + + float getX(); + + float getY(); + + float getZ(); + + public void setPosition(final float x, final float y, final float z); + + public void setOrientation(final float forwardX, final float forwardY, final float forwardZ, final float upX, + final float upY, final float upZ); + + boolean is3DSupported(); + + Listener DO_NOTHING = new Listener() { + private float x; + private float y; + private float z; + + @Override + public void setPosition(final float x, final float y, final float z) { + this.x = x; + this.y = y; + this.z = z; + } + + @Override + public float getX() { + return x; + } + + @Override + public float getY() { + return y; + } + + @Override + public float getZ() { + return z; + } + + @Override + public void setOrientation(final float forwardX, final float forwardY, final float forwardZ, + final float upX, final float upY, final float upZ) { + + } + + @Override + public boolean is3DSupported() { + return false; + } + }; + } + + public AudioPanner createPanner() { + return new AudioPanner(this.listener) { + @Override + public void connect(final AudioDestination destination) { + } + }; + } + + public AudioBufferSource createBufferSource() { + return new AudioBufferSource(); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/AudioDestination.java b/core/src/com/etheller/warsmash/viewer5/AudioDestination.java new file mode 100644 index 0000000..d028de9 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/AudioDestination.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.viewer5; + +public interface AudioDestination { + +} diff --git a/core/src/com/etheller/warsmash/viewer5/AudioPanner.java b/core/src/com/etheller/warsmash/viewer5/AudioPanner.java new file mode 100644 index 0000000..a83b365 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/AudioPanner.java @@ -0,0 +1,39 @@ +package com.etheller.warsmash.viewer5; + +import com.etheller.warsmash.viewer5.AudioContext.Listener; + +public abstract class AudioPanner { + public Listener listener; + public float x; + public float y; + public float z; + + public AudioPanner(final Listener listener) { + this.listener = listener; + } + + public void setPosition(final float x, final float y, final float z) { + this.x = x; + this.y = y; + this.z = z; + } + + public void setDistances(final float maxDistance, final float refDistance) { + this.maxDistance = maxDistance; + this.refDistance = refDistance; + this.maxDistanceSq = maxDistance * maxDistance; + } + + public float maxDistance; + public float refDistance; + public float maxDistanceSq; + + public abstract void connect(AudioDestination destination); + + public boolean isWithinListenerDistance() { + final float dx = this.listener.getX() - this.x; + final float dy = this.listener.getY() - this.y; + final float dz = this.listener.getZ() - this.z; + return ((dx * dx) + (dy * dy) + (dz * dz)) <= this.maxDistanceSq; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/BatchedInstance.java b/core/src/com/etheller/warsmash/viewer5/BatchedInstance.java new file mode 100644 index 0000000..dca3214 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/BatchedInstance.java @@ -0,0 +1,16 @@ +package com.etheller.warsmash.viewer5; + +/** + * A batched model instance. + */ +public abstract class BatchedInstance extends ModelInstance { + + public BatchedInstance(final Model model) { + super(model); + } + + @Override + public boolean isBatched() { + return true; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/Bounds.java b/core/src/com/etheller/warsmash/viewer5/Bounds.java new file mode 100644 index 0000000..e00e044 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/Bounds.java @@ -0,0 +1,38 @@ +package com.etheller.warsmash.viewer5; + +import com.badlogic.gdx.math.Intersector; +import com.badlogic.gdx.math.Vector3; +import com.badlogic.gdx.math.collision.BoundingBox; +import com.badlogic.gdx.math.collision.Ray; + +public class Bounds { + public float x, y, z, r; + private BoundingBox boundingBox; + + public void fromExtents(final float[] min, final float[] max, final float boundsRadius) { + final float x = min[0]; + final float y = min[1]; + final float z = min[2]; + final float w = max[0] - x; + final float d = max[1] - y; + final float h = max[2] - z; + + this.x = x + (w / 2f); + this.y = y + (d / 2f); + this.z = z + (h / 2f); + this.r = boundsRadius > 0 ? boundsRadius : (float) (Math.max(Math.max(w, d), h) / 2.); + this.boundingBox = new BoundingBox(new Vector3(min), new Vector3(max)); + } + + public void intersectRay(final Ray ray, final Vector3 intersection) { + Intersector.intersectRayBounds(ray, this.boundingBox, intersection); + } + + public boolean intersectRayFast(final Ray ray) { + return Intersector.intersectRayBoundsFast(ray, this.boundingBox); + } + + public BoundingBox getBoundingBox() { + return this.boundingBox; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/Camera.java b/core/src/com/etheller/warsmash/viewer5/Camera.java new file mode 100644 index 0000000..1a97cd6 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/Camera.java @@ -0,0 +1,346 @@ +package com.etheller.warsmash.viewer5; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Quaternion; +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.util.RenderMathUtils; +import com.etheller.warsmash.util.Vector4; + +public class Camera { + private static final Vector3 vectorHeap = new Vector3(); + private static final Vector3 vectorHeap2 = new Vector3(); + private static final Vector3 vectorHeap3 = new Vector3(); + private static final Quaternion quatHeap = new Quaternion(); + private static final Matrix4 matHeap = new Matrix4(); + + public final Rectangle rect; + + private boolean isPerspective; + private float fov; + private float aspect; + + public boolean isOrtho; + private float leftClipPlane; + private float rightClipPlane; + private float bottomClipPlane; + private float topClipPlane; + + private float nearClipPlane; + private float farClipPlane; + + public final Vector3 location; + public final Quaternion rotation; + + public Quaternion inverseRotation; + /** + * World -> View. + */ + private final Matrix4 viewMatrix; + /** + * View -> Clip. + */ + private final Matrix4 projectionMatrix; + /** + * World -> Clip. + */ + public final Matrix4 viewProjectionMatrix; + /** + * View -> World. + */ + private final Matrix4 inverseViewMatrix; + /** + * Clip -> World. + */ + private final Matrix4 inverseViewProjectionMatrix; + public final Vector3 directionX; + public final Vector3 directionY; + public final Vector3 directionZ; + public final Vector3[] vectors; + public final Vector3[] billboardedVectors; + + public final Vector4[] planes; + private boolean dirty; + + public Camera() { + // rencered viewport + this.rect = new Rectangle(); + + // perspective values + this.isPerspective = true; + this.fov = 0; + this.aspect = 0; + + // Orthogonal values + this.isOrtho = false; + this.leftClipPlane = 0f; + this.rightClipPlane = 0f; + this.bottomClipPlane = 0f; + this.topClipPlane = 0f; + + // Shared values + this.nearClipPlane = 0f; + this.farClipPlane = 0f; + + // World values + this.location = new Vector3(); + this.rotation = new Quaternion(); + + // Derived values. + this.inverseRotation = new Quaternion(); + this.viewMatrix = new Matrix4(); + this.projectionMatrix = new Matrix4(); + this.viewProjectionMatrix = new Matrix4(); + this.inverseViewMatrix = new Matrix4(); + this.inverseViewProjectionMatrix = new Matrix4(); + this.directionX = new Vector3(); + this.directionY = new Vector3(); + this.directionZ = new Vector3(); + + // First four vectors are the corners of a 2x2 rectangle, the last three vectors + // are the unit axes + this.vectors = new Vector3[] { new Vector3(-1, -1, 0), new Vector3(-1, 1, 0), new Vector3(1, 1, 0), + new Vector3(1, -1, 0), new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, 1) }; + + // First four vectors are the corners of a 2x2 rectangle billboarded to the + // camera, the last three vectors are the unit axes billboarded + this.billboardedVectors = new Vector3[] { new Vector3(), new Vector3(), new Vector3(), new Vector3(), + new Vector3(), new Vector3(), new Vector3() }; + + // Left, right, top, bottom, near, far + this.planes = new Vector4[] { new Vector4(), new Vector4(), new Vector4(), new Vector4(), new Vector4(), + new Vector4() }; + + this.dirty = true; + } + + public void perspective(final float fov, final float aspect, final float near, final float far) { + this.isPerspective = true; + this.isOrtho = false; + this.fov = fov; + this.aspect = aspect; + this.nearClipPlane = near; + this.farClipPlane = far; + + this.dirty = true; + } + + public void ortho(final float left, final float right, final float bottom, final float top, final float near, + final float far) { + this.isPerspective = false; + this.isOrtho = true; + this.leftClipPlane = left; + this.rightClipPlane = right; + this.bottomClipPlane = bottom; + this.topClipPlane = top; + this.nearClipPlane = near; + this.farClipPlane = far; + + this.dirty = true; + } + + public void viewport(final Rectangle viewport) { + this.rect.set(viewport); + + this.aspect = viewport.width / viewport.height; + + this.dirty = true; + } + + public float getAspect() { + return this.aspect; + } + + public void setLocation(final Vector3 location) { + this.location.set(location); + + this.dirty = true; + } + + public void move(final Vector3 offset) { + this.location.add(offset); + + this.dirty = true; + } + + public void setRotation(final Quaternion rotation) { + this.rotation.set(rotation); + + this.dirty = true; + } + + public void rotate(final Quaternion rotation) { + this.rotation.mul(rotation); + + this.dirty = true; + } + + public void setRotationAngles(final float horizontalAngle, final float verticalAngle) { + this.rotation.idt(); +// this.rotateAngles(horizontalAngle, verticalAngle); + throw new UnsupportedOperationException( + "Ghostwolf called a function that does not exist, so I did not know what to do here"); + } + + public void rotateAround(final Quaternion rotation, final Vector3 point) { + this.rotate(rotation); + + quatHeap.conjugate(); // TODO ????????? + vectorHeap.set(this.location); + vectorHeap.sub(point); + rotation.transform(vectorHeap); + vectorHeap.add(point); + this.location.set(vectorHeap); + } + + public void setRotationAround(final Quaternion rotation, final Vector3 point) { + this.setRotation(rotation); + ; + + final float length = vectorHeap.set(this.location).sub(point).len(); + + quatHeap.conjugate(); // TODO ????????? + vectorHeap.set(RenderMathUtils.VEC3_UNIT_Z); + quatHeap.transform(vectorHeap); + vectorHeap.scl(length); + this.location.set(vectorHeap.add(point)); + } + + public void setRotationAroundAngles(final float horizontalAngle, final float verticalAngle, final Vector3 point) { + quatHeap.idt(); + RenderMathUtils.rotateX(quatHeap, quatHeap, verticalAngle); + RenderMathUtils.rotateY(quatHeap, quatHeap, horizontalAngle); + + this.setRotationAround(quatHeap, point); + } + + public void face(final Vector3 point, final Vector3 worldUp) { + matHeap.setToLookAt(this.location, point, worldUp); + matHeap.getRotation(this.rotation); + + this.dirty = true; + } + + public void moveToAndFace(final Vector3 location, final Vector3 target, final Vector3 worldUp) { + this.location.set(location); + this.face(target, worldUp); + } + + public void reset() { + this.location.set(0, 0, 0); + this.rotation.idt(); + + this.dirty = true; + } + + public void update() { + if (this.dirty) { + final Vector3 location = this.location; + final Quaternion rotation = this.rotation; + final Quaternion inverseRotation = this.inverseRotation; + final Matrix4 viewMatrix = this.viewMatrix; + final Matrix4 projectionMatrix = this.projectionMatrix; + final Matrix4 viewProjectionMatrix = this.viewProjectionMatrix; + final Vector3[] vectors = this.vectors; + final Vector3[] billboardedVectors = this.billboardedVectors; + + if (this.isPerspective) { + RenderMathUtils.perspective(projectionMatrix, this.fov, this.aspect, this.nearClipPlane, + this.farClipPlane); + } + else { + RenderMathUtils.ortho(projectionMatrix, this.leftClipPlane, this.rightClipPlane, this.bottomClipPlane, + this.topClipPlane, this.nearClipPlane, this.farClipPlane); + } + + rotation.toMatrix(viewMatrix.val); + viewMatrix.translate(vectorHeap.set(location).scl(-1)); + inverseRotation.set(rotation).conjugate(); + + // World projection matrix + // World space -> NDC space + viewProjectionMatrix.set(projectionMatrix).mul(viewMatrix); + + // Recalculate the camera's frustum planes + RenderMathUtils.unpackPlanes(this.planes, viewProjectionMatrix); + + // Inverse world matrix + // Camera space -> world space + this.inverseViewMatrix.set(viewMatrix).inv(); + + this.directionX.set(RenderMathUtils.VEC3_UNIT_X); + inverseRotation.transform(this.directionX); + this.directionY.set(RenderMathUtils.VEC3_UNIT_Y); + inverseRotation.transform(this.directionY); + this.directionZ.set(RenderMathUtils.VEC3_UNIT_Z); + inverseRotation.transform(this.directionZ); + + // Inverse world projection matrix + // NDC space -> World space + this.inverseViewProjectionMatrix.set(viewProjectionMatrix); + this.inverseViewProjectionMatrix.inv(); + + for (int i = 0; i < 7; i++) { + billboardedVectors[i].set(vectors[i]); + inverseRotation.transform(billboardedVectors[i]); + } + this.dirty = false; + } + } + + public boolean testSphere(final Vector3 center, final float radius) { + for (final Vector4 plane : this.planes) { + if (RenderMathUtils.distanceToPlane(plane, center) <= -radius) { + return false; + } + } + return true; + } + + public Vector3 cameraToWorld(final Vector3 out, final Vector3 v) { + return out.set(v).prj(this.inverseViewMatrix); + } + + public Vector3 worldToCamera(final Vector3 out, final Vector3 v) { + return out.set(v).prj(this.viewMatrix); + } + + public Vector2 worldToScreen(final Vector2 out, final Vector3 v) { + final Rectangle viewport = this.rect; + + vectorHeap.set(v); + vectorHeap.prj(this.viewProjectionMatrix); + + out.x = Math.round(((vectorHeap.x + 1) / 2) * viewport.width); + out.y = ((Gdx.graphics.getHeight() - viewport.y - viewport.height) + (viewport.height)) + - Math.round(((vectorHeap.y + 1) / 2) * viewport.height); + + return out; + } + + public float[] screenToWorldRay(final float[] out, final Vector2 v) { + final Vector3 a = vectorHeap; + final Vector3 b = vectorHeap2; + final Vector3 c = vectorHeap3; + final float x = v.x; + final float y = v.y; + final Rectangle viewport = this.rect; + + // Intersection on the near-plane + RenderMathUtils.unproject(a, c.set(x, y, 0), this.inverseViewProjectionMatrix, viewport); + + // Intersection on the far-plane + RenderMathUtils.unproject(b, c.set(x, y, 1), this.inverseViewProjectionMatrix, viewport); + + out[0] = a.x; + out[1] = a.y; + out[2] = a.z; + out[3] = b.x; + out[4] = b.y; + out[5] = b.z; + + return out; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/CanvasProvider.java b/core/src/com/etheller/warsmash/viewer5/CanvasProvider.java new file mode 100644 index 0000000..7d0aaab --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/CanvasProvider.java @@ -0,0 +1,7 @@ +package com.etheller.warsmash.viewer5; + +public interface CanvasProvider { + float getWidth(); + + float getHeight(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/EmittedObject.java b/core/src/com/etheller/warsmash/viewer5/EmittedObject.java new file mode 100644 index 0000000..24eec9d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/EmittedObject.java @@ -0,0 +1,15 @@ +package com.etheller.warsmash.viewer5; + +public abstract class EmittedObject>> { + public abstract void update(float dt); + + public float health; + public EMITTER emitter; + public int index; + + public EmittedObject(final EMITTER emitter) { + this.emitter = emitter; + } + + protected abstract void bind(int flags); +} diff --git a/core/src/com/etheller/warsmash/viewer5/EmittedObjectUpdater.java b/core/src/com/etheller/warsmash/viewer5/EmittedObjectUpdater.java new file mode 100644 index 0000000..e99816d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/EmittedObjectUpdater.java @@ -0,0 +1,43 @@ +package com.etheller.warsmash.viewer5; + +import java.util.ArrayList; +import java.util.List; + +public class EmittedObjectUpdater { + final List objects; + private int alive; + + public EmittedObjectUpdater() { + this.objects = new ArrayList<>(); + this.alive = 0; + } + + public void add(final EmittedObject object) { + this.objects.add(object); + this.alive++; + } + + public void update(final float dt) { + for (int i = 0; i < this.alive; i++) { + final EmittedObject object = this.objects.get(i); + + object.update(dt); + + if (object.health <= 0) { + this.alive -= 1; + + object.emitter.kill(object); + + // Swap between this object and the last living object. + // Decrement the iterator so the swapped object is updated this frame. + if (i != this.alive) { + this.objects.set(i, this.objects.remove(this.alive)); + i -= 1; + } + else { + this.objects.remove(this.alive); + } + } + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/Emitter.java b/core/src/com/etheller/warsmash/viewer5/Emitter.java new file mode 100644 index 0000000..690dc63 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/Emitter.java @@ -0,0 +1,82 @@ +package com.etheller.warsmash.viewer5; + +import java.util.ArrayList; +import java.util.List; + +public abstract class Emitter>> + implements UpdatableObject { + + public final MODEL_INSTANCE instance; + public final List objects; + public int alive; + protected float currentEmission; + + public Emitter(final MODEL_INSTANCE instance) { + this.instance = instance; + this.objects = new ArrayList<>(); + this.alive = 0; + this.currentEmission = 0; + } + + public final EMITTED_OBJECT emitObject(final int flags) { + if (this.alive == this.objects.size()) { + this.objects.add(this.createObject()); + } + + final EMITTED_OBJECT object = this.objects.get(this.alive); + + object.index = this.alive; + + object.bind(flags); + + this.alive += 1; + this.currentEmission -= 1; + + this.instance.scene.emitterObjectUpdater.add(object); + + return object; + } + + @Override + public void update(final float dt, final boolean objectVisible) { + if (!objectVisible) { + return; + } + this.updateEmission(dt); + + final float currentEmission = this.currentEmission; + + if (currentEmission >= 1) { + for (int i = 0; i < currentEmission; i += 1) { + this.emit(); + } + } + } + + public void kill(final EMITTED_OBJECT object) { + this.alive -= 1; + + final EMITTED_OBJECT otherObject = this.objects.get(this.alive); + if (object.index == -1) { + System.err.println("bad"); + } + this.objects.set(object.index, otherObject); + this.objects.set(this.alive, object); + + otherObject.index = object.index; + object.index = -1; + } + + public final void clear() { + for (int i = 0; i < this.alive; i++) { + this.objects.get(i).health = 0; + } + this.currentEmission = 0; + } + + protected abstract void updateEmission(float dt); + + protected abstract void emit(); + + protected abstract EMITTED_OBJECT createObject(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/GdxTextureResource.java b/core/src/com/etheller/warsmash/viewer5/GdxTextureResource.java new file mode 100644 index 0000000..aff666b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/GdxTextureResource.java @@ -0,0 +1,63 @@ +package com.etheller.warsmash.viewer5; + +import com.badlogic.gdx.graphics.Texture.TextureWrap; +import com.etheller.warsmash.viewer5.handlers.ResourceHandler; + +public abstract class GdxTextureResource extends Texture { + private com.badlogic.gdx.graphics.Texture gdxTexture; + + public GdxTextureResource(final ModelViewer viewer, final ResourceHandler handler, final String extension, + final PathSolver pathSolver, final String fetchUrl) { + super(viewer, extension, pathSolver, fetchUrl, handler); + } + + public void setGdxTexture(final com.badlogic.gdx.graphics.Texture gdxTexture) { + this.gdxTexture = gdxTexture; + } + + @Override + protected void error(final Exception e) { + e.printStackTrace(); + } + + @Override + public void bind(final int unit) { + this.viewer.webGL.bindTexture(this, unit); + } + + @Override + public void internalBind() { + this.gdxTexture.bind(); + } + + @Override + public int getWidth() { + return this.gdxTexture.getWidth(); + } + + @Override + public int getHeight() { + return this.gdxTexture.getHeight(); + } + + @Override + public int getGlTarget() { + return this.gdxTexture.glTarget; + } + + @Override + public int getGlHandle() { + return this.gdxTexture.getTextureObjectHandle(); + } + + @Override + public void setWrapS(final boolean wrapS) { + this.gdxTexture.setWrap(wrapS ? TextureWrap.Repeat : TextureWrap.ClampToEdge, this.gdxTexture.getVWrap()); + } + + @Override + public void setWrapT(final boolean wrapT) { + this.gdxTexture.setWrap(this.gdxTexture.getUWrap(), wrapT ? TextureWrap.Repeat : TextureWrap.ClampToEdge); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/GenericNode.java b/core/src/com/etheller/warsmash/viewer5/GenericNode.java new file mode 100644 index 0000000..bc4f4f0 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/GenericNode.java @@ -0,0 +1,33 @@ +package com.etheller.warsmash.viewer5; + +import java.util.List; + +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Quaternion; +import com.badlogic.gdx.math.Vector3; + +public abstract class GenericNode { + + public Vector3 pivot; + public Vector3 localLocation; + public Quaternion localRotation; + public Vector3 localScale; + public Vector3 worldLocation; + public Quaternion worldRotation; + public Vector3 worldScale; + public Vector3 inverseWorldLocation; + public Quaternion inverseWorldRotation; + public Vector3 inverseWorldScale; + public Matrix4 localMatrix; + public Matrix4 worldMatrix; + public GenericNode parent; + public List children; + public boolean dontInheritTranslation; + public boolean dontInheritRotation; + public boolean dontInheritScaling; + public boolean visible; + public boolean wasDirty; + public boolean dirty; + + protected abstract void update(float dt, Scene scene); +} diff --git a/core/src/com/etheller/warsmash/viewer5/GenericResource.java b/core/src/com/etheller/warsmash/viewer5/GenericResource.java new file mode 100644 index 0000000..79ff358 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/GenericResource.java @@ -0,0 +1,35 @@ +package com.etheller.warsmash.viewer5; + +import java.io.InputStream; + +import com.etheller.warsmash.common.LoadGenericCallback; + +public final class GenericResource extends Resource { + + public Object data; // TODO this likely won't work, just brainstorming until I get to the part of + // using the data + private final LoadGenericCallback callback; + + public GenericResource(final ModelViewer viewer, final String extension, final PathSolver pathSolver, + final String fetchUrl, final LoadGenericCallback callback) { + super(viewer, extension, pathSolver, fetchUrl); + this.callback = callback; + } + + @Override + protected void lateLoad() { + + } + + @Override + protected void load(final InputStream src, final Object options) { + this.data = this.callback.call(src); + + } + + @Override + protected void error(final Exception e) { + e.printStackTrace(); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/Grid.java b/core/src/com/etheller/warsmash/viewer5/Grid.java new file mode 100644 index 0000000..ee2b427 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/Grid.java @@ -0,0 +1,120 @@ +package com.etheller.warsmash.viewer5; + +import com.badlogic.gdx.math.Vector3; + +public class Grid { + private final float x; + private final float y; + private final int width; + private final int depth; + private final int cellWidth; + private final int cellDepth; + private final int columns; + private final int rows; + final GridCell[] cells; + + public Grid(final float x, final float y, final int width, final int depth, final int cellWidth, + final int cellDepth) { + final int columns = width / cellWidth; + final int rows = depth / cellDepth; + + this.x = x; + this.y = y; + this.width = width; + this.depth = depth; + this.cellWidth = cellWidth; + this.cellDepth = cellDepth; + this.columns = columns; + this.rows = rows; + this.cells = new GridCell[rows * columns]; + + for (int row = 0; row < rows; row++) { + for (int column = 0; column < columns; column++) { + final float left = x + (column * cellWidth); + final float right = left + cellWidth; + final float bottom = y + (row * cellDepth); + final float top = bottom + cellDepth; + + this.cells[(row * columns) + column] = new GridCell(left, right, bottom, top); + } + } + } + + public void add(final ModelInstance instance) { + final int left = instance.left; + final int right = instance.right + 1; + final int bottom = instance.bottom; + final int top = instance.top + 1; + + if (left != -1) { + for (int y = bottom; y < top; y++) { + for (int x = left; x < right; x++) { + this.cells[(y * this.columns) + x].add(instance); + } + } + } + } + + public void remove(final ModelInstance instance) { + final int left = instance.left; + final int right = instance.right + 1; + final int bottom = instance.bottom; + final int top = instance.top + 1; + + if (left != -1) { + instance.left = -1; + + for (int y = bottom; y < top; y++) { + for (int x = left; x < right; x++) { + this.cells[(y * this.columns) + x].remove(instance); + } + } + } + } + + public void moved(final ModelInstance instance, final float upcomingX, final float upcomingY) { + final Bounds bounds = instance.model.bounds; + final float x = (upcomingX + bounds.x) - this.x; + final float y = (upcomingY + bounds.y) - this.y; + final float r = bounds.r; + final Vector3 s = instance.worldScale; + int left = (int) (Math.floor((x - (r * s.x)) / this.cellWidth)); + int right = (int) (Math.floor((x + (r * s.x)) / this.cellWidth)); + int bottom = (int) (Math.floor((y - (r * s.y)) / this.cellDepth)); + int top = (int) (Math.floor((y + (r * s.y)) / this.cellDepth)); + + if ((right < 0) || (left > (this.columns - 1)) || (top < 0) || (bottom > (this.rows - 1))) { + // The instance is outside of the grid, so remove it. + this.remove(instance); + } + else { + // Clamp the values so they are in the grid. + left = Math.max(left, 0); + right = Math.min(right, this.columns - 1); + bottom = Math.max(bottom, 0); + top = Math.min(top, this.rows - 1); + + // If the values actually changed, update the cells. + if ((left != instance.left) || (right != instance.right) || (bottom != instance.bottom) + || (top != instance.top)) { + /// TODO: This can be optimized by checking if there are shared cells. + /// That can be done in precisely the same way as done a few lines above, i.e. + /// simple rectangle intersection. + this.remove(instance); + + instance.left = left; + instance.right = right; + instance.bottom = bottom; + instance.top = top; + + this.add(instance); + } + } + } + + public void clear() { + for (final GridCell cell : this.cells) { + cell.clear(); + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/GridCell.java b/core/src/com/etheller/warsmash/viewer5/GridCell.java new file mode 100644 index 0000000..a3c035d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/GridCell.java @@ -0,0 +1,44 @@ +package com.etheller.warsmash.viewer5; + +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.util.RenderMathUtils; + +public class GridCell { + public final float left; + public final float right; + public final float bottom; + public final float top; + public int plane; + final List instances; + public final boolean visible; + + public GridCell(final float left, final float right, final float bottom, final float top) { + this.left = left; + this.right = right; + this.bottom = bottom; + this.top = top; + this.plane = -1; + this.instances = new ArrayList(); + this.visible = false; + } + + public void add(final ModelInstance instance) { + this.instances.add(instance); + } + + public void remove(final ModelInstance instance) { + this.instances.remove(instance); + } + + public void clear() { + this.instances.clear(); + } + + public boolean isVisible(final Camera camera) { + this.plane = RenderMathUtils.testCell(camera.planes, this.left, this.right, this.bottom, this.top, this.plane); + + return this.plane == -1; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/HandlerResource.java b/core/src/com/etheller/warsmash/viewer5/HandlerResource.java new file mode 100644 index 0000000..fac3d88 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/HandlerResource.java @@ -0,0 +1,14 @@ +package com.etheller.warsmash.viewer5; + +import com.etheller.warsmash.viewer5.handlers.ResourceHandler; + +public abstract class HandlerResource extends Resource { + public final HANDLER handler; + + public HandlerResource(final ModelViewer viewer, final String extension, final PathSolver pathSolver, + final String fetchUrl, final HANDLER handler) { + super(viewer, extension, pathSolver, fetchUrl); + this.handler = handler; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/Model.java b/core/src/com/etheller/warsmash/viewer5/Model.java new file mode 100644 index 0000000..fbedd8f --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/Model.java @@ -0,0 +1,37 @@ +package com.etheller.warsmash.viewer5; + +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.viewer5.handlers.ModelHandler; + +public abstract class Model extends HandlerResource { + public final Bounds bounds; + public List preloadedInstances; + + public Model(final HANDLER handler, final ModelViewer viewer, final String extension, final PathSolver pathSolver, + final String fetchUrl) { + super(viewer, extension, pathSolver, fetchUrl, handler); + this.bounds = new Bounds(); + this.preloadedInstances = new ArrayList<>(); + } + + protected abstract ModelInstance createInstance(int type); + + public ModelInstance addInstance() { + return addInstance(0); + } + + public ModelInstance addInstance(final int type) { + final ModelInstance instance = createInstance(type); + + if (this.ok) { + instance.load(); + } + else { + this.preloadedInstances.add(instance); + } + + return instance; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/ModelInstance.java b/core/src/com/etheller/warsmash/viewer5/ModelInstance.java new file mode 100644 index 0000000..c145516 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/ModelInstance.java @@ -0,0 +1,229 @@ +package com.etheller.warsmash.viewer5; + +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.util.RenderMathUtils; +import com.etheller.warsmash.util.Vector4; + +public abstract class ModelInstance extends Node { + + public int left; + public int right; + public int bottom; + public int top; + public int plane; + public float depth; + public int updateFrame; + public int cullFrame; + public Model model; + public TextureMapper textureMapper; + public boolean paused; + public boolean rendered; + + public Scene scene; + + public ModelInstance(final Model model) { + this.scene = null; + this.left = -1; + this.right = -1; + this.bottom = -1; + this.top = -1; + this.plane = -1; + this.depth = 0; + this.updateFrame = 0; + this.cullFrame = 0; + this.model = model; + this.textureMapper = model.viewer.baseTextureMapper(this); + this.paused = false; + this.rendered = true; + } + + public void setTexture(final int index, final Texture texture) { + this.textureMapper = this.model.viewer.changeTextureMapper(this, index, texture); + } + + public void show() { + this.rendered = true; + } + + public void hide() { + this.rendered = false; + } + + public boolean shown() { + return this.rendered; + } + + public boolean hidden() { + return !this.rendered; + } + + public boolean detach() { + if (this.scene != null) { + return this.scene.removeInstance(this); + } + + return false; + } + + public abstract void updateAnimations(float dt); + + public abstract void clearEmittedObjects(); + + @Override + protected final void updateObject(final float dt, final Scene scene) { + if (this.updateFrame < this.model.viewer.frame) { + if (this.rendered && !this.paused) { + this.updateAnimations(dt); + } + } + + this.updateLights(scene); + + this.updateFrame = this.model.viewer.frame; + } + + protected abstract void updateLights(Scene scene2); + + public boolean setScene(final Scene scene) { + return scene.addInstance(this); + } + + @Override + public Node move(final float[] offset) { + final Node result = super.move(offset); + updateSceneGridLocationInfo(); + return result; + } + + @Override + public Node moveTo(final float[] offset) { + final Node result = super.moveTo(offset); + updateSceneGridLocationInfo(); + return result; + } + + @Override + public Node setLocation(final float x, final float y, final float z) { + final Node result = super.setLocation(x, y, z); + updateSceneGridLocationInfo(); + return result; + } + + @Override + public Node setLocation(final float[] location) { + final Node result = super.setLocation(location); + updateSceneGridLocationInfo(); + return result; + } + + @Override + public Node setLocation(final Vector3 location) { + final Node result = super.setLocation(location); + updateSceneGridLocationInfo(); + return result; + } + + private void updateSceneGridLocationInfo() { + if (this.scene != null) { + // can't just use world location if it moves + float x, y; + if (this.dirty) { + // TODO this is an incorrect, predicted location for dirty case + if ((this.parent != null) && !this.dontInheritTranslation) { + x = this.parent.localLocation.x + this.localLocation.x; + y = this.parent.localLocation.y + this.localLocation.y; + } + else { + x = this.localLocation.x; + y = this.localLocation.y; + } + } + else { + x = this.worldLocation.x; + y = this.worldLocation.y; + } + this.scene.instanceMoved(this, x, y); + } + } + + @Override + public void recalculateTransformation() { + super.recalculateTransformation(); + + if (this.scene != null) { + this.scene.instanceMoved(this, this.worldLocation.x, this.worldLocation.y); + } + } + + public boolean isVisible(final Camera camera) { + // can't just use world location if it moves + float x, y, z; + float sx, sy, sz; + if (this.dirty) { + // TODO this is an incorrect, predicted location for dirty case + if ((this.parent != null) && !this.dontInheritTranslation) { + x = this.parent.localLocation.x + this.localLocation.x; + y = this.parent.localLocation.y + this.localLocation.y; + z = this.parent.localLocation.z + this.localLocation.z; + sx = this.parent.localScale.x * this.localScale.x; + sy = this.parent.localScale.y * this.localScale.y; + sz = this.parent.localScale.z * this.localScale.z; + } + else { + x = this.localLocation.x; + y = this.localLocation.y; + z = this.localLocation.z; + sx = this.localScale.x; + sy = this.localScale.y; + sz = this.localScale.z; + } + } + else { + x = this.worldLocation.x; + y = this.worldLocation.y; + z = this.worldLocation.z; + sx = this.worldScale.x; + sy = this.worldScale.y; + sz = this.worldScale.z; + } + // Get the biggest scaling dimension. + if (sy > sx) { + sx = sy; + } + + if (sz > sx) { + sx = sz; + } + + final Bounds bounds = this.model.bounds; + final Vector4[] planes = camera.planes; + + this.plane = RenderMathUtils.testSphere(planes, x + bounds.x, y + bounds.y, z + bounds.z, bounds.r * sx, + this.plane); + + if (this.plane == -1) { + this.depth = RenderMathUtils.distanceToPlane3(planes[4], x, y, z); + + return true; + } + + return false; + } + + public boolean isBatched() { + return false; + } + + public abstract void renderOpaque(Matrix4 mvp); + + public abstract void renderTranslucent(); + + public abstract void load(); + + protected abstract RenderBatch getBatch(TextureMapper textureMapper2); + + public abstract void setReplaceableTexture(int replaceableTextureId, String replaceableTextureFile); + + protected abstract void removeLights(Scene scene2); +} diff --git a/core/src/com/etheller/warsmash/viewer5/ModelViewer.java b/core/src/com/etheller/warsmash/viewer5/ModelViewer.java new file mode 100644 index 0000000..730d242 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/ModelViewer.java @@ -0,0 +1,380 @@ +package com.etheller.warsmash.viewer5; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL20; +import com.etheller.warsmash.common.FetchDataTypeName; +import com.etheller.warsmash.common.LoadGenericCallback; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.viewer5.gl.ClientBuffer; +import com.etheller.warsmash.viewer5.gl.WebGL; +import com.etheller.warsmash.viewer5.handlers.ResourceHandler; +import com.etheller.warsmash.viewer5.handlers.ResourceHandlerConstructionParams; + +public abstract class ModelViewer { + public DataSource dataSource; + public final CanvasProvider canvas; + public List resources; + public Map fetchCache; + public int frameTime; + public GL20 gl; + public WebGL webGL; + public List scenes; + private int visibleCells; + private int visibleInstances; + private int updatedParticles; + public int frame; + public final int rectBuffer; + public ClientBuffer buffer; + public boolean audioEnabled; + private final Map> textureMappers; + private final Set handlers; + + public ModelViewer(final DataSource dataSource, final CanvasProvider canvas) { + this.dataSource = dataSource; + this.canvas = canvas; + this.resources = new ArrayList<>(); + this.fetchCache = new HashMap<>(); + this.handlers = new HashSet(); + this.frameTime = 1000 / 60; + this.gl = Gdx.gl; + this.webGL = new WebGL(this.gl); + this.scenes = new ArrayList<>(); + this.visibleCells = 0; + this.visibleInstances = 0; + this.updatedParticles = 0; + + this.frame = 0; + + this.rectBuffer = this.gl.glGenBuffer(); + this.buffer = new ClientBuffer(this.gl); + this.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.rectBuffer); + final ByteBuffer temp = ByteBuffer.allocateDirect(6).order(ByteOrder.nativeOrder()); + temp.put((byte) 0); + temp.put((byte) 1); + temp.put((byte) 2); + temp.put((byte) 0); + temp.put((byte) 2); + temp.put((byte) 3); + temp.clear(); + this.gl.glBufferData(GL20.GL_ARRAY_BUFFER, temp.capacity(), temp, GL20.GL_STATIC_DRAW); + this.audioEnabled = false; + this.textureMappers = new HashMap>(); + } + + public void setDataSource(final DataSource dataSource) { + this.dataSource = dataSource; + } + + public boolean enableAudio() { + this.audioEnabled = true; + return this.audioEnabled; + } + + public boolean addHandler(ResourceHandler handler) { + if (handler != null) { + + // Allow to pass also the handler's module for convenience. + if (handler.handler != null) { + handler = handler.handler; + } + + if (!this.handlers.contains(handler)) { + // Check if the handler has a loader, and if so load it. + if (handler.load && !handler.load(this)) { + onResourceLoadError(); + return false; + } + + this.handlers.add(handler); + + return true; + } + } + return false; + } + + public Scene addSimpleScene() { + final Scene scene = new SimpleScene(this, createLightManager(true)); + + this.scenes.add(scene); + + return scene; + } + + public WorldScene addWorldScene() { + final WorldScene scene = new WorldScene(this, createLightManager(false)); + + this.scenes.add(scene); + + return scene; + } + + public boolean removeScene(final Scene scene) { + return this.scenes.remove(scene); + } + + public void clear() { + this.scenes.clear(); + } + + public Object[] findHandler(final String ext) { + for (final ResourceHandler handler : this.handlers) { + for (final String[] extension : handler.extensions) { + if (extension[0].equals(ext)) { + return new Object[] { handler, extension[1] }; + } + } + } + return null; + } + + public Resource load(final String src, final PathSolver pathSolver, final Object solverParams) { + String finalSrc = src; + String extension = ""; + boolean isFetch = false; + + // If a given path solver, resolve. + if (pathSolver != null) { + final SolvedPath solved = pathSolver.solve(src, solverParams); + + finalSrc = solved.getFinalSrc(); + if (!this.dataSource.has(finalSrc)) { + final String ddsPath = finalSrc.substring(0, finalSrc.lastIndexOf('.')) + ".dds"; + if (this.dataSource.has(ddsPath)) { + finalSrc = ddsPath; + } + else { + System.err.println("Attempting to load non-existant file: " + finalSrc); + } + } + extension = solved.getExtension(); + isFetch = solved.isFetch(); + + if (!(extension instanceof String)) { + throw new IllegalStateException("The path solver did not return an extension!"); + } + + if (extension.charAt(0) != '.') { + extension = '.' + extension; + } + // Built-in texture sources + // ---- TODO not using JS code here + + final Object[] handlerAndDataType = this.findHandler(extension.toLowerCase()); + + // Is there a handler for this file type? + if (handlerAndDataType != null) { + if (isFetch) { + final Resource resource = this.fetchCache.get(finalSrc); + + if (resource != null) { + return resource; + } + } + + final ResourceHandler handler = (ResourceHandler) handlerAndDataType[0]; + final Resource resource = handler.construct(new ResourceHandlerConstructionParams(this, handler, + extension, pathSolver, isFetch ? finalSrc : "")); + + this.resources.add(resource); + +// if (isFetch) { + this.fetchCache.put(finalSrc, resource); +// } + + // TODO this is a synchronous hack, skipped some Ghostwolf code + try { + resource.loadData(this.dataSource.getResourceAsStream(finalSrc), null); + } + catch (final IOException e) { + throw new IllegalStateException("Unable to load data: " + finalSrc); + } + + return resource; + } + else { + throw new IllegalStateException("Missing handler for: " + finalSrc); + } + } + else { + throw new IllegalStateException( + "Could not resolve " + finalSrc + ". Did you forget to pass a path solver?"); + } + + } + + public boolean has(final String key) { + return this.fetchCache.containsKey(key); + } + + public Resource get(final String key) { + return this.fetchCache.get(key); + } + + public GenericResource loadGeneric(final String path, final FetchDataTypeName dataType, + final LoadGenericCallback callback) { + return loadGeneric(path, dataType, callback, this.dataSource); + } + + /** + * Load something generic. + * + * Unlike load(), this does not use handlers or construct any internal objects. + * + * `dataType` can be one of: `"image"`, `"string"`, `"arrayBuffer"`, `"blob"`. + * + * If `callback` isn't given, the resource's `data` is the fetch data, according + * to `dataType`. + * + * If `callback` is given, the resource's `data` is the value returned by it + * when called with the fetch data. + * + * If `callback` returns a promise, the resource's `data` will be whatever the + * promise resolved to. + */ + public GenericResource loadGeneric(final String path, final FetchDataTypeName dataType, + final LoadGenericCallback callback, final DataSource dataSource) { + final Resource cachedResource = this.fetchCache.get(path); + + if (cachedResource != null) { + // Technically also non-generic resources can be returned here, since the fetch + // cache is shared. + // That being said, this should be used for generic resources, and it makes the + // typing a lot easier. + return (GenericResource) cachedResource; + } + + final GenericResource resource = new GenericResource(this, null, null, path, callback); + + this.resources.add(resource); + this.fetchCache.put(path, resource); + + // TODO this is a synchronous hack, skipped some Ghostwolf code + try { + resource.loadData(dataSource.getResourceAsStream(path), null); + } + catch (final IOException e) { + throw new IllegalStateException("Unable to load data: " + path); + } + + return resource; + + } + + public void updateAndRender() { + this.update(); + this.startFrame(); + this.render(); + } + +// public Resource loadGeneric(String path, String dataType, ) + + public boolean unload(final Resource resource) { + // TODO Auto-generated method stub + final String fetchUrl = resource.fetchUrl; + if (!"".equals(fetchUrl)) { + this.fetchCache.remove(fetchUrl); + } + return this.resources.remove(resource); + } + + public void update() { + final float dt = Gdx.graphics.getRawDeltaTime();// this.frameTime * 0.001f; + + this.frame += 1; + + this.visibleCells = 0; + this.visibleInstances = 0; + this.updatedParticles = 0; + + for (final Scene scene : this.scenes) { + scene.update(dt); + + this.visibleCells += scene.visibleCells; + this.visibleInstances += scene.visibleInstances; + this.updatedParticles += scene.updatedParticles; + } + } + + public void startFrame() { + Gdx.gl.glScissor(0, 0, (int) this.canvas.getWidth(), (int) this.canvas.getHeight()); + this.gl.glDepthMask(true); + this.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT); +// WarsmashGdxGame.bindDefaultVertexArray(); + } + + public void render() { + for (final Scene scene : this.scenes) { + scene.startFrame(); + } + this.renderOpaque(); + this.renderTranslucent(); + } + + private void renderOpaque() { + for (final Scene scene : this.scenes) { + scene.renderOpaque(); + } + } + + private void renderTranslucent() { + for (final Scene scene : this.scenes) { + scene.renderTranslucent(); + } + } + + public TextureMapper baseTextureMapper(final ModelInstance instance) { + final Model model = instance.model; + List mappers = this.textureMappers.get(model); + if (mappers == null) { + mappers = new ArrayList<>(); + this.textureMappers.put(model, mappers); + } + if (mappers.isEmpty()) { + mappers.add(new TextureMapper(model)); + } + return mappers.get(0); + } + + public TextureMapper changeTextureMapper(final ModelInstance instance, final Object key, final Texture texture) { + final Map map = new HashMap<>(instance.textureMapper.textures); + + if (texture instanceof Texture) { // not null? + map.put(key, texture); + } + else { + map.remove(key); + } + + final Model model = instance.model; + final List mappers = this.textureMappers.get(model); + + for (final TextureMapper mapper : mappers) { + if (mapper.textures.equals(map)) { + return mapper; + } + } + + final TextureMapper mapper = new TextureMapper(model, map); + + mappers.add(mapper); + + return mapper; + } + + private void onResourceLoadError() { + System.err.println("error, this, InvalidHandler, FailedToLoad"); + } + + public abstract SceneLightManager createLightManager(boolean simple); +} diff --git a/core/src/com/etheller/warsmash/viewer5/Node.java b/core/src/com/etheller/warsmash/viewer5/Node.java new file mode 100644 index 0000000..be2d95a --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/Node.java @@ -0,0 +1,327 @@ +package com.etheller.warsmash.viewer5; + +import java.util.ArrayList; +import java.util.List; + +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Quaternion; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.util.Descriptor; +import com.etheller.warsmash.util.RenderMathUtils; + +public abstract class Node extends GenericNode { + protected static final Vector3 locationHeap = new Vector3(); + protected static final Quaternion rotationHeap = new Quaternion(); + protected static final Vector3 scalingHeap = new Vector3(); + + public Node() { + this.pivot = new Vector3(); + this.localLocation = new Vector3(); + this.localRotation = new Quaternion(); + this.localScale = new Vector3(1, 1, 1); + this.worldLocation = new Vector3(); + this.worldRotation = new Quaternion(); + this.worldScale = new Vector3(1, 1, 1); + this.inverseWorldLocation = new Vector3(); + this.inverseWorldRotation = new Quaternion(); + this.inverseWorldScale = new Vector3(); + this.localMatrix = new Matrix4(); +// this.localMatrix.val[0] = 1; +// this.localMatrix.val[5] = 1; +// this.localMatrix.val[10] = 1; +// this.localMatrix.val[15] = 1; + this.worldMatrix = new Matrix4(); + this.parent = null; + this.children = new ArrayList<>(); + this.dontInheritTranslation = false; + this.dontInheritRotation = false; + this.dontInheritScaling = false; + + this.visible = true; + this.wasDirty = false; + this.dirty = true; + } + + public Node setPivot(final float[] pivot) { + this.pivot.set(pivot); + this.dirty = true; + return this; + } + + public Node setLocation(final float x, final float y, final float z) { + this.localLocation.set(x, y, z); + this.dirty = true; + return this; + } + + public Node setLocation(final float[] location) { + this.localLocation.set(location); + this.dirty = true; + return this; + } + + public Node setLocation(final Vector3 location) { + this.localLocation.set(location); + this.dirty = true; + return this; + } + + public Node setRotation(final float[] rotation) { + this.localRotation.set(rotation[0], rotation[1], rotation[2], rotation[3]); + this.dirty = true; + return this; + } + + public Node setScale(final float[] varying) { + this.localScale.set(varying); + this.dirty = true; + return this; + } + + public Node setUniformScale(final float uniform) { + this.localScale.set(uniform, uniform, uniform); + this.dirty = true; + return this; + } + + public Node setTransformation(final Vector3 location, final Quaternion rotation, final Vector3 scale) { + // TODO for performance, Ghostwolf did a direct field write on everything here. + // I'm hoping we can get Java's JIT to just figure it out and do it on its own + this.localLocation.set(location); + this.localRotation.set(rotation); + this.localScale.set(scale); + this.dirty = true; + return this; + } + + public Node resetTransformation() { + this.pivot.set(Vector3.Zero); + this.localLocation.set(Vector3.Zero); + this.localRotation.set(RenderMathUtils.QUAT_DEFAULT); + this.localScale.set(RenderMathUtils.VEC3_ONE); + + this.dirty = true; + return this; + } + + public Node movePivot(final float[] offset) { + this.pivot.add(offset[0], offset[1], offset[2]); + + this.dirty = true; + + return this; + } + + public Node move(final float[] offset) { + this.localLocation.add(offset[0], offset[1], offset[2]); + + this.dirty = true; + + return this; + } + + public Node moveTo(final float[] offset) { + this.localLocation.set(offset[0], offset[1], offset[2]); + + this.dirty = true; + + return this; + } + + public Node rotate(final Quaternion rotation) { + RenderMathUtils.mul(this.localRotation, this.localRotation, rotation); + + this.dirty = true; + + return this; + } + + public Node setLocalRotation(final Quaternion rotation) { + this.localRotation.set(rotation); + + this.dirty = true; + + return this; + } + + public Node rotateLocal(final Quaternion rotation) { + RenderMathUtils.mul(this.localRotation, rotation, this.localRotation); + + this.dirty = true; + + return this; + } + + public Node scale(final float[] scale) { + this.localScale.x *= scale[0]; + this.localScale.y *= scale[1]; + this.localScale.z *= scale[2]; + + this.dirty = true; + + return this; + } + + public Node uniformScale(final float scale) { + this.localScale.x *= scale; + this.localScale.y *= scale; + this.localScale.z *= scale; + + this.dirty = true; + + return this; + } + + public Node setParent(final GenericNode parent) { + if (this.parent != null) { + this.parent.children.remove(this); + } + + this.parent = parent; + + if (parent != null) { + parent.children.add(this); + } + + this.dirty = true; + + return this; + } + + public void recalculateTransformation() { + boolean dirty = this.dirty; + final GenericNode parent = this.parent; + + this.wasDirty = this.dirty; + + if (parent != null) { + dirty = dirty || parent.wasDirty; + } + + this.wasDirty = dirty; + + if (dirty) { + this.dirty = false; + + if (parent != null) { + Vector3 computedLocation; + Quaternion computedRotation; + Vector3 computedScaling; + + final Vector3 parentPivot = parent.pivot; + + computedLocation = locationHeap; + computedLocation.x = this.localLocation.x + parentPivot.x; + computedLocation.y = this.localLocation.y + parentPivot.y; + computedLocation.z = this.localLocation.z + parentPivot.z; + + if (this.dontInheritRotation) { + computedRotation = rotationHeap; + + computedRotation.set(this.localRotation); + computedRotation.mul(parent.inverseWorldRotation); + } + else { + computedRotation = this.localRotation; + } + + if (this.dontInheritScaling) { + computedScaling = scalingHeap; + + final Vector3 parentInverseScale = parent.inverseWorldScale; + computedScaling.x = parentInverseScale.x * this.localScale.x; + computedScaling.y = parentInverseScale.y * this.localScale.y; + computedScaling.z = parentInverseScale.z * this.localScale.z; + + this.worldScale.x = this.localScale.x; + this.worldScale.y = this.localScale.y; + this.worldScale.z = this.localScale.z; + } + else { + computedScaling = this.localScale; + + final Vector3 parentScale = parent.worldScale; + this.worldScale.x = parentScale.x * this.localScale.x; + this.worldScale.y = parentScale.y * this.localScale.y; + this.worldScale.z = parentScale.z * this.localScale.z; + } + + RenderMathUtils.fromRotationTranslationScale(computedRotation, computedLocation, computedScaling, + this.localMatrix); + + RenderMathUtils.mul(this.worldMatrix, parent.worldMatrix, this.localMatrix); + + RenderMathUtils.mul(this.worldRotation, parent.worldRotation, computedRotation); + } + else { + RenderMathUtils.fromRotationTranslationScale(this.localRotation, this.localLocation, this.localScale, + this.localMatrix); + + this.worldMatrix.set(this.localMatrix); + + this.worldRotation.set(this.localRotation); + + this.worldScale.set(this.localScale); + } + } + + // Inverse world rotation + this.inverseWorldRotation.x = -this.worldRotation.x; + this.inverseWorldRotation.y = -this.worldRotation.y; + this.inverseWorldRotation.z = -this.worldRotation.z; + this.inverseWorldRotation.w = this.worldRotation.w; + + // Inverse world scale + this.inverseWorldScale.x = 1 / this.worldScale.x; + this.inverseWorldScale.y = 1 / this.worldScale.y; + this.inverseWorldScale.z = 1 / this.worldScale.z; + + // World location + this.worldLocation.x = this.worldMatrix.val[Matrix4.M03]; + this.worldLocation.y = this.worldMatrix.val[Matrix4.M13]; + this.worldLocation.z = this.worldMatrix.val[Matrix4.M23]; + + // Inverse world location + this.inverseWorldLocation.x = -this.worldLocation.x; + this.inverseWorldLocation.y = -this.worldLocation.y; + this.inverseWorldLocation.z = -this.worldLocation.z; + + } + + @Override + public void update(final float dt, final Scene scene) { + if (this.dirty || ((this.parent != null) && this.parent.wasDirty)) { + this.dirty = true; // in case this node isn't dirty, but the parent was + this.wasDirty = true; + this.recalculateTransformation(); + } + else { + this.wasDirty = false; + } + + this.updateObject(dt, scene); + this.updateChildren(dt, scene); + } + + protected abstract void updateObject(float dt, Scene scene); + + private void updateChildren(final float dt, final Scene scene) { + final int childrenSize = this.children.size(); + for (int i = 0; i < childrenSize; i++) { + this.children.get(i).update(dt, scene); + } + } + + public static Object[] createSkeletalNodes(final int count, + final Descriptor nodeDescriptor) { + final List nodes = new ArrayList<>(); + final List worldMatrices = new ArrayList<>(); + for (int i = 0; i < count; i++) { + final NODE node = nodeDescriptor.create(); + nodes.add(node); + worldMatrices.add(node.worldMatrix); + } + final Object[] data = { nodes, worldMatrices }; + return data; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/PathSolver.java b/core/src/com/etheller/warsmash/viewer5/PathSolver.java new file mode 100644 index 0000000..159e097 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/PathSolver.java @@ -0,0 +1,31 @@ +package com.etheller.warsmash.viewer5; + +public interface PathSolver { + SolvedPath solve(String src, Object solverParams); + + // We generally just use the default path solver. + // These things were apparently meant to work as the Ghostwolf's JavaScript's + // equivalent of the DataSource interface you will find in this Java repo. + // But I did not know that and wasn't sure what it was for, so I kept it in the + // port of his code. Eventually it should be removed. + public static final PathSolver DEFAULT = new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + final int dotIndex = src.lastIndexOf('.'); + if ((dotIndex == -1)) { + throw new IllegalStateException("unable to resolve: " + src); + } + return new SolvedPath(src, src.substring(dotIndex), true); + } + }; + public static final PathSolver NOFETCH = new PathSolver() { + @Override + public SolvedPath solve(final String src, final Object solverParams) { + final int dotIndex = src.lastIndexOf('.'); + if ((dotIndex == -1)) { + throw new IllegalStateException("unable to resolve: " + src); + } + return new SolvedPath(src, src.substring(dotIndex), false); + } + }; +} diff --git a/core/src/com/etheller/warsmash/viewer5/RawOpenGLTextureResource.java b/core/src/com/etheller/warsmash/viewer5/RawOpenGLTextureResource.java new file mode 100644 index 0000000..0d09915 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/RawOpenGLTextureResource.java @@ -0,0 +1,157 @@ +package com.etheller.warsmash.viewer5; + +import java.awt.image.BufferedImage; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.GL30; +import com.etheller.warsmash.viewer5.handlers.ResourceHandler; + +/** + * Similar to GdxTextureResource, but now I'm probably replacing use of that one + * with this one. I'm trying to fight the system here and avoid porting + * Ghostwolf's BLP parser to java, and just use the Java BLP parser that I + * already had, but the libraries are not playing nicely with each other, so + * this class is written to be a lower level solution (OpenGL calls instead of + * LibGDX api) that will work. + * + * My theory is that because doing it THIS way works on Retera Model Studio, + * therefore it should work here as well. + */ +public abstract class RawOpenGLTextureResource extends Texture { + private static final int BYTES_PER_PIXEL = 4; + private final int target; + protected int handle; + private int width; + private int height; + private int wrapS = GL20.GL_CLAMP_TO_EDGE; + private int wrapT = GL20.GL_CLAMP_TO_EDGE; + private final int magFilter = GL20.GL_LINEAR; + private final int minFilter = GL20.GL_LINEAR; + private ByteBuffer data; + + public RawOpenGLTextureResource(final ModelViewer viewer, final String extension, final PathSolver pathSolver, + final String fetchUrl, final ResourceHandler handler) { + super(viewer, extension, pathSolver, fetchUrl, handler); + final GL20 gl = this.viewer.gl; + this.handle = gl.glGenTexture(); + this.target = GL20.GL_TEXTURE_2D; + gl.glBindTexture(this.target, this.handle); + gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MIN_FILTER, this.minFilter); + gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MAG_FILTER, this.magFilter); + } + + @Override + protected void error(final Exception e) { + e.printStackTrace(); + } + + @Override + public void bind(final int unit) { + this.viewer.webGL.bindTexture(this, unit); + } + + @Override + public void internalBind() { + this.viewer.gl.glBindTexture(this.target, this.handle); + this.viewer.gl.glTexParameteri(this.target, GL20.GL_TEXTURE_WRAP_S, this.wrapS); + this.viewer.gl.glTexParameteri(this.target, GL20.GL_TEXTURE_WRAP_T, this.wrapT); + } + + @Override + public int getWidth() { + return this.width; + } + + @Override + public int getHeight() { + return this.height; + } + + @Override + public int getGlTarget() { + return this.target; + } + + @Override + public int getGlHandle() { + return this.handle; + } + + @Override + public void setWrapS(final boolean wrapS) { + this.wrapS = wrapS ? GL20.GL_REPEAT : GL20.GL_CLAMP_TO_EDGE; + final GL20 gl = this.viewer.gl; + } + + @Override + public void setWrapT(final boolean wrapT) { + this.wrapT = wrapT ? GL20.GL_REPEAT : GL20.GL_CLAMP_TO_EDGE; + final GL20 gl = this.viewer.gl; + } + + public void update(final BufferedImage image, final boolean sRGBFix) { + final GL20 gl = this.viewer.gl; + + final int imageWidth = image.getWidth(); + final int imageHeight = image.getHeight(); + final int[] pixels = new int[imageWidth * imageHeight]; + image.getRGB(0, 0, imageWidth, imageHeight, pixels, 0, imageWidth); + + final ByteBuffer buffer = ByteBuffer.allocateDirect(imageWidth * imageHeight * BYTES_PER_PIXEL) + .order(ByteOrder.nativeOrder()); + // 4 + // for + // RGBA, + // 3 + // for + // RGB + + for (int y = 0; y < imageHeight; y++) { + for (int x = 0; x < imageWidth; x++) { + final int pixel = pixels[(y * imageWidth) + x]; + buffer.put((byte) ((pixel >> 16) & 0xFF)); // Red component + buffer.put((byte) ((pixel >> 8) & 0xFF)); // Green component + buffer.put((byte) (pixel & 0xFF)); // Blue component + buffer.put((byte) ((pixel >> 24) & 0xFF)); // Alpha component. + // Only for RGBA + } + } + + buffer.flip(); + this.data = buffer; + + gl.glBindTexture(GL20.GL_TEXTURE_2D, this.handle); + +// if ((this.width == imageWidth) && (this.height == imageHeight)) { +// gl.glTexSubImage2D(GL20.GL_TEXTURE_2D, 0, 0, 0, imageWidth, imageHeight, GL20.GL_RGBA, +// GL20.GL_UNSIGNED_BYTE, buffer); +// } +// else { + gl.glTexImage2D(GL20.GL_TEXTURE_2D, 0, sRGBFix ? GL30.GL_SRGB8_ALPHA8 : GL30.GL_RGBA8, imageWidth, imageHeight, + 0, GL20.GL_RGBA, GL20.GL_UNSIGNED_BYTE, buffer); + + this.width = imageWidth; + this.height = imageHeight; +// } + } + + /** + * I really don't like holding the reference to the original buffer like this. + * Seems wasteful. It's already on the GPU. However, while porting some code for + * shadow maps I hit a point where I really finally felt obligated to add this + * (there is some code in the Terrain stuff that should've had this, but + * doesn't, and does its own texture management as a result). + * + * So, as a note to future authors, please reinvent the system such that this + * cached buffer data is only stored for shadow maps and terrain textures or + * whatever. Right now, this holds a reference to these guys on every texture, + * on every unit, on every doodad, etc. Java will not be able to garbage collect + * them because we hold on to the buffer in "update()". + */ + public ByteBuffer getData() { + return this.data; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/RenderBatch.java b/core/src/com/etheller/warsmash/viewer5/RenderBatch.java new file mode 100644 index 0000000..83dbb4d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/RenderBatch.java @@ -0,0 +1,40 @@ +package com.etheller.warsmash.viewer5; + +import java.util.ArrayList; +import java.util.List; + +/** + * A render batch. + */ +public abstract class RenderBatch { + public Scene scene; + public Model model; + public TextureMapper textureMapper; + public List instances = new ArrayList<>(); + public int count = 0; + + public abstract void render(); + + public RenderBatch(final Scene scene, final Model model, final TextureMapper textureMapper) { + this.scene = scene; + this.model = model; + this.textureMapper = textureMapper; + } + + public void clear() { + this.count = 0; + } + + public void add(final ModelInstance instance) { + if (this.count == this.instances.size()) { + this.instances.add(instance); + } + else if (this.count > this.instances.size()) { + throw new IllegalStateException("count > size"); + } + else { + this.instances.set(this.count, instance); + } + this.count++; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/Resource.java b/core/src/com/etheller/warsmash/viewer5/Resource.java new file mode 100644 index 0000000..cc9f619 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/Resource.java @@ -0,0 +1,46 @@ +package com.etheller.warsmash.viewer5; + +import java.io.InputStream; + +public abstract class Resource { + public final ModelViewer viewer; + public final String extension; + public final String fetchUrl; + public boolean ok; + public boolean loaded; + public final PathSolver pathSolver; + public final Object solverParams = null; + + public Resource(final ModelViewer viewer, final String extension, final PathSolver pathSolver, + final String fetchUrl) { + this.viewer = viewer; + this.extension = extension; + this.pathSolver = pathSolver; + this.fetchUrl = fetchUrl; + this.ok = false; + this.loaded = false; + } + + public void loadData(final InputStream src, final Object options) { + this.loaded = true; + + try { + this.load(src, options); + this.ok = true; + this.lateLoad(); + } + catch (final Exception e) { + this.error(e); + } + } + + public boolean detach() { + return this.viewer.unload(this); + } + + protected abstract void lateLoad(); + + protected abstract void load(InputStream src, Object options); + + protected abstract void error(Exception e); +} diff --git a/core/src/com/etheller/warsmash/viewer5/ResourceLoader.java b/core/src/com/etheller/warsmash/viewer5/ResourceLoader.java new file mode 100644 index 0000000..ce56cf8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/ResourceLoader.java @@ -0,0 +1,4 @@ +package com.etheller.warsmash.viewer5; + +public interface ResourceLoader { +} diff --git a/core/src/com/etheller/warsmash/viewer5/Scene.java b/core/src/com/etheller/warsmash/viewer5/Scene.java new file mode 100644 index 0000000..3b3b593 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/Scene.java @@ -0,0 +1,328 @@ +package com.etheller.warsmash.viewer5; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.GL30; +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Rectangle; +import com.etheller.warsmash.viewer5.gl.Extensions; +import com.etheller.warsmash.viewer5.gl.WebGL; +import com.etheller.warsmash.viewer5.handlers.w3x.DynamicShadowManager; + +/** + * A scene. + * + * Every scene has its own list of model instances, and its own camera and + * viewport. + * + * In addition, in Ghostwolf's original code every scene may have its own + * AudioContext if enableAudio() is called. If audo is enabled, the + * AudioContext's listener's location will be updated automatically. Note that + * due to browser policies, this may be done only after user interaction with + * the web page. + * + * In "Warsmash", we are starting from an attempt to replicate Ghostwolf, but + * audio is always on in LibGDX generally. So we will probably simplify or skip + * over those behaviors other than a boolean on/off toggle for audio. + */ +public abstract class Scene { + + public final ModelViewer viewer; + public int visibleCells; + public int visibleInstances; + public final Camera camera; + public int updatedParticles; + public boolean audioEnabled; + public AudioContext audioContext; + + public final List instances; + public int currentInstance; + public final List batchedInstances; + public int currentBatchedInstance; + public final EmittedObjectUpdater emitterObjectUpdater; + public final Map batches; + public final Comparator instanceDepthComparator; + public DynamicShadowManager shadowManager; + /** + * Similar to WebGL's own `alpha` parameter. + * + * If false, the scene will be cleared before rendering, meaning that scenes + * behind it won't be visible through it. + * + * If true, alpha works as usual. + */ + public boolean alpha = false; + private final SceneLightManager lightManager; + + public Scene(final ModelViewer viewer, final SceneLightManager lightManager) { + final CanvasProvider canvas = viewer.canvas; + this.viewer = viewer; + this.camera = new Camera(); + + this.updatedParticles = 0; + + this.audioEnabled = false; + this.audioContext = null; + + // Use the whole canvas, and standard perspective projection values. + this.camera.viewport(new Rectangle(0, 0, canvas.getWidth(), canvas.getHeight())); + this.camera.perspective((float) (Math.PI / 4), canvas.getWidth() / canvas.getHeight(), 8, 10000); + + this.instances = new ArrayList<>(); + this.currentInstance = 0; + + this.batchedInstances = new ArrayList<>(); + this.currentBatchedInstance = 0; + + this.emitterObjectUpdater = new EmittedObjectUpdater(); + + this.batches = new HashMap<>(); + this.instanceDepthComparator = new InstanceDepthComparator(); + this.visibleCells = 0; + this.visibleInstances = 0; + + this.lightManager = lightManager; + } + + public boolean enableAudio() { + if (this.audioContext == null) { + this.audioContext = Extensions.audio.createContext(this instanceof WorldScene); + } + if (!this.audioContext.isRunning()) { + this.audioContext.resume(); + } + this.audioEnabled = this.audioContext.isRunning(); + return this.audioEnabled; + } + + public void disableAudio() { + if (this.audioContext != null) { + this.audioContext.suspend(); + } + this.audioEnabled = false; + } + + public boolean addInstance(final ModelInstance instance) { + if (instance.scene != this) { + if (instance.scene != null) { + instance.scene.removeInstance(instance); + } + + instance.scene = this; + + // Only allow instances that are actually ok to be added the scene. + if (instance.model.ok) { + // predict x and y of model + float x, y; + if (instance.dirty) { + // TODO this is an incorrect, predicted location for dirty case + if ((instance.parent != null) && !instance.dontInheritTranslation) { + x = instance.parent.localLocation.x + instance.localLocation.x; + y = instance.parent.localLocation.y + instance.localLocation.y; + } + else { + x = instance.localLocation.x; + y = instance.localLocation.y; + } + } + else { + x = instance.worldLocation.x; + y = instance.worldLocation.y; + } + instanceMoved(instance, x, y); + return true; + } + } + + return false; + } + + public abstract void instanceMoved(ModelInstance instance, float x, float y); + + public boolean removeInstance(final ModelInstance instance) { + if (instance.scene == this) { + instance.removeLights(this); + innerRemove(instance); + + instance.scene = null; + this.instances.remove(instance); + + return true; + } + return false; + } + + protected abstract void innerRemove(ModelInstance instance); + + public abstract void clear(); + + public boolean detach() { + if (this.viewer != null) { + return this.viewer.removeScene(this); + } + return false; + } + + public void addToBatch(final ModelInstance instance) { + final TextureMapper textureMapper = instance.textureMapper; + RenderBatch batch = this.batches.get(textureMapper); + + if (batch == null) { + batch = instance.getBatch(textureMapper); + + this.batches.put(textureMapper, batch); + } + + batch.add(instance); + } + + public void update(final float dt) { + this.camera.update(); + + if (this.audioEnabled) { + final float x = this.camera.location.x; + final float y = this.camera.location.y; + final float z = this.camera.location.z; + final float forwardX = this.camera.directionY.x; + final float forwardY = this.camera.directionY.y; + final float forwardZ = this.camera.directionY.z; + final float upX = this.camera.directionZ.x; + final float upY = this.camera.directionZ.y; + final float upZ = this.camera.directionZ.z; + final AudioContext.Listener listener = this.audioContext.listener; + + listener.setPosition(x, y, z); + listener.setOrientation(forwardX, forwardY, forwardZ, upX, upY, upZ); + } + + final int frame = this.viewer.frame; + + this.currentInstance = 0; + this.currentBatchedInstance = 0; + + innerUpdate(dt, frame); + + for (int i = this.batchedInstances.size() - 1; i >= this.currentBatchedInstance; i--) { + this.batchedInstances.remove(i); + } + + for (int i = this.instances.size() - 1; i >= this.currentInstance; i--) { + this.instances.remove(i); + } + Collections.sort(this.instances, this.instanceDepthComparator); + + this.emitterObjectUpdater.update(dt); + this.updatedParticles = this.emitterObjectUpdater.objects.size(); + + } + + protected abstract void innerUpdate(float dt, int frame); + + public void startFrame() { + final GL20 gl = this.viewer.gl; + final Rectangle viewport = this.camera.rect; + + // Set the viewport + gl.glViewport((int) viewport.x, (int) viewport.y, (int) viewport.width, (int) viewport.height); + + // Allow to render only in the viewport + gl.glScissor((int) viewport.x, (int) viewport.y, (int) viewport.width, (int) viewport.height); + + // If this scene doesn't want alpha, clear it. + if (!this.alpha) { + gl.glDepthMask(true); + gl.glClear(GL20.GL_DEPTH_BUFFER_BIT | GL20.GL_COLOR_BUFFER_BIT); + } + this.lightManager.update(); + } + + public void renderOpaque() { + final Rectangle viewport = this.camera.rect; + this.viewer.gl.glViewport((int) viewport.x, (int) viewport.y, (int) viewport.width, (int) viewport.height); + + // Clear all of the batches. + for (final RenderBatch batch : this.batches.values()) { + batch.clear(); + } + + // Add all of the batched instances to batches. + for (final ModelInstance instance : this.batchedInstances) { + this.addToBatch(instance); + } + + // Render all of the batches. + for (final RenderBatch batch : this.batches.values()) { + batch.render(); + } + + // Render all of the opaque things of non-batched instances. + for (final ModelInstance instance : this.instances) { + instance.renderOpaque(this.camera.viewProjectionMatrix); + } + } + + public void renderOpaque(final DynamicShadowManager dynamicShadowManager, final WebGL webGL) { + final Matrix4 depthMatrix = dynamicShadowManager.prepareShadowMatrix(); + dynamicShadowManager.beginShadowMap(webGL); + Gdx.gl30.glDepthMask(true); + Gdx.gl30.glClear(GL20.GL_DEPTH_BUFFER_BIT | GL20.GL_COLOR_BUFFER_BIT); + Gdx.gl30.glDisable(GL30.GL_SCISSOR_TEST); + + // Render all of the opaque things of non-batched instances. +// for (final ModelInstance instance : this.instances) { +// instance.renderOpaque(depthMatrix); +// } + + dynamicShadowManager.endShadowMap(); + final Rectangle viewport = this.camera.rect; + this.viewer.gl.glViewport((int) viewport.x, (int) viewport.y, (int) viewport.width, (int) viewport.height); + Gdx.gl30.glEnable(GL30.GL_SCISSOR_TEST); + } + + /** + * Renders all translucent things in this scene. Automatically applies the + * camera's viewport. + */ + public void renderTranslucent() { + final Rectangle viewport = this.camera.rect; + + this.viewer.gl.glViewport((int) viewport.x, (int) viewport.y, (int) viewport.width, (int) viewport.height); + + for (final ModelInstance instance : this.instances) { + instance.renderTranslucent(); + } + + } + + public void clearEmitterObjects() { + for (final EmittedObject object : this.emitterObjectUpdater.objects) { + object.health = 0; + } + } + + public void addLight(final SceneLightInstance lightInstance) { + this.lightManager.add(lightInstance); + } + + public void removeLight(final SceneLightInstance lightInstance) { + this.lightManager.remove(lightInstance); + } + + private static final class InstanceDepthComparator implements Comparator { + @Override + public int compare(final ModelInstance o1, final ModelInstance o2) { + return -Float.compare(o2.depth, o1.depth); + } + } + + public SceneLightManager getLightManager() { + return this.lightManager; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/SceneLightInstance.java b/core/src/com/etheller/warsmash/viewer5/SceneLightInstance.java new file mode 100644 index 0000000..8a94922 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/SceneLightInstance.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.viewer5; + +public interface SceneLightInstance { + +} diff --git a/core/src/com/etheller/warsmash/viewer5/SceneLightManager.java b/core/src/com/etheller/warsmash/viewer5/SceneLightManager.java new file mode 100644 index 0000000..efd69f4 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/SceneLightManager.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.viewer5; + +public interface SceneLightManager { + public void add(final SceneLightInstance lightInstance); + + public void remove(final SceneLightInstance lightInstance); + + public void update(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/Shaders.java b/core/src/com/etheller/warsmash/viewer5/Shaders.java new file mode 100644 index 0000000..6171fb3 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/Shaders.java @@ -0,0 +1,115 @@ +package com.etheller.warsmash.viewer5; + +public class Shaders { + public static final String boneTexture = ""// + + " uniform sampler2D u_boneMap;\r\n" + // + " uniform float u_vectorSize;\r\n" + // + " uniform float u_rowSize;\r\n" + // + " mat4 fetchMatrix(float column, float row) {\r\n" + // + " column *= u_vectorSize * 4.0;\r\n" + // + " row *= u_rowSize;\r\n" + // + " // Add in half texel to sample in the middle of the texel.\r\n" + // + " // Otherwise, since the sample is directly on the boundry, small floating point errors can cause the sample to get the wrong pixel.\r\n" + + // + " // This is mostly noticable with NPOT textures, which the bone maps are.\r\n" + // + " column += 0.5 * u_vectorSize;\r\n" + // + " row += 0.5 * u_rowSize;\r\n" + // + " return mat4(texture2D(u_boneMap, vec2(column, row)),\r\n" + // + " texture2D(u_boneMap, vec2(column + u_vectorSize, row)),\r\n" + // + " texture2D(u_boneMap, vec2(column + u_vectorSize * 2.0, row)),\r\n" + // + " texture2D(u_boneMap, vec2(column + u_vectorSize * 3.0, row)));\r\n" + // + " }"; + + public static final String decodeFloat = "\r\n" + // + " vec2 decodeFloat2(float f) {\r\n" + // + " vec2 v;\r\n" + // + " v[1] = floor(f / 256.0);\r\n" + // + " v[0] = floor(f - v[1] * 256.0);\r\n" + // + " return v;\r\n" + // + " }\r\n" + // + " vec3 decodeFloat3(float f) {\r\n" + // + " vec3 v;\r\n" + // + " v[2] = floor(f / 65536.0);\r\n" + // + " v[1] = floor((f - v[2] * 65536.0) / 256.0);\r\n" + // + " v[0] = floor(f - v[2] * 65536.0 - v[1] * 256.0);\r\n" + // + " return v;\r\n" + // + " }\r\n" + // + " vec4 decodeFloat4(float v) {\r\n" + // + " vec4 enc = vec4(1.0, 255.0, 65025.0, 16581375.0) * v;\r\n" + // + " enc = fract(enc);\r\n" + // + " enc -= enc.yzww * vec4(1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0, 0.0);\r\n" + // + " return enc;\r\n" + // + " }"; + + public static final String quatTransform = "\r\n" + // + " // A 2D quaternion*vector.\r\n" + // + " // q is the zw components of the original quaternion.\r\n" + // + " vec2 quat_transform(vec2 q, vec2 v) {\r\n" + // + " vec2 uv = vec2(-q.x * v.y, q.x * v.x);\r\n" + // + " vec2 uuv = vec2(-q.x * uv.y, q.x * uv.x);\r\n" + // + " return v + 2.0 * (uv * q.y + uuv);\r\n" + // + " }\r\n" + // + " // A 2D quaternion*vector.\r\n" + // + " // q is the zw components of the original quaternion.\r\n" + // + " vec3 quat_transform(vec2 q, vec3 v) {\r\n" + // + " return vec3(quat_transform(q, v.xy), v.z);\r\n" + // + " }\r\n" + // + " "; + + public static String lightSystem(final String normalName, final String positionName, final String lightTexture, + final String lightTextureHeight, final String lightCount, final boolean terrain) { + return " vec3 lightFactor = vec3(0.0,0.0,0.0);\r\n" + // + " for(float lightIndex = 0.5; lightIndex < " + lightCount + "; lightIndex += 1.0) {\r\n" + // + " float rowPos = (lightIndex) / " + lightTextureHeight + ";\r\n" + // + " vec4 lightPosition = texture2D(" + lightTexture + ", vec2(0.125, rowPos));\r\n" + // + " vec3 lightExtra = texture2D(" + lightTexture + ", vec2(0.375, rowPos)).xyz;\r\n" + // + " vec4 lightColor = texture2D(" + lightTexture + ", vec2(0.625, rowPos));\r\n" + // + " vec4 lightAmbColor = texture2D(" + lightTexture + ", vec2(0.875, rowPos));\r\n" + // + " if(lightExtra.x > 1.5) {\r\n" + // + " // Ambient light;\r\n" + // + " float dist = length(" + positionName + " - vec3(lightPosition." + (terrain ? "xyw" : "xyz") + + "));\r\n" + // + " float attenuationStart = lightExtra.y;\r\n" + // + " float attenuationEnd = lightExtra.z;\r\n" + // + " if( dist <= attenuationEnd ) {\r\n" + // + " float attenuationDist = clamp((dist-attenuationStart), 0.001, (attenuationEnd-attenuationStart));\r\n" + + // + " float attenuationFactor = 1.0/(attenuationDist);\r\n" + // + " lightFactor += attenuationFactor * lightAmbColor.a * lightAmbColor.rgb;\r\n" + // + " \r\n" + // + " }\r\n" + // + " } else if(lightExtra.x > 0.5) {\r\n" + // + " // Directional (sun) light;\r\n" + // + " vec3 lightDirection = vec3(lightPosition.xyz);\r\n" + // + " vec3 lightFactorContribution = lightColor.a * lightColor.rgb * clamp(dot(" + normalName + + ", lightDirection), 0.0, 1.0);\r\n" + // + " if(lightFactorContribution.r > 1.0 || lightFactorContribution.g > 1.0 || lightFactorContribution.b > 1.0) {\r\n" + + // + " lightFactorContribution = clamp(lightFactorContribution, 0.0, 1.0);\r\n" + // + " }\r\n" + // + " lightFactor += lightFactorContribution + lightAmbColor.a * lightAmbColor.rgb;\r\n" + // + " } else {\r\n" + // + " // Omnidirectional light;\r\n" + // + " vec3 deltaBtwn = " + positionName + " - lightPosition.xyz;\r\n" + // + " float dist = length(" + positionName + " - vec3(lightPosition." + (terrain ? "xyz" : "xyz") + + ")) / 64.0 + 1.0;\r\n" + // + " vec3 lightDirection = normalize(-deltaBtwn);\r\n" + // + " vec3 lightFactorContribution = (lightColor.a/(pow(dist, 2.0))) * lightColor.rgb * clamp(dot(" + + normalName + ", lightDirection), 0.0, 1.0);\r\n" + // + " if(lightFactorContribution.r > 1.0 || lightFactorContribution.g > 1.0 || lightFactorContribution.b > 1.0) {\r\n" + + // + " lightFactorContribution = clamp(lightFactorContribution, 0.0, 1.0);\r\n" + // + " }\r\n" + // + " lightFactor += lightFactorContribution + (lightAmbColor.a/(pow(dist, 2.0))) * lightAmbColor.rgb;\r\n" + + // + " }\r\n" + // + " }\r\n";// + // + +// " vec4 sRGB = vec4(lightFactor, 1.0);" + // +// " bvec4 cutoff = lessThan(sRGB, vec4(0.04045));" + // +// " vec4 higher = pow((sRGB + vec4(0.055))/vec4(1.055), vec4(2.4));" + // +// " vec4 lower = sRGB/vec4(12.92);" + // +// "" + // +// " lightFactor = (higher * (vec4(1.0) - vec4(cutoff)) + lower * vec4(cutoff)).xyz;"; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/SimpleScene.java b/core/src/com/etheller/warsmash/viewer5/SimpleScene.java new file mode 100644 index 0000000..f68a8ff --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/SimpleScene.java @@ -0,0 +1,81 @@ +package com.etheller.warsmash.viewer5; + +import java.util.ArrayList; +import java.util.List; + +public class SimpleScene extends Scene { + private final List allInstances = new ArrayList<>(); + + public SimpleScene(final ModelViewer viewer, final SceneLightManager lightManager) { + super(viewer, lightManager); + this.visibleCells = 1; + this.visibleInstances = 0; + } + + @Override + public void instanceMoved(final ModelInstance instance, final float x, final float y) { + if (instance.left == -1) { + instance.left = 0; + this.allInstances.add(instance); + } + } + + @Override + protected void innerRemove(final ModelInstance instance) { + this.allInstances.remove(instance); + instance.left = -1; + } + + @Override + public void clear() { + for (final ModelInstance instance : this.allInstances) { + instance.scene = null; + } + this.allInstances.clear(); + } + + @Override + protected void innerUpdate(final float dt, final int frame) { + + // Update and collect all of the visible instances. + for (final ModelInstance instance : new ArrayList<>(this.allInstances)) { + // Below: current SimpleScene is not checking instance visibility. + // It's meant to be simple. Low number of models. Render everything, + // dont check visible. Then I had to add a call to isVisible() because it + // assigns depth, which is crazy. + instance.isVisible(this.camera); + if (instance.rendered && (instance.cullFrame < frame)) { + instance.cullFrame = frame; + + if (instance.updateFrame < frame) { + instance.update(dt, this); + if (!instance.rendered) { + // it became hidden while it updated + continue; + } + } + + if (instance.isBatched()) { + if (this.currentBatchedInstance < this.batchedInstances.size()) { + this.batchedInstances.set(this.currentBatchedInstance++, instance); + } + else { + this.batchedInstances.add(instance); + this.currentBatchedInstance++; + } + } + else { + if (this.currentInstance < this.instances.size()) { + this.instances.set(this.currentInstance++, instance); + } + else { + this.instances.add(instance); + this.currentInstance++; + } + } + + } + } + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/SkeletalNode.java b/core/src/com/etheller/warsmash/viewer5/SkeletalNode.java new file mode 100644 index 0000000..16fb9e7 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/SkeletalNode.java @@ -0,0 +1,251 @@ +package com.etheller.warsmash.viewer5; + +import java.util.ArrayList; + +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Quaternion; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.util.RenderMathUtils; + +public abstract class SkeletalNode extends GenericNode { + protected static final Vector3 cameraRayHeap = new Vector3(); + protected static final Vector3 billboardAxisHeap = new Vector3(); + protected static final Quaternion rotationHeap = new Quaternion(); + protected static final Quaternion rotationHeap2 = new Quaternion(); + protected static final Vector3 scalingHeap = new Vector3(); + protected static final Vector3 blendLocationHeap = new Vector3(); + protected static final Vector3 blendHeap = new Vector3(); + protected static final Vector3 blendScaleHeap = new Vector3(); + + public UpdatableObject object; + + public boolean billboarded; + public boolean billboardedX; + public boolean billboardedY; + public boolean billboardedZ; + + public Vector3 localBlendLocation; + public Quaternion localBlendRotation; + public Vector3 localBlendScale; + + public SkeletalNode() { + this.pivot = new Vector3(); + this.localLocation = new Vector3(); + this.localRotation = new Quaternion(0, 0, 0, 1); + this.localScale = new Vector3(1, 1, 1); + this.worldLocation = new Vector3(); + this.worldRotation = new Quaternion(); + this.worldScale = new Vector3(1, 1, 1); + this.inverseWorldLocation = new Vector3(); + this.inverseWorldRotation = new Quaternion(); + this.inverseWorldScale = new Vector3(); + this.localMatrix = new Matrix4(); + this.localBlendLocation = new Vector3(); + this.localBlendRotation = new Quaternion(0, 0, 0, 1); + this.localBlendScale = new Vector3(1, 1, 1); + this.worldMatrix = new Matrix4(); + this.dontInheritTranslation = false; + this.dontInheritRotation = false; + this.dontInheritScaling = false; + this.children = new ArrayList<>(); + + this.visible = true; + this.wasDirty = false; + + /** + * The object associated with this node, if there is any. + * + * @member {?} + */ + this.object = null; + + this.localRotation.w = 1; + + this.localScale.set(1, 1, 1); + + this.localMatrix.val[0] = 1; + this.localMatrix.val[5] = 1; + this.localMatrix.val[10] = 1; + this.localMatrix.val[15] = 1; + + this.dirty = true; + + this.billboarded = false; + this.billboardedX = false; + this.billboardedY = false; + this.billboardedZ = false; + } + + public void recalculateTransformation(final Scene scene, final float blendTimeRatio) { + final float inverseBlendRatio = 1 - blendTimeRatio; + final Quaternion computedRotation; + Vector3 computedScaling; + Vector3 computedLocation; + + if (this.dontInheritScaling) { + computedScaling = scalingHeap; + + final Vector3 parentInverseScale = this.parent.inverseWorldScale; + computedScaling.x = parentInverseScale.x * this.localScale.x; + computedScaling.y = parentInverseScale.y * this.localScale.y; + computedScaling.z = parentInverseScale.z * this.localScale.z; + + this.worldScale.x = this.localScale.x; + this.worldScale.y = this.localScale.y; + this.worldScale.z = this.localScale.z; + } + else { + if (!Float.isNaN(blendTimeRatio) && (blendTimeRatio > 0)) { + blendScaleHeap.set(this.localScale).scl(inverseBlendRatio) + .add(blendHeap.set(this.localBlendScale).scl(blendTimeRatio)); + computedScaling = blendScaleHeap; + } + else { + computedScaling = this.localScale; + } + + final Vector3 parentScale = this.parent.worldScale; + this.worldScale.x = parentScale.x * this.localScale.x; + this.worldScale.y = parentScale.y * this.localScale.y; + this.worldScale.z = parentScale.z * this.localScale.z; + } + + if (this.billboarded) { + computedRotation = rotationHeap; + + computedRotation.set(this.parent.inverseWorldRotation); + computedRotation.mul(scene.camera.inverseRotation); + + this.convertBasis(computedRotation); + } + else { + computedRotation = rotationHeap.set(this.localRotation); + if (!Float.isNaN(blendTimeRatio) && (blendTimeRatio > 0)) { + computedRotation.slerp(this.localBlendRotation, blendTimeRatio); + } + + if (this.billboardedX) { + if (computedScaling == this.localScale) { + computedScaling = scalingHeap.set(computedScaling); + } + // It took me many hours to deduce from playing around that this negative one + // multiplier should be here. I suggest a lot of testing before you remove it. + computedScaling.z *= -1; + + final Camera camera = scene.camera; + cameraRayHeap.set(camera.billboardedVectors[6]); + + rotationHeap2.set(computedRotation); + // Inverse that local rotation + rotationHeap2.x = -rotationHeap2.x; + rotationHeap2.y = -rotationHeap2.y; + rotationHeap2.z = -rotationHeap2.z; + + rotationHeap2.mul(this.parent.inverseWorldRotation); + + rotationHeap2.transform(cameraRayHeap); + + billboardAxisHeap.set(1, 0, 0); + final float angle = (float) Math.atan2(cameraRayHeap.z, cameraRayHeap.y); + rotationHeap2.setFromAxisRad(billboardAxisHeap, angle); + + RenderMathUtils.mul(computedRotation, computedRotation, rotationHeap2); + } + else if (this.billboardedY) { + final Camera camera = scene.camera; + cameraRayHeap.set(camera.billboardedVectors[6]); + + rotationHeap2.set(computedRotation); + // Inverse that local rotation + rotationHeap2.x = -rotationHeap2.x; + rotationHeap2.y = -rotationHeap2.y; + rotationHeap2.z = -rotationHeap2.z; + + rotationHeap2.mul(this.parent.inverseWorldRotation); + + rotationHeap2.transform(cameraRayHeap); + + billboardAxisHeap.set(0, 1, 0); + final float angle = (float) Math.atan2(-cameraRayHeap.z, cameraRayHeap.x); + rotationHeap2.setFromAxisRad(billboardAxisHeap, angle); + + RenderMathUtils.mul(computedRotation, computedRotation, rotationHeap2); + } + else if (this.billboardedZ) { + final Camera camera = scene.camera; + cameraRayHeap.set(camera.billboardedVectors[6]); + + rotationHeap2.set(computedRotation); + // Inverse that local rotation + rotationHeap2.x = -rotationHeap2.x; + rotationHeap2.y = -rotationHeap2.y; + rotationHeap2.z = -rotationHeap2.z; + + rotationHeap2.mul(this.parent.inverseWorldRotation); + + rotationHeap2.transform(cameraRayHeap); + + billboardAxisHeap.set(0, 0, 1); + final float angle = (float) Math.atan2(cameraRayHeap.y, cameraRayHeap.x); + rotationHeap2.setFromAxisRad(billboardAxisHeap, angle); + + RenderMathUtils.mul(computedRotation, computedRotation, rotationHeap2); + } + } + + if (!Float.isNaN(blendTimeRatio) && (blendTimeRatio > 0)) { + computedLocation = blendLocationHeap.set(this.localLocation).scl(inverseBlendRatio) + .add(blendHeap.set(this.localBlendLocation).scl(blendTimeRatio)); + } + else { + computedLocation = this.localLocation; + } + RenderMathUtils.fromRotationTranslationScaleOrigin(computedRotation, computedLocation, computedScaling, + this.localMatrix, this.pivot); + + RenderMathUtils.mul(this.worldMatrix, this.parent.worldMatrix, this.localMatrix); + + RenderMathUtils.mul(this.worldRotation, this.parent.worldRotation, computedRotation); + + // Inverse world rotation + this.inverseWorldRotation.x = -this.worldRotation.x; + this.inverseWorldRotation.y = -this.worldRotation.y; + this.inverseWorldRotation.z = -this.worldRotation.z; + this.inverseWorldRotation.w = this.worldRotation.w; + + // Inverse world scale + this.inverseWorldScale.x = 1 / this.worldScale.x; + this.inverseWorldScale.y = 1 / this.worldScale.y; + this.inverseWorldScale.z = 1 / this.worldScale.z; + + // World location + final float x = this.pivot.x; + final float y = this.pivot.y; + final float z = this.pivot.z; + this.worldLocation.x = (this.worldMatrix.val[Matrix4.M00] * x) + (this.worldMatrix.val[Matrix4.M01] * y) + + (this.worldMatrix.val[Matrix4.M02] * z) + this.worldMatrix.val[Matrix4.M03]; + this.worldLocation.y = (this.worldMatrix.val[Matrix4.M10] * x) + (this.worldMatrix.val[Matrix4.M11] * y) + + (this.worldMatrix.val[Matrix4.M12] * z) + this.worldMatrix.val[Matrix4.M13]; + this.worldLocation.z = (this.worldMatrix.val[Matrix4.M20] * x) + (this.worldMatrix.val[Matrix4.M21] * y) + + (this.worldMatrix.val[Matrix4.M22] * z) + this.worldMatrix.val[Matrix4.M23]; + + // Inverse world location + this.inverseWorldLocation.x = -this.worldLocation.x; + this.inverseWorldLocation.y = -this.worldLocation.y; + this.inverseWorldLocation.z = -this.worldLocation.z; + } + + public void beginBlending() { + this.localBlendLocation.set(this.localLocation); + this.localBlendRotation.set(this.localRotation); + this.localBlendScale.set(this.localScale); + } + + public void updateChildren(final float dt, final Scene scene) { + for (int i = 0, l = this.children.size(); i < l; i++) { + this.children.get(i).update(dt, scene); + } + } + + protected abstract void convertBasis(Quaternion computedRotation); +} diff --git a/core/src/com/etheller/warsmash/viewer5/SolvedPath.java b/core/src/com/etheller/warsmash/viewer5/SolvedPath.java new file mode 100644 index 0000000..537e89e --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/SolvedPath.java @@ -0,0 +1,26 @@ +package com.etheller.warsmash.viewer5; + +public class SolvedPath { + public String finalSrc; + public String extension; + public boolean fetch; + + public SolvedPath(final String finalSrc, final String extension, final boolean fetch) { + this.finalSrc = finalSrc; + this.extension = extension; + this.fetch = fetch; + } + + public String getFinalSrc() { + return this.finalSrc; + } + + public String getExtension() { + return this.extension; + } + + public boolean isFetch() { + return this.fetch; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/Texture.java b/core/src/com/etheller/warsmash/viewer5/Texture.java new file mode 100644 index 0000000..d55596f --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/Texture.java @@ -0,0 +1,30 @@ +package com.etheller.warsmash.viewer5; + +import com.etheller.warsmash.viewer5.handlers.ResourceHandler; + +public abstract class Texture extends HandlerResource implements ViewerTextureRenderable { + + public Texture(final ModelViewer viewer, final String extension, final PathSolver pathSolver, final String fetchUrl, + final ResourceHandler handler) { + super(viewer, extension, pathSolver, fetchUrl, handler); + } + + public abstract void bind(final int unit); + + public abstract void internalBind(); + + public abstract int getWidth(); + + public abstract int getHeight(); + + @Override + public abstract int getGlTarget(); + + @Override + public abstract int getGlHandle(); + + public abstract void setWrapS(final boolean wrapS); + + public abstract void setWrapT(final boolean wrapT); + +} diff --git a/core/src/com/etheller/warsmash/viewer5/TextureMapper.java b/core/src/com/etheller/warsmash/viewer5/TextureMapper.java new file mode 100644 index 0000000..5f9eead --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/TextureMapper.java @@ -0,0 +1,23 @@ +package com.etheller.warsmash.viewer5; + +import java.util.HashMap; +import java.util.Map; + +public class TextureMapper { + public final Model model; + public final Map textures; + + public TextureMapper(final Model model) { + this.model = model; + this.textures = new HashMap<>(); + } + + public TextureMapper(final Model model, final Map textures) { + this.model = model; + this.textures = new HashMap<>(textures); + } + + public Texture get(final Object key) { + return this.textures.get(key); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/UpdatableObject.java b/core/src/com/etheller/warsmash/viewer5/UpdatableObject.java new file mode 100644 index 0000000..7534f54 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/UpdatableObject.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.viewer5; + +public interface UpdatableObject { + void update(float dt, boolean visible); +} diff --git a/core/src/com/etheller/warsmash/viewer5/ViewerTextureRenderable.java b/core/src/com/etheller/warsmash/viewer5/ViewerTextureRenderable.java new file mode 100644 index 0000000..7f29f0e --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/ViewerTextureRenderable.java @@ -0,0 +1,29 @@ +package com.etheller.warsmash.viewer5; + +import com.badlogic.gdx.graphics.Texture; + +public interface ViewerTextureRenderable { + // TODO bind method makes more sense here + + int getGlTarget(); + + int getGlHandle(); + + class GdxViewerTextureRenderable implements ViewerTextureRenderable { + private final com.badlogic.gdx.graphics.Texture gdxTexture; + + public GdxViewerTextureRenderable(final Texture texture) { + this.gdxTexture = texture; + } + + @Override + public int getGlTarget() { + return this.gdxTexture.glTarget; + } + + @Override + public int getGlHandle() { + return this.gdxTexture.getTextureObjectHandle(); + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/WorldScene.java b/core/src/com/etheller/warsmash/viewer5/WorldScene.java new file mode 100644 index 0000000..f05a550 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/WorldScene.java @@ -0,0 +1,101 @@ +package com.etheller.warsmash.viewer5; + +import java.util.ArrayList; + +/** + * A scene. + * + * Every scene has its own list of model instances, and its own camera and + * viewport. + * + * In addition, in Ghostwolf's original code every scene may have its own + * AudioContext if enableAudio() is called. If audo is enabled, the + * AudioContext's listener's location will be updated automatically. Note that + * due to browser policies, this may be done only after user interaction with + * the web page. + * + * In "Warsmash", we are starting from an attempt to replicate Ghostwolf, but + * audio is always on in LibGDX generally. So we will probably simplify or skip + * over those behaviors other than a boolean on/off toggle for audio. + */ +public class WorldScene extends Scene { + + public Grid grid; + + public WorldScene(final ModelViewer viewer, final SceneLightManager lightManager) { + super(viewer, lightManager); + this.grid = new Grid(-100000, -100000, 200000, 200000, 200000, 200000); + } + + @Override + public void instanceMoved(final ModelInstance instance, final float x, final float y) { + this.grid.moved(instance, x, y); + } + + @Override + protected void innerRemove(final ModelInstance instance) { + this.grid.remove(instance); + } + + @Override + public void clear() { + // First remove references to this scene stored in the instances. + for (final GridCell cell : this.grid.cells) { + for (final ModelInstance instance : cell.instances) { + instance.scene = null; + } + } + + // Then remove references to the instances. + this.grid.clear(); + } + + @Override + protected void innerUpdate(final float dt, final int frame) { + this.visibleCells = 0; + this.visibleInstances = 0; + + // Update and collect all of the visible instances. + for (final GridCell cell : this.grid.cells) { + if (cell.isVisible(this.camera) || true) { + this.visibleCells += 1; + + for (final ModelInstance instance : new ArrayList<>(cell.instances)) { +// final ModelInstance instance = cell.instances.get(i); + if (instance.rendered && (instance.cullFrame < frame) && instance.isVisible(this.camera)) { + instance.cullFrame = frame; + + if (instance.updateFrame < frame) { + instance.update(dt, this); + if (!instance.rendered) { + // it became hidden while it updated + continue; + } + } + + if (instance.isBatched()) { + if (this.currentBatchedInstance < this.batchedInstances.size()) { + this.batchedInstances.set(this.currentBatchedInstance++, instance); + } + else { + this.batchedInstances.add(instance); + this.currentBatchedInstance++; + } + } + else { + if (this.currentInstance < this.instances.size()) { + this.instances.set(this.currentInstance++, instance); + } + else { + this.instances.add(instance); + this.currentInstance++; + } + } + + this.visibleInstances += 1; + } + } + } + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/deprecated/ShaderProgram.java b/core/src/com/etheller/warsmash/viewer5/deprecated/ShaderProgram.java new file mode 100644 index 0000000..51117e5 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/deprecated/ShaderProgram.java @@ -0,0 +1,15 @@ +package com.etheller.warsmash.viewer5.deprecated; + +import com.etheller.warsmash.viewer5.gl.WebGL; + +public class ShaderProgram { + + public boolean ok; + public int attribsCount; + public int webglResource; + + public ShaderProgram(final WebGL webGL, final ShaderUnitDeprecated vertexShader, final ShaderUnitDeprecated fragmentShader) { + // TODO Auto-generated constructor stub + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/deprecated/ShaderUnitDeprecated.java b/core/src/com/etheller/warsmash/viewer5/deprecated/ShaderUnitDeprecated.java new file mode 100644 index 0000000..75897cd --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/deprecated/ShaderUnitDeprecated.java @@ -0,0 +1,31 @@ +package com.etheller.warsmash.viewer5.deprecated; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; + +import com.badlogic.gdx.graphics.GL20; + +public class ShaderUnitDeprecated { + + public boolean ok; + private final int webglResource; + private final String src; + private final int shaderType; + + public ShaderUnitDeprecated(final GL20 gl, final String src, final int type) { + final int id = gl.glCreateShader(type); + this.ok = false; + this.webglResource = id; + this.src = src; + this.shaderType = type; + + gl.glShaderSource(id, src); + gl.glCompileShader(id); + + final IntBuffer success = ByteBuffer.allocateDirect(8).order(ByteOrder.nativeOrder()).asIntBuffer(); + gl.glGetShaderiv(id, GL20.GL_COMPILE_STATUS, success); + throw new UnsupportedOperationException("Not yet implemented, probably using library instead"); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/gl/ANGLEInstancedArrays.java b/core/src/com/etheller/warsmash/viewer5/gl/ANGLEInstancedArrays.java new file mode 100644 index 0000000..c1219af --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/gl/ANGLEInstancedArrays.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.viewer5.gl; + +/** + * TODO what is this? + */ +public interface ANGLEInstancedArrays { + + void glVertexAttribDivisorANGLE(int index, int divisor); + + void glDrawArraysInstancedANGLE(int mode, int first, int count, int instanceCount); + + void glDrawElementsInstancedANGLE(int mode, int count, int type, int indicesOffset, int instanceCount); +} diff --git a/core/src/com/etheller/warsmash/viewer5/gl/AudioExtension.java b/core/src/com/etheller/warsmash/viewer5/gl/AudioExtension.java new file mode 100644 index 0000000..8af22f0 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/gl/AudioExtension.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.viewer5.gl; + +import com.badlogic.gdx.audio.Sound; +import com.etheller.warsmash.viewer5.AudioContext; + +public interface AudioExtension { + AudioContext createContext(boolean world); + + float getDuration(Sound sound); + + void play(Sound buffer, final float volume, final float pitch, final float x, final float y, final float z, + final boolean is3DSound, float maxDistance, float refDistance, boolean looping); +} diff --git a/core/src/com/etheller/warsmash/viewer5/gl/ClientBuffer.java b/core/src/com/etheller/warsmash/viewer5/gl/ClientBuffer.java new file mode 100644 index 0000000..0c513f6 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/gl/ClientBuffer.java @@ -0,0 +1,55 @@ +package com.etheller.warsmash.viewer5.gl; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +import com.badlogic.gdx.graphics.GL20; + +public class ClientBuffer { + private final GL20 gl; + private final int buffer; + private int size; + private ByteBuffer arrayBuffer; + public ByteBuffer byteView; + public FloatBuffer floatView; + + public ClientBuffer(final GL20 gl) { + this(gl, 4); + } + + public ClientBuffer(final GL20 gl, final int size) { + this.gl = gl; + this.buffer = gl.glGenBuffer(); + this.arrayBuffer = null; + + this.reserve(size); + } + + public void reserve(final int size) { + if (this.size < size) { + + // Ensure the size is on a 4 byte boundary. + this.size = (int) Math.ceil(size / 4.) * 4; + + this.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.buffer); + + this.arrayBuffer = ByteBuffer.allocateDirect(this.size).order(ByteOrder.nativeOrder()); + this.gl.glBufferData(GL20.GL_ARRAY_BUFFER, this.size, this.arrayBuffer, GL20.GL_DYNAMIC_DRAW); + this.byteView = this.arrayBuffer; + this.floatView = this.arrayBuffer.asFloatBuffer(); + + } + } + + public void bindAndUpdate() { + bindAndUpdate(this.size); + } + + public void bindAndUpdate(final int size) { + final GL20 gl = this.gl; + + gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.buffer); + gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, 0, size, this.byteView); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/gl/DataTexture.java b/core/src/com/etheller/warsmash/viewer5/gl/DataTexture.java new file mode 100644 index 0000000..f19e50c --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/gl/DataTexture.java @@ -0,0 +1,70 @@ +package com.etheller.warsmash.viewer5.gl; + +import java.nio.Buffer; + +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.GL30; + +public class DataTexture { + public GL20 gl; + public int texture; + public int format; + public int internalFormat; + public int width = 0; + public int height = 0; + + public DataTexture(final GL20 gl, final int channels, final int width, final int height) { + this.gl = gl; + this.texture = gl.glGenTexture(); + this.format = (channels == 3 ? GL20.GL_RGB : GL20.GL_RGBA); + this.internalFormat = (channels == 3 ? GL20.GL_RGB : GL30.GL_RGBA32F); + + gl.glBindTexture(GL20.GL_TEXTURE_2D, this.texture); + gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_S, GL20.GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_T, GL20.GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MIN_FILTER, GL20.GL_NEAREST); + gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MAG_FILTER, GL20.GL_NEAREST); + + this.reserve(width, height); + } + + public void reserve(final int width, final int height) { + if ((this.width < width) || (this.height < height)) { + final GL20 gl = this.gl; + + this.width = Math.max(this.width, width); + this.height = Math.max(this.height, height); + + gl.glBindTexture(GL20.GL_TEXTURE_2D, this.texture); + gl.glTexImage2D(GL20.GL_TEXTURE_2D, 0, this.internalFormat, this.width, this.height, 0, this.format, + GL20.GL_FLOAT, null); + } + + } + + public void bindAndUpdate(final Buffer buffer) { + bindAndUpdate(buffer, this.width, this.height); + } + + public void bindAndUpdate(final Buffer buffer, final int width, final int height) { + final GL20 gl = this.gl; + + gl.glBindTexture(GL20.GL_TEXTURE_2D, this.texture); + gl.glTexSubImage2D(GL20.GL_TEXTURE_2D, 0, 0, 0, width, height, this.format, GL20.GL_FLOAT, buffer); + } + + public void bind(final int unit) { + final GL20 gl = this.gl; + + gl.glActiveTexture(GL20.GL_TEXTURE0 + unit); + gl.glBindTexture(GL20.GL_TEXTURE_2D, this.texture); + } + + public int getWidth() { + return this.width; + } + + public int getHeight() { + return this.height; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/gl/DynamicShadowExtension.java b/core/src/com/etheller/warsmash/viewer5/gl/DynamicShadowExtension.java new file mode 100644 index 0000000..aeae3c1 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/gl/DynamicShadowExtension.java @@ -0,0 +1,7 @@ +package com.etheller.warsmash.viewer5.gl; + +public interface DynamicShadowExtension { + void glFramebufferTexture(int target, int attachment, int texture, int level); + + void glDrawBuffer(int mode); +} diff --git a/core/src/com/etheller/warsmash/viewer5/gl/Extensions.java b/core/src/com/etheller/warsmash/viewer5/gl/Extensions.java new file mode 100644 index 0000000..3bc0699 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/gl/Extensions.java @@ -0,0 +1,14 @@ +package com.etheller.warsmash.viewer5.gl; + +public class Extensions { + public static ANGLEInstancedArrays angleInstancedArrays; + + public static DynamicShadowExtension dynamicShadowExtension; + + public static WireframeExtension wireframeExtension; + + public static AudioExtension audio; + + public static int GL_LINE = 0; + public static int GL_FILL = 0; +} diff --git a/core/src/com/etheller/warsmash/viewer5/gl/WebGL.java b/core/src/com/etheller/warsmash/viewer5/gl/WebGL.java new file mode 100644 index 0000000..6235a95 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/gl/WebGL.java @@ -0,0 +1,168 @@ +package com.etheller.warsmash.viewer5.gl; + +import java.util.HashMap; +import java.util.Map; + +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.Pixmap; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; +import com.etheller.warsmash.viewer5.Texture; +import com.etheller.warsmash.viewer5.deprecated.ShaderUnitDeprecated; + +/** + * This needs a rename. Just a ripoff of ghostwolf's wrapper utility class, it's + * a utility, not a webgl + */ +public class WebGL { + public GL20 gl; + public Map shaderUnits; + public Map shaderPrograms; + public ShaderProgram currentShaderProgram; + public String floatPrecision; + public final com.badlogic.gdx.graphics.Texture emptyTexture; + public ANGLEInstancedArrays instancedArrays; + + public WebGL(final GL20 gl) { + gl.glDepthFunc(GL20.GL_LEQUAL); + gl.glEnable(GL20.GL_DEPTH_TEST); + + // TODO here ghostwolf throws exceptions for unsupported versions of opengl + + this.gl = gl; + + this.shaderUnits = new HashMap<>(); + + this.shaderPrograms = new HashMap<>(); + + this.currentShaderProgram = null; + this.floatPrecision = "precision mediump float;\n"; + + final Pixmap imageData = new Pixmap(2, 2, Pixmap.Format.RGBA8888); + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 2; j++) { + imageData.drawPixel(i, j, 0x000000FF); + } + } + this.emptyTexture = new com.badlogic.gdx.graphics.Texture(imageData); + this.instancedArrays = Extensions.angleInstancedArrays; + } + + public ShaderUnitDeprecated createShaderUnit(final String src, final int type) { + final int hash = stringHash(src); // TODO: why on earth are we doing this, what about hash collisions? + if (!this.shaderUnits.containsKey(hash)) { + this.shaderUnits.put(hash, new ShaderUnitDeprecated(this.gl, src, type)); + } + return this.shaderUnits.get(hash); + } + + public ShaderProgram createShaderProgram(String vertexSrc, String fragmentSrc) { + vertexSrc = vertexSrc.replace("mediump", ""); + fragmentSrc = fragmentSrc.replace("mediump", ""); + final Map shaderPrograms = this.shaderPrograms; + + final int hash = stringHash(vertexSrc + fragmentSrc); + ShaderProgram.pedantic = false; + if (!shaderPrograms.containsKey(hash)) { + shaderPrograms.put(hash, new ShaderProgram(vertexSrc, fragmentSrc)); + } + + final ShaderProgram shaderProgram = shaderPrograms.get(hash); + + if (shaderProgram.isCompiled()) { + return shaderProgram; + } + else { + System.err.println(shaderProgram.getLog()); + if (true) { + throw new IllegalStateException("Bad shader"); + } + } + return null; + } + + public void enableVertexAttribs(final int start, final int end) { + final GL20 gl = this.gl; + + for (int i = start; i < end; i++) { + gl.glEnableVertexAttribArray(i); + } + } + + public void disableVertexAttribs(final int start, final int end) { + final GL20 gl = this.gl; + + for (int i = start; i < end; i++) { + gl.glDisableVertexAttribArray(i); + } + } + + public void useShaderProgram(final ShaderProgram shaderProgram) { + final ShaderProgram currentShaderProgram = this.currentShaderProgram; + + if ((shaderProgram != null) && shaderProgram.isCompiled() && (shaderProgram != currentShaderProgram)) { + int oldAttribs = 0; + final int newAttribs = shaderProgram.getAttributes().length; + + if (currentShaderProgram != null) { + oldAttribs = currentShaderProgram.getAttributes().length; + } + + shaderProgram.begin(); + + if (newAttribs > oldAttribs) { + this.enableVertexAttribs(oldAttribs, newAttribs); + } + else if (newAttribs < oldAttribs) { + this.disableVertexAttribs(newAttribs, oldAttribs); + } + + this.currentShaderProgram = shaderProgram; + } + else if (shaderProgram == null) { + int oldAttribs = 0; + final int newAttribs = 0; + + if (currentShaderProgram != null) { + oldAttribs = currentShaderProgram.getAttributes().length; + currentShaderProgram.end(); + } + + if (newAttribs > oldAttribs) { + this.enableVertexAttribs(oldAttribs, newAttribs); + } + else if (newAttribs < oldAttribs) { + this.disableVertexAttribs(newAttribs, oldAttribs); + } + + this.currentShaderProgram = shaderProgram; + } + } + + public void bindTexture(final Texture texture, final int unit) { + final GL20 gl = this.gl; + + gl.glActiveTexture(GL20.GL_TEXTURE0 + unit); + + if (texture != null /* && texture.ok */) { + texture.internalBind(); + } + else { + this.emptyTexture.bind(); + } + } + + public void setTextureMode(final int wrapS, final int wrapT, final int magFilter, final int minFilter) { + final GL20 gl = this.gl; + + // TODO make sure we dont assign this parameter doubly if we're already using + // libgdx texture, which does do some wrapS and wrapT stuff already + gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_S, wrapS); + gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_T, wrapT); + gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MAG_FILTER, magFilter); + gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MIN_FILTER, minFilter); + } + + private int stringHash(final String src) { + return src.hashCode(); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/gl/WireframeExtension.java b/core/src/com/etheller/warsmash/viewer5/gl/WireframeExtension.java new file mode 100644 index 0000000..1041bc6 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/gl/WireframeExtension.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.viewer5.gl; + +public interface WireframeExtension { + void glPolygonMode(int face, int mode); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/AbstractMdxModelViewer.java b/core/src/com/etheller/warsmash/viewer5/handlers/AbstractMdxModelViewer.java new file mode 100644 index 0000000..012b055 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/AbstractMdxModelViewer.java @@ -0,0 +1,21 @@ +package com.etheller.warsmash.viewer5.handlers; + +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.util.StringBundle; +import com.etheller.warsmash.viewer5.CanvasProvider; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.PathSolver; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer.SolverParams; + +public abstract class AbstractMdxModelViewer extends ModelViewer { + public PathSolver wc3PathSolver = PathSolver.DEFAULT; + public PathSolver mapPathSolver = PathSolver.DEFAULT; + public SolverParams solverParams = new SolverParams(); + + public AbstractMdxModelViewer(final DataSource dataSource, final CanvasProvider canvas) { + super(dataSource, canvas); + } + + public abstract StringBundle getWorldEditStrings(); + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/EmitterObject.java b/core/src/com/etheller/warsmash/viewer5/handlers/EmitterObject.java new file mode 100644 index 0000000..e9300b7 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/EmitterObject.java @@ -0,0 +1,7 @@ +package com.etheller.warsmash.viewer5.handlers; + +public interface EmitterObject { + boolean ok(); + + int getGeometryEmitterType(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/ModelHandler.java b/core/src/com/etheller/warsmash/viewer5/handlers/ModelHandler.java new file mode 100644 index 0000000..6598bdc --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/ModelHandler.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.viewer5.handlers; + +public abstract class ModelHandler extends ResourceHandler { + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/ModelInstanceDescriptor.java b/core/src/com/etheller/warsmash/viewer5/handlers/ModelInstanceDescriptor.java new file mode 100644 index 0000000..336e9ed --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/ModelInstanceDescriptor.java @@ -0,0 +1,8 @@ +package com.etheller.warsmash.viewer5.handlers; + +import com.etheller.warsmash.viewer5.Model; +import com.etheller.warsmash.viewer5.ModelInstance; + +public interface ModelInstanceDescriptor { + ModelInstance create(Model model); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/ResourceHandler.java b/core/src/com/etheller/warsmash/viewer5/handlers/ResourceHandler.java new file mode 100644 index 0000000..2571736 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/ResourceHandler.java @@ -0,0 +1,16 @@ +package com.etheller.warsmash.viewer5.handlers; + +import java.util.List; + +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.HandlerResource; + +public abstract class ResourceHandler { + public ResourceHandler handler; + public boolean load; + public List extensions; + + public abstract boolean load(ModelViewer modelViewer); + + public abstract HandlerResource construct(ResourceHandlerConstructionParams params); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/ResourceHandlerConstructionParams.java b/core/src/com/etheller/warsmash/viewer5/handlers/ResourceHandlerConstructionParams.java new file mode 100644 index 0000000..1eb95b3 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/ResourceHandlerConstructionParams.java @@ -0,0 +1,42 @@ +package com.etheller.warsmash.viewer5.handlers; + +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.PathSolver; + +public class ResourceHandlerConstructionParams { + public final ModelViewer viewer; + public final ResourceHandler handler; + public final String extension; + public final PathSolver pathSolver; + public final String fetchUrl; + + public ResourceHandlerConstructionParams(final ModelViewer viewer, final ResourceHandler handler, + final String extension, final PathSolver pathSolver, final String fetchUrl) { + this.viewer = viewer; + this.handler = handler; + this.extension = extension; + this.pathSolver = pathSolver; + this.fetchUrl = fetchUrl; + } + + public ModelViewer getViewer() { + return this.viewer; + } + + public ResourceHandler getHandler() { + return this.handler; + } + + public String getExtension() { + return this.extension; + } + + public PathSolver getPathSolver() { + return this.pathSolver; + } + + public String getFetchUrl() { + return this.fetchUrl; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/blp/BlpGdxTexture.java b/core/src/com/etheller/warsmash/viewer5/handlers/blp/BlpGdxTexture.java new file mode 100644 index 0000000..a29e6d6 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/blp/BlpGdxTexture.java @@ -0,0 +1,39 @@ +package com.etheller.warsmash.viewer5.handlers.blp; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; + +import javax.imageio.ImageIO; + +import com.etheller.warsmash.util.ImageUtils; +import com.etheller.warsmash.viewer5.GdxTextureResource; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.PathSolver; +import com.etheller.warsmash.viewer5.handlers.ResourceHandler; + +public class BlpGdxTexture extends GdxTextureResource { + + public BlpGdxTexture(final ModelViewer viewer, final ResourceHandler handler, final String extension, + final PathSolver pathSolver, final String fetchUrl) { + super(viewer, handler, extension, pathSolver, fetchUrl); + } + + @Override + protected void lateLoad() { + + } + + @Override + protected void load(final InputStream src, final Object options) { + BufferedImage img; + try { + img = ImageIO.read(src); + setGdxTexture(ImageUtils.getTexture(img, true)); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/blp/BlpHandler.java b/core/src/com/etheller/warsmash/viewer5/handlers/blp/BlpHandler.java new file mode 100644 index 0000000..372ad6a --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/blp/BlpHandler.java @@ -0,0 +1,28 @@ +package com.etheller.warsmash.viewer5.handlers.blp; + +import java.util.ArrayList; + +import com.etheller.warsmash.viewer5.HandlerResource; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.handlers.ResourceHandler; +import com.etheller.warsmash.viewer5.handlers.ResourceHandlerConstructionParams; + +public class BlpHandler extends ResourceHandler { + + public BlpHandler() { + this.extensions = new ArrayList<>(); + this.extensions.add(new String[] { ".blp", "arrayBuffer" }); + } + + @Override + public boolean load(final ModelViewer modelViewer) { + return true; + } + + @Override + public HandlerResource construct(final ResourceHandlerConstructionParams params) { + return new BlpTexture(params.getViewer(), params.getHandler(), params.getExtension(), params.getPathSolver(), + params.getFetchUrl()); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/blp/BlpTexture.java b/core/src/com/etheller/warsmash/viewer5/handlers/blp/BlpTexture.java new file mode 100644 index 0000000..7ba8968 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/blp/BlpTexture.java @@ -0,0 +1,38 @@ +package com.etheller.warsmash.viewer5.handlers.blp; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; + +import javax.imageio.ImageIO; + +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.PathSolver; +import com.etheller.warsmash.viewer5.RawOpenGLTextureResource; +import com.etheller.warsmash.viewer5.handlers.ResourceHandler; + +public class BlpTexture extends RawOpenGLTextureResource { + + public BlpTexture(final ModelViewer viewer, final ResourceHandler handler, final String extension, + final PathSolver pathSolver, final String fetchUrl) { + super(viewer, extension, pathSolver, fetchUrl, handler); + } + + @Override + protected void lateLoad() { + + } + + @Override + protected void load(final InputStream src, final Object options) { + BufferedImage img; + try { + img = ImageIO.read(src); + update(img, true); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/blp/DdsHandler.java b/core/src/com/etheller/warsmash/viewer5/handlers/blp/DdsHandler.java new file mode 100644 index 0000000..69f5f78 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/blp/DdsHandler.java @@ -0,0 +1,28 @@ +package com.etheller.warsmash.viewer5.handlers.blp; + +import java.util.ArrayList; + +import com.etheller.warsmash.viewer5.HandlerResource; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.handlers.ResourceHandler; +import com.etheller.warsmash.viewer5.handlers.ResourceHandlerConstructionParams; + +public class DdsHandler extends ResourceHandler { + + public DdsHandler() { + this.extensions = new ArrayList<>(); + this.extensions.add(new String[] { ".dds", "arrayBuffer" }); + } + + @Override + public boolean load(final ModelViewer modelViewer) { + return true; + } + + @Override + public HandlerResource construct(final ResourceHandlerConstructionParams params) { + return new DdsTexture(params.getViewer(), params.getHandler(), params.getExtension(), params.getPathSolver(), + params.getFetchUrl()); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/blp/DdsTexture.java b/core/src/com/etheller/warsmash/viewer5/handlers/blp/DdsTexture.java new file mode 100644 index 0000000..f33529e --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/blp/DdsTexture.java @@ -0,0 +1,38 @@ +package com.etheller.warsmash.viewer5.handlers.blp; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; + +import javax.imageio.ImageIO; + +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.PathSolver; +import com.etheller.warsmash.viewer5.RawOpenGLTextureResource; +import com.etheller.warsmash.viewer5.handlers.ResourceHandler; + +public class DdsTexture extends RawOpenGLTextureResource { + + public DdsTexture(final ModelViewer viewer, final ResourceHandler handler, final String extension, + final PathSolver pathSolver, final String fetchUrl) { + super(viewer, extension, pathSolver, fetchUrl, handler); + } + + @Override + protected void lateLoad() { + + } + + @Override + protected void load(final InputStream src, final Object options) { + BufferedImage img; + try { + img = ImageIO.read(src); + update(img, false); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/AnimatedObject.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/AnimatedObject.java new file mode 100644 index 0000000..96576b9 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/AnimatedObject.java @@ -0,0 +1,152 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.util.HashMap; +import java.util.Map; + +import com.etheller.warsmash.util.War3ID; +import com.hiveworkshop.rms.parsers.mdlx.MdlxAnimatedObject; +import com.hiveworkshop.rms.parsers.mdlx.timeline.MdlxFloatArrayTimeline; +import com.hiveworkshop.rms.parsers.mdlx.timeline.MdlxFloatTimeline; +import com.hiveworkshop.rms.parsers.mdlx.timeline.MdlxTimeline; +import com.hiveworkshop.rms.parsers.mdlx.timeline.MdlxUInt32Timeline; + +public class AnimatedObject { + public MdxModel model; + public Map> timelines; + public Map variants; + + public AnimatedObject(final MdxModel model, final MdlxAnimatedObject object) { + this.model = model; + this.timelines = new HashMap<>(); + this.variants = new HashMap<>(); + + for (final MdlxTimeline timeline : object.getTimelines()) { + this.timelines.put(timeline.getName(), createTypedSd(model, timeline)); + } + } + + public int getScalarValue(final float[] out, final War3ID name, final int sequence, final int frame, + final int counter, final float defaultValue) { + if (sequence != -1) { + final Sd animation = this.timelines.get(name); + + if (animation instanceof ScalarSd) { + return ((ScalarSd) animation).getValue(out, sequence, frame, counter); + } + } + + out[0] = defaultValue; + + return -1; + } + + public int getScalarValue(final long[] out, final War3ID name, final int sequence, final int frame, + final int counter, final long defaultValue) { + if (sequence != -1) { + final Sd animation = this.timelines.get(name); + + if (animation instanceof UInt32Sd) { + return ((UInt32Sd) animation).getValue(out, sequence, frame, counter); + } + } + + out[0] = defaultValue; + + return -1; + } + + public int getVectorValue(final float[] out, final War3ID name, final int sequence, final int frame, + final int counter, final float[] defaultValue) { + if (sequence != -1) { + final Sd animation = this.timelines.get(name); + + if (animation instanceof VectorSd) { + return ((VectorSd) animation).getValue(out, sequence, frame, counter); + } + } + + System.arraycopy(defaultValue, 0, out, 0, 3); + + return -1; + } + + public int getQuatValue(final float[] out, final War3ID name, final int sequence, final int frame, + final int counter, final float[] defaultValue) { + if (sequence != -1) { + final Sd animation = this.timelines.get(name); + + if (animation instanceof QuaternionSd) { + return ((QuaternionSd) animation).getValue(out, sequence, frame, counter); + } + } + + System.arraycopy(defaultValue, 0, out, 0, 4); + + return -1; + } + + public void addVariants(final War3ID name, final String variantName) { + final Sd timeline = this.timelines.get(name); + final int sequences = this.model.getSequences().size(); + final byte[] variants = new byte[sequences]; + + if (timeline != null) { + for (int i = 0; i < sequences; i++) { + if (timeline.isVariant(i)) { + variants[i] = 1; + } + } + } + + this.variants.put(variantName, variants); + } + + public void addVariantIntersection(final String[] names, final String variantName) { + final int sequences = this.model.getSequences().size(); + final byte[] variants = new byte[sequences]; + + for (int i = 0; i < sequences; i++) { + for (final String name : names) { + final byte[] variantsAtName = this.variants.get(name); + if ((variantsAtName != null) && (variantsAtName[i] != 0)) { + variants[i] = 1; + } + } + } + + this.variants.put(variantName, variants); + } + + public boolean isVariant(final War3ID name, final int sequence) { + final Sd timeline = this.timelines.get(name); + + if (timeline != null) { + return timeline.isVariant(sequence); + } + + return false; + } + + private Sd createTypedSd(final MdxModel model, final MdlxTimeline timeline) { + if (timeline instanceof MdlxUInt32Timeline) { + return new UInt32Sd(model, (MdlxUInt32Timeline) timeline); + } + else if (timeline instanceof MdlxFloatTimeline) { + return new ScalarSd(model, (MdlxFloatTimeline) timeline); + } + else if (timeline instanceof MdlxFloatArrayTimeline) { + final MdlxFloatArrayTimeline faTimeline = (MdlxFloatArrayTimeline) timeline; + final int arraySize = faTimeline.getArraySize(); + if (arraySize == 3) { + return new VectorSd(model, faTimeline); + } + else if (arraySize == 4) { + return new QuaternionSd(model, faTimeline); + } + else { + throw new IllegalStateException("Unsupported arraySize = " + arraySize); + } + } + throw new IllegalStateException("Unsupported timeline type " + timeline.getClass()); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Attachment.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Attachment.java new file mode 100644 index 0000000..96f05b4 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Attachment.java @@ -0,0 +1,40 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.hiveworkshop.rms.parsers.mdlx.AnimationMap; +import com.hiveworkshop.rms.parsers.mdlx.MdlxAttachment; + +public class Attachment extends GenericObject { + protected String name; + protected final String path; + protected final int attachmentId; + protected MdxModel internalModel; + + public Attachment(final MdxModel model, final MdlxAttachment attachment, final int index) { + super(model, attachment, index); + + final String path = attachment.getPath().toLowerCase().replace(".mdl", ".mdx"); + + this.name = attachment.getName().toLowerCase(); + this.path = path; + this.attachmentId = attachment.getAttachmentId(); + this.internalModel = null; + + // Second condition is against custom resources using arbitrary paths + if (!path.equals("") && (path.indexOf(".mdx") != -1)) { + this.internalModel = (MdxModel) model.viewer.load(path, model.pathSolver, model.solverParams); + } + } + + @Override + public int getVisibility(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KATV.getWar3id(), sequence, frame, counter, 1); + } + + public String getName() { + return this.name; + } + + public int getAttachmentId() { + return this.attachmentId; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/AttachmentInstance.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/AttachmentInstance.java new file mode 100644 index 0000000..c898223 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/AttachmentInstance.java @@ -0,0 +1,58 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.etheller.warsmash.viewer5.UpdatableObject; + +public class AttachmentInstance implements UpdatableObject { + private static final float[] visbilityHeap = new float[1]; + + private final MdxComplexInstance instance; + private final Attachment attachment; + public final MdxComplexInstance internalInstance; + + public AttachmentInstance(final MdxComplexInstance instance, final Attachment attachment) { + final MdxModel internalModel = attachment.internalModel; + final MdxComplexInstance internalInstance = (MdxComplexInstance) internalModel.addInstance(); + + internalInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + internalInstance.dontInheritScaling = false; + internalInstance.hide(); + internalInstance.setParent(instance.nodes[attachment.objectId]); + internalInstance.setAnimationSpeed(instance.getAnimationSpeed()); + + this.instance = instance; + this.attachment = attachment; + this.internalInstance = internalInstance; + } + + @Override + public void update(final float dt, final boolean objectVisible) { + final MdxComplexInstance internalInstance = this.internalInstance; + if (internalInstance.model.ok) { + if (!objectVisible) { + internalInstance.hide(); + } + else { + this.attachment.getVisibility(visbilityHeap, this.instance.sequence, this.instance.frame, + this.instance.counter); + + if (visbilityHeap[0] > 0.1) { + // The parent instance might not actually be in a scene. + // This happens if loading a local model, where loading is instant and adding to + // a scene always comes afterwards. + // Therefore, do it here dynamically. + this.instance.scene.addInstance(internalInstance); + + if (internalInstance.hidden()) { + internalInstance.show(); + + // Every time the attachment becomes visible again, restart its first sequence. + internalInstance.setSequence(0); + } + } + else { + internalInstance.hide(); + } + } + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Batch.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Batch.java new file mode 100644 index 0000000..02dc3b1 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Batch.java @@ -0,0 +1,21 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +public class Batch implements GenericIndexed { + public int index; + public Geoset geoset; + public Layer layer; + public boolean isExtended; + + public Batch(final int index, final Geoset geoset, final Layer layer, final boolean isExtended) { + this.index = index; + this.geoset = geoset; + this.layer = layer; + this.isExtended = isExtended; + } + + @Override + public int getIndex() { + return this.index; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/BatchGroup.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/BatchGroup.java new file mode 100644 index 0000000..d80a550 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/BatchGroup.java @@ -0,0 +1,155 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.util.List; + +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; +import com.badlogic.gdx.math.Matrix4; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.Texture; +import com.etheller.warsmash.viewer5.gl.DataTexture; +import com.etheller.warsmash.viewer5.gl.WebGL; +import com.etheller.warsmash.viewer5.handlers.w3x.DynamicShadowManager; +import com.etheller.warsmash.viewer5.handlers.w3x.W3xSceneLightManager; + +public class BatchGroup extends GenericGroup { + + private final MdxModel model; + public final boolean isExtended; + + public BatchGroup(final MdxModel model, final boolean isExtended) { + this.model = model; + this.isExtended = isExtended; + } + + @Override + public void render(final MdxComplexInstance instance, final Matrix4 mvp) { + final Scene scene = instance.scene; + final MdxModel model = this.model; + final List textures = model.getTextures(); + final MdxHandler handler = model.handler; + final List batches = model.batches; + final List replaceables = model.replaceables; + final ModelViewer viewer = model.viewer; + final GL20 gl = viewer.gl; + final WebGL webGL = viewer.webGL; + final boolean isExtended = this.isExtended; + final ShaderProgram shader; + final W3xSceneLightManager lightManager = (W3xSceneLightManager) scene.getLightManager(); + + if (isExtended) { + if (DynamicShadowManager.IS_SHADOW_MAPPING) { + shader = MdxHandler.Shaders.extendedShadowMap; + } + else { + shader = MdxHandler.Shaders.extended; + } + } + else { + if (DynamicShadowManager.IS_SHADOW_MAPPING) { + shader = MdxHandler.Shaders.complexShadowMap; + } + else { + shader = MdxHandler.Shaders.complex; + } + } + + webGL.useShaderProgram(shader); + + shader.setUniformMatrix("u_mvp", mvp); + + final DataTexture boneTexture = instance.boneTexture; + final DataTexture unitLightsTexture = lightManager.getUnitLightsTexture(); + + unitLightsTexture.bind(14); + shader.setUniformi("u_lightTexture", 14); + shader.setUniformf("u_lightCount", lightManager.getUnitLightCount()); + shader.setUniformf("u_lightTextureHeight", unitLightsTexture.getHeight()); + + // Instances of models with no bones don't have a bone texture. + if (boneTexture != null) { + boneTexture.bind(15); + + shader.setUniformf("u_hasBones", 1); + shader.setUniformi("u_boneMap", 15); + shader.setUniformf("u_vectorSize", 1f / boneTexture.getWidth()); + shader.setUniformf("u_rowSize", 1); + } + else { + shader.setUniformf("u_hasBones", 0); + } + + shader.setUniformi("u_texture", 0); + + gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, model.arrayBuffer); + gl.glBindBuffer(GL20.GL_ELEMENT_ARRAY_BUFFER, model.elementBuffer); + + shader.setUniform4fv("u_vertexColor", instance.vertexColor, 0, instance.vertexColor.length); + + for (final int index : this.objects) { + final Batch batch = batches.get(index); + final Geoset geoset = batch.geoset; + final Layer layer = batch.layer; + final int geosetIndex = geoset.index; + final int layerIndex = layer.index; + final float[] geosetColor = instance.geosetColors[geosetIndex]; + final float layerAlpha = instance.layerAlphas[layerIndex]; + + if ((geosetColor[3] > 0.01) && (layerAlpha > 0.01)) { + // BELOW: I updated it to "Math.max(0," because MDL and MDX parser for PRSCMOD + // menu screen behaved differently, + // the MDL case was getting "no data" for default value when unanimated, and "no + // data" resolved to -1, + // whereas MDX binary contained an "unused" 0 value. + final int layerTexture = Math.max(0, instance.layerTextures[layerIndex]); + final float[] uvAnim = instance.uvAnims[layerIndex]; + + shader.setUniform4fv("u_geosetColor", geosetColor, 0, geosetColor.length); + + shader.setUniformf("u_layerAlpha", layerAlpha); + shader.setUniformf("u_unshaded", layer.unshaded); + + shader.setUniform2fv("u_uvTrans", uvAnim, 0, 2); + shader.setUniform2fv("u_uvRot", uvAnim, 2, 2); + shader.setUniform1fv("u_uvScale", uvAnim, 4, 1); + + if (instance.vertexColor[3] < 1.0f) { + layer.bindBlended(shader); + } + else { + layer.bind(shader); + } + + final Integer replaceable = replaceables.get(layerTexture); // TODO is this OK? + Texture texture; + + if ((replaceable > 0) && (replaceable < WarsmashConstants.REPLACEABLE_TEXTURE_LIMIT) + && (instance.replaceableTextures[replaceable] != null)) { + texture = instance.replaceableTextures[replaceable]; + } + else { + texture = textures.get(layerTexture); + + Texture textureLookup = instance.textureMapper.get(texture); + if (textureLookup == null) { + textureLookup = texture; + } + texture = textureLookup; + } + + viewer.webGL.bindTexture(texture, 0); + + if (isExtended) { + geoset.bindExtended(shader, layer.coordId); + } + else { + geoset.bind(shader, layer.coordId); + } + + geoset.render(); + } + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Bone.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Bone.java new file mode 100644 index 0000000..eab052c --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Bone.java @@ -0,0 +1,40 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.hiveworkshop.rms.parsers.mdlx.MdlxBone; + +public class Bone extends GenericObject { + + private final GeosetAnimation geosetAnimation; + + public Bone(final MdxModel model, final MdlxBone bone, final int index) { + super(model, bone, index); + + GeosetAnimation geosetAnimation = null; + final int geosetId = bone.getGeosetId(); + if (geosetId != -1) { + final Geoset geoset = model.getGeosets().get(geosetId); + if (geoset.geosetAnimation != null) { + geosetAnimation = geoset.geosetAnimation; + } + else { + final int geosetAnimationId = bone.getGeosetAnimationId(); + if (geosetAnimationId != -1) { + geosetAnimation = model.getGeosetAnimations().get(geosetAnimationId); + } + } + } + this.geosetAnimation = geosetAnimation; + } + + @Override + public int getVisibility(final float[] out, final int sequence, final int frame, final int counter) { + if (this.geosetAnimation != null) { + return this.geosetAnimation.getAlpha(out, sequence, frame, counter); + } + + out[0] = 1; + + return -1; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Camera.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Camera.java new file mode 100644 index 0000000..cfcc6af --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Camera.java @@ -0,0 +1,41 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.etheller.warsmash.util.RenderMathUtils; +import com.hiveworkshop.rms.parsers.mdlx.AnimationMap; +import com.hiveworkshop.rms.parsers.mdlx.MdlxCamera; + +public class Camera extends AnimatedObject { + + public final String name; + public final float[] position; + public final float fieldOfView; + public final float farClippingPlane; + public final float nearClippingPlane; + public final float[] targetPosition; + + public Camera(final MdxModel model, final MdlxCamera camera) { + super(model, camera); + + this.name = camera.getName(); + this.position = camera.getPosition(); + this.fieldOfView = camera.getFieldOfView(); + this.farClippingPlane = camera.getFarClippingPlane(); + this.nearClippingPlane = camera.getNearClippingPlane(); + this.targetPosition = camera.getTargetPosition(); + } + + public int getPositionTranslation(final float[] out, final int sequence, final int frame, final int counter) { + return this.getVectorValue(out, AnimationMap.KCTR.getWar3id(), sequence, frame, counter, + RenderMathUtils.FLOAT_VEC3_ZERO); + } + + public int getTargetTranslation(final float[] out, final int sequence, final int frame, final int counter) { + return this.getVectorValue(out, AnimationMap.KTTR.getWar3id(), sequence, frame, counter, + RenderMathUtils.FLOAT_VEC3_ZERO); + } + + public int getRotation(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KCRL.getWar3id(), sequence, frame, counter, 0); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/CollisionShape.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/CollisionShape.java new file mode 100644 index 0000000..7044eff --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/CollisionShape.java @@ -0,0 +1,110 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.badlogic.gdx.math.Intersector; +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Vector3; +import com.badlogic.gdx.math.collision.BoundingBox; +import com.badlogic.gdx.math.collision.Ray; +import com.etheller.warsmash.util.RenderMathUtils; +import com.etheller.warsmash.viewer5.GenericNode; +import com.hiveworkshop.rms.parsers.mdlx.MdlxCollisionShape; + +public class CollisionShape extends GenericObject { + private static Vector3 intersectHeap = new Vector3(); + private static Vector3 intersectHeap2 = new Vector3(); + private static Matrix4 intersectMatrixHeap = new Matrix4(); + private static Ray intersectRayHeap = new Ray(); + private Intersectable intersectable; + + public CollisionShape(final MdxModel model, final MdlxCollisionShape object, final int index) { + super(model, object, index); + final float[][] vertices = object.getVertices(); + + switch (object.getType()) { + case BOX: + this.intersectable = new IntersectableBox(vertices[0], vertices[1]); + break; + case CYLINDER: + this.intersectable = null; // TODO + break; + case PLANE: + this.intersectable = null; // TODO + break; + case SPHERE: + this.intersectable = new IntersectableSphere(vertices[0], object.getBoundsRadius()); + break; + } + } + + public boolean checkIntersect(final Ray ray, final MdxNode mdxNode, final Vector3 intersection) { + if (this.intersectable != null) { + return this.intersectable.checkIntersect(ray, mdxNode, intersection); + } + return false; + } + + private static interface Intersectable { + boolean checkIntersect(final Ray ray, final MdxNode mdxNode, final Vector3 intersection); + } + + private static final class IntersectableBox implements Intersectable { + private final BoundingBox boundingBox; + + public IntersectableBox(final float[] vertex1, final float[] vertex2) { + this.boundingBox = new BoundingBox(new Vector3(vertex1), new Vector3(vertex2)); + } + + @Override + public boolean checkIntersect(final Ray ray, final MdxNode mdxNode, final Vector3 intersection) { + intersectMatrixHeap.set(mdxNode.worldMatrix); + Matrix4.inv(intersectMatrixHeap.val); + intersectHeap.set(ray.origin); + intersectHeap2.set(ray.direction); + intersectHeap2.add(ray.origin); + intersectHeap.prj(intersectMatrixHeap); + intersectHeap2.prj(intersectMatrixHeap); + intersectHeap2.sub(intersectHeap); + intersectRayHeap.set(intersectHeap, intersectHeap2); + if (Intersector.intersectRayBounds(intersectRayHeap, this.boundingBox, intersection)) { + intersection.prj(mdxNode.worldMatrix); + return true; + } + return false; + } + } + + private static final class IntersectableSphere implements Intersectable { + private final Vector3 center; + private final float radius; + + public IntersectableSphere(final float[] center, final float radius) { + this.center = new Vector3(center); + this.radius = radius; + } + + @Override + public boolean checkIntersect(final Ray ray, final MdxNode mdxNode, final Vector3 intersection) { + intersectHeap.set(this.center); + intersectHeap.prj(mdxNode.worldMatrix); + return Intersector.intersectRaySphere(ray, intersectHeap, this.radius, intersection); + } + } + + public static boolean intersectRayTriangles(final Ray ray, final GenericNode mdxNode, final float[] vertices, + final int[] indices, final int vertexSize, final Vector3 intersection) { + intersectMatrixHeap.set(mdxNode.worldMatrix); + Matrix4.inv(intersectMatrixHeap.val); + intersectHeap.set(ray.origin); + intersectHeap2.set(ray.direction); + intersectHeap2.add(ray.origin); + intersectHeap.prj(intersectMatrixHeap); + intersectHeap2.prj(intersectMatrixHeap); + intersectHeap2.sub(intersectHeap); + intersectRayHeap.set(intersectHeap, intersectHeap2); + if (RenderMathUtils.intersectRayTriangles(intersectRayHeap, vertices, indices, vertexSize, intersection)) { + intersection.prj(mdxNode.worldMatrix); + return true; + } + return false; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EmitterGroup.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EmitterGroup.java new file mode 100644 index 0000000..7fed7a8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EmitterGroup.java @@ -0,0 +1,73 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; +import com.badlogic.gdx.math.Matrix4; +import com.etheller.warsmash.viewer5.Model; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.SkeletalNode; +import com.etheller.warsmash.viewer5.gl.ANGLEInstancedArrays; +import com.etheller.warsmash.viewer5.handlers.w3x.DynamicShadowManager; + +public class EmitterGroup extends GenericGroup { + private final MdxModel model; + + public EmitterGroup(final MdxModel model) { + this.model = model; + } + + @Override + public void render(final MdxComplexInstance instance, final Matrix4 mvp) { + if (DynamicShadowManager.IS_SHADOW_MAPPING) { + return; + } + + final Scene scene = instance.scene; + final SkeletalNode[] nodes = instance.nodes; + final Model model = instance.model; + final ModelViewer viewer = model.viewer; + final GL20 gl = viewer.gl; + final ANGLEInstancedArrays instancedArrays = viewer.webGL.instancedArrays; + final ShaderProgram shader = MdxHandler.Shaders.particles; + + gl.glDepthMask(false); + gl.glEnable(GL20.GL_BLEND); + gl.glDisable(GL20.GL_CULL_FACE); + gl.glEnable(GL20.GL_DEPTH_TEST); + + viewer.webGL.useShaderProgram(shader); + + shader.setUniformMatrix("u_mvp", mvp); + shader.setUniformi("u_texture", 0); + + final int a_position = shader.getAttributeLocation("a_position"); + instancedArrays.glVertexAttribDivisorANGLE(a_position, 0); + + gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, viewer.rectBuffer); + gl.glVertexAttribPointer(a_position, 1, GL20.GL_UNSIGNED_BYTE, false, 0, 0); + + instancedArrays.glVertexAttribDivisorANGLE(shader.getAttributeLocation("a_p0"), 1); + instancedArrays.glVertexAttribDivisorANGLE(shader.getAttributeLocation("a_p1"), 1); + instancedArrays.glVertexAttribDivisorANGLE(shader.getAttributeLocation("a_p2"), 1); + instancedArrays.glVertexAttribDivisorANGLE(shader.getAttributeLocation("a_p3"), 1); + instancedArrays.glVertexAttribDivisorANGLE(shader.getAttributeLocation("a_health"), 1); + instancedArrays.glVertexAttribDivisorANGLE(shader.getAttributeLocation("a_color"), 1); + instancedArrays.glVertexAttribDivisorANGLE(shader.getAttributeLocation("a_tail"), 1); + instancedArrays.glVertexAttribDivisorANGLE(shader.getAttributeLocation("a_leftRightTop"), 1); + + for (final int index : this.objects) { + GeometryEmitterFuncs.renderEmitter((MdxEmitter) nodes[index].object, shader); + } + + instancedArrays.glVertexAttribDivisorANGLE(shader.getAttributeLocation("a_leftRightTop"), 0); + instancedArrays.glVertexAttribDivisorANGLE(shader.getAttributeLocation("a_tail"), 0); + instancedArrays.glVertexAttribDivisorANGLE(shader.getAttributeLocation("a_color"), 0); + instancedArrays.glVertexAttribDivisorANGLE(shader.getAttributeLocation("a_health"), 0); + instancedArrays.glVertexAttribDivisorANGLE(shader.getAttributeLocation("a_p3"), 0); + instancedArrays.glVertexAttribDivisorANGLE(shader.getAttributeLocation("a_p2"), 0); + instancedArrays.glVertexAttribDivisorANGLE(shader.getAttributeLocation("a_p1"), 0); + instancedArrays.glVertexAttribDivisorANGLE(shader.getAttributeLocation("a_p0"), 0); + + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectEmitter.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectEmitter.java new file mode 100644 index 0000000..9c5a225 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectEmitter.java @@ -0,0 +1,42 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.etheller.warsmash.viewer5.EmittedObject; + +public abstract class EventObjectEmitter>> + extends MdxEmitter { + private static final long[] valueHeap = { 0L }; + + private int lastEmissionKey; + + public EventObjectEmitter(final MdxComplexInstance instance, final EMITTER_OBJECT emitterObject) { + super(instance, emitterObject); + this.lastEmissionKey = -1; + } + + @Override + protected void updateEmission(final float dt) { + final MdxComplexInstance instance = this.instance; + + if (instance.allowParticleSpawn) { + final EMITTER_OBJECT emitterObject = this.emitterObject; + + final int keyframe = emitterObject.getValue(valueHeap, instance); + + if (keyframe != this.lastEmissionKey) { + this.currentEmission += 1; + this.lastEmissionKey = keyframe; + } + } + + } + + public void reset() { + this.lastEmissionKey = -1; + } + + @Override + protected void emit() { + this.emitObject(0); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectEmitterObject.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectEmitterObject.java new file mode 100644 index 0000000..0b47093 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectEmitterObject.java @@ -0,0 +1,369 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.audio.Sound; +import com.badlogic.gdx.files.FileHandle; +import com.etheller.warsmash.common.FetchDataTypeName; +import com.etheller.warsmash.common.LoadGenericCallback; +import com.etheller.warsmash.util.MappedData; +import com.etheller.warsmash.util.MappedDataRow; +import com.etheller.warsmash.viewer5.GenericResource; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.PathSolver; +import com.etheller.warsmash.viewer5.Texture; +import com.etheller.warsmash.viewer5.handlers.EmitterObject; +import com.hiveworkshop.rms.parsers.mdlx.MdlxEventObject; +import com.hiveworkshop.rms.parsers.mdlx.MdlxParticleEmitter2; + +public class EventObjectEmitterObject extends GenericObject implements EmitterObject { + private static final class LoadGenericSoundCallback implements LoadGenericCallback { + private final String filename; + + public LoadGenericSoundCallback(final String filename) { + this.filename = filename; + } + + @Override + public Object call(final InputStream data) { + final FileHandle temp = new FileHandle(this.filename) { + @Override + public InputStream read() { + return data; + } + + ; + }; + if (data != null) { + return Gdx.audio.newSound(temp); + } + else { + System.err.println("Warning: missing sound file: " + this.filename); + return null; + } + } + } + + private static final LoadGenericCallback mappedDataCallback = new LoadGenericCallback() { + + @Override + public Object call(final InputStream data) { + final StringBuilder stringBuilder = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(data, "utf-8"))) { + String line; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); + stringBuilder.append("\n"); + } + } + catch (final UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + return new MappedData(stringBuilder.toString()); + } + }; + + private int geometryEmitterType = -1; + public final String type; + private final String id; + public final long[] keyFrames; + private long globalSequence = -1; + private final long[] defval = { 1 }; + public MdxModel internalModel; + public Texture internalTexture; + public float[][] colors; + public float[] intervalTimes; + public float scale; + public int columns; + public int rows; + public float lifeSpan; + public int blendSrc; + public int blendDst; + public float[][] intervals; + public float distanceCutoff; + private float maxDistance; + public float minDistance; + public float pitch; + public float pitchVariance; + public float volume; + public List decodedBuffers = new ArrayList<>(); + /** + * If this is an SPL/UBR emitter object, ok will be set to true if the tables + * are loaded. + *

+ * This is because, like the other geometry emitters, it is fine to use them + * even if the textures don't load. + *

+ * The particles will simply be black. + */ + private boolean ok = false; + + public EventObjectEmitterObject(final MdxModel model, final MdlxEventObject eventObject, final int index) { + super(model, eventObject, index); + + final ModelViewer viewer = model.viewer; + final String name = eventObject.getName(); + String type = name.substring(0, 3); + final String id = name.substring(4); + + // Same thing + if ("FPT".equals(type)) { + type = "SPL"; + } + + if ("SPL".equals(type)) { + this.geometryEmitterType = GeometryEmitterFuncs.EMITTER_SPLAT; + } + else if ("UBR".equals(type)) { + this.geometryEmitterType = GeometryEmitterFuncs.EMITTER_UBERSPLAT; + } + else if ("SPN".equals(type)) { + this.geometryEmitterType = GeometryEmitterFuncs.EMITTER_SPN; + } + + this.type = type; + this.id = id; + this.keyFrames = eventObject.getKeyFrames(); + + final int globalSequenceId = eventObject.getGlobalSequenceId(); + if (globalSequenceId != -1) { + this.globalSequence = model.getGlobalSequences().get(globalSequenceId); + } + + final List tables = new ArrayList<>(); + final PathSolver pathSolver = model.pathSolver; + final Object solverParams = model.solverParams; + + if ("SPN".equals(type)) { + tables.add(viewer.loadGeneric(pathSolver.solve("Splats\\SpawnData.slk", solverParams).finalSrc, + FetchDataTypeName.SLK, mappedDataCallback)); + } + else if ("SPL".equals(type)) { + tables.add(viewer.loadGeneric(pathSolver.solve("Splats\\SplatData.slk", solverParams).finalSrc, + FetchDataTypeName.SLK, mappedDataCallback)); + } + else if ("UBR".equals(type)) { + tables.add(viewer.loadGeneric(pathSolver.solve("Splats\\UberSplatData.slk", solverParams).finalSrc, + FetchDataTypeName.SLK, mappedDataCallback)); + } + else if ("SND".equals(type)) { + if (!model.reforged) { + tables.add(viewer.loadGeneric(pathSolver.solve("UI\\SoundInfo\\AnimLookups.slk", solverParams).finalSrc, + FetchDataTypeName.SLK, mappedDataCallback)); + } + tables.add(viewer.loadGeneric(pathSolver.solve("UI\\SoundInfo\\AnimSounds.slk", solverParams).finalSrc, + FetchDataTypeName.SLK, mappedDataCallback)); + } + else { + // Units\Critters\BlackStagMale\BlackStagMale.mdx has an event object named + // "Point01". + return; + } + + // TODO I am scrapping some async stuff with promises here from the JS and + // calling load + this.load(tables); + } + + private float getFloat(final MappedDataRow row, final String name) { + final Float x = (Float) row.get(name); + if (x == null) { + return Float.NaN; + } + else { + return x.floatValue(); + } + } + + private int getInt(final MappedDataRow row, final String name) { + return getInt(row, name, Integer.MIN_VALUE); + } + + private int getInt(final MappedDataRow row, final String name, final int defaultValue) { + final Number x = (Number) row.get(name); + if (x == null) { + return defaultValue; + } + else { + return x.intValue(); + } + } + + private void load(final List tables) { + final MappedData firstTable = (MappedData) tables.get(0).data; + if (firstTable == null) { + return; + } + final MappedDataRow row = firstTable.getRow(this.id.trim()); + + if (row != null) { + final MdxModel model = this.model; + final ModelViewer viewer = model.viewer; + final PathSolver pathSolver = model.pathSolver; + + if ("SPN".equals(this.type)) { + this.internalModel = (MdxModel) viewer.load(((String) row.get("Model")).replace(".mdl", ".mdx"), + pathSolver, model.solverParams); + + if (this.internalModel != null) { + // TODO javascript async code removed here +// this.internalModel.whenLoaded((model) => this.ok = model.ok) + this.ok = this.internalModel.ok; + } + } + else if ("SPL".equals(this.type) || "UBR".equals(this.type)) { + final String texturesExt = model.reforged ? ".dds" : ".blp"; + + this.internalTexture = (Texture) viewer.load( + "ReplaceableTextures\\Splats\\" + row.get("file") + texturesExt, pathSolver, + model.solverParams); + + this.scale = getFloat(row, "Scale"); + this.colors = new float[][] { + { getFloat(row, "StartR"), getFloat(row, "StartG"), getFloat(row, "StartB"), + getFloat(row, "StartA") }, + { getFloat(row, "MiddleR"), getFloat(row, "MiddleG"), getFloat(row, "MiddleB"), + getFloat(row, "MiddleA") }, + { getFloat(row, "EndR"), getFloat(row, "EndG"), getFloat(row, "EndB"), + getFloat(row, "EndA") } }; + + if ("SPL".equals(this.type)) { + this.columns = getInt(row, "Columns"); + this.rows = getInt(row, "Rows"); + this.lifeSpan = getFloat(row, "Lifespan") + getFloat(row, "Decay"); + this.intervalTimes = new float[] { getFloat(row, "Lifespan"), getFloat(row, "Decay") }; + this.intervals = new float[][] { + { getFloat(row, "UVLifespanStart"), getFloat(row, "UVLifespanEnd"), + getFloat(row, "LifespanRepeat") }, + { getFloat(row, "UVDecayStart"), getFloat(row, "UVDecayEnd"), + getFloat(row, "DecayRepeat") }, }; + } + else { + this.columns = 1; + this.rows = 1; + this.lifeSpan = getFloat(row, "BirthTime") + getFloat(row, "PauseTime") + getFloat(row, "Decay"); + this.intervalTimes = new float[] { getFloat(row, "BirthTime"), getFloat(row, "PauseTime"), + getFloat(row, "Decay") }; + } + + final int[] blendModes = FilterMode + .emitterFilterMode(MdlxParticleEmitter2.FilterMode.fromId(getInt(row, "BlendMode"))); + + this.blendSrc = blendModes[0]; + this.blendDst = blendModes[1]; + + this.ok = true; + } + else if ("SND".equals(this.type)) { + // Only load sounds if audio is enabled. + // This is mostly to save on bandwidth and loading time, especially when loading + // full maps. + if (viewer.audioEnabled) { + final MappedData animSounds = (MappedData) tables.get(1).data; + + final MappedDataRow animSoundsRow = animSounds.getRow((String) row.get("SoundLabel")); + + if (animSoundsRow != null) { + this.distanceCutoff = getFloat(animSoundsRow, "DistanceCutoff"); + this.maxDistance = getFloat(animSoundsRow, "MaxDistance"); + this.minDistance = getFloat(animSoundsRow, "MinDistance"); + this.pitch = getFloat(animSoundsRow, "Pitch"); + this.pitchVariance = getFloat(animSoundsRow, "PitchVariance"); + this.volume = getFloat(animSoundsRow, "Volume") / 127f; + + final String[] fileNames = ((String) animSoundsRow.get("FileNames")).split(","); + final GenericResource[] resources = new GenericResource[fileNames.length]; + for (int i = 0; i < fileNames.length; i++) { + final String path = ((String) animSoundsRow.get("DirectoryBase")) + fileNames[i]; + try { + final String pathString = pathSolver.solve(path, model.solverParams).finalSrc; + final GenericResource genericResource = viewer.loadGeneric(pathString, + FetchDataTypeName.ARRAY_BUFFER, new LoadGenericSoundCallback(pathString)); + if (genericResource == null) { + System.err.println("Null sound: " + fileNames[i]); + } + resources[i] = genericResource; + } + catch (final Exception exc) { + System.err.println("Failed to load sound: " + path); + exc.printStackTrace(); + } + } + + // TODO JS async removed + for (final GenericResource resource : resources) { + if (resource != null) { + this.decodedBuffers.add((Sound) resource.data); + } + } + this.ok = true; + } + } + } + else { + System.err.println("Unknown event object type: " + this.type + this.id); + } + } + else { + System.err.println("Unknown event object ID: " + this.type + this.id); + } + } + + public int getValue(final long[] out, final MdxComplexInstance instance) { + if (this.globalSequence != -1) { + + return this.getValueAtTime(out, instance.counter % this.globalSequence, 0, this.globalSequence); + } + else if (instance.sequence != -1) { + final long[] interval = this.model.getSequences().get(instance.sequence).getInterval(); + + return this.getValueAtTime(out, instance.frame, interval[0], interval[1]); + } + else { + out[0] = this.defval[0]; + + return -1; + } + } + + public int getValueAtTime(final long[] out, final long frame, final long start, final long end) { + if ((frame >= start) && (frame <= end)) { + for (int i = this.keyFrames.length - 1; i > -1; i--) { + if (this.keyFrames[i] < start) { + out[0] = 0; + + return -1; + } + else if (this.keyFrames[i] <= frame) { + out[0] = 1; + + return i; + } + } + } + + out[0] = 0; + + return -1; + } + + @Override + public boolean ok() { + return this.ok; + } + + @Override + public int getGeometryEmitterType() { + return this.geometryEmitterType; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSnd.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSnd.java new file mode 100644 index 0000000..4afebca --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSnd.java @@ -0,0 +1,60 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.util.List; + +import com.badlogic.gdx.audio.Sound; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.viewer5.AudioBufferSource; +import com.etheller.warsmash.viewer5.AudioContext; +import com.etheller.warsmash.viewer5.AudioPanner; +import com.etheller.warsmash.viewer5.EmittedObject; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.Scene; + +public class EventObjectSnd extends EmittedObject { + public EventObjectSnd(final EventObjectSndEmitter emitter) { + super(emitter); + } + + @Override + protected void bind(final int flags) { + final EventObjectSndEmitter emitter = this.emitter; + final MdxComplexInstance instance = emitter.instance; + final ModelViewer viewer = instance.model.viewer; + final Scene scene = instance.scene; + + // Is audio enabled both viewer-wide and in this scene? + if (viewer.audioEnabled && scene.audioEnabled) { + final EventObjectEmitterObject emitterObject = emitter.emitterObject; + final MdxNode node = instance.nodes[emitterObject.index]; + final AudioContext audioContext = scene.audioContext; + final List decodedBuffers = emitterObject.decodedBuffers; + if (decodedBuffers.isEmpty()) { + return; + } + final AudioPanner panner = audioContext.createPanner(); + final AudioBufferSource source = audioContext.createBufferSource(); + final Vector3 location = node.worldLocation; + + // Panner settings + panner.setPosition(location.x, location.y, location.z); + panner.setDistances(emitterObject.distanceCutoff, emitterObject.minDistance); + panner.connect(audioContext.destination); + + // Source. + source.buffer = decodedBuffers.get((int) (Math.random() * decodedBuffers.size())); + source.connect(panner); + + // Make a sound. + source.start(0, emitterObject.volume, + (emitterObject.pitch + ((float) Math.random() * emitterObject.pitchVariance * 2)) + - emitterObject.pitchVariance, + false); + } + } + + @Override + public void update(final float dt) { + + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSndEmitter.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSndEmitter.java new file mode 100644 index 0000000..6eba361 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSndEmitter.java @@ -0,0 +1,14 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +public class EventObjectSndEmitter extends EventObjectEmitter { + + public EventObjectSndEmitter(final MdxComplexInstance instance, final EventObjectEmitterObject emitterObject) { + super(instance, emitterObject); + } + + @Override + protected EventObjectSnd createObject() { + return new EventObjectSnd(this); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSplEmitter.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSplEmitter.java new file mode 100644 index 0000000..e112023 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSplEmitter.java @@ -0,0 +1,12 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +public class EventObjectSplEmitter extends EventObjectEmitter { + public EventObjectSplEmitter(final MdxComplexInstance instance, final EventObjectEmitterObject emitterObject) { + super(instance, emitterObject); + } + + @Override + protected EventObjectSplUbr createObject() { + return new EventObjectSplUbr(this); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSplUbr.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSplUbr.java new file mode 100644 index 0000000..ac7b8d7 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSplUbr.java @@ -0,0 +1,63 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.viewer5.EmittedObject; + +public class EventObjectSplUbr + extends EmittedObject> { + private static final Vector3 vertexHeap = new Vector3(); + + public final float[] vertices = new float[12]; + + public EventObjectSplUbr(final EventObjectEmitter emitter) { + super(emitter); + } + + @Override + protected void bind(final int flags) { + final EventObjectEmitter emitter = this.emitter; + final MdxComplexInstance instance = emitter.instance; + final EventObjectEmitterObject emitterObject = emitter.emitterObject; + final float[] vertices = this.vertices; + final float scale = emitterObject.scale; + final MdxNode node = instance.nodes[emitterObject.index]; + final Matrix4 worldMatrix = node.worldMatrix; + + this.health = emitterObject.lifeSpan; + + vertexHeap.x = scale; + vertexHeap.y = scale; + vertexHeap.prj(worldMatrix); + vertices[0] = vertexHeap.x; + vertices[1] = vertexHeap.y; + vertices[2] = vertexHeap.z; + + vertexHeap.x = -scale; + vertexHeap.y = scale; + vertexHeap.prj(worldMatrix); + vertices[3] = vertexHeap.x; + vertices[4] = vertexHeap.y; + vertices[5] = vertexHeap.z; + + vertexHeap.x = -scale; + vertexHeap.y = -scale; + vertexHeap.prj(worldMatrix); + vertices[6] = vertexHeap.x; + vertices[7] = vertexHeap.y; + vertices[8] = vertexHeap.z; + + vertexHeap.x = scale; + vertexHeap.y = -scale; + vertexHeap.prj(worldMatrix); + vertices[9] = vertexHeap.x; + vertices[10] = vertexHeap.y; + vertices[11] = vertexHeap.z; + + } + + @Override + public void update(final float dt) { + this.health -= dt; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSpn.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSpn.java new file mode 100644 index 0000000..9c58ef8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSpn.java @@ -0,0 +1,51 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.etheller.warsmash.viewer5.EmittedObject; +import com.etheller.warsmash.viewer5.Scene; + +public class EventObjectSpn extends EmittedObject { + private final MdxComplexInstance internalInstance; + + public EventObjectSpn(final EventObjectSpnEmitter emitter) { + super(emitter); + + final EventObjectEmitterObject emitterObject = emitter.emitterObject; + final MdxModel internalModel = emitterObject.internalModel; + + this.internalInstance = (MdxComplexInstance) internalModel.addInstance(); + } + + @Override + protected void bind(final int flags) { + final EventObjectSpnEmitter emitter = this.emitter; + final MdxComplexInstance instance = emitter.instance; + final Scene scene = instance.scene; + final MdxNode node = instance.nodes[emitter.emitterObject.index]; + final MdxComplexInstance internalInstance = this.internalInstance; + + internalInstance.setSequence(0); + internalInstance.setTransformation(node.worldLocation, node.worldRotation, node.worldScale); + internalInstance.show(); + + scene.addInstance(internalInstance); + + this.health = 1; + } + + @Override + public void update(final float dt) { + final MdxComplexInstance instance = this.internalInstance; + final MdxModel model = (MdxModel) instance.model; + + // Once the sequence finishes, this event object dies + if (model.getSequences().isEmpty()) { + System.err.println("NO SEQ FOR " + model.name); + } + if (instance.frame >= model.getSequences().get(0).getInterval()[1]) { + this.health = 0; + + instance.hide(); + } + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSpnEmitter.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSpnEmitter.java new file mode 100644 index 0000000..786e5ba --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectSpnEmitter.java @@ -0,0 +1,14 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +public class EventObjectSpnEmitter extends EventObjectEmitter { + + public EventObjectSpnEmitter(final MdxComplexInstance instance, final EventObjectEmitterObject emitterObject) { + super(instance, emitterObject); + } + + @Override + protected EventObjectSpn createObject() { + return new EventObjectSpn(this); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectUbrEmitter.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectUbrEmitter.java new file mode 100644 index 0000000..7589e78 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/EventObjectUbrEmitter.java @@ -0,0 +1,12 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +public class EventObjectUbrEmitter extends EventObjectEmitter { + public EventObjectUbrEmitter(final MdxComplexInstance instance, final EventObjectEmitterObject emitterObject) { + super(instance, emitterObject); + } + + @Override + protected EventObjectSplUbr createObject() { + return new EventObjectSplUbr(this); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/FilterMode.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/FilterMode.java new file mode 100644 index 0000000..aa82c90 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/FilterMode.java @@ -0,0 +1,47 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.badlogic.gdx.graphics.GL20; +import com.hiveworkshop.rms.parsers.mdlx.MdlxLayer; +import com.hiveworkshop.rms.parsers.mdlx.MdlxParticleEmitter2; + +public class FilterMode { + private static final int[] ERROR_DEFAULT = new int[] { 0, 0 }; + private static final int[] MODULATE_2X = new int[] { GL20.GL_DST_COLOR, GL20.GL_SRC_COLOR }; + private static final int[] MODULATE = new int[] { GL20.GL_ZERO, GL20.GL_SRC_COLOR }; + private static final int[] ADDITIVE_ALPHA = new int[] { GL20.GL_SRC_ALPHA, GL20.GL_ONE }; + private static final int[] BLEND = new int[] { GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA }; + + public static int[] layerFilterMode(final MdlxLayer.FilterMode filterMode) { + switch (filterMode) { + case BLEND: + return BLEND; // Blend + case ADDITIVE: + return ADDITIVE_ALPHA; // Additive + case ADDALPHA: + return ADDITIVE_ALPHA; // Add alpha + case MODULATE: + return MODULATE; // Modulate + case MODULATE2X: + return MODULATE_2X; // Modulate 2x + default: + return ERROR_DEFAULT; + } + } + + public static int[] emitterFilterMode(final MdlxParticleEmitter2.FilterMode filterMode) { + switch (filterMode) { + case BLEND: + return BLEND; // Blend + case ADDITIVE: + return ADDITIVE_ALPHA; // Add alpha + case MODULATE: + return MODULATE; // Modulate + case MODULATE2X: + return MODULATE_2X; // Modulate 2x + case ALPHAKEY: + return BLEND; // Add alpha + default: + return ERROR_DEFAULT; + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/GenericGroup.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/GenericGroup.java new file mode 100644 index 0000000..f4b27e5 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/GenericGroup.java @@ -0,0 +1,17 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.util.ArrayList; +import java.util.List; + +import com.badlogic.gdx.math.Matrix4; + +public abstract class GenericGroup { + public final List objects; + + public abstract void render(MdxComplexInstance instance, Matrix4 mvp); + + public GenericGroup() { + this.objects = new ArrayList<>(); // TODO IntArrayList + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/GenericIndexed.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/GenericIndexed.java new file mode 100644 index 0000000..f5151a5 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/GenericIndexed.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +public interface GenericIndexed { + public int getIndex(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/GenericObject.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/GenericObject.java new file mode 100644 index 0000000..f71ac28 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/GenericObject.java @@ -0,0 +1,171 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.etheller.warsmash.util.RenderMathUtils; +import com.hiveworkshop.rms.parsers.mdlx.AnimationMap; +import com.hiveworkshop.rms.parsers.mdlx.MdlxGenericObject; + +public class GenericObject extends AnimatedObject implements GenericIndexed { + + public final int index; + public final String name; + public final int objectId; + public final int parentId; + public final float[] pivot; + public final int dontInheritTranslation; + public final int dontInheritRotation; + public final int dontInheritScaling; + public final int billboarded; + public final int billboardedX; + public final int billboardedY; + public final int billboardedZ; + public final int cameraAnchored; + public final int bone; + public final int light; + public final int eventObject; + public final int attachment; + public final int particleEmitter; + public final int collisionShape; + public final int ribbonEmitter; + public final int emitterUsesMdlOrUnshaded; + public final int emitterUsesTgaOrSortPrimitivesFarZ; + public final int lineEmitter; + public final int unfogged; + public final int modelSpace; + public final int xYQuad; + public final boolean anyBillboarding; + public final Variants variants; + public final boolean hasTranslationAnim; + public final boolean hasRotationAnim; + public final boolean hasScaleAnim; + public final boolean hasGenericAnim; + + public GenericObject(final MdxModel model, final MdlxGenericObject object, final int index) { + super(model, object); + + this.index = index; + this.name = object.getName(); + int objectId = object.getObjectId(); + if (objectId == -1) { + objectId = index; + } + this.objectId = objectId; + int parentId = object.getParentId(); + this.pivot = ((this.objectId < model.getPivotPoints().size())) ? model.getPivotPoints().get(this.objectId) + : new float[] { 0, 0, 0 }; + + final int flags = object.getFlags(); + + this.dontInheritTranslation = flags & 0x1; + this.dontInheritRotation = flags & 0x2; + this.dontInheritScaling = flags & 0x4; + this.billboarded = flags & 0x8; + this.billboardedX = flags & 0x10; + this.billboardedY = flags & 0x20; + this.billboardedZ = flags & 0x40; + this.cameraAnchored = flags & 0x80; + this.bone = flags & 0x100; + this.light = flags & 0x200; + this.eventObject = flags & 0x400; + this.attachment = flags & 0x800; + this.particleEmitter = flags & 0x1000; + this.collisionShape = flags & 0x2000; + this.ribbonEmitter = flags & 0x4000; + this.emitterUsesMdlOrUnshaded = flags & 0x8000; + this.emitterUsesTgaOrSortPrimitivesFarZ = flags & 0x10000; + this.lineEmitter = flags & 0x20000; + this.unfogged = flags & 0x40000; + this.modelSpace = flags & 0x80000; + this.xYQuad = flags & 0x100000; + + this.anyBillboarding = (this.billboarded != 0) || (this.billboardedX != 0) || (this.billboardedY != 0) + || (this.billboardedZ != 0); + + if (object.getObjectId() == object.getParentId()) { + parentId = -1; // + } + this.parentId = parentId; + + final Variants variants = new Variants(model.getSequences().size()); + + boolean hasTranslationAnim = false; + boolean hasRotationAnim = false; + boolean hasScaleAnim = false; + + for (int i = 0; i < model.getSequences().size(); i++) { + final boolean translation = this.isTranslationVariant(i); + final boolean rotation = this.isRotationVariant(i); + final boolean scale = this.isScaleVariant(i); + + variants.translation[i] = translation; + variants.rotation[i] = rotation; + variants.scale[i] = scale; + variants.generic[i] = translation || rotation || scale; + + hasTranslationAnim = hasTranslationAnim || translation; + hasRotationAnim = hasRotationAnim || rotation; + hasScaleAnim = hasScaleAnim || scale; + } + + this.variants = variants; + this.hasTranslationAnim = hasTranslationAnim; + this.hasRotationAnim = hasRotationAnim; + this.hasScaleAnim = hasScaleAnim; + this.hasGenericAnim = hasTranslationAnim || hasRotationAnim || hasScaleAnim; + } + + /** + * Many of the generic objects have animated visibilities. This is a generic + * getter to allow the code to be consistent. + */ + public int getVisibility(final float[] out, final int sequence, final int frame, final int counter) { + out[0] = 1; + return -1; + } + + public int getTranslation(final float[] out, final int sequence, final int frame, final int counter) { + return this.getVectorValue(out, AnimationMap.KGTR.getWar3id(), sequence, frame, counter, + RenderMathUtils.FLOAT_VEC3_ZERO); + } + + public int getRotation(final float[] out, final int sequence, final int frame, final int counter) { + return this.getQuatValue(out, AnimationMap.KGRT.getWar3id(), sequence, frame, counter, + RenderMathUtils.FLOAT_QUAT_DEFAULT); + } + + public int getScale(final float[] out, final int sequence, final int frame, final int counter) { + return this.getVectorValue(out, AnimationMap.KGSC.getWar3id(), sequence, frame, counter, + RenderMathUtils.FLOAT_VEC3_ONE); + } + + public boolean isTranslationVariant(final int sequence) { + return this.isVariant(AnimationMap.KGTR.getWar3id(), sequence); + } + + public boolean isRotationVariant(final int sequence) { + return this.isVariant(AnimationMap.KGRT.getWar3id(), sequence); + } + + public boolean isScaleVariant(final int sequence) { + return this.isVariant(AnimationMap.KGSC.getWar3id(), sequence); + } + + public static final class Variants { + boolean[] translation; + boolean[] rotation; + boolean[] scale; + boolean[] generic; + + public Variants(final int sequencesCount) { + this.translation = new boolean[sequencesCount]; + this.rotation = new boolean[sequencesCount]; + this.scale = new boolean[sequencesCount]; + this.generic = new boolean[sequencesCount]; + } + } + + @Override + public int getIndex() { + return this.index; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/GeometryEmitterFuncs.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/GeometryEmitterFuncs.java new file mode 100644 index 0000000..378e5c7 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/GeometryEmitterFuncs.java @@ -0,0 +1,459 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.util.List; + +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.Camera; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.Texture; +import com.etheller.warsmash.viewer5.TextureMapper; +import com.etheller.warsmash.viewer5.gl.ANGLEInstancedArrays; +import com.etheller.warsmash.viewer5.gl.ClientBuffer; +import com.etheller.warsmash.viewer5.handlers.EmitterObject; + +//The total storage that emitted objects can use. +//This is enough to support all of the MDX geometry emitters. +//The memory layout is the same as this C struct: +// +//struct { +// float p0[3] +// float p1[3] +// float p2[3] +// float p3[3] +// float health +// byte color[4] +// byte tail +// byte leftRightTop[3] +//} +// +public class GeometryEmitterFuncs { + public static final int BYTES_PER_OBJECT = 60; + public static final int FLOATS_PER_OBJECT = BYTES_PER_OBJECT >> 2; + + // Offsets into the emitted object structure + public static final int BYTE_OFFSET_P0 = 0; + public static final int BYTE_OFFSET_P1 = 12; + public static final int BYTE_OFFSET_P2 = 24; + public static final int BYTE_OFFSET_P3 = 36; + public static final int BYTE_OFFSET_HEALTH = 48; + public static final int BYTE_OFFSET_COLOR = 52; + public static final int BYTE_OFFSET_TAIL = 56; + public static final int BYTE_OFFSET_LEFT_RIGHT_TOP = 57; + + // Offset aliases + public static final int FLOAT_OFFSET_P0 = BYTE_OFFSET_P0 >> 2; + public static final int FLOAT_OFFSET_P1 = BYTE_OFFSET_P1 >> 2; + public static final int FLOAT_OFFSET_P2 = BYTE_OFFSET_P2 >> 2; + public static final int FLOAT_OFFSET_P3 = BYTE_OFFSET_P3 >> 2; + public static final int FLOAT_OFFSET_HEALTH = BYTE_OFFSET_HEALTH >> 2; + public static final int BYTE_OFFSET_TEAM_COLOR = BYTE_OFFSET_LEFT_RIGHT_TOP; + + // Head or tail. + public static final int HEAD = 0; + public static final int TAIL = 1; + + // Emitter types + public static final int EMITTER_PARTICLE2 = 0; + public static final int EMITTER_RIBBON = 1; + public static final int EMITTER_SPLAT = 2; + public static final int EMITTER_UBERSPLAT = 3; + public static final int EMITTER_SPN = 4; // added by Retera because reasons + + private static final Vector3 locationHeap = new Vector3(); + private static final Vector3 startHeap = new Vector3(); + private static final Vector3 endHeap = new Vector3(); + private static final float[] vectorTemp = new float[3]; + private static final Vector3[] vector3Heap = { new Vector3(), new Vector3(), new Vector3(), new Vector3(), + new Vector3(), new Vector3() }; + + public static void bindParticleEmitter2Buffer(final ParticleEmitter2 emitter, final ClientBuffer buffer) { + final MdxComplexInstance instance = emitter.instance; + final List objects = emitter.objects; + final ByteBuffer byteView = buffer.byteView; + final FloatBuffer floatView = buffer.floatView; + final ParticleEmitter2Object emitterObject = emitter.emitterObject; + final int modelSpace = emitterObject.modelSpace; + final float tailLength = emitterObject.tailLength; + int offset = 0; + + for (int objectIndex = 0; objectIndex < emitter.alive; objectIndex++) { + final Particle2 object = objects.get(objectIndex); + final int byteOffset = offset * BYTES_PER_OBJECT; + final int floatOffset = offset * FLOATS_PER_OBJECT; + final int p0Offset = floatOffset + FLOAT_OFFSET_P0; + Vector3 location = object.location; + final Vector3 scale = object.scale; + final int tail = object.tail; + + if (tail == HEAD) { + // If this is a model space emitter, the location is in local space, so convert + // it to world space. + if (modelSpace != 0) { + location = locationHeap.set(location).prj(emitter.node.worldMatrix); + } + + floatView.put(p0Offset + 0, location.x); + floatView.put(p0Offset + 1, location.y); + floatView.put(p0Offset + 2, location.z); + if (emitterObject.xYQuad != 0) { + final Vector3 velocity = object.velocity; + floatView.put(p0Offset + 3, velocity.x); + floatView.put(p0Offset + 4, velocity.y); + floatView.put(p0Offset + 5, velocity.z); + } + else { + floatView.put(p0Offset + 3, 0); + floatView.put(p0Offset + 4, 0); + } + } + else { + final Vector3 velocity = object.velocity; + final Vector3 start = startHeap; + Vector3 end = location; + + start.x = end.x - (tailLength * velocity.x); + start.y = end.y - (tailLength * velocity.y); + start.z = end.z - (tailLength * velocity.z); + + // If this is a model space emitter, the start and end are in local space, so + // convert them to world space. + if (modelSpace != 0) { + start.prj(emitter.node.worldMatrix); + end = endHeap.set(end).prj(emitter.node.worldMatrix); + } + + floatView.put(p0Offset + 0, start.x); + floatView.put(p0Offset + 1, start.y); + floatView.put(p0Offset + 2, start.z); + floatView.put(p0Offset + 3, end.x); + floatView.put(p0Offset + 4, end.y); + floatView.put(p0Offset + 5, end.z); + } + + floatView.put(p0Offset + 6, scale.x); + floatView.put(p0Offset + 7, scale.y); + floatView.put(p0Offset + 8, scale.z); + + floatView.put(floatOffset + FLOAT_OFFSET_HEALTH, object.health); + + byteView.put(byteOffset + BYTE_OFFSET_TAIL, (byte) tail); + byteView.put(byteOffset + BYTE_OFFSET_TEAM_COLOR, (byte) 0); + + offset += 1; + } + + } + + public static void bindParticleEmitter2Shader(final ParticleEmitter2 emitter, final ShaderProgram shader) { + final MdxComplexInstance instance = emitter.instance; + final Scene scene = instance.scene; + final Camera camera = scene.camera; + final ParticleEmitter2Object emitterObject = emitter.emitterObject; + final MdxModel model = emitterObject.model; + final ModelViewer viewer = model.viewer; + final GL20 gl = viewer.gl; + final float[][] colors = emitterObject.colors; + final float[][] intervals = emitterObject.intervals; + final long replaceableId = emitterObject.replaceableId; + Vector3[] vectors; + Texture texture; + + gl.glBlendFunc(emitterObject.blendSrc, emitterObject.blendDst); + + if ((replaceableId > 0) && (replaceableId < WarsmashConstants.REPLACEABLE_TEXTURE_LIMIT) + && (instance.replaceableTextures[(int) replaceableId] != null)) { + texture = instance.replaceableTextures[(int) replaceableId]; + } + else { + texture = emitterObject.internalTexture; + } + + viewer.webGL.bindTexture(texture, 0); + + // Choose between a default rectangle and a billboarded one + if (emitterObject.xYQuad != 0) { + vectors = camera.vectors; + } + else { + vectors = camera.billboardedVectors; + } + + shader.setUniformf("u_emitter", EMITTER_PARTICLE2); + + shader.setUniformf("u_lifeSpan", emitterObject.lifeSpan); + shader.setUniformf("u_timeMiddle", emitterObject.timeMiddle); + shader.setUniformf("u_columns", emitterObject.columns); + shader.setUniformf("u_rows", emitterObject.rows); + shader.setUniformf("u_teamColored", emitterObject.teamColored); + + shader.setUniform3fv("u_intervals[0]", intervals[0], 0, 3); + shader.setUniform3fv("u_intervals[1]", intervals[1], 0, 3); + shader.setUniform3fv("u_intervals[2]", intervals[2], 0, 3); + shader.setUniform3fv("u_intervals[3]", intervals[3], 0, 3); + + shader.setUniform4fv("u_colors[0]", colors[0], 0, 4); + shader.setUniform4fv("u_colors[1]", colors[1], 0, 4); + shader.setUniform4fv("u_colors[2]", colors[2], 0, 4); + + shader.setUniform3fv("u_scaling", emitterObject.scaling, 0, 3); + + if (emitterObject.head) { + shader.setUniform3fv("u_vertices[0]", asFloatArray(vectors[0]), 0, 3); + shader.setUniform3fv("u_vertices[1]", asFloatArray(vectors[1]), 0, 3); + shader.setUniform3fv("u_vertices[2]", asFloatArray(vectors[2]), 0, 3); + shader.setUniform3fv("u_vertices[3]", asFloatArray(vectors[3]), 0, 3); + } + + if (emitterObject.tail) { + shader.setUniform3fv("u_cameraZ", asFloatArray(camera.billboardedVectors[6]), 0, 3); + } + } + + public static void bindRibbonEmitterBuffer(final RibbonEmitter emitter, final ClientBuffer buffer) { + Ribbon object = emitter.first; + final ByteBuffer byteView = buffer.byteView; + final FloatBuffer floatView = buffer.floatView; + final RibbonEmitterObject emitterObject = emitter.emitterObject; + final long columns = emitterObject.columns; + final int alive = emitter.alive; + final float chainLengthFactor = 1 / (float) (alive - 1); + int offset = 0; + + while (object.next != null) { + final float[] next = object.next.vertices; + final int byteOffset = offset * BYTES_PER_OBJECT; + final int floatOffset = offset * FLOATS_PER_OBJECT; + final int p0Offset = floatOffset + FLOAT_OFFSET_P0; + final int colorOffset = byteOffset + BYTE_OFFSET_COLOR; + final int leftRightTopOffset = byteOffset + BYTE_OFFSET_LEFT_RIGHT_TOP; + final float left = ((object.slot % columns) + (1 - (offset * chainLengthFactor) - chainLengthFactor)) + / columns; + final float top = object.slot / (float) columns; + final float right = left + chainLengthFactor; + final float[] vertices = object.vertices; + final byte[] color = object.color; + + floatView.put(p0Offset + 0, vertices[0]); + floatView.put(p0Offset + 1, vertices[1]); + floatView.put(p0Offset + 2, vertices[2]); + floatView.put(p0Offset + 3, vertices[3]); + floatView.put(p0Offset + 4, vertices[4]); + floatView.put(p0Offset + 5, vertices[5]); + floatView.put(p0Offset + 6, next[3]); + floatView.put(p0Offset + 7, next[4]); + floatView.put(p0Offset + 8, next[5]); + floatView.put(p0Offset + 9, next[0]); + floatView.put(p0Offset + 10, next[1]); + floatView.put(p0Offset + 11, next[2]); + + byteView.put(colorOffset + 0, color[0]); + byteView.put(colorOffset + 1, color[1]); + byteView.put(colorOffset + 2, color[2]); + byteView.put(colorOffset + 3, color[3]); + + byteView.put(leftRightTopOffset + 0, (byte) (left * 255)); + byteView.put(leftRightTopOffset + 1, (byte) (right * 255)); + byteView.put(leftRightTopOffset + 2, (byte) (top * 255)); + + object = object.next; + offset += 1; + + } + } + + public static void bindRibbonEmitterShader(final RibbonEmitter emitter, final ShaderProgram shader) { + final TextureMapper textureMapper = emitter.instance.textureMapper; + final RibbonEmitterObject emitterObject = emitter.emitterObject; + final Layer layer = emitterObject.layer; + final MdxModel model = emitterObject.model; + final GL20 gl = model.viewer.gl; + final Texture texture = model.getTextures().get(layer.textureId); + + layer.bind(shader); + + Texture mappedTexture = textureMapper.get(texture); + if (mappedTexture == null) { + mappedTexture = texture; + } + model.viewer.webGL.bindTexture(mappedTexture, 0); + + shader.setUniformf("u_emitter", EMITTER_RIBBON); + + shader.setUniformf("u_columns", emitterObject.columns); + shader.setUniformf("u_rows", emitterObject.rows); + } + + public static void bindEventObjectEmitterBuffer( + final EventObjectEmitter emitter, final ClientBuffer buffer) { + final List objects = emitter.objects; + final FloatBuffer floatView = buffer.floatView; + int offset = 0; + + for (final EventObjectSplUbr object : objects) { + final int floatOffset = offset * FLOATS_PER_OBJECT; + final int p0Offset = floatOffset + FLOAT_OFFSET_P0; + final float[] vertices = object.vertices; + + floatView.put(p0Offset + 0, vertices[0]); + floatView.put(p0Offset + 1, vertices[1]); + floatView.put(p0Offset + 2, vertices[2]); + floatView.put(p0Offset + 3, vertices[3]); + floatView.put(p0Offset + 4, vertices[4]); + floatView.put(p0Offset + 5, vertices[5]); + floatView.put(p0Offset + 6, vertices[6]); + floatView.put(p0Offset + 7, vertices[7]); + floatView.put(p0Offset + 8, vertices[8]); + floatView.put(p0Offset + 9, vertices[9]); + floatView.put(p0Offset + 10, vertices[10]); + floatView.put(p0Offset + 11, vertices[11]); + + floatView.put(floatOffset + FLOAT_OFFSET_HEALTH, object.health); + + offset += 1; + } + } + + public static void bindEventObjectSplEmitterShader(final EventObjectSplEmitter emitter, + final ShaderProgram shader) { + final TextureMapper textureMapper = emitter.instance.textureMapper; + final EventObjectEmitterObject emitterObject = emitter.emitterObject; + final float[] intervalTimes = emitterObject.intervalTimes; + final float[][] intervals = emitterObject.intervals; + final float[][] colors = emitterObject.colors; + final MdxModel model = emitterObject.model; + final GL20 gl = model.viewer.gl; + final Texture texture = emitterObject.internalTexture; + + gl.glBlendFunc(emitterObject.blendSrc, emitterObject.blendDst); + + Texture finalTexture = textureMapper.get(texture); + if (finalTexture == null) { + finalTexture = texture; + } + model.viewer.webGL.bindTexture(finalTexture, 0); + + shader.setUniformf("u_lifeSpan", emitterObject.lifeSpan); + shader.setUniformf("u_columns", emitterObject.columns); + shader.setUniformf("rows", emitterObject.rows); + + // 3 because the uniform is shared with UBR, which has 3 values. + vectorTemp[0] = intervalTimes[0]; + vectorTemp[1] = intervalTimes[1]; + vectorTemp[2] = 0; + shader.setUniform3fv("u_intervalTimes", vectorTemp, 0, 3); + + shader.setUniform3fv("u_intervals[0]", intervals[0], 0, 3); + shader.setUniform3fv("u_intervals[1]", intervals[1], 0, 3); + + shader.setUniform3fv("u_colors[0]", colors[0], 0, 3); + shader.setUniform3fv("u_colors[1]", colors[1], 0, 3); + shader.setUniform3fv("u_colors[2]", colors[2], 0, 3); + } + + public static void bindEventObjectUbrEmitterShader(final EventObjectUbrEmitter emitter, + final ShaderProgram shader) { + final TextureMapper textureMapper = emitter.instance.textureMapper; + final EventObjectEmitterObject emitterObject = emitter.emitterObject; + final float[] intervalTimes = emitterObject.intervalTimes; + final float[][] colors = emitterObject.colors; + final MdxModel model = emitterObject.model; + final GL20 gl = model.viewer.gl; + final Texture texture = emitterObject.internalTexture; + + gl.glBlendFunc(emitterObject.blendSrc, emitterObject.blendDst); + + Texture finalTexture = textureMapper.get(texture); + if (finalTexture == null) { + finalTexture = texture; + } + model.viewer.webGL.bindTexture(finalTexture, 0); + + shader.setUniformf("u_lifeSpan", emitterObject.lifeSpan); + shader.setUniformf("u_columns", emitterObject.columns); + shader.setUniformf("rows", emitterObject.rows); + + shader.setUniform3fv("u_intervalTimes", intervalTimes, 0, 3); + + shader.setUniform3fv("u_colors[0]", colors[0], 0, 3); + shader.setUniform3fv("u_colors[1]", colors[1], 0, 3); + shader.setUniform3fv("u_colors[2]", colors[2], 0, 3); + } + + public static void renderEmitter(final MdxEmitter emitter, final ShaderProgram shader) { + int alive = emitter.alive; + final EmitterObject emitterObject = emitter.emitterObject; + final int emitterType = emitterObject.getGeometryEmitterType(); + + if (emitterType == EMITTER_RIBBON) { + alive -= 1; + } + else if (emitterType == EMITTER_SPN) { + return; + } + + if (alive > 0) { + final ModelViewer viewer = emitter.instance.model.viewer; + final ANGLEInstancedArrays instancedArrays = viewer.webGL.instancedArrays; + final ClientBuffer buffer = viewer.buffer; + final GL20 gl = viewer.gl; + final int size = alive * BYTES_PER_OBJECT; + + buffer.reserve(size); + + switch (emitterType) { + case EMITTER_PARTICLE2: + bindParticleEmitter2Buffer((ParticleEmitter2) emitter, buffer); + bindParticleEmitter2Shader((ParticleEmitter2) emitter, shader); + break; + case EMITTER_RIBBON: + bindRibbonEmitterBuffer((RibbonEmitter) emitter, buffer); + bindRibbonEmitterShader((RibbonEmitter) emitter, shader); + break; + case EMITTER_SPLAT: + if (true) { + return; + } + bindEventObjectEmitterBuffer((EventObjectSplEmitter) emitter, buffer); + bindEventObjectSplEmitterShader((EventObjectSplEmitter) emitter, shader); + break; + default: + if (true) { + return; + } + bindEventObjectEmitterBuffer((EventObjectUbrEmitter) emitter, buffer); + bindEventObjectUbrEmitterShader((EventObjectUbrEmitter) emitter, shader); + break; + } + + buffer.bindAndUpdate(size); + + shader.setUniformf("u_emitter", emitterType); + + shader.setVertexAttribute("a_p0", 3, GL20.GL_FLOAT, false, BYTES_PER_OBJECT, BYTE_OFFSET_P0); + shader.setVertexAttribute("a_p1", 3, GL20.GL_FLOAT, false, BYTES_PER_OBJECT, BYTE_OFFSET_P1); + shader.setVertexAttribute("a_p2", 3, GL20.GL_FLOAT, false, BYTES_PER_OBJECT, BYTE_OFFSET_P2); + shader.setVertexAttribute("a_p3", 3, GL20.GL_FLOAT, false, BYTES_PER_OBJECT, BYTE_OFFSET_P3); + shader.setVertexAttribute("a_health", 1, GL20.GL_FLOAT, false, BYTES_PER_OBJECT, BYTE_OFFSET_HEALTH); + shader.setVertexAttribute("a_color", 4, GL20.GL_UNSIGNED_BYTE, true, BYTES_PER_OBJECT, BYTE_OFFSET_COLOR); + shader.setVertexAttribute("a_tail", 1, GL20.GL_UNSIGNED_BYTE, false, BYTES_PER_OBJECT, BYTE_OFFSET_TAIL); + shader.setVertexAttribute("a_leftRightTop", 3, GL20.GL_UNSIGNED_BYTE, false, BYTES_PER_OBJECT, + BYTE_OFFSET_LEFT_RIGHT_TOP); + + instancedArrays.glDrawArraysInstancedANGLE(GL20.GL_TRIANGLES, 0, 6, alive); + } + } + + private static final float[] asFloatArray(final Vector3 vec) { + vectorTemp[0] = vec.x; + vectorTemp[1] = vec.y; + vectorTemp[2] = vec.z; + return vectorTemp; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Geoset.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Geoset.java new file mode 100644 index 0000000..9e8bdd8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Geoset.java @@ -0,0 +1,166 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.util.Arrays; + +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; +import com.etheller.warsmash.viewer5.gl.ANGLEInstancedArrays; +import com.hiveworkshop.rms.parsers.mdlx.MdlxGeoset; + +public class Geoset { + public MdxModel model; + public int index; + public int positionOffset; + public int normalOffset; + public int uvOffset; + public int skinOffset; + public int faceOffset; + public int vertices; + public int elements; + public GeosetAnimation geosetAnimation; + public Variants variants; + public boolean hasAlphaAnim; + public boolean hasColorAnim; + public boolean hasObjectAnim; + private final int openGLSkinType; + private final int skinStride; + private final int boneCountOffsetBytes; + public final boolean unselectable; + public final MdlxGeoset mdlxGeoset; + + public Geoset(final MdxModel model, final int index, final int positionOffset, final int normalOffset, + final int uvOffset, final int skinOffset, final int faceOffset, final int vertices, final int elements, + final int openGLSkinType, final int skinStride, final int boneCountOffsetBytes, final boolean unselectable, + final MdlxGeoset mdlxGeoset) { + this.model = model; + this.index = index; + this.positionOffset = positionOffset; + this.normalOffset = normalOffset; + this.uvOffset = uvOffset; + this.skinOffset = skinOffset; + this.faceOffset = faceOffset; + this.vertices = vertices; + this.elements = elements; + this.openGLSkinType = openGLSkinType; + this.skinStride = skinStride; + this.boneCountOffsetBytes = boneCountOffsetBytes; + this.unselectable = unselectable; + this.mdlxGeoset = mdlxGeoset; + + for (final GeosetAnimation geosetAnimation : model.getGeosetAnimations()) { + if (geosetAnimation.geosetId == index) { + this.geosetAnimation = geosetAnimation; + } + } + + final Variants variants = new Variants(model.getSequences().size()); + + final GeosetAnimation geosetAnimation = this.geosetAnimation; + boolean hasAlphaAnim = false; + boolean hasColorAnim = false; + + if (geosetAnimation != null) { + for (int i = 0, l = model.getSequences().size(); i < l; i++) { + final boolean alpha = geosetAnimation.isAlphaVariant(i); + final boolean color = geosetAnimation.isColorVariant(i); + + variants.alpha[i] = alpha; + variants.color[i] = color; + variants.object[i] = alpha || color; + + hasAlphaAnim = hasAlphaAnim || alpha; + hasColorAnim = hasColorAnim || color; + } + } + else { + for (int i = 0, l = model.getSequences().size(); i < l; i++) { + variants.alpha[i] = false; + variants.color[i] = false; + variants.object[i] = false; + } + } + + this.variants = variants; + this.hasAlphaAnim = hasAlphaAnim; + this.hasColorAnim = hasColorAnim; + this.hasObjectAnim = hasAlphaAnim || hasColorAnim; + } + + public int getAlpha(final float[] out, final int sequence, final int frame, final int counter) { + if (this.geosetAnimation != null) { + return this.geosetAnimation.getAlpha(out, sequence, frame, counter); + } + + out[0] = 1; + return -1; + } + + public int getColor(final float[] out, final int sequence, final int frame, final int counter) { + if (this.geosetAnimation != null) { + return this.geosetAnimation.getAlpha(out, sequence, frame, counter); + } + + Arrays.fill(out, 1); + return -1; + } + + public void bind(final ShaderProgram shader, final int coordId) { + // TODO use indices instead of strings for attributes + shader.setVertexAttribute("a_position", 3, GL20.GL_FLOAT, false, 0, this.positionOffset); + shader.setVertexAttribute("a_normal", 3, GL20.GL_FLOAT, false, 0, this.normalOffset); + shader.setVertexAttribute("a_uv", 2, GL20.GL_FLOAT, false, 0, this.uvOffset + (coordId * this.vertices * 8)); + shader.setVertexAttribute("a_bones", 4, this.openGLSkinType, false, this.skinStride, this.skinOffset); + shader.setVertexAttribute("a_boneNumber", 1, this.openGLSkinType, false, this.skinStride, + this.skinOffset + this.boneCountOffsetBytes); + } + + public void bindExtended(final ShaderProgram shader, final int coordId) { + // TODO use indices instead of strings for attributes + shader.setVertexAttribute("a_position", 3, GL20.GL_FLOAT, false, 0, this.positionOffset); + shader.setVertexAttribute("a_normal", 3, GL20.GL_FLOAT, false, 0, this.normalOffset); + shader.setVertexAttribute("a_uv", 2, GL20.GL_FLOAT, false, 0, this.uvOffset + (coordId * this.vertices * 8)); + shader.setVertexAttribute("a_bones", 4, this.openGLSkinType, false, this.skinStride, this.skinOffset); + shader.setVertexAttribute("a_extendedBones", 4, this.openGLSkinType, false, this.skinStride, + this.skinOffset + (this.boneCountOffsetBytes / 2)); + shader.setVertexAttribute("a_boneNumber", 1, this.openGLSkinType, false, this.skinStride, + this.skinOffset + this.boneCountOffsetBytes); + } + + public void render() { + final GL20 gl = this.model.viewer.gl; + + gl.glDrawElements(GL20.GL_TRIANGLES, this.elements, GL20.GL_UNSIGNED_SHORT, this.faceOffset); + } + + public void bindSimple(final ShaderProgram shader) { + shader.setVertexAttribute("a_position", 3, GL20.GL_FLOAT, false, 0, this.positionOffset); + shader.setVertexAttribute("a_uv", 2, GL20.GL_FLOAT, false, 0, this.uvOffset); + } + + public void renderSimple(final int instances) { + final ANGLEInstancedArrays instancedArrays = this.model.viewer.webGL.instancedArrays; + instancedArrays.glDrawElementsInstancedANGLE(GL20.GL_TRIANGLES, this.elements, GL20.GL_UNSIGNED_SHORT, + this.faceOffset, instances); + } + + public void bindHd(final ShaderProgram shader, final int coordId) { + shader.setVertexAttribute("a_position", 3, GL20.GL_FLOAT, false, 0, this.positionOffset); + shader.setVertexAttribute("a_normal", 3, GL20.GL_FLOAT, false, 0, this.positionOffset); + shader.setVertexAttribute("a_uv", 2, GL20.GL_FLOAT, false, 0, this.uvOffset + (coordId * this.vertices * 8)); + shader.setVertexAttribute("a_bones", 4, GL20.GL_UNSIGNED_BYTE, false, 8, this.skinOffset); + shader.setVertexAttribute("a_weights", 4, GL20.GL_UNSIGNED_BYTE, false, 8, this.skinOffset + 4); + } + + private static final class Variants { + private final boolean[] alpha; + private final boolean[] color; + private final boolean[] object; + + public Variants(final int size) { + this.alpha = new boolean[size]; + this.color = new boolean[size]; + this.object = new boolean[size]; + } + + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/GeosetAnimation.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/GeosetAnimation.java new file mode 100644 index 0000000..4e8a256 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/GeosetAnimation.java @@ -0,0 +1,41 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.hiveworkshop.rms.parsers.mdlx.AnimationMap; +import com.hiveworkshop.rms.parsers.mdlx.MdlxGeosetAnimation; + +public class GeosetAnimation extends AnimatedObject { + + private final float alpha; + private final float[] color; + public final int geosetId; + + public GeosetAnimation(final MdxModel model, final MdlxGeosetAnimation geosetAnimation) { + super(model, geosetAnimation); + + final float[] color = geosetAnimation.getColor(); + + this.alpha = geosetAnimation.getAlpha(); + this.color = new float[] { color[2], color[1], color[0] }; // Stored as RGB, but animated colors are stored as + // BGR, so sizzle. + this.geosetId = geosetAnimation.getGeosetId(); + + this.addVariants(AnimationMap.KGAO.getWar3id(), "alpha"); + this.addVariants(AnimationMap.KGAC.getWar3id(), "color"); + } + + public int getAlpha(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KGAO.getWar3id(), sequence, frame, counter, this.alpha); + } + + public int getColor(final float[] out, final int sequence, final int frame, final int counter) { + return this.getVectorValue(out, AnimationMap.KGAC.getWar3id(), sequence, frame, counter, this.color); + } + + public boolean isAlphaVariant(final int sequence) { + return this.isVariant(AnimationMap.KGAO.getWar3id(), sequence); + } + + public boolean isColorVariant(final int sequence) { + return this.isVariant(AnimationMap.KGAC.getWar3id(), sequence); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Helper.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Helper.java new file mode 100644 index 0000000..ba0133b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Helper.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.hiveworkshop.rms.parsers.mdlx.MdlxGenericObject; + +/** + * An MDX helper. + */ +public class Helper extends GenericObject { + public Helper(final MdxModel model, final MdlxGenericObject object, final int index) { + super(model, object, index); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Layer.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Layer.java new file mode 100644 index 0000000..72dfbc8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Layer.java @@ -0,0 +1,157 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; +import com.hiveworkshop.rms.parsers.mdlx.AnimationMap; +import com.hiveworkshop.rms.parsers.mdlx.MdlxLayer; + +/** + * An MDX layer. + */ +public class Layer extends AnimatedObject { + public int index; + public int priorityPlane; + public int filterMode; + public int textureId; + public int coordId; + public float alpha; + public int unshaded; + public int sphereEnvironmentMap; + public int twoSided; + public int unfogged; + public int noDepthTest; + public int noDepthSet; + public boolean depthMaskValue; + public int blendSrc; + public int blendDst; + public boolean blended; + public TextureAnimation textureAnimation; + + public Layer(final MdxModel model, final MdlxLayer layer, final int layerId, final int priorityPlane) { + super(model, layer); + + final MdlxLayer.FilterMode filterMode = layer.getFilterMode(); + final int textureAnimationId = layer.getTextureAnimationId(); + final GL20 gl = model.viewer.gl; + + this.index = layerId; + this.priorityPlane = priorityPlane; + this.filterMode = filterMode.ordinal(); + this.textureId = layer.getTextureId(); + this.coordId = (int) layer.getCoordId(); + this.alpha = layer.getAlpha(); + + final int flags = layer.getFlags(); + + this.unshaded = flags & 0x1; + this.sphereEnvironmentMap = flags & 0x2; + this.twoSided = flags & 0x10; + this.unfogged = flags & 0x20; + this.noDepthTest = flags & 0x40; + this.noDepthSet = flags & 0x80; + + this.depthMaskValue = ((filterMode == MdlxLayer.FilterMode.NONE) + || (filterMode == MdlxLayer.FilterMode.TRANSPARENT)); + + this.blendSrc = 0; + this.blendDst = 0; + this.blended = (filterMode.ordinal() > 1); + + if (this.blended) { + final int[] result = FilterMode.layerFilterMode(filterMode); + this.blendSrc = result[0]; + this.blendDst = result[1]; + } + + if (textureAnimationId != -1) { + final TextureAnimation textureAnimation = model.getTextureAnimations().get(textureAnimationId); + + if (textureAnimation != null) { + this.textureAnimation = textureAnimation; + } + } + + this.addVariants(AnimationMap.KMTA.getWar3id(), "alpha"); + this.addVariants(AnimationMap.KMTF.getWar3id(), "textureId"); + } + + public void bind(final ShaderProgram shader) { + final GL20 gl = this.model.viewer.gl; + + // gl.uniform1f(shader.uniforms.u_unshaded, this.unshaded); + shader.setUniformf("u_filterMode", this.filterMode); + + if (this.blended) { + gl.glEnable(GL20.GL_BLEND); + gl.glBlendFunc(this.blendSrc, this.blendDst); + } + else { + gl.glDisable(GL20.GL_BLEND); + } + + if (this.twoSided != 0) { + gl.glDisable(GL20.GL_CULL_FACE); + } + else { + gl.glEnable(GL20.GL_CULL_FACE); + } + + if (this.noDepthTest != 0) { + gl.glDisable(GL20.GL_DEPTH_TEST); + } + else { + gl.glEnable(GL20.GL_DEPTH_TEST); + } + + if (this.noDepthSet != 0) { + gl.glDepthMask(false); + } + else { + gl.glDepthMask(this.depthMaskValue); + } + } + + public void bindBlended(final ShaderProgram shader) { + final GL20 gl = this.model.viewer.gl; + + // gl.uniform1f(shader.uniforms.u_unshaded, this.unshaded); + shader.setUniformf("u_filterMode", this.filterMode); + + gl.glEnable(GL20.GL_BLEND); + if ((this.blendSrc == 0) && (this.blendDst == 0)) { + gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA); + } + else { + gl.glBlendFunc(this.blendSrc, this.blendDst); + } + + if (this.twoSided != 0) { + gl.glDisable(GL20.GL_CULL_FACE); + } + else { + gl.glEnable(GL20.GL_CULL_FACE); + } + + if (this.noDepthTest != 0) { + gl.glDisable(GL20.GL_DEPTH_TEST); + } + else { + gl.glEnable(GL20.GL_DEPTH_TEST); + } + + if (this.noDepthSet != 0) { + gl.glDepthMask(false); + } + else { + gl.glDepthMask(this.depthMaskValue); + } + } + + public int getAlpha(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KMTA.getWar3id(), sequence, frame, counter, this.alpha); + } + + public int getTextureId(final long[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KMTF.getWar3id(), sequence, frame, counter, this.textureId); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Light.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Light.java new file mode 100644 index 0000000..2aaeef8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Light.java @@ -0,0 +1,82 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.hiveworkshop.rms.parsers.mdlx.AnimationMap; +import com.hiveworkshop.rms.parsers.mdlx.MdlxLight; + +public class Light extends GenericObject { + + private final Type type; + private final float[] attenuation; + private final float[] color; + private final float intensity; + private final float[] ambientColor; + private final float ambientIntensity; + + public Light(final MdxModel model, final MdlxLight light, final int index) { + super(model, light, index); + + switch (light.getType()) { + case OMNIDIRECTIONAL: + this.type = Type.OMNIDIRECTIONAL; + break; + case DIRECTIONAL: + this.type = Type.DIRECTIONAL; + break; + case AMBIENT: + this.type = Type.AMBIENT; + break; + default: + this.type = Type.DIRECTIONAL; + break; + } + this.attenuation = light.getAttenuation(); + this.color = light.getColor(); + this.intensity = light.getIntensity(); + this.ambientColor = light.getAmbientColor(); + this.ambientIntensity = light.getAmbientIntensity(); + } + + public Type getType() { + return this.type; + } + + public int getAttenuationStart(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KLAS.getWar3id(), sequence, frame, counter, this.attenuation[0]); + } + + public int getAttenuationEnd(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KLAE.getWar3id(), sequence, frame, counter, this.attenuation[1]); + } + + public int getIntensity(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KLAI.getWar3id(), sequence, frame, counter, this.intensity); + } + + public int getColor(final float[] out, final int sequence, final int frame, final int counter) { + return this.getVectorValue(out, AnimationMap.KLAC.getWar3id(), sequence, frame, counter, this.color); + } + + public int getAmbientIntensity(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KLBI.getWar3id(), sequence, frame, counter, this.ambientIntensity); + } + + public int getAmbientColor(final float[] out, final int sequence, final int frame, final int counter) { + return this.getVectorValue(out, AnimationMap.KLBC.getWar3id(), sequence, frame, counter, this.ambientColor); + } + + @Override + public int getVisibility(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KLAV.getWar3id(), sequence, frame, counter, 1); + } + + public static enum Type { + // Omnidirectional light used for in-game sun + OMNIDIRECTIONAL, + // Directional light used for torches in the game world, and similar objects + // that "glow" + DIRECTIONAL, + // Directional ambient light used for torches in the game world, and similar + // objects that "glow" + AMBIENT; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/LightInstance.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/LightInstance.java new file mode 100644 index 0000000..4e5a26f --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/LightInstance.java @@ -0,0 +1,111 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.nio.FloatBuffer; + +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.SceneLightInstance; +import com.etheller.warsmash.viewer5.UpdatableObject; + +public class LightInstance implements UpdatableObject, SceneLightInstance { + private static final Matrix4 matrix4Heap = new Matrix4(); + private static final Vector3 vector3Heap = new Vector3(); + private static final float[] vectorHeap = new float[3]; + private static final float[] scalarHeap = new float[1]; + protected final MdxNode node; + protected final Light light; + private boolean visible; + private boolean loadedInScene; + private final MdxComplexInstance instance; + + public LightInstance(final MdxComplexInstance instance, final Light light) { + this.instance = instance; + this.node = instance.nodes[light.index]; + this.light = light; + } + + public void bind(final int offset, final FloatBuffer floatBuffer) { + final int sequence = this.instance.sequence; + final int frame = this.instance.frame; + final int counter = this.instance.counter; + this.light.getAttenuationStart(scalarHeap, sequence, frame, counter); + final float attenuationStart = scalarHeap[0]; + this.light.getAttenuationEnd(scalarHeap, sequence, frame, counter); + final float attenuationEnd = scalarHeap[0]; + this.light.getIntensity(scalarHeap, sequence, frame, counter); + final float intensity = scalarHeap[0]; + this.light.getColor(vectorHeap, sequence, frame, counter); + final float colorRed = vectorHeap[0]; + final float colorGreen = vectorHeap[1]; + final float colorBlue = vectorHeap[2]; + this.light.getAmbientIntensity(scalarHeap, sequence, frame, counter); + final float ambientIntensity = scalarHeap[0]; + this.light.getAmbientColor(vectorHeap, sequence, frame, counter); + final float ambientColorRed = vectorHeap[0]; + final float ambientColorGreen = vectorHeap[1]; + final float ambientColorBlue = vectorHeap[2]; + switch (this.light.getType()) { + case AMBIENT: + case OMNIDIRECTIONAL: + floatBuffer.put(offset, this.node.worldLocation.x); + floatBuffer.put(offset + 1, this.node.worldLocation.y); + floatBuffer.put(offset + 2, this.node.worldLocation.z); + break; + case DIRECTIONAL: + vector3Heap.set(0, 0, 1); + this.node.localRotation.transform(vector3Heap); + vector3Heap.nor(); + floatBuffer.put(offset, vector3Heap.x); + floatBuffer.put(offset + 1, vector3Heap.y); + floatBuffer.put(offset + 2, vector3Heap.z); + break; + } + // I use some padding to make the memory structure of the light be a 4x4 float + // grid, when somebody who actually has experience with this stuff comes along + // to change this to something smart, maybe they'll remove the padding if it's + // not necessary. I'm basing how I implement this on how Ghostwolf did + // BoneTexture + floatBuffer.put(offset + 3, this.instance.worldLocation.z); + floatBuffer.put(offset + 4, this.light.getType().ordinal()); + floatBuffer.put(offset + 5, attenuationStart); + floatBuffer.put(offset + 6, attenuationEnd); + floatBuffer.put(offset + 7, 0); + floatBuffer.put(offset + 8, colorRed); + floatBuffer.put(offset + 9, colorGreen); + floatBuffer.put(offset + 10, colorBlue); + floatBuffer.put(offset + 11, intensity); + floatBuffer.put(offset + 12, ambientColorRed); + floatBuffer.put(offset + 13, ambientColorGreen); + floatBuffer.put(offset + 14, ambientColorBlue); + floatBuffer.put(offset + 15, ambientIntensity); + } + + @Override + public void update(final float dt, final boolean visible) { + } + + public void update(final Scene scene) { + this.light.getVisibility(scalarHeap, this.instance.sequence, this.instance.frame, this.instance.counter); + this.visible = scalarHeap[0] > 0; + updateVisibility(scene, this.visible); + } + + public void remove(final Scene scene) { + updateVisibility(scene, false); + } + + private void updateVisibility(final Scene scene, final boolean visible) { + if (scene != null) { + if (this.loadedInScene != visible) { + if (visible) { + scene.addLight(this); + } + else { + scene.removeLight(this); + } + this.loadedInScene = visible; + } + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Material.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Material.java new file mode 100644 index 0000000..a07ca08 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Material.java @@ -0,0 +1,16 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.util.List; + +public class Material { + public final MdxModel model; + public final String shader; + public final List layers; + + public Material(final MdxModel model, final String shader, final List layers) { + this.model = model; + this.shader = shader; + this.layers = layers; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxComplexInstance.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxComplexInstance.java new file mode 100644 index 0000000..33f6cf9 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxComplexInstance.java @@ -0,0 +1,843 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.List; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Quaternion; +import com.badlogic.gdx.math.Vector3; +import com.badlogic.gdx.math.collision.Ray; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.Bounds; +import com.etheller.warsmash.viewer5.GenericNode; +import com.etheller.warsmash.viewer5.ModelInstance; +import com.etheller.warsmash.viewer5.Node; +import com.etheller.warsmash.viewer5.PathSolver; +import com.etheller.warsmash.viewer5.RenderBatch; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.SkeletalNode; +import com.etheller.warsmash.viewer5.Texture; +import com.etheller.warsmash.viewer5.TextureMapper; +import com.etheller.warsmash.viewer5.UpdatableObject; +import com.etheller.warsmash.viewer5.gl.DataTexture; +import com.etheller.warsmash.viewer5.handlers.w3x.DynamicShadowManager; +import com.hiveworkshop.rms.parsers.mdlx.MdlxGeoset; + +public class MdxComplexInstance extends ModelInstance { + private static final float[] visibilityHeap = new float[1]; + private static final float[] translationHeap = new float[3]; + private static final float[] rotationHeap = new float[4]; + private static final float[] scaleHeap = new float[3]; + private static final float[] colorHeap = new float[3]; + private static final float[] alphaHeap = new float[1]; + private static final long[] textureIdHeap = new long[1]; + + public List lights = new ArrayList<>(); + public List attachments = new ArrayList<>(); + public List particleEmitters = new ArrayList<>(); + public List particleEmitters2 = new ArrayList<>(); + public List ribbonEmitters = new ArrayList<>(); + public List> eventObjectEmitters = new ArrayList<>(); + public MdxNode[] nodes; + public SkeletalNode[] sortedNodes; + public int frame = 0; + public float floatingFrame = 0; + // Global sequences + public int counter = 0; + public int sequence = -1; + public SequenceLoopMode sequenceLoopMode = SequenceLoopMode.NEVER_LOOP; + public boolean sequenceEnded = false; + public float[] vertexColor = { 1, 1, 1, 1 }; + // Particles do not spawn when the sequence is -1, or when the sequence finished + // and it's not repeating + public boolean allowParticleSpawn = false; + // If forced is true, everything will update regardless of variancy. + // Any later non-forced update can then use variancy to skip updating things. + // It is set to true every time the sequence is set with setSequence(). + public boolean forced = true; + public float[][] geosetColors; + public float[] layerAlphas; + public int[] layerTextures; + public float[][] uvAnims; + public Matrix4[] worldMatrices; + public FloatBuffer worldMatricesCopyHeap; + public DataTexture boneTexture; + public Texture[] replaceableTextures = new Texture[WarsmashConstants.REPLACEABLE_TEXTURE_LIMIT]; + private float animationSpeed = 1.0f; + private float blendTime; + private float blendTimeRemaining; + + public MdxComplexInstance(final MdxModel model) { + super(model); + } + + @Override + public void load() { + final MdxModel model = (MdxModel) this.model; + + this.geosetColors = new float[model.geosets.size()][]; + for (int i = 0, l = model.geosets.size(); i < l; i++) { + this.geosetColors[i] = new float[4]; + } + + this.layerAlphas = new float[model.layers.size()]; + this.layerTextures = new int[model.layers.size()]; + this.uvAnims = new float[model.layers.size()][]; + for (int i = 0, l = model.layers.size(); i < l; i++) { + this.layerAlphas[i] = 0; + this.layerTextures[i] = 0; + this.uvAnims[i] = new float[5]; + } + + // Create the needed amount of shared nodes. + final Object[] sharedNodeData = Node.createSkeletalNodes(model.genericObjects.size(), + MdxNodeDescriptor.INSTANCE); + final List nodes = (List) sharedNodeData[0]; + int nodeIndex = 0; + this.nodes = nodes.toArray(new MdxNode[nodes.size()]); + + // A shared typed array for all world matrices of the internal nodes. + this.worldMatrices = ((List) sharedNodeData[1]).toArray(new Matrix4[0]); + this.worldMatricesCopyHeap = ByteBuffer.allocateDirect(16 * this.worldMatrices.length * 4) + .order(ByteOrder.nativeOrder()).asFloatBuffer(); + + // And now initialize all of the nodes and objects + for (final Bone bone : model.bones) { + this.initNode(this.nodes, this.nodes[nodeIndex++], bone); + } + + for (final Light light : model.lights) { + final LightInstance lightInstance = new LightInstance(this, light); + this.lights.add(lightInstance); + this.initNode(this.nodes, this.nodes[nodeIndex++], light, lightInstance); + } + + for (final Helper helper : model.helpers) { + this.initNode(this.nodes, this.nodes[nodeIndex++], helper); + } + + for (final Attachment attachment : model.attachments) { + AttachmentInstance attachmentInstance = null; + + // Attachments may have game models attached to them, such as Undead and + // Nightelf building animations. + if (attachment.internalModel != null) { + attachmentInstance = new AttachmentInstance(this, attachment); + + this.attachments.add(attachmentInstance); + } + + this.initNode(this.nodes, this.nodes[nodeIndex++], attachment, attachmentInstance); + } + + for (final ParticleEmitterObject emitterObject : model.particleEmitters) { + final ParticleEmitter emitter = new ParticleEmitter(this, emitterObject); + + this.particleEmitters.add(emitter); + + this.initNode(this.nodes, this.nodes[nodeIndex++], emitterObject, emitter); + } + + for (final ParticleEmitter2Object emitterObject : model.particleEmitters2) { + final ParticleEmitter2 emitter = new ParticleEmitter2(this, emitterObject); + + this.particleEmitters2.add(emitter); + + this.initNode(this.nodes, this.nodes[nodeIndex++], emitterObject, emitter); + } + + for (final RibbonEmitterObject emitterObject : model.ribbonEmitters) { + final RibbonEmitter emitter = new RibbonEmitter(this, emitterObject); + + this.ribbonEmitters.add(emitter); + + this.initNode(this.nodes, this.nodes[nodeIndex++], emitterObject, emitter); + } + + for (final EventObjectEmitterObject emitterObject : model.eventObjects) { + final String type = emitterObject.type; + EventObjectEmitter emitter; + + if ("SPN".equals(type)) { + emitter = new EventObjectSpnEmitter(this, emitterObject); + } + else if ("SPL".equals(type)) { + emitter = new EventObjectSplEmitter(this, emitterObject); + } + else if ("UBR".equals(type)) { + emitter = new EventObjectUbrEmitter(this, emitterObject); + } + else { + emitter = new EventObjectSndEmitter(this, emitterObject); + } + + this.eventObjectEmitters.add(emitter); + + this.initNode(this.nodes, this.nodes[nodeIndex++], emitterObject, emitter); + } + + for (final CollisionShape collisionShape : model.collisionShapes) { + this.initNode(this.nodes, this.nodes[nodeIndex++], collisionShape); + } + + // Save a sorted array of all of the nodes, such that every child node comes + // after its parent. + // This allows for flat iteration when updating. + final List hierarchy = model.hierarchy; + + this.sortedNodes = new SkeletalNode[nodes.size()]; + for (int i = 0, l = nodes.size(); i < l; i++) { + this.sortedNodes[i] = this.nodes[hierarchy.get(i)]; + } + + // If the sequence was changed before the model was loaded, reset it now that + // the model loaded. + this.setSequence(this.sequence); + + if (model.bones.size() != 0) { + this.boneTexture = new DataTexture(model.viewer.gl, 4, model.bones.size() * 4, 1); + } + } + + /* + * Clear all of the emitted objects that belong to this instance. + */ + @Override + public void clearEmittedObjects() { + for (final ParticleEmitter emitter : this.particleEmitters) { + emitter.clear(); + } + + for (final ParticleEmitter2 emitter : this.particleEmitters2) { + emitter.clear(); + } + + for (final RibbonEmitter emitter : this.ribbonEmitters) { + emitter.clear(); + } + + for (final EventObjectEmitter emitter : this.eventObjectEmitters) { + emitter.clear(); + } + } + + private void initNode(final MdxNode[] nodes, final SkeletalNode node, final GenericObject genericObject) { + initNode(nodes, node, genericObject, null); + } + + /** + * Initialize a skeletal node. + */ + private void initNode(final MdxNode[] nodes, final SkeletalNode node, final GenericObject genericObject, + final UpdatableObject object) { + node.pivot.set(genericObject.pivot); + + if (genericObject.parentId == -1) { + node.parent = this; + } + else { + node.parent = nodes[genericObject.parentId]; + } + + /// TODO: single-axis billboarding + if (genericObject.billboarded != 0) { + node.billboarded = true; + } + else if (genericObject.billboardedX != 0) { + node.billboardedX = true; + } + else if (genericObject.billboardedY != 0) { + node.billboardedY = true; + } + else if (genericObject.billboardedZ != 0) { + node.billboardedZ = true; + } + + if (object != null) { + node.object = object; + } + + } + + /* + * Overriden to hide also attachment models. + */ + @Override + public void hide() { + super.hide(); + + for (final AttachmentInstance attachment : this.attachments) { + attachment.internalInstance.hide(); + } + } + + /** + * Updates all of this instance internal nodes and objects. Nodes that are + * determined to not be visible will not be updated, nor will any of their + * children down the hierarchy. + */ + public void updateNodes(final float dt, final boolean forced) { + if (!this.model.ok) { + return; + } + final int sequence = this.sequence; + final int frame = this.frame; + final int counter = this.counter; + final SkeletalNode[] sortedNodes = this.sortedNodes; + final MdxModel model = (MdxModel) this.model; + final List sortedGenericObjects = model.sortedGenericObjects; + final Scene scene = this.scene; + + // Update the nodes + for (int i = 0, l = sortedNodes.length; i < l; i++) { + final GenericObject genericObject = sortedGenericObjects.get(i); + final SkeletalNode node = sortedNodes[i]; + final GenericNode parent = node.parent; + + genericObject.getVisibility(visibilityHeap, sequence, frame, counter); + + final boolean objectVisible = visibilityHeap[0] > 0; + final boolean nodeVisible = forced || (parent.visible && objectVisible); + + node.visible = nodeVisible; + + // Every node only needs to be updated if this is a forced update, or if both + // the parent node and the generic object corresponding to this node are + // visible. + // Incoming messy code for optimizations! + if (nodeVisible) { + boolean wasDirty = false; + final GenericObject.Variants variants = genericObject.variants; + final Vector3 localLocation = node.localLocation; + final Quaternion localRotation = node.localRotation; + final Vector3 localScale = node.localScale; + + // Only update the local node data if there is a need to + if (forced || variants.generic[sequence]) { + wasDirty = true; + + // Translation + if (forced || variants.translation[sequence]) { + genericObject.getTranslation(translationHeap, sequence, frame, counter); + + localLocation.x = translationHeap[0]; + localLocation.y = translationHeap[1]; + localLocation.z = translationHeap[2]; + } + + // Rotation + if (forced || variants.rotation[sequence]) { + genericObject.getRotation(rotationHeap, sequence, frame, counter); + + localRotation.x = rotationHeap[0]; + localRotation.y = rotationHeap[1]; + localRotation.z = rotationHeap[2]; + localRotation.w = rotationHeap[3]; + } + + // Scale + if (forced || variants.scale[sequence]) { + genericObject.getScale(scaleHeap, sequence, frame, counter); + + localScale.x = scaleHeap[0]; + localScale.y = scaleHeap[1]; + localScale.z = scaleHeap[2]; + } + } + + final boolean wasReallyDirty = forced || wasDirty || parent.wasDirty || genericObject.anyBillboarding; + + node.wasDirty = wasReallyDirty; + + // If this is a forced update, or this node's local data was updated, or the + // parent node was updated, do a full world update. + if (wasReallyDirty) { + node.recalculateTransformation(scene, this.blendTimeRemaining / this.blendTime); + } + + // If there is an instance object associated with this node, and the node is + // visible (which might not be the case for a forced update!), update the + // object. + // This includes attachments and emitters. + final UpdatableObject object = node.object; + + if (object != null) { + object.update(dt, objectVisible); + } + + // Update all of the node's non-skeletal children, which will update their + // children, and so on. + node.updateChildren(dt, scene); + } + } + } + + /** + * Update the batch data. + */ + public void updateBatches(final boolean forced) { + final int sequence = this.sequence; + final int frame = this.frame; + final int counter = this.counter; + final MdxModel model = (MdxModel) this.model; + if (!model.ok) { + return; + } + final List geosets = model.geosets; + final List layers = model.layers; + final float[][] geosetColors = this.geosetColors; + final float[] layerAlphas = this.layerAlphas; + final int[] layerTextures = this.layerTextures; + final float[][] uvAnims = this.uvAnims; + + // Geoset + for (int i = 0, l = geosets.size(); i < l; i++) { + final Geoset geoset = geosets.get(i); + final GeosetAnimation geosetAnimation = geoset.geosetAnimation; + final float[] geosetColor = geosetColors[i]; + + if (geosetAnimation != null) { + // Color + if (forced || (geosetAnimation.variants.get("color")[sequence] != 0)) { + geosetAnimation.getColor(colorHeap, sequence, frame, counter); + + geosetColor[0] = colorHeap[0]; + geosetColor[1] = colorHeap[1]; + geosetColor[2] = colorHeap[2]; + } + + // Alpha + if (forced || (geosetAnimation.variants.get("alpha")[sequence] != 0)) { + geosetAnimation.getAlpha(alphaHeap, sequence, frame, counter); + + geosetColor[3] = alphaHeap[0]; + } + } + else if (forced) { + geosetColor[0] = 1; + geosetColor[1] = 1; + geosetColor[2] = 1; + geosetColor[3] = 1; + } + } + + // Layers + for (int i = 0, l = layers.size(); i < l; i++) { + final Layer layer = layers.get(i); + final TextureAnimation textureAnimation = layer.textureAnimation; + final float[] uvAnim = uvAnims[i]; + + // Alpha + if (forced || (layer.variants.get("alpha")[sequence] != 0)) { + layer.getAlpha(alphaHeap, sequence, frame, counter); + + layerAlphas[i] = alphaHeap[0]; + } + + // Sprite animation + if (forced || (layer.variants.get("textureId")[sequence] != 0)) { + layer.getTextureId(textureIdHeap, sequence, frame, counter); + + layerTextures[i] = (int) textureIdHeap[0]; + } + + if (textureAnimation != null) { + // UV translation animation + if (forced || (textureAnimation.variants.get("translation")[sequence] != 0)) { + textureAnimation.getTranslation(translationHeap, sequence, frame, counter); + + uvAnim[0] = translationHeap[0]; + uvAnim[1] = translationHeap[1]; + } + + // UV rotation animation + if (forced || (textureAnimation.variants.get("rotation")[sequence] != 0)) { + textureAnimation.getRotation(rotationHeap, sequence, frame, counter); + + uvAnim[2] = rotationHeap[2]; + uvAnim[3] = rotationHeap[3]; + } + + // UV scale animation + if (forced || (textureAnimation.variants.get("scale")[sequence] != 0)) { + textureAnimation.getScale(scaleHeap, sequence, frame, counter); + + uvAnim[4] = scaleHeap[0]; + } + } + else if (forced) { + uvAnim[0] = 0; + uvAnim[1] = 0; + uvAnim[2] = 0; + uvAnim[3] = 1; + uvAnim[4] = 1; + } + } + } + + public void updateBoneTexture() { + if (this.boneTexture != null) { + this.worldMatricesCopyHeap.clear(); + for (int i = 0, l = this.worldMatrices.length; i < l; i++) { + final Matrix4 worldMatrix = this.worldMatrices[i]; + this.worldMatricesCopyHeap.put((i * 16) + 0, worldMatrix.val[Matrix4.M00]); + this.worldMatricesCopyHeap.put((i * 16) + 1, worldMatrix.val[Matrix4.M10]); + this.worldMatricesCopyHeap.put((i * 16) + 2, worldMatrix.val[Matrix4.M20]); + this.worldMatricesCopyHeap.put((i * 16) + 3, worldMatrix.val[Matrix4.M30]); + this.worldMatricesCopyHeap.put((i * 16) + 4, worldMatrix.val[Matrix4.M01]); + this.worldMatricesCopyHeap.put((i * 16) + 5, worldMatrix.val[Matrix4.M11]); + this.worldMatricesCopyHeap.put((i * 16) + 6, worldMatrix.val[Matrix4.M21]); + this.worldMatricesCopyHeap.put((i * 16) + 7, worldMatrix.val[Matrix4.M31]); + this.worldMatricesCopyHeap.put((i * 16) + 8, worldMatrix.val[Matrix4.M02]); + this.worldMatricesCopyHeap.put((i * 16) + 9, worldMatrix.val[Matrix4.M12]); + this.worldMatricesCopyHeap.put((i * 16) + 10, worldMatrix.val[Matrix4.M22]); + this.worldMatricesCopyHeap.put((i * 16) + 11, worldMatrix.val[Matrix4.M32]); + this.worldMatricesCopyHeap.put((i * 16) + 12, worldMatrix.val[Matrix4.M03]); + this.worldMatricesCopyHeap.put((i * 16) + 13, worldMatrix.val[Matrix4.M13]); + this.worldMatricesCopyHeap.put((i * 16) + 14, worldMatrix.val[Matrix4.M23]); + this.worldMatricesCopyHeap.put((i * 16) + 15, worldMatrix.val[Matrix4.M33]); + } + this.boneTexture.bindAndUpdate(this.worldMatricesCopyHeap); + } + } + + @Override + public void renderOpaque(final Matrix4 mvp) { + final MdxModel model = (MdxModel) this.model; + + for (final GenericGroup group : model.opaqueGroups) { + group.render(this, mvp); + } + + final int glGetError = Gdx.gl.glGetError(); + if (glGetError != GL20.GL_NO_ERROR) { + throw new IllegalStateException("GL ERROR: " + glGetError + " ON " + model.name + " (Opaque)"); + } + } + + @Override + public void renderTranslucent() { + if (DynamicShadowManager.IS_SHADOW_MAPPING) { + return; + } + final MdxModel model = (MdxModel) this.model; + + for (final GenericGroup group : model.translucentGroups) { + group.render(this, this.scene.camera.viewProjectionMatrix); + + final int glGetError = Gdx.gl.glGetError(); + if (glGetError != GL20.GL_NO_ERROR) { + throw new IllegalStateException("GL ERROR: " + glGetError + " ON " + model.name + " (Translucent)"); + } + } + } + + @Override + public void updateAnimations(final float dt) { + final MdxModel model = (MdxModel) this.model; + final int sequenceId = this.sequence; + + if ((sequenceId != -1) && (model.sequences.size() != 0)) { + final Sequence sequence = model.sequences.get(sequenceId); + final long[] interval = sequence.getInterval(); + final float frameTime = (dt * 1000 * this.animationSpeed); + + final int lastIntegerFrame = this.frame; + this.floatingFrame += frameTime; + this.blendTimeRemaining -= frameTime; + this.frame = (int) this.floatingFrame; + final int integerFrameTime = this.frame - lastIntegerFrame; + this.counter += integerFrameTime; + this.allowParticleSpawn = true; + + final long animEnd = interval[1] - 1; + if (this.floatingFrame >= animEnd) { + if ((this.sequenceLoopMode == SequenceLoopMode.ALWAYS_LOOP) + || ((this.sequenceLoopMode == SequenceLoopMode.MODEL_LOOP) && (sequence.getFlags() == 0))) { + this.floatingFrame = this.frame = (int) interval[0]; // TODO not cast + + this.resetEventEmitters(); + } + else if (this.sequenceLoopMode == SequenceLoopMode.LOOP_TO_NEXT_ANIMATION) { // faux queued animation + // mode + final float framesPast = this.floatingFrame - animEnd; + + final List sequences = model.sequences; + this.sequence = (this.sequence + 1) % sequences.size(); + this.floatingFrame = sequences.get(this.sequence).getInterval()[0] + framesPast; // TODO not cast + this.frame = (int) this.floatingFrame; + this.sequenceEnded = false; + this.resetEventEmitters(); + this.forced = true; + } + else { + this.floatingFrame = this.frame = (int) animEnd; // TODO not cast + this.counter -= integerFrameTime; + this.allowParticleSpawn = false; + } + if (this.sequenceLoopMode == SequenceLoopMode.NEVER_LOOP_AND_HIDE_WHEN_DONE) { + hide(); + } + + this.sequenceEnded = true; + } + else { + this.sequenceEnded = false; + } + } + + final boolean forced = this.forced; + + if (sequenceId == -1) { + if (forced) { + // Update the nodes + this.updateNodes(dt, forced); + + this.updateBoneTexture(); + + // Update the batches + this.updateBatches(forced); + } + } + else { + // let variants = model.variants; + + // if (forced || variants.nodes[sequenceId]) { + // Update the nodes + this.updateNodes(dt, forced); + + this.updateBoneTexture(); + // } + + // if (forced || variants.batches[sequenceId]) { + // Update the batches + this.updateBatches(forced); + // } + } + + this.forced = false; + + } + + @Override + protected void updateLights(final Scene scene) { + for (final LightInstance light : this.lights) { + light.update(scene); + } + } + + @Override + protected void removeLights(final Scene scene2) { + for (final LightInstance light : this.lights) { + light.remove(this.scene); + } + } + + /** + * Set the team color of this instance. + */ + public MdxComplexInstance setTeamColor(final int id) { + this.replaceableTextures[1] = (Texture) this.model.viewer.load( + "ReplaceableTextures\\" + ReplaceableIds.getPathString(1) + ReplaceableIds.getIdString(id) + ".blp", + PathSolver.DEFAULT, null); + this.replaceableTextures[2] = (Texture) this.model.viewer.load( + "ReplaceableTextures\\" + ReplaceableIds.getPathString(2) + ReplaceableIds.getIdString(id) + ".blp", + PathSolver.DEFAULT, null); + return this; + } + + @Override + public void setReplaceableTexture(final int replaceableTextureId, final String replaceableTextureFile) { + this.replaceableTextures[replaceableTextureId] = (Texture) this.model.viewer.load(replaceableTextureFile, + PathSolver.DEFAULT, null); + } + + /** + * Set the vertex color of this instance. + */ + public MdxComplexInstance setVertexColor(final float[] color) { + System.arraycopy(color, 0, this.vertexColor, 0, color.length); + + return this; + } + + /** + * Set the sequence of this instance. + */ + public MdxComplexInstance setSequence(final int id) { + final MdxModel model = (MdxModel) this.model; + + final int lastSequence = this.sequence; + this.sequence = id; + + if (model.ok) { + final List sequences = model.sequences; + + if ((id < 0) || (id > (sequences.size() - 1))) { + this.sequence = -1; + this.frame = 0; + this.floatingFrame = 0; + this.allowParticleSpawn = false; + } + else { + if ((this.blendTime > 0) && (lastSequence != this.sequence)) { + this.blendTimeRemaining = this.blendTime; + for (int i = 0, l = this.sortedNodes.length; i < l; i++) { + final SkeletalNode node = this.sortedNodes[i]; + node.beginBlending(); + } + } + + this.frame = (int) sequences.get(id).getInterval()[0]; // TODO not cast + this.floatingFrame = this.frame; + this.sequenceEnded = false; + } + + this.resetEventEmitters(); + + this.forced = true; + } + + return this; + } + + /** + * Set the seuqnece loop mode. 0 to never loop, 1 to loop based on the model, + * and 2 to always loop. 3 was added by Retera as "hide after done" for gameplay + * spawned effects + */ + public MdxComplexInstance setSequenceLoopMode(final SequenceLoopMode mode) { + this.sequenceLoopMode = mode; + + return this; + } + + /** + * Get an attachment node. + */ + public MdxNode getAttachment(final int id) { + final MdxModel model = (MdxModel) this.model; + final Attachment attachment = model.attachments.get(id); + + if (attachment != null) { + return this.nodes[attachment.index]; + } + + return null; + } + + /** + * Event emitters depend on keyframe index changes to emit, rather than only + * values. To work, they need to check what the last keyframe was, and only if + * it's a different one, do something. When changing sequences, these states + * need to be reset, so they can immediately emit things if needed. + */ + private void resetEventEmitters() { + /// TODO: Update this. Said Ghostwolf. + for (final EventObjectEmitter eventObjectEmitter : this.eventObjectEmitters) { + eventObjectEmitter.reset(); + } + } + + @Override + protected RenderBatch getBatch(final TextureMapper textureMapper2) { + throw new UnsupportedOperationException("NOT API"); + } + + public Bounds getBounds() { + if (this.sequence == -1) { + return this.model.bounds; + } + else { + final Bounds sequenceBounds = ((MdxModel) this.model).sequences.get(this.sequence).getBounds(); + if (sequenceBounds.r == 0) { + return this.model.bounds; + } + else { + return sequenceBounds; + } + } + } + + public void intersectRayBounds(final Ray ray, final Vector3 intersection) { + getBounds().intersectRay(ray, intersection); + } + + /** + * Intersects a world ray with the model's CollisionShapes. Only ever call this + * function on the Gdx thread because it uses static variables to hold state + * while processing. + * + * @param ray + */ + public boolean intersectRayWithCollision(final Ray ray, final Vector3 intersection, final boolean alwaysUseMesh, + final boolean onlyUseMesh) { + final MdxModel mdxModel = (MdxModel) this.model; + final List collisionShapes = mdxModel.collisionShapes; + if (!onlyUseMesh) { + for (final CollisionShape collisionShape : collisionShapes) { + final MdxNode mdxNode = this.nodes[collisionShape.index]; + if (collisionShape.checkIntersect(ray, mdxNode, intersection)) { + return true; + } + } + } + if (collisionShapes.isEmpty() || alwaysUseMesh) { + for (final Geoset geoset : mdxModel.geosets) { + if (!geoset.unselectable) { + geoset.getAlpha(alphaHeap, this.sequence, this.frame, this.counter); + if (alphaHeap[0] > 0) { + final MdlxGeoset mdlxGeoset = geoset.mdlxGeoset; + if (CollisionShape.intersectRayTriangles(ray, this, mdlxGeoset.getVertices(), + mdlxGeoset.getFaces(), 3, intersection)) { + return true; + } + } + } + } + } + return false; + } + + public void setAnimationSpeed(final float speedRatio) { + this.animationSpeed = speedRatio; + for (final AttachmentInstance attachmentInstance : this.attachments) { + if (attachmentInstance.internalInstance != null) { + attachmentInstance.internalInstance.setAnimationSpeed(speedRatio); + } + } + } + + public float getAnimationSpeed() { + return this.animationSpeed; + } + + public void setBlendTime(final float blendTime) { + this.blendTime = blendTime; + } + + public void setFrame(final int frame) { + this.frame = frame; + this.floatingFrame = frame; + } + + public void setFrameByRatio(final float ratioOfAnimationCompleted) { + if (this.sequence != -1) { + final Sequence currentlyPlayingSequence = ((MdxModel) this.model).sequences.get(this.sequence); + this.floatingFrame = currentlyPlayingSequence.getInterval()[0] + + ((currentlyPlayingSequence.getInterval()[1] - currentlyPlayingSequence.getInterval()[0]) + * ratioOfAnimationCompleted); + this.frame = (int) this.floatingFrame; + for (final AttachmentInstance attachmentInstance : this.attachments) { + if (attachmentInstance.internalInstance != null) { + attachmentInstance.internalInstance.setFrameByRatio(ratioOfAnimationCompleted); + } + } + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxEmitter.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxEmitter.java new file mode 100644 index 0000000..f6a0fee --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxEmitter.java @@ -0,0 +1,28 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.etheller.warsmash.viewer5.EmittedObject; +import com.etheller.warsmash.viewer5.Emitter; +import com.etheller.warsmash.viewer5.ModelInstance; +import com.etheller.warsmash.viewer5.handlers.EmitterObject; + +public abstract class MdxEmitter>> + extends Emitter { + + protected final EMITTER_OBJECT emitterObject; + + public MdxEmitter(final MODEL_INSTANCE instance, final EMITTER_OBJECT emitterObject) { + super(instance); + + this.emitterObject = emitterObject; + } + + @Override + public void update(final float dt, final boolean objectVisible) { + if (!objectVisible) { + return; + } + if (this.emitterObject.ok()) { + super.update(dt, objectVisible); + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxHandler.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxHandler.java new file mode 100644 index 0000000..5c94309 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxHandler.java @@ -0,0 +1,67 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.util.ArrayList; + +import com.badlogic.gdx.graphics.glutils.ShaderProgram; +import com.etheller.warsmash.viewer5.HandlerResource; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.handlers.ModelHandler; +import com.etheller.warsmash.viewer5.handlers.ResourceHandlerConstructionParams; +import com.etheller.warsmash.viewer5.handlers.blp.BlpHandler; +import com.etheller.warsmash.viewer5.handlers.blp.DdsHandler; +import com.etheller.warsmash.viewer5.handlers.tga.TgaHandler; + +public class MdxHandler extends ModelHandler { + + public MdxHandler() { + this.extensions = new ArrayList<>(); + this.extensions.add(new String[] { ".mdx", "arrayBuffer" }); + this.extensions.add(new String[] { ".mdl", "text" }); + this.load = true; + } + + @Override + public boolean load(final ModelViewer viewer) { + viewer.addHandler(new BlpHandler()); + viewer.addHandler(new DdsHandler()); + viewer.addHandler(new TgaHandler()); + + Shaders.complex = viewer.webGL.createShaderProgram(MdxShaders.vsComplex, MdxShaders.fsComplex); + Shaders.extended = viewer.webGL.createShaderProgram("#define EXTENDED_BONES\r\n" + MdxShaders.vsComplex, + MdxShaders.fsComplex); + Shaders.complexShadowMap = viewer.webGL.createShaderProgram(MdxShaders.vsComplex, + MdxShaders.fsComplexShadowMap); + Shaders.extendedShadowMap = viewer.webGL.createShaderProgram( + "#define EXTENDED_BONES\r\n" + MdxShaders.vsComplex, MdxShaders.fsComplexShadowMap); + Shaders.particles = viewer.webGL.createShaderProgram(MdxShaders.vsParticles, MdxShaders.fsParticles); + // Shaders.simple = viewer.webGL.createShaderProgram(MdxShaders.vsSimple, + // MdxShaders.fsSimple); +// Shaders.hd = viewer.webGL.createShaderProgram(MdxShaders.vsHd, MdxShaders.fsHd); + // TODO HD reforged + + // If a shader failed to compile, don't allow the handler to be registered, and + // send an error instead. + return Shaders.complex.isCompiled() && Shaders.extended.isCompiled() && Shaders.particles.isCompiled() + /* && Shaders.simple.isCompiled() && Shaders.hd.isCompiled() */; + } + + @Override + public HandlerResource construct(final ResourceHandlerConstructionParams params) { + return new MdxModel((MdxHandler) params.getHandler(), params.getViewer(), params.getExtension(), + params.getPathSolver(), params.getFetchUrl()); + } + + public static final class Shaders { + private Shaders() { + + } + + public static ShaderProgram complex; + public static ShaderProgram complexShadowMap; + public static ShaderProgram extended; + public static ShaderProgram extendedShadowMap; + public static ShaderProgram simple; + public static ShaderProgram particles; + public static ShaderProgram hd; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxModel.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxModel.java new file mode 100644 index 0000000..953aa6b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxModel.java @@ -0,0 +1,376 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.compress.utils.IOUtils; + +import com.badlogic.gdx.graphics.GL20; +import com.etheller.warsmash.viewer5.ModelInstance; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.PathSolver; +import com.etheller.warsmash.viewer5.Texture; +import com.hiveworkshop.rms.parsers.mdlx.MdlxAttachment; +import com.hiveworkshop.rms.parsers.mdlx.MdlxBone; +import com.hiveworkshop.rms.parsers.mdlx.MdlxCamera; +import com.hiveworkshop.rms.parsers.mdlx.MdlxCollisionShape; +import com.hiveworkshop.rms.parsers.mdlx.MdlxEventObject; +import com.hiveworkshop.rms.parsers.mdlx.MdlxExtent; +import com.hiveworkshop.rms.parsers.mdlx.MdlxGeosetAnimation; +import com.hiveworkshop.rms.parsers.mdlx.MdlxHelper; +import com.hiveworkshop.rms.parsers.mdlx.MdlxLayer; +import com.hiveworkshop.rms.parsers.mdlx.MdlxLight; +import com.hiveworkshop.rms.parsers.mdlx.MdlxMaterial; +import com.hiveworkshop.rms.parsers.mdlx.MdlxModel; +import com.hiveworkshop.rms.parsers.mdlx.MdlxParticleEmitter; +import com.hiveworkshop.rms.parsers.mdlx.MdlxParticleEmitter2; +import com.hiveworkshop.rms.parsers.mdlx.MdlxRibbonEmitter; +import com.hiveworkshop.rms.parsers.mdlx.MdlxSequence; +import com.hiveworkshop.rms.parsers.mdlx.MdlxTexture; +import com.hiveworkshop.rms.parsers.mdlx.MdlxTexture.WrapMode; +import com.hiveworkshop.rms.parsers.mdlx.MdlxTextureAnimation; + +public class MdxModel extends com.etheller.warsmash.viewer5.Model { + public boolean reforged = false; + public boolean hd = false; + public SolverParams solverParams = new SolverParams(); + public String name = ""; + public List sequences = new ArrayList<>(); + public List globalSequences = new ArrayList<>(); + public List materials = new ArrayList<>(); + public List layers = new ArrayList<>(); + public List replaceables = new ArrayList<>(); + public List textures = new ArrayList<>(); + public List textureAnimations = new ArrayList<>(); + public List geosets = new ArrayList<>(); + public List geosetAnimations = new ArrayList<>(); + public List bones = new ArrayList<>(); + public List lights = new ArrayList<>(); + public List helpers = new ArrayList<>(); + public List attachments = new ArrayList<>(); + public List pivotPoints = new ArrayList<>(); + public List particleEmitters = new ArrayList<>(); + public List particleEmitters2 = new ArrayList<>(); + public List ribbonEmitters = new ArrayList<>(); + public List cameras = new ArrayList<>(); + public List eventObjects = new ArrayList<>(); + public List collisionShapes = new ArrayList<>(); + public boolean hasLayerAnims = false; + public boolean hasGeosetAnims = false; + public List batches = new ArrayList<>(); + public List genericObjects = new ArrayList<>(); + public List sortedGenericObjects = new ArrayList<>(); + public List hierarchy = new ArrayList<>(); + public List opaqueGroups = new ArrayList<>(); + public List translucentGroups = new ArrayList<>(); + public List simpleGroups = new ArrayList<>(); + public int arrayBuffer; + public int elementBuffer; + + public MdxModel(final MdxHandler handler, final ModelViewer viewer, final String extension, + final PathSolver pathSolver, final String fetchUrl) { + super(handler, viewer, extension, pathSolver, fetchUrl); + } + + @Override + public ModelInstance createInstance(final int type) { + if (type == 1) { + return new MdxSimpleInstance(this); + } + else { + return new MdxComplexInstance(this); + } + } + + public void load(final Object bufferOrParser) throws IOException { + MdlxModel parser; + + if (bufferOrParser instanceof MdlxModel) { + parser = (MdlxModel) bufferOrParser; + } + else { + System.err.println("Wasting memory with conversion from InputStream to buffer in MdxModel"); + parser = new MdlxModel(ByteBuffer.wrap(IOUtils.toByteArray((InputStream) bufferOrParser))); + } + + final ModelViewer viewer = this.viewer; + final PathSolver pathSolver = this.pathSolver; + final SolverParams solverParams = this.solverParams; + final boolean reforged = parser.getVersion() > 800; + final String texturesExt = reforged ? ".dds" : ".blp"; + + this.reforged = reforged; + this.name = parser.getName(); + + // Initialize the bounds. + final MdlxExtent extent = parser.getExtent(); + final float[] min = extent.getMin(); + final float[] max = extent.getMax(); + for (int i = 0; i < 3; i++) { + if (min[i] > max[i]) { + min[i] = max[i] = 0; + } + } + this.bounds.fromExtents(min, max, extent.getBoundsRadius()); + + // Sequences + for (final MdlxSequence sequence : parser.getSequences()) { + this.sequences.add(new Sequence(sequence)); + } + + // Global sequences + this.globalSequences.addAll(parser.getGlobalSequences()); + + // Texture animations + for (final MdlxTextureAnimation textureAnimation : parser.getTextureAnimations()) { + this.textureAnimations.add(new TextureAnimation(this, textureAnimation)); + } + + // Materials + int layerId = 0; + for (final MdlxMaterial material : parser.getMaterials()) { + final List layers = new ArrayList<>(); + + for (final MdlxLayer layer : material.getLayers()) { + final Layer vLayer = new Layer(this, layer, layerId++, material.getPriorityPlane()); + + layers.add(vLayer); + + this.layers.add(vLayer); + } + + this.materials.add(new Material(this, "" /* material.shader */, layers)); + + if (false /* !"".equals(material.shader) */) { + this.hd = true; + } + } + + if (reforged) { + solverParams.reforged = true; + } + + if (this.hd) { + solverParams.hd = true; + } + + final GL20 gl = viewer.gl; + + // Textures. + for (final MdlxTexture texture : parser.getTextures()) { + String path = texture.getPath(); + final int replaceableId = texture.getReplaceableId(); + final WrapMode wrapMode = texture.getWrapMode(); + + if (replaceableId != 0) { + // TODO This uses dumb, stupid, terrible, no-good hardcoded replaceable IDs + // instead of the real system, because currently MdxSimpleInstance is not + // supporting it correctly. + final String idString = ((replaceableId == 1) || (replaceableId == 2)) ? ReplaceableIds.getIdString(0) + : ""; + path = "ReplaceableTextures\\" + ReplaceableIds.getPathString(replaceableId) + idString + ".blp"; + } + + if (reforged && !path.endsWith(".dds")) { + path = path.substring(0, path.length() - 4) + ".dds"; + } + else if ("".equals(path)) { + path = "Textures\\white.blp"; + } + + final Texture viewerTexture = (Texture) viewer.load(path, pathSolver, solverParams); + + // When the texture will load, it will apply its wrap modes. + if (wrapMode.isWrapWidth()) { + viewerTexture.setWrapS(true); + } + + if (wrapMode.isWrapHeight()) { + viewerTexture.setWrapT(true); + } + + this.replaceables.add(replaceableId); + this.textures.add(viewerTexture); + } + + // Geoset animations + for (final MdlxGeosetAnimation geosetAnimation : parser.getGeosetAnimations()) { + this.geosetAnimations.add(new GeosetAnimation(this, geosetAnimation)); + } + + // Geosets + SetupGeosets.setupGeosets(this, parser.getGeosets(), parser.getBones().size() >= 256); + + this.pivotPoints = parser.getPivotPoints(); + + // Tracks the IDs of all generic objects + int objectId = 0; + + // Bones + for (final MdlxBone bone : parser.getBones()) { + this.bones.add(new Bone(this, bone, objectId++)); + } + + // Lights + for (final MdlxLight light : parser.getLights()) { + this.lights.add(new Light(this, light, objectId++)); + } + + // Helpers + for (final MdlxHelper helper : parser.getHelpers()) { + this.helpers.add(new Helper(this, helper, objectId++)); + } + + // Attachments + for (final MdlxAttachment attachment : parser.getAttachments()) { + this.attachments.add(new Attachment(this, attachment, objectId++)); + } + + // Particle Emitters + for (final MdlxParticleEmitter particleEmitter : parser.getParticleEmitters()) { + this.particleEmitters.add(new ParticleEmitterObject(this, particleEmitter, objectId++)); + } + + // Particle Emitters 2 + for (final MdlxParticleEmitter2 particleEmitter2 : parser.getParticleEmitters2()) { + this.particleEmitters2.add(new ParticleEmitter2Object(this, particleEmitter2, objectId++)); + } + + // Ribbon emitters + for (final MdlxRibbonEmitter ribbonEmitter : parser.getRibbonEmitters()) { + this.ribbonEmitters.add(new RibbonEmitterObject(this, ribbonEmitter, objectId++)); + } + + // Camera + for (final MdlxCamera camera : parser.getCameras()) { + this.cameras.add(new Camera(this, camera)); + } + + // Event objects + for (final MdlxEventObject eventObject : parser.getEventObjects()) { + this.eventObjects.add(new EventObjectEmitterObject(this, eventObject, objectId++)); + } + + // Collision shapes + for (final MdlxCollisionShape collisionShape : parser.getCollisionShapes()) { + this.collisionShapes.add(new CollisionShape(this, collisionShape, objectId++)); + } + + // One array for all generic objects. + this.genericObjects.addAll(this.bones); + this.genericObjects.addAll(this.lights); + this.genericObjects.addAll(this.helpers); + this.genericObjects.addAll(this.attachments); + this.genericObjects.addAll(this.particleEmitters); + this.genericObjects.addAll(this.particleEmitters2); + this.genericObjects.addAll(this.ribbonEmitters); + this.genericObjects.addAll(this.eventObjects); + this.genericObjects.addAll(this.collisionShapes); + + // Render groups. + SetupGroups.setupGroups(this); + + // SimpleInstance render group. + SetupSimpleGroups.setupSimpleGroups(this); + + // Creates the sorted indices array of the generic objects + try { + this.setupHierarchy(-1); + } + catch (final StackOverflowError e) { + System.out.println("bah"); + } + + // Keep a sorted array. + for (int i = 0, l = this.genericObjects.size(); i < l; i++) { + this.sortedGenericObjects.add(this.genericObjects.get(this.hierarchy.get(i))); + } + + } + + private void setupHierarchy(final int parent) { + for (int i = 0, l = this.genericObjects.size(); i < l; i++) { + final GenericObject object = this.genericObjects.get(i); + + if (object.parentId == parent) { + this.hierarchy.add(i); + + this.setupHierarchy(object.objectId); + } + } + } + + @Override + protected void lateLoad() { + } + + @Override + protected void load(final InputStream src, final Object options) { + try { + this.load(src); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + + @Override + protected void error(final Exception e) { + e.printStackTrace(); + } + + // TODO typing + public List getGlobalSequences() { + return this.globalSequences; + } + + public List getSequences() { + return this.sequences; + } + + public List getPivotPoints() { + return this.pivotPoints; + } + + public List getGeosetAnimations() { + return this.geosetAnimations; + } + + public List getTextures() { + return this.textures; + } + + public List getMaterials() { + return this.materials; + } + + public List getTextureAnimations() { + return this.textureAnimations; + } + + public List getGeosets() { + return this.geosets; + } + + private static final class SolverParams { + public boolean reforged; + public boolean hd; + + } + + public List getCameras() { + return this.cameras; + } + + public List getEventObjects() { + return this.eventObjects; + } + + public List getBones() { + return this.bones; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxNode.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxNode.java new file mode 100644 index 0000000..cafc3a2 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxNode.java @@ -0,0 +1,22 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.badlogic.gdx.math.Quaternion; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.SkeletalNode; + +public class MdxNode extends SkeletalNode { + private static final Quaternion HALF_PI_X = new Quaternion().setFromAxisRad(1, 0, 0, (float) (-Math.PI / 2)); + private static final Quaternion HALF_PI_Y = new Quaternion().setFromAxisRad(0, 1, 0, (float) (-Math.PI / 2)); + + @Override + protected void convertBasis(final Quaternion computedRotation) { + computedRotation.mul(HALF_PI_Y); + computedRotation.mul(HALF_PI_X); + } + + @Override + protected void update(final float dt, final Scene scene) { + + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxNodeDescriptor.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxNodeDescriptor.java new file mode 100644 index 0000000..72c3665 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxNodeDescriptor.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.etheller.warsmash.util.Descriptor; + +public class MdxNodeDescriptor implements Descriptor { + public static final MdxNodeDescriptor INSTANCE = new MdxNodeDescriptor(); + + @Override + public MdxNode create() { + return new MdxNode(); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxRenderBatch.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxRenderBatch.java new file mode 100644 index 0000000..91f6586 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxRenderBatch.java @@ -0,0 +1,135 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.nio.FloatBuffer; +import java.util.List; + +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; +import com.badlogic.gdx.math.Matrix4; +import com.etheller.warsmash.viewer5.Model; +import com.etheller.warsmash.viewer5.ModelInstance; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.RenderBatch; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.Texture; +import com.etheller.warsmash.viewer5.TextureMapper; +import com.etheller.warsmash.viewer5.gl.ANGLEInstancedArrays; +import com.etheller.warsmash.viewer5.gl.ClientBuffer; +import com.etheller.warsmash.viewer5.gl.WebGL; +import com.etheller.warsmash.viewer5.handlers.w3x.DynamicShadowManager; + +public class MdxRenderBatch extends RenderBatch { + private static final Matrix4 transposeHeap = new Matrix4(); + + public MdxRenderBatch(final Scene scene, final Model model, final TextureMapper textureMapper) { + super(scene, model, textureMapper); + } + + private void bindAndUpdateBuffer(final ClientBuffer buffer) { + final int count = this.count; + final List instances = this.instances; + + // Ensure there is enough memory for all of the instances data. + buffer.reserve(count * 48); + + final FloatBuffer floatView = buffer.floatView; + + // "Copy" the instances into the buffer + for (int i = 0; i < count; i++) { + final ModelInstance instance = instances.get(i); + final Matrix4 worldMatrix = instance.worldMatrix; + final int offset = i * 12; + + floatView.put(offset + 0, worldMatrix.val[Matrix4.M00]); + floatView.put(offset + 1, worldMatrix.val[Matrix4.M10]); + floatView.put(offset + 2, worldMatrix.val[Matrix4.M20]); + floatView.put(offset + 3, worldMatrix.val[Matrix4.M01]); + floatView.put(offset + 4, worldMatrix.val[Matrix4.M11]); + floatView.put(offset + 5, worldMatrix.val[Matrix4.M21]); + floatView.put(offset + 6, worldMatrix.val[Matrix4.M02]); + floatView.put(offset + 7, worldMatrix.val[Matrix4.M12]); + floatView.put(offset + 8, worldMatrix.val[Matrix4.M22]); + floatView.put(offset + 9, worldMatrix.val[Matrix4.M03]); + floatView.put(offset + 10, worldMatrix.val[Matrix4.M13]); + floatView.put(offset + 11, worldMatrix.val[Matrix4.M23]); + } + + // Update the buffer. + buffer.bindAndUpdate(count * 48); + } + + @Override + public void render() { + if (DynamicShadowManager.IS_SHADOW_MAPPING) { + return; + } + final int count = this.count; + + if (count != 0) { + final MdxModel model = (MdxModel) this.model; + final List batches = model.batches; + final List textures = model.textures; + final ModelViewer viewer = model.viewer; + final GL20 gl = viewer.gl; + final WebGL webGL = viewer.webGL; + final ANGLEInstancedArrays instancedArrays = webGL.instancedArrays; + final ShaderProgram shader = MdxHandler.Shaders.simple; + final int m0 = shader.getAttributeLocation("a_m0"); + final int m1 = shader.getAttributeLocation("a_m1"); + final int m2 = shader.getAttributeLocation("a_m2"); + final int m3 = shader.getAttributeLocation("a_m3"); + final ClientBuffer buffer = viewer.buffer; + final TextureMapper textureMapper = this.textureMapper; + + webGL.useShaderProgram(shader); + + this.bindAndUpdateBuffer(buffer); + + shader.setVertexAttribute(m0, 3, GL20.GL_FLOAT, false, 48, 0); + shader.setVertexAttribute(m1, 3, GL20.GL_FLOAT, false, 48, 12); + shader.setVertexAttribute(m2, 3, GL20.GL_FLOAT, false, 48, 24); + shader.setVertexAttribute(m3, 3, GL20.GL_FLOAT, false, 48, 36); + + transposeHeap.set(this.scene.camera.viewProjectionMatrix); + transposeHeap.tra(); + shader.setUniformMatrix4fv("u_VP", this.scene.camera.viewProjectionMatrix.val, 0, + this.scene.camera.viewProjectionMatrix.val.length); + + gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, model.arrayBuffer); + gl.glBindBuffer(GL20.GL_ELEMENT_ARRAY_BUFFER, model.elementBuffer); + + instancedArrays.glVertexAttribDivisorANGLE(m0, 1); + instancedArrays.glVertexAttribDivisorANGLE(m1, 1); + instancedArrays.glVertexAttribDivisorANGLE(m2, 1); + instancedArrays.glVertexAttribDivisorANGLE(m3, 1); + + for (final GenericGroup group : model.simpleGroups) { + for (final Integer object : group.objects) { + final Batch batch = batches.get(object); + final Geoset geoset = batch.geoset; + final Layer layer = batch.layer; + final Texture texture = textures.get(layer.textureId); + + shader.setUniformi("u_texture", 0); + + Texture mappedTexture = textureMapper.get(texture); + if (mappedTexture == null) { + mappedTexture = texture; + } + viewer.webGL.bindTexture(mappedTexture, 0); + + layer.bind(shader); + + geoset.bindSimple(shader); + geoset.renderSimple(count); + } + } + + instancedArrays.glVertexAttribDivisorANGLE(m3, 0); + instancedArrays.glVertexAttribDivisorANGLE(m2, 0); + instancedArrays.glVertexAttribDivisorANGLE(m1, 0); + instancedArrays.glVertexAttribDivisorANGLE(m0, 0); + } + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxShaders.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxShaders.java new file mode 100644 index 0000000..7e7cb8c --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxShaders.java @@ -0,0 +1,472 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.etheller.warsmash.viewer5.Shaders; + +public class MdxShaders { + public static final String vsHd = Shaders.boneTexture + "\r\n" + // + " uniform mat4 u_mvp;\r\n" + // + " uniform float u_layerAlpha;\r\n" + // + " attribute vec3 a_position;\r\n" + // + " attribute vec3 a_normal;\r\n" + // + " attribute vec2 a_uv;\r\n" + // + " attribute vec4 a_bones;\r\n" + // + " attribute vec4 a_weights;\r\n" + // + " varying vec3 v_normal;\r\n" + // + " varying vec2 v_uv;\r\n" + // + " varying float v_layerAlpha;\r\n" + // + " void transform(inout vec3 position, inout vec3 normal) {\r\n" + // + " mat4 bone;\r\n" + // + " bone += fetchMatrix(a_bones[0], 0.0) * a_weights[0];\r\n" + // + " bone += fetchMatrix(a_bones[1], 0.0) * a_weights[1];\r\n" + // + " bone += fetchMatrix(a_bones[2], 0.0) * a_weights[2];\r\n" + // + " bone += fetchMatrix(a_bones[3], 0.0) * a_weights[3];\r\n" + // + " position = vec3(bone * vec4(position, 1.0));\r\n" + // + " normal = mat3(bone) * normal;\r\n" + // + " }\r\n" + // + " void main() {\r\n" + // + " vec3 position = a_position;\r\n" + // + " vec3 normal = a_normal;\r\n" + // + " transform(position, normal);\r\n" + // + " v_normal = normal;\r\n" + // + " v_uv = a_uv;\r\n" + // + " v_layerAlpha = u_layerAlpha;\r\n" + // + " gl_Position = u_mvp * vec4(position, 1.0);\r\n" + // + " }"; + + public static final String fsHd = "\r\n" + // + " uniform sampler2D u_diffuseMap;\r\n" + // + " uniform sampler2D u_ormMap;\r\n" + // + " uniform sampler2D u_teamColorMap;\r\n" + // + " uniform float u_filterMode;\r\n" + // + " varying vec3 v_normal;\r\n" + // + " varying vec2 v_uv;\r\n" + // + " varying float v_layerAlpha;\r\n" + // + " void main() {\r\n" + // + " vec4 texel = texture2D(u_diffuseMap, v_uv);\r\n" + // + " vec4 color = vec4(texel.rgb, texel.a * v_layerAlpha);\r\n" + // + " vec4 orma = texture2D(u_ormMap, v_uv);\r\n" + // + " if (orma.a > 0.1) {\r\n" + // + " color *= texture2D(u_teamColorMap, v_uv) * orma.a;\r\n" + // + " }\r\n" + // + " // 1bit Alpha\r\n" + // + " if (u_filterMode == 1.0 && color.a < 0.75) {\r\n" + // + " discard;\r\n" + // + " }\r\n" + // + " gl_FragColor = color;\r\n" + // + " }"; + + public static final String vsSimple = "\r\n" + // + " uniform mat4 u_VP;\r\n" + // + " attribute vec3 a_m0;\r\n" + // + " attribute vec3 a_m1;\r\n" + // + " attribute vec3 a_m2;\r\n" + // + " attribute vec3 a_m3;\r\n" + // + " attribute vec3 a_position;\r\n" + // + " attribute vec2 a_uv;\r\n" + // + " varying vec2 v_uv;\r\n" + // + " void main() {\r\n" + // + " v_uv = a_uv;\r\n" + // + " gl_Position = u_VP * mat4(a_m0, 0.0, a_m1, 0.0, a_m2, 0.0, a_m3, 1.0) * vec4(a_position, 1.0);\r\n" + // + " }\r\n"; + + public static final String fsSimple = "\r\n" + // + " precision mediump float;\r\n" + // + " uniform sampler2D u_texture;\r\n" + // + " uniform float u_filterMode;\r\n" + // + " varying vec2 v_uv;\r\n" + // + " void main() {\r\n" + // + " vec4 color = texture2D(u_texture, v_uv);\r\n" + // + " // 1bit Alpha\r\n" + // + " if (u_filterMode == 1.0 && color.a < 0.75) {\r\n" + // + " discard;\r\n" + // + " }\r\n" + // + " gl_FragColor = color;\r\n" + // + " }\r\n"; + + public static final String vsComplex = "\r\n" + // + "\r\n" + // + " uniform mat4 u_mvp;\r\n" + // + " uniform vec4 u_vertexColor;\r\n" + // + " uniform vec4 u_geosetColor;\r\n" + // + " uniform float u_layerAlpha;\r\n" + // + " uniform vec2 u_uvTrans;\r\n" + // + " uniform vec2 u_uvRot;\r\n" + // + " uniform float u_uvScale;\r\n" + // + " uniform bool u_hasBones;\r\n" + // + " uniform bool u_unshaded;\r\n" + // + " attribute vec3 a_position;\r\n" + // + " attribute vec3 a_normal;\r\n" + // + " attribute vec2 a_uv;\r\n" + // + " attribute vec4 a_bones;\r\n" + // + " #ifdef EXTENDED_BONES\r\n" + // + " attribute vec4 a_extendedBones;\r\n" + // + " #endif\r\n" + // + " attribute float a_boneNumber;\r\n" + // + " varying vec2 v_uv;\r\n" + // + " varying vec4 v_color;\r\n" + // + " varying vec4 v_uvTransRot;\r\n" + // + " varying float v_uvScale;\r\n" + // + " uniform sampler2D u_lightTexture;\r\n" + // + " uniform float u_lightCount;\r\n" + // + " uniform float u_lightTextureHeight;\r\n" + // + Shaders.boneTexture + "\r\n" + // + " void transform(inout vec3 position, inout vec3 normal) {\r\n" + // + " // For the broken models out there, since the game supports this.\r\n" + // + " if (a_boneNumber > 0.0) {\r\n" + // + " vec4 position4 = vec4(position, 1.0);\r\n" + // + " vec4 normal4 = vec4(normal, 0.0);\r\n" + // + " mat4 bone;\r\n" + // + " vec4 p = vec4(0.0,0.0,0.0,0.0);\r\n" + // + " vec4 n = vec4(0.0,0.0,0.0,0.0);\r\n" + // + " for (int i = 0; i < 4; i++) {\r\n" + // + " if (a_bones[i] > 0.0) {\r\n" + // + " bone = fetchMatrix(a_bones[i] - 1.0, 0.0);\r\n" + // + " p += bone * position4;\r\n" + // + " n += bone * normal4;\r\n" + // + " }\r\n" + // + " }\r\n" + // + " #ifdef EXTENDED_BONES\r\n" + // + " for (int i = 0; i < 4; i++) {\r\n" + // + " if (a_extendedBones[i] > 0.0) {\r\n" + // + " bone = fetchMatrix(a_extendedBones[i] - 1.0, 0.0);\r\n" + // + " p += bone * position4;\r\n" + // + " n += bone * normal4;\r\n" + // + " }\r\n" + // + " }\r\n" + // + " #endif\r\n" + // + " position = p.xyz / a_boneNumber;\r\n" + // + " normal = normalize(n.xyz);\r\n" + // + " } else {\r\n" + // + " position.x += 100.0;\r\n" + // + " }\r\n" + // + "\r\n" + // + " }\r\n" + // + " void main() {\r\n" + // + " vec3 position = a_position;\r\n" + // + " vec3 normal = a_normal;\r\n" + // + " if (u_hasBones) {\r\n" + // + " transform(position, normal);\r\n" + // + " }\r\n" + // + " v_uv = a_uv;\r\n" + // + " v_color = u_vertexColor * u_geosetColor.bgra * vec4(1.0, 1.0, 1.0, u_layerAlpha);\r\n" + // + " v_uvTransRot = vec4(u_uvTrans, u_uvRot);\r\n" + // + " v_uvScale = u_uvScale;\r\n" + // + " gl_Position = u_mvp * vec4(position, 1.0);\r\n" + // + " if(!u_unshaded) {\r\n" + // + Shaders.lightSystem("normal", "position", "u_lightTexture", "u_lightTextureHeight", "u_lightCount", false) + + "\r\n" + // + " v_color.xyz *= clamp(lightFactor, 0.0, 1.0);\r\n" + // + " }\r\n" + // + " }"; + + public static final String fsComplex = Shaders.quatTransform + "\r\n\r\n" + // + " uniform sampler2D u_texture;\r\n" + // + " uniform vec4 u_vertexColor;\r\n" + // + " uniform float u_filterMode;\r\n" + // + " varying vec2 v_uv;\r\n" + // + " varying vec4 v_color;\r\n" + // + " varying vec4 v_uvTransRot;\r\n" + // + " varying float v_uvScale;\r\n" + // + " void main() {\r\n" + // + " vec2 uv = v_uv;\r\n" + // + " // Translation animation\r\n" + // + " uv += v_uvTransRot.xy;\r\n" + // + " // Rotation animation\r\n" + // + " uv = quat_transform(v_uvTransRot.zw, uv - 0.5) + 0.5;\r\n" + // + " // Scale animation\r\n" + // + " uv = v_uvScale * (uv - 0.5) + 0.5;\r\n" + // + " vec4 texel = texture2D(u_texture, uv);\r\n" + // + " vec4 color = texel * v_color;\r\n" + // + " // 1bit Alpha\r\n" + // + " if (u_vertexColor.a == 1.0 && u_filterMode == 1.0 && color.a < 0.75) {\r\n" + // + " discard;\r\n" + // + " }\r\n" + // + " // \"Close to 0 alpha\"\r\n" + // + " if (u_filterMode >= 5.0 && color.a < 0.02) {\r\n" + // + " discard;\r\n" + // + " }\r\n" + // + " gl_FragColor = color;\r\n" + // + " }"; + + public static final String fsComplexShadowMap = "\r\n\r\n" + // + Shaders.quatTransform + "\r\n\r\n" + // + " uniform sampler2D u_texture;\r\n" + // + " uniform float u_filterMode;\r\n" + // + " varying vec2 v_uv;\r\n" + // + " varying vec4 v_color;\r\n" + // + " varying vec4 v_uvTransRot;\r\n" + // + " varying float v_uvScale;\r\n" + // + " varying vec3 v_normal;\r\n" + // +// " layout(location = 0) out float fragmentdepth;\r\n" + // + " void main() {\r\n" + // + " vec2 uv = v_uv;\r\n" + // + " // Translation animation\r\n" + // + " uv += v_uvTransRot.xy;\r\n" + // + " // Rotation animation\r\n" + // + " uv = quat_transform(v_uvTransRot.zw, uv - 0.5) + 0.5;\r\n" + // + " // Scale animation\r\n" + // + " uv = v_uvScale * (uv - 0.5) + 0.5;\r\n" + // + " vec4 texel = texture2D(u_texture, uv);\r\n" + // + " vec4 color = texel * v_color;\r\n" + // + " // 1bit Alpha\r\n" + // + " if (u_filterMode == 1.0 && color.a < 0.75) {\r\n" + // + " discard;\r\n" + // + " }\r\n" + // + " // \"Close to 0 alpha\"\r\n" + // + " if (u_filterMode >= 5.0 && color.a < 0.02) {\r\n" + // + " discard;\r\n" + // + " }\r\n" + // + " gl_FragColor = vec4(0.0, 0, 0, 1.0);//gl_FragCoord.z;\r\n" + // + " }"; + + public static final String vsParticles = "\r\n" + // + " #define EMITTER_PARTICLE2 0.0\r\n" + // + " #define EMITTER_RIBBON 1.0\r\n" + // + " #define EMITTER_SPLAT 2.0\r\n" + // + " #define EMITTER_UBERSPLAT 3.0\r\n" + // + " #define HEAD 0.0\r\n" + // + " uniform mat4 u_mvp;\r\n" + // + " uniform mediump float u_emitter;\r\n" + // + " // Shared\r\n" + // + " uniform vec4 u_colors[3];\r\n" + // + " uniform vec3 u_vertices[4];\r\n" + // + " uniform vec3 u_intervals[4];\r\n" + // + " uniform float u_lifeSpan;\r\n" + // + " uniform float u_columns;\r\n" + // + " uniform float u_rows;\r\n" + // + " // Particle2\r\n" + // + " uniform vec3 u_scaling;\r\n" + // + " uniform vec3 u_cameraZ;\r\n" + // + " uniform float u_timeMiddle;\r\n" + // + " uniform bool u_teamColored;\r\n" + // + " // Splat and Uber.\r\n" + // + " uniform vec3 u_intervalTimes;\r\n" + // + " // Vertices\r\n" + // + " attribute float a_position;\r\n" + // + " // Instances\r\n" + // + " attribute vec3 a_p0;\r\n" + // + " attribute vec3 a_p1;\r\n" + // + " attribute vec3 a_p2;\r\n" + // + " attribute vec3 a_p3;\r\n" + // + " attribute float a_health;\r\n" + // + " attribute vec4 a_color;\r\n" + // + " attribute float a_tail;\r\n" + // + " attribute vec3 a_leftRightTop;\r\n" + // + " varying vec2 v_texcoord;\r\n" + // + " varying vec4 v_color;\r\n" + // + " float getCell(vec3 interval, float factor) {\r\n" + // + " float start = interval[0];\r\n" + // + " float end = interval[1];\r\n" + // + " float repeat = interval[2];\r\n" + // + " float spriteCount = end - start;\r\n" + // + " if (spriteCount > 0.0) {\r\n" + // + " // Repeating speeds up the sprite animation, which makes it effectively run N times in its interval.\r\n" + + // + " // E.g. if repeat is 4, the sprite animation will be seen 4 times, and thus also run 4 times as fast.\r\n" + + // + " // The sprite index is limited to the number of actual sprites.\r\n" + // + " return min(start + mod(floor(spriteCount * repeat * factor), spriteCount), u_columns * u_rows - 1.0);\r\n" + + // + " }\r\n" + // + " return 0.0;\r\n" + // + " }\r\n" + // + " void particle2() {\r\n" + // + " float factor = (u_lifeSpan - a_health) / u_lifeSpan;\r\n" + // + " int index = 0;\r\n" + // + " if (factor < u_timeMiddle) {\r\n" + // + " factor = factor / u_timeMiddle;\r\n" + // + " index = 0;\r\n" + // + " } else {\r\n" + // + " factor = (factor - u_timeMiddle) / (1.0 - u_timeMiddle);\r\n" + // + " index = 1;\r\n" + // + " }\r\n" + // + " factor = min(factor, 1.0);\r\n" + // + " float scale = mix(u_scaling[index], u_scaling[index + 1], factor);\r\n" + // + " vec4 color = mix(u_colors[index], u_colors[index + 1], factor);\r\n" + // + " float cell = 0.0;\r\n" + // + " if (u_teamColored) {\r\n" + // + " cell = a_leftRightTop[0];\r\n" + // + " } else {\r\n" + // + " vec3 interval;\r\n" + // + " if (a_tail == HEAD) {\r\n" + // + " interval = u_intervals[index];\r\n" + // + " } else {\r\n" + // + " interval = u_intervals[index + 2];\r\n" + // + " }\r\n" + // + " cell = getCell(interval, factor);\r\n" + // + " }\r\n" + // + " float left = floor(mod(cell, u_columns));\r\n" + // + " float top = floor(cell / u_columns);\r\n" + // + " float right = left + 1.0;\r\n" + // + " float bottom = top + 1.0;\r\n" + // + " left /= u_columns;\r\n" + // + " right /= u_columns;\r\n" + // + " top /= u_rows;\r\n" + // + " bottom /= u_rows;\r\n" + // + " if (a_position == 0.0) {\r\n" + // + " v_texcoord = vec2(right, top);\r\n" + // + " } else if (a_position == 1.0) {\r\n" + // + " v_texcoord = vec2(left, top);\r\n" + // + " } else if (a_position == 2.0) {\r\n" + // + " v_texcoord = vec2(left, bottom);\r\n" + // + " } else if (a_position == 3.0) {\r\n" + // + " v_texcoord = vec2(right, bottom);\r\n" + // + " }\r\n" + // + " v_color = color;\r\n" + // + " \r\n" + // + " if (a_tail == HEAD) {\r\n" + // + " vec3 vertices[4];\r\n" + // + " if(a_p1[0] != 0.0 || a_p1[1] != 0.0) {\r\n" + // + " vec3 vx;\r\n" + // + " vx[0] = a_p1[0];\r\n" + // + " vx[1] = a_p1[1];\r\n" + // + " vx[2] = 0.0;\r\n" + // + " vx = normalize(vx);\r\n" + // + " vec3 vy;\r\n" + // + " vy[0] = -vx[1];\r\n" + // + " vy[1] = vx[0];\r\n" + // + " vy[2] = 0.0;\r\n" + // + " vertices[2] = - vx - vy;\r\n" + // + " vertices[1] = vx - vy;\r\n" + // + " vertices[0] = -vertices[2];\r\n" + // + " vertices[3] = -vertices[1];\r\n" + // + " } else {\r\n" + // + " vertices[0] = u_vertices[0];\r\n" + // + " vertices[1] = u_vertices[1];\r\n" + // + " vertices[2] = u_vertices[2];\r\n" + // + " vertices[3] = u_vertices[3];\r\n" + // + " }\r\n" + // + " gl_Position = u_mvp * vec4(a_p0 + (vertices[int(a_position)] * scale), 1.0);\r\n" + // + " } else {\r\n" + // + " // Get the normal to the tail in camera space.\r\n" + // + " // This allows to build a 2D rectangle around the 3D tail.\r\n" + // + " vec3 normal = cross(u_cameraZ, normalize(a_p1 - a_p0));\r\n" + // + " vec3 boundary = normal * scale * a_p2[0];\r\n" + // + " vec3 position;\r\n" + // + " if (a_position == 0.0) {\r\n" + // + " position = a_p0 - boundary;\r\n" + // + " } else if (a_position == 1.0) {\r\n" + // + " position = a_p1 - boundary;\r\n" + // + " } else if (a_position == 2.0) {\r\n" + // + " position = a_p1 + boundary;\r\n" + // + " } else if (a_position == 3.0) {\r\n" + // + " position = a_p0 + boundary;\r\n" + // + " }\r\n" + // + " gl_Position = u_mvp * vec4(position, 1.0);\r\n" + // + " }\r\n" + // + " }\r\n" + // + " void ribbon() {\r\n" + // + " vec3 position;\r\n" + // + " float left = a_leftRightTop[0] / 255.0;\r\n" + // + " float right = a_leftRightTop[1] / 255.0;\r\n" + // + " float top = a_leftRightTop[2] / 255.0;\r\n" + // + " float bottom = top + 1.0;\r\n" + // + " if (a_position == 0.0) {\r\n" + // + " v_texcoord = vec2(right, top);\r\n" + // + " position = a_p0;\r\n" + // + " } else if (a_position == 1.0) {\r\n" + // + " v_texcoord = vec2(right, bottom);\r\n" + // + " position = a_p1;\r\n" + // + " } else if (a_position == 2.0) {\r\n" + // + " v_texcoord = vec2(left, bottom);\r\n" + // + " position = a_p2;\r\n" + // + " } else if (a_position == 3.0) {\r\n" + // + " v_texcoord = vec2(left, top);\r\n" + // + " position = a_p3;\r\n" + // + " }\r\n" + // + " v_texcoord[0] /= u_columns;\r\n" + // + " v_texcoord[1] /= u_rows;\r\n" + // + " v_color = a_color;\r\n" + // + " gl_Position = u_mvp * vec4(position, 1.0);\r\n" + // + " }\r\n" + // + " void splat() {\r\n" + // + " float factor = u_lifeSpan - a_health;\r\n" + // + " int index;\r\n" + // + " if (factor < u_intervalTimes[0]) {\r\n" + // + " factor = factor / u_intervalTimes[0];\r\n" + // + " index = 0;\r\n" + // + " } else {\r\n" + // + " factor = (factor - u_intervalTimes[0]) / u_intervalTimes[1];\r\n" + // + " index = 1;\r\n" + // + " }\r\n" + // + " float cell = getCell(u_intervals[index], factor);\r\n" + // + " float left = floor(mod(cell, u_columns));\r\n" + // + " float top = floor(cell / u_columns);\r\n" + // + " float right = left + 1.0;\r\n" + // + " float bottom = top + 1.0;\r\n" + // + " vec3 position;\r\n" + // + " if (a_position == 0.0) {\r\n" + // + " v_texcoord = vec2(left, top);\r\n" + // + " position = a_p0;\r\n" + // + " } else if (a_position == 1.0) {\r\n" + // + " v_texcoord = vec2(left, bottom);\r\n" + // + " position = a_p1;\r\n" + // + " } else if (a_position == 2.0) {\r\n" + // + " v_texcoord = vec2(right, bottom);\r\n" + // + " position = a_p2;\r\n" + // + " } else if (a_position == 3.0) {\r\n" + // + " v_texcoord = vec2(right, top);\r\n" + // + " position = a_p3;\r\n" + // + " }\r\n" + // + " v_texcoord[0] /= u_columns;\r\n" + // + " v_texcoord[1] /= u_rows;\r\n" + // + " v_color = mix(u_colors[index], u_colors[index + 1], factor) / 255.0;\r\n" + // + " gl_Position = u_mvp * vec4(position, 1.0);\r\n" + // + " }\r\n" + // + " void ubersplat() {\r\n" + // + " float factor = u_lifeSpan - a_health;\r\n" + // + " vec4 color;\r\n" + // + " if (factor < u_intervalTimes[0]) {\r\n" + // + " color = mix(u_colors[0], u_colors[1], factor / u_intervalTimes[0]);\r\n" + // + " } else if (factor < u_intervalTimes[0] + u_intervalTimes[1]) {\r\n" + // + " color = u_colors[1];\r\n" + // + " } else {\r\n" + // + " color = mix(u_colors[1], u_colors[2], (factor - u_intervalTimes[0] - u_intervalTimes[1]) / u_intervalTimes[2]);\r\n" + + // + " }\r\n" + // + " vec3 position;\r\n" + // + " if (a_position == 0.0) {\r\n" + // + " v_texcoord = vec2(0.0, 0.0);\r\n" + // + " position = a_p0;\r\n" + // + " } else if (a_position == 1.0) {\r\n" + // + " v_texcoord = vec2(0.0, 1.0);\r\n" + // + " position = a_p1;\r\n" + // + " } else if (a_position == 2.0) {\r\n" + // + " v_texcoord = vec2(1.0, 1.0);\r\n" + // + " position = a_p2;\r\n" + // + " } else if (a_position == 3.0) {\r\n" + // + " v_texcoord = vec2(1.0, 0.0);\r\n" + // + " position = a_p3;\r\n" + // + " }\r\n" + // + " v_color = color / 255.0;\r\n" + // + " gl_Position = u_mvp * vec4(position, 1.0);\r\n" + // + " }\r\n" + // + " void main() {\r\n" + // + " if (u_emitter == EMITTER_PARTICLE2) {\r\n" + // + " particle2();\r\n" + // + " } else if (u_emitter == EMITTER_RIBBON) {\r\n" + // + " ribbon();\r\n" + // + " } else if (u_emitter == EMITTER_SPLAT) {\r\n" + // + " splat();\r\n" + // + " } else if (u_emitter == EMITTER_UBERSPLAT) {\r\n" + // + " ubersplat();\r\n" + // + " }\r\n" + // + " }"; + + public static final String fsParticles = "\r\n" + // + " #define EMITTER_RIBBON 1.0\r\n" + // + " uniform sampler2D u_texture;\r\n" + // + " uniform mediump float u_emitter;\r\n" + // + " uniform float u_filterMode;\r\n" + // + " varying vec2 v_texcoord;\r\n" + // + " varying vec4 v_color;\r\n" + // + " void main() {\r\n" + // + " vec4 texel = texture2D(u_texture, v_texcoord);\r\n" + // + " vec4 color = texel * v_color;\r\n" + // + " // 1bit Alpha, used by ribbon emitters.\r\n" + // + " if (u_emitter == EMITTER_RIBBON && u_filterMode == 1.0 && color.a < 0.75) {\r\n" + // + " discard;\r\n" + // + " }\r\n" + // + " gl_FragColor = color;\r\n" + // + " }"; +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxSimpleInstance.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxSimpleInstance.java new file mode 100644 index 0000000..f56d476 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxSimpleInstance.java @@ -0,0 +1,58 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.badlogic.gdx.math.Matrix4; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.BatchedInstance; +import com.etheller.warsmash.viewer5.Model; +import com.etheller.warsmash.viewer5.PathSolver; +import com.etheller.warsmash.viewer5.RenderBatch; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.Texture; +import com.etheller.warsmash.viewer5.TextureMapper; + +public class MdxSimpleInstance extends BatchedInstance { + public Texture[] replaceableTextures = new Texture[WarsmashConstants.REPLACEABLE_TEXTURE_LIMIT]; + + public MdxSimpleInstance(final Model model) { + super(model); + } + + @Override + public void updateAnimations(final float dt) { + } + + @Override + public void clearEmittedObjects() { + } + + @Override + public void renderOpaque(final Matrix4 mvp) { + } + + @Override + public void renderTranslucent() { + } + + @Override + protected void updateLights(final Scene scene2) { + } + + @Override + protected void removeLights(final Scene scene2) { + } + + @Override + public void load() { + } + + @Override + public RenderBatch getBatch(final TextureMapper textureMapper) { + return new MdxRenderBatch(this.scene, this.model, textureMapper); + } + + @Override + public void setReplaceableTexture(final int replaceableTextureId, final String replaceableTextureFile) { + this.replaceableTextures[replaceableTextureId] = (Texture) this.model.viewer.load(replaceableTextureFile, + PathSolver.DEFAULT, null); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxViewer.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxViewer.java new file mode 100644 index 0000000..ea4fe18 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/MdxViewer.java @@ -0,0 +1,33 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.util.StringBundle; +import com.etheller.warsmash.util.WorldEditStrings; +import com.etheller.warsmash.viewer5.CanvasProvider; +import com.etheller.warsmash.viewer5.SceneLightManager; +import com.etheller.warsmash.viewer5.handlers.AbstractMdxModelViewer; +import com.etheller.warsmash.viewer5.handlers.w3x.W3xScenePortraitLightManager; + +public class MdxViewer extends AbstractMdxModelViewer { + + private final WorldEditStrings worldEditStrings; + private final Vector3 defaultLighting; + + public MdxViewer(final DataSource dataSource, final CanvasProvider canvas, final Vector3 defaultLighting) { + super(dataSource, canvas); + this.defaultLighting = defaultLighting; + this.worldEditStrings = new WorldEditStrings(this.dataSource); + } + + @Override + public SceneLightManager createLightManager(final boolean simple) { + return new W3xScenePortraitLightManager(this, this.defaultLighting); + } + + @Override + public StringBundle getWorldEditStrings() { + return this.worldEditStrings; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Particle.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Particle.java new file mode 100644 index 0000000..f146e7c --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Particle.java @@ -0,0 +1,103 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.badlogic.gdx.math.Quaternion; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.util.RenderMathUtils; +import com.etheller.warsmash.viewer5.EmittedObject; +import com.etheller.warsmash.viewer5.Scene; + +/** + * A spawned model particle. + */ +public class Particle extends EmittedObject { + private static final Quaternion rotationHeap = new Quaternion(); + private static final Quaternion rotationHeap2 = new Quaternion(); + private static final Vector3 velocityHeap = new Vector3(); + private static final float[] latitudeHeap = new float[1]; +// private static final float[] longitudeHeap = new float[1]; + private static final float[] lifeSpanHeap = new float[1]; + private static final float[] gravityHeap = new float[1]; + private static final float[] speedHeap = new float[1]; + private static final float[] tempVector = new float[3]; + + private final MdxComplexInstance internalInstance; + private final Vector3 velocity = new Vector3(); + private float gravity; + + public Particle(final ParticleEmitter emitter) { + super(emitter); + + final ParticleEmitterObject emitterObject = emitter.emitterObject; + + this.internalInstance = (MdxComplexInstance) emitterObject.internalModel.addInstance(); + } + + @Override + protected void bind(final int flags) { + final ParticleEmitter emitter = this.emitter; + final MdxComplexInstance instance = emitter.instance; + final int sequence = instance.sequence; + final int frame = instance.frame; + final int counter = instance.counter; + final Scene scene = instance.scene; + final ParticleEmitterObject emitterObject = emitter.emitterObject; + final MdxNode node = instance.nodes[emitterObject.index]; + final MdxComplexInstance internalInstance = this.internalInstance; + final Vector3 scale = node.worldScale; + final Vector3 velocity = this.velocity; + + emitterObject.getLatitude(latitudeHeap, sequence, frame, counter); + // longitude?? commented in ghostwolf JS + emitterObject.getLifeSpan(lifeSpanHeap, sequence, frame, counter); + emitterObject.getGravity(gravityHeap, sequence, frame, counter); + emitterObject.getSpeed(speedHeap, sequence, frame, counter); + + this.health = lifeSpanHeap[0]; + this.gravity = gravityHeap[0] * scale.z; + + // Local rotation + rotationHeap.idt(); + rotationHeap.mul(rotationHeap2.setFromAxisRad(0, 0, 1, + RenderMathUtils.randomInRange((float) -Math.PI, (float) Math.PI))); + rotationHeap.mul(rotationHeap2.setFromAxisRad(0, 1, 0, + RenderMathUtils.randomInRange(-latitudeHeap[0], latitudeHeap[0]))); + velocity.set(RenderMathUtils.VEC3_UNIT_Z); + rotationHeap.transform(velocity); + + // World rotation + node.worldRotation.transform(velocity); + + // Apply speed + velocity.scl(speedHeap[0]); + + // Apply the parent's scale + velocity.scl(scale); + + scene.addInstance(internalInstance); + + internalInstance.setTransformation(node.worldLocation, rotationHeap.setFromAxisRad(RenderMathUtils.VEC3_UNIT_Z, + RenderMathUtils.randomInRange(0, (float) Math.PI * 2)), node.worldScale); + internalInstance.setSequence(0); + internalInstance.show(); + } + + @Override + public void update(final float dt) { + final MdxComplexInstance internalInstance = this.internalInstance; + + internalInstance.paused = false; /// Why is this here? + + this.health -= dt; + + if (this.health > 0) { + final Vector3 velocity = this.velocity; + + velocity.z -= this.gravity * dt; + + tempVector[0] = velocity.x * dt; + tempVector[1] = velocity.y * dt; + tempVector[2] = velocity.z * dt; + internalInstance.move(tempVector); + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Particle2.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Particle2.java new file mode 100644 index 0000000..91f5e6f --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Particle2.java @@ -0,0 +1,109 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.badlogic.gdx.math.Quaternion; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.util.RenderMathUtils; +import com.etheller.warsmash.viewer5.EmittedObject; + +public class Particle2 extends EmittedObject { + private static final Quaternion HALF_PI_Z = new Quaternion().setFromAxisRad(0, 0, 1, (float) (Math.PI / 2)); + public int tail = 0; + private float gravity = 0; + public final Vector3 location = new Vector3(); + public final Vector3 velocity = new Vector3(); + public final Vector3 scale = new Vector3(1, 1, 1); + + private static final Quaternion rotationHeap = new Quaternion(); + private static final Quaternion rotationHeap2 = new Quaternion(); + private static final float[] widthHeap = new float[1]; + private static final float[] lengthHeap = new float[1]; + private static final float[] latitudeHeap = new float[1]; + private static final float[] variationHeap = new float[1]; + private static final float[] speedHeap = new float[1]; + private static final float[] gravityHeap = new float[1]; + + public Particle2(final ParticleEmitter2 emitter) { + super(emitter); + } + + @Override + protected void bind(final int flags) { + final MdxComplexInstance instance = this.emitter.instance; + final ParticleEmitter2Object emitterObject = this.emitter.emitterObject; + + emitterObject.getWidth(widthHeap, instance.sequence, instance.frame, instance.counter); + emitterObject.getLength(lengthHeap, instance.sequence, instance.frame, instance.counter); + emitterObject.getLatitude(latitudeHeap, instance.sequence, instance.frame, instance.counter); + emitterObject.getVariation(variationHeap, instance.sequence, instance.frame, instance.counter); + emitterObject.getSpeed(speedHeap, instance.sequence, instance.frame, instance.counter); + emitterObject.getGravity(gravityHeap, instance.sequence, instance.frame, instance.counter); + + final MdxNode node = this.emitter.node; + final Vector3 pivot = node.pivot; + final Vector3 scale = node.worldScale; + final float width = widthHeap[0] * 0.5f; + final float length = lengthHeap[0] * 0.5f; + final float latitude = (float) Math.toRadians(latitudeHeap[0]); + final float variation = variationHeap[0]; + final float speed = speedHeap[0]; + final Vector3 location = this.location; + final Vector3 velocity = this.velocity; + + this.health = emitterObject.lifeSpan; + this.tail = flags; + this.gravity = gravityHeap[0] * scale.z; + + this.scale.set(scale); + + // Local location + location.x = pivot.x + RenderMathUtils.randomInRange(-width, width); + location.y = pivot.y + RenderMathUtils.randomInRange(-length, length); + location.z = pivot.z; + + // World location + if (emitterObject.modelSpace == 0) { + location.prj(node.worldMatrix); + } + + // Local rotation + rotationHeap.idt(); + rotationHeap.mul(HALF_PI_Z); + rotationHeap.mul(rotationHeap2.setFromAxisRad(0, 1, 0, RenderMathUtils.randomInRange(-latitude, latitude))); + + // If this is not a line emitter, emit in a sphere rather than a circle + if (emitterObject.lineEmitter == 0) { + rotationHeap.mul(rotationHeap2.setFromAxisRad(1, 0, 0, RenderMathUtils.randomInRange(-latitude, latitude))); + } + + // World rotation + if (emitterObject.modelSpace == 0) { + rotationHeap.mulLeft(node.worldRotation); + } + + // Apply the rotation + velocity.set(RenderMathUtils.VEC3_UNIT_Z); + rotationHeap.transform(velocity); + + // Apply speed + velocity.scl(speed + RenderMathUtils.randomInRange(-variation, variation)); + + // Apply the parent's scale + if (emitterObject.modelSpace == 0) { + velocity.scl(scale); + } + } + + @Override + public void update(final float dt) { + this.health -= dt; + + if (this.health > 0) { + this.velocity.z -= this.gravity * dt; + + this.location.x += this.velocity.x * dt; + this.location.y += this.velocity.y * dt; + this.location.z += this.velocity.z * dt; + } + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ParticleEmitter.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ParticleEmitter.java new file mode 100644 index 0000000..7b9f8eb --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ParticleEmitter.java @@ -0,0 +1,34 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +public class ParticleEmitter extends MdxEmitter { + + private static final float[] emissionRateHeap = new float[1]; + + public ParticleEmitter(final MdxComplexInstance instance, final ParticleEmitterObject emitterObject) { + super(instance, emitterObject); + } + + @Override + protected void updateEmission(final float dt) { + final MdxComplexInstance instance = this.instance; + + if (instance.allowParticleSpawn) { + final ParticleEmitterObject emitterObject = this.emitterObject; + + emitterObject.getEmissionRate(emissionRateHeap, instance.sequence, instance.frame, instance.counter); + + this.currentEmission += emissionRateHeap[0] * dt; + } + } + + @Override + protected void emit() { + this.emitObject(0); + } + + @Override + protected Particle createObject() { + return new Particle(this); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ParticleEmitter2.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ParticleEmitter2.java new file mode 100644 index 0000000..77b2a30 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ParticleEmitter2.java @@ -0,0 +1,57 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.etheller.warsmash.util.WarsmashConstants; + +public class ParticleEmitter2 extends MdxEmitter { + private static final float[] emissionRateHeap = new float[1]; + + protected final MdxNode node; + private int lastEmissionKey; + + public ParticleEmitter2(final MdxComplexInstance instance, final ParticleEmitter2Object emitterObject) { + super(instance, emitterObject); + + this.node = instance.nodes[emitterObject.index]; + this.lastEmissionKey = -1; + } + + @Override + protected void updateEmission(final float dt) { + final MdxComplexInstance instance = this.instance; + + if (instance.allowParticleSpawn) { + final ParticleEmitter2Object emitterObject = this.emitterObject; + final int keyframe = emitterObject.getEmissionRate(emissionRateHeap, instance.sequence, instance.frame, + instance.counter); + + if (emitterObject.squirt != 0) { + if (keyframe != this.lastEmissionKey) { + this.currentEmission += emissionRateHeap[0]; + } + + this.lastEmissionKey = keyframe; + } + else { + this.currentEmission += emissionRateHeap[0] * dt * WarsmashConstants.MODEL_DETAIL_PARTICLE_FACTOR; + } + } + } + + @Override + protected void emit() { + if (this.emitterObject.head) { + this.emitObject(0); + } + + if (this.emitterObject.tail) { + this.emitObject(1); + } + + } + + @Override + protected Particle2 createObject() { + return new Particle2(this); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ParticleEmitter2Object.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ParticleEmitter2Object.java new file mode 100644 index 0000000..a771836 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ParticleEmitter2Object.java @@ -0,0 +1,154 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.Texture; +import com.etheller.warsmash.viewer5.handlers.EmitterObject; +import com.hiveworkshop.rms.parsers.mdlx.AnimationMap; +import com.hiveworkshop.rms.parsers.mdlx.MdlxParticleEmitter2; +import com.hiveworkshop.rms.parsers.mdlx.MdlxParticleEmitter2.HeadOrTail; + +public class ParticleEmitter2Object extends GenericObject implements EmitterObject { + public float width; + public float length; + public float speed; + public float latitude; + public float gravity; + public float emissionRate; + public long squirt; + public float lifeSpan; + public float variation; + public float tailLength; + public float timeMiddle; + public long columns; + public long rows; + public int teamColored = 0; + public Texture internalTexture; + public long replaceableId; + public boolean head; + public boolean tail; + public float cellWidth; + public float cellHeight; + public float[][] colors; + public float[] scaling; + public float[][] intervals; + public int blendSrc; + public int blendDst; + public int priorityPlane; + + public ParticleEmitter2Object(final MdxModel model, final MdlxParticleEmitter2 emitter, final int index) { + super(model, emitter, index); + + this.width = emitter.getWidth(); + this.length = emitter.getLength(); + this.speed = emitter.getSpeed(); + this.latitude = emitter.getLatitude(); + this.gravity = emitter.getGravity(); + this.emissionRate = emitter.getEmissionRate(); + this.squirt = emitter.getSquirt(); + this.lifeSpan = emitter.getLifeSpan(); + this.variation = emitter.getVariation(); + this.tailLength = emitter.getTailLength(); + this.timeMiddle = emitter.getTimeMiddle(); + + final long replaceableId = emitter.getReplaceableId(); + + this.columns = emitter.getColumns(); + this.rows = emitter.getRows(); + + if (replaceableId == 0) { + this.internalTexture = model.getTextures().get(emitter.getTextureId()); + } + else if ((replaceableId == 1) || (replaceableId == 2)) { + this.teamColored = 1; + } + else { + this.internalTexture = (Texture) model.viewer.load( + "ReplaceableTextures\\" + ReplaceableIds.getPathString(replaceableId) + ".blp", model.pathSolver, + model.solverParams); + } + + this.replaceableId = emitter.getReplaceableId(); + + final HeadOrTail headOrTail = emitter.getHeadOrTail(); + + this.head = headOrTail.isIncludesHead(); + this.tail = headOrTail.isIncludesTail(); + + this.cellWidth = 1f / emitter.getColumns(); + this.cellHeight = 1f / emitter.getRows(); + this.colors = new float[3][0]; + + final float[][] colors = emitter.getSegmentColors(); + final short[] alpha = emitter.getSegmentAlphas(); + + for (int i = 0; i < 3; i++) { + final float[] color = colors[i]; + + this.colors[i] = new float[] { color[0], color[1], color[2], + (alpha[i] / 255f) * WarsmashConstants.MODEL_DETAIL_PARTICLE_FACTOR_INVERSE }; + } + + this.scaling = emitter.getSegmentScaling(); + + final long[][] headIntervals = emitter.getHeadIntervals(); + final long[][] tailIntervals = emitter.getTailIntervals(); + + // Change to Float32Array instead of Uint32Array to be able to pass the + // intervals directly using uniform3fv(). + this.intervals = new float[][] { { headIntervals[0][0], headIntervals[0][1], headIntervals[0][2] }, + { headIntervals[1][0], headIntervals[1][1], headIntervals[1][2] }, + { tailIntervals[0][0], tailIntervals[0][1], tailIntervals[0][2] }, + { tailIntervals[1][0], tailIntervals[1][1], tailIntervals[1][2] }, }; + + final int[] blendModes = FilterMode.emitterFilterMode(emitter.getFilterMode()); + + this.blendSrc = blendModes[0]; + this.blendDst = blendModes[1]; + + this.priorityPlane = emitter.getPriorityPlane(); + } + + public int getWidth(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KP2N.getWar3id(), sequence, frame, counter, this.length); + } + + public int getLength(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KP2W.getWar3id(), sequence, frame, counter, this.width); + } + + public int getSpeed(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KP2S.getWar3id(), sequence, frame, counter, this.speed); + } + + public int getLatitude(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KP2L.getWar3id(), sequence, frame, counter, this.latitude); + } + + public int getGravity(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KP2G.getWar3id(), sequence, frame, counter, this.gravity); + } + + public int getEmissionRate(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KP2E.getWar3id(), sequence, frame, counter, this.emissionRate); + } + + @Override + public int getVisibility(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KP2V.getWar3id(), sequence, frame, counter, 1); + } + + public int getVariation(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KP2R.getWar3id(), sequence, frame, counter, this.variation); + } + + @Override + public boolean ok() { + return true; + } + + @Override + public int getGeometryEmitterType() { + return GeometryEmitterFuncs.EMITTER_PARTICLE2; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ParticleEmitterObject.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ParticleEmitterObject.java new file mode 100644 index 0000000..66822c1 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ParticleEmitterObject.java @@ -0,0 +1,83 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.util.Locale; + +import com.etheller.warsmash.viewer5.handlers.EmitterObject; +import com.hiveworkshop.rms.parsers.mdlx.AnimationMap; +import com.hiveworkshop.rms.parsers.mdlx.MdlxParticleEmitter; + +public class ParticleEmitterObject extends GenericObject implements EmitterObject { + public MdxModel internalModel; + public float speed; + public float latitude; + public float longitude; + public float lifeSpan; + public float gravity; + public float emissionRate; + + /** + * No need to create instances of the internal model if it didn't load. + * + * Such instances won't actually render, and who knows if the model will ever + * load? + */ + public boolean ok = false; + + public ParticleEmitterObject(final MdxModel model, final MdlxParticleEmitter emitter, final int index) { + super(model, emitter, index); + + this.internalModel = (MdxModel) model.viewer.load( + emitter.getPath().replace("\\", "/").toLowerCase(Locale.US).replace(".mdl", ".mdx"), model.pathSolver, + model.solverParams); + this.speed = emitter.getSpeed(); + this.latitude = emitter.getLatitude(); + this.longitude = emitter.getLongitude(); + this.lifeSpan = emitter.getLifeSpan(); + this.gravity = emitter.getGravity(); + this.emissionRate = emitter.getEmissionRate(); + + // Activate emitters based on this emitter object only when and if the internal + // model loads successfully. + // TODO async removed here + this.ok = this.internalModel.ok; + } + + public int getSpeed(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KPES.getWar3id(), sequence, frame, counter, this.speed); + } + + public int getLatitude(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KPLT.getWar3id(), sequence, frame, counter, this.latitude); + } + + public int getLongitude(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KPLN.getWar3id(), sequence, frame, counter, this.longitude); + } + + public int getLifeSpan(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KPEL.getWar3id(), sequence, frame, counter, this.lifeSpan); + } + + public int getGravity(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KPEG.getWar3id(), sequence, frame, counter, this.gravity); + } + + public int getEmissionRate(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KPEE.getWar3id(), sequence, frame, counter, this.emissionRate); + } + + @Override + public int getVisibility(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KPEV.getWar3id(), sequence, frame, counter, 1); + } + + @Override + public int getGeometryEmitterType() { + throw new UnsupportedOperationException("ghostwolf doesnt have this in the JS"); + } + + @Override + public boolean ok() { + return this.ok; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/QuaternionSd.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/QuaternionSd.java new file mode 100644 index 0000000..a50cf96 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/QuaternionSd.java @@ -0,0 +1,32 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.etheller.warsmash.util.Interpolator; +import com.etheller.warsmash.util.RenderMathUtils; +import com.hiveworkshop.rms.parsers.mdlx.timeline.MdlxTimeline; + +public class QuaternionSd extends Sd { + + public QuaternionSd(final MdxModel model, final MdlxTimeline timeline) { + super(model, timeline, SdArrayDescriptor.FLOAT_ARRAY); + } + + @Override + protected float[] convertDefaultValue(final float[] defaultValue) { + return defaultValue; + } + + @Override + protected void copy(final float[] out, final float[] value) { + System.arraycopy(value, 0, out, 0, value.length); + } + + @Override + protected void interpolate(final float[] out, final float[][] values, final float[][] inTans, + final float[][] outTans, final int start, final int end, final float t) { + Interpolator.interpolateQuaternion(out, values[start], + (start < outTans.length) ? outTans[start] : RenderMathUtils.EMPTY_FLOAT_ARRAY, + (start < inTans.length) ? inTans[end] : RenderMathUtils.EMPTY_FLOAT_ARRAY, values[end], t, + this.interpolationType); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ReplaceableIds.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ReplaceableIds.java new file mode 100644 index 0000000..f62fc99 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ReplaceableIds.java @@ -0,0 +1,41 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.util.HashMap; +import java.util.Map; + +import com.etheller.warsmash.util.WarsmashConstants; + +public class ReplaceableIds { + private static final Map ID_TO_STR = new HashMap<>(); + private static final Map REPLACEABLE_ID_TO_STR = new HashMap<>(); + + static { + for (int i = 0; i < WarsmashConstants.MAX_PLAYERS; i++) { + ID_TO_STR.put(Long.valueOf(i), String.format("%2d", i).replace(' ', '0')); + } + REPLACEABLE_ID_TO_STR.put(Long.valueOf(1), "TeamColor\\TeamColor"); + REPLACEABLE_ID_TO_STR.put(Long.valueOf(2), "TeamGlow\\TeamGlow"); + REPLACEABLE_ID_TO_STR.put(Long.valueOf(11), "Cliff\\Cliff0"); + REPLACEABLE_ID_TO_STR.put(Long.valueOf(21), ""); // Used by all cursor models (HumanCursor, OrcCursor, + // UndeadCursor, NightElfCursor) + REPLACEABLE_ID_TO_STR.put(Long.valueOf(31), "LordaeronTree\\LordaeronSummerTree"); + REPLACEABLE_ID_TO_STR.put(Long.valueOf(32), "AshenvaleTree\\AshenTree"); + REPLACEABLE_ID_TO_STR.put(Long.valueOf(33), "BarrensTree\\BarrensTree"); + REPLACEABLE_ID_TO_STR.put(Long.valueOf(34), "NorthrendTree\\NorthTree"); + REPLACEABLE_ID_TO_STR.put(Long.valueOf(35), "Mushroom\\MushroomTree"); + REPLACEABLE_ID_TO_STR.put(Long.valueOf(36), "RuinsTree\\RuinsTree"); + REPLACEABLE_ID_TO_STR.put(Long.valueOf(37), "OutlandMushroomTree\\MushroomTree"); + } + + public static void main(final String[] args) { + System.out.println(ID_TO_STR); + } + + public static String getIdString(final long replaceableId) { + return ID_TO_STR.get(replaceableId); + } + + public static String getPathString(final long replaceableId) { + return REPLACEABLE_ID_TO_STR.get(replaceableId); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Ribbon.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Ribbon.java new file mode 100644 index 0000000..b05f55d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Ribbon.java @@ -0,0 +1,90 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.viewer5.EmittedObject; + +public class Ribbon extends EmittedObject { + private static final float[] vectorHeap = new float[3]; + private static final Vector3 belowHeap = new Vector3(); + private static final Vector3 aboveHeap = new Vector3(); + private static final float[] colorHeap = new float[3]; + private static final float[] alphaHeap = new float[1]; + private static final long[] slotHeap = new long[1]; + + public float[] vertices = new float[6]; + public byte[] color = new byte[4]; + public int slot; + public Ribbon prev; + public Ribbon next; + + public Ribbon(final RibbonEmitter emitter) { + super(emitter); + } + + @Override + protected void bind(final int flags) { + final RibbonEmitter emitter = this.emitter; + final MdxComplexInstance instance = emitter.instance; + final RibbonEmitterObject emitterObject = emitter.emitterObject; + final MdxNode node = instance.nodes[emitterObject.index]; + final Vector3 pivot = node.pivot; + final float x = pivot.x, y = pivot.y, z = pivot.z; + final Matrix4 worldMatrix = node.worldMatrix; + final float[] vertices = this.vertices; + + this.health = emitter.emitterObject.lifeSpan; + + emitterObject.getHeightBelow(vectorHeap, instance.sequence, instance.frame, instance.counter); + belowHeap.set(vectorHeap); + emitterObject.getHeightAbove(vectorHeap, instance.sequence, instance.frame, instance.counter); + aboveHeap.set(vectorHeap); + + belowHeap.y = y - belowHeap.x; + belowHeap.x = x; + belowHeap.z = z; + + aboveHeap.y = y + aboveHeap.x; + aboveHeap.x = x; + aboveHeap.z = z; + + belowHeap.prj(worldMatrix); + aboveHeap.prj(worldMatrix); + + vertices[0] = aboveHeap.x; + vertices[1] = aboveHeap.y; + vertices[2] = aboveHeap.z; + vertices[3] = belowHeap.x; + vertices[4] = belowHeap.y; + vertices[5] = belowHeap.z; + } + + @Override + public void update(final float dt) { + this.health -= dt; + + if (this.health > 0) { + final RibbonEmitter emitter = this.emitter; + final MdxComplexInstance instance = emitter.instance; + final RibbonEmitterObject emitterObject = emitter.emitterObject; + final byte[] color = this.color; + final float[] vertices = this.vertices; + final float gravity = emitterObject.gravity * dt * dt; + + emitterObject.getColor(colorHeap, instance.sequence, instance.frame, instance.counter); + emitterObject.getAlpha(alphaHeap, instance.sequence, instance.frame, instance.counter); + emitterObject.getTextureSlot(slotHeap, instance.sequence, instance.frame, instance.counter); + + vertices[1] -= gravity; + vertices[4] -= gravity; + + color[0] = (byte) (colorHeap[0] * 255); + color[1] = (byte) (colorHeap[1] * 255); + color[2] = (byte) (colorHeap[2] * 255); + color[3] = (byte) (alphaHeap[0] * 255); + + this.slot = (int) slotHeap[0]; + } + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/RibbonEmitter.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/RibbonEmitter.java new file mode 100644 index 0000000..e815f21 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/RibbonEmitter.java @@ -0,0 +1,73 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +public class RibbonEmitter extends MdxEmitter { + public Ribbon first; + public Ribbon last; + + public RibbonEmitter(final MdxComplexInstance instance, final RibbonEmitterObject emitterObject) { + super(instance, emitterObject); + } + + @Override + protected void updateEmission(final float dt) { + final MdxComplexInstance instance = this.instance; + + if (instance.allowParticleSpawn) { + final RibbonEmitterObject emitterObject = this.emitterObject; + + // It doesn't make sense to emit more than 1 ribbon at the same time. + this.currentEmission = Math.min(this.currentEmission + (emitterObject.emissionRate * dt), 1); + } + + } + + @Override + protected void emit() { + final Ribbon ribbon = this.emitObject(0); + final Ribbon last = this.last; + + if (last != null) { + last.next = ribbon; + ribbon.prev = last; + } + else { + this.first = ribbon; + } + + this.last = ribbon; + } + + @Override + public void kill(final Ribbon object) { + super.kill(object); + + final Ribbon prev = object.prev; + final Ribbon next = object.next; + + if (object == this.first) { + this.first = next; + } + + if (object == this.last) { + this.first = null; + this.last = null; + } + + if (prev != null) { + prev.next = next; + } + + if (next != null) { + next.prev = prev; + } + + object.prev = null; + object.next = null; + } + + @Override + protected Ribbon createObject() { + return new Ribbon(this); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/RibbonEmitterObject.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/RibbonEmitterObject.java new file mode 100644 index 0000000..ef18c46 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/RibbonEmitterObject.java @@ -0,0 +1,77 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.etheller.warsmash.viewer5.handlers.EmitterObject; +import com.hiveworkshop.rms.parsers.mdlx.AnimationMap; +import com.hiveworkshop.rms.parsers.mdlx.MdlxRibbonEmitter; + +public class RibbonEmitterObject extends GenericObject implements EmitterObject { + public Layer layer; + public float heightAbove; + public float heightBelow; + public float alpha; + public float[] color; + public float lifeSpan; + public long textureSlot; + public long emissionRate; + public float gravity; + public long columns; + public long rows; + /** + * Even if the internal texture isn't loaded, it's fine to run emitters based on + * this emitter object. + * + * The ribbons will simply be black. + */ + public boolean ok = true; + + public RibbonEmitterObject(final MdxModel model, final MdlxRibbonEmitter emitter, final int index) { + super(model, emitter, index); + + this.layer = model.getMaterials().get(emitter.getMaterialId()).layers.get(0); + this.heightAbove = emitter.getHeightAbove(); + this.heightBelow = emitter.getHeightBelow(); + this.alpha = emitter.getAlpha(); + this.color = emitter.getColor(); + this.lifeSpan = emitter.getLifeSpan(); + this.textureSlot = emitter.getTextureSlot(); + this.emissionRate = emitter.getEmissionRate(); + this.gravity = emitter.getGravity(); + this.columns = emitter.getColumns(); + this.rows = emitter.getRows(); + } + + public int getHeightBelow(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KRHB.getWar3id(), sequence, frame, counter, this.heightBelow); + } + + public int getHeightAbove(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KRHA.getWar3id(), sequence, frame, counter, this.heightAbove); + } + + public int getTextureSlot(final long[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KRTX.getWar3id(), sequence, frame, counter, 0); + } + + public int getColor(final float[] out, final int sequence, final int frame, final int counter) { + return this.getVectorValue(out, AnimationMap.KRCO.getWar3id(), sequence, frame, counter, this.color); + } + + public int getAlpha(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KRAL.getWar3id(), sequence, frame, counter, this.alpha); + } + + @Override + public int getVisibility(final float[] out, final int sequence, final int frame, final int counter) { + return this.getScalarValue(out, AnimationMap.KRVS.getWar3id(), sequence, frame, counter, 1f); + } + + @Override + public int getGeometryEmitterType() { + return GeometryEmitterFuncs.EMITTER_RIBBON; + } + + @Override + public boolean ok() { + return this.ok; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ScalarSd.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ScalarSd.java new file mode 100644 index 0000000..5ea5036 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/ScalarSd.java @@ -0,0 +1,46 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.etheller.warsmash.util.RenderMathUtils; +import com.hiveworkshop.rms.parsers.mdlx.timeline.MdlxTimeline; + +public class ScalarSd extends Sd { + + public ScalarSd(final MdxModel model, final MdlxTimeline timeline) { + super(model, timeline, SdArrayDescriptor.FLOAT_ARRAY); + } + + @Override + protected float[] convertDefaultValue(final float[] defaultValue) { + return defaultValue; + } + + @Override + protected void copy(final float[] out, final float[] value) { + out[0] = value[0]; + } + + @Override + protected void interpolate(final float[] out, final float[][] values, final float[][] inTans, + final float[][] outTans, final int start, final int end, final float t) { + final float startValue = values[start][0]; + + switch (this.interpolationType) { + case 0: + out[0] = startValue; + break; + case 1: + out[0] = RenderMathUtils.lerp(startValue, values[end][0], t); + break; + case 2: + out[0] = RenderMathUtils.hermite(startValue, (start < outTans.length) ? outTans[start][0] : 0f, + (start < inTans.length) ? inTans[end][0] : 0f, values[end][0], t); + break; + case 3: + out[0] = RenderMathUtils.bezier(startValue, (start < outTans.length) ? outTans[start][0] : 0f, + (start < inTans.length) ? inTans[end][0] : 0f, values[end][0], t); + break; + } + + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Sd.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Sd.java new file mode 100644 index 0000000..baf09f6 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Sd.java @@ -0,0 +1,149 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.etheller.warsmash.util.War3ID; +import com.hiveworkshop.rms.parsers.mdlx.timeline.MdlxTimeline; + +public abstract class Sd { + public MdxModel model; + public int interpolationType; + public War3ID name; + public TYPE defval; + public SdSequence globalSequence; + public List> sequences; + + public static Map forcedInterpMap = new HashMap<>(); + + static { + forcedInterpMap.put(War3ID.fromString("KLAV"), 0); + forcedInterpMap.put(War3ID.fromString("KATV"), 0); + forcedInterpMap.put(War3ID.fromString("KPEV"), 0); + forcedInterpMap.put(War3ID.fromString("KP2V"), 0); + forcedInterpMap.put(War3ID.fromString("KRVS"), 0); + } + + public static Map defVals = new HashMap<>(); + + static { + // LAYS + defVals.put(War3ID.fromString("KMTF"), new float[] { 0 }); + defVals.put(War3ID.fromString("KMTA"), new float[] { 1 }); + // TXAN + defVals.put(War3ID.fromString("KTAT"), new float[] { 0, 0, 0 }); + defVals.put(War3ID.fromString("KTAR"), new float[] { 0, 0, 0, 1 }); + defVals.put(War3ID.fromString("KTAS"), new float[] { 1, 1, 1 }); + // GEOA + defVals.put(War3ID.fromString("KGAO"), new float[] { 1 }); + defVals.put(War3ID.fromString("KGAC"), new float[] { 0, 0, 0 }); + // LITE + defVals.put(War3ID.fromString("KLAS"), new float[] { 0 }); + defVals.put(War3ID.fromString("KLAE"), new float[] { 0 }); + defVals.put(War3ID.fromString("KLAC"), new float[] { 0, 0, 0 }); + defVals.put(War3ID.fromString("KLAI"), new float[] { 0 }); + defVals.put(War3ID.fromString("KLBI"), new float[] { 0 }); + defVals.put(War3ID.fromString("KLBC"), new float[] { 0, 0, 0 }); + defVals.put(War3ID.fromString("KLAV"), new float[] { 1 }); + // ATCH + defVals.put(War3ID.fromString("KATV"), new float[] { 1 }); + // PREM + defVals.put(War3ID.fromString("KPEE"), new float[] { 0 }); + defVals.put(War3ID.fromString("KPEG"), new float[] { 0 }); + defVals.put(War3ID.fromString("KPLN"), new float[] { 0 }); + defVals.put(War3ID.fromString("KPLT"), new float[] { 0 }); + defVals.put(War3ID.fromString("KPEL"), new float[] { 0 }); + defVals.put(War3ID.fromString("KPES"), new float[] { 0 }); + defVals.put(War3ID.fromString("KPEV"), new float[] { 1 }); + // PRE2 + defVals.put(War3ID.fromString("KP2S"), new float[] { 0 }); + defVals.put(War3ID.fromString("KP2R"), new float[] { 0 }); + defVals.put(War3ID.fromString("KP2L"), new float[] { 0 }); + defVals.put(War3ID.fromString("KP2G"), new float[] { 0 }); + defVals.put(War3ID.fromString("KP2E"), new float[] { 0 }); + defVals.put(War3ID.fromString("KP2N"), new float[] { 0 }); + defVals.put(War3ID.fromString("KP2W"), new float[] { 0 }); + defVals.put(War3ID.fromString("KP2V"), new float[] { 1 }); + // RIBB + defVals.put(War3ID.fromString("KRHA"), new float[] { 0 }); + defVals.put(War3ID.fromString("KRHB"), new float[] { 0 }); + defVals.put(War3ID.fromString("KRAL"), new float[] { 1 }); + defVals.put(War3ID.fromString("KRCO"), new float[] { 0, 0, 0 }); + defVals.put(War3ID.fromString("KRTX"), new float[] { 0 }); + defVals.put(War3ID.fromString("KRVS"), new float[] { 1 }); + // CAMS + defVals.put(War3ID.fromString("KCTR"), new float[] { 0, 0, 0 }); + defVals.put(War3ID.fromString("KTTR"), new float[] { 0, 0, 0 }); + defVals.put(War3ID.fromString("KCRL"), new float[] { 0 }); + // NODE + defVals.put(War3ID.fromString("KGTR"), new float[] { 0, 0, 0 }); + defVals.put(War3ID.fromString("KGRT"), new float[] { 0, 0, 0, 1 }); + defVals.put(War3ID.fromString("KGSC"), new float[] { 1, 1, 1 }); + + } + + public Sd(final MdxModel model, final MdlxTimeline timeline, final SdArrayDescriptor arrayDescriptor) { + final List globalSequences = model.getGlobalSequences(); + final int globalSequenceId = timeline.getGlobalSequenceId(); + final Integer forcedInterp = forcedInterpMap.get(timeline.getName()); + + this.model = model; + this.name = timeline.getName(); + this.defval = convertDefaultValue(defVals.get(timeline.getName())); + this.globalSequence = null; + this.sequences = new ArrayList<>(); + + // Allow to force an interpolation type. + // The game seems to do this with visibility tracks, where the type is + // forced to None. + // It came up as a bug report by a user who used the wrong interpolation + // type. + this.interpolationType = forcedInterp != null ? forcedInterp : timeline.getInterpolationType().ordinal(); + + if ((globalSequenceId != -1) && (globalSequences.size() > 0)) { + this.globalSequence = new SdSequence(this, 0, globalSequences.get(globalSequenceId).longValue(), + timeline, true, arrayDescriptor); + } + else { + for (final Sequence sequence : model.getSequences()) { + final long[] interval = sequence.getInterval(); + + this.sequences + .add(new SdSequence(this, interval[0], interval[1], timeline, false, arrayDescriptor)); + } + } + } + + public int getValue(final TYPE out, final int sequence, final int frame, final int counter) { + if (this.globalSequence != null) { + return this.globalSequence.getValue(out, + this.globalSequence.end == 0 ? 0 : counter % this.globalSequence.end); + } + else if ((sequence != -1) && (this.sequences.size() > sequence)) { + return this.sequences.get(sequence).getValue(out, frame); + } + else { + this.copy(out, this.defval); + + return -1; + } + } + + public boolean isVariant(final int sequence) { + if (this.globalSequence != null) { + return !this.globalSequence.constant; + } + else { + return !this.sequences.get(sequence).constant; + } + } + + protected abstract TYPE convertDefaultValue(float[] defaultValue); + + protected abstract void copy(TYPE out, TYPE value); + + protected abstract void interpolate(TYPE out, TYPE[] values, TYPE[] inTans, TYPE[] outTans, int start, int end, + float t); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SdArrayDescriptor.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SdArrayDescriptor.java new file mode 100644 index 0000000..ff20f4b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SdArrayDescriptor.java @@ -0,0 +1,24 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +public interface SdArrayDescriptor { + SdArrayDescriptor GENERIC = new SdArrayDescriptor() { + @Override + public Object[] create(final int size) { + return new Object[size]; + } + }; + SdArrayDescriptor FLOAT_ARRAY = new SdArrayDescriptor() { + @Override + public float[][] create(final int size) { + return new float[size][]; + } + }; + SdArrayDescriptor LONG_ARRAY = new SdArrayDescriptor() { + @Override + public long[][] create(final int size) { + return new long[size][]; + } + }; + + TYPE[] create(int size); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SdSequence.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SdSequence.java new file mode 100644 index 0000000..5ec445c --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SdSequence.java @@ -0,0 +1,224 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.util.ArrayList; +import java.util.Arrays; + +import com.etheller.warsmash.util.ParseUtils; +import com.hiveworkshop.rms.parsers.mdlx.AnimationMap; +import com.hiveworkshop.rms.parsers.mdlx.timeline.MdlxTimeline; + +public final class SdSequence { + private static boolean INJECT_FRAMES_GHOSTWOLF_STYLE = false; + + private final Sd sd; + public final long start; // UInt32 + public final long end; // UInt32 + public long[] frames; + public TYPE[] values; + public TYPE[] inTans; + public TYPE[] outTans; + public boolean constant; + + public SdSequence(final Sd sd, final long start, final long end, final MdlxTimeline timeline, + final boolean isGlobalSequence, final SdArrayDescriptor arrayDescriptor) { + this.sd = sd; + this.start = start; + this.end = end; + final ArrayList framesBuilder = new ArrayList<>(); + final ArrayList valuesBuilder = new ArrayList<>(); + final ArrayList inTansBuilder = new ArrayList<>(); + final ArrayList outTansBuilder = new ArrayList<>(); + this.constant = false; + + final int interpolationType = sd.interpolationType; + final long[] frames = timeline.getFrames(); + final TYPE[] values = getValues(timeline); + final TYPE[] inTans = getInTans(timeline); + final TYPE[] outTans = getOutTans(timeline); + final TYPE defval = sd.defval; + + // When using a global sequence, where the first key is outside of the + // sequence's length, it becomes its constant value. + // When having one key in the sequence's range, and one key outside of + // it, results seem to be non-deterministic. + // Sometimes the second key is used too, sometimes not. + // It also differs depending where the model is viewed - the WE + // previewer, the WE itself, or the game. + // All three show different results, none of them make sense. + // Therefore, only handle the case where the first key is outside. + // This fixes problems spread over many models, e.g. HeroMountainKing + // (compare in WE and in Magos). + if (isGlobalSequence && (frames.length > 0) && (frames[0] > end)) { + if (start == end) { + framesBuilder.add(start); + } + else { + framesBuilder.add(frames[0]); + } + valuesBuilder.add(values[0]); + } + + // Go over the keyframes, and add all of the ones that are in this + // sequence (start <= frame <= end). + for (int i = 0, l = frames.length; i < l; i++) { + final long frame = frames[i]; + + if ((frame >= start) && (frame <= end)) { + framesBuilder.add(frame); + valuesBuilder.add(values[i]); + + if (interpolationType > 1) { + inTansBuilder.add(inTans[i]); + outTansBuilder.add(outTans[i]); + } + } + } + + final int keyframeCount = framesBuilder.size(); + + if (keyframeCount == 0) { + // if there are no keys, use the default value directly. + this.constant = true; + framesBuilder.add(start); + valuesBuilder.add(defval); + } + else if (keyframeCount == 1) { + // If there's only one key, use it directly + this.constant = true; + } + else { + final TYPE firstValue = valuesBuilder.get(0); + + // If all of the values in this sequence are the same, might as well + // make it constant. + boolean allFramesMatch = true; + for (final TYPE value : valuesBuilder) { + if (!equals(firstValue, value)) { + allFramesMatch = false; + } + } + this.constant = allFramesMatch; + + if (!this.constant && INJECT_FRAMES_GHOSTWOLF_STYLE) { + // If there is no opening keyframe for this sequence, inject one + // with the default value. + final boolean hasStart = framesBuilder.get(0) == start; + if (!hasStart) { + framesBuilder.add(start); + valuesBuilder.add(defval); + + if (interpolationType > 1) { + inTansBuilder.add(defval); + outTansBuilder.add(defval); + } + } + + // If there is no closing keyframe for this sequence, inject one + // with the default value. + if (framesBuilder.get(framesBuilder.size() - 1) != end) { + framesBuilder.add(end); + final int sourceIndex = hasStart ? 0 : (valuesBuilder.size() - 1); + valuesBuilder.add(valuesBuilder.get(sourceIndex)); + + if (interpolationType > 1) { + inTansBuilder.add(inTansBuilder.get(sourceIndex)); + outTansBuilder.add(outTansBuilder.get(sourceIndex)); + } + } + } + } + this.frames = new long[framesBuilder.size()]; + for (int i = 0; i < framesBuilder.size(); i++) { + this.frames[i] = framesBuilder.get(i); + } + this.values = valuesBuilder.toArray(arrayDescriptor.create(valuesBuilder.size())); + this.inTans = inTansBuilder.toArray(arrayDescriptor.create(inTansBuilder.size())); + this.outTans = outTansBuilder.toArray(arrayDescriptor.create(outTansBuilder.size())); + } + + private TYPE[] getValues(final MdlxTimeline timeline) { + final TYPE[] values = timeline.getValues(); + return fixTimelineArray(timeline, values); + } + + private TYPE[] getOutTans(final MdlxTimeline timeline) { + final TYPE[] outTans = timeline.getOutTans(); + return fixTimelineArray(timeline, outTans); + } + + private TYPE[] getInTans(final MdlxTimeline timeline) { + final TYPE[] inTans = timeline.getInTans(); + return fixTimelineArray(timeline, inTans); + } + + private TYPE[] fixTimelineArray(final MdlxTimeline timeline, final TYPE[] values) { + if (values == null) { + return null; + } + if (timeline.getName().equals(AnimationMap.KLAC.getWar3id()) + || timeline.getName().equals(AnimationMap.KLBC.getWar3id())) { + final float[][] flippedColorData = new float[values.length][3]; + for (int i = 0; i < values.length; i++) { + flippedColorData[i] = ParseUtils.newFlippedRGB((float[]) values[i]); + } + return (TYPE[]) flippedColorData; + } + return values; + } + + public int getValue(final TYPE out, final long frame) { + final int length = this.frames.length; + + if (this.constant || (frame < this.start)) { + this.sd.copy(out, this.values[0]); + + return -1; + } + else { + int startFrameIndex = -1; + int endFrameIndex = -1; + final int lengthLessOne = length - 1; + if ((frame < this.frames[0]) || (frame >= this.frames[lengthLessOne])) { + startFrameIndex = lengthLessOne; + endFrameIndex = 0; + } + else { + for (int i = 1; i < length; i++) { + if (this.frames[i] > frame) { + startFrameIndex = i - 1; + endFrameIndex = i; + break; + } + } + } + long startFrame = this.frames[startFrameIndex]; + final long endFrame = this.frames[endFrameIndex]; + long timeBetweenFrames = endFrame - startFrame; + if (timeBetweenFrames < 0) { + timeBetweenFrames += (this.end - this.start); + if (frame < startFrame) { + startFrame = endFrame; + } + } + final float t = ((timeBetweenFrames) == 0 ? 0 : ((frame - startFrame) / (float) (timeBetweenFrames))); + this.sd.interpolate(out, this.values, this.inTans, this.outTans, startFrameIndex, endFrameIndex, t); + return startFrameIndex; + } + } + + protected final boolean equals(final TYPE a, final TYPE b) { + if ((a instanceof Float) && (b instanceof Float)) { + return a.equals(b); + } + else if ((a instanceof Long) && (b instanceof Long)) { + return a.equals(b); + } + else if ((a instanceof float[]) && (b instanceof float[])) { + return Arrays.equals(((float[]) a), (float[]) b); + } + else if ((a instanceof long[]) && (b instanceof long[])) { + return Arrays.equals(((long[]) a), (long[]) b); + } + return false; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Sequence.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Sequence.java new file mode 100644 index 0000000..d3b916a --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/Sequence.java @@ -0,0 +1,87 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.Bounds; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.SecondaryTag; +import com.hiveworkshop.rms.parsers.mdlx.MdlxExtent; +import com.hiveworkshop.rms.parsers.mdlx.MdlxSequence; + +public class Sequence { + private final MdlxSequence sequence; + private final Bounds bounds; + private final EnumSet primaryTags = EnumSet.noneOf(AnimationTokens.PrimaryTag.class); + private final EnumSet secondaryTags = EnumSet + .noneOf(AnimationTokens.SecondaryTag.class); + + public Sequence(final MdlxSequence sequence) { + this.sequence = sequence; + this.bounds = new Bounds(); + final MdlxExtent sequenceExtent = sequence.getExtent(); + this.bounds.fromExtents(sequenceExtent.getMin(), sequenceExtent.getMax(), sequenceExtent.getBoundsRadius()); + populateTags(); + } + + private void populateTags() { + this.primaryTags.clear(); + this.secondaryTags.clear(); + TokenLoop: for (final String token : this.sequence.name.split("\\s+")) { + final String upperCaseToken = token.toUpperCase(); + for (final PrimaryTag primaryTag : PrimaryTag.values()) { + if (upperCaseToken.equals(primaryTag.name())) { + this.primaryTags.add(primaryTag); + continue TokenLoop; + } + } + for (final SecondaryTag secondaryTag : SecondaryTag.values()) { + if (upperCaseToken.equals(secondaryTag.name())) { + this.secondaryTags.add(secondaryTag); + continue TokenLoop; + } + } + break; + } + } + + public String getName() { + return this.sequence.getName(); + } + + public long[] getInterval() { + return this.sequence.getInterval(); + } + + public float getMoveSpeed() { + return this.sequence.getMoveSpeed(); + } + + public int getFlags() { + return this.sequence.getFlags(); + } + + public float getRarity() { + return this.sequence.getRarity(); + } + + public long getSyncPoint() { + return this.sequence.getSyncPoint(); + } + + public Bounds getBounds() { + return this.bounds; + } + + public MdlxExtent getExtent() { + return this.sequence.getExtent(); + } + + public EnumSet getPrimaryTags() { + return this.primaryTags; + } + + public EnumSet getSecondaryTags() { + return this.secondaryTags; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SequenceLoopMode.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SequenceLoopMode.java new file mode 100644 index 0000000..16c249a --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SequenceLoopMode.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +public enum SequenceLoopMode { + NEVER_LOOP, + MODEL_LOOP, + ALWAYS_LOOP, + NEVER_LOOP_AND_HIDE_WHEN_DONE, // used by spawned effects + LOOP_TO_NEXT_ANIMATION; // used by the Arthas vs Illidan tech demo +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SetupGeosets.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SetupGeosets.java new file mode 100644 index 0000000..76485b2 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SetupGeosets.java @@ -0,0 +1,208 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.badlogic.gdx.graphics.GL20; +import com.etheller.warsmash.util.RenderMathUtils; +import com.hiveworkshop.rms.parsers.mdlx.MdlxGeoset; + +public class SetupGeosets { + private static final int NORMAL_BATCH = 0; + private static final int EXTENDED_BATCH = 1; + private static final int REFORGED_BATCH = 2; + + public static void setupGeosets(final MdxModel model, final List geosets, final boolean bigNodeSpace) { + if (geosets.size() > 0) { + final GL20 gl = model.viewer.gl; + int positionBytes = 0; + int normalBytes = 0; + int uvBytes = 0; + int skinBytes = 0; + int faceBytes = 0; + final int[] batchTypes = new int[geosets.size()]; + + final int extendedBatchStride = bigNodeSpace ? 36 : 9; + final int normalBatchStride = bigNodeSpace ? 20 : 5; + final int openGLSkinType = bigNodeSpace ? GL20.GL_UNSIGNED_INT : GL20.GL_UNSIGNED_BYTE; + final int normalBatchBoneCountOffsetBytes = bigNodeSpace ? 16 : 4; + final int extendedBatchBoneCountOffsetBytes = bigNodeSpace ? 32 : 8; + + for (int i = 0, l = geosets.size(); i < l; i++) { + final MdlxGeoset geoset = geosets.get(i); + + if (true /* geoset.getLod() == 0 */) { + final int vertices = geoset.getVertices().length / 3; + + positionBytes += vertices * 12; + normalBytes += vertices * 12; + uvBytes += geoset.getUvSets().length * vertices * 8; + + if (false /* geoset.skin.length */) { + skinBytes += vertices * 8; + + batchTypes[i] = REFORGED_BATCH; + } + else { + long biggestGroup = 0; + + for (final long group : geoset.getMatrixGroups()) { + if (group > biggestGroup) { + biggestGroup = group; + } + } + + if (biggestGroup > 4) { + skinBytes += vertices * extendedBatchStride; + + batchTypes[i] = EXTENDED_BATCH; + } + else { + skinBytes += vertices * normalBatchStride; + + batchTypes[i] = NORMAL_BATCH; + } + } + + faceBytes += geoset.getFaces().length * 2; + } + } + + int positionOffset = 0; + int normalOffset = positionOffset + positionBytes; + int uvOffset = normalOffset + normalBytes; + int skinOffset = uvOffset + uvBytes; + int faceOffset = 0; + + model.arrayBuffer = gl.glGenBuffer(); + gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, model.arrayBuffer); + gl.glBufferData(GL20.GL_ARRAY_BUFFER, skinOffset + skinBytes, null, GL20.GL_STATIC_DRAW); + + model.elementBuffer = gl.glGenBuffer(); + gl.glBindBuffer(GL20.GL_ELEMENT_ARRAY_BUFFER, model.elementBuffer); + gl.glBufferData(GL20.GL_ELEMENT_ARRAY_BUFFER, faceBytes, null, GL20.GL_STATIC_DRAW); + + for (int i = 0, l = geosets.size(); i < l; i++) { + final MdlxGeoset geoset = geosets.get(i); + + final int batchType = batchTypes[i]; + if (true /* geoset.lod == 0 */) { + final float[] positions = geoset.getVertices(); + final float[] normals = geoset.getNormals(); + final float[][] uvSets = geoset.getUvSets(); + final int[] faces = geoset.getFaces(); + int[] skin = null; + final int vertices = geoset.getVertices().length / 3; + + int maxBones; + int skinStride; + int boneCountOffsetBytes; + if (batchType == EXTENDED_BATCH) { + maxBones = 8; + skinStride = extendedBatchStride; + boneCountOffsetBytes = extendedBatchBoneCountOffsetBytes; + } + else { + maxBones = 4; + skinStride = normalBatchStride; + boneCountOffsetBytes = normalBatchBoneCountOffsetBytes; + } + + if (batchType == REFORGED_BATCH) { + // skin = geoset.skin; // THIS IS NOT IMPLEMENTED + } + else { + final long[] matrixIndices = geoset.getMatrixIndices(); + final short[] vertexGroups = geoset.getVertexGroups(); + final List matrixGroups = new ArrayList<>(); + int offset = 0; + // Normally the shader supports up to 4 bones per vertex. + // This is enough for almost every existing Warcraft 3 model. + // That being said, there are a few models with geosets that need more, for + // example the Water Elemental. + // These geosets use a different shader, which support up to 8 bones per vertex. + + skin = new int[vertices * (maxBones + 1)]; + + // Slice the matrix groups + for (final long size : geoset.getMatrixGroups()) { + matrixGroups.add(Arrays.copyOfRange(matrixIndices, offset, (int) (offset + size))); + offset += size; + } + + // Parse the skinning. + for (int si = 0; si < vertices; si++) { + final short vertexGroup = vertexGroups[si]; + final long[] matrixGroup = (vertexGroup >= matrixGroups.size()) ? null + : matrixGroups.get(vertexGroup); + + offset = si * (maxBones + 1); + + // Somehow in some bad models a vertex group index refers to an invalid matrix + // group. + // Such models are still loaded by the game. + if (matrixGroup != null) { + final int bones = Math.min(matrixGroup.length, maxBones); + + for (int j = 0; j < bones; j++) { + skin[offset + j] = (int) (matrixGroup[j] + 1); // 1 is added to diffrentiate + // between matrix 0, and no matrix. + } + + skin[offset + maxBones] = bones; + } + } + } + + final boolean unselectable = geoset.getSelectionFlags() == 4; + final Geoset vGeoset = new Geoset(model, model.getGeosets().size(), positionOffset, normalOffset, + uvOffset, skinOffset, faceOffset, vertices, faces.length, openGLSkinType, skinStride, + boneCountOffsetBytes, unselectable, geoset); + + model.getGeosets().add(vGeoset); + + if (batchType == REFORGED_BATCH) { + throw new UnsupportedOperationException("NYI"); +// model.batches.add(new Reforged) + } + else { + final boolean isExtended = batchType == EXTENDED_BATCH; + + for (final Layer layer : model.getMaterials().get((int) geoset.getMaterialId()).layers) { + model.batches.add(new Batch(model.batches.size(), vGeoset, layer, isExtended)); + } + } + + // Positions. + gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, positionOffset, positions.length, + RenderMathUtils.wrap(positions)); + positionOffset += positions.length * 4; + + // Normals. + gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, normalOffset, normals.length, + RenderMathUtils.wrap(normals)); + normalOffset += normals.length * 4; + + // Texture coordinates. + for (final float[] uvSet : uvSets) { + gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, uvOffset, uvSet.length, RenderMathUtils.wrap(uvSet)); + uvOffset += uvSet.length * 4; + } + + // Skin. + gl.glBufferSubData(GL20.GL_ARRAY_BUFFER, skinOffset, skin.length, + bigNodeSpace ? RenderMathUtils.wrap(skin) : RenderMathUtils.wrapAsBytes(skin)); + skinOffset += skin.length * (bigNodeSpace ? 4 : 1); + + // Faces. + gl.glBufferSubData(GL20.GL_ELEMENT_ARRAY_BUFFER, faceOffset, faces.length, + RenderMathUtils.wrapFaces(faces)); + faceOffset += faces.length * 2; + } + + } + } + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SetupGroups.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SetupGroups.java new file mode 100644 index 0000000..58f0bca --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SetupGroups.java @@ -0,0 +1,123 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import com.etheller.warsmash.viewer5.handlers.EmitterObject; + +public class SetupGroups { + public static int getPrio(final Batch object) { + return object.layer.priorityPlane; + } + + public static int getPrio(final ParticleEmitter2Object object) { + return object.priorityPlane; + } + + public static int getPrio(final RibbonEmitterObject object) { + return object.layer.priorityPlane; + } + + public static int getPrio(final Object object) { + if (object instanceof Batch) { + return getPrio((Batch) object); + } + else if (object instanceof RibbonEmitterObject) { + return getPrio((RibbonEmitterObject) object); + } + else if (object instanceof ParticleEmitter2Object) { + return getPrio((ParticleEmitter2Object) object); + } + else { + throw new IllegalArgumentException(object.getClass().getName()); + } + } + + public static boolean matchingGroup(final Object group, final Object object) { + if (group instanceof BatchGroup) { + return (object instanceof Batch) && (((Batch) object).isExtended == ((BatchGroup) group).isExtended); +// } else if(group instanceof ReforgedBatch) { TODO +// return (object instanceof ReforgedBatch) && (object.material.shader === group.shader); + } + else { + // All of the emitter objects are generic objects. + return (object instanceof GenericObject); + } + } + + public static GenericGroup createMatchingGroup(final MdxModel model, final Object object) { + if (object instanceof Batch) { + return new BatchGroup(model, ((Batch) object).isExtended); +// } else if(object instanceof ReforgedBatch) { TODO +// return new ReforgedBatchGroup(model, ((ReforgedBatch)object).material.shader); + } + else { + return new EmitterGroup(model); + } + } + + public static void setupGroups(final MdxModel model) { + final List opaqueBatches = new ArrayList<>(); + final List translucentBatches = new ArrayList<>(); + + for (final Batch batch : model.batches) {// TODO reforged + if (/* batch instanceof ReforgedBatch || */batch.layer.filterMode < 2) { + opaqueBatches.add(batch); + } + else { + translucentBatches.add(batch); + } + } + + final List opaqueGroups = model.opaqueGroups; + final List translucentGroups = model.translucentGroups; + GenericGroup currentGroup = null; + + for (final Batch object : opaqueBatches) { + if ((currentGroup == null) || !matchingGroup(currentGroup, object)) { + currentGroup = createMatchingGroup(model, object); + + opaqueGroups.add(currentGroup); + } + + final int index = object.index; + currentGroup.objects.add(index); + } + + // Sort between all of the translucent batches and emitters that have priority + // planes + final List sorted = new ArrayList<>(); + sorted.addAll(translucentBatches); + sorted.addAll(model.particleEmitters2); + sorted.addAll(model.ribbonEmitters); + Collections.sort(sorted, new Comparator() { + @Override + public int compare(final Object o1, final Object o2) { + return getPrio(o1) - getPrio(o2); + } + }); + + // Event objects have no priority planes, so they might as well always be last. + final List objects = new ArrayList<>(); + objects.addAll(sorted); + objects.addAll(model.eventObjects); + + currentGroup = null; + + for (final Object object : objects) { // TODO reforged + if ((object instanceof Batch /* || object instanceof ReforgedBatch */) + || (object instanceof EmitterObject)) { + if ((currentGroup == null) || !matchingGroup(currentGroup, object)) { + currentGroup = createMatchingGroup(model, object); + + translucentGroups.add(currentGroup); + } + + final int index = ((GenericIndexed) object).getIndex(); + currentGroup.objects.add(index); + } + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SetupSimpleGroups.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SetupSimpleGroups.java new file mode 100644 index 0000000..ebb45ce --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/SetupSimpleGroups.java @@ -0,0 +1,62 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import java.util.List; + +public class SetupSimpleGroups { + private static final float[] alphaHeap = new float[1]; + + public static boolean isBatchSimple(final Batch batch) { + final GeosetAnimation geosetAnimation = batch.geoset.geosetAnimation; + + if (geosetAnimation != null) { + geosetAnimation.getAlpha(alphaHeap, 0, 0, 0); + + if (alphaHeap[0] <= 0.01) { + return false; + } + } + + Layer layer; + + if (batch instanceof Batch) { + layer = batch.layer; + } + else { + throw new IllegalStateException("reforged?"); // TODO +// layer = batch.material.layers[0]; + } + + layer.getAlpha(alphaHeap, 0, 0, 0); + + if (alphaHeap[0] < 0.01) { + return false; + } + + return true; + } + + public static void setupSimpleGroups(final MdxModel model) { + final List batches = model.batches; + final List simpleGroups = model.simpleGroups; + + for (final GenericGroup group : model.opaqueGroups) { + GenericGroup simpleGroup; + + if (group instanceof BatchGroup) { + simpleGroup = new BatchGroup(model, ((BatchGroup) group).isExtended); + } + else { + throw new IllegalStateException("reforged?"); // TODO + // simpleGroup = new ReforgedBatchGroup(model, group.shader); + } + + for (final Integer object : group.objects) { + if (isBatchSimple(batches.get(object))) { + simpleGroup.objects.add(object); + } + } + + simpleGroups.add(simpleGroup); + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/TextureAnimation.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/TextureAnimation.java new file mode 100644 index 0000000..8e6fb2b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/TextureAnimation.java @@ -0,0 +1,44 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.etheller.warsmash.util.RenderMathUtils; +import com.hiveworkshop.rms.parsers.mdlx.AnimationMap; +import com.hiveworkshop.rms.parsers.mdlx.MdlxTextureAnimation; + +public class TextureAnimation extends AnimatedObject { + + public TextureAnimation(final MdxModel model, final MdlxTextureAnimation textureAnimation) { + super(model, textureAnimation); + + this.addVariants(AnimationMap.KTAT.getWar3id(), "translation"); + this.addVariants(AnimationMap.KTAR.getWar3id(), "rotation"); + this.addVariants(AnimationMap.KTAS.getWar3id(), "scale"); + } + + public int getTranslation(final float[] out, final int sequence, final int frame, final int counter) { + return this.getVectorValue(out, AnimationMap.KTAT.getWar3id(), sequence, frame, counter, + RenderMathUtils.FLOAT_VEC3_ZERO); + } + + public int getRotation(final float[] out, final int sequence, final int frame, final int counter) { + return this.getQuatValue(out, AnimationMap.KTAR.getWar3id(), sequence, frame, counter, + RenderMathUtils.FLOAT_QUAT_DEFAULT); + } + + public int getScale(final float[] out, final int sequence, final int frame, final int counter) { + return this.getVectorValue(out, AnimationMap.KTAS.getWar3id(), sequence, frame, counter, + RenderMathUtils.FLOAT_VEC3_ONE); + } + + public boolean isTranslationVariant(final int sequence) { + return this.isVariant(AnimationMap.KTAT.getWar3id(), sequence); + } + + public boolean isRotationVariant(final int sequence) { + return this.isVariant(AnimationMap.KTAR.getWar3id(), sequence); + } + + public boolean isScaleVariant(final int sequence) { + return this.isVariant(AnimationMap.KTAS.getWar3id(), sequence); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/UInt32Sd.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/UInt32Sd.java new file mode 100644 index 0000000..d5025f8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/UInt32Sd.java @@ -0,0 +1,50 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.etheller.warsmash.util.RenderMathUtils; +import com.hiveworkshop.rms.parsers.mdlx.timeline.MdlxTimeline; + +public class UInt32Sd extends Sd { + + public UInt32Sd(final MdxModel model, final MdlxTimeline timeline) { + super(model, timeline, SdArrayDescriptor.LONG_ARRAY); + } + + @Override + protected long[] convertDefaultValue(final float[] defaultValue) { + final long[] returnValue = new long[defaultValue.length]; + for (int i = 0; i < defaultValue.length; i++) { + returnValue[i] = (long) defaultValue[i]; + } + return returnValue; + } + + @Override + protected void copy(final long[] out, final long[] value) { + out[0] = value[0]; + } + + @Override + protected void interpolate(final long[] out, final long[][] values, final long[][] inTans, final long[][] outTans, + final int start, final int end, final float t) { + final long startValue = values[start][0]; + + switch (this.interpolationType) { + case 0: + out[0] = startValue; + break; + case 1: + out[0] = (long) RenderMathUtils.lerp(startValue, values[end][0], t); + break; + case 2: + out[0] = (long) RenderMathUtils.hermite(startValue, (start < outTans.length) ? outTans[start][0] : 0, + (start < inTans.length) ? inTans[end][0] : 0, values[end][0], t); + break; + case 3: + out[0] = (long) RenderMathUtils.bezier(startValue, (start < outTans.length) ? outTans[start][0] : 0, + (start < inTans.length) ? inTans[end][0] : 0, values[end][0], t); + break; + } + + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/mdx/VectorSd.java b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/VectorSd.java new file mode 100644 index 0000000..038a25e --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/mdx/VectorSd.java @@ -0,0 +1,32 @@ +package com.etheller.warsmash.viewer5.handlers.mdx; + +import com.etheller.warsmash.util.Interpolator; +import com.etheller.warsmash.util.RenderMathUtils; +import com.hiveworkshop.rms.parsers.mdlx.timeline.MdlxTimeline; + +public class VectorSd extends Sd { + + public VectorSd(final MdxModel model, final MdlxTimeline timeline) { + super(model, timeline, SdArrayDescriptor.FLOAT_ARRAY); + } + + @Override + protected float[] convertDefaultValue(final float[] defaultValue) { + return defaultValue; + } + + @Override + protected void copy(final float[] out, final float[] value) { + System.arraycopy(value, 0, out, 0, value.length); + } + + @Override + protected void interpolate(final float[] out, final float[][] values, final float[][] inTans, + final float[][] outTans, final int start, final int end, final float t) { + Interpolator.interpolateVector(out, values[start], + (start < outTans.length) ? outTans[start] : RenderMathUtils.EMPTY_FLOAT_ARRAY, + (start < outTans.length) ? inTans[end] : RenderMathUtils.EMPTY_FLOAT_ARRAY, values[end], t, + this.interpolationType); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/tga/ImageUtils.java b/core/src/com/etheller/warsmash/viewer5/handlers/tga/ImageUtils.java new file mode 100644 index 0000000..feebac6 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/tga/ImageUtils.java @@ -0,0 +1,179 @@ +package com.etheller.warsmash.viewer5.handlers.tga; + +import java.awt.AlphaComposite; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; + +public class ImageUtils { + + /** + * Takes an images as input and generates an array containing this image and all + * possible mipmaps + * + * @param input + * @return + */ + public static BufferedImage[] generateMipMaps(final BufferedImage input) { + int num = 0; + int curWidth = input.getWidth(); + int curHeight = input.getHeight(); + int pow; + do { + num++; + pow = (int) Math.pow(2.0D, num - 1); + } + while ((pow < curWidth) || (pow < curHeight)); + final BufferedImage[] result = new BufferedImage[num]; + result[0] = input; + for (int i = 1; i < num; i++) { + curWidth /= 2; + curHeight /= 2; + if (curHeight == 0) { + curHeight = 1; + } + if (curWidth == 0) { + curWidth = 1; + } + result[i] = ImageUtils.getScaledInstance(result[(i - 1)], curWidth, curHeight, + RenderingHints.VALUE_INTERPOLATION_BICUBIC, true); + } + return result; + } + + /** + * Scales an Image + * + * @param img + * @param targetWidth + * @param targetHeight + * @param hint Rendering Hint + * @param higherQuality + * @return + */ + public static BufferedImage getScaledInstance(final BufferedImage img, final int targetWidth, + final int targetHeight, final Object hint, final boolean higherQuality) { + final int type = img.getTransparency() == 1 ? 1 : 2; + BufferedImage ret = img; + int h; + int w; + if (higherQuality) { + w = img.getWidth(); + h = img.getHeight(); + } + else { + w = targetWidth; + h = targetHeight; + } + do { + if ((higherQuality) && (w > targetWidth)) { + w /= 2; + if (w < targetWidth) { + w = targetWidth; + } + } + if ((higherQuality) && (h > targetHeight)) { + h /= 2; + if (h < targetHeight) { + h = targetHeight; + } + } + + BufferedImage tmp; + if (img.getColorModel().hasAlpha() == false) { + tmp = new BufferedImage(w, h, type); + final Graphics2D g2 = tmp.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint); + g2.drawImage(ret, 0, 0, w, h, null); + g2.dispose(); + } + else { + // Necessary because otherwise Bilinear resize would couse transparent pixel to + // change color + tmp = resizeWorkAround(ret, w, h, hint); + } + + ret = tmp; + } + while ((w != targetWidth) || (h != targetHeight)); + return ret; + } + + private static BufferedImage resizeWorkAround(final BufferedImage ret, final int w, final int h, + final Object hint) { + + final BufferedImage noAlpha = new BufferedImage(ret.getWidth(), ret.getHeight(), BufferedImage.TYPE_INT_ARGB); + + for (int x = 0; x < ret.getWidth(); x++) { + for (int y = 0; y < ret.getHeight(); y++) { + int color = ret.getRGB(x, y); + color = color | 0xff000000; + noAlpha.setRGB(x, y, color); + } + } + + final BufferedImage noAlphaSmall = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = noAlphaSmall.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint); + g2.drawImage(noAlpha, 0, 0, w, h, null); + g2.dispose(); + + final BufferedImage tmp = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + g2 = tmp.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint); + g2.drawImage(ret, 0, 0, w, h, null); + g2.dispose(); + + noAlphaSmall.getAlphaRaster().setRect(0, 0, tmp.getAlphaRaster()); + + return noAlphaSmall; + } + + /** + * An alternative way to convert an image to type_byte_indexed (paletted) that + * avoids dithering. + * + * @param src + * @return + */ + public static BufferedImage antiDitherConvert(final BufferedImage src) { + final BufferedImage convertedImage = new BufferedImage(src.getWidth(), src.getHeight(), + BufferedImage.TYPE_BYTE_INDEXED); + for (int x = 0; x < src.getWidth(); x++) { + for (int y = 0; y < src.getHeight(); y++) { + convertedImage.setRGB(x, y, src.getRGB(x, y)); + } + } + return convertedImage; + } + + public static BufferedImage changeImageType(final BufferedImage src, final int type) { + final BufferedImage img = new BufferedImage(src.getWidth(), src.getHeight(), type); + final Graphics2D g = (Graphics2D) img.getGraphics(); + + if (img.getColorModel().hasAlpha()) { + final Composite comp = AlphaComposite.getInstance(AlphaComposite.SRC); + g.setComposite(comp); + } + + g.drawImage(src, 0, 0, null); + g.dispose(); + + return img; + } + + public static BufferedImage convertStandardImageType(final BufferedImage src, final boolean useAlpha) { + + if (useAlpha && (src.getType() == BufferedImage.TYPE_INT_ARGB)) { + return src; + } + + if ((useAlpha == false) && (src.getType() == BufferedImage.TYPE_INT_RGB)) { + return src; + } + + return ImageUtils.changeImageType(src, useAlpha ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB); + + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/tga/TgaFile.java b/core/src/com/etheller/warsmash/viewer5/handlers/tga/TgaFile.java new file mode 100644 index 0000000..8ac69dc --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/tga/TgaFile.java @@ -0,0 +1,217 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.etheller.warsmash.viewer5.handlers.tga; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferInt; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; + +/** + * + * @author Riven, modified by Oger-Lord + */ +public class TgaFile { + + /** + * Read a TGA image from a file + * + * @param file + * @return + * @throws FileNotFoundException + * @throws IOException + */ + public static BufferedImage readTGA(final File file) throws FileNotFoundException, IOException { + return readTGA(file.getName(), new FileInputStream(file)); + + } + + /** + * Read a TGA image from an input stream. + * + * @param name + * @param stream + * @return + * @throws IOException + */ + public static BufferedImage readTGA(final String name, final InputStream stream) throws IOException { + + // Read Header + final byte[] header = new byte[18]; + stream.read(header); + + // Read pixel data + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int nRead; + byte[] data = new byte[16384]; + + while ((nRead = stream.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + buffer.flush(); + data = buffer.toByteArray(); + + // Verify Header + if ((header[0] | header[1]) != 0) { + throw new IllegalStateException("Error " + name); + } + if (header[2] != 2) { + throw new IllegalStateException("Error " + name); + } + int w = 0, h = 0; + w |= (header[12] & 0xFF) << 0; + w |= (header[13] & 0xFF) << 8; + h |= (header[14] & 0xFF) << 0; + h |= (header[15] & 0xFF) << 8; + + boolean alpha; + + if ((header[16] == 24)) { + alpha = false; + } + else if (header[16] == 32) { + alpha = true; + } + else { + throw new IllegalStateException("Error " + name + " invalid pixel depth: " + header[16]); + } + + if ((header[17] & 15) != (alpha ? 8 : 0)) { + throw new IllegalStateException("Error " + name); + } + + final BufferedImage dst = new BufferedImage(w, h, + alpha ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB); + final int[] pixels = ((DataBufferInt) dst.getRaster().getDataBuffer()).getData(); + if (pixels.length != (w * h)) { + throw new IllegalStateException("Error " + name); + } + if (data.length < (pixels.length * (alpha ? 4 : 3))) { + throw new IllegalStateException("Error " + name + " not enaugh pixel data"); + } + + if (alpha) { + for (int i = 0, p = (pixels.length - 1) * 4; i < pixels.length; i++, p -= 4) { + pixels[i] |= ((data[p + 0]) & 0xFF) << 0; + pixels[i] |= ((data[p + 1]) & 0xFF) << 8; + pixels[i] |= ((data[p + 2]) & 0xFF) << 16; + pixels[i] |= ((data[p + 3]) & 0xFF) << 24; + } + } + else { + for (int i = 0, p = (pixels.length - 1) * 3; i < pixels.length; i++, p -= 3) { + pixels[i] |= ((data[p + 0]) & 0xFF) << 0; + pixels[i] |= ((data[p + 1]) & 0xFF) << 8; + pixels[i] |= ((data[p + 2]) & 0xFF) << 16; + } + } + + if ((header[17] >> 4) == 1) { + // ok + } + else if ((header[17] >> 4) == 0) { + // flip horizontally + + for (int y = 0; y < h; y++) { + final int w2 = w / 2; + for (int x = 0; x < w2; x++) { + final int a = (y * w) + x; + final int b = (y * w) + (w - 1 - x); + final int t = pixels[a]; + pixels[a] = pixels[b]; + pixels[b] = t; + } + } + } + else { + throw new UnsupportedOperationException("Error " + name); + } + + return dst; + } + + /** + * Write a BufferedImage to a TGA file BufferedImages should be TYPE_INT_ARGB or + * TYPE_INT_RGB + * + * @param src + * @param file + * @throws IOException + */ + public static void writeTGA(BufferedImage src, final File file) throws IOException { + + final boolean alpha = src.getColorModel().hasAlpha(); + src = ImageUtils.convertStandardImageType(src, alpha); + + final DataBuffer buffer = src.getRaster().getDataBuffer(); + byte[] data; + + if (buffer instanceof DataBufferByte) { + + // Not used anymore because convert to standard image type => Buffer is int + final byte[] pixels = ((DataBufferByte) src.getRaster().getDataBuffer()).getData(); + if (pixels.length != (src.getWidth() * src.getHeight() * (alpha ? 4 : 3))) { + throw new IllegalStateException(); + } + + data = pixels; + + } + else if (buffer instanceof DataBufferInt) { + + final int[] pixels = ((DataBufferInt) src.getRaster().getDataBuffer()).getData(); + if (pixels.length != (src.getWidth() * src.getHeight())) { + throw new IllegalStateException(); + } + + data = new byte[pixels.length * (alpha ? 4 : 3)]; + + if (alpha) { + + for (int p = 0; p < pixels.length; p++) { + final int i = p * 4; + data[i + 0] = (byte) ((pixels[p] >> 0) & 0xFF); + data[i + 1] = (byte) ((pixels[p] >> 8) & 0xFF); + data[i + 2] = (byte) ((pixels[p] >> 16) & 0xFF); + data[i + 3] = (byte) ((pixels[p] >> 24) & 0xFF); + } + } + else { + + for (int p = 0; p < pixels.length; p++) { + final int i = p * 3; + data[i + 0] = (byte) ((pixels[p] >> 0) & 0xFF); + data[i + 1] = (byte) ((pixels[p] >> 8) & 0xFF); + data[i + 2] = (byte) ((pixels[p] >> 16) & 0xFF); + } + } + } + else { + throw new UnsupportedOperationException(); + } + + final byte[] header = new byte[18]; + header[2] = 2; // uncompressed, true-color image + header[12] = (byte) ((src.getWidth() >> 0) & 0xFF); + header[13] = (byte) ((src.getWidth() >> 8) & 0xFF); + header[14] = (byte) ((src.getHeight() >> 0) & 0xFF); + header[15] = (byte) ((src.getHeight() >> 8) & 0xFF); + header[16] = (byte) (alpha ? 32 : 24); // bits per pixel + header[17] = (byte) ((alpha ? 8 : 0) | (2 << 4)); + + final RandomAccessFile raf = new RandomAccessFile(file, "rw"); + raf.write(header); + raf.write(data); + raf.setLength(raf.getFilePointer()); // trim + raf.close(); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/tga/TgaHandler.java b/core/src/com/etheller/warsmash/viewer5/handlers/tga/TgaHandler.java new file mode 100644 index 0000000..1505f0d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/tga/TgaHandler.java @@ -0,0 +1,28 @@ +package com.etheller.warsmash.viewer5.handlers.tga; + +import java.util.ArrayList; + +import com.etheller.warsmash.viewer5.HandlerResource; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.handlers.ResourceHandler; +import com.etheller.warsmash.viewer5.handlers.ResourceHandlerConstructionParams; + +public class TgaHandler extends ResourceHandler { + + public TgaHandler() { + this.extensions = new ArrayList<>(); + this.extensions.add(new String[] { ".tga", "arrayBuffer" }); + } + + @Override + public boolean load(final ModelViewer modelViewer) { + return true; + } + + @Override + public HandlerResource construct(final ResourceHandlerConstructionParams params) { + return new TgaTexture(params.getViewer(), params.getHandler(), params.getExtension(), params.getPathSolver(), + params.getFetchUrl()); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/tga/TgaTexture.java b/core/src/com/etheller/warsmash/viewer5/handlers/tga/TgaTexture.java new file mode 100644 index 0000000..5f3bd58 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/tga/TgaTexture.java @@ -0,0 +1,36 @@ +package com.etheller.warsmash.viewer5.handlers.tga; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; + +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.PathSolver; +import com.etheller.warsmash.viewer5.RawOpenGLTextureResource; +import com.etheller.warsmash.viewer5.handlers.ResourceHandler; + +public class TgaTexture extends RawOpenGLTextureResource { + + public TgaTexture(final ModelViewer viewer, final ResourceHandler handler, final String extension, + final PathSolver pathSolver, final String fetchUrl) { + super(viewer, extension, pathSolver, fetchUrl, handler); + } + + @Override + protected void lateLoad() { + + } + + @Override + protected void load(final InputStream src, final Object options) { + BufferedImage img; + try { + img = TgaFile.readTGA(this.fetchUrl, src); + update(img, false); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/AnimationTokens.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/AnimationTokens.java new file mode 100644 index 0000000..29801ae --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/AnimationTokens.java @@ -0,0 +1,73 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +public class AnimationTokens { + public static enum PrimaryTag { + ATTACK, + BIRTH, + CINEMATIC, + DEATH, + DECAY, + DISSIPATE, + MORPH, + PORTRAIT, + SLEEP, +// SPELL, + STAND, + WALK; + } + + public static enum SecondaryTag { + ALTERNATE, + ALTERNATEEX, + BONE, + CHAIN, + CHANNEL, + COMPLETE, + CRITICAL, + DEFEND, + DRAIN, + EATTREE, + FAST, + FILL, + FLAIL, + FLESH, + FIFTH, + FIRE, + FIRST, + FIVE, + FOUR, + FOURTH, + GOLD, + HIT, + LARGE, + LEFT, + LIGHT, + LOOPING, + LUMBER, + MEDIUM, + MODERATE, + OFF, + ONE, + PUKE, + READY, + RIGHT, + SECOND, + SEVERE, + SLAM, + SMALL, + SPIKED, + SPIN, + SPELL, + SWIM, + TALK, + THIRD, + THREE, + THROW, + TWO, + TURN, + VICTORY, + WORK, + WOUNDED, + UPGRADE; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/DynamicShadowManager.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/DynamicShadowManager.java new file mode 100644 index 0000000..5847c68 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/DynamicShadowManager.java @@ -0,0 +1,139 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL30; +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.util.RenderMathUtils; +import com.etheller.warsmash.viewer5.gl.Extensions; +import com.etheller.warsmash.viewer5.gl.WebGL; + +public class DynamicShadowManager { + public static boolean IS_SHADOW_MAPPING = false; + + private final Vector3 shadowVector = new Vector3(); + private final Matrix4 depthProjectionMatrix = new Matrix4(); + private final Matrix4 depthViewMatrix = new Matrix4(); + private final Matrix4 depthModelMatrix = new Matrix4(); + private final Matrix4 depthMVP = new Matrix4(); + private final Matrix4 biasMatrix = new Matrix4(); + private final Matrix4 depthBiasMVP = new Matrix4(); + + public boolean setup(final WebGL webGL) { + // The framebuffer, which regroups 0, 1, or more textures, and 0 or 1 depth + // buffer. + final GL30 gl = Gdx.gl30; + this.framebufferName = 0; + this.framebufferName = gl.glGenFramebuffer(); + gl.glBindFramebuffer(GL30.GL_FRAMEBUFFER, this.framebufferName); + + this.depthTexture = gl.glGenTexture(); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.depthTexture); + gl.glTexImage2D(GL30.GL_TEXTURE_2D, 0, GL30.GL_DEPTH_COMPONENT16, 1024, 1024, 0, GL30.GL_DEPTH_COMPONENT, + GL30.GL_FLOAT, null); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_MAG_FILTER, GL30.GL_NEAREST); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_MIN_FILTER, GL30.GL_NEAREST); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_WRAP_S, GL30.GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_WRAP_T, GL30.GL_CLAMP_TO_EDGE); + Extensions.dynamicShadowExtension.glFramebufferTexture(GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, + this.depthTexture, 0); + + Extensions.dynamicShadowExtension.glDrawBuffer(GL30.GL_NONE); // No color buffer is drawn to. + + // Always check that our framebuffer is ok + if (gl.glCheckFramebufferStatus(GL30.GL_FRAMEBUFFER) != GL30.GL_FRAMEBUFFER_COMPLETE) { + return false; + } + Gdx.gl30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0); + + return true; + } + + /** + * @return + */ + public Matrix4 prepareShadowMatrix() { + final Vector3 lightInvDir = this.shadowVector; + lightInvDir.set(500f, 2000, 2000); + + // Compute the MVP matrix from the light's point of view + this.depthProjectionMatrix.setToOrtho(-10, 10, -10, 10, -10, 20); + this.depthViewMatrix.set(this.depthProjectionMatrix); + this.depthViewMatrix.setToLookAt(lightInvDir, Vector3.Zero, RenderMathUtils.VEC3_UNIT_Y); + this.depthModelMatrix.idt(); + this.depthMVP.set(this.depthProjectionMatrix).mul(this.depthViewMatrix).mul(this.depthModelMatrix); + +// this.shader.setUniformMatrix("depthMVP", this.depthMVP); + + this.biasMatrix.val[Matrix4.M00] = 0.5f; + this.biasMatrix.val[Matrix4.M10] = 0.0f; + this.biasMatrix.val[Matrix4.M20] = 0.0f; + this.biasMatrix.val[Matrix4.M30] = 0.5f; + this.biasMatrix.val[Matrix4.M01] = 0.0f; + this.biasMatrix.val[Matrix4.M11] = 0.5f; + this.biasMatrix.val[Matrix4.M21] = 0.0f; + this.biasMatrix.val[Matrix4.M31] = 0.5f; + this.biasMatrix.val[Matrix4.M02] = 0.0f; + this.biasMatrix.val[Matrix4.M12] = 0.0f; + this.biasMatrix.val[Matrix4.M22] = 0.5f; + this.biasMatrix.val[Matrix4.M32] = 0.5f; + this.biasMatrix.val[Matrix4.M03] = 0.0f; + this.biasMatrix.val[Matrix4.M13] = 0.0f; + this.biasMatrix.val[Matrix4.M23] = 0.0f; + this.biasMatrix.val[Matrix4.M33] = 1.0f; + this.depthBiasMVP.set(this.biasMatrix).mul(this.depthMVP); + + return this.depthMVP; + } + + public void beginShadowMap(final WebGL webGL) { + IS_SHADOW_MAPPING = true; + + Gdx.gl30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, this.framebufferName); + Extensions.dynamicShadowExtension.glFramebufferTexture(GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, + this.depthTexture, 0); + + Extensions.dynamicShadowExtension.glDrawBuffer(GL30.GL_NONE); // No color buffer is drawn to. + Gdx.gl30.glViewport(0, 0, 1024, 1024); + + } + + public Matrix4 getDepthBiasMVP() { + return this.depthBiasMVP; + } + + // Don't forget to change viewport back + public void endShadowMap() { + IS_SHADOW_MAPPING = false; + Gdx.gl30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0); + } + + public int getDepthTexture() { + return this.depthTexture; + } + + public static final String vertexShader = "#version 330 core\r\n" + // + "\r\n" + // + "// Input vertex data, different for all executions of this shader.\r\n" + // + "layout(location = 0) in vec3 vertexPosition_modelspace;\r\n" + // + "\r\n" + // + "// Values that stay constant for the whole mesh.\r\n" + // + "uniform mat4 depthMVP;\r\n" + // + "\r\n" + // + "void main(){\r\n" + // + " gl_Position = depthMVP * vec4(vertexPosition_modelspace,1);\r\n" + // + "}"; + + public static final String fragmentShader = "#version 330 core\r\n" + // + "\r\n" + // + "// Ouput data\r\n" + // + "layout(location = 0) out float fragmentdepth;\r\n" + // + "\r\n" + // + "void main(){\r\n" + // + " // Not really needed, OpenGL does it anyway\r\n" + // + " fragmentdepth = gl_FragCoord.z;\r\n" + // + "}"; + private int depthTexture; + private int framebufferName; + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/IndexedSequence.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/IndexedSequence.java new file mode 100644 index 0000000..f1677e8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/IndexedSequence.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import com.etheller.warsmash.viewer5.handlers.mdx.Sequence; + +public class IndexedSequence { + public final Sequence sequence; + public final int index; + + public IndexedSequence(final Sequence sequence, final int index) { + this.sequence = sequence; + this.index = index; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SecondaryTagSequenceComparator.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SecondaryTagSequenceComparator.java new file mode 100644 index 0000000..1a5ab6b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SecondaryTagSequenceComparator.java @@ -0,0 +1,41 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import java.util.Comparator; +import java.util.EnumSet; + +public class SecondaryTagSequenceComparator implements Comparator { + private final StandSequenceComparator standSequenceComparator; + private EnumSet ignoredTags; + + public SecondaryTagSequenceComparator(StandSequenceComparator standSequenceComparator) { + this.standSequenceComparator = standSequenceComparator; + } + + public SecondaryTagSequenceComparator reset(EnumSet ignoredTags) { + this.ignoredTags = ignoredTags; + return this; + } + + @Override + public int compare(final IndexedSequence a, final IndexedSequence b) { + EnumSet secondaryTagsA = a.sequence.getSecondaryTags(); + EnumSet secondaryTagsB = b.sequence.getSecondaryTags(); + int secondaryTagsAOrdinal = getTagsOrdinal(secondaryTagsA, ignoredTags); + int secondaryTagsBOrdinal = getTagsOrdinal(secondaryTagsB, ignoredTags); + if (secondaryTagsAOrdinal != secondaryTagsBOrdinal) { + return secondaryTagsBOrdinal - secondaryTagsAOrdinal; + } + return standSequenceComparator.compare(a, b); + } + + public static int getTagsOrdinal(EnumSet secondaryTagsA, EnumSet ignoredTags) { + int secondaryTagsBOrdinal = Integer.MAX_VALUE; + for (AnimationTokens.SecondaryTag secondaryTag : secondaryTagsA) { + if (secondaryTag.ordinal() < secondaryTagsBOrdinal && !ignoredTags.contains(secondaryTag)) { + secondaryTagsBOrdinal = secondaryTag.ordinal(); + } + } + return secondaryTagsBOrdinal; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SequenceUtils.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SequenceUtils.java new file mode 100644 index 0000000..c9e0583 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SequenceUtils.java @@ -0,0 +1,300 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.List; + +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.mdx.Sequence; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.SecondaryTag; + +public class SequenceUtils { + public static final EnumSet EMPTY = EnumSet.noneOf(SecondaryTag.class); + public static final EnumSet READY = EnumSet.of(SecondaryTag.READY); + public static final EnumSet FLESH = EnumSet.of(SecondaryTag.FLESH); + public static final EnumSet TALK = EnumSet.of(SecondaryTag.TALK); + public static final EnumSet BONE = EnumSet.of(SecondaryTag.BONE); + public static final EnumSet HIT = EnumSet.of(SecondaryTag.HIT); + public static final EnumSet SPELL = EnumSet.of(SecondaryTag.SPELL); + public static final EnumSet WORK = EnumSet.of(SecondaryTag.WORK); + + private static final StandSequenceComparator STAND_SEQUENCE_COMPARATOR = new StandSequenceComparator(); + private static final SecondaryTagSequenceComparator SECONDARY_TAG_SEQUENCE_COMPARATOR = new SecondaryTagSequenceComparator( + STAND_SEQUENCE_COMPARATOR); + + public static List filterSequences(final String type, final List sequences) { + final List filtered = new ArrayList<>(); + + for (int i = 0, l = sequences.size(); i < l; i++) { + final Sequence sequence = sequences.get(i); + final String name = sequence.getName().split("-")[0].trim().toLowerCase(); + + if (name.equals(type)) { + filtered.add(new IndexedSequence(sequence, i)); + } + } + + return filtered; + } + + private static List filterSequences(final PrimaryTag type, final EnumSet tags, + final List sequences) { + final List filtered = new ArrayList<>(); + + for (int i = 0, l = sequences.size(); i < l; i++) { + final Sequence sequence = sequences.get(i); + if ((sequence.getPrimaryTags().contains(type) || (type == null)) + && (sequence.getSecondaryTags().containsAll(tags) + && tags.containsAll(sequence.getSecondaryTags()))) { + filtered.add(new IndexedSequence(sequence, i)); + } + } + + return filtered; + } + + public static IndexedSequence selectSequence(final String type, final List sequences) { + final List filtered = filterSequences(type, sequences); + + filtered.sort(STAND_SEQUENCE_COMPARATOR); + + int i = 0; + final double randomRoll = Math.random() * 100; + for (final int l = filtered.size(); i < l; i++) { + final Sequence sequence = filtered.get(i).sequence; + final float rarity = sequence.getRarity(); + + if (rarity == 0) { + break; + } + + if (randomRoll < (10 - rarity)) { + return filtered.get(i); + } + } + + final int sequencesLeft = filtered.size() - i; + final int random = (int) (i + Math.floor(Math.random() * sequencesLeft)); + if (sequencesLeft <= 0) { + return null; // new IndexedSequence(null, 0); + } + final IndexedSequence sequence = filtered.get(random); + + return sequence; + } + + public static int matchCount(final EnumSet goalTagSet, + final EnumSet tagsToTest) { + int matches = 0; + for (final AnimationTokens.SecondaryTag goalTag : goalTagSet) { + if (tagsToTest.contains(goalTag)) { + matches++; + } + } + return matches; + } + + public static IndexedSequence selectSequence(final AnimationTokens.PrimaryTag type, + final EnumSet tags, final List sequences, + final boolean allowRarityVariations) { + List filtered = filterSequences(type, tags, sequences); + final Comparator sequenceComparator = STAND_SEQUENCE_COMPARATOR; + +// if (filtered.isEmpty() && !tags.isEmpty()) { +// filtered = filterSequences(type, EMPTY, sequences); +// } + if (filtered.isEmpty()) { + // find tags + EnumSet fallbackTags = null; + int fallbackTagsMatchCount = 0; + for (int i = 0, l = sequences.size(); i < l; i++) { + final Sequence sequence = sequences.get(i); + if (sequence.getPrimaryTags().contains(type) || (type == null)) { + final int matchCount = matchCount(tags, sequence.getSecondaryTags()); + if (matchCount > fallbackTagsMatchCount) { + fallbackTags = sequence.getSecondaryTags(); + fallbackTagsMatchCount = matchCount; + } + } + } + if (fallbackTags == null) { + for (int i = 0, l = sequences.size(); i < l; i++) { + final Sequence sequence = sequences.get(i); + if (sequence.getPrimaryTags().contains(type) || (type == null)) { + if ((fallbackTags == null) || (sequence.getSecondaryTags().size() < fallbackTags.size()) + || ((sequence.getSecondaryTags().size() == fallbackTags.size()) + && (SecondaryTagSequenceComparator.getTagsOrdinal(sequence.getSecondaryTags(), + tags) > SecondaryTagSequenceComparator.getTagsOrdinal(fallbackTags, + tags)))) { + fallbackTags = sequence.getSecondaryTags(); + } + } + } + } + if (fallbackTags != null) { + filtered = filterSequences(type, fallbackTags, sequences); + } + } + + filtered.sort(sequenceComparator); + + int i = 0; + final double randomRoll = Math.random() * 100; + for (final int l = filtered.size(); i < l; i++) { + final Sequence sequence = filtered.get(i).sequence; + final float rarity = sequence.getRarity(); + + if (rarity == 0) { + break; + } + + if ((randomRoll < (10 - rarity)) && allowRarityVariations) { + return filtered.get(i); + } + } + + final int sequencesLeft = filtered.size() - i; + final int random = (int) (i + Math.floor(Math.random() * sequencesLeft)); + if (sequencesLeft <= 0) { + return null; // new IndexedSequence(null, 0); + } + final IndexedSequence sequence = filtered.get(random); + + return sequence; + } + + public static void randomStandSequence(final MdxComplexInstance target) { + final MdxModel model = (MdxModel) target.model; + final List sequences = model.getSequences(); + final IndexedSequence sequence = selectSequence("stand", sequences); + + if (sequence != null) { + target.setSequence(sequence.index); + } + else { + target.setSequence(0); + } + } + + public static void randomDeathSequence(final MdxComplexInstance target) { + final MdxModel model = (MdxModel) target.model; + final List sequences = model.getSequences(); + final IndexedSequence sequence = selectSequence("death", sequences); + + if (sequence != null) { + target.setSequence(sequence.index); + } + else { + target.setSequence(0); + } + } + + public static void randomWalkSequence(final MdxComplexInstance target) { + final MdxModel model = (MdxModel) target.model; + final List sequences = model.getSequences(); + final IndexedSequence sequence = selectSequence("walk", sequences); + + if (sequence != null) { + target.setSequence(sequence.index); + } + else { + randomStandSequence(target); + } + } + + public static void randomBirthSequence(final MdxComplexInstance target) { + final MdxModel model = (MdxModel) target.model; + final List sequences = model.getSequences(); + final IndexedSequence sequence = selectSequence("birth", sequences); + + if (sequence != null) { + target.setSequence(sequence.index); + } + else { + randomStandSequence(target); + } + } + + public static void randomPortraitSequence(final MdxComplexInstance target) { + final MdxModel model = (MdxModel) target.model; + final List sequences = model.getSequences(); + final IndexedSequence sequence = selectSequence("portrait", sequences); + + if (sequence != null) { + target.setSequence(sequence.index); + } + else { + randomStandSequence(target); + } + } + + public static void randomPortraitTalkSequence(final MdxComplexInstance target) { + final MdxModel model = (MdxModel) target.model; + final List sequences = model.getSequences(); + final IndexedSequence sequence = selectSequence("portrait talk", sequences); + + if (sequence != null) { + target.setSequence(sequence.index); + } + else { + randomPortraitSequence(target); + } + } + + public static void randomSequence(final MdxComplexInstance target, final String sequenceName) { + final MdxModel model = (MdxModel) target.model; + final List sequences = model.getSequences(); + final IndexedSequence sequence = selectSequence(sequenceName, sequences); + + if (sequence != null) { + target.setSequence(sequence.index); + } + else { + randomStandSequence(target); + } + } + + public static Sequence randomSequence(final MdxComplexInstance target, final PrimaryTag animationName, + final boolean allowRarityVariations) { + final MdxModel model = (MdxModel) target.model; + final List sequences = model.getSequences(); + final IndexedSequence sequence = selectSequence(animationName, null, sequences, allowRarityVariations); + + if (sequence != null) { + target.setSequence(sequence.index); + return sequence.sequence; + } + else { + return null; + } + } + + public static Sequence randomSequence(final MdxComplexInstance target, final PrimaryTag animationName, + final EnumSet secondaryAnimationTags, final boolean allowRarityVariations) { + final MdxModel model = (MdxModel) target.model; + final List sequences = model.getSequences(); + final IndexedSequence sequence = selectSequence(animationName, secondaryAnimationTags, sequences, + allowRarityVariations); + + if (sequence != null) { + target.setSequence(sequence.index); + return sequence.sequence; + } + else { + if ((animationName == null) || (secondaryAnimationTags.size() != 1) + || !secondaryAnimationTags.contains(SecondaryTag.SPELL)) { + return null; + } + else { + return randomSequence(target, null, secondaryAnimationTags, allowRarityVariations); + } + } + } + + public static Sequence randomSequence(final MdxComplexInstance target, final PrimaryTag animationName) { + return randomSequence(target, animationName, EMPTY, false); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SplatModel.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SplatModel.java new file mode 100644 index 0000000..334aeb6 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/SplatModel.java @@ -0,0 +1,543 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.GL30; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; +import com.etheller.warsmash.util.RenderMathUtils; +import com.etheller.warsmash.viewer5.ViewerTextureRenderable; + +/** + * TODO this is copied from RivSoft stuff. + * https://github.com/d07RiV/wc3data/blob/3435e9728663825d892693318d0a0bb823dfad8c/src/mdx/viewer/handlers/w3x/splatmodel.js + * + * Shouldn't this just be a geomtry shader that takes X/Y/Texture as input and + * renders a splat, so that we can simply change the X/Y attribute values and + * move around the unit selection circles without memory allocations? For now I + * plan to simply port the RivSoft stuff, and come back later. + */ +public class SplatModel implements Comparable { + private static final int MAX_VERTICES = 65000; + private static final float NO_ABS_HEIGHT = -257f; + private final ViewerTextureRenderable texture; + private final List batches; + public final float[] color; + private final List locations; + private final List splatInstances; + private final boolean unshaded; + private final boolean noDepthTest; + private final boolean highPriority; + + public SplatModel(final GL30 gl, final ViewerTextureRenderable texture, final List locations, + final float[] centerOffset, final List> unitMapping, final boolean unshaded, + final boolean noDepthTest, final boolean highPriority) { + this.texture = texture; + this.unshaded = unshaded; + this.noDepthTest = noDepthTest; + this.highPriority = highPriority; + this.batches = new ArrayList<>(); + this.color = new float[] { 1, 1, 1, 1 }; + + this.locations = locations; + if (unitMapping != null) { + this.splatInstances = new ArrayList<>(); + for (int i = 0; i < unitMapping.size(); i++) { + this.splatInstances.add(new SplatMover(this)); + } + } + else { + this.splatInstances = null; + } + loadBatches(gl, centerOffset); + if (unitMapping != null) { + if (this.splatInstances.size() != unitMapping.size()) { + throw new IllegalStateException(); + } + for (int i = 0; i < this.splatInstances.size(); i++) { + unitMapping.get(i).accept(this.splatInstances.get(i)); + } + } + } + + public void compact(final GL30 gl, final float[] centerOffset) { + // delete all the batches + for (final Batch b : this.batches) { + // Vertices + gl.glDeleteBuffer(b.vertexBuffer); + + // Faces. + gl.glDeleteBuffer(b.faceBuffer); + } + this.batches.clear(); + + loadBatches(gl, centerOffset); + } + + private void loadBatches(final GL30 gl, final float[] centerOffset) { + final List vertices = new ArrayList<>(); + final List uvs = new ArrayList<>(); + final List absoluteHeights = new ArrayList<>(); + final List indices = new ArrayList<>(); + final List batchRenderUnits = new ArrayList<>(); + final int instances = this.locations.size(); + for (int idx = 0; idx < instances; ++idx) { + final float[] locs = this.locations.get(idx); + final float x0 = locs[0]; + final float y0 = locs[1]; + final float x1 = locs[2]; + final float y1 = locs[3]; + final float zoffs = locs[4]; + + final float centerOffsetX = centerOffset[0]; + final float centerOffsetY = centerOffset[1]; + final int ix0 = (int) Math.floor((x0 - centerOffsetX) / 128.0); + final int ix1 = (int) Math.ceil((x1 - centerOffsetX) / 128.0); + final int iy0 = (int) Math.floor((y0 - centerOffsetY) / 128.0); + final int iy1 = (int) Math.ceil((y1 - centerOffsetY) / 128.0); + + final int newVerts = ((iy1 - iy0) + 1) * ((ix1 - ix0) + 1); + final int maxPossibleVerts = ((int) Math.ceil((y1 - y0) / 128.0) + 2) + * ((int) Math.ceil((x1 - x0) / 128.0) + 2); + final int maxPossibleFaces = ((int) Math.ceil((y1 - y0) / 128.0) + 1) + * ((int) Math.ceil((x1 - x0) / 128.0) + 1); + + int start = vertices.size(); + final SplatMover splatMover = (this.splatInstances == null) ? null + : this.splatInstances.get(idx).reset(start * 3 * 4, indices.size() * 6 * 2, idx); + + final int numVertsToCrate = splatMover == null ? newVerts : maxPossibleVerts; + if (numVertsToCrate > MAX_VERTICES) { + continue; + } + + final int step = (ix1 - ix0) + 1; + if ((start + numVertsToCrate) > MAX_VERTICES) { + this.addBatch(gl, vertices, uvs, absoluteHeights, indices, batchRenderUnits); + vertices.clear(); + uvs.clear(); + absoluteHeights.clear(); + indices.clear(); + batchRenderUnits.clear(); + start = 0; + } + + final float uvXScale = x1 - x0; + final float uvYScale = y1 - y0; + for (int iy = iy0; iy <= iy1; ++iy) { + final float y = (iy * 128.0f) + centerOffsetY; + for (int ix = ix0; ix <= ix1; ++ix) { + final float x = (ix * 128.0f) + centerOffsetX; + final float[] vertex = new float[] { x, y, zoffs }; + vertices.add(vertex); + final float[] uv = new float[] { (x - x0) / uvXScale, 1.0f - ((y - y0) / uvYScale) }; + uvs.add(uv); + final float[] absHeight = new float[] { NO_ABS_HEIGHT }; + absoluteHeights.add(absHeight); + if (splatMover != null) { + splatMover.vertices.add(vertex); + splatMover.uvs.add(uv); + splatMover.absoluteHeights.add(absHeight); + } + } + } + if (splatMover != null) { + splatMover.uvXScale = uvXScale; + splatMover.uvYScale = uvYScale; + splatMover.locs = locs; + splatMover.ix0 = ix0; + splatMover.iy0 = iy0; + splatMover.ix1 = ix1; + splatMover.iy1 = iy1; + + final float y = (iy1 * 128.0f) + centerOffsetY; + final float x = (ix1 * 128.0f) + centerOffsetX; + while (splatMover.vertices.size() < maxPossibleVerts) { + final float[] vertex = new float[] { x, y, zoffs }; + vertices.add(vertex); + final float[] uv = new float[] { (x - x0) / uvXScale, 1.0f - ((y - y0) / uvYScale) }; + uvs.add(uv); + final float[] absHeight = new float[] { NO_ABS_HEIGHT }; + absoluteHeights.add(absHeight); + splatMover.vertices.add(vertex); + splatMover.uvs.add(uv); + splatMover.absoluteHeights.add(absHeight); + } + } + for (int i = 0; i < (iy1 - iy0); ++i) { + for (int j = 0; j < (ix1 - ix0); ++j) { + final int i0 = start + (i * step) + j; + final int[] indexArray = new int[] { i0, i0 + 1, i0 + step, i0 + 1, i0 + step + 1, i0 + step }; + indices.add(indexArray); + if (splatMover != null) { + splatMover.indices.add(indexArray); + } + } + } + if (this.splatInstances != null) { + batchRenderUnits.add(splatMover); + + while (splatMover.indices.size() < maxPossibleFaces) { + final int i0 = start; + final int[] indexArray = new int[] { i0, i0, i0, i0, i0, i0 }; + indices.add(indexArray); + splatMover.indices.add(indexArray); + } + } + + } + if (indices.size() > 0) { + this.addBatch(gl, vertices, uvs, absoluteHeights, indices, batchRenderUnits); + } + if (this.splatInstances != null) { + for (final SplatMover splatMover : this.splatInstances) { + if (splatMover.hidden) { + splatMover.hide(); + } + } + } + } + + private void addBatch(final GL30 gl, final List vertices, final List uvs, + final List absoluteHeights, final List indices, final List batchRenderUnits) { + final int uvsOffset = vertices.size() * 3 * 4; + final int paramsOffset = uvsOffset + (uvs.size() * 4 * 2); + + final int vertexBuffer = gl.glGenBuffer(); + gl.glBindBuffer(GL30.GL_ARRAY_BUFFER, vertexBuffer); + gl.glBufferData(GL30.GL_ARRAY_BUFFER, uvsOffset + (uvs.size() * 4 * 2) + (absoluteHeights.size() * 4), null, + GL30.GL_STATIC_DRAW); + gl.glBufferSubData(GL30.GL_ARRAY_BUFFER, 0, vertices.size() * 4 * 3, RenderMathUtils.wrap(vertices)); + gl.glBufferSubData(GL30.GL_ARRAY_BUFFER, uvsOffset, uvs.size() * 4 * 2, RenderMathUtils.wrap(uvs)); + gl.glBufferSubData(GL30.GL_ARRAY_BUFFER, paramsOffset, absoluteHeights.size() * 4, + RenderMathUtils.wrap(absoluteHeights)); + + final int faceBuffer = gl.glGenBuffer(); + gl.glBindBuffer(GL30.GL_ELEMENT_ARRAY_BUFFER, faceBuffer); + gl.glBufferData(GL30.GL_ELEMENT_ARRAY_BUFFER, indices.size() * 6 * 2, RenderMathUtils.wrapFaces(indices), + GL30.GL_STATIC_DRAW); + + this.batches.add(new Batch(uvsOffset, vertexBuffer, faceBuffer, indices.size() * 6, paramsOffset)); + for (final SplatMover mover : batchRenderUnits) { + mover.vertexBuffer = vertexBuffer; + mover.uvsOffset = uvsOffset; + mover.faceBuffer = faceBuffer; + mover.absHeightsOffset = paramsOffset; + } + } + + public void render(final GL30 gl, final ShaderProgram shader) { + // Texture + + if (this.noDepthTest) { + gl.glDisable(GL20.GL_DEPTH_TEST); + } + else { + gl.glEnable(GL20.GL_DEPTH_TEST); + } + gl.glActiveTexture(GL30.GL_TEXTURE1); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.texture.getGlHandle()); + shader.setUniformi("u_show_lighting", this.unshaded ? 0 : 1); + shader.setUniform4fv("u_color", this.color, 0, 4); + + for (final Batch b : this.batches) { + // Vertices + gl.glBindBuffer(GL30.GL_ARRAY_BUFFER, b.vertexBuffer); + shader.setVertexAttribute("a_position", 3, GL30.GL_FLOAT, false, 12, 0); + shader.setVertexAttribute("a_uv", 2, GL30.GL_FLOAT, false, 8, b.uvsOffset); + shader.setVertexAttribute("a_absoluteHeight", 1, GL30.GL_FLOAT, false, 4, b.paramsOffset); + + // Faces. + gl.glBindBuffer(GL30.GL_ELEMENT_ARRAY_BUFFER, b.faceBuffer); + + // Draw + gl.glDrawElements(GL30.GL_TRIANGLES, b.elements, GL30.GL_UNSIGNED_SHORT, 0); + } + + } + + public boolean isNoDepthTest() { + return this.noDepthTest; + } + + public boolean isHighPriority() { + return this.highPriority; + } + + public SplatMover add(final float x, final float y, final float x2, final float y2, final float zDepthUpward, + final float[] centerOffset) { + this.locations.add(new float[] { x, y, x2, y2, zDepthUpward }); + final SplatMover splatMover; + if (this.splatInstances != null) { + splatMover = new SplatMover(this); + this.splatInstances.add(splatMover); + } + else { + splatMover = null; + } + compact(Gdx.gl30, centerOffset); + return splatMover; + } + + private static final class Batch { + private final int uvsOffset; + private final int vertexBuffer; + private final int faceBuffer; + private final int elements; + private final int paramsOffset; + + public Batch(final int uvsOffset, final int vertexBuffer, final int faceBuffer, final int elements, + final int paramsOffset) { + this.uvsOffset = uvsOffset; + this.vertexBuffer = vertexBuffer; + this.faceBuffer = faceBuffer; + this.elements = elements; + this.paramsOffset = paramsOffset; + } + } + + public static final class SplatMover { + public int absHeightsOffset; + public int faceBuffer; + public int uvsOffset; + public int iy1; + public int ix1; + public int iy0; + public int ix0; + public float[] locs; + public float uvYScale; + public float uvXScale; + private int vertexBuffer; + private int startOffset; + private int start; + private final List vertices = new ArrayList<>(); + private final List uvs = new ArrayList<>(); + private final List absoluteHeights = new ArrayList<>(); + private final List indices = new ArrayList<>(); + private int indicesStartOffset; + private int index; + private final SplatModel splatModel; + private boolean hidden = false; + private boolean heightIsAbsolute = false; + private float absoluteHeightValue = 0.0f; + + private SplatMover(final SplatModel splatModel) { + this.splatModel = splatModel; + } + + private SplatMover reset(final int i, final int indicesStartOffset, final int index) { + this.startOffset = i; + this.indicesStartOffset = indicesStartOffset; + this.start = i / 12; + this.index = index; + this.vertices.clear(); + this.uvs.clear(); + this.absoluteHeights.clear(); + this.indices.clear(); + return this; + } + + public void move(final float deltaX, final float deltaY, final float[] centerOffset) { + this.locs[0] += deltaX; + this.locs[2] += deltaX; + this.locs[1] += deltaY; + this.locs[3] += deltaY; + updateAfterMove(centerOffset); + } + + public void setLocation(final float x, final float y, final float[] centerOffset) { + final float width = this.locs[2] - this.locs[0]; + final float height = this.locs[3] - this.locs[1]; + this.locs[0] = x - (width / 2); + this.locs[2] = x + (width / 2); + this.locs[1] = y - (height / 2); + this.locs[3] = y + (height / 2); + updateAfterMove(centerOffset); + } + + private void updateAfterMove(final float[] centerOffset) { + final float x0 = this.locs[0]; + final float y0 = this.locs[1]; + final float x1 = this.locs[2]; + final float y1 = this.locs[3]; + + final float centerOffsetX = centerOffset[0]; + final float centerOffsetY = centerOffset[1]; + final int ix0 = (int) Math.floor((x0 - centerOffsetX) / 128.0); + final int ix1 = (int) Math.ceil((x1 - centerOffsetX) / 128.0); + final int iy0 = (int) Math.floor((y0 - centerOffsetY) / 128.0); + final int iy1 = (int) Math.ceil((y1 - centerOffsetY) / 128.0); + + final GL30 gl = Gdx.gl30; + gl.glBindBuffer(GL30.GL_ARRAY_BUFFER, this.vertexBuffer); + if ((ix0 != this.ix0) || (iy0 != this.iy0) || (ix1 != this.ix1) || (iy1 != this.iy1)) { + // splat geometry has moved, difficult case + final float newVerts = ((iy1 - iy0) + 1) * ((ix1 - ix0) + 1); + if (newVerts <= this.uvs.size()) { + } + int vertexIndex = 0; + float y = 0; + float x = 0; + for (int iy = iy0; iy <= iy1; ++iy) { + y = (iy * 128.0f) + centerOffsetY; + for (int ix = ix0; ix <= ix1; ++ix) { + x = (ix * 128.0f) + centerOffsetX; + final float[] vertexToUpdate = this.vertices.get(vertexIndex); + vertexToUpdate[0] = x; + vertexToUpdate[1] = y; + final float[] uvItem = this.uvs.get(vertexIndex); + uvItem[0] = (x - x0) / this.uvXScale; + uvItem[1] = 1.0f - ((y - y0) / this.uvYScale); + vertexIndex++; + } + } + + for (; vertexIndex < this.vertices.size(); vertexIndex++) { + final float[] vertexToUpdate = this.vertices.get(vertexIndex); + vertexToUpdate[0] = x; + vertexToUpdate[1] = y; + final float[] uvItem = this.uvs.get(vertexIndex); + uvItem[0] = (x - x0) / this.uvXScale; + uvItem[1] = 1.0f - ((y - y0) / this.uvYScale); + } + gl.glBufferSubData(GL30.GL_ARRAY_BUFFER, this.startOffset, 4 * 3 * this.vertices.size(), + RenderMathUtils.wrap(this.vertices)); + + final int step = (ix1 - ix0) + 1; + int faceIndicesIndex = 0; + for (int i = 0; i < (iy1 - iy0); ++i) { + for (int j = 0; j < (ix1 - ix0); ++j) { + final int i0 = this.start + (i * step) + j; + final int[] indexArr = this.indices.get(faceIndicesIndex++); + indexArr[0] = i0; + indexArr[1] = i0 + 1; + indexArr[2] = i0 + step; + indexArr[3] = i0 + 1; + indexArr[4] = i0 + step + 1; + indexArr[5] = i0 + step; + } + } + + for (; faceIndicesIndex < this.indices.size(); faceIndicesIndex++) { + final int i0 = this.start; + final int[] indexArr = this.indices.get(faceIndicesIndex); + for (int i = 0; i < indexArr.length; i++) { + indexArr[i] = i0; + } + } + gl.glBindBuffer(GL30.GL_ELEMENT_ARRAY_BUFFER, this.faceBuffer); + gl.glBufferSubData(GL30.GL_ELEMENT_ARRAY_BUFFER, this.indicesStartOffset, 6 * 2 * this.indices.size(), + RenderMathUtils.wrapFaces(this.indices)); + this.ix0 = ix0; + this.iy0 = iy0; + this.ix1 = ix1; + this.iy1 = iy1; + } + else { + // splat will use same geometry, easy case, just update the UVs + + int index = 0; + for (int iy = iy0; iy <= iy1; ++iy) { + final float y = (iy * 128.0f) + centerOffsetY; + for (int ix = ix0; ix <= ix1; ++ix) { + final float x = (ix * 128.0f) + centerOffsetX; + final float[] uvItem = this.uvs.get(index++); + uvItem[0] = (x - x0) / this.uvXScale; + uvItem[1] = 1.0f - ((y - y0) / this.uvYScale); + } + } + } + gl.glBufferSubData(GL30.GL_ARRAY_BUFFER, this.uvsOffset + ((this.startOffset / 3) * 2), + 4 * 2 * this.uvs.size(), RenderMathUtils.wrap(this.uvs)); + if (this.heightIsAbsolute) { + updateAbsoluteHeightParams(); + } + } + + public void destroy(final GL30 gl, final float[] centerOffset) { + this.splatModel.locations.remove(this.index); + this.splatModel.splatInstances.remove(this.index); + this.splatModel.compact(gl, centerOffset); + } + + public void hide() { + // does not remove the shadow, just makes it not show, so it would still be + // using GPU resources + final GL30 gl = Gdx.gl30; + for (final float[] vertex : this.vertices) { + for (int i = 0; i < vertex.length; i++) { + vertex[i] = 0.0f; + } + } + for (final int[] indices : this.indices) { + for (int i = 0; i < indices.length; i++) { + indices[i] = 0; + } + } + for (final float[] uv : this.uvs) { + for (int i = 0; i < uv.length; i++) { + uv[i] = 0; + } + } + gl.glBindBuffer(GL30.GL_ARRAY_BUFFER, this.vertexBuffer); + gl.glBufferSubData(GL30.GL_ARRAY_BUFFER, this.startOffset, 4 * 3 * this.vertices.size(), + RenderMathUtils.wrap(this.vertices)); + gl.glBindBuffer(GL30.GL_ELEMENT_ARRAY_BUFFER, this.faceBuffer); + gl.glBufferSubData(GL30.GL_ELEMENT_ARRAY_BUFFER, this.indicesStartOffset, 6 * 2 * this.indices.size(), + RenderMathUtils.wrapFaces(this.indices)); + gl.glBufferSubData(GL30.GL_ARRAY_BUFFER, this.uvsOffset + ((this.startOffset / 3) * 2), + 4 * 2 * this.uvs.size(), RenderMathUtils.wrap(this.uvs)); + this.hidden = true; + } + + public void show(final float[] centerOffset) { + // It tries to only update if it is located at a new position... but here we are + // forcing it visible again by putting the position outside the map + this.ix0 = this.ix1 = this.iy0 = this.iy1 = Integer.MIN_VALUE; + move(0, 0, centerOffset); + this.hidden = false; + } + + public void setHeightAbsolute(final boolean absolute, final float absoluteHeightValue) { + this.absoluteHeightValue = absoluteHeightValue; + if (absolute != this.heightIsAbsolute) { + this.heightIsAbsolute = absolute; + updateAbsoluteHeightParams(); + } + } + + private void updateAbsoluteHeightParams() { + final GL30 gl = Gdx.gl30; + final float height = this.heightIsAbsolute ? this.absoluteHeightValue : NO_ABS_HEIGHT; + for (final float[] absHeight : this.absoluteHeights) { + absHeight[0] = height; + } + gl.glBindBuffer(GL30.GL_ARRAY_BUFFER, this.vertexBuffer); + gl.glBufferSubData(GL30.GL_ARRAY_BUFFER, this.absHeightsOffset + (this.startOffset / 3), + this.absoluteHeights.size() * 4, RenderMathUtils.wrap(this.absoluteHeights)); + } + } + + @Override + public int compareTo(final SplatModel other) { + if (this.locations.isEmpty()) { + if (other.locations.isEmpty()) { + return 0; + } + else { + return 1; + } + } + else { + if (other.locations.isEmpty()) { + return -1; + } + else { + return Float.compare(this.locations.get(0)[4], other.locations.get(0)[4]); + } + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/StandSequenceComparator.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/StandSequenceComparator.java new file mode 100644 index 0000000..1aa65af --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/StandSequenceComparator.java @@ -0,0 +1,11 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import java.util.Comparator; + +public class StandSequenceComparator implements Comparator { + @Override + public int compare(final IndexedSequence a, final IndexedSequence b) { + return (int) Math.signum(b.sequence.getRarity() - a.sequence.getRarity()); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/TerrainDoodad.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/TerrainDoodad.java new file mode 100644 index 0000000..eda3fc4 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/TerrainDoodad.java @@ -0,0 +1,38 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import java.awt.image.BufferedImage; + +import com.badlogic.gdx.math.Quaternion; +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.RenderMathUtils; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; + +public class TerrainDoodad { + private static final float[] locationHeap = new float[3]; + public final MdxComplexInstance instance; + private final MutableGameObject row; + + public TerrainDoodad(final War3MapViewer map, final MdxModel model, final MutableGameObject row, + final com.etheller.warsmash.parsers.w3x.doo.TerrainDoodad doodad, final BufferedImage pathingTextureImage) { + final float[] centerOffset = map.terrain.centerOffset; + final MdxComplexInstance instance = (MdxComplexInstance) model.addInstance(0); + + final int textureWidth = pathingTextureImage.getWidth(); + final int textureHeight = pathingTextureImage.getHeight(); + final int textureWidthTerrainCells = textureWidth / 4; + final int textureHeightTerrainCells = textureHeight / 4; + final int minCellX = ((int) doodad.getLocation()[0]); + final int minCellY = ((int) doodad.getLocation()[1]); + locationHeap[0] = ((minCellX * 128) + (textureWidthTerrainCells * 64) + centerOffset[0]); + locationHeap[1] = ((minCellY * 128) + (textureHeightTerrainCells * 64) + centerOffset[1]); + + instance.move(locationHeap); + instance.rotate(new Quaternion().setFromAxisRad(RenderMathUtils.VEC3_UNIT_Z, + (float) Math.toRadians(row.readSLKTagFloat("fixedRot")))); + instance.setScene(map.worldScene); + + this.instance = instance; + this.row = row; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/TextTag.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/TextTag.java new file mode 100644 index 0000000..921995b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/TextTag.java @@ -0,0 +1,45 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.math.Vector3; + +public class TextTag { + private final Vector3 position; + private float screenCoordsZHeight; + private final String text; + private final Color color; + private float lifetime = 0; + + public TextTag(final Vector3 position, final String text, final Color color) { + this.position = position; + this.text = text; + this.color = color; + position.z += 64f; + } + + public boolean update(final float deltaTime) { + this.screenCoordsZHeight += 60.0f * deltaTime; + this.lifetime += deltaTime; + return this.lifetime > 2.5f; + } + + public Vector3 getPosition() { + return this.position; + } + + public float getRemainingLife() { + return 2.5f - this.lifetime; + } + + public Color getColor() { + return this.color; + } + + public String getText() { + return this.text; + } + + public float getScreenCoordsZHeight() { + return this.screenCoordsZHeight; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitSound.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitSound.java new file mode 100644 index 0000000..0df2734 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitSound.java @@ -0,0 +1,143 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import java.util.ArrayList; +import java.util.List; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.audio.Sound; +import com.badlogic.gdx.utils.TimeUtils; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.units.Element; +import com.etheller.warsmash.util.DataSourceFileHandle; +import com.etheller.warsmash.viewer5.AudioBufferSource; +import com.etheller.warsmash.viewer5.AudioContext; +import com.etheller.warsmash.viewer5.AudioPanner; +import com.etheller.warsmash.viewer5.gl.Extensions; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderUnit; + +public final class UnitSound { + private static final UnitSound SILENT = new UnitSound(0, 0, 0, 0, 0, 0, false); + + private final List sounds = new ArrayList<>(); + private final float volume; + private final float pitch; + private final float pitchVariance; + private final float minDistance; + private final float maxDistance; + private final float distanceCutoff; + private final boolean looping; + + private Sound lastPlayedSound; + + public static UnitSound create(final DataSource dataSource, final DataTable unitAckSounds, final String soundName, + final String soundType) { + final Element row = unitAckSounds.get(soundName + soundType); + if (row == null) { + return SILENT; + } + final String fileNames = row.getField("FileNames"); + String directoryBase = row.getField("DirectoryBase"); + if ((directoryBase.length() > 1) && !directoryBase.endsWith("\\")) { + directoryBase += "\\"; + } + final float volume = row.getFieldFloatValue("Volume") / 127f; + final float pitch = row.getFieldFloatValue("Pitch"); + float pitchVariance = row.getFieldFloatValue("PitchVariance"); + if (pitchVariance == 1.0f) { + pitchVariance = 0.0f; + } + final float minDistance = row.getFieldFloatValue("MinDistance"); + final float maxDistance = row.getFieldFloatValue("MaxDistance"); + final float distanceCutoff = row.getFieldFloatValue("DistanceCutoff"); + final String[] flags = row.getField("Flags").split(","); + boolean looping = false; + for (final String flag : flags) { + if ("LOOPING".equals(flag)) { + looping = true; + } + } + final UnitSound sound = new UnitSound(volume, pitch, pitchVariance, minDistance, maxDistance, distanceCutoff, + looping); + for (final String fileName : fileNames.split(",")) { + String filePath = directoryBase + fileName; + final int lastDotIndex = filePath.lastIndexOf('.'); + if (lastDotIndex != -1) { + filePath = filePath.substring(0, lastDotIndex); + } + if (dataSource.has(filePath + ".wav") || dataSource.has(filePath + ".flac")) { + sound.sounds.add(Gdx.audio.newSound(new DataSourceFileHandle(dataSource, filePath + ".wav"))); + } + } + return sound; + } + + public UnitSound(final float volume, final float pitch, final float pitchVariation, final float minDistance, + final float maxDistance, final float distanceCutoff, final boolean looping) { + this.volume = volume; + this.pitch = pitch; + this.pitchVariance = pitchVariation; + this.minDistance = minDistance; + this.maxDistance = maxDistance; + this.distanceCutoff = distanceCutoff; + this.looping = looping; + } + + public boolean playUnitResponse(final AudioContext audioContext, final RenderUnit unit) { + return playUnitResponse(audioContext, unit, (int) (Math.random() * this.sounds.size())); + } + + public boolean playUnitResponse(final AudioContext audioContext, final RenderUnit unit, final int index) { + final long millisTime = TimeUtils.millis(); + if (millisTime < unit.lastUnitResponseEndTimeMillis) { + return false; + } + if (play(audioContext, unit.location[0], unit.location[1], unit.location[2])) { + final float duration = Extensions.audio.getDuration(this.lastPlayedSound); + unit.lastUnitResponseEndTimeMillis = millisTime + (long) (1000 * duration); + return true; + } + return false; + } + + public boolean play(final AudioContext audioContext, final float x, final float y, final float z) { + return play(audioContext, x, y, z, (int) (Math.random() * this.sounds.size())); + } + + public boolean play(final AudioContext audioContext, final float x, final float y, final float z, final int index) { + if (this.sounds.isEmpty()) { + return false; + } + + if (audioContext == null) { + return true; + } + final AudioPanner panner = audioContext.createPanner(); + final AudioBufferSource source = audioContext.createBufferSource(); + + // Panner settings + panner.setPosition(x, y, z); + panner.setDistances(this.distanceCutoff, this.minDistance); + panner.connect(audioContext.destination); + + // Source. + source.buffer = this.sounds.get(index); + source.connect(panner); + + // Make a sound. + source.start(0, this.volume, + (this.pitch + ((float) Math.random() * this.pitchVariance * 2)) - this.pitchVariance, this.looping); + this.lastPlayedSound = source.buffer; + return true; + } + + public int getSoundCount() { + return this.sounds.size(); + } + + public void stop() { + for (final Sound sound : this.sounds) { + sound.stop(); + } + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitSoundset.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitSoundset.java new file mode 100644 index 0000000..70553ef --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/UnitSoundset.java @@ -0,0 +1,23 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.units.DataTable; + +public class UnitSoundset { + public final UnitSound what; + public final UnitSound pissed; + public final UnitSound yesAttack; + public final UnitSound yes; + public final UnitSound ready; + public final UnitSound warcry; + + public UnitSoundset(final DataSource dataSource, final DataTable unitAckSounds, final String soundName) { + this.what = UnitSound.create(dataSource, unitAckSounds, soundName, "What"); + this.pissed = UnitSound.create(dataSource, unitAckSounds, soundName, "Pissed"); + this.yesAttack = UnitSound.create(dataSource, unitAckSounds, soundName, "YesAttack"); + this.yes = UnitSound.create(dataSource, unitAckSounds, soundName, "Yes"); + this.ready = UnitSound.create(dataSource, unitAckSounds, soundName, "Ready"); + this.warcry = UnitSound.create(dataSource, unitAckSounds, soundName, "Warcry"); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/Variations.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/Variations.java new file mode 100644 index 0000000..d121da5 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/Variations.java @@ -0,0 +1,159 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import java.util.HashMap; +import java.util.Map; + +public class Variations { + public static final Map CLIFF_VARS; + public static final Map CITY_CLIFF_VARS; + + static { + final Map cliffVariations = new HashMap<>(); + cliffVariations.put("AAAB", 1); + cliffVariations.put("AAAC", 1); + cliffVariations.put("AABA", 1); + cliffVariations.put("AABB", 2); + cliffVariations.put("AABC", 0); + cliffVariations.put("AACA", 1); + cliffVariations.put("AACB", 0); + cliffVariations.put("AACC", 1); + cliffVariations.put("ABAA", 1); + cliffVariations.put("ABAB", 1); + cliffVariations.put("ABAC", 0); + cliffVariations.put("ABBA", 2); + cliffVariations.put("ABBB", 1); + cliffVariations.put("ABBC", 0); + cliffVariations.put("ABCA", 0); + cliffVariations.put("ABCB", 0); + cliffVariations.put("ABCC", 0); + cliffVariations.put("ACAA", 1); + cliffVariations.put("ACAB", 0); + cliffVariations.put("ACAC", 1); + cliffVariations.put("ACBA", 0); + cliffVariations.put("ACBB", 0); + cliffVariations.put("ACBC", 0); + cliffVariations.put("ACCA", 1); + cliffVariations.put("ACCB", 0); + cliffVariations.put("ACCC", 1); + cliffVariations.put("BAAA", 1); + cliffVariations.put("BAAB", 1); + cliffVariations.put("BAAC", 0); + cliffVariations.put("BABA", 1); + cliffVariations.put("BABB", 1); + cliffVariations.put("BABC", 0); + cliffVariations.put("BACA", 0); + cliffVariations.put("BACB", 0); + cliffVariations.put("BACC", 0); + cliffVariations.put("BBAA", 1); + cliffVariations.put("BBAB", 1); + cliffVariations.put("BBAC", 0); + cliffVariations.put("BBBA", 1); + cliffVariations.put("BBCA", 0); + cliffVariations.put("BCAA", 0); + cliffVariations.put("BCAB", 0); + cliffVariations.put("BCAC", 0); + cliffVariations.put("BCBA", 0); + cliffVariations.put("BCCA", 0); + cliffVariations.put("CAAA", 1); + cliffVariations.put("CAAB", 0); + cliffVariations.put("CAAC", 1); + cliffVariations.put("CABA", 0); + cliffVariations.put("CABB", 0); + cliffVariations.put("CABC", 0); + cliffVariations.put("CACA", 1); + cliffVariations.put("CACB", 0); + cliffVariations.put("CACC", 1); + cliffVariations.put("CBAA", 0); + cliffVariations.put("CBAB", 0); + cliffVariations.put("CBAC", 0); + cliffVariations.put("CBBA", 0); + cliffVariations.put("CBCA", 0); + cliffVariations.put("CCAA", 1); + cliffVariations.put("CCAB", 0); + cliffVariations.put("CCAC", 1); + cliffVariations.put("CCBA", 0); + cliffVariations.put("CCCA", 1); + CLIFF_VARS = cliffVariations; + + final Map cityCliffVariations = new HashMap<>(); + cityCliffVariations.put("AAAB", 2); + cityCliffVariations.put("AAAC", 1); + cityCliffVariations.put("AABA", 1); + cityCliffVariations.put("AABB", 3); + cityCliffVariations.put("AABC", 0); + cityCliffVariations.put("AACA", 1); + cityCliffVariations.put("AACB", 0); + cityCliffVariations.put("AACC", 3); + cityCliffVariations.put("ABAA", 1); + cityCliffVariations.put("ABAB", 2); + cityCliffVariations.put("ABAC", 0); + cityCliffVariations.put("ABBA", 3); + cityCliffVariations.put("ABBB", 0); + cityCliffVariations.put("ABBC", 0); + cityCliffVariations.put("ABCA", 0); + cityCliffVariations.put("ABCB", 0); + cityCliffVariations.put("ABCC", 0); + cityCliffVariations.put("ACAA", 1); + cityCliffVariations.put("ACAB", 0); + cityCliffVariations.put("ACAC", 2); + cityCliffVariations.put("ACBA", 0); + cityCliffVariations.put("ACBB", 0); + cityCliffVariations.put("ACBC", 0); + cityCliffVariations.put("ACCA", 3); + cityCliffVariations.put("ACCB", 0); + cityCliffVariations.put("ACCC", 1); + cityCliffVariations.put("BAAA", 1); + cityCliffVariations.put("BAAB", 3); + cityCliffVariations.put("BAAC", 0); + cityCliffVariations.put("BABA", 2); + cityCliffVariations.put("BABB", 0); + cityCliffVariations.put("BABC", 0); + cityCliffVariations.put("BACA", 0); + cityCliffVariations.put("BACB", 0); + cityCliffVariations.put("BACC", 0); + cityCliffVariations.put("BBAA", 3); + cityCliffVariations.put("BBAB", 1); + cityCliffVariations.put("BBAC", 0); + cityCliffVariations.put("BBBA", 1); + cityCliffVariations.put("BBCA", 0); + cityCliffVariations.put("BCAA", 0); + cityCliffVariations.put("BCAB", 0); + cityCliffVariations.put("BCAC", 0); + cityCliffVariations.put("BCBA", 0); + cityCliffVariations.put("BCCA", 0); + cityCliffVariations.put("CAAA", 1); + cityCliffVariations.put("CAAB", 0); + cityCliffVariations.put("CAAC", 3); + cityCliffVariations.put("CABA", 0); + cityCliffVariations.put("CABB", 0); + cityCliffVariations.put("CABC", 0); + cityCliffVariations.put("CACA", 2); + cityCliffVariations.put("CACB", 0); + cityCliffVariations.put("CACC", 1); + cityCliffVariations.put("CBAA", 0); + cityCliffVariations.put("CBAB", 0); + cityCliffVariations.put("CBAC", 0); + cityCliffVariations.put("CBBA", 0); + cityCliffVariations.put("CBCA", 0); + cityCliffVariations.put("CCAA", 3); + cityCliffVariations.put("CCAB", 0); + cityCliffVariations.put("CCAC", 1); + cityCliffVariations.put("CCBA", 0); + cityCliffVariations.put("CCCA", 1); + CITY_CLIFF_VARS = cityCliffVariations; + } + + public static int getCliffVariation(final String dir, final String tag, final int variation) { + final Integer vars; + if ("Cliffs".equals(dir)) { + vars = CLIFF_VARS.get(tag); + } + else { + vars = CITY_CLIFF_VARS.get(tag); + } + if (variation < vars) { + return variation; + } + return variation % (vars + 1); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xSceneLight.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xSceneLight.java new file mode 100644 index 0000000..1bc01c4 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xSceneLight.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +public class W3xSceneLight { + + public static enum Type { + OMNIDIRECTIONAL, + DIRECTIONAL; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xSceneLightManager.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xSceneLightManager.java new file mode 100644 index 0000000..282d947 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xSceneLightManager.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import com.etheller.warsmash.viewer5.gl.DataTexture; + +public interface W3xSceneLightManager { + public DataTexture getUnitLightsTexture(); + + public int getUnitLightCount(); + + public DataTexture getTerrainLightsTexture(); + + public int getTerrainLightCount(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xScenePortraitLightManager.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xScenePortraitLightManager.java new file mode 100644 index 0000000..df3f4be --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xScenePortraitLightManager.java @@ -0,0 +1,106 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.List; + +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.viewer5.ModelViewer; +import com.etheller.warsmash.viewer5.SceneLightInstance; +import com.etheller.warsmash.viewer5.SceneLightManager; +import com.etheller.warsmash.viewer5.gl.DataTexture; +import com.etheller.warsmash.viewer5.handlers.mdx.Light; +import com.etheller.warsmash.viewer5.handlers.mdx.LightInstance; + +public class W3xScenePortraitLightManager implements SceneLightManager, W3xSceneLightManager { + private final ModelViewer viewer; + private final Vector3 hardcodedLightDirection; + public final List lights; + private FloatBuffer lightDataCopyHeap; + private final DataTexture unitLightsTexture; + private int unitLightCount; + + public W3xScenePortraitLightManager(final ModelViewer viewer, final Vector3 lightDirection) { + this.viewer = viewer; + this.hardcodedLightDirection = lightDirection; + this.lights = new ArrayList<>(); + this.unitLightsTexture = new DataTexture(viewer.gl, 4, 4, 1); + this.lightDataCopyHeap = ByteBuffer.allocateDirect(16 * 1 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); + } + + @Override + public void add(final SceneLightInstance lightInstance) { + // TODO redesign to avoid cast + final LightInstance mdxLight = (LightInstance) lightInstance; + this.lights.add(mdxLight); + } + + @Override + public void remove(final SceneLightInstance lightInstance) { + // TODO redesign to avoid cast + final LightInstance mdxLight = (LightInstance) lightInstance; + this.lights.remove(mdxLight); + } + + @Override + public void update() { + final int numberOfLights = this.lights.size() + 1; + final int bytesNeeded = numberOfLights * 4 * 16; + if (bytesNeeded > (this.lightDataCopyHeap.capacity() * 4)) { + this.lightDataCopyHeap = ByteBuffer.allocateDirect(bytesNeeded).order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + this.unitLightsTexture.reserve(4, numberOfLights); + } + + this.unitLightCount = 0; + this.lightDataCopyHeap.clear(); + int offset = 0; + this.lightDataCopyHeap.put(offset, this.hardcodedLightDirection.y); + this.lightDataCopyHeap.put(offset + 1, -this.hardcodedLightDirection.x); + this.lightDataCopyHeap.put(offset + 2, -this.hardcodedLightDirection.z); + this.lightDataCopyHeap.put(offset + 3, -this.hardcodedLightDirection.z); + this.lightDataCopyHeap.put(offset + 4, Light.Type.DIRECTIONAL.ordinal()); + this.lightDataCopyHeap.put(offset + 5, 1); + this.lightDataCopyHeap.put(offset + 6, 2); + this.lightDataCopyHeap.put(offset + 7, 0); + this.lightDataCopyHeap.put(offset + 8, 1); + this.lightDataCopyHeap.put(offset + 9, 1); + this.lightDataCopyHeap.put(offset + 10, 1); + this.lightDataCopyHeap.put(offset + 11, 1); + this.lightDataCopyHeap.put(offset + 12, 1); + this.lightDataCopyHeap.put(offset + 13, 1); + this.lightDataCopyHeap.put(offset + 14, 1); + this.lightDataCopyHeap.put(offset + 15, 0.3f); + offset += 16; + this.unitLightCount++; + for (final LightInstance light : this.lights) { + light.bind(offset, this.lightDataCopyHeap); + offset += 16; + this.unitLightCount++; + } + this.lightDataCopyHeap.limit(offset); + this.unitLightsTexture.bindAndUpdate(this.lightDataCopyHeap, 4, this.unitLightCount); + } + + @Override + public DataTexture getUnitLightsTexture() { + return this.unitLightsTexture; + } + + @Override + public int getUnitLightCount() { + return this.unitLightCount; + } + + @Override + public DataTexture getTerrainLightsTexture() { + return null; + } + + @Override + public int getTerrainLightCount() { + return 0; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xSceneWorldLightManager.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xSceneWorldLightManager.java new file mode 100644 index 0000000..e8c4ccb --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xSceneWorldLightManager.java @@ -0,0 +1,112 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.viewer5.SceneLightInstance; +import com.etheller.warsmash.viewer5.SceneLightManager; +import com.etheller.warsmash.viewer5.gl.DataTexture; +import com.etheller.warsmash.viewer5.handlers.mdx.LightInstance; + +public class W3xSceneWorldLightManager implements SceneLightManager, W3xSceneLightManager { + public final List lights; + private FloatBuffer lightDataCopyHeap; + private final DataTexture unitLightsTexture; + private final DataTexture terrainLightsTexture; + private final War3MapViewer viewer; + private int terrainLightCount; + private int unitLightCount; + + public W3xSceneWorldLightManager(final War3MapViewer viewer) { + this.viewer = viewer; + this.lights = new ArrayList<>(); + this.unitLightsTexture = new DataTexture(viewer.gl, 4, 4, 1); + this.terrainLightsTexture = new DataTexture(viewer.gl, 4, 4, 1); + this.lightDataCopyHeap = ByteBuffer.allocateDirect(16 * 1 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); + } + + @Override + public void add(final SceneLightInstance lightInstance) { + // TODO redesign to avoid cast + final LightInstance mdxLight = (LightInstance) lightInstance; + this.lights.add(mdxLight); + } + + @Override + public void remove(final SceneLightInstance lightInstance) { + // TODO redesign to avoid cast + final LightInstance mdxLight = (LightInstance) lightInstance; + this.lights.remove(mdxLight); + } + + @Override + public void update() { + final int numberOfLights = this.lights.size() + 1; + final int bytesNeeded = numberOfLights * 4 * 16; + if (bytesNeeded > (this.lightDataCopyHeap.capacity() * 4)) { + this.lightDataCopyHeap = ByteBuffer.allocateDirect(bytesNeeded).order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + this.unitLightsTexture.reserve(4, numberOfLights); + this.terrainLightsTexture.reserve(4, numberOfLights); + } + + this.unitLightCount = 0; + this.lightDataCopyHeap.clear(); + int offset = 0; + if (this.viewer.dncUnit != null) { + if (!this.viewer.dncUnit.lights.isEmpty()) { + this.viewer.dncUnit.lights.get(0).bind(0, this.lightDataCopyHeap); + offset += 16; + this.unitLightCount++; + } + } + for (final LightInstance light : this.lights) { + light.bind(offset, this.lightDataCopyHeap); + offset += 16; + this.unitLightCount++; + } + this.lightDataCopyHeap.limit(offset); + this.unitLightsTexture.bindAndUpdate(this.lightDataCopyHeap, 4, this.unitLightCount); + + this.terrainLightCount = 0; + this.lightDataCopyHeap.clear(); + offset = 0; + if (this.viewer.dncTerrain != null) { + if (!this.viewer.dncTerrain.lights.isEmpty()) { + this.viewer.dncTerrain.lights.get(0).bind(0, this.lightDataCopyHeap); + offset += 16; + this.terrainLightCount++; + } + } + for (final LightInstance light : this.lights) { + light.bind(offset, this.lightDataCopyHeap); + offset += 16; + this.terrainLightCount++; + } + this.lightDataCopyHeap.limit(offset); + this.terrainLightsTexture.bindAndUpdate(this.lightDataCopyHeap, 4, this.terrainLightCount); + } + + @Override + public DataTexture getUnitLightsTexture() { + return this.unitLightsTexture; + } + + @Override + public int getUnitLightCount() { + return this.unitLightCount; + } + + @Override + public DataTexture getTerrainLightsTexture() { + return this.terrainLightsTexture; + } + + @Override + public int getTerrainLightCount() { + return this.terrainLightCount; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xShaders.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xShaders.java new file mode 100644 index 0000000..891120a --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xShaders.java @@ -0,0 +1,93 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import com.etheller.warsmash.viewer5.Shaders; + +public class W3xShaders { + public static final class UberSplat { + private UberSplat() { + } + + public static final String vert = "\r\n" + // + "\r\n" + // + " uniform mat4 u_mvp;\r\n" + // + " uniform sampler2D u_heightMap;\r\n" + // + " uniform vec2 u_pixel;\r\n" + // + " uniform vec2 u_size;\r\n" + // + " uniform vec2 u_shadowPixel;\r\n" + // + " uniform vec2 u_centerOffset;\r\n" + // + " uniform sampler2D u_lightTexture;\r\n" + // + " uniform float u_lightCount;\r\n" + // + " uniform float u_lightTextureHeight;\r\n" + // + " attribute vec3 a_position;\r\n" + // + " attribute vec2 a_uv;\r\n" + // + " attribute float a_absoluteHeight;\r\n" + // + " varying vec2 v_uv;\r\n" + // + " varying vec2 v_suv;\r\n" + // + " varying vec3 v_normal;\r\n" + // + " varying float a_positionHeight;\r\n" + // + " varying vec3 shadeColor;\r\n" + // + " const float normalDist = 0.25;\r\n" + // + " void main() {\r\n" + // + " vec2 halfPixel = u_pixel * 0.5;\r\n" + // + " vec2 base = (a_position.xy - u_centerOffset) / 128.0;\r\n" + // + " float height;\r\n" + // + " float hL;\r\n" + // + " float hR;\r\n" + // + " float hD;\r\n" + // + " float hU;\r\n" + // + " if (a_absoluteHeight < -256.0) {\r\n" + // + " height = texture2D(u_heightMap, base * u_pixel + halfPixel).r * 128.0;\r\n" + // + " hL = texture2D(u_heightMap, vec2(base - vec2(normalDist, 0.0)) * u_pixel + halfPixel).r;\r\n" + // + " hR = texture2D(u_heightMap, vec2(base + vec2(normalDist, 0.0)) * u_pixel + halfPixel).r;\r\n" + // + " hD = texture2D(u_heightMap, vec2(base - vec2(0.0, normalDist)) * u_pixel + halfPixel).r;\r\n" + // + " hU = texture2D(u_heightMap, vec2(base + vec2(0.0, normalDist)) * u_pixel + halfPixel).r;\r\n" + // + " } else {\r\n" + // + " height = a_absoluteHeight;\r\n" + // + " hL = a_absoluteHeight;\r\n" + // + " hR = a_absoluteHeight;\r\n" + // + " hD = a_absoluteHeight;\r\n" + // + " hU = a_absoluteHeight;\r\n" + // + " }\r\n" + // + " v_normal = normalize(vec3(hL - hR, hD - hU, normalDist * 2.0));\r\n" + // + " v_uv = a_uv;\r\n" + // + " v_suv = base / u_size;\r\n" + // + " vec3 myposition = vec3(a_position.xy, height + a_position.z);\r\n" + // + " gl_Position = u_mvp * vec4(myposition.xyz, 1.0);\r\n" + // + " a_positionHeight = a_position.z;\r\n" + // + Shaders.lightSystem("v_normal", "myposition", "u_lightTexture", "u_lightTextureHeight", "u_lightCount", + true) + + "\r\n" + // + " shadeColor = clamp(lightFactor, 0.0, 1.0);\r\n" + // + " }\r\n" + // + " "; + + public static final String frag = "\r\n" + // + " uniform sampler2D u_texture;\r\n" + // + " uniform sampler2D u_shadowMap;\r\n" + // + " uniform vec4 u_color;\r\n" + // + " uniform bool u_show_lighting;\r\n" + // + " varying vec2 v_uv;\r\n" + // + " varying vec2 v_suv;\r\n" + // + " varying vec3 v_normal;\r\n" + // + " varying float a_positionHeight;\r\n" + // + " varying vec3 shadeColor;\r\n" + // + // " const vec3 lightDirection = normalize(vec3(-0.3, -0.3, 0.25));\r\n" + // + " void main() {\r\n" + // + " if (any(bvec4(lessThan(v_uv, vec2(0.0)), greaterThan(v_uv, vec2(1.0))))) {\r\n" + // + " discard;\r\n" + // + " }\r\n" + // + " vec4 color = texture2D(u_texture, clamp(v_uv, 0.0, 1.0)).rgba * u_color;\r\n" + // + " float shadow = texture2D(u_shadowMap, v_suv).r;\r\n" + // + // " color.xyz *= clamp(dot(v_normal, lightDirection) + 0.45, 0.0, 1.0);\r\n" + + // // + " if (a_positionHeight <= 4.0) {;\r\n" + // + " color.xyz *= 1.0 - shadow;\r\n" + // + " };\r\n" + // + " if (u_show_lighting) {;\r\n" + // + " color.xyz *= shadeColor;\r\n" + // + " };\r\n" + // + " gl_FragColor = color;\r\n" + // + " }\r\n" + // + " "; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xShadersWebGLDeprecated.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xShadersWebGLDeprecated.java new file mode 100644 index 0000000..1562e36 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/W3xShadersWebGLDeprecated.java @@ -0,0 +1,230 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +public class W3xShadersWebGLDeprecated { + public static final class Cliffs { + private Cliffs() { + } + + public static final String vert = "\r\n" + // + "uniform mat4 u_VP;\r\n" + // + "uniform sampler2D u_heightMap;\r\n" + // + "uniform vec2 u_pixel;\r\n" + // + "uniform vec2 u_centerOffset;\r\n" + // + "attribute vec3 a_position;\r\n" + // + "attribute vec3 a_normal;\r\n" + // + "attribute vec2 a_uv;\r\n" + // + "attribute vec3 a_instancePosition;\r\n" + // + "attribute float a_instanceTexture;\r\n" + // + "varying vec3 v_normal;\r\n" + // + "varying vec2 v_uv;\r\n" + // + "varying float v_texture;\r\n" + // + "varying vec3 v_position;\r\n" + // + "void main() {\r\n" + // + " // Half of a pixel in the cliff height map.\r\n" + // + " vec2 halfPixel = u_pixel * 0.5;\r\n" + // + " // The bottom left corner of the map tile this vertex is on.\r\n" + // + " vec2 corner = floor((a_instancePosition.xy - vec2(1.0, 0.0) - u_centerOffset.xy) / 128.0);\r\n" + // + " // Get the 4 closest heights in the height map.\r\n" + // + " float bottomLeft = texture2D(u_heightMap, corner * u_pixel + halfPixel).a;\r\n" + // + " float bottomRight = texture2D(u_heightMap, (corner + vec2(1.0, 0.0)) * u_pixel + halfPixel).a;\r\n" + // + " float topLeft = texture2D(u_heightMap, (corner + vec2(0.0, 1.0)) * u_pixel + halfPixel).a;\r\n" + // + " float topRight = texture2D(u_heightMap, (corner + vec2(1.0, 1.0)) * u_pixel + halfPixel).a;\r\n" + // + " \r\n" + // + " // Do a bilinear interpolation between the heights to get the final value.\r\n" + // + " float bottom = mix(bottomRight, bottomLeft, -a_position.x / 128.0);\r\n" + // + " float top = mix(topRight, topLeft, -a_position.x / 128.0);\r\n" + // + " float height = mix(bottom, top, a_position.y / 128.0);\r\n" + // + " v_normal = a_normal;\r\n" + // + " v_uv = a_uv;\r\n" + // + " v_texture = a_instanceTexture;\r\n" + // + " v_position = a_position + vec3(a_instancePosition.xy, a_instancePosition.z + height * 128.0);\r\n" + // + " gl_Position = u_VP * vec4(v_position, 1.0);\r\n" + // + "}\r\n" + // + ""; + + public static final String frag = "\r\n" + // + "// #extension GL_OES_standard_derivatives : enable\r\n" + // + "precision mediump float;\r\n" + // + "uniform sampler2D u_texture1;\r\n" + // + "uniform sampler2D u_texture2;\r\n" + // + "varying vec3 v_normal;\r\n" + // + "varying vec2 v_uv;\r\n" + // + "varying float v_texture;\r\n" + // + "varying vec3 v_position;\r\n" + // + "// const vec3 lightDirection = normalize(vec3(-0.3, -0.3, 0.25));\r\n" + // + "vec4 sample(int texture, vec2 uv) {\r\n" + // + " if (texture == 0) {\r\n" + // + " return texture2D(u_texture1, uv);\r\n" + // + " } else {\r\n" + // + " return texture2D(u_texture2, uv);\r\n" + // + " }\r\n" + // + "}\r\n" + // + "void main() {\r\n" + // + " vec4 color = sample(int(v_texture), v_uv);\r\n" + // + " // vec3 faceNormal = cross(dFdx(v_position), dFdy(v_position));\r\n" + // + " // vec3 normal = normalize((faceNormal + v_normal) * 0.5);\r\n" + // + " // color *= clamp(dot(normal, lightDirection) + 0.45, 0.1, 1.0);\r\n" + // + " gl_FragColor = color;\r\n" + // + "}\r\n" + // + ""; + } + + public static final class Ground { + private Ground() { + } + + public static final String frag = "\r\n" + // + "precision mediump float;\r\n" + // + "uniform sampler2D u_tilesets[15];\r\n" + // + "varying vec4 v_tilesets;\r\n" + // + "varying vec2 v_uv[4];\r\n" + // + "varying vec3 v_normal;\r\n" + // + "const vec3 lightDirection = normalize(vec3(-0.3, -0.3, 0.25));\r\n" + // + "vec4 sample(float tileset, vec2 uv) {\r\n" + // + " if (tileset == 0.0) {\r\n" + // + " return texture2D(u_tilesets[0], uv);\r\n" + // + " } else if (tileset == 1.0) {\r\n" + // + " return texture2D(u_tilesets[1], uv);\r\n" + // + " } else if (tileset == 2.0) {\r\n" + // + " return texture2D(u_tilesets[2], uv);\r\n" + // + " } else if (tileset == 3.0) {\r\n" + // + " return texture2D(u_tilesets[3], uv);\r\n" + // + " } else if (tileset == 4.0) {\r\n" + // + " return texture2D(u_tilesets[4], uv);\r\n" + // + " } else if (tileset == 5.0) {\r\n" + // + " return texture2D(u_tilesets[5], uv);\r\n" + // + " } else if (tileset == 6.0) {\r\n" + // + " return texture2D(u_tilesets[6], uv);\r\n" + // + " } else if (tileset == 7.0) {\r\n" + // + " return texture2D(u_tilesets[7], uv);\r\n" + // + " } else if (tileset == 8.0) {\r\n" + // + " return texture2D(u_tilesets[8], uv);\r\n" + // + " } else if (tileset == 9.0) {\r\n" + // + " return texture2D(u_tilesets[9], uv);\r\n" + // + " } else if (tileset == 10.0) {\r\n" + // + " return texture2D(u_tilesets[10], uv);\r\n" + // + " } else if (tileset == 11.0) {\r\n" + // + " return texture2D(u_tilesets[11], uv);\r\n" + // + " } else if (tileset == 12.0) {\r\n" + // + " return texture2D(u_tilesets[12], uv);\r\n" + // + " } else if (tileset == 13.0) {\r\n" + // + " return texture2D(u_tilesets[13], uv);\r\n" + // + " } else if (tileset == 14.0) {\r\n" + // + " return texture2D(u_tilesets[14], uv);\r\n" + // + " }\r\n" + // + "}\r\n" + // + "vec4 blend(vec4 color, float tileset, vec2 uv) {\r\n" + // + " vec4 texel = sample(tileset, uv);\r\n" + // + " return mix(color, texel, texel.a);\r\n" + // + "}\r\n" + // + "void main() {\r\n" + // + " vec4 color = sample(v_tilesets[0] - 1.0, v_uv[0]);\r\n" + // + " if (v_tilesets[1] > 0.5) {\r\n" + // + " color = blend(color, v_tilesets[1] - 1.0, v_uv[1]);\r\n" + // + " }\r\n" + // + " if (v_tilesets[2] > 0.5) {\r\n" + // + " color = blend(color, v_tilesets[2] - 1.0, v_uv[2]);\r\n" + // + " }\r\n" + // + " if (v_tilesets[3] > 0.5) {\r\n" + // + " color = blend(color, v_tilesets[3] - 1.0, v_uv[3]);\r\n" + // + " }\r\n" + // + " // color *= clamp(dot(v_normal, lightDirection) + 0.45, 0.0, 1.0);\r\n" + // + " gl_FragColor = color;\r\n" + // + "}\r\n" + // + ""; + + public static final String vert = "\r\n" + // + "uniform mat4 u_VP;\r\n" + // + "uniform sampler2D u_heightMap;\r\n" + // + "uniform vec2 u_pixel;\r\n" + // + "uniform vec2 u_centerOffset;\r\n" + // + "attribute vec3 a_position;\r\n" + // + "attribute vec3 a_normal;\r\n" + // + "attribute vec2 a_uv;\r\n" + // + "attribute vec3 a_instancePosition;\r\n" + // + "attribute float a_instanceTexture;\r\n" + // + "varying vec3 v_normal;\r\n" + // + "varying vec2 v_uv;\r\n" + // + "varying float v_texture;\r\n" + // + "varying vec3 v_position;\r\n" + // + "void main() {\r\n" + // + " // Half of a pixel in the cliff height map.\r\n" + // + " vec2 halfPixel = u_pixel * 0.5;\r\n" + // + " // The bottom left corner of the map tile this vertex is on.\r\n" + // + " vec2 corner = floor((a_instancePosition.xy - vec2(1.0, 0.0) - u_centerOffset.xy) / 128.0);\r\n" + // + " // Get the 4 closest heights in the height map.\r\n" + // + " float bottomLeft = texture2D(u_heightMap, corner * u_pixel + halfPixel).a;\r\n" + // + " float bottomRight = texture2D(u_heightMap, (corner + vec2(1.0, 0.0)) * u_pixel + halfPixel).a;\r\n" + // + " float topLeft = texture2D(u_heightMap, (corner + vec2(0.0, 1.0)) * u_pixel + halfPixel).a;\r\n" + // + " float topRight = texture2D(u_heightMap, (corner + vec2(1.0, 1.0)) * u_pixel + halfPixel).a;\r\n" + // + " \r\n" + // + " // Do a bilinear interpolation between the heights to get the final value.\r\n" + // + " float bottom = mix(bottomRight, bottomLeft, -a_position.x / 128.0);\r\n" + // + " float top = mix(topRight, topLeft, -a_position.x / 128.0);\r\n" + // + " float height = mix(bottom, top, a_position.y / 128.0);\r\n" + // + " v_normal = a_normal;\r\n" + // + " v_uv = a_uv;\r\n" + // + " v_texture = a_instanceTexture;\r\n" + // + " v_position = a_position + vec3(a_instancePosition.xy, a_instancePosition.z + height * 128.0);\r\n" + // + " gl_Position = u_VP * vec4(v_position, 1.0);\r\n" + // + "}\r\n" + // + ""; + } + + public static final class Water { + private Water() { + } + + public static final String frag = "\r\n" + // + "precision mediump float;\r\n" + // + "uniform sampler2D u_waterTexture;\r\n" + // + "varying vec2 v_uv;\r\n" + // + "varying vec4 v_color;\r\n" + // + "void main() {\r\n" + // + " gl_FragColor = texture2D(u_waterTexture, v_uv) * v_color;\r\n" + // + "}\r\n" + // + ""; + public static final String vert = "\r\n" + // + "uniform mat4 u_VP;\r\n" + // + "uniform sampler2D u_heightMap;\r\n" + // + "uniform sampler2D u_waterHeightMap;\r\n" + // + "uniform vec2 u_size;\r\n" + // + "uniform vec2 u_offset;\r\n" + // + "uniform float u_offsetHeight;\r\n" + // + "uniform vec4 u_minDeepColor;\r\n" + // + "uniform vec4 u_maxDeepColor;\r\n" + // + "uniform vec4 u_minShallowColor;\r\n" + // + "uniform vec4 u_maxShallowColor;\r\n" + // + "attribute vec2 a_position;\r\n" + // + "attribute float a_InstanceID;\r\n" + // + "attribute float a_isWater;\r\n" + // + "varying vec2 v_uv;\r\n" + // + "varying vec4 v_color;\r\n" + // + "const float minDepth = 10.0 / 128.0;\r\n" + // + "const float deepLevel = 64.0 / 128.0;\r\n" + // + "const float maxDepth = 72.0 / 128.0;\r\n" + // + "void main() {\r\n" + // + " if (a_isWater > 0.5) {\r\n" + // + " v_uv = a_position;\r\n" + // + " vec2 corner = vec2(mod(a_InstanceID, u_size.x), floor(a_InstanceID / u_size.x));\r\n" + // + " vec2 base = corner + a_position;\r\n" + // + " float height = texture2D(u_heightMap, base / u_size).a;\r\n" + // + " float waterHeight = texture2D(u_waterHeightMap, base / u_size).a + u_offsetHeight;\r\n" + // + " float value = clamp(waterHeight - height, 0.0, 1.0);\r\n" + // + " if (value <= deepLevel) {\r\n" + // + " value = max(0.0, value - minDepth) / (deepLevel - minDepth);\r\n" + // + " v_color = mix(u_minShallowColor, u_maxShallowColor, value) / 255.0;\r\n" + // + " } else {\r\n" + // + " value = clamp(value - deepLevel, 0.0, maxDepth - deepLevel) / (maxDepth - deepLevel);\r\n" + // + " v_color = mix(u_minDeepColor, u_maxDeepColor, value) / 255.0;\r\n" + // + " }\r\n" + // + " gl_Position = u_VP * vec4(base * 128.0 + u_offset, waterHeight * 128.0, 1.0);\r\n" + // + " } else {\r\n" + // + " v_uv = vec2(0.0);\r\n" + // + " v_color = vec4(0.0);\r\n" + // + " gl_Position = vec4(0.0);\r\n" + // + " }\r\n" + // + "}\r\n" + // + ""; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/War3MapViewer.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/War3MapViewer.java new file mode 100644 index 0000000..dbd628d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/War3MapViewer.java @@ -0,0 +1,1945 @@ +package com.etheller.warsmash.viewer5.handlers.w3x; + +import java.awt.image.BufferedImage; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.nio.channels.SeekableByteChannel; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.function.Consumer; + +import javax.imageio.ImageIO; + +import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.math.Vector3; +import com.badlogic.gdx.math.collision.BoundingBox; +import com.badlogic.gdx.math.collision.Ray; +import com.etheller.warsmash.common.FetchDataTypeName; +import com.etheller.warsmash.common.LoadGenericCallback; +import com.etheller.warsmash.datasources.CompoundDataSource; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.datasources.MpqDataSource; +import com.etheller.warsmash.datasources.SubdirDataSource; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.w3x.War3Map; +import com.etheller.warsmash.parsers.w3x.doo.War3MapDoo; +import com.etheller.warsmash.parsers.w3x.objectdata.Warcraft3MapObjectData; +import com.etheller.warsmash.parsers.w3x.unitsdoo.War3MapUnitsDoo; +import com.etheller.warsmash.parsers.w3x.w3e.War3MapW3e; +import com.etheller.warsmash.parsers.w3x.w3i.Player; +import com.etheller.warsmash.parsers.w3x.w3i.War3MapW3i; +import com.etheller.warsmash.parsers.w3x.wpm.War3MapWpm; +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.units.Element; +import com.etheller.warsmash.units.StandardObjectData; +import com.etheller.warsmash.units.custom.WTS; +import com.etheller.warsmash.units.manager.MutableObjectData; +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.units.manager.MutableObjectData.WorldEditorDataType; +import com.etheller.warsmash.util.MappedData; +import com.etheller.warsmash.util.Quadtree; +import com.etheller.warsmash.util.QuadtreeIntersector; +import com.etheller.warsmash.util.RenderMathUtils; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.util.WorldEditStrings; +import com.etheller.warsmash.viewer5.CanvasProvider; +import com.etheller.warsmash.viewer5.GenericResource; +import com.etheller.warsmash.viewer5.Grid; +import com.etheller.warsmash.viewer5.ModelInstance; +import com.etheller.warsmash.viewer5.PathSolver; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.SceneLightManager; +import com.etheller.warsmash.viewer5.Texture; +import com.etheller.warsmash.viewer5.WorldScene; +import com.etheller.warsmash.viewer5.gl.WebGL; +import com.etheller.warsmash.viewer5.handlers.AbstractMdxModelViewer; +import com.etheller.warsmash.viewer5.handlers.mdx.Attachment; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxHandler; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxNode; +import com.etheller.warsmash.viewer5.handlers.mdx.SequenceLoopMode; +import com.etheller.warsmash.viewer5.handlers.tga.TgaFile; +import com.etheller.warsmash.viewer5.handlers.w3x.SplatModel.SplatMover; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.BuildingShadow; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.RemovablePathingMapInstance; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.Terrain; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.Terrain.Splat; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderAttackInstant; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderAttackProjectile; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderDestructable; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderDoodad; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderEffect; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderItem; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderUnitTypeData; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.ability.AbilityDataUI; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.ability.AbilityUI; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CDestructable; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitClassification; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidgetFilterFunction; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackInstant; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackListener; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackMissile; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.projectile.CAttackProjectile; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.config.CBasePlayer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.config.War3MapConfig; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CMapControl; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CRacePreference; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.ResourceType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.SimulationRenderController; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.SettableCommandErrorListener; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.sound.KeyedSounds; + +import mpq.MPQArchive; +import mpq.MPQException; + +public class War3MapViewer extends AbstractMdxModelViewer { + private static final War3ID ABILITY_HERO_RAWCODE = War3ID.fromString("AHer"); + private static final Color PLACEHOLDER_LUMBER_COLOR = new Color(0.0f, 200f / 255f, 80f / 255f, 1.0f); + private static final Color PLACEHOLDER_GOLD_COLOR = new Color(1.0f, 220f / 255f, 0f, 1.0f); + private static final War3ID UNIT_FILE = War3ID.fromString("umdl"); + private static final War3ID UNIT_SPECIAL = War3ID.fromString("uspa"); + private static final War3ID UBER_SPLAT = War3ID.fromString("uubs"); + private static final War3ID UNIT_SHADOW = War3ID.fromString("ushu"); + private static final War3ID UNIT_SHADOW_X = War3ID.fromString("ushx"); + private static final War3ID UNIT_SHADOW_Y = War3ID.fromString("ushy"); + private static final War3ID UNIT_SHADOW_W = War3ID.fromString("ushw"); + private static final War3ID UNIT_SHADOW_H = War3ID.fromString("ushh"); + private static final War3ID BUILDING_SHADOW = War3ID.fromString("ushb"); + public static final War3ID UNIT_SELECT_SCALE = War3ID.fromString("ussc"); + private static final War3ID UNIT_SOUNDSET = War3ID.fromString("usnd"); + private static final War3ID ITEM_FILE = War3ID.fromString("ifil"); + private static final War3ID UNIT_PATHING = War3ID.fromString("upat"); + private static final War3ID DESTRUCTABLE_PATHING = War3ID.fromString("bptx"); + private static final War3ID DESTRUCTABLE_PATHING_DEATH = War3ID.fromString("bptd"); + private static final War3ID ELEVATION_SAMPLE_RADIUS = War3ID.fromString("uerd"); + private static final War3ID MAX_PITCH = War3ID.fromString("umxp"); + private static final War3ID ALLOW_CUSTOM_TEAM_COLOR = War3ID.fromString("utcc"); + private static final War3ID TEAM_COLOR = War3ID.fromString("utco"); + private static final War3ID MAX_ROLL = War3ID.fromString("umxr"); + private static final War3ID sloc = War3ID.fromString("sloc"); + private static final LoadGenericCallback stringDataCallback = new StringDataCallbackImplementation(); + private static final float[] rayHeap = new float[6]; + public static final Ray gdxRayHeap = new Ray(); + private static final Vector2 mousePosHeap = new Vector2(); + private static final Vector3 normalHeap = new Vector3(); + public static final Vector3 intersectionHeap = new Vector3(); + private static final Rectangle rectangleHeap = new Rectangle(); + public static final StreamDataCallbackImplementation streamDataCallback = new StreamDataCallbackImplementation(); + private static final boolean ENABLE_WORLDEDIT_AS_GAMEPLAY_DATA_HACK = true; + + public WorldScene worldScene; + public boolean anyReady; + public MappedData terrainData = new MappedData(); + public MappedData cliffTypesData = new MappedData(); + public MappedData waterData = new MappedData(); + public boolean terrainReady; + public boolean cliffsReady; + public boolean doodadsAndDestructiblesLoaded; + public MappedData doodadsData = new MappedData(); + public MappedData doodadMetaData = new MappedData(); + public MappedData destructableMetaData = new MappedData(); + public List doodads = new ArrayList<>(); + public List terrainDoodads = new ArrayList<>(); + public boolean doodadsReady; + public boolean unitsAndItemsLoaded; + public MappedData unitsData = new MappedData(); + public MappedData unitMetaData = new MappedData(); + public List widgets = new ArrayList<>(); + public List units = new ArrayList<>(); + public List projectiles = new ArrayList<>(); + public boolean unitsReady; + public War3Map mapMpq; + + private final DataSource gameDataSource; + + public Terrain terrain; + public int renderPathing = 0; + public int renderLighting = 1; + + public List selModels = new ArrayList<>(); + public List selected = new ArrayList<>(); + private DataTable unitAckSoundsTable; + private DataTable unitCombatSoundsTable; + public DataTable miscData; + private DataTable unitGlobalStrings; + public DataTable uiSoundsTable; + private MdxComplexInstance confirmationInstance; + public MdxComplexInstance dncUnit; + public MdxComplexInstance dncUnitDay; + public MdxComplexInstance dncTerrain; + public MdxComplexInstance dncTarget; + public CSimulation simulation; + private float updateTime; + + // for World Editor, I think + public Vector2[] startLocations = new Vector2[WarsmashConstants.MAX_PLAYERS]; + + private final DynamicShadowManager dynamicShadowManager = new DynamicShadowManager(); + + private final Random seededRandom = new Random(1337L); + + private final Map filePathToPathingMap = new HashMap<>(); + + private final List selectionCircleSizes = new ArrayList<>(); + + private final Map unitToRenderPeer = new HashMap<>(); + private final Map destructableToRenderPeer = new HashMap<>(); + private final Map itemToRenderPeer = new HashMap<>(); + private final Map unitIdToTypeData = new HashMap<>(); + private GameUI gameUI; + private Vector3 lightDirection; + + private Quadtree walkableObjectsTree; + private final QuadtreeIntersectorFindsWalkableRenderHeight walkablesIntersector = new QuadtreeIntersectorFindsWalkableRenderHeight(); + private final QuadtreeIntersectorFindsHitPoint walkablesIntersectionFinder = new QuadtreeIntersectorFindsHitPoint(); + private final QuadtreeIntersectorFindsHighestWalkable intersectorFindsHighestWalkable = new QuadtreeIntersectorFindsHighestWalkable(); + + private KeyedSounds uiSounds; + private int localPlayerIndex; + private final SettableCommandErrorListener commandErrorListener; + + public final List textTags = new ArrayList<>(); + + private final War3MapConfig mapConfig; + + public War3MapViewer(final DataSource dataSource, final CanvasProvider canvas, final War3MapConfig mapConfig) { + super(dataSource, canvas); + this.gameDataSource = dataSource; + + final WebGL webGL = this.webGL; + + this.addHandler(new MdxHandler()); + + this.wc3PathSolver = PathSolver.DEFAULT; + + this.worldScene = this.addWorldScene(); + + if (!this.dynamicShadowManager.setup(webGL)) { + throw new IllegalStateException("FrameBuffer setup failed"); + } + + this.commandErrorListener = new SettableCommandErrorListener(); + this.mapConfig = mapConfig; + } + + public void loadSLKs(final WorldEditStrings worldEditStrings) throws IOException { + final GenericResource terrain = this.loadMapGeneric("TerrainArt\\Terrain.slk", FetchDataTypeName.SLK, + stringDataCallback); + final GenericResource cliffTypes = this.loadMapGeneric("TerrainArt\\CliffTypes.slk", FetchDataTypeName.SLK, + stringDataCallback); + final GenericResource water = this.loadMapGeneric("TerrainArt\\Water.slk", FetchDataTypeName.SLK, + stringDataCallback); + + // == when loaded, which is always in our system == + this.terrainData.load(terrain.data.toString()); + this.cliffTypesData.load(cliffTypes.data.toString()); + this.waterData.load(water.data.toString()); + // emit terrain loaded?? + + final GenericResource doodads = this.loadMapGeneric("Doodads\\Doodads.slk", FetchDataTypeName.SLK, + stringDataCallback); + final GenericResource doodadMetaData = this.loadMapGeneric("Doodads\\DoodadMetaData.slk", FetchDataTypeName.SLK, + stringDataCallback); + final GenericResource destructableData = this.loadMapGeneric("Units\\DestructableData.slk", + FetchDataTypeName.SLK, stringDataCallback); + final GenericResource destructableMetaData = this.loadMapGeneric("Units\\DestructableMetaData.slk", + FetchDataTypeName.SLK, stringDataCallback); + + // == when loaded, which is always in our system == + this.doodadsAndDestructiblesLoaded = true; + this.doodadsData.load(doodads.data.toString()); + this.doodadMetaData.load(doodadMetaData.data.toString()); + this.doodadsData.load(destructableData.data.toString()); + this.destructableMetaData.load(destructableData.data.toString()); + // emit doodads loaded + + final GenericResource unitData = this.loadMapGeneric("Units\\UnitData.slk", FetchDataTypeName.SLK, + stringDataCallback); + final GenericResource unitUi = this.loadMapGeneric("Units\\unitUI.slk", FetchDataTypeName.SLK, + stringDataCallback); + final GenericResource itemData = this.loadMapGeneric("Units\\ItemData.slk", FetchDataTypeName.SLK, + stringDataCallback); + final GenericResource unitMetaData = this.loadMapGeneric("Units\\UnitMetaData.slk", FetchDataTypeName.SLK, + stringDataCallback); + + // == when loaded, which is always in our system == + this.unitsAndItemsLoaded = true; + this.unitsData.load(unitData.data.toString()); + this.unitsData.load(unitUi.data.toString()); + this.unitsData.load(itemData.data.toString()); + this.unitMetaData.load(unitMetaData.data.toString()); + // emit loaded + + this.unitAckSoundsTable = new DataTable(worldEditStrings); + try (InputStream terrainSlkStream = this.dataSource.getResourceAsStream("UI\\SoundInfo\\UnitAckSounds.slk")) { + this.unitAckSoundsTable.readSLK(terrainSlkStream); + } + this.unitCombatSoundsTable = new DataTable(worldEditStrings); + try (InputStream terrainSlkStream = this.dataSource + .getResourceAsStream("UI\\SoundInfo\\UnitCombatSounds.slk")) { + this.unitCombatSoundsTable.readSLK(terrainSlkStream); + } + this.miscData = new DataTable(worldEditStrings); + try (InputStream miscDataTxtStream = this.dataSource.getResourceAsStream("UI\\MiscData.txt")) { + this.miscData.readTXT(miscDataTxtStream, true); + } + try (InputStream miscDataTxtStream = this.dataSource.getResourceAsStream("Units\\MiscData.txt")) { + this.miscData.readTXT(miscDataTxtStream, true); + } + try (InputStream miscDataTxtStream = this.dataSource.getResourceAsStream("Units\\MiscGame.txt")) { + this.miscData.readTXT(miscDataTxtStream, true); + } + try (InputStream miscDataTxtStream = this.dataSource.getResourceAsStream("UI\\MiscUI.txt")) { + this.miscData.readTXT(miscDataTxtStream, true); + } + try (InputStream miscDataTxtStream = this.dataSource.getResourceAsStream("UI\\SoundInfo\\MiscData.txt")) { + this.miscData.readTXT(miscDataTxtStream, true); + } + if (this.dataSource.has("war3mapMisc.txt")) { + try (InputStream miscDataTxtStream = this.dataSource.getResourceAsStream("war3mapMisc.txt")) { + this.miscData.readTXT(miscDataTxtStream, true); + } + } + final Element misc = this.miscData.get("Misc"); + // TODO Find the upkeep constants inside the assets files ????? + if (!misc.hasField("UpkeepUsage")) { + misc.setField("UpkeepUsage", "50,80,10000,10000,10000,10000,10000,10000,10000,10000"); + } + if (!misc.hasField("UpkeepGoldTax")) { + misc.setField("UpkeepGoldTax", "0.00,0.30,0.60,0.60,0.60,0.60,0.60,0.60,0.60,0.60"); + } + if (!misc.hasField("UpkeepLumberTax")) { + misc.setField("UpkeepLumberTax", "0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00"); + } + final Element light = this.miscData.get("Light"); + final float lightX = light.getFieldFloatValue("Direction", 0); + final float lightY = light.getFieldFloatValue("Direction", 1); + final float lightZ = light.getFieldFloatValue("Direction", 2); + this.lightDirection = new Vector3(lightX, lightY, lightZ).nor(); + this.unitGlobalStrings = new DataTable(worldEditStrings); + try (InputStream miscDataTxtStream = this.dataSource.getResourceAsStream("Units\\UnitGlobalStrings.txt")) { + this.unitGlobalStrings.readTXT(miscDataTxtStream, true); + } + final Element categories = this.unitGlobalStrings.get("Categories"); + for (final CUnitClassification unitClassification : CUnitClassification.values()) { + if (unitClassification.getLocaleKey() != null) { + final String displayName = categories.getField(unitClassification.getLocaleKey()); + unitClassification.setDisplayName(displayName); + } + } + this.selectionCircleSizes.clear(); + final Element selectionCircleData = this.miscData.get("SelectionCircle"); + final int selectionCircleNumSizes = selectionCircleData.getFieldValue("NumSizes"); + for (int i = 0; i < selectionCircleNumSizes; i++) { + final String indexString = i < 10 ? "0" + i : Integer.toString(i); + final float size = selectionCircleData.getFieldFloatValue("Size" + indexString); + final String texture = selectionCircleData.getField("Texture" + indexString); + final String textureDotted = selectionCircleData.getField("TextureDotted" + indexString); + this.selectionCircleSizes.add(new SelectionCircleSize(size, texture, textureDotted)); + } + this.selectionCircleScaleFactor = selectionCircleData.getFieldFloatValue("ScaleFactor"); + this.imageWalkableZOffset = selectionCircleData.getFieldValue("ImageWalkableZOffset"); + + this.uiSoundsTable = new DataTable(worldEditStrings); + try (InputStream miscDataTxtStream = this.dataSource.getResourceAsStream("UI\\SoundInfo\\UISounds.slk")) { + this.uiSoundsTable.readSLK(miscDataTxtStream); + } + try (InputStream miscDataTxtStream = this.dataSource.getResourceAsStream("UI\\SoundInfo\\AmbienceSounds.slk")) { + this.uiSoundsTable.readSLK(miscDataTxtStream); + } + } + + public GenericResource loadMapGeneric(final String path, final FetchDataTypeName dataType, + final LoadGenericCallback callback) { + if (this.mapMpq == null) { + return loadGeneric(path, dataType, callback); + } + else { + return loadGeneric(path, dataType, callback, this.dataSource); + } + } + + public War3Map beginLoadingMap(final String mapFilePath) throws IOException { + return new War3Map(this.gameDataSource, mapFilePath); + } + + public DataTable loadWorldEditData(final War3Map map) { + final StandardObjectData standardObjectData = new StandardObjectData(map); + this.worldEditData = standardObjectData.getWorldEditData(); + return this.worldEditData; + } + + public WTS preloadWTS(final War3Map map) { + try { + this.preloadedWTS = Warcraft3MapObjectData.loadWTS(map); + return this.preloadedWTS; + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + + public void loadMap(final War3Map war3Map, final War3MapW3i w3iFile, final int localPlayerIndex) + throws IOException { + this.localPlayerIndex = localPlayerIndex; + + this.mapMpq = war3Map; + + final PathSolver wc3PathSolver = this.wc3PathSolver; + + char tileset = 'A'; + + if (ENABLE_WORLDEDIT_AS_GAMEPLAY_DATA_HACK) { + int playerIndex = 0; + for (final Player player : w3iFile.getPlayers()) { + final CBasePlayer cfgPlayer = this.mapConfig.getPlayer(playerIndex); + cfgPlayer.setName(player.getName()); + cfgPlayer.setRacePref(CRacePreference.VALUES[player.getRace()]); + cfgPlayer.setController(CMapControl.VALUES[player.getType()]); + playerIndex++; + } + } + + tileset = w3iFile.getTileset(); + + DataSource tilesetSource; + try { + // Slightly complex. Here's the theory: + // 1.) Copy map into RAM + // 2.) Setup a Data Source that will read assets + // from either the map or the game, giving the map priority. + SeekableByteChannel sbc; + final CompoundDataSource compoundDataSource = war3Map.getCompoundDataSource(); + try (InputStream mapStream = compoundDataSource.getResourceAsStream(tileset + ".mpq")) { + if (mapStream == null) { + tilesetSource = new CompoundDataSource(Arrays.asList(compoundDataSource, + new SubdirDataSource(compoundDataSource, tileset + ".mpq/"), + new SubdirDataSource(compoundDataSource, "_tilesets/" + tileset + ".w3mod/"))); + } + else { + final byte[] mapData = IOUtils.toByteArray(mapStream); + sbc = new SeekableInMemoryByteChannel(mapData); + final DataSource internalMpqContentsDataSource = new MpqDataSource(new MPQArchive(sbc), sbc); + tilesetSource = new CompoundDataSource( + Arrays.asList(compoundDataSource, internalMpqContentsDataSource)); + } + } + catch (final IOException exc) { + tilesetSource = new CompoundDataSource( + Arrays.asList(compoundDataSource, new SubdirDataSource(compoundDataSource, tileset + ".mpq/"), + new SubdirDataSource(compoundDataSource, "_tilesets/" + tileset + ".w3mod/"))); + } + } + catch (final MPQException e) { + throw new RuntimeException(e); + } + setDataSource(tilesetSource); + this.worldEditStrings = new WorldEditStrings(this.dataSource); + loadSLKs(this.worldEditStrings); + + this.solverParams.tileset = Character.toLowerCase(tileset); + + final War3MapW3e terrainData = this.mapMpq.readEnvironment(); + + final War3MapWpm terrainPathing = this.mapMpq.readPathing(); + + this.terrain = new Terrain(terrainData, terrainPathing, w3iFile, this.webGL, this.dataSource, + this.worldEditStrings, this, this.worldEditData); + + final float[] centerOffset = terrainData.getCenterOffset(); + final int[] mapSize = terrainData.getMapSize(); + + this.terrainReady = true; + this.anyReady = true; + this.cliffsReady = true; + + // Override the grid based on the map. + this.worldScene.grid = new Grid(centerOffset[0], centerOffset[1], (mapSize[0] * 128) - 128, + (mapSize[1] * 128) - 128, 16 * 128, 16 * 128); + + final MdxModel confirmation = (MdxModel) load("UI\\Feedback\\Confirmation\\Confirmation.mdx", + PathSolver.DEFAULT, null); + this.confirmationInstance = (MdxComplexInstance) confirmation.addInstance(); + this.confirmationInstance.setSequenceLoopMode(SequenceLoopMode.NEVER_LOOP_AND_HIDE_WHEN_DONE); + this.confirmationInstance.setSequence(0); + this.confirmationInstance.setScene(this.worldScene); + + if (this.preloadedWTS != null) { + this.allObjectData = this.mapMpq.readModifications(this.preloadedWTS); + } + else { + this.allObjectData = this.mapMpq.readModifications(); + } + this.simulation = new CSimulation(this.mapConfig, this.miscData, this.allObjectData.getUnits(), + this.allObjectData.getItems(), this.allObjectData.getDestructibles(), this.allObjectData.getAbilities(), + new SimulationRenderController() { + private final Map keyToCombatSound = new HashMap<>(); + + @Override + public CAttackProjectile createAttackProjectile(final CSimulation simulation, final float launchX, + final float launchY, final float launchFacing, final CUnit source, + final CUnitAttackMissile unitAttack, final AbilityTarget target, final float damage, + final int bounceIndex, final CUnitAttackListener attackListener) { + final War3ID typeId = source.getTypeId(); + final int projectileSpeed = unitAttack.getProjectileSpeed(); + final float projectileArc = unitAttack.getProjectileArc(); + String missileArt = unitAttack.getProjectileArt(); + final float projectileLaunchX = simulation.getUnitData().getProjectileLaunchX(typeId); + final float projectileLaunchY = simulation.getUnitData().getProjectileLaunchY(typeId); + final float projectileLaunchZ = simulation.getUnitData().getProjectileLaunchZ(typeId); + + missileArt = mdx(missileArt); + final float facing = launchFacing; + final float sinFacing = (float) Math.sin(facing); + final float cosFacing = (float) Math.cos(facing); + final float x = (launchX + (projectileLaunchY * cosFacing)) + (projectileLaunchX * sinFacing); + final float y = (launchY + (projectileLaunchY * sinFacing)) - (projectileLaunchX * cosFacing); + + final float height = War3MapViewer.this.terrain.getGroundHeight(x, y) + source.getFlyHeight() + + projectileLaunchZ; + final CAttackProjectile simulationAttackProjectile = new CAttackProjectile(x, y, + projectileSpeed, target, source, damage, unitAttack, bounceIndex, attackListener); + + final MdxModel model = (MdxModel) load(missileArt, War3MapViewer.this.mapPathSolver, + War3MapViewer.this.solverParams); + final MdxComplexInstance modelInstance = (MdxComplexInstance) model.addInstance(); + modelInstance.setTeamColor(source.getPlayerIndex()); + modelInstance.setScene(War3MapViewer.this.worldScene); + if (bounceIndex == 0) { + SequenceUtils.randomBirthSequence(modelInstance); + } + else { + SequenceUtils.randomStandSequence(modelInstance); + } + modelInstance.setLocation(x, y, height); + final RenderAttackProjectile renderAttackProjectile = new RenderAttackProjectile( + simulationAttackProjectile, modelInstance, height, projectileArc, War3MapViewer.this); + + War3MapViewer.this.projectiles.add(renderAttackProjectile); + + return simulationAttackProjectile; + } + + @Override + public void createInstantAttackEffect(final CSimulation cSimulation, final CUnit source, + final CUnitAttackInstant unitAttack, final CWidget target) { + final War3ID typeId = source.getTypeId(); + + String missileArt = unitAttack.getProjectileArt(); + final float projectileLaunchX = War3MapViewer.this.simulation.getUnitData() + .getProjectileLaunchX(typeId); + final float projectileLaunchY = War3MapViewer.this.simulation.getUnitData() + .getProjectileLaunchY(typeId); + missileArt = mdx(missileArt); + final float facing = (float) Math.toRadians(source.getFacing()); + final float sinFacing = (float) Math.sin(facing); + final float cosFacing = (float) Math.cos(facing); + final float x = (source.getX() + (projectileLaunchY * cosFacing)) + + (projectileLaunchX * sinFacing); + final float y = (source.getY() + (projectileLaunchY * sinFacing)) + - (projectileLaunchX * cosFacing); + + final float targetX = target.getX(); + final float targetY = target.getY(); + final float angleToTarget = (float) Math.atan2(targetY - y, targetX - x); + + final float height = War3MapViewer.this.terrain.getGroundHeight(targetX, targetY) + + target.getFlyHeight() + target.getImpactZ(); + + final MdxModel model = (MdxModel) load(missileArt, War3MapViewer.this.mapPathSolver, + War3MapViewer.this.solverParams); + final MdxComplexInstance modelInstance = (MdxComplexInstance) model.addInstance(); + modelInstance.setTeamColor(source.getPlayerIndex()); + SequenceUtils.randomBirthSequence(modelInstance); + modelInstance.setLocation(targetX, targetY, height); + modelInstance.setScene(War3MapViewer.this.worldScene); + War3MapViewer.this.projectiles + .add(new RenderAttackInstant(modelInstance, War3MapViewer.this, angleToTarget)); + } + + @Override + public void spawnDamageSound(final CWidget damagedDestructable, final String weaponSound, + final String armorType) { + final RenderWidget damagedWidget = getRenderPeer(damagedDestructable); + if (damagedWidget == null) { + return; + } + final String key = weaponSound + armorType; + UnitSound combatSound = this.keyToCombatSound.get(key); + if (combatSound == null) { + combatSound = UnitSound.create(War3MapViewer.this.dataSource, + War3MapViewer.this.unitCombatSoundsTable, weaponSound, armorType); + this.keyToCombatSound.put(key, combatSound); + } + combatSound.play(War3MapViewer.this.worldScene.audioContext, damagedDestructable.getX(), + damagedDestructable.getY(), damagedWidget.getZ()); + } + + @Override + public void spawnUnitConstructionSound(final CUnit constructingUnit, + final CUnit constructedStructure) { + final UnitSound constructingBuilding = War3MapViewer.this.uiSounds + .getSound(War3MapViewer.this.gameUI.getSkinField("ConstructingBuilding")); + if (constructingBuilding != null) { + constructingBuilding.playUnitResponse(War3MapViewer.this.worldScene.audioContext, + War3MapViewer.this.unitToRenderPeer.get(constructedStructure)); + } + } + + @Override + public void removeUnit(final CUnit unit) { + final RenderUnit renderUnit = War3MapViewer.this.unitToRenderPeer.remove(unit); + War3MapViewer.this.widgets.remove(renderUnit); + War3MapViewer.this.units.remove(renderUnit); + War3MapViewer.this.worldScene.removeInstance(renderUnit.instance); + } + + @Override + public void removeDestructable(final CDestructable dest) { + final RenderDestructable renderPeer = War3MapViewer.this.destructableToRenderPeer.remove(dest); + War3MapViewer.this.worldScene.removeInstance(renderPeer.instance); + if (renderPeer.walkableBounds != null) { + War3MapViewer.this.walkableObjectsTree.remove((MdxComplexInstance) renderPeer.instance, + renderPeer.walkableBounds); + } + } + + @Override + public BufferedImage getBuildingPathingPixelMap(final War3ID rawcode) { + return War3MapViewer.this + .getBuildingPathingPixelMap(War3MapViewer.this.allObjectData.getUnits().get(rawcode)); + } + + @Override + public BufferedImage getDestructablePathingDeathPixelMap(final War3ID rawcode) { + return War3MapViewer.this.getDestructablePathingDeathPixelMap( + War3MapViewer.this.allObjectData.getDestructibles().get(rawcode)); + } + + @Override + public BufferedImage getDestructablePathingPixelMap(final War3ID rawcode) { + return War3MapViewer.this.getDestructablePathingPixelMap( + War3MapViewer.this.allObjectData.getDestructibles().get(rawcode)); + } + + @Override + public void spawnUnitConstructionFinishSound(final CUnit constructedStructure) { + final UnitSound constructingBuilding = War3MapViewer.this.uiSounds + .getSound(War3MapViewer.this.gameUI.getSkinField("JobDoneSound")); + final RenderUnit renderUnit = War3MapViewer.this.unitToRenderPeer.get(constructedStructure); + if (constructingBuilding != null) { + constructingBuilding.play(War3MapViewer.this.worldScene.audioContext, + constructedStructure.getX(), constructedStructure.getY(), renderUnit.getZ()); + } + } + + @Override + public CUnit createUnit(final CSimulation simulation, final War3ID typeId, final int playerIndex, + final float x, final float y, final float facing) { + return createNewUnit(War3MapViewer.this.allObjectData, typeId, x, y, 0f, playerIndex, + playerIndex, (float) Math.toRadians(facing)); + } + + @Override + public void spawnBuildingDeathEffect(final CUnit source) { + final RenderUnit renderUnit = War3MapViewer.this.unitToRenderPeer.get(source); + if (renderUnit.specialArtModel != null) { + final MdxComplexInstance modelInstance = (MdxComplexInstance) renderUnit.specialArtModel + .addInstance(); + modelInstance.setTeamColor(source.getPlayerIndex()); + modelInstance.setLocation(renderUnit.location); + modelInstance.setScene(War3MapViewer.this.worldScene); + SequenceUtils.randomBirthSequence(modelInstance); + War3MapViewer.this.projectiles + .add(new RenderAttackInstant(modelInstance, War3MapViewer.this, + (float) Math.toRadians(renderUnit.getSimulationUnit().getFacing()))); + } + } + + @Override + public void spawnGainLevelEffect(final CUnit source) { + final AbilityUI heroUI = War3MapViewer.this.abilityDataUI.getUI(ABILITY_HERO_RAWCODE); + final RenderUnit renderUnit = War3MapViewer.this.unitToRenderPeer.get(source); + final String heroLevelUpArt = heroUI.getCasterArt(0); + final MdxModel heroLevelUpModel = loadModel(heroLevelUpArt); + if (heroLevelUpModel != null) { + final MdxComplexInstance modelInstance = (MdxComplexInstance) heroLevelUpModel + .addInstance(); + modelInstance.setTeamColor(source.getPlayerIndex()); + + final MdxModel model = (MdxModel) renderUnit.instance.model; + int index = -1; + for (int i = 0; i < model.attachments.size(); i++) { + final Attachment attachment = model.attachments.get(i); + if (attachment.getName().startsWith("origin ref")) { + index = i; + break; + } + } + if (index != -1) { + final MdxNode attachment = renderUnit.instance.getAttachment(index); + modelInstance.setParent(attachment); + } + else { + modelInstance.setLocation(renderUnit.location); + } + + modelInstance.setScene(War3MapViewer.this.worldScene); + SequenceUtils.randomBirthSequence(modelInstance); + War3MapViewer.this.projectiles + .add(new RenderAttackInstant(modelInstance, War3MapViewer.this, + (float) Math.toRadians(renderUnit.getSimulationUnit().getFacing()))); + } + } + + @Override + public void spawnEffectOnUnit(final CUnit unit, final String effectPath) { + final RenderUnit renderUnit = War3MapViewer.this.unitToRenderPeer.get(unit); + final MdxModel spawnedEffectModel = (MdxModel) load(mdx(effectPath), PathSolver.DEFAULT, null); + if (spawnedEffectModel != null) { + final MdxComplexInstance modelInstance = (MdxComplexInstance) spawnedEffectModel + .addInstance(); + modelInstance.setTeamColor(unit.getPlayerIndex()); + modelInstance.setLocation(renderUnit.location); + modelInstance.setScene(War3MapViewer.this.worldScene); + SequenceUtils.randomBirthSequence(modelInstance); + War3MapViewer.this.projectiles + .add(new RenderAttackInstant(modelInstance, War3MapViewer.this, + (float) Math.toRadians(renderUnit.getSimulationUnit().getFacing()))); + } + + } + + @Override + public void spawnUnitReadySound(final CUnit trainedUnit) { + final RenderUnit renderPeer = War3MapViewer.this.unitToRenderPeer.get(trainedUnit); + renderPeer.soundset.ready.playUnitResponse(War3MapViewer.this.worldScene.audioContext, + renderPeer); + } + + @Override + public void unitRepositioned(final CUnit cUnit) { + final RenderUnit renderPeer = War3MapViewer.this.unitToRenderPeer.get(cUnit); + renderPeer.repositioned(War3MapViewer.this); + } + + @Override + public void spawnGainResourceTextTag(final CUnit gainingUnit, final ResourceType resourceType, + final int amount) { + final RenderUnit renderPeer = War3MapViewer.this.unitToRenderPeer.get(gainingUnit); + switch (resourceType) { + case FOOD: + throw new IllegalArgumentException(); + case GOLD: + War3MapViewer.this.textTags.add(new TextTag(new Vector3(renderPeer.location), "+" + amount, + PLACEHOLDER_GOLD_COLOR)); + break; + case LUMBER: + War3MapViewer.this.textTags.add(new TextTag(new Vector3(renderPeer.location), "+" + amount, + PLACEHOLDER_LUMBER_COLOR)); + break; + } + } + + @Override + public void spawnUIUnitGetItemSound(final CUnit cUnit, final CItem item) { + final RenderUnit renderPeer = War3MapViewer.this.unitToRenderPeer.get(cUnit); + if (localPlayerIndex == renderPeer.getSimulationUnit().getPlayerIndex()) { + War3MapViewer.this.uiSounds.getSound("ItemGet").play( + War3MapViewer.this.worldScene.audioContext, renderPeer.getX(), renderPeer.getY(), + renderPeer.getZ()); + } + } + + @Override + public void spawnUIUnitDropItemSound(final CUnit cUnit, final CItem item) { + final RenderUnit renderPeer = War3MapViewer.this.unitToRenderPeer.get(cUnit); + if (localPlayerIndex == renderPeer.getSimulationUnit().getPlayerIndex()) { + War3MapViewer.this.uiSounds.getSound("ItemDrop").play( + War3MapViewer.this.worldScene.audioContext, renderPeer.getX(), renderPeer.getY(), + renderPeer.getZ()); + } + } + }, this.terrain.pathingGrid, this.terrain.getEntireMap(), this.seededRandom, this.commandErrorListener); + + this.walkableObjectsTree = new Quadtree<>(this.terrain.getEntireMap()); + if (this.doodadsAndDestructiblesLoaded) { + this.loadDoodadsAndDestructibles(this.allObjectData); + } + else { + throw new IllegalStateException("transcription of JS has not loaded a map and has no JS async promises"); + } + + loadSounds(); + + this.terrain.createWaves(); + + loadLightsAndShading(tileset); + } + + protected BufferedImage getDestructablePathingPixelMap(final MutableGameObject row) { + return loadPathingTexture(row.getFieldAsString(DESTRUCTABLE_PATHING, 0)); + } + + protected BufferedImage getDestructablePathingDeathPixelMap(final MutableGameObject row) { + return loadPathingTexture(row.getFieldAsString(DESTRUCTABLE_PATHING_DEATH, 0)); + } + + private void loadSounds() { + this.uiSounds = new KeyedSounds(this.uiSoundsTable, this.mapMpq); + } + + /** + * Loads the map information that should be loaded after UI, such as units, who + * need to be able to setup their UI counterparts (icons, etc) for their + * abilities while loading. This allows the dynamic creation of units while the + * game is playing to better share code with the startup sequence's creation of + * units. + * + * @throws IOException + */ + public void loadAfterUI() throws IOException { + if (this.unitsAndItemsLoaded) { + this.loadUnitsAndItems(this.allObjectData); + } + else { + throw new IllegalStateException("transcription of JS has not loaded a map and has no JS async promises"); + } + + // After we finish loading units, we need to update & create the stored shadow + // information for all unit shadows + this.terrain.initShadows(); + } + + private void loadLightsAndShading(final char tileset) { + // TODO this should be set by the war3map.j actually, not by the tileset, so the + // call to set day night models is just for testing to make the test look pretty + final Element defaultTerrainLights = this.worldEditData.get("TerrainLights"); + final Element defaultUnitLights = this.worldEditData.get("UnitLights"); + setDayNightModels(defaultTerrainLights.getField(Character.toString(tileset)), + defaultUnitLights.getField(Character.toString(tileset))); + + } + + private void loadDoodadsAndDestructibles(final Warcraft3MapObjectData modifications) throws IOException { + this.applyModificationFile(this.doodadsData, this.doodadMetaData, modifications.getDoodads(), + WorldEditorDataType.DOODADS); + this.applyModificationFile(this.doodadsData, this.destructableMetaData, modifications.getDestructibles(), + WorldEditorDataType.DESTRUCTIBLES); + + final War3MapDoo doo = this.mapMpq.readDoodads(); + + for (final com.etheller.warsmash.parsers.w3x.doo.Doodad doodad : doo.getDoodads()) { + WorldEditorDataType type = WorldEditorDataType.DOODADS; + MutableGameObject row = modifications.getDoodads().get(doodad.getId()); + if (row == null) { + row = modifications.getDestructibles().get(doodad.getId()); + type = WorldEditorDataType.DESTRUCTIBLES; + } + if (row != null) { + BuildingShadow destructableShadow = null; + RemovablePathingMapInstance destructablePathing = null; + RemovablePathingMapInstance destructablePathingDeath = null; + String file = row.readSLKTag("file"); + final int numVar = row.readSLKTagInt("numVar"); + + if (file.endsWith(".mdx") || file.endsWith(".mdl")) { + file = file.substring(0, file.length() - 4); + } + + String fileVar = file; + + file += ".mdx"; + + if (numVar > 1) { + fileVar += Math.min(doodad.getVariation(), numVar - 1); + } + + fileVar += ".mdx"; + + final float maxPitch = row.readSLKTagFloat("maxPitch"); + final float maxRoll = row.readSLKTagFloat("maxRoll"); + if (type == WorldEditorDataType.DESTRUCTIBLES) { + final String shadowString = row.readSLKTag("shadow"); + if ((shadowString != null) && (shadowString.length() > 0) && !"_".equals(shadowString)) { + destructableShadow = this.terrain.addShadow(shadowString, doodad.getLocation()[0], + doodad.getLocation()[1]); + } + + final BufferedImage destructablePathingPixelMap = getDestructablePathingPixelMap(row); + if (destructablePathingPixelMap != null) { + destructablePathing = this.terrain.pathingGrid.createRemovablePathingOverlayTexture( + doodad.getLocation()[0], doodad.getLocation()[1], + (int) Math.toDegrees(doodad.getAngle()), destructablePathingPixelMap); + if (doodad.getLife() > 0) { + destructablePathing.add(); + } + } + final BufferedImage destructablePathingDeathPixelMap = getDestructablePathingDeathPixelMap(row); + if (destructablePathingDeathPixelMap != null) { + destructablePathingDeath = this.terrain.pathingGrid.createRemovablePathingOverlayTexture( + doodad.getLocation()[0], doodad.getLocation()[1], + (int) Math.toDegrees(doodad.getAngle()), destructablePathingDeathPixelMap); + if (doodad.getLife() <= 0) { + destructablePathingDeath.add(); + } + } + } + // First see if the model is local. + // Doodads referring to local models may have invalid variations, so if the + // variation doesn't exist, try without a variation. + + String path; + if (this.mapMpq.has(fileVar)) { + path = fileVar; + } + else { + path = file; + } + MdxModel model; + if (this.mapMpq.has(path)) { + model = (MdxModel) this.load(path, this.mapPathSolver, this.solverParams); + } + else { + model = (MdxModel) this.load(fileVar, this.mapPathSolver, this.solverParams); + } + + if (type == WorldEditorDataType.DESTRUCTIBLES) { + final float x = doodad.getLocation()[0]; + final float y = doodad.getLocation()[1]; + final CDestructable simulationDestructable = this.simulation.createDestructable(row.getAlias(), x, + y, destructablePathing, destructablePathingDeath); + simulationDestructable.setLife(this.simulation, + simulationDestructable.getLife() * (doodad.getLife() / 100f)); + final RenderDestructable renderDestructable = new RenderDestructable(this, model, row, doodad, type, + maxPitch, maxRoll, doodad.getLife(), destructableShadow, simulationDestructable); + if (row.readSLKTagBoolean("walkable")) { + final float angle = doodad.getAngle(); + final BoundingBox boundingBox = model.bounds.getBoundingBox(); + final Rectangle renderDestructableBounds = getRotatedBoundingBox(x, y, angle, boundingBox); + System.out.println("ROTATED BOUNDS TO: " + renderDestructableBounds); + this.walkableObjectsTree.add((MdxComplexInstance) renderDestructable.instance, + renderDestructableBounds); + renderDestructable.walkableBounds = renderDestructableBounds; + } + this.widgets.add(renderDestructable); + this.destructableToRenderPeer.put(simulationDestructable, renderDestructable); + } + else { + this.doodads.add(new RenderDoodad(this, model, row, doodad, type, maxPitch, maxRoll)); + } + } + } + + // Cliff/Terrain doodads. + for (final com.etheller.warsmash.parsers.w3x.doo.TerrainDoodad doodad : doo.getTerrainDoodads()) { + final MutableGameObject row = modifications.getDoodads().get(doodad.getId()); + String file = row.readSLKTag("file");// + if ("".equals(file)) { + final String blaBla = row.readSLKTag("file"); + System.out.println("bla"); + } + if (file.toLowerCase().endsWith(".mdl")) { + file = file.substring(0, file.length() - 4); + } + if (!file.toLowerCase().endsWith(".mdx")) { + file += ".mdx"; + } + final MdxModel model = (MdxModel) this.load(file, this.mapPathSolver, this.solverParams); + + final String pathingTexture = row.readSLKTag("pathTex"); + BufferedImage pathingTextureImage; + if ((pathingTexture != null) && (pathingTexture.length() > 0) && !"_".equals(pathingTexture)) { + + pathingTextureImage = this.filePathToPathingMap.get(pathingTexture.toLowerCase()); + if (pathingTextureImage == null) { + if (this.mapMpq.has(pathingTexture)) { + try { + pathingTextureImage = TgaFile.readTGA(pathingTexture, + this.mapMpq.getResourceAsStream(pathingTexture)); + this.filePathToPathingMap.put(pathingTexture.toLowerCase(), pathingTextureImage); + } + catch (final Exception exc) { + exc.printStackTrace(); + } + } + } + } + else { + pathingTextureImage = null; + } + if (pathingTextureImage != null) { + // blit out terrain cells under this TerrainDoodad + final int textureWidth = pathingTextureImage.getWidth(); + final int textureHeight = pathingTextureImage.getHeight(); + final int textureWidthTerrainCells = textureWidth / 4; + final int textureHeightTerrainCells = textureHeight / 4; + final int minCellX = ((int) doodad.getLocation()[0]); + final int minCellY = ((int) doodad.getLocation()[1]); + final int maxCellX = (minCellX + textureWidthTerrainCells) - 1; + final int maxCellY = (minCellY + textureHeightTerrainCells) - 1; + for (int j = minCellY; j <= maxCellY; j++) { + for (int i = minCellX; i <= maxCellX; i++) { + this.terrain.removeTerrainCellWithoutFlush(i, j); + } + } + this.terrain.flushRemovedTerrainCells(); + } + + System.out.println("Loading terrain doodad: " + file); + this.terrainDoodads.add(new TerrainDoodad(this, model, row, doodad, pathingTextureImage)); + } + + this.doodadsReady = true; + this.anyReady = true; + } + + private Rectangle getRotatedBoundingBox(final float x, final float y, final float angle, + final BoundingBox boundingBox) { + final float x1 = boundingBox.min.x; + final float y1 = boundingBox.min.y; + final float x2 = boundingBox.min.x + boundingBox.getWidth(); + final float y2 = boundingBox.min.y; + final float x3 = boundingBox.min.x + boundingBox.getWidth(); + final float y3 = boundingBox.min.y + boundingBox.getHeight(); + final float x4 = boundingBox.min.x; + final float y4 = boundingBox.min.y + boundingBox.getHeight(); + final float angle1 = (float) StrictMath.atan2(y1, x1) + angle; + final float len1 = (float) StrictMath.sqrt((x1 * x1) + (y1 * y1)); + final float angle2 = (float) StrictMath.atan2(y2, x2) + angle; + final float len2 = (float) StrictMath.sqrt((x2 * x2) + (y2 * y2)); + final float angle3 = (float) StrictMath.atan2(y3, x3) + angle; + final float len3 = (float) StrictMath.sqrt((x3 * x3) + (y3 * y3)); + final float angle4 = (float) StrictMath.atan2(y4, x4) + angle; + final float len4 = (float) StrictMath.sqrt((x4 * x4) + (y4 * y4)); + final double x1prime = StrictMath.cos(angle1) * len1; + final double x2prime = StrictMath.cos(angle2) * len2; + final double x3prime = StrictMath.cos(angle3) * len3; + final double x4prime = StrictMath.cos(angle4) * len4; + final double y1prime = StrictMath.sin(angle1) * len1; + final double y2prime = StrictMath.sin(angle2) * len2; + final double y3prime = StrictMath.sin(angle3) * len3; + final double y4prime = StrictMath.sin(angle4) * len4; + final float minX = (float) StrictMath.min(StrictMath.min(x1prime, x2prime), StrictMath.min(x3prime, x4prime)); + final float minY = (float) StrictMath.min(StrictMath.min(y1prime, y2prime), StrictMath.min(y3prime, y4prime)); + final float maxX = (float) StrictMath.max(StrictMath.max(x1prime, x2prime), StrictMath.max(x3prime, x4prime)); + final float maxY = (float) StrictMath.max(StrictMath.max(y1prime, y2prime), StrictMath.max(y3prime, y4prime)); + return new Rectangle(x + minX, y + minY, maxX - minX, maxY - minY); + } + + private void applyModificationFile(final MappedData doodadsData2, final MappedData doodadMetaData2, + final MutableObjectData destructibles, final WorldEditorDataType dataType) { + // TODO condense ported MappedData from Ghostwolf and MutableObjectData from + // Retera + + } + + private void loadUnitsAndItems(final Warcraft3MapObjectData modifications) throws IOException { + final War3Map mpq = this.mapMpq; + this.unitsReady = false; + + this.soundsetNameToSoundset = new HashMap<>(); + + if (this.dataSource.has("war3mapUnits.doo")) { + final War3MapUnitsDoo dooFile = mpq.readUnits(); + + // Collect the units and items data. + for (final com.etheller.warsmash.parsers.w3x.unitsdoo.Unit unit : dooFile.getUnits()) { + final War3ID unitId = unit.getId(); + final float unitX = unit.getLocation()[0]; + final float unitY = unit.getLocation()[1]; + final float unitZ = unit.getLocation()[2]; + final int playerIndex = unit.getPlayer(); + final int customTeamColor = unit.getCustomTeamColor(); + final float unitAngle = unit.getAngle(); + + final CUnit unitCreated = createNewUnit(modifications, unitId, unitX, unitY, unitZ, playerIndex, + customTeamColor, unitAngle); + if (unit.getGoldAmount() != 0) { + unitCreated.setGold(unit.getGoldAmount()); + } + } + } + this.simulation.unitsLoaded(); + + this.terrain.loadSplats(); + + this.unitsReady = true; + this.anyReady = true; + } + + private CUnit createNewUnit(final Warcraft3MapObjectData modifications, final War3ID unitId, float unitX, + float unitY, final float unitZ, final int playerIndex, int customTeamColor, final float unitAngle) { + UnitSoundset soundset = null; + MutableGameObject row = null; + String path = null; + Splat unitShadowSplat = null; + SplatMover unitShadowSplatDynamicIngame = null; + Splat buildingUberSplat = null; + SplatMover buildingUberSplatDynamicIngame = null; + BufferedImage buildingPathingPixelMap = null; + final float unitVertexScale = 1.0f; + RemovablePathingMapInstance pathingInstance = null; + BuildingShadow buildingShadowInstance = null; + + // Hardcoded? + WorldEditorDataType type = null; + if (sloc.equals(unitId)) { +// path = "Objects\\StartLocation\\StartLocation.mdx"; + type = null; /// ?????? + this.startLocations[playerIndex] = new Vector2(unitX, unitY); + } + else { + row = modifications.getUnits().get(unitId); + if (row == null) { + row = modifications.getItems().get(unitId); + if (row != null) { + type = WorldEditorDataType.ITEM; + path = row.getFieldAsString(ITEM_FILE, 0); + + if (path.toLowerCase().endsWith(".mdl") || path.toLowerCase().endsWith(".mdx")) { + path = path.substring(0, path.length() - 4); + } + + final Element misc = this.miscData.get("Misc"); + final String itemShadowFile = misc.getField("ItemShadowFile"); + final int itemShadowWidth = misc.getFieldValue("ItemShadowSize", 0); + final int itemShadowHeight = misc.getFieldValue("ItemShadowSize", 1); + final int itemShadowX = misc.getFieldValue("ItemShadowOffset", 0); + final int itemShadowY = misc.getFieldValue("ItemShadowOffset", 1); + if ((itemShadowFile != null) && !"_".equals(itemShadowFile)) { + final String texture = "ReplaceableTextures\\Shadows\\" + itemShadowFile + ".blp"; + final float shadowX = itemShadowX; + final float shadowY = itemShadowY; + final float shadowWidth = itemShadowWidth; + final float shadowHeight = itemShadowHeight; + if (!this.terrain.splats.containsKey(texture)) { + final Splat splat = new Splat(); + splat.opacity = 0.5f; + this.terrain.splats.put(texture, splat); + } + final float x = unitX - shadowX; + final float y = unitY - shadowY; + this.terrain.splats.get(texture).locations + .add(new float[] { x, y, x + shadowWidth, y + shadowHeight, 3 }); + unitShadowSplat = this.terrain.splats.get(texture); + } + + path += ".mdx"; + } + } + else { + type = WorldEditorDataType.UNITS; + path = getUnitModelPath(row); + + buildingPathingPixelMap = getBuildingPathingPixelMap(row); + if (buildingPathingPixelMap != null) { + unitX = (float) Math.floor(unitX / 64f) * 64f; + unitY = (float) Math.floor(unitY / 64f) * 64f; + if (((buildingPathingPixelMap.getWidth() / 2) % 2) == 1) { + unitX += 32f; + } + if (((buildingPathingPixelMap.getHeight() / 2) % 2) == 1) { + unitY += 32f; + } + pathingInstance = this.terrain.pathingGrid.blitRemovablePathingOverlayTexture(unitX, unitY, + (int) Math.toDegrees(unitAngle), buildingPathingPixelMap); + } + + final String uberSplat = row.getFieldAsString(UBER_SPLAT, 0); + if (uberSplat != null) { + final Element uberSplatInfo = this.terrain.uberSplatTable.get(uberSplat); + if (uberSplatInfo != null) { + final String texturePath = uberSplatInfo.getField("Dir") + "\\" + uberSplatInfo.getField("file") + + ".blp"; + final float s = uberSplatInfo.getFieldFloatValue("Scale"); + if (this.unitsReady) { + buildingUberSplatDynamicIngame = this.terrain.addUberSplat(texturePath, unitX, unitY, 1, s, + false, false, false); + } + else { + if (!this.terrain.splats.containsKey(texturePath)) { + this.terrain.splats.put(texturePath, new Splat()); + } + final float x = unitX; + final float y = unitY; + buildingUberSplat = this.terrain.splats.get(texturePath); + buildingUberSplat.locations.add(new float[] { x - s, y - s, x + s, y + s, 1 }); + } + } + } + + final String unitShadow = row.getFieldAsString(UNIT_SHADOW, 0); + if ((unitShadow != null) && !"_".equals(unitShadow)) { + final String texture = "ReplaceableTextures\\Shadows\\" + unitShadow + ".blp"; + final float shadowX = row.getFieldAsFloat(UNIT_SHADOW_X, 0); + final float shadowY = row.getFieldAsFloat(UNIT_SHADOW_Y, 0); + final float shadowWidth = row.getFieldAsFloat(UNIT_SHADOW_W, 0); + final float shadowHeight = row.getFieldAsFloat(UNIT_SHADOW_H, 0); + if (this.mapMpq.has(texture)) { + final float x = unitX - shadowX; + final float y = unitY - shadowY; + if (this.unitsReady) { + unitShadowSplatDynamicIngame = this.terrain.addUnitShadowSplat(texture, x, y, + x + shadowWidth, y + shadowHeight, 3, 0.5f); + } + else { + if (!this.terrain.splats.containsKey(texture)) { + final Splat splat = new Splat(); + splat.opacity = 0.5f; + this.terrain.splats.put(texture, splat); + } + this.terrain.splats.get(texture).locations + .add(new float[] { x, y, x + shadowWidth, y + shadowHeight, 3 }); + unitShadowSplat = this.terrain.splats.get(texture); + } + } + } + + final String buildingShadow = row.getFieldAsString(BUILDING_SHADOW, 0); + if ((buildingShadow != null) && !"_".equals(buildingShadow)) { + buildingShadowInstance = this.terrain.addShadow(buildingShadow, unitX, unitY); + } + + final String soundName = row.getFieldAsString(UNIT_SOUNDSET, 0); + UnitSoundset unitSoundset = this.soundsetNameToSoundset.get(soundName); + if (unitSoundset == null) { + unitSoundset = new UnitSoundset(this.dataSource, this.unitAckSoundsTable, soundName); + this.soundsetNameToSoundset.put(soundName, unitSoundset); + } + soundset = unitSoundset; + + } + } + + if (path != null) { + final String unitSpecialArtPath = row.getFieldAsString(UNIT_SPECIAL, 0); + MdxModel specialArtModel; + if (unitSpecialArtPath != null) { + try { + specialArtModel = (MdxModel) this.load(mdx(unitSpecialArtPath), this.mapPathSolver, + this.solverParams); + } + catch (final Exception exc) { + exc.printStackTrace(); + specialArtModel = null; + } + } + else { + specialArtModel = null; + } + final MdxModel model = (MdxModel) this.load(path, this.mapPathSolver, this.solverParams); + MdxModel portraitModel; + final String portraitPath = path.substring(0, path.length() - 4) + "_portrait.mdx"; + if (this.dataSource.has(portraitPath)) { + portraitModel = (MdxModel) this.load(portraitPath, this.mapPathSolver, this.solverParams); + } + else { + portraitModel = model; + } + if (type == WorldEditorDataType.UNITS) { + final float angle = (float) Math.toDegrees(unitAngle); + final CUnit simulationUnit = this.simulation.createUnit(row.getAlias(), playerIndex, unitX, unitY, + angle, buildingPathingPixelMap, pathingInstance); + final RenderUnitTypeData typeData = getUnitTypeData(unitId, row); + if (!typeData.isAllowCustomTeamColor() || (customTeamColor == -1)) { + if (typeData.getTeamColor() != -1) { + customTeamColor = typeData.getTeamColor(); + } + else { + customTeamColor = playerIndex; + } + } + final RenderUnit renderUnit = new RenderUnit(this, model, row, unitX, unitY, unitZ, customTeamColor, + soundset, portraitModel, simulationUnit, typeData, specialArtModel, buildingShadowInstance, + this.selectionCircleScaleFactor); + this.unitToRenderPeer.put(simulationUnit, renderUnit); + this.widgets.add(renderUnit); + this.units.add(renderUnit); + if (unitShadowSplat != null) { + unitShadowSplat.unitMapping.add(new Consumer() { + @Override + public void accept(final SplatMover t) { + renderUnit.shadow = t; + } + }); + } + if (unitShadowSplatDynamicIngame != null) { + renderUnit.shadow = unitShadowSplatDynamicIngame; + } + if (buildingUberSplat != null) { + buildingUberSplat.unitMapping.add(new Consumer() { + @Override + public void accept(final SplatMover t) { + renderUnit.uberSplat = t; + } + }); + } + if (buildingUberSplatDynamicIngame != null) { + renderUnit.uberSplat = buildingUberSplatDynamicIngame; + } + return simulationUnit; + } + else { + final CItem simulationItem = this.simulation.createItem(row.getAlias(), unitX, unitY); + final RenderItem renderItem = new RenderItem(this, model, row, unitX, unitY, unitZ, unitAngle, soundset, + portraitModel, simulationItem); + this.widgets.add(renderItem); + this.itemToRenderPeer.put(simulationItem, renderItem); + + if (unitShadowSplat != null) { + unitShadowSplat.unitMapping.add(new Consumer() { + @Override + public void accept(final SplatMover t) { + renderItem.shadow = t; + } + }); + } + if (unitShadowSplatDynamicIngame != null) { + renderItem.shadow = unitShadowSplatDynamicIngame; + } + } + } + else { + System.err.println("Unknown unit ID: " + unitId); + } + return null; + } + + public String getUnitModelPath(final MutableGameObject row) { + String path; + path = row.getFieldAsString(UNIT_FILE, 0); + + if (path.toLowerCase().endsWith(".mdl") || path.toLowerCase().endsWith(".mdx")) { + path = path.substring(0, path.length() - 4); + } + if ((row.readSLKTagInt("fileVerFlags") == 2) && this.dataSource.has(path + "_V1.mdx")) { + path += "_V1"; + } + + path += ".mdx"; + return path; + } + + private BufferedImage getBuildingPathingPixelMap(final MutableGameObject row) { + final String pathingTexture = row.getFieldAsString(UNIT_PATHING, 0); + final BufferedImage buildingPathingPixelMap = loadPathingTexture(pathingTexture); + return buildingPathingPixelMap; + } + + private BufferedImage loadPathingTexture(final String pathingTexture) { + BufferedImage buildingPathingPixelMap = null; + if ((pathingTexture != null) && (pathingTexture.length() > 0) && !"_".equals(pathingTexture)) { + buildingPathingPixelMap = this.filePathToPathingMap.get(pathingTexture.toLowerCase()); + if (buildingPathingPixelMap == null) { + try { + if (pathingTexture.toLowerCase().endsWith(".tga")) { + buildingPathingPixelMap = TgaFile.readTGA(pathingTexture, + this.mapMpq.getResourceAsStream(pathingTexture)); + } + else { + try (InputStream stream = this.mapMpq.getResourceAsStream(pathingTexture)) { + buildingPathingPixelMap = ImageIO.read(stream); + System.out.println("LOADING BLP PATHING: " + pathingTexture); + } + } + this.filePathToPathingMap.put(pathingTexture.toLowerCase(), buildingPathingPixelMap); + } + catch (final IOException exc) { + System.err.println("Failure to get pathing: " + exc.getClass() + ":" + exc.getMessage()); + } + } + } + return buildingPathingPixelMap; + } + + public RenderUnitTypeData getUnitTypeData(final War3ID key, final MutableGameObject row) { + RenderUnitTypeData unitTypeData = this.unitIdToTypeData.get(key); + if (unitTypeData == null) { + unitTypeData = new RenderUnitTypeData(row.getFieldAsFloat(MAX_PITCH, 0), row.getFieldAsFloat(MAX_ROLL, 0), + row.getFieldAsFloat(ELEVATION_SAMPLE_RADIUS, 0), row.getFieldAsBoolean(ALLOW_CUSTOM_TEAM_COLOR, 0), + row.getFieldAsInteger(TEAM_COLOR, 0)); + this.unitIdToTypeData.put(key, unitTypeData); + } + return unitTypeData; + } + + @Override + public void update() { + if (this.anyReady) { + final float deltaTime = Gdx.graphics.getDeltaTime(); + this.terrain.update(deltaTime); + + super.update(); + + final Iterator textTagIterator = this.textTags.iterator(); + while (textTagIterator.hasNext()) { + if (textTagIterator.next().update(deltaTime)) { + textTagIterator.remove(); + } + } + for (final RenderWidget unit : this.widgets) { + unit.updateAnimations(this); + } + final Iterator projectileIterator = this.projectiles.iterator(); + while (projectileIterator.hasNext()) { + final RenderEffect projectile = projectileIterator.next(); + if (projectile.updateAnimations(this, Gdx.graphics.getDeltaTime())) { + projectileIterator.remove(); + } + } + for (final RenderDoodad item : this.doodads) { + final ModelInstance instance = item.instance; + if (instance instanceof MdxComplexInstance) { + final MdxComplexInstance mdxComplexInstance = (MdxComplexInstance) instance; + if ((mdxComplexInstance.sequence == -1) || (mdxComplexInstance.sequenceEnded + && ((item.getAnimation() != AnimationTokens.PrimaryTag.DEATH) + || (((MdxModel) mdxComplexInstance.model).sequences.get(mdxComplexInstance.sequence) + .getFlags() == 0)))) { + SequenceUtils.randomSequence(mdxComplexInstance, item.getAnimation(), SequenceUtils.EMPTY, + true); + + } + } + } + + final float rawDeltaTime = Gdx.graphics.getRawDeltaTime(); + this.updateTime += rawDeltaTime; + while (this.updateTime >= WarsmashConstants.SIMULATION_STEP_TIME) { + this.updateTime -= WarsmashConstants.SIMULATION_STEP_TIME; + this.simulation.update(); + } + this.dncTerrain.setFrameByRatio( + this.simulation.getGameTimeOfDay() / this.simulation.getGameplayConstants().getGameDayHours()); + this.dncTerrain.update(rawDeltaTime, null); + this.dncUnit.setFrameByRatio( + this.simulation.getGameTimeOfDay() / this.simulation.getGameplayConstants().getGameDayHours()); + this.dncUnit.update(rawDeltaTime, null); + this.dncUnitDay.setFrameByRatio(0.5f); + this.dncUnitDay.update(rawDeltaTime, null); + this.dncTarget.setFrameByRatio( + this.simulation.getGameTimeOfDay() / this.simulation.getGameplayConstants().getGameDayHours()); + this.dncTarget.update(rawDeltaTime, null); + } + } + + @Override + public void render() { + if (this.anyReady) { + final Scene worldScene = this.worldScene; + + startFrame(); + worldScene.startFrame(); + worldScene.renderOpaque(this.dynamicShadowManager, this.webGL); + this.terrain.renderGround(this.dynamicShadowManager); + this.terrain.renderCliffs(); + worldScene.renderOpaque(); + this.terrain.renderUberSplats(false); + this.terrain.renderWater(); + worldScene.renderTranslucent(); + this.terrain.renderUberSplats(true); + + final List scenes = this.scenes; + for (final Scene scene : scenes) { + if (scene != worldScene) { + scene.startFrame(); + scene.renderOpaque(); + scene.renderTranslucent(); + } + } + + final int glGetError = Gdx.gl.glGetError(); + if (glGetError != GL20.GL_NO_ERROR) { + throw new IllegalStateException("GL ERROR: " + glGetError); + } + } + } + + public void deselect() { + if (!this.selModels.isEmpty()) { + for (final SplatModel model : this.selModels) { + this.terrain.removeSplatBatchModel("selection"); + } + this.selModels.clear(); + for (final RenderWidget unit : this.selected) { + unit.unassignSelectionCircle(); + } + } + this.selected.clear(); + } + + public void doSelectUnit(final List units) { + deselect(); + if (units.isEmpty()) { + return; + } + + final Map splats = new HashMap(); + for (final RenderWidget unit : units) { + if (unit.getSelectionScale() > 0) { + final float selectionSize = unit.getSelectionScale(); + String path = null; + for (int i = 0; i < this.selectionCircleSizes.size(); i++) { + final SelectionCircleSize selectionCircleSize = this.selectionCircleSizes.get(i); + if ((selectionSize < selectionCircleSize.size) || (i == (this.selectionCircleSizes.size() - 1))) { + path = selectionCircleSize.texture; + break; + } + } + if (!path.toLowerCase().endsWith(".blp")) { + path += ".blp"; + } + if (!splats.containsKey(path)) { + splats.put(path, new Splat()); + } + final float x = unit.getX(); + final float y = unit.getY(); + System.out.println("Selecting a unit at " + x + "," + y); + splats.get(path).locations.add(new float[] { x - (selectionSize / 2), y - (selectionSize / 2), + x + (selectionSize / 2), y + (selectionSize / 2), 5 }); + splats.get(path).unitMapping.add(new Consumer() { + @Override + public void accept(final SplatMover t) { + unit.assignSelectionCircle(t); + if (unit.getInstance().hidden()) { + t.hide(); + } + } + }); + } + this.selected.add(unit); + } + this.selModels.clear(); + for (final Map.Entry entry : splats.entrySet()) { + final String path = entry.getKey(); + final Splat locations = entry.getValue(); + final SplatModel model = new SplatModel(Gdx.gl30, (Texture) load(path, PathSolver.DEFAULT, null), + locations.locations, this.terrain.centerOffset, locations.unitMapping, true, false, true); + model.color[0] = 0; + model.color[1] = 1; + model.color[2] = 0; + model.color[3] = 1; + this.selModels.add(model); + this.terrain.addSplatBatchModel("selection", model); + } + } + + public void getClickLocation(final Vector3 out, final int screenX, final int screenY) { + final float[] ray = rayHeap; + mousePosHeap.set(screenX, screenY); + this.worldScene.camera.screenToWorldRay(ray, mousePosHeap); + gdxRayHeap.set(ray[0], ray[1], ray[2], ray[3] - ray[0], ray[4] - ray[1], ray[5] - ray[2]); + gdxRayHeap.direction.nor();// needed for libgdx + RenderMathUtils.intersectRayTriangles(gdxRayHeap, this.terrain.softwareGroundMesh.vertices, + this.terrain.softwareGroundMesh.indices, 3, out); + rectangleHeap.set(Math.min(out.x, gdxRayHeap.origin.x), Math.min(out.y, gdxRayHeap.origin.y), + Math.abs(out.x - gdxRayHeap.origin.x), Math.abs(out.y - gdxRayHeap.origin.y)); + this.walkableObjectsTree.intersect(rectangleHeap, this.walkablesIntersectionFinder.reset(gdxRayHeap)); + if (this.walkablesIntersectionFinder.found) { + out.set(this.walkablesIntersectionFinder.intersection); + } + else { + out.z = Math.max(getWalkableRenderHeight(out.x, out.y), this.terrain.getGroundHeight(out.x, out.y)); + } + } + + public void showConfirmation(final Vector3 position, final float red, final float green, final float blue) { + this.confirmationInstance.show(); + this.confirmationInstance.setSequence(0); + this.confirmationInstance.setLocation(position); + this.worldScene.instanceMoved(this.confirmationInstance, position.x, position.y); + this.confirmationInstance.vertexColor[0] = red; + this.confirmationInstance.vertexColor[1] = green; + this.confirmationInstance.vertexColor[2] = blue; + } + + public List selectUnit(final float x, final float y, final boolean toggle) { + System.out.println("world: " + x + "," + y); + final RenderWidget entity = rayPickUnit(x, y, CWidgetFilterFunction.ACCEPT_ALL_LIVING); + List sel; + if (entity != null) { + if (toggle) { + sel = new ArrayList<>(this.selected); + final int idx = sel.indexOf(entity); + if (idx >= 0) { + sel.remove(idx); + } + else { + sel.add(entity); + } + } + else { + sel = Arrays.asList(entity); + } + this.doSelectUnit(sel); + } + else { + sel = Collections.emptyList(); + } + return sel; + } + + public RenderWidget rayPickUnit(final float x, final float y) { + return rayPickUnit(x, y, CWidgetFilterFunction.ACCEPT_ALL); + } + + public RenderWidget rayPickUnit(final float x, final float y, final CWidgetFilterFunction filter) { + final float[] ray = rayHeap; + mousePosHeap.set(x, y); + this.worldScene.camera.screenToWorldRay(ray, mousePosHeap); + gdxRayHeap.set(ray[0], ray[1], ray[2], ray[3] - ray[0], ray[4] - ray[1], ray[5] - ray[2]); + gdxRayHeap.direction.nor();// needed for libgdx + + RenderWidget entity = null; + for (final RenderWidget unit : this.widgets) { + final MdxComplexInstance instance = unit.getInstance(); + if (instance.shown() && instance.isVisible(this.worldScene.camera) && instance + .intersectRayWithCollision(gdxRayHeap, intersectionHeap, unit.isIntersectedOnMeshAlways(), false)) { + if (filter.call(unit.getSimulationWidget()) && (intersectionHeap.z > this.terrain + .getGroundHeight(intersectionHeap.x, intersectionHeap.y))) { + if ((entity == null) || (entity.getInstance().depth > instance.depth)) { + entity = unit; + } + } + } + } + return entity; + } + + private static final class MappedDataCallbackImplementation implements LoadGenericCallback { + @Override + public Object call(final InputStream data) { + final StringBuilder stringBuilder = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(data, "utf-8"))) { + String line; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); + stringBuilder.append("\n"); + } + } + catch (final UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + return new MappedData(stringBuilder.toString()); + } + } + + private static final class StringDataCallbackImplementation implements LoadGenericCallback { + @Override + public Object call(final InputStream data) { + if (data == null) { + System.err.println("data null"); + } + final StringBuilder stringBuilder = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(data, "utf-8"))) { + String line; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); + stringBuilder.append("\n"); + } + } + catch (final UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + return stringBuilder.toString(); + } + } + + private static final class StreamDataCallbackImplementation implements LoadGenericCallback { + @Override + public Object call(final InputStream data) { + return data; + } + } + + public static final class SolverParams { + public char tileset; + public boolean reforged; + public boolean hd; + } + + public static final class CliffInfo { + public List locations = new ArrayList<>(); + public List textures = new ArrayList<>(); + } + + private static final int MAXIMUM_ACCEPTED = 1 << 30; + private float selectionCircleScaleFactor; + private DataTable worldEditData; + private WorldEditStrings worldEditStrings; + private Warcraft3MapObjectData allObjectData; + private AbilityDataUI abilityDataUI; + private Map soundsetNameToSoundset; + public int imageWalkableZOffset; + private WTS preloadedWTS; + + /** + * Returns a power of two size for the given target capacity. + */ + private static final int pow2GreaterThan(final int capacity) { + int numElements = capacity - 1; + numElements |= numElements >>> 1; + numElements |= numElements >>> 2; + numElements |= numElements >>> 4; + numElements |= numElements >>> 8; + numElements |= numElements >>> 16; + return (numElements < 0) ? 1 : (numElements >= MAXIMUM_ACCEPTED) ? MAXIMUM_ACCEPTED : numElements + 1; + } + + public void standOnRepeat(final MdxComplexInstance instance) { + instance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + SequenceUtils.randomStandSequence(instance); + } + + private static final class SelectionCircleSize { + private final float size; + private final String texture; + private final String textureDotted; + + public SelectionCircleSize(final float size, final String texture, final String textureDotted) { + this.size = size; + this.texture = texture; + this.textureDotted = textureDotted; + } + } + + public void setDayNightModels(final String terrainDNCFile, final String unitDNCFile) { + final MdxModel terrainDNCModel = (MdxModel) load(mdx(terrainDNCFile), PathSolver.DEFAULT, null); + this.dncTerrain = (MdxComplexInstance) terrainDNCModel.addInstance(); + this.dncTerrain.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + this.dncTerrain.setSequence(0); + final MdxModel unitDNCModel = (MdxModel) load(mdx(unitDNCFile), PathSolver.DEFAULT, null); + this.dncUnit = (MdxComplexInstance) unitDNCModel.addInstance(); + this.dncUnit.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + this.dncUnit.setSequence(0); + this.dncUnitDay = (MdxComplexInstance) unitDNCModel.addInstance(); + this.dncUnitDay.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + this.dncUnitDay.setSequence(0); + final MdxModel targetDNCModel = (MdxModel) load( + mdx("Environment\\DNC\\DNCLordaeron\\DNCLordaeronTarget\\DNCLordaeronTarget.mdl"), PathSolver.DEFAULT, + null); + this.dncTarget = (MdxComplexInstance) targetDNCModel.addInstance(); + this.dncTarget.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + this.dncTarget.setSequence(0); + } + + public static String mdx(String mdxPath) { + if (mdxPath.toLowerCase().endsWith(".mdl")) { + mdxPath = mdxPath.substring(0, mdxPath.length() - 4); + } + if (!mdxPath.toLowerCase().endsWith(".mdx")) { + mdxPath += ".mdx"; + } + return mdxPath; + } + + public static String mdl(String mdxPath) { + if (mdxPath.toLowerCase().endsWith(".mdx")) { + mdxPath = mdxPath.substring(0, mdxPath.length() - 4); + } + if (!mdxPath.toLowerCase().endsWith(".mdl")) { + mdxPath += ".mdl"; + } + return mdxPath; + } + + public String blp(String iconPath) { + final int lastDotIndex = iconPath.lastIndexOf('.'); + if (lastDotIndex != -1) { + iconPath = iconPath.substring(0, lastDotIndex); + } + if (!iconPath.toLowerCase().endsWith(".blp")) { + iconPath += ".blp"; + } + return iconPath; + } + + public MdxModel loadModel(final String path) { + return (MdxModel) load(mdx(path), PathSolver.DEFAULT, null); + } + + @Override + public SceneLightManager createLightManager(final boolean simple) { + if (simple) { + return new W3xScenePortraitLightManager(this, this.lightDirection); + } + else { + return new W3xSceneWorldLightManager(this); + } + } + + @Override + public WorldEditStrings getWorldEditStrings() { + return this.worldEditStrings; + } + + public void setGameUI(final GameUI gameUI) { + this.gameUI = gameUI; + this.abilityDataUI = new AbilityDataUI(this.allObjectData.getAbilities(), this.allObjectData.getUnits(), + this.allObjectData.getItems(), this.allObjectData.getUpgrades(), gameUI, this); + } + + public GameUI getGameUI() { + return this.gameUI; + } + + public AbilityDataUI getAbilityDataUI() { + return this.abilityDataUI; + } + + public KeyedSounds getUiSounds() { + return this.uiSounds; + } + + public Warcraft3MapObjectData getAllObjectData() { + return this.allObjectData; + } + + public float getWalkableRenderHeight(final float x, final float y) { + this.walkableObjectsTree.intersect(x, y, this.walkablesIntersector.reset(x, y)); + return this.walkablesIntersector.z; + } + + public MdxComplexInstance getHighestWalkableUnder(final float x, final float y) { + this.walkableObjectsTree.intersect(x, y, this.intersectorFindsHighestWalkable.reset(x, y)); + return this.intersectorFindsHighestWalkable.highestInstance; + } + + public int getLocalPlayerIndex() { + return this.localPlayerIndex; + } + + public RenderUnit getRenderPeer(final CUnit unit) { + return this.unitToRenderPeer.get(unit); + } + + public RenderDestructable getRenderPeer(final CDestructable dest) { + return this.destructableToRenderPeer.get(dest); + } + + public RenderItem getRenderPeer(final CItem item) { + return this.itemToRenderPeer.get(item); + } + + private RenderWidget getRenderPeer(final CWidget damagedDestructable) { + RenderWidget damagedWidget = War3MapViewer.this.unitToRenderPeer.get(damagedDestructable); + if (damagedWidget == null) { + damagedWidget = War3MapViewer.this.destructableToRenderPeer.get(damagedDestructable); + } + if (damagedWidget == null) { + damagedWidget = War3MapViewer.this.itemToRenderPeer.get(damagedDestructable); + } + return damagedWidget; + } + + private static final class QuadtreeIntersectorFindsWalkableRenderHeight + implements QuadtreeIntersector { + private float z; + private final Ray ray = new Ray(); + private final Vector3 intersection = new Vector3(); + + private QuadtreeIntersectorFindsWalkableRenderHeight reset(final float x, final float y) { + this.z = -Float.MAX_VALUE; + this.ray.set(x, y, 4096, 0, 0, -8192); + return this; + } + + @Override + public boolean onIntersect(final MdxComplexInstance intersectingObject) { + if (intersectingObject.intersectRayWithCollision(this.ray, this.intersection, true, true)) { + this.z = Math.max(this.z, this.intersection.z); + } + return false; + } + } + + private static final class QuadtreeIntersectorFindsHighestWalkable + implements QuadtreeIntersector { + private float z; + private final Ray ray = new Ray(); + private final Vector3 intersection = new Vector3(); + private MdxComplexInstance highestInstance; + + private QuadtreeIntersectorFindsHighestWalkable reset(final float x, final float y) { + this.z = -Float.MAX_VALUE; + this.ray.set(x, y, 4096, 0, 0, -8192); + this.highestInstance = null; + return this; + } + + @Override + public boolean onIntersect(final MdxComplexInstance intersectingObject) { + if (intersectingObject.intersectRayWithCollision(this.ray, this.intersection, true, true)) { + if (this.intersection.z > this.z) { + this.z = this.intersection.z; + this.highestInstance = intersectingObject; + } + } + return false; + } + } + + private static final class QuadtreeIntersectorFindsHitPoint implements QuadtreeIntersector { + private Ray ray; + private final Vector3 intersection = new Vector3(); + private boolean found; + + private QuadtreeIntersectorFindsHitPoint reset(final Ray ray) { + this.ray = ray; + this.found = false; + return this; + } + + @Override + public boolean onIntersect(final MdxComplexInstance intersectingObject) { + if (intersectingObject.intersectRayWithCollision(this.ray, this.intersection, true, true)) { + this.found = true; + return true; + } + return false; + } + } + + public void add(final TextTag textTag) { + this.textTags.add(textTag); + } + + public SettableCommandErrorListener getCommandErrorListener() { + return this.commandErrorListener; + } + + public War3MapConfig getMapConfig() { + return this.mapConfig; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/CameraManager.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/CameraManager.java new file mode 100644 index 0000000..d44d73e --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/CameraManager.java @@ -0,0 +1,62 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.camera; + +import com.badlogic.gdx.math.Quaternion; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.viewer5.Camera; +import com.etheller.warsmash.viewer5.CanvasProvider; +import com.etheller.warsmash.viewer5.Scene; + +public abstract class CameraManager { + private static final double HORIZONTAL_ANGLE_INCREMENT = Math.PI / 60; + protected final float[] cameraPositionTemp = new float[3]; + protected final float[] cameraTargetTemp = new float[3]; + protected CanvasProvider canvas; + public Camera camera; + protected float moveSpeed; + protected float rotationSpeed; + protected float zoomFactor; + public float horizontalAngle; + public float verticalAngle; + public float distance; + protected Vector3 position; + public Vector3 target; + protected Vector3 worldUp; + protected Vector3 vecHeap; + protected Quaternion quatHeap; + protected Quaternion quatHeap2; + + public CameraManager() { + } + + // An orbit camera setup example. + // Left mouse button controls the orbit itself. + // The right mouse button allows to move the camera and the point it's looking + // at on the XY plane. + // Scrolling zooms in and out. + public void setupCamera(final Scene scene) { + this.canvas = scene.viewer.canvas; + this.camera = scene.camera; + this.moveSpeed = 2; + this.rotationSpeed = (float) HORIZONTAL_ANGLE_INCREMENT; + this.zoomFactor = 0.1f; + this.horizontalAngle = 0;// (float) (Math.PI / 2); + this.verticalAngle = (float) Math.toRadians(34); + this.distance = 1650; + this.position = new Vector3(); + this.target = new Vector3(0, 0, 0); + this.worldUp = new Vector3(0, 0, 1); + this.vecHeap = new Vector3(); + this.quatHeap = new Quaternion(); + this.quatHeap2 = new Quaternion(); + + updateCamera(); + +// cameraUpdate(); + } + + public abstract void updateCamera(); + +// private void cameraUpdate() { +// +// } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/CameraPanControls.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/CameraPanControls.java new file mode 100644 index 0000000..964dbb9 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/CameraPanControls.java @@ -0,0 +1,10 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.camera; + +public final class CameraPanControls { + protected boolean down; + protected boolean up; + protected boolean left; + protected boolean right; + protected boolean insertDown; + protected boolean deleteDown; +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/CameraPreset.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/CameraPreset.java new file mode 100644 index 0000000..92ed43c --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/CameraPreset.java @@ -0,0 +1,85 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.camera; + +public class CameraPreset { + private final float aoa; + private final float fov; + private final float rotation; + private final float rotationInsert; + private final float rotationDelete; + private final float distance; + private final float farZ; + private final float nearZ; + private final float height; + private final float listenerDistance; + private final float listenerAOA; + + public CameraPreset(final float aoa, final float fov, final float rotation, final float rotationInsert, + final float rotationDelete, final float distance, final float farZ, final float nearZ, final float height, + final float listenerDistance, final float listenerAOA) { + this.aoa = aoa; + this.fov = fov; + this.rotation = rotation; + this.rotationInsert = rotationInsert; + this.rotationDelete = rotationDelete; + this.distance = distance; + this.farZ = farZ; + this.nearZ = nearZ; + this.height = height; + this.listenerDistance = listenerDistance; + this.listenerAOA = listenerAOA; + } + + public float getRotation(final boolean insertDown, final boolean deleteDown) { + if (insertDown && !deleteDown) { + return this.rotationInsert; + } + if (!insertDown && deleteDown) { + return this.rotationDelete; + } + return this.rotation; + } + + public float getHeight() { + return this.height; + } + + public float getAoa() { + return this.aoa; + } + + public float getFov() { + return this.fov; + } + + public float getRotation() { + return this.rotation; + } + + public float getRotationInsert() { + return this.rotationInsert; + } + + public float getRotationDelete() { + return this.rotationDelete; + } + + public float getDistance() { + return this.distance; + } + + public float getFarZ() { + return this.farZ; + } + + public float getNearZ() { + return this.nearZ; + } + + public float getListenerDistance() { + return this.listenerDistance; + } + + public float getListenerAOA() { + return this.listenerAOA; + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/CameraRates.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/CameraRates.java new file mode 100644 index 0000000..430d4e8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/CameraRates.java @@ -0,0 +1,20 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.camera; + +public class CameraRates { + public final float aoa; + public final float fov; + public final float rotation; + public final float distance; + public final float forward; + public final float strafe; + + public CameraRates(final float aoa, final float fov, final float rotation, final float distance, + final float forward, final float strafe) { + this.aoa = aoa; + this.fov = fov; + this.rotation = rotation; + this.distance = distance; + this.forward = forward; + this.strafe = strafe; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/GameCameraManager.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/GameCameraManager.java new file mode 100644 index 0000000..013b276 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/GameCameraManager.java @@ -0,0 +1,154 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.camera; + +import com.badlogic.gdx.Input; +import com.badlogic.gdx.math.Rectangle; + +public final class GameCameraManager extends CameraManager { + private final CameraPreset[] presets; + private final CameraRates cameraRates; + public final CameraPanControls cameraPanControls; + private int currentPreset = 0; + + public GameCameraManager(final CameraPreset[] presets, final CameraRates cameraRates) { + this.presets = presets; + this.cameraRates = cameraRates; + this.cameraPanControls = new CameraPanControls(); + } + + @Override + public void updateCamera() { + this.quatHeap2.idt(); + final CameraPreset cameraPreset = this.presets[this.currentPreset]; + this.quatHeap.idt(); + this.horizontalAngle = (float) Math.toRadians( + cameraPreset.getRotation(this.cameraPanControls.insertDown, this.cameraPanControls.deleteDown) - 90); + this.quatHeap.setFromAxisRad(0, 0, 1, this.horizontalAngle); + this.distance = Math.max(1200, cameraPreset.getDistance()); + this.verticalAngle = (float) Math.toRadians(Math.min(335, cameraPreset.getAoa()) - 270); + this.quatHeap2.setFromAxisRad(1, 0, 0, this.verticalAngle); + this.quatHeap.mul(this.quatHeap2); + + this.position.set(0, 0, 1); + this.quatHeap.transform(this.position); + this.position.nor(); + this.position.scl(this.distance); + this.position = this.position.add(this.target); + this.camera.perspective((float) Math.toRadians(cameraPreset.getFov() / 2), this.camera.getAspect(), + cameraPreset.getNearZ(), cameraPreset.getFarZ()); + + this.camera.moveToAndFace(this.position, this.target, this.worldUp); + } + + public void resize(final Rectangle viewport) { + this.camera.viewport(viewport); + } + + public void applyVelocity(final float deltaTime, boolean up, boolean down, boolean left, boolean right) { + final float velocityX; + final float velocityY; + up |= this.cameraPanControls.up; + down |= this.cameraPanControls.down; + left |= this.cameraPanControls.left; + right |= this.cameraPanControls.right; + if (up) { + if (down) { + velocityY = 0; + } + else { + velocityY = this.cameraRates.forward; + } + } + else if (down) { + velocityY = -this.cameraRates.forward; + } + else { + velocityY = 0; + } + if (right) { + if (left) { + velocityX = 0; + } + else { + velocityX = this.cameraRates.strafe; + } + } + else if (left) { + velocityX = -this.cameraRates.strafe; + } + else { + velocityX = 0; + } + this.target.add(velocityX * deltaTime, velocityY * deltaTime, 0); + + } + + public void updateTargetZ(final float groundHeight) { + this.target.z = groundHeight + this.presets[this.currentPreset].getHeight(); + } + + public void scrolled(final int amount) { + this.currentPreset -= amount; + if (this.currentPreset < 0) { + this.currentPreset = 0; + } + if (this.currentPreset >= this.presets.length) { + this.currentPreset = this.presets.length - 1; + } + } + + public boolean keyDown(final int keycode) { + if (keycode == Input.Keys.LEFT) { + this.cameraPanControls.left = true; + return true; + } + else if (keycode == Input.Keys.RIGHT) { + this.cameraPanControls.right = true; + return true; + } + else if (keycode == Input.Keys.DOWN) { + this.cameraPanControls.down = true; + return true; + } + else if (keycode == Input.Keys.UP) { + this.cameraPanControls.up = true; + return true; + } + else if (keycode == Input.Keys.INSERT) { + this.cameraPanControls.insertDown = true; + return true; + } + else if (keycode == Input.Keys.FORWARD_DEL) { + this.cameraPanControls.deleteDown = true; + return true; + } + return false; + } + + public boolean keyUp(final int keycode) { + if (keycode == Input.Keys.LEFT) { + this.cameraPanControls.left = false; + return true; + } + else if (keycode == Input.Keys.RIGHT) { + this.cameraPanControls.right = false; + return true; + } + else if (keycode == Input.Keys.DOWN) { + this.cameraPanControls.down = false; + return true; + } + else if (keycode == Input.Keys.UP) { + this.cameraPanControls.up = false; + return true; + } + else if (keycode == Input.Keys.INSERT) { + this.cameraPanControls.insertDown = false; + return true; + } + else if (keycode == Input.Keys.FORWARD_DEL) { + this.cameraPanControls.deleteDown = false; + return true; + } + return false; + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/PortraitCameraManager.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/PortraitCameraManager.java new file mode 100644 index 0000000..4ecf106 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/camera/PortraitCameraManager.java @@ -0,0 +1,52 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.camera; + +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; + +public final class PortraitCameraManager extends CameraManager { + public com.etheller.warsmash.viewer5.handlers.mdx.Camera modelCamera; + protected MdxComplexInstance modelInstance; + + @Override + public void updateCamera() { + this.quatHeap.idt(); + this.quatHeap.setFromAxisRad(0, 0, 1, this.horizontalAngle); + this.quatHeap2.idt(); + this.quatHeap2.setFromAxisRad(1, 0, 0, this.verticalAngle); + this.quatHeap.mul(this.quatHeap2); + + this.position.set(0, 0, 1); + this.quatHeap.transform(this.position); + this.position.scl(this.distance); + this.position = this.position.add(this.target); + if (this.modelCamera != null) { + this.modelCamera.getPositionTranslation(this.cameraPositionTemp, this.modelInstance.sequence, + this.modelInstance.frame, this.modelInstance.counter); + this.modelCamera.getTargetTranslation(this.cameraTargetTemp, this.modelInstance.sequence, + this.modelInstance.frame, this.modelInstance.counter); + + this.position.set(this.modelCamera.position); + this.target.set(this.modelCamera.targetPosition); + + this.position.add(this.cameraPositionTemp[0], this.cameraPositionTemp[1], this.cameraPositionTemp[2]); + this.target.add(this.cameraTargetTemp[0], this.cameraTargetTemp[1], this.cameraTargetTemp[2]); + this.camera.perspective(this.modelCamera.fieldOfView * 0.75f, this.camera.getAspect(), + this.modelCamera.nearClippingPlane, this.modelCamera.farClippingPlane); + } + else { + this.camera.perspective(70, this.camera.getAspect(), 100, 5000); + } + + this.camera.moveToAndFace(this.position, this.target, this.worldUp); + } + + public void setModelInstance(final MdxComplexInstance modelInstance, final MdxModel portraitModel) { + this.modelInstance = modelInstance; + if (modelInstance == null) { + this.modelCamera = null; + } + else if ((portraitModel != null) && (portraitModel.getCameras().size() > 0)) { + this.modelCamera = portraitModel.getCameras().get(0); + } + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/BuildingShadow.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/BuildingShadow.java new file mode 100644 index 0000000..942790e --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/BuildingShadow.java @@ -0,0 +1,7 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.environment; + +public interface BuildingShadow { + void remove(); + + void move(float x, float y); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/CliffMesh.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/CliffMesh.java new file mode 100644 index 0000000..bebea05 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/CliffMesh.java @@ -0,0 +1,103 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.environment; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.GL30; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.util.RenderMathUtils; +import com.hiveworkshop.rms.parsers.mdlx.MdlxGeoset; +import com.hiveworkshop.rms.parsers.mdlx.MdlxModel; + +public class CliffMesh { + public int vertexBuffer; + public int uvBuffer; + public int normalBuffer; + public int indexBuffer; + public int instanceBuffer; + public int indices; + + private FloatBuffer renderJobs = ByteBuffer.allocateDirect(16 * 16).order(ByteOrder.nativeOrder()).asFloatBuffer(); + private final GL30 gl; + + public CliffMesh(final String path, final DataSource dataSource, final GL30 gl) throws IOException { + this.gl = gl; + if (path.endsWith(".mdx") || path.endsWith(".MDX")) { + final MdlxModel model = new MdlxModel(dataSource.read(path)); + final MdlxGeoset geoset = model.getGeosets().get(0); + + this.vertexBuffer = gl.glGenBuffer(); + gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.vertexBuffer); + gl.glBufferData(GL20.GL_ARRAY_BUFFER, geoset.getVertices().length * 4, + RenderMathUtils.wrap(geoset.getVertices()), GL20.GL_STATIC_DRAW); + + this.uvBuffer = gl.glGenBuffer(); + gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.uvBuffer); + gl.glBufferData(GL20.GL_ARRAY_BUFFER, geoset.getUvSets()[0].length * 4, + RenderMathUtils.wrap(geoset.getUvSets()[0]), GL20.GL_STATIC_DRAW); + + this.normalBuffer = gl.glGenBuffer(); + gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.normalBuffer); + gl.glBufferData(GL20.GL_ARRAY_BUFFER, geoset.getNormals().length * 4, + RenderMathUtils.wrap(geoset.getNormals()), GL20.GL_STATIC_DRAW); + + this.instanceBuffer = gl.glGenBuffer(); + + this.indices = geoset.getFaces().length; + this.indexBuffer = gl.glGenBuffer(); + gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.indexBuffer); + gl.glBufferData(GL20.GL_ARRAY_BUFFER, geoset.getFaces().length * 2, + RenderMathUtils.wrapFaces(geoset.getFaces()), GL20.GL_STATIC_DRAW); + } + } + + public void renderQueue(final float[] position) { + if (this.renderJobs.remaining() < 4) { + final int newCapacity = this.renderJobs.capacity() * 2; + final FloatBuffer newRenderJobs = ByteBuffer.allocateDirect(newCapacity * 4).order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + newRenderJobs.clear(); + this.renderJobs.flip(); + newRenderJobs.put(this.renderJobs); + this.renderJobs = newRenderJobs; + } + this.renderJobs.put(position); + } + + public void render(final ShaderProgram cliffShader) { + if (this.renderJobs.position() == 0) { + return; + } + this.renderJobs.flip(); + + this.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.instanceBuffer); + this.gl.glBufferData(GL20.GL_ARRAY_BUFFER, this.renderJobs.remaining() * 4, this.renderJobs, + GL20.GL_DYNAMIC_DRAW); + + this.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.vertexBuffer); + this.gl.glVertexAttribPointer(cliffShader.getAttributeLocation("vPosition"), 3, GL20.GL_FLOAT, false, 0, 0); + + this.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.uvBuffer); + this.gl.glVertexAttribPointer(cliffShader.getAttributeLocation("vUV"), 2, GL20.GL_FLOAT, false, 0, 0); + + this.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.normalBuffer); + this.gl.glVertexAttribPointer(cliffShader.getAttributeLocation("vNormal"), 3, GL20.GL_FLOAT, false, 0, 0); + + this.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.instanceBuffer); + final int offsetAttributeLocation = cliffShader.getAttributeLocation("vOffset"); + this.gl.glVertexAttribPointer(offsetAttributeLocation, 4, GL20.GL_FLOAT, false, 0, 0); + this.gl.glVertexAttribDivisor(offsetAttributeLocation, 1); + + this.gl.glBindBuffer(GL20.GL_ELEMENT_ARRAY_BUFFER, this.indexBuffer); + this.gl.glDrawElementsInstanced(GL20.GL_TRIANGLES, this.indices, GL30.GL_UNSIGNED_SHORT, 0, + this.renderJobs.remaining() / 4); + + this.gl.glVertexAttribDivisor(offsetAttributeLocation, 0); // ToDo use vao + + this.renderJobs.clear(); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/GroundTexture.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/GroundTexture.java new file mode 100644 index 0000000..2f7dcf5 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/GroundTexture.java @@ -0,0 +1,58 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.environment; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.Buffer; + +import com.badlogic.gdx.graphics.GL30; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.util.ImageUtils; +import com.etheller.warsmash.util.ImageUtils.AnyExtensionImage; + +public class GroundTexture { + public int id; + private int tileSize; + public boolean extended; + + public GroundTexture(final String path, final DataSource dataSource, final GL30 gl) throws IOException { + final AnyExtensionImage imageInfo = ImageUtils.getAnyExtensionImageFixRGB(dataSource, path, "ground texture"); + loadImage(path, gl, imageInfo.getImageData(), imageInfo.isNeedsSRGBFix()); + } + + private void loadImage(final String path, final GL30 gl, final BufferedImage image, final boolean sRGBFix) { + if (image == null) { + throw new IllegalStateException("Missing ground texture: " + path); + } + final Buffer buffer = ImageUtils.getTextureBuffer(sRGBFix ? ImageUtils.forceBufferedImagesRGB(image) : image); + final int width = image.getWidth(); + final int height = image.getHeight(); + + this.tileSize = (int) (height * 0.25); + this.extended = (width > height); + + this.id = gl.glGenTexture(); + gl.glBindTexture(GL30.GL_TEXTURE_2D_ARRAY, this.id); + gl.glTexImage3D(GL30.GL_TEXTURE_2D_ARRAY, 0, GL30.GL_RGBA8, this.tileSize, this.tileSize, + this.extended ? 32 : 16, 0, GL30.GL_RGBA, GL30.GL_UNSIGNED_BYTE, null); + gl.glTexParameteri(GL30.GL_TEXTURE_2D_ARRAY, GL30.GL_TEXTURE_MIN_FILTER, GL30.GL_LINEAR_MIPMAP_LINEAR); + gl.glTexParameteri(GL30.GL_TEXTURE_2D_ARRAY, GL30.GL_TEXTURE_WRAP_S, GL30.GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL30.GL_TEXTURE_2D_ARRAY, GL30.GL_TEXTURE_WRAP_T, GL30.GL_CLAMP_TO_EDGE); + + gl.glPixelStorei(GL30.GL_UNPACK_ROW_LENGTH, width); + for (int y = 0; y < 4; y++) { + for (int x = 0; x < 4; x++) { + buffer.position(((y * this.tileSize * width) + (x * this.tileSize)) * 4); + gl.glTexSubImage3D(GL30.GL_TEXTURE_2D_ARRAY, 0, 0, 0, (y * 4) + x, this.tileSize, this.tileSize, 1, + GL30.GL_RGBA, GL30.GL_UNSIGNED_BYTE, buffer); + + if (this.extended) { + buffer.position(((y * this.tileSize * width) + ((x + 4) * this.tileSize)) * 4); + gl.glTexSubImage3D(GL30.GL_TEXTURE_2D_ARRAY, 0, 0, 0, (y * 4) + x + 16, this.tileSize, + this.tileSize, 1, GL30.GL_RGBA, GL30.GL_UNSIGNED_BYTE, buffer); + } + } + } + gl.glPixelStorei(GL30.GL_UNPACK_ROW_LENGTH, 0); + gl.glGenerateMipmap(GL30.GL_TEXTURE_2D_ARRAY); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/IVec3.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/IVec3.java new file mode 100644 index 0000000..08ef443 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/IVec3.java @@ -0,0 +1,38 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.environment; + +public class IVec3 { + public int x; + public int y; + public int z; + + public IVec3(final int x, final int y, final int z) { + this.x = x; + this.y = y; + this.z = z; + } + + public int getX() { + return this.x; + } + + public int getY() { + return this.y; + } + + public int getZ() { + return this.z; + } + + public void setX(final int x) { + this.x = x; + } + + public void setY(final int y) { + this.y = y; + } + + public void setZ(final int z) { + this.z = z; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/PathingGrid.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/PathingGrid.java new file mode 100644 index 0000000..7f1b4ba --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/PathingGrid.java @@ -0,0 +1,466 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.environment; + +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.badlogic.gdx.math.Rectangle; +import com.etheller.warsmash.parsers.w3x.wpm.War3MapWpm; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWorldCollision; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.pathing.CBuildingPathingType; + +public class PathingGrid { + private static final Map movetpToMovementType = new HashMap<>(); + static { + for (final MovementType movementType : MovementType.values()) { + if (!movementType.typeKey.isEmpty()) { + movetpToMovementType.put(movementType.typeKey, movementType); + } + } + } + + private final short[] pathingGrid; + private final short[] dynamicPathingOverlay; // for buildings and trees + private final int[] pathingGridSizes; + private final float[] centerOffset; + private final List dynamicPathingInstances; + + public PathingGrid(final War3MapWpm terrainPathing, final float[] centerOffset) { + this.centerOffset = centerOffset; + this.pathingGrid = terrainPathing.getPathing(); + this.pathingGridSizes = terrainPathing.getSize(); + this.dynamicPathingOverlay = new short[this.pathingGrid.length]; + this.dynamicPathingInstances = new ArrayList<>(); + } + + // this blit function is basically copied from HiveWE, maybe remember to mention + // that in credits as well: + // https://github.com/stijnherfst/HiveWE/blob/master/Base/PathingMap.cpp + private void blitPathingOverlayTexture(final float positionX, final float positionY, final int rotationInput, + final BufferedImage pathingTextureTga) { + final int rotation = (rotationInput + 450) % 360; + final int divW = ((rotation % 180) != 0) ? pathingTextureTga.getHeight() : pathingTextureTga.getWidth(); + final int divH = ((rotation % 180) != 0) ? pathingTextureTga.getWidth() : pathingTextureTga.getHeight(); + for (int j = 0; j < pathingTextureTga.getHeight(); j++) { + for (int i = 0; i < pathingTextureTga.getWidth(); i++) { + int x = i; + int y = j; + + switch (rotation) { + case 90: + x = pathingTextureTga.getHeight() - 1 - j; + y = i; + break; + case 180: + x = pathingTextureTga.getWidth() - 1 - i; + y = pathingTextureTga.getHeight() - 1 - j; + break; + case 270: + x = j; + y = pathingTextureTga.getWidth() - 1 - i; + break; + } + // Width and height for centering change if rotation is not divisible by 180 + final int xx = (getCellX(positionX) + x) - (divW / 2); + final int yy = (getCellY(positionY) + y) - (divH / 2); + + if ((xx < 0) || (xx > (this.pathingGridSizes[0] - 1)) || (yy < 0) + || (yy > (this.pathingGridSizes[1] - 1))) { + continue; + } + + final int rgb = pathingTextureTga.getRGB(i, pathingTextureTga.getHeight() - 1 - j); + byte data = 0; + if ((rgb & 0xFF) > 127) { + data |= PathingFlags.UNBUILDABLE; + } + if (((rgb & 0xFF00) >>> 8) > 127) { + data |= PathingFlags.UNFLYABLE; + } + if (((rgb & 0xFF0000) >>> 16) > 127) { + data |= PathingFlags.UNWALKABLE | PathingFlags.UNSWIMABLE; + } + this.dynamicPathingOverlay[(yy * this.pathingGridSizes[0]) + xx] |= data; + } + } + } + + public boolean checkPathingTexture(final float positionX, final float positionY, final int rotationInput, + final BufferedImage pathingTextureTga, final EnumSet preventPathingTypes, + final EnumSet requirePathingTypes, final CWorldCollision cWorldCollision, + final CUnit unitToExcludeFromCollisionChecks) { + final int rotation = (rotationInput + 450) % 360; + final int divW = ((rotation % 180) != 0) ? pathingTextureTga.getHeight() : pathingTextureTga.getWidth(); + final int divH = ((rotation % 180) != 0) ? pathingTextureTga.getWidth() : pathingTextureTga.getHeight(); + short anyPathingTypesInRegion = 0; + short pathingTypesFillingRegion = (short) 0xFFFF; + for (int j = 0; j < pathingTextureTga.getHeight(); j++) { + for (int i = 0; i < pathingTextureTga.getWidth(); i++) { + int x = i; + int y = j; + + switch (rotation) { + case 90: + x = pathingTextureTga.getHeight() - 1 - j; + y = i; + break; + case 180: + x = pathingTextureTga.getWidth() - 1 - i; + y = pathingTextureTga.getHeight() - 1 - j; + break; + case 270: + x = j; + y = pathingTextureTga.getWidth() - 1 - i; + break; + } + // Width and height for centering change if rotation is not divisible by 180 + final int xx = (getCellX(positionX) + x) - (divW / 2); + final int yy = (getCellY(positionY) + y) - (divH / 2); + + if ((xx < 0) || (xx > (this.pathingGridSizes[0] - 1)) || (yy < 0) + || (yy > (this.pathingGridSizes[1] - 1))) { + continue; + } + + final short cellPathing = getCellPathing(xx, yy); + anyPathingTypesInRegion |= cellPathing; + pathingTypesFillingRegion &= cellPathing; + } + } + final float width = pathingTextureTga.getWidth() * 32f; + final float height = pathingTextureTga.getHeight() * 32f; + final float offsetX = ((pathingTextureTga.getWidth() % 2) == 1) ? 16f : 0f; + final float offsetY = ((pathingTextureTga.getHeight() % 2) == 1) ? 16f : 0f; + final Rectangle pathingMapRectangle = new Rectangle((positionX - (width / 2)) + offsetX, + (positionY - (height / 2)) + offsetY, width, height); + short anyPathingTypeWithUnit = 0; + if (cWorldCollision.intersectsAnythingOtherThan(pathingMapRectangle, unitToExcludeFromCollisionChecks, + MovementType.AMPHIBIOUS)) { + System.out.println("intersects amph unit"); + anyPathingTypesInRegion |= PathingFlags.UNBUILDABLE | PathingFlags.UNWALKABLE | PathingFlags.UNSWIMABLE; + anyPathingTypeWithUnit |= PathingFlags.UNBUILDABLE | PathingFlags.UNWALKABLE | PathingFlags.UNSWIMABLE; + } + if (cWorldCollision.intersectsAnythingOtherThan(pathingMapRectangle, unitToExcludeFromCollisionChecks, + MovementType.FLOAT)) { + System.out.println("intersects float unit"); + anyPathingTypesInRegion |= PathingFlags.UNSWIMABLE; + anyPathingTypeWithUnit |= PathingFlags.UNSWIMABLE; + } + if (cWorldCollision.intersectsAnythingOtherThan(pathingMapRectangle, unitToExcludeFromCollisionChecks, + MovementType.FLY)) { + System.out.println("intersects fly unit"); + anyPathingTypesInRegion |= PathingFlags.UNFLYABLE; + anyPathingTypeWithUnit |= PathingFlags.UNFLYABLE; + } + if (cWorldCollision.intersectsAnythingOtherThan(pathingMapRectangle, unitToExcludeFromCollisionChecks, + MovementType.FOOT)) { + System.out.println("intersects foot unit"); + anyPathingTypesInRegion |= PathingFlags.UNBUILDABLE | PathingFlags.UNWALKABLE; + anyPathingTypeWithUnit |= PathingFlags.UNBUILDABLE | PathingFlags.UNWALKABLE; + } + pathingTypesFillingRegion &= anyPathingTypeWithUnit; + for (final CBuildingPathingType pathingType : preventPathingTypes) { + if (PathingFlags.isPathingFlag(anyPathingTypesInRegion, pathingType)) { + return false; + } + } + for (final CBuildingPathingType pathingType : requirePathingTypes) { + if (!PathingFlags.isPathingFlag(pathingTypesFillingRegion, pathingType)) { + return false; + } + } + return true; + } + + public RemovablePathingMapInstance blitRemovablePathingOverlayTexture(final float positionX, final float positionY, + final int rotationInput, final BufferedImage pathingTextureTga) { + final RemovablePathingMapInstance removablePathingMapInstance = new RemovablePathingMapInstance(positionX, + positionY, rotationInput, pathingTextureTga); + removablePathingMapInstance.blit(); + this.dynamicPathingInstances.add(removablePathingMapInstance); + return removablePathingMapInstance; + } + + public RemovablePathingMapInstance createRemovablePathingOverlayTexture(final float positionX, + final float positionY, final int rotationInput, final BufferedImage pathingTextureTga) { + return new RemovablePathingMapInstance(positionX, positionY, rotationInput, pathingTextureTga); + } + + public int getWidth() { + return this.pathingGridSizes[0]; + } + + public int getHeight() { + return this.pathingGridSizes[1]; + } + + public boolean contains(final float x, final float y) { + final int cellX = getCellX(x); + final int cellY = getCellY(y); + return (cellX >= 0) && (cellY >= 0) && (cellX < this.pathingGridSizes[0]) && (cellY < this.pathingGridSizes[1]); + } + + public short getPathing(final float x, final float y) { + return getCellPathing(getCellX(x), getCellY(y)); + } + + public int getCellX(final float x) { + final float userCellSpaceX = (x - this.centerOffset[0]) / 32.0f; + final int cellX = (int) userCellSpaceX; + return cellX; + } + + public int getCellY(final float y) { + final float userCellSpaceY = (y - this.centerOffset[1]) / 32.0f; + final int cellY = (int) userCellSpaceY; + return cellY; + } + + public float getWorldX(final int cellX) { + return (cellX * 32f) + this.centerOffset[0] + 16f; + } + + public float getWorldY(final int cellY) { + return (cellY * 32f) + this.centerOffset[1] + 16f; + } + + public float getWorldXFromCorner(final int cornerX) { + return (cornerX * 32f) + this.centerOffset[0]; + } + + public float getWorldYFromCorner(final int cornerY) { + return (cornerY * 32f) + this.centerOffset[1]; + } + + public int getCornerX(final float x) { + final float userCellSpaceX = ((x + 16f) - this.centerOffset[0]) / 32.0f; + final int cellX = (int) userCellSpaceX; + return cellX; + } + + public int getCornerY(final float y) { + final float userCellSpaceY = ((y + 16f) - this.centerOffset[1]) / 32.0f; + final int cellY = (int) userCellSpaceY; + return cellY; + } + + public short getCellPathing(final int cellX, final int cellY) { + final int index = (cellY * this.pathingGridSizes[0]) + cellX; + return (short) (this.pathingGrid[index] | this.dynamicPathingOverlay[index]); + } + + public boolean isPathable(final float x, final float y, final PathingType pathingType) { + return !PathingFlags.isPathingFlag(getPathing(x, y), pathingType.preventionFlag); + } + + public boolean isPathable(final float x, final float y, final MovementType pathingType) { + return pathingType.isPathable(getPathing(x, y)); + } + + public boolean isPathable(final float unitX, final float unitY, final MovementType pathingType, + final float collisionSize) { + if (collisionSize == 0f) { + return pathingType.isPathable(getPathing(unitX, unitY)); + } + for (int i = -1; i <= 1; i++) { + for (int j = -1; j <= 1; j++) { + final float unitPathingX = unitX + (i * collisionSize); + final float unitPathingY = unitY + (j * collisionSize); + if (!contains(unitPathingX, unitPathingY) + || !pathingType.isPathable(getPathing(unitPathingX, unitPathingY))) { + return false; + } + } + } +// final float maxX = unitX + collisionSize; +// final float maxY = unitY + collisionSize; +// for (float minX = unitX - collisionSize; minX < maxX; minX += 32f) { +// for (float minY = unitY - collisionSize; minY < maxY; minY += 32f) { +// if (!pathingType.isPathable(getPathing(minX, minY))) { +// return false; +// } +// } +// } + return true; + } + + public boolean isUnitCell(final float queryX, final float queryY, final float unitX, final float unitY, + final MovementType movementType, final float collisionSize) { + final float maxX = unitX + collisionSize; + final float maxY = unitY + collisionSize; + final int cellX = getCellX(queryX); + final int cellY = getCellY(queryY); + for (float minX = unitX - collisionSize; minX < maxX; minX += 32f) { + for (float minY = unitY - collisionSize; minY < maxY; minY += 32f) { + final int yy = getCellY(minY); + final int xx = getCellX(minX); + if ((yy == cellY) && (xx == cellX)) { + return true; + } + } + } + return false; + } + + public boolean isCellPathable(final int x, final int y, final MovementType pathingType, final float collisionSize) { + return isPathable(getWorldX(x), getWorldY(y), pathingType, collisionSize); + } + + public boolean isCellPathable(final int x, final int y, final MovementType pathingType) { + return pathingType.isPathable(getCellPathing(x, y)); + } + + public static boolean isPathingFlag(final short pathingValue, final PathingType pathingType) { + return !PathingFlags.isPathingFlag(pathingValue, pathingType.preventionFlag); + } + + // movetp referring to the unit data field of the same name + public static MovementType getMovementType(final String movetp) { + return movetpToMovementType.get(movetp); + } + + public static final class PathingFlags { + public static int UNWALKABLE = 0x2; + public static int UNFLYABLE = 0x4; + public static int UNBUILDABLE = 0x8; + public static int UNSWIMABLE = 0x40; // PROBABLY, didn't confirm this flag value is accurate + + public static boolean isPathingFlag(final short pathingValue, final int flag) { + return (pathingValue & flag) != 0; + } + + public static boolean isPathingFlag(final short pathingValue, final CBuildingPathingType pathingType) { + switch (pathingType) { + case BLIGHTED: + throw new IllegalArgumentException("Blight pathing check system is Not Yet Implemented"); + case UNAMPH: + return PathingFlags.isPathingFlag(pathingValue, PathingFlags.UNWALKABLE) + && PathingFlags.isPathingFlag(pathingValue, PathingFlags.UNSWIMABLE); + case UNBUILDABLE: + return PathingFlags.isPathingFlag(pathingValue, PathingFlags.UNBUILDABLE); + case UNFLOAT: + return PathingFlags.isPathingFlag(pathingValue, PathingFlags.UNSWIMABLE); + case UNFLYABLE: + return PathingFlags.isPathingFlag(pathingValue, PathingFlags.UNFLYABLE); + case UNWALKABLE: + return PathingFlags.isPathingFlag(pathingValue, PathingFlags.UNWALKABLE); + default: + return false; + } + } + + private PathingFlags() { + } + } + + public static enum MovementType { + FOOT("foot") { + @Override + public boolean isPathable(final short pathingValue) { + return !PathingFlags.isPathingFlag(pathingValue, PathingFlags.UNWALKABLE); + } + }, + FOOT_NO_COLLISION("") { + @Override + public boolean isPathable(final short pathingValue) { + return !PathingFlags.isPathingFlag(pathingValue, PathingFlags.UNWALKABLE); + } + }, + HORSE("horse") { + @Override + public boolean isPathable(final short pathingValue) { + return !PathingFlags.isPathingFlag(pathingValue, PathingFlags.UNWALKABLE); + } + }, + FLY("fly") { + @Override + public boolean isPathable(final short pathingValue) { + return !PathingFlags.isPathingFlag(pathingValue, PathingFlags.UNFLYABLE); + } + }, + HOVER("hover") { + @Override + public boolean isPathable(final short pathingValue) { + return !PathingFlags.isPathingFlag(pathingValue, PathingFlags.UNWALKABLE); + } + }, + FLOAT("float") { + @Override + public boolean isPathable(final short pathingValue) { + return !PathingFlags.isPathingFlag(pathingValue, PathingFlags.UNSWIMABLE); + } + }, + AMPHIBIOUS("amph") { + @Override + public boolean isPathable(final short pathingValue) { + return !PathingFlags.isPathingFlag(pathingValue, PathingFlags.UNWALKABLE) + || !PathingFlags.isPathingFlag(pathingValue, PathingFlags.UNSWIMABLE); + } + }, + DISABLED("") { + @Override + public boolean isPathable(final short pathingValue) { + return true; + } + }; + private final String typeKey; + + // TODO windwalk pathing type can walk through units but not through items + + private MovementType(final String typeKey) { + this.typeKey = typeKey; + } + + public abstract boolean isPathable(short pathingValue); + } + + public static enum PathingType { + WALKABLE(PathingFlags.UNWALKABLE), + FLYABLE(PathingFlags.UNFLYABLE), + BUILDABLE(PathingFlags.UNBUILDABLE), + SWIMMABLE(PathingFlags.UNSWIMABLE); + + private final int preventionFlag; + + private PathingType(final int preventionFlag) { + this.preventionFlag = preventionFlag; + } + } + + public final class RemovablePathingMapInstance { + private final float positionX; + private final float positionY; + private final int rotationInput; + private final BufferedImage pathingTextureTga; + + public RemovablePathingMapInstance(final float positionX, final float positionY, final int rotationInput, + final BufferedImage pathingTextureTga) { + this.positionX = positionX; + this.positionY = positionY; + this.rotationInput = rotationInput; + this.pathingTextureTga = pathingTextureTga; + } + + private void blit() { + blitPathingOverlayTexture(this.positionX, this.positionY, this.rotationInput, this.pathingTextureTga); + } + + public void remove() { + PathingGrid.this.dynamicPathingInstances.remove(this); + Arrays.fill(PathingGrid.this.dynamicPathingOverlay, (short) 0); + for (final RemovablePathingMapInstance instance : PathingGrid.this.dynamicPathingInstances) { + instance.blit(); + } + } + + public void add() { + PathingGrid.this.dynamicPathingInstances.add(this); + blit(); + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/RenderCorner.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/RenderCorner.java new file mode 100644 index 0000000..421385e --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/RenderCorner.java @@ -0,0 +1,16 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.environment; + +import com.etheller.warsmash.parsers.w3x.w3e.Corner; + +public class RenderCorner extends Corner { + public boolean cliff; + public boolean romp; + public float rampAdjust; + public float depth; + public boolean hideCliff; + + public RenderCorner(final Corner corner) { + super(corner); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/Shapes.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/Shapes.java new file mode 100644 index 0000000..588a682 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/Shapes.java @@ -0,0 +1,32 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.environment; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL30; +import com.etheller.warsmash.util.RenderMathUtils; + +public class Shapes { + public static Shapes INSTANCE = new Shapes(); + static { + INSTANCE.init(); + } + + public int vertexBuffer; + public int indexBuffer; + + float[][] quadVertices = { { 1, 1 }, { 0, 1 }, { 0, 0 }, { 1, 0 } }; + + int[][] quadIndices = { { 1, 3, 0 }, { 2, 3, 1 } }; + + public void init() { + this.vertexBuffer = Gdx.gl30.glGenBuffer(); + Gdx.gl30.glBindBuffer(GL30.GL_ARRAY_BUFFER, this.vertexBuffer); + Gdx.gl30.glBufferData(GL30.GL_ARRAY_BUFFER, this.quadVertices.length * 8, + RenderMathUtils.wrapPairs(this.quadVertices), GL30.GL_STATIC_DRAW); + + this.indexBuffer = Gdx.gl30.glGenBuffer(); + Gdx.gl30.glBindBuffer(GL30.GL_ELEMENT_ARRAY_BUFFER, this.indexBuffer); + Gdx.gl30.glBufferData(GL30.GL_ELEMENT_ARRAY_BUFFER, this.quadIndices.length * 3 * 4, + RenderMathUtils.wrap(this.quadIndices), GL30.GL_STATIC_DRAW); + + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/Terrain.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/Terrain.java new file mode 100644 index 0000000..3cdb5a7 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/Terrain.java @@ -0,0 +1,1559 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.environment; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.nio.Buffer; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import java.util.function.Consumer; + +import org.apache.commons.compress.utils.IOUtils; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.GL30; +import com.badlogic.gdx.graphics.glutils.ShaderProgram; +import com.badlogic.gdx.math.Intersector; +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.parsers.w3x.w3e.Corner; +import com.etheller.warsmash.parsers.w3x.w3e.War3MapW3e; +import com.etheller.warsmash.parsers.w3x.w3i.War3MapW3i; +import com.etheller.warsmash.parsers.w3x.wpm.War3MapWpm; +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.units.Element; +import com.etheller.warsmash.util.ImageUtils; +import com.etheller.warsmash.util.ImageUtils.AnyExtensionImage; +import com.etheller.warsmash.util.RenderMathUtils; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.util.WorldEditStrings; +import com.etheller.warsmash.viewer5.Camera; +import com.etheller.warsmash.viewer5.PathSolver; +import com.etheller.warsmash.viewer5.RawOpenGLTextureResource; +import com.etheller.warsmash.viewer5.Texture; +import com.etheller.warsmash.viewer5.gl.DataTexture; +import com.etheller.warsmash.viewer5.gl.Extensions; +import com.etheller.warsmash.viewer5.gl.WebGL; +import com.etheller.warsmash.viewer5.handlers.w3x.DynamicShadowManager; +import com.etheller.warsmash.viewer5.handlers.w3x.SplatModel; +import com.etheller.warsmash.viewer5.handlers.w3x.SplatModel.SplatMover; +import com.etheller.warsmash.viewer5.handlers.w3x.Variations; +import com.etheller.warsmash.viewer5.handlers.w3x.W3xSceneLightManager; +import com.etheller.warsmash.viewer5.handlers.w3x.W3xShaders; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; + +public class Terrain { + private static final String[] colorTags = { "R", "G", "B", "A" }; + private static final float[] sizeHeap = new float[2]; + private static final Vector3 normalHeap1 = new Vector3(); + private static final Vector3 normalHeap2 = new Vector3(); + private static final float[] fourComponentHeap = new float[4]; + private static final Matrix4 tempMatrix = new Matrix4(); + private static final boolean WIREFRAME_TERRAIN = false; + // In WC3 they didn't finish developing the height 3 ramps + // There are a couple of models for some of them but generally they are just bad + // voodoo. Enabling this setting should be coupled with creating + // new artwork for advanced ramp use cases that don't exist in WC3. + private static final boolean DISALLOW_HEIGHT_3_RAMPS = true; + + public ShaderProgram groundShader; + public ShaderProgram waterShader; + public ShaderProgram cliffShader; + public float waterIndex; + public float waterIncreasePerFrame; + public float waterHeightOffset; + + // + public List groundTextures = new ArrayList<>(); + public List cliffTextures = new ArrayList<>(); + public RenderCorner[][] corners; + public int columns; + public int rows; + public int blightTextureIndex = -1; + public float[] maxDeepColor = new float[4]; + public float[] minDeepColor = new float[4]; + public float[] maxShallowColor = new float[4]; + public float[] minShallowColor = new float[4]; + + private final DataTable terrainTable; + private final DataTable cliffTable; + private final DataTable waterTable; + private final int waterTextureCount; + private int cliffTexturesSize; + private final List cliffMeshes = new ArrayList<>(); + private final Map pathToCliff = new HashMap<>(); + private final Map groundTextureToId = new HashMap<>(); + private final List cliffToGroundTexture = new ArrayList<>(); + private final List cliffs = new ArrayList<>(); + private final DataSource dataSource; + private final float[] groundHeights; + private final float[] groundCornerHeights; + private final short[] groundTextureList; + private final float[] waterHeights; + private final byte[] waterExistsData; + + private int groundTextureData = -1; + private final int groundHeight; + private final int groundCornerHeight; + private final int groundCornerHeightLinear; + private final int cliffTextureArray; + private final int waterHeight; + private final int waterExists; + private final int waterTextureArray; + private final Camera camera; + private final War3MapViewer viewer; + public float[] centerOffset; + private final WebGL webGL; + private final ShaderProgram uberSplatShader; + public final DataTable uberSplatTable; + + private final Map uberSplatModels; + private final List uberSplatModelsList; + private int shadowMap; + public final Map splats = new HashMap<>(); + public final Map> shadows = new HashMap<>(); + public final Map shadowTextures = new HashMap<>(); + private final int[] mapBounds; + private final float[] shaderMapBounds; + private final int[] mapSize; + public final SoftwareGroundMesh softwareGroundMesh; + private final int testArrayBuffer; + private final int testElementBuffer; + private boolean initShadowsFinished = false; + private byte[] staticShadowData; + private byte[] shadowData; + + public Terrain(final War3MapW3e w3eFile, final War3MapWpm terrainPathing, final War3MapW3i w3iFile, + final WebGL webGL, final DataSource dataSource, final WorldEditStrings worldEditStrings, + final War3MapViewer viewer, final DataTable worldEditData) throws IOException { + this.webGL = webGL; + this.viewer = viewer; + this.camera = viewer.worldScene.camera; + this.dataSource = dataSource; + final String texturesExt = ".blp"; + final Corner[][] corners = w3eFile.getCorners(); + this.corners = new RenderCorner[corners[0].length][corners.length]; + for (int i = 0; i < corners.length; i++) { + for (int j = 0; j < corners[i].length; j++) { + this.corners[j][i] = new RenderCorner(corners[i][j]); + } + } + final int width = w3eFile.getMapSize()[0]; + final int height = w3eFile.getMapSize()[1]; + this.columns = width; + this.rows = height; + for (int i = 0; i < (width - 1); i++) { + for (int j = 0; j < (height - 1); j++) { + final RenderCorner bottomLeft = this.corners[i][j]; + final RenderCorner bottomRight = this.corners[i + 1][j]; + final RenderCorner topLeft = this.corners[i][j + 1]; + final RenderCorner topRight = this.corners[i + 1][j + 1]; + + bottomLeft.cliff = (bottomLeft.getLayerHeight() != bottomRight.getLayerHeight()) + || (bottomLeft.getLayerHeight() != topLeft.getLayerHeight()) + || (bottomLeft.getLayerHeight() != topRight.getLayerHeight()); + } + } + + this.terrainTable = new DataTable(worldEditStrings); + try (InputStream terrainSlkStream = dataSource.getResourceAsStream("TerrainArt\\Terrain.slk")) { + this.terrainTable.readSLK(terrainSlkStream); + } + this.cliffTable = new DataTable(worldEditStrings); + try (InputStream cliffSlkStream = dataSource.getResourceAsStream("TerrainArt\\CliffTypes.slk")) { + this.cliffTable.readSLK(cliffSlkStream); + } + this.waterTable = new DataTable(worldEditStrings); + try (InputStream waterSlkStream = dataSource.getResourceAsStream("TerrainArt\\Water.slk")) { + this.waterTable.readSLK(waterSlkStream); + } + this.uberSplatTable = new DataTable(worldEditStrings); + try (InputStream uberSlkStream = dataSource.getResourceAsStream("Splats\\UberSplatData.slk")) { + this.uberSplatTable.readSLK(uberSlkStream); + } + + final char tileset = w3eFile.getTileset(); + final Element waterInfo = this.waterTable.get(tileset + "Sha"); + if (waterInfo != null) { + this.waterHeightOffset = waterInfo.getFieldFloatValue("height"); + this.waterTextureCount = waterInfo.getFieldValue("numTex"); + this.waterIncreasePerFrame = waterInfo.getFieldValue("texRate"); + } + else { + this.waterHeightOffset = 0; + this.waterTextureCount = 0; + this.waterIncreasePerFrame = 0; + } + + loadWaterColor(this.minShallowColor, "Smin", waterInfo); + loadWaterColor(this.maxShallowColor, "Smax", waterInfo); + loadWaterColor(this.minDeepColor, "Dmin", waterInfo); + loadWaterColor(this.maxDeepColor, "Dmax", waterInfo); + for (int i = 0; i < 3; i++) { + if (this.minDeepColor[i] > this.maxDeepColor[i]) { + this.maxDeepColor[i] = this.minDeepColor[i]; + } + } + + // Cliff Meshes + + Map cliffVars = Variations.CLIFF_VARS; + for (final Map.Entry cliffVar : cliffVars.entrySet()) { + final Integer maxVariations = cliffVar.getValue(); + for (int variation = 0; variation <= maxVariations; variation++) { + final String fileName = "Doodads\\Terrain\\Cliffs\\Cliffs" + cliffVar.getKey() + variation + ".mdx"; + this.cliffMeshes.add(new CliffMesh(fileName, dataSource, Gdx.gl30)); + this.pathToCliff.put("Cliffs" + cliffVar.getKey() + variation, this.cliffMeshes.size() - 1); + } + } + cliffVars = Variations.CITY_CLIFF_VARS; + for (final Map.Entry cliffVar : cliffVars.entrySet()) { + final Integer maxVariations = cliffVar.getValue(); + for (int variation = 0; variation <= maxVariations; variation++) { + final String fileName = "Doodads\\Terrain\\CityCliffs\\CityCliffs" + cliffVar.getKey() + variation + + ".mdx"; + this.cliffMeshes.add(new CliffMesh(fileName, dataSource, Gdx.gl30)); + this.pathToCliff.put("CityCliffs" + cliffVar.getKey() + variation, this.cliffMeshes.size() - 1); + } + } + + // Ground textures + for (final War3ID groundTile : w3eFile.getGroundTiles()) { + final Element terrainTileInfo = this.terrainTable.get(groundTile.asStringValue()); + if (terrainTileInfo == null) { + throw new RuntimeException("No terrain info for: " + groundTile.asStringValue()); + } + final String dir = terrainTileInfo.getField("dir"); + final String file = terrainTileInfo.getField("file"); + this.groundTextures.add(new GroundTexture(dir + "\\" + file + texturesExt, dataSource, Gdx.gl30)); + this.groundTextureToId.put(groundTile.asStringValue(), this.groundTextures.size() - 1); + } + + final Element tilesets = worldEditData.get("TileSets"); + + this.blightTextureIndex = this.groundTextures.size(); + this.groundTextures.add(new GroundTexture( + tilesets.getField(Character.toString(tileset)).split(",")[1] + texturesExt, dataSource, Gdx.gl30)); + + // Cliff Textures + for (final War3ID cliffTile : w3eFile.getCliffTiles()) { + final Element cliffInfo = this.cliffTable.get(cliffTile.asStringValue()); + if (cliffInfo == null) { + System.err.println("Missing cliff type: " + cliffTile.asStringValue()); + continue; + } + final String texDir = cliffInfo.getField("texDir"); + final String texFile = cliffInfo.getField("texFile"); + final AnyExtensionImage imageInfo = ImageUtils.getAnyExtensionImageFixRGB(dataSource, + texDir + "\\" + texFile + texturesExt, "cliff texture"); + final BufferedImage image = imageInfo.getRGBCorrectImageData(); + this.cliffTextures + .add(new UnloadedTexture(image.getWidth(), image.getHeight(), ImageUtils.getTextureBuffer(image), + cliffInfo.getField("cliffModelDir"), cliffInfo.getField("rampModelDir"))); + this.cliffTexturesSize = Math.max(this.cliffTexturesSize, + this.cliffTextures.get(this.cliffTextures.size() - 1).width); + this.cliffToGroundTexture.add(this.groundTextureToId.get(cliffInfo.getField("groundTile"))); + } + + updateCliffMeshes(new Rectangle(0, 0, width - 1, height - 1)); + + // prepare GPU data + this.groundHeights = new float[width * height]; + this.groundCornerHeights = new float[width * height]; + this.groundTextureList = new short[(width - 1) * (height - 1) * 4]; + this.waterHeights = new float[width * height]; + this.waterExistsData = new byte[width * height]; + + updateGroundTextures(new Rectangle(0, 0, width - 1, height - 1)); + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + this.groundCornerHeights[(j * width) + i] = this.corners[i][j].computeFinalGroundHeight(); + this.waterExistsData[(j * width) + i] = (byte) this.corners[i][j].getWater(); + this.groundHeights[(j * width) + i] = this.corners[i][j].getGroundHeight(); + this.waterHeights[(j * width) + i] = this.corners[i][j].getWaterHeight(); + } + } + + final GL30 gl = Gdx.gl30; + // Ground + this.groundTextureData = gl.glGenTexture(); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.groundTextureData); + gl.glTexImage2D(GL30.GL_TEXTURE_2D, 0, GL30.GL_RGBA16UI, width - 1, height - 1, 0, GL30.GL_RGBA_INTEGER, + GL30.GL_UNSIGNED_SHORT, RenderMathUtils.wrapShort(this.groundTextureList)); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_MAG_FILTER, GL30.GL_NEAREST); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_MIN_FILTER, GL30.GL_NEAREST); + + this.groundHeight = gl.glGenTexture(); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.groundHeight); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_MAG_FILTER, GL30.GL_LINEAR); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_MIN_FILTER, GL30.GL_LINEAR); + + gl.glTexImage2D(GL30.GL_TEXTURE_2D, 0, GL30.GL_R16F, width, height, 0, GL30.GL_RED, GL30.GL_FLOAT, + RenderMathUtils.wrap(this.groundHeights)); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_WRAP_S, GL30.GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_WRAP_T, GL30.GL_CLAMP_TO_EDGE); + + this.groundCornerHeight = gl.glGenTexture(); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.groundCornerHeight); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_MAG_FILTER, GL30.GL_NEAREST); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_MIN_FILTER, GL30.GL_NEAREST); + + gl.glTexImage2D(GL30.GL_TEXTURE_2D, 0, GL30.GL_R16F, width, height, 0, GL30.GL_RED, GL30.GL_FLOAT, + RenderMathUtils.wrap(this.groundCornerHeights)); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_WRAP_S, GL30.GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_WRAP_T, GL30.GL_CLAMP_TO_EDGE); + + this.groundCornerHeightLinear = gl.glGenTexture(); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.groundCornerHeightLinear); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_MAG_FILTER, GL30.GL_LINEAR); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_MIN_FILTER, GL30.GL_LINEAR); + + gl.glTexImage2D(GL30.GL_TEXTURE_2D, 0, GL30.GL_R16F, width, height, 0, GL30.GL_RED, GL30.GL_FLOAT, + RenderMathUtils.wrap(this.groundCornerHeights)); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_WRAP_S, GL30.GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_WRAP_T, GL30.GL_CLAMP_TO_EDGE); + + // Cliff + this.cliffTextureArray = gl.glGenTexture(); + gl.glBindTexture(GL30.GL_TEXTURE_2D_ARRAY, this.cliffTextureArray); + gl.glTexImage3D(GL30.GL_TEXTURE_2D_ARRAY, 0, GL30.GL_RGBA8, this.cliffTexturesSize, this.cliffTexturesSize, + this.cliffTextures.size(), 0, GL30.GL_RGBA, GL30.GL_UNSIGNED_BYTE, null); + gl.glTexParameteri(GL30.GL_TEXTURE_2D_ARRAY, GL30.GL_TEXTURE_MIN_FILTER, GL30.GL_LINEAR_MIPMAP_LINEAR); + gl.glTexParameteri(GL30.GL_TEXTURE_2D_ARRAY, GL30.GL_TEXTURE_BASE_LEVEL, 0); + + int sub = 0; + for (final UnloadedTexture i : this.cliffTextures) { + gl.glTexSubImage3D(GL30.GL_TEXTURE_2D_ARRAY, 0, 0, 0, sub, i.width, i.height, 1, GL30.GL_RGBA, + GL30.GL_UNSIGNED_BYTE, i.data); + sub += 1; + } + gl.glGenerateMipmap(GL30.GL_TEXTURE_2D_ARRAY); + + // Water + this.waterHeight = gl.glGenTexture(); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.waterHeight); + gl.glTexImage2D(GL30.GL_TEXTURE_2D, 0, GL30.GL_R16F, width, height, 0, GL30.GL_RED, GL30.GL_FLOAT, + RenderMathUtils.wrap(this.waterHeights)); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_MAG_FILTER, GL30.GL_NEAREST); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_MIN_FILTER, GL30.GL_NEAREST); + + this.waterExists = gl.glGenTexture(); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.waterExists); + gl.glPixelStorei(GL30.GL_UNPACK_ALIGNMENT, 1); + gl.glTexImage2D(GL30.GL_TEXTURE_2D, 0, GL30.GL_R8, width, height, 0, GL30.GL_RED, GL30.GL_UNSIGNED_BYTE, + RenderMathUtils.wrap(this.waterExistsData)); + gl.glPixelStorei(GL30.GL_UNPACK_ALIGNMENT, 4); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_MAG_FILTER, GL30.GL_NEAREST); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_MIN_FILTER, GL30.GL_NEAREST); + + // Water textures + this.waterTextureArray = gl.glGenTexture(); + gl.glBindTexture(GL30.GL_TEXTURE_2D_ARRAY, this.waterTextureArray); + + final String fileName = waterInfo.getField("texFile"); + final List waterTextures = new ArrayList<>(); + boolean anyWaterTextureNeedsSRGB = false; + for (int i = 0; i < this.waterTextureCount; i++) { + final AnyExtensionImage imageInfo = ImageUtils.getAnyExtensionImageFixRGB(dataSource, + fileName + (i < 10 ? "0" : "") + Integer.toString(i) + texturesExt, "water texture"); + final BufferedImage image = imageInfo.getImageData(); + if ((image.getWidth() != 128) || (image.getHeight() != 128)) { + System.err + .println("Odd water texture size detected of " + image.getWidth() + " x " + image.getHeight()); + } + anyWaterTextureNeedsSRGB |= imageInfo.isNeedsSRGBFix(); + waterTextures.add(image); + } + + gl.glTexImage3D(GL30.GL_TEXTURE_2D_ARRAY, 0, anyWaterTextureNeedsSRGB ? GL30.GL_SRGB8_ALPHA8 : GL30.GL_RGBA8, + 128, 128, this.waterTextureCount, 0, GL30.GL_RGBA, GL30.GL_UNSIGNED_BYTE, null); + gl.glTexParameteri(GL30.GL_TEXTURE_2D_ARRAY, GL30.GL_TEXTURE_WRAP_S, GL30.GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL30.GL_TEXTURE_2D_ARRAY, GL30.GL_TEXTURE_WRAP_T, GL30.GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL30.GL_TEXTURE_2D_ARRAY, GL30.GL_TEXTURE_BASE_LEVEL, 0); + + for (int i = 0; i < waterTextures.size(); i++) { + final BufferedImage image = waterTextures.get(i); + gl.glTexSubImage3D(GL30.GL_TEXTURE_2D_ARRAY, 0, 0, 0, i, image.getWidth(), image.getHeight(), 1, + GL30.GL_RGBA, GL30.GL_UNSIGNED_BYTE, ImageUtils.getTextureBuffer(image)); + } + gl.glGenerateMipmap(GL30.GL_TEXTURE_2D_ARRAY); + + updateGroundHeights(new Rectangle(0, 0, width - 1, height - 1)); + + this.groundShader = webGL.createShaderProgram(TerrainShaders.Terrain.vert, TerrainShaders.Terrain.frag); + this.cliffShader = webGL.createShaderProgram(TerrainShaders.Cliffs.vert, TerrainShaders.Cliffs.frag); + this.waterShader = webGL.createShaderProgram(TerrainShaders.Water.vert, TerrainShaders.Water.frag); + + this.uberSplatShader = webGL.createShaderProgram(W3xShaders.UberSplat.vert, W3xShaders.UberSplat.frag); + + // TODO collision bodies (?) + + this.centerOffset = w3eFile.getCenterOffset(); + this.uberSplatModels = new LinkedHashMap<>(); + this.uberSplatModelsList = new ArrayList<>(); + this.mapBounds = w3iFile.getCameraBoundsComplements(); + this.shaderMapBounds = new float[] { (this.mapBounds[0] * 128.0f) + this.centerOffset[0], + (this.mapBounds[2] * 128.0f) + this.centerOffset[1], + ((this.columns - this.mapBounds[1] - 1) * 128.0f) + this.centerOffset[0], + ((this.rows - this.mapBounds[3] - 1) * 128.0f) + this.centerOffset[1] }; + this.shaderMapBoundsRectangle = new Rectangle(this.shaderMapBounds[0], this.shaderMapBounds[1], + this.shaderMapBounds[2] - this.shaderMapBounds[0], this.shaderMapBounds[3] - this.shaderMapBounds[1]); + this.mapSize = w3eFile.getMapSize(); + this.entireMapRectangle = new Rectangle(this.centerOffset[0], this.centerOffset[1], + (this.mapSize[0] * 128f) - 128, (this.mapSize[1] * 128f) - 128); + this.softwareGroundMesh = new SoftwareGroundMesh(this.groundHeights, this.groundCornerHeights, + this.centerOffset, width, height); + + this.testArrayBuffer = gl.glGenBuffer(); + gl.glBindBuffer(GL30.GL_ARRAY_BUFFER, this.testArrayBuffer); + gl.glBufferData(GL30.GL_ARRAY_BUFFER, this.softwareGroundMesh.vertices.length, + RenderMathUtils.wrap(this.softwareGroundMesh.vertices), GL30.GL_STATIC_DRAW); + + this.testElementBuffer = gl.glGenBuffer(); +// gl.glBindBuffer(GL30.GL_ELEMENT_ARRAY_BUFFER, this.testElementBuffer); +// gl.glBufferData(GL30.GL_ELEMENT_ARRAY_BUFFER, this.softwareGroundMesh.indices.length, +// RenderMathUtils.wrap(this.softwareGroundMesh.indices), GL30.GL_STATIC_DRAW); + + this.waveBuilder = new WaveBuilder(this.mapSize, this.waterTable, viewer, this.corners, this.centerOffset, + this.waterHeightOffset, w3eFile, w3iFile); + this.pathingGrid = new PathingGrid(terrainPathing, this.centerOffset); + } + + public void createWaves() { + this.waveBuilder.createWaves(this); + } + + private void updateGroundHeights(final Rectangle area) { + for (int j = (int) area.y; j < (area.y + area.height); j++) { + for (int i = (int) area.x; i < (area.x + area.width); i++) { + this.groundHeights[(j * this.columns) + i] = this.corners[i][j].getGroundHeight(); + + float rampHeight = 0f; + // Check if in one of the configurations the bottom_left is a ramp + XLoop: for (int xOffset = -1; xOffset <= 0; xOffset++) { + for (int yOffset = -1; yOffset <= 0; yOffset++) { + if (((i + xOffset) >= 0) && ((i + xOffset) < (this.columns - 1)) && ((j + yOffset) >= 0) + && ((j + yOffset) < (this.rows - 1))) { + final RenderCorner bottomLeft = this.corners[i + xOffset][j + yOffset]; + final RenderCorner bottomRight = this.corners[i + 1 + xOffset][j + yOffset]; + final RenderCorner topLeft = this.corners[i + xOffset][j + 1 + yOffset]; + final RenderCorner topRight = this.corners[i + 1 + xOffset][j + 1 + yOffset]; + + final int base = Math.min( + Math.min(bottomLeft.getLayerHeight(), bottomRight.getLayerHeight()), + Math.min(topLeft.getLayerHeight(), topRight.getLayerHeight())); + if (this.corners[i][j].getLayerHeight() != base) { + continue; + } + + if (isCornerRampEntrance(i + xOffset, j + yOffset)) { + rampHeight = 0.5f; + break XLoop; + } + } + } + } + + final RenderCorner corner = this.corners[i][j]; + final float newGroundCornerHeight = corner.computeFinalGroundHeight() + rampHeight; + this.groundCornerHeights[(j * this.columns) + i] = newGroundCornerHeight; + corner.depth = (corner.getWater() != 0) + ? (this.waterHeightOffset + corner.getWaterHeight()) - newGroundCornerHeight + : 0; + } + } + updateGroundHeights(); + updateCornerHeights(); + } + + private void updateGroundHeights() { + Gdx.gl30.glBindTexture(GL30.GL_TEXTURE_2D, this.groundHeight); + Gdx.gl30.glTexSubImage2D(GL30.GL_TEXTURE_2D, 0, 0, 0, this.columns, this.rows, GL30.GL_RED, GL30.GL_FLOAT, + RenderMathUtils.wrap(this.groundHeights)); + } + + private void updateCornerHeights() { + final FloatBuffer groundCornerHeightsWrapped = RenderMathUtils.wrap(this.groundCornerHeights); + Gdx.gl30.glBindTexture(GL30.GL_TEXTURE_2D, this.groundCornerHeight); + Gdx.gl30.glTexSubImage2D(GL30.GL_TEXTURE_2D, 0, 0, 0, this.columns, this.rows, GL30.GL_RED, GL30.GL_FLOAT, + groundCornerHeightsWrapped); + Gdx.gl30.glBindTexture(GL30.GL_TEXTURE_2D, this.groundCornerHeightLinear); + Gdx.gl30.glTexSubImage2D(GL30.GL_TEXTURE_2D, 0, 0, 0, this.columns, this.rows, GL30.GL_RED, GL30.GL_FLOAT, + groundCornerHeightsWrapped); + } + + /** + * calculateRamps() is copied from Riv whereas a lot of the rest of the terrain + * was copied from HiveWE + */ + private void calculateRamps() { + final int columns = this.mapSize[0]; + final int rows = this.mapSize[1]; + + final String[] ramps = { "AAHL", "AALH", "ABHL", "AHLA", "ALHA", "ALHB", "BALH", "BHLA", "HAAL", "HBAL", "HLAA", + "HLAB", "LAAH", "LABH", "LHAA", "LHBA" }; + + // Adjust terrain height inside ramps (set rampAdjust) + for (int y = 1; y < (rows - 1); ++y) { + for (int x = 1; x < (columns - 1); ++x) { + final RenderCorner o = this.corners[x][y]; + if (!o.isRamp()) { + continue; + } + final RenderCorner a = this.corners[x - 1][y - 1]; + final RenderCorner b = this.corners[x - 1][y]; + final RenderCorner c = this.corners[x - 1][y + 1]; + final RenderCorner d = this.corners[x][y + 1]; + final RenderCorner e = this.corners[x + 1][y + 1]; + final RenderCorner f = this.corners[x + 1][y]; + final RenderCorner g = this.corners[x + 1][y - 1]; + final RenderCorner h = this.corners[x][y - 1]; + final int base = o.getLayerHeight(); + if ((b.isRamp() && f.isRamp()) || (d.isRamp() && h.isRamp())) { + float adjust = 0; + if (b.isRamp() && f.isRamp()) { + adjust = Math.max(adjust, ((b.getLayerHeight() + f.getLayerHeight()) / 2) - base); + } + if (d.isRamp() && h.isRamp()) { + adjust = Math.max(adjust, ((d.getLayerHeight() + h.getLayerHeight()) / 2) - base); + } + if (a.isRamp() && e.isRamp()) { + adjust = Math.max(adjust, (((a.getLayerHeight() + e.getLayerHeight()) / 2) - base) / 2); + } + if (c.isRamp() && g.isRamp()) { + adjust = Math.max(adjust, (((c.getLayerHeight() + g.getLayerHeight()) / 2) - base) / 2); + } + o.rampAdjust = adjust; + } + } + } + } + + /// TODO clean + /// Function is a bit of a mess + /// Updates the cliff and ramp meshes for an area + private void updateCliffMeshes(final Rectangle area) throws IOException { + // Remove all existing cliff meshes in area + for (int i = this.cliffs.size(); i-- > 0;) { + final IVec3 pos = this.cliffs.get(i); + if (area.contains(pos.x, pos.y)) { + this.cliffs.remove(i); + } + } + + for (int i = (int) area.getX(); i < (int) (area.getX() + area.getWidth()); i++) { + for (int j = (int) area.getY(); j < (int) (area.getY() + area.getHeight()); j++) { + this.corners[i][j].romp = false; + } + } + + final Rectangle adjusted = new Rectangle(area.x - 2, area.y - 2, area.width + 4, area.height + 4); + final Rectangle rampArea = new Rectangle(); + Intersector.intersectRectangles(new Rectangle(0, 0, this.columns, this.rows), adjusted, rampArea); + + // Add new cliff meshes + final int xLimit = (int) ((rampArea.getX() + rampArea.getWidth()) - 1); + for (int i = (int) rampArea.getX(); i < xLimit; i++) { + final int yLimit = (int) ((rampArea.getY() + rampArea.getHeight()) - 1); + for (int j = (int) rampArea.getY(); j < yLimit; j++) { + final RenderCorner bottomLeft = this.corners[i][j]; + final RenderCorner bottomRight = this.corners[i + 1][j]; + final RenderCorner topLeft = this.corners[i][j + 1]; + final RenderCorner topRight = this.corners[i + 1][j + 1]; + + if (bottomLeft.cliff && !bottomLeft.hideCliff) { + final int base = Math.min(Math.min(bottomLeft.getLayerHeight(), bottomRight.getLayerHeight()), + Math.min(topLeft.getLayerHeight(), topRight.getLayerHeight())); + + final boolean facingDown = (topLeft.getLayerHeight() >= bottomLeft.getLayerHeight()) + && (topRight.getLayerHeight() >= bottomRight.getLayerHeight()); + final boolean facingLeft = (bottomRight.getLayerHeight() >= bottomLeft.getLayerHeight()) + && (topRight.getLayerHeight() >= topLeft.getLayerHeight()); + + int bottomLeftCliffTex = bottomLeft.getCliffTexture(); + if (bottomLeftCliffTex == 15) { + bottomLeftCliffTex -= 14; + } + if (!(facingDown && (j == 0)) && !(!facingDown && (j >= (this.rows - 2))) + && !(facingLeft && (i == 0)) && !(!facingLeft && (i >= (this.columns - 2)))) { + final boolean verticalRamp = ((bottomLeft.isRamp()) != (bottomRight.isRamp())) + && ((topLeft.isRamp()) != (topRight.isRamp())); + + final boolean horizontalRamp = ((bottomLeft.isRamp()) != (topLeft.isRamp())) + && ((bottomRight.isRamp()) != (topRight.isRamp())); + + if (verticalRamp || horizontalRamp) { + final boolean rampBlockedByCliff = ((verticalRamp + && this.corners[i][j + (facingDown ? -1 : 1)].cliff) + || (horizontalRamp && this.corners[i + (facingLeft ? -1 : 1)][j].cliff)); + final int topLeftHeight = topLeft.getLayerHeight() - base; + final int topRightHeight = topRight.getLayerHeight() - base; + final int bottomRightHeight = bottomRight.getLayerHeight() - base; + final int bottomLeftHeight = bottomLeft.getLayerHeight() - base; + boolean invalidRamp = false; + if (DISALLOW_HEIGHT_3_RAMPS) { + if (rampBlockedByCliff) { + invalidRamp = true; + } + else if (topLeftHeight > 1) { + invalidRamp = true; + topLeft.setRamp(0); + } + else if (topRightHeight > 1) { + invalidRamp = true; + topRight.setRamp(0); + } + else if (bottomRightHeight > 1) { + invalidRamp = true; + bottomRight.setRamp(0); + } + else if (bottomLeftHeight > 1) { + invalidRamp = true; + bottomLeft.setRamp(0); + } + } + if (!invalidRamp) { + String fileName = "" + getRampLetter(topLeftHeight, topLeft.isRamp()) + + getRampLetter(topRightHeight, topRight.isRamp()) + + getRampLetter(bottomRightHeight, bottomRight.isRamp()) + + getRampLetter(bottomLeftHeight, bottomLeft.isRamp()); + + final String rampModelDir = this.cliffTextures.get(bottomLeftCliffTex).rampModelDir; + fileName = "Doodads\\Terrain\\" + rampModelDir + "\\" + rampModelDir + fileName + + "0.mdx"; + + if (this.dataSource.has(fileName)) { + if (!this.pathToCliff.containsKey(fileName)) { + this.cliffMeshes.add(new CliffMesh(fileName, this.dataSource, Gdx.gl30)); + this.pathToCliff.put(fileName, this.cliffMeshes.size() - 1); + } + + for (int ji = this.cliffs.size(); ji-- > 0;) { + final IVec3 pos = this.cliffs.get(ji); + if ((pos.x == (i + ((horizontalRamp ? 1 : 0) * (facingLeft ? -1 : 0)))) + && (pos.y == (j - ((verticalRamp ? 1 : 0) * (facingDown ? 1 : 0))))) { + this.cliffs.remove(ji); + break; + } + } + + this.cliffs.add(new IVec3((i + ((horizontalRamp ? 1 : 0) * (facingLeft ? -1 : 0))), + (j - ((verticalRamp ? 1 : 0) * (facingDown ? 1 : 0))), + this.pathToCliff.get(fileName))); + bottomLeft.romp = true; + bottomLeft.setCliffTexture(bottomLeftCliffTex); + bottomRight.setCliffTexture(bottomLeftCliffTex); + topLeft.setCliffTexture(bottomLeftCliffTex); + topRight.setCliffTexture(bottomLeftCliffTex); + this.corners[i + ((facingLeft ? -1 : 1) * (horizontalRamp ? 1 : 0))][j + + ((facingDown ? -1 : 1) * (verticalRamp ? 1 : 0))] + .setCliffTexture(bottomLeftCliffTex); + + this.corners[i + ((facingLeft ? -1 : 1) * (horizontalRamp ? 1 : 0))][j + + ((facingDown ? -1 : 1) * (verticalRamp ? 1 : 0))].romp = true; + + continue; + } + } + } + } + + if (isCornerRampEntrance(i, j)) { + continue; + } + + // Ramps move 1 right/down in some cases and thus their area is one bigger to + // the top and left. + if (!area.contains(i, j)) { + continue; + } + + // Cliff model path + + String fileName = "" + (char) (('A' + topLeft.getLayerHeight()) - base) + + (char) (('A' + topRight.getLayerHeight()) - base) + + (char) (('A' + bottomRight.getLayerHeight()) - base) + + (char) (('A' + bottomLeft.getLayerHeight()) - base); + + if ("AAAA".equals(fileName)) { + continue; + } + + // Clamp to within max variations + + fileName = this.cliffTextures.get(bottomLeftCliffTex).cliffModelDir + fileName + + Variations.getCliffVariation(this.cliffTextures.get(bottomLeftCliffTex).cliffModelDir, + fileName, bottomLeft.getCliffVariation()); + if (!this.pathToCliff.containsKey(fileName)) { + throw new IllegalArgumentException("No such pathToCliff entry: " + fileName); + } + this.cliffs.add(new IVec3(i, j, this.pathToCliff.get(fileName))); + } + } + } + for (int i = (int) rampArea.getX(); i < xLimit; i++) { + final int yLimit = (int) ((rampArea.getY() + rampArea.getHeight()) - 1); + for (int j = (int) rampArea.getY(); j < yLimit; j++) { + final RenderCorner bottomLeft = this.corners[i][j]; + if (bottomLeft.isRamp() && !bottomLeft.romp) { + bottomLeft.hideCliff = true; + } + } + } + + } + + public void logRomp(final int x, final int y) { + System.out.println("romp: " + this.corners[x][y].romp); + System.out.println("ramp: " + this.corners[x][y].isRamp()); + System.out.println("cliff: " + this.corners[x][y].cliff); + } + + private void updateGroundTextures(final Rectangle area) { + final Rectangle adjusted = new Rectangle(area.x - 1, area.y - 1, area.width + 2, area.height + 2); + final Rectangle updateArea = new Rectangle(); + Intersector.intersectRectangles(new Rectangle(0, 0, this.columns - 1, this.rows - 1), adjusted, updateArea); + + for (int j = (int) (updateArea.getY()); j <= (int) ((updateArea.getY() + updateArea.getHeight()) - 1); j++) { + for (int i = (int) (updateArea.getX()); i <= (int) ((updateArea.getX() + updateArea.getWidth()) - 1); i++) { + getTextureVariations(i, j, this.groundTextureList, ((j * (this.columns - 1)) + i) * 4); + + if (this.corners[i][j].cliff || this.corners[i][j].romp) { + if (isCornerRampEntrance(i, j)) { + continue; + } + this.groundTextureList[(((j * (this.columns - 1)) + i) * 4) + 3] |= 0b1000000000000000; + } + } + } + + uploadGroundTexture(); + } + + public void removeTerrainCell(final int i, final int j) { + this.groundTextureList[(((j * (this.columns - 1)) + i) * 4) + 3] |= 0b1000000000000000; + this.corners[i][j].hideCliff = true; + uploadGroundTexture(); + try { + updateCliffMeshes(new Rectangle(i - 1, j - 1, 1, 1)); // TODO does this work? + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + + public void removeTerrainCellWithoutFlush(final int i, final int j) { + this.groundTextureList[(((j * (this.columns - 1)) + i) * 4) + 3] |= 0b1000000000000000; + this.corners[i][j].hideCliff = true; + } + + public void flushRemovedTerrainCells() { + uploadGroundTexture(); + try { + updateCliffMeshes(new Rectangle(0, 0, this.columns - 1, this.rows - 1)); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + + private void uploadGroundTexture() { + if (this.groundTextureData != -1) { + Gdx.gl30.glBindTexture(GL30.GL_TEXTURE_2D, this.groundTextureData); + Gdx.gl30.glTexSubImage2D(GL30.GL_TEXTURE_2D, 0, 0, 0, this.columns - 1, this.rows - 1, GL30.GL_RGBA_INTEGER, + GL30.GL_UNSIGNED_SHORT, RenderMathUtils.wrapShort(this.groundTextureList)); + } + } + + /// The 4 ground textures of the tilepoint. First 5 bits are which texture array + /// to use and the next 5 bits are which subtexture to use + private void getTextureVariations(final int x, final int y, final short[] out, final int outStartOffset) { + final int bottomLeft = realTileTexture(x, y); + final int bottomRight = realTileTexture(x + 1, y); + final int topLeft = realTileTexture(x, y + 1); + final int topRight = realTileTexture(x + 1, y + 1); + + final TreeSet set = new TreeSet<>(); + set.add(bottomLeft); + set.add(bottomRight); + set.add(topLeft); + set.add(topRight); + Arrays.fill(out, outStartOffset, outStartOffset + 4, (short) 17); + int component = outStartOffset + 1; + + final Iterator iterator = set.iterator(); + iterator.hasNext(); + final short firstValue = iterator.next().shortValue(); + out[outStartOffset] = (short) (firstValue + + (getVariation(firstValue, this.corners[x][y].getGroundVariation()) << 5)); + + int index; + while (iterator.hasNext()) { + index = 0; + final int texture = iterator.next().intValue(); + index |= (bottomRight == texture ? 1 : 0) << 0; + index |= (bottomLeft == texture ? 1 : 0) << 1; + index |= (topRight == texture ? 1 : 0) << 2; + index |= (topLeft == texture ? 1 : 0) << 3; + + out[component++] = (short) (texture + (index << 5)); + } + } + + private int realTileTexture(final int x, final int y) { + ILoop: for (int i = -1; i < 1; i++) { + for (int j = -1; j < 1; j++) { + if (((x + i) >= 0) && ((x + i) < this.columns) && ((y + j) >= 0) && ((y + j) < this.rows)) { + if (this.corners[x + i][y + j].cliff) { + if (((x + i) < (this.columns - 1)) && ((y + j) < (this.rows - 1))) { + final RenderCorner bottomLeft = this.corners[x + i][y + j]; + final RenderCorner bottomRight = this.corners[x + i + 1][y + j]; + final RenderCorner topLeft = this.corners[x + i][y + j + 1]; + final RenderCorner topRight = this.corners[x + i + 1][y + j + 1]; + + if ((bottomLeft.isRamp()) && (topLeft.isRamp()) && (bottomRight.isRamp()) + && (topRight.isRamp()) && (!bottomLeft.romp) && (!bottomRight.romp) + && (!topLeft.romp) && (!topRight.romp)) { + break ILoop; + } + } + } + if (this.corners[x + i][y + j].romp || this.corners[x + i][y + j].cliff) { + int texture = this.corners[x + i][y + j].getCliffTexture(); + // Number 15 seems to be something + if (texture == 15) { + texture -= 14; + } + + return this.cliffToGroundTexture.get(texture); + } + } + } + } + + if (this.corners[x][y].getBlight() != 0) { + return this.blightTextureIndex; + } + + return this.corners[x][y].getGroundTexture(); + } + + private boolean isCornerRampEntrance(final int x, final int y) { + if ((x == this.columns) || (y == this.rows)) { + return false; + } + + final RenderCorner bottomLeft = this.corners[x][y]; + final RenderCorner bottomRight = this.corners[x + 1][y]; + final RenderCorner topLeft = this.corners[x][y + 1]; + final RenderCorner topRight = this.corners[x + 1][y + 1]; + + return (bottomLeft.isRamp()) && (topLeft.isRamp()) && (bottomRight.isRamp()) && (topRight.isRamp()) + && !((bottomLeft.getLayerHeight() == topRight.getLayerHeight()) + && (topLeft.getLayerHeight() == bottomRight.getLayerHeight())); + } + + private static void loadWaterColor(final float[] out, final String prefix, final Element waterInfo) { + for (int i = 0; i < colorTags.length; i++) { + final String colorTag = colorTags[i]; + out[i] = waterInfo == null ? 0.0f : waterInfo.getFieldFloatValue(prefix + "_" + colorTag) / 255f; + } + } + + public short getVariation(final int groundTexture, final int variation) { + final GroundTexture texture = this.groundTextures.get(groundTexture); + + // Extended ? + if (texture.extended) { + if (variation <= 15) { + return (short) (16 + variation); + } + else if (variation == 16) { + return 15; + } + else { + return 0; + } + } + else { + if (variation == 0) { + return 0; + } + else { + return 15; + } + } + } + + public void update(final float deltaTime) { + this.waterIndex += this.waterIncreasePerFrame * deltaTime; + + if (this.waterIndex >= this.waterTextureCount) { + this.waterIndex = 0; + } + } + + public void renderGround(final DynamicShadowManager dynamicShadowManager) { + // Render tiles + + this.webGL.useShaderProgram(this.groundShader); + + final GL30 gl = Gdx.gl30; + gl.glEnable(GL20.GL_CULL_FACE); + gl.glDisable(GL30.GL_BLEND); + gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA); + gl.glEnable(GL20.GL_DEPTH_TEST); + gl.glDepthMask(true); + + gl.glUniformMatrix4fv(this.groundShader.getUniformLocation("MVP"), 1, false, + this.camera.viewProjectionMatrix.val, 0); + gl.glUniform1i(this.groundShader.getUniformLocation("show_pathing_map"), this.viewer.renderPathing); + gl.glUniform1i(this.groundShader.getUniformLocation("show_lighting"), this.viewer.renderLighting); + gl.glUniform1i(this.groundShader.getUniformLocation("height_texture"), 0); + gl.glUniform1i(this.groundShader.getUniformLocation("height_cliff_texture"), 1); + gl.glUniform1i(this.groundShader.getUniformLocation("terrain_texture_list"), 2); + gl.glUniform1i(this.groundShader.getUniformLocation("shadowMap"), 20); + gl.glUniform1f(this.groundShader.getUniformLocation("centerOffsetX"), this.centerOffset[0]); + gl.glUniform1f(this.groundShader.getUniformLocation("centerOffsetY"), this.centerOffset[1]); + + final W3xSceneLightManager lightManager = (W3xSceneLightManager) this.viewer.worldScene.getLightManager(); + final DataTexture unitLightsTexture = lightManager.getTerrainLightsTexture(); + + unitLightsTexture.bind(21); + gl.glUniform1i(this.groundShader.getUniformLocation("lightTexture"), 21); + gl.glUniform1f(this.groundShader.getUniformLocation("lightCount"), lightManager.getTerrainLightCount()); + gl.glUniform1f(this.groundShader.getUniformLocation("lightTextureHeight"), unitLightsTexture.getHeight()); + + gl.glUniformMatrix4fv(this.groundShader.getUniformLocation("DepthBiasMVP"), 1, false, + dynamicShadowManager.getDepthBiasMVP().val, 0); + + gl.glUniform1i(this.groundShader.getUniformLocation("cliff_textures"), 0); + gl.glActiveTexture(GL30.GL_TEXTURE0); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.groundHeight); + + gl.glActiveTexture(GL30.GL_TEXTURE1); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.groundCornerHeight); + + gl.glUniform1i(this.groundShader.getUniformLocation("pathing_map_static"), 2); + gl.glActiveTexture(GL30.GL_TEXTURE2); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.groundTextureData); + + gl.glUniform1i(this.groundShader.getUniformLocation("sample0"), 3); + gl.glUniform1i(this.groundShader.getUniformLocation("sample1"), 4); + gl.glUniform1i(this.groundShader.getUniformLocation("sample2"), 5); + gl.glUniform1i(this.groundShader.getUniformLocation("sample3"), 6); + gl.glUniform1i(this.groundShader.getUniformLocation("sample4"), 7); + gl.glUniform1i(this.groundShader.getUniformLocation("sample5"), 8); + gl.glUniform1i(this.groundShader.getUniformLocation("sample6"), 9); + gl.glUniform1i(this.groundShader.getUniformLocation("sample7"), 10); + gl.glUniform1i(this.groundShader.getUniformLocation("sample8"), 11); + gl.glUniform1i(this.groundShader.getUniformLocation("sample9"), 12); + gl.glUniform1i(this.groundShader.getUniformLocation("sample10"), 13); + gl.glUniform1i(this.groundShader.getUniformLocation("sample11"), 14); + gl.glUniform1i(this.groundShader.getUniformLocation("sample12"), 15); + gl.glUniform1i(this.groundShader.getUniformLocation("sample13"), 16); + gl.glUniform1i(this.groundShader.getUniformLocation("sample14"), 17); + gl.glUniform1i(this.groundShader.getUniformLocation("sample15"), 18); + gl.glUniform1i(this.groundShader.getUniformLocation("sample16"), 19); + gl.glUniform1i(this.groundShader.getUniformLocation("shadowMap"), 20); + for (int i = 0; i < this.groundTextures.size(); i++) { + gl.glActiveTexture(GL30.GL_TEXTURE3 + i); + gl.glBindTexture(GL30.GL_TEXTURE_2D_ARRAY, this.groundTextures.get(i).id); + } + +// gl.glActiveTexture(GL30.GL_TEXTURE20, /*pathingMap.getTextureStatic()*/); +// gl.glActiveTexture(GL30.GL_TEXTURE21, /*pathingMap.getTextureDynamic()*/); + + gl.glActiveTexture(GL30.GL_TEXTURE20); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.shadowMap); + +// gl.glEnableVertexAttribArray(0); + gl.glBindBuffer(GL30.GL_ARRAY_BUFFER, Shapes.INSTANCE.vertexBuffer); + gl.glVertexAttribPointer(this.groundShader.getAttributeLocation("vPosition"), 2, GL30.GL_FLOAT, false, 0, 0); + + gl.glBindBuffer(GL30.GL_ELEMENT_ARRAY_BUFFER, Shapes.INSTANCE.indexBuffer); + if (WIREFRAME_TERRAIN) { + Extensions.wireframeExtension.glPolygonMode(GL20.GL_FRONT_AND_BACK, Extensions.GL_LINE); + } + gl.glDrawElementsInstanced(GL30.GL_TRIANGLES, Shapes.INSTANCE.quadIndices.length * 3, GL30.GL_UNSIGNED_INT, 0, + (this.columns - 1) * (this.rows - 1)); + if (WIREFRAME_TERRAIN) { + Extensions.wireframeExtension.glPolygonMode(GL20.GL_FRONT_AND_BACK, Extensions.GL_FILL); + } + +// gl.glDisableVertexAttribArray(0); + + gl.glEnable(GL30.GL_BLEND); + + } + + public void renderUberSplats(final boolean onTopLayer) { + final GL30 gl = Gdx.gl30; + final WebGL webGL = this.webGL; + final ShaderProgram shader = this.uberSplatShader; + + gl.glDepthMask(false); + gl.glEnable(GL30.GL_BLEND); + gl.glBlendFunc(GL30.GL_SRC_ALPHA, GL30.GL_ONE_MINUS_SRC_ALPHA); + gl.glBlendEquation(GL30.GL_FUNC_ADD); + + webGL.useShaderProgram(this.uberSplatShader); + + shader.setUniformMatrix("u_mvp", this.camera.viewProjectionMatrix); + shader.setUniformi("u_heightMap", 0); + sizeHeap[0] = this.columns - 1; + sizeHeap[1] = this.rows - 1; + shader.setUniform2fv("u_size", sizeHeap, 0, 2); + sizeHeap[0] = 1 / (float) this.columns; + sizeHeap[1] = 1 / (float) this.rows; + shader.setUniform2fv("u_pixel", sizeHeap, 0, 2); + shader.setUniform2fv("u_centerOffset", this.centerOffset, 0, 2); + shader.setUniformi("u_texture", 1); + shader.setUniformi("u_shadowMap", 2); + + gl.glActiveTexture(GL30.GL_TEXTURE0); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.groundCornerHeightLinear); + + gl.glActiveTexture(GL30.GL_TEXTURE2); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.shadowMap); + + final W3xSceneLightManager lightManager = (W3xSceneLightManager) this.viewer.worldScene.getLightManager(); + final DataTexture terrainLightsTexture = lightManager.getTerrainLightsTexture(); + + terrainLightsTexture.bind(21); + gl.glUniform1i(shader.getUniformLocation("u_lightTexture"), 21); + gl.glUniform1f(shader.getUniformLocation("u_lightCount"), lightManager.getTerrainLightCount()); + gl.glUniform1f(shader.getUniformLocation("u_lightTextureHeight"), terrainLightsTexture.getHeight()); + + // Render the cliffs + for (final SplatModel splat : this.uberSplatModelsList) { + if (splat.isHighPriority() == onTopLayer) { + splat.render(gl, shader); + } + } + } + + public void renderWater() { + // Render water + this.webGL.useShaderProgram(this.waterShader); + + final GL30 gl = Gdx.gl30; + gl.glDepthMask(false); + gl.glDisable(GL30.GL_CULL_FACE); + gl.glEnable(GL30.GL_BLEND); + gl.glBlendFunc(GL30.GL_SRC_ALPHA, GL30.GL_ONE_MINUS_SRC_ALPHA); + + this.waterShader.setUniformMatrix4fv("MVP", this.camera.viewProjectionMatrix.val, 0, 16); + this.waterShader.setUniform4fv("shallow_color_min", this.minShallowColor, 0, 4); + this.waterShader.setUniform4fv("shallow_color_max", this.maxShallowColor, 0, 4); + this.waterShader.setUniform4fv("deep_color_min", this.minDeepColor, 0, 4); + this.waterShader.setUniform4fv("deep_color_max", this.maxDeepColor, 0, 4); + this.waterShader.setUniformf("water_offset", this.waterHeightOffset); + this.waterShader.setUniformi("current_texture", (int) this.waterIndex); + this.waterShader.setUniformf("centerOffsetX", this.centerOffset[0]); + this.waterShader.setUniformf("centerOffsetY", this.centerOffset[1]); + this.waterShader.setUniform4fv("mapBounds", this.shaderMapBounds, 0, 4); + + final W3xSceneLightManager lightManager = (W3xSceneLightManager) this.viewer.worldScene.getLightManager(); + final DataTexture terrainLightsTexture = lightManager.getTerrainLightsTexture(); + + terrainLightsTexture.bind(3); + this.waterShader.setUniformi("lightTexture", 3); + this.waterShader.setUniformf("lightCount", lightManager.getTerrainLightCount()); + this.waterShader.setUniformf("lightTextureHeight", terrainLightsTexture.getHeight()); + + this.waterShader.setUniformi("water_height_texture", 0); + this.waterShader.setUniformi("ground_height_texture", 1); + this.waterShader.setUniformi("water_exists_texture", 2); + this.waterShader.setUniformi("water_textures", 4); + gl.glActiveTexture(GL30.GL_TEXTURE0); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.waterHeight); + gl.glActiveTexture(GL30.GL_TEXTURE1); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.groundCornerHeight); + gl.glActiveTexture(GL30.GL_TEXTURE2); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.waterExists); + gl.glActiveTexture(GL30.GL_TEXTURE4); + gl.glBindTexture(GL30.GL_TEXTURE_2D_ARRAY, this.waterTextureArray); + + gl.glBindBuffer(GL30.GL_ARRAY_BUFFER, Shapes.INSTANCE.vertexBuffer); + gl.glVertexAttribPointer(this.waterShader.getAttributeLocation("vPosition"), 2, GL30.GL_FLOAT, false, 0, 0); + + gl.glBindBuffer(GL30.GL_ELEMENT_ARRAY_BUFFER, Shapes.INSTANCE.indexBuffer); + gl.glDrawElementsInstanced(GL30.GL_TRIANGLES, Shapes.INSTANCE.quadIndices.length * 3, GL30.GL_UNSIGNED_INT, 0, + (this.columns - 1) * (this.rows - 1)); + + gl.glEnable(GL30.GL_BLEND); + } + + public void renderCliffs() { + + // Render cliffs + for (final IVec3 i : this.cliffs) { + final RenderCorner bottomLeft = this.corners[i.x][i.y]; + final RenderCorner bottomRight = this.corners[i.x + 1][i.y]; + final RenderCorner topLeft = this.corners[i.x][i.y + 1]; + final RenderCorner topRight = this.corners[i.x + 1][i.y + 1]; + + final float min = Math.min(Math.min(bottomLeft.getLayerHeight() - 2, bottomRight.getLayerHeight() - 2), + Math.min(topLeft.getLayerHeight() - 2, topRight.getLayerHeight() - 2)); + + fourComponentHeap[0] = i.x; + fourComponentHeap[1] = i.y; + fourComponentHeap[2] = min; + fourComponentHeap[3] = bottomLeft.getCliffTexture(); + this.cliffMeshes.get(i.z).renderQueue(fourComponentHeap); + } + + this.webGL.useShaderProgram(this.cliffShader); + + final GL30 gl = Gdx.gl30; + gl.glDisable(GL30.GL_BLEND); + + // WC3 models are 128x too large + tempMatrix.set(this.camera.viewProjectionMatrix); + gl.glUniformMatrix4fv(this.cliffShader.getUniformLocation("MVP"), 1, false, tempMatrix.val, 0); + gl.glUniform1i(this.cliffShader.getUniformLocation("show_lighting"), this.viewer.renderLighting); + + final W3xSceneLightManager lightManager = (W3xSceneLightManager) this.viewer.worldScene.getLightManager(); + final DataTexture unitLightsTexture = lightManager.getTerrainLightsTexture(); + + unitLightsTexture.bind(21); + gl.glUniform1i(this.cliffShader.getUniformLocation("lightTexture"), 21); + gl.glUniform1f(this.cliffShader.getUniformLocation("lightCount"), lightManager.getTerrainLightCount()); + gl.glUniform1f(this.cliffShader.getUniformLocation("lightTextureHeight"), unitLightsTexture.getHeight()); + + this.cliffShader.setUniformi("shadowMap", 2); + gl.glActiveTexture(GL30.GL_TEXTURE2); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.shadowMap); + + this.cliffShader.setUniformi("cliff_textures", 0); + gl.glActiveTexture(GL30.GL_TEXTURE0); + gl.glBindTexture(GL30.GL_TEXTURE_2D_ARRAY, this.cliffTextureArray); + this.cliffShader.setUniformi("height_texture", 1); + gl.glActiveTexture(GL30.GL_TEXTURE1); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.groundHeight); +// gl.glActiveTexture(GL30.GL_TEXTURE2); + for (final CliffMesh i : this.cliffMeshes) { + gl.glUniform1f(this.cliffShader.getUniformLocation("centerOffsetX"), this.centerOffset[0]); + gl.glUniform1f(this.cliffShader.getUniformLocation("centerOffsetY"), this.centerOffset[1]); + i.render(this.cliffShader); + } + } + + public BuildingShadow addShadow(final String file, final float shadowX, final float shadowY) { + if (!this.shadows.containsKey(file)) { + final String path = "ReplaceableTextures\\Shadows\\" + file + ".blp"; + this.shadows.put(file, new ArrayList<>()); + this.shadowTextures.put(file, (Texture) this.viewer.load(path, PathSolver.DEFAULT, null)); + } + final List shadowList = this.shadows.get(file); + final float[] shadowPositionArray = new float[] { shadowX, shadowY }; + shadowList.add(shadowPositionArray); + if (this.initShadowsFinished) { + final Texture texture = this.shadowTextures.get(file); + + final int columns = (this.columns - 1) * 4; + final int rows = (this.rows - 1) * 4; + blitShadowData(columns, rows, shadowX, shadowY, texture); + final GL30 gl = Gdx.gl30; + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.shadowMap); + gl.glTexImage2D(GL30.GL_TEXTURE_2D, 0, GL30.GL_R8, columns, rows, 0, GL30.GL_RED, GL30.GL_UNSIGNED_BYTE, + RenderMathUtils.wrap(this.shadowData)); + } + return new BuildingShadow() { + @Override + public void remove() { + shadowList.remove(shadowPositionArray); + reloadShadowDataToGPU(); + } + + @Override + public void move(final float x, final float y) { + shadowPositionArray[0] = x; + shadowPositionArray[1] = y; + reloadShadowDataToGPU(); + } + }; + } + + public void blitShadowData(final int columns, final int rows, final float shadowX, final float shadowY, + final Texture texture) { + final int width = texture.getWidth(); + final int height = texture.getHeight(); + final int ox = (int) Math.round(width * 0.3); + final int oy = (int) Math.round(height * 0.7); + blitShadowDataLocation(columns, rows, (RawOpenGLTextureResource) texture, width, height, ox, oy, + this.centerOffset, shadowX, shadowY, this.shadowData); + } + + public void initShadows() throws IOException { + final GL30 gl = Gdx.gl30; + final float[] centerOffset = this.centerOffset; + final int columns = (this.columns - 1) * 4; + final int rows = (this.rows - 1) * 4; + + final int shadowSize = columns * rows; + this.staticShadowData = new byte[columns * rows]; + this.shadowData = new byte[columns * rows]; + if (this.viewer.mapMpq.has("war3map.shd")) { + final byte[] buffer; + + try (final InputStream shadowSource = this.viewer.mapMpq.getResourceAsStream("war3map.shd")) { + buffer = IOUtils.toByteArray(shadowSource); + } + + for (int i = 0; i < shadowSize; i++) { + this.staticShadowData[i] = (byte) ((buffer[i] & 0xFF) / 2f); + } + } + + final byte outsideArea = (byte) 204; + final int x0 = this.mapBounds[0] * 4, x1 = (this.mapSize[0] - this.mapBounds[1] - 1) * 4, + y0 = this.mapBounds[2] * 4, y1 = (this.mapSize[1] - this.mapBounds[3] - 1) * 4; + for (int y = y0; y < y1; ++y) { + for (int x = x0; x < x1; ++x) { + final RenderCorner c = this.corners[x >> 2][y >> 2]; + if (c.getBoundary() != 0) { + this.staticShadowData[(y * columns) + x] = outsideArea; + } + } + } + for (int y = 0; y < rows; ++y) { + for (int x = 0; x < x0; ++x) { + this.staticShadowData[(y * columns) + x] = outsideArea; + } + for (int x = x1; x < columns; ++x) { + this.staticShadowData[(y * columns) + x] = outsideArea; + } + } + for (int x = x0; x < x1; ++x) { + for (int y = 0; y < y0; ++y) { + this.staticShadowData[(y * columns) + x] = outsideArea; + } + for (int y = y1; y < rows; ++y) { + this.staticShadowData[(y * columns) + x] = outsideArea; + } + } + reloadShadowData(centerOffset, columns, rows); + + this.shadowMap = gl.glGenTexture(); + gl.glBindTexture(GL30.GL_TEXTURE_2D, this.shadowMap); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_MAG_FILTER, GL30.GL_LINEAR); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_MIN_FILTER, GL30.GL_LINEAR); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_WRAP_S, GL30.GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL30.GL_TEXTURE_2D, GL30.GL_TEXTURE_WRAP_T, GL30.GL_CLAMP_TO_EDGE); + gl.glTexImage2D(GL30.GL_TEXTURE_2D, 0, GL30.GL_R8, columns, rows, 0, GL30.GL_RED, GL30.GL_UNSIGNED_BYTE, + RenderMathUtils.wrap(this.shadowData)); + this.initShadowsFinished = true; + } + + private void reloadShadowData(final float[] centerOffset, final int columns, final int rows) { + System.arraycopy(this.staticShadowData, 0, this.shadowData, 0, this.staticShadowData.length); + for (final Map.Entry fileAndTexture : this.shadowTextures.entrySet()) { + final String file = fileAndTexture.getKey(); + final Texture texture = fileAndTexture.getValue(); + + final int width = texture.getWidth(); + final int height = texture.getHeight(); + final int ox = (int) Math.round(width * 0.3); + final int oy = (int) Math.round(height * 0.7); + for (final float[] location : this.shadows.get(file)) { + blitShadowDataLocation(columns, rows, (RawOpenGLTextureResource) texture, width, height, ox, oy, + centerOffset, location[0], location[1], this.shadowData); + } + } + } + + public void blitShadowDataLocation(final int columns, final int rows, final RawOpenGLTextureResource texture, + final int width, final int height, final int x01, final int y01, final float[] centerOffset, final float v, + final float v2, final byte[] shadowData) { + final int x0 = (int) Math.floor((v - centerOffset[0]) / 32.0) - x01; + final int y0 = (int) Math.floor((v2 - centerOffset[1]) / 32.0) + y01; + for (int y = 0; y < height; ++y) { + if (((y0 - y) < 0) || ((y0 - y) >= rows)) { + continue; + } + for (int x = 0; x < width; ++x) { + if (((x0 + x) < 0) || ((x0 + x) >= columns)) { + continue; + } + if (texture.getData().get((((y * width) + x) * 4) + 3) != 0) { + shadowData[((y0 - y) * columns) + x0 + x] = (byte) 128; + } + } + } + } + +// public Vector3 groundNormal(final Vector3 out, int x, int y) { +// final float[] centerOffset = this.centerOffset; +// final int[] mapSize = this.mapSize; +// +// x = (int) ((x - centerOffset[0]) / 128); +// y = (int) ((y - centerOffset[1]) / 128); +// +// final int cellX = x; +// final int cellY = y; +// +// // See if this coordinate is in the map +// +// if ((cellX >= 0) && (cellX < (mapSize[0] - 1)) && (cellY >= 0) && (cellY < (mapSize[1] - 1))) { +// // See http://gamedev.stackexchange.com/a/24574 +// final Corner[][] corners = this.corners; +// final float bottomLeft = corners[cellY][cellX].getGroundHeight(); +// final float bottomRight = corners[cellY][cellX + 1].getGroundHeight(); +// final float topLeft = corners[cellY + 1][cellX].getGroundHeight(); +// final float topRight = corners[cellY + 1][cellX + 1].getGroundHeight(); +// final int sqX = x - cellX; +// final int sqY = y - cellY; +// +// if ((sqX + sqY) < 1) { +// normalHeap1.set(1, 0, bottomRight - bottomLeft); +// normalHeap2.set(0, 1, topLeft - bottomLeft); +// } +// else { +// normalHeap1.set(-1, 0, topRight - topLeft); +// normalHeap2.set(0, 1, topRight - bottomRight); +// } +// +// out.set(normalHeap1.crs(normalHeap2)).nor(); +// } +// else { +// out.set(0, 0, 1); +// } +// +// return out; +// } + + private final WaveBuilder waveBuilder; + public PathingGrid pathingGrid; + private final Rectangle shaderMapBoundsRectangle; + private final Rectangle entireMapRectangle; + + private static final class UnloadedTexture { + private final int width; + private final int height; + private final Buffer data; + private final String cliffModelDir; + private final String rampModelDir; + + public UnloadedTexture(final int width, final int height, final Buffer data, final String cliffModelDir, + final String rampModelDir) { + this.width = width; + this.height = height; + this.data = data; + this.cliffModelDir = cliffModelDir; + this.rampModelDir = rampModelDir; + } + + } + + public float getGroundHeight(final float x, final float y) { + final float userCellSpaceX = (x - this.centerOffset[0]) / 128.0f; + final float userCellSpaceY = (y - this.centerOffset[1]) / 128.0f; + final int cellX = (int) userCellSpaceX; + final int cellY = (int) userCellSpaceY; + + if ((cellX >= 0) && (cellX < (this.mapSize[0] - 1)) && (cellY >= 0) && (cellY < (this.mapSize[1] - 1))) { + final float bottomLeft = this.groundCornerHeights[(cellY * this.columns) + cellX]; + final float bottomRight = this.groundCornerHeights[(cellY * this.columns) + cellX + 1]; + final float topLeft = this.groundCornerHeights[((cellY + 1) * this.columns) + cellX]; + final float topRight = this.groundCornerHeights[((cellY + 1) * this.columns) + cellX + 1]; + final float sqX = userCellSpaceX - cellX; + final float sqY = userCellSpaceY - cellY; + float height; + + if ((sqX + sqY) < 1) { + height = bottomLeft + ((bottomRight - bottomLeft) * sqX) + ((topLeft - bottomLeft) * sqY); + } + else { + height = topRight + ((bottomRight - topRight) * (1 - sqY)) + ((topLeft - topRight) * (1 - sqX)); + } + + return height * 128.0f; + } + + return 0; + } + + public float getWaterHeight(final float x, final float y) { + final float userCellSpaceX = (x - this.centerOffset[0]) / 128.0f; + final float userCellSpaceY = (y - this.centerOffset[1]) / 128.0f; + final int cellX = (int) userCellSpaceX; + final int cellY = (int) userCellSpaceY; + + if ((cellX >= 0) && (cellX < (this.mapSize[0] - 1)) && (cellY >= 0) && (cellY < (this.mapSize[1] - 1))) { + final float bottomLeft = this.waterHeights[(cellY * this.columns) + cellX]; + final float bottomRight = this.waterHeights[(cellY * this.columns) + cellX + 1]; + final float topLeft = this.waterHeights[((cellY + 1) * this.columns) + cellX]; + final float topRight = this.waterHeights[((cellY + 1) * this.columns) + cellX + 1]; + final float sqX = userCellSpaceX - cellX; + final float sqY = userCellSpaceY - cellY; + float height; + + if ((sqX + sqY) < 1) { + height = bottomLeft + ((bottomRight - bottomLeft) * sqX) + ((topLeft - bottomLeft) * sqY); + } + else { + height = topRight + ((bottomRight - topRight) * (1 - sqY)) + ((topLeft - topRight) * (1 - sqX)); + } + + return ((height + this.waterHeightOffset) * 128.0f); + } + + return this.waterHeightOffset * 128.0f; + } + + public static final class Splat { + public List locations = new ArrayList<>(); + public List> unitMapping = new ArrayList<>(); + public float opacity = 1; + } + + public void loadSplats() throws IOException { + for (final Map.Entry entry : this.splats.entrySet()) { + final String path = entry.getKey(); + final Splat splat = entry.getValue(); + + final SplatModel splatModel = new SplatModel(Gdx.gl30, + (Texture) this.viewer.load(path, PathSolver.DEFAULT, null), splat.locations, this.centerOffset, + splat.unitMapping.isEmpty() ? null : splat.unitMapping, false, false, false); + splatModel.color[3] = splat.opacity; + this.addSplatBatchModel(path, splatModel); + } + } + + public void removeSplatBatchModel(final String path) { + this.uberSplatModelsList.remove(this.uberSplatModels.remove(path)); + } + + public void addSplatBatchModel(final String path, final SplatModel model) { + this.uberSplatModels.put(path, model); + this.uberSplatModelsList.add(model); + Collections.sort(this.uberSplatModelsList); + } + + public SplatMover addUberSplat(final String path, final float x, final float y, final float z, final float scale, + final boolean unshaded, final boolean noDepthTest, final boolean highPriority) { + SplatModel splatModel = this.uberSplatModels.get(path); + if (splatModel == null) { + splatModel = new SplatModel(Gdx.gl30, (Texture) this.viewer.load(path, PathSolver.DEFAULT, null), + new ArrayList<>(), this.centerOffset, new ArrayList<>(), unshaded, noDepthTest, highPriority); + this.addSplatBatchModel(path, splatModel); + } + return splatModel.add(x - scale, y - scale, x + scale, y + scale, z, this.centerOffset); + } + + public SplatMover addUnitShadowSplat(final String texture, final float x, final float y, final float x2, + final float y2, final float zDepthUpward, final float opacity) { + SplatModel splatModel = this.uberSplatModels.get(texture); + if (splatModel == null) { + splatModel = new SplatModel(Gdx.gl30, (Texture) this.viewer.load(texture, PathSolver.DEFAULT, null), + new ArrayList<>(), this.centerOffset, new ArrayList<>(), false, false, false); + splatModel.color[3] = opacity; + this.addSplatBatchModel(texture, splatModel); + } + return splatModel.add(x, y, x2, y2, zDepthUpward, this.centerOffset); + } + + public static final class SoftwareGroundMesh { + public final float[] vertices; + public final int[] indices; + + private SoftwareGroundMesh(final float[] groundHeights, final float[] groundCornerHeights, + final float[] centerOffset, final int columns, final int rows) { + this.vertices = new float[(columns - 1) * (rows - 1) * Shapes.INSTANCE.quadVertices.length * 3]; + this.indices = new int[(columns - 1) * (rows - 1) * Shapes.INSTANCE.quadIndices.length * 3]; + for (int y = 0; y < (rows - 1); y++) { + for (int x = 0; x < (columns - 1); x++) { + final int instanceId = (y * (columns - 1)) + x; + for (int vertexId = 0; vertexId < Shapes.INSTANCE.quadVertices.length; vertexId++) { + final float vPositionX = Shapes.INSTANCE.quadVertices[vertexId][0]; + final float vPositionY = Shapes.INSTANCE.quadVertices[vertexId][1]; + final int groundCornerHeightIndex = (int) (((vPositionY + y) * (columns)) + (vPositionX + x)); + final float height = groundCornerHeights[groundCornerHeightIndex]; + this.vertices[(instanceId * 4 * 3) + (vertexId * 3)] = ((vPositionX + x) * 128f) + + centerOffset[0]; + this.vertices[(instanceId * 4 * 3) + (vertexId * 3) + 1] = ((vPositionY + y) * 128f) + + centerOffset[1]; + this.vertices[(instanceId * 4 * 3) + (vertexId * 3) + 2] = height * 128f; + } + for (int triangle = 0; triangle < Shapes.INSTANCE.quadIndices.length; triangle++) { + for (int vertexId = 0; vertexId < Shapes.INSTANCE.quadIndices[triangle].length; vertexId++) { + final int vertexIndex = Shapes.INSTANCE.quadIndices[triangle][vertexId]; + final int indexValue = (vertexIndex + (instanceId * 4)); + if ((indexValue * 3) >= this.vertices.length) { + throw new IllegalStateException(); + } + this.indices[(instanceId * 2 * 3) + (triangle * 3) + vertexId] = indexValue; + } + } + } + } + } + } + + public boolean inPlayableArea(float x, float y) { + x = (x - this.centerOffset[0]) / 128.0f; + y = (y - this.centerOffset[1]) / 128.0f; + if (x < this.mapBounds[0]) { + return false; + } + if (x >= (this.mapSize[0] - this.mapBounds[1] - 1)) { + return false; + } + if (y < this.mapBounds[2]) { + return false; + } + if (y >= (this.mapSize[1] - this.mapBounds[3] - 1)) { + return false; + } // TODO why do we use floor if we can use int cast? + return this.corners[(int) Math.floor(x)][(int) Math.floor(y)].getBoundary() == 0; + } + + public Rectangle getPlayableMapArea() { + return this.shaderMapBoundsRectangle; + } + + public Rectangle getEntireMap() { + return this.entireMapRectangle; + } + + private void reloadShadowDataToGPU() { + final int columns = (Terrain.this.columns - 1) * 4; + final int rows = (Terrain.this.rows - 1) * 4; + reloadShadowData(Terrain.this.centerOffset, columns, rows); + final GL30 gl = Gdx.gl30; + gl.glBindTexture(GL30.GL_TEXTURE_2D, Terrain.this.shadowMap); + gl.glTexImage2D(GL30.GL_TEXTURE_2D, 0, GL30.GL_R8, columns, rows, 0, GL30.GL_RED, GL30.GL_UNSIGNED_BYTE, + RenderMathUtils.wrap(Terrain.this.shadowData)); + } + + private static char getRampLetter(final int layerHeightOffset, final boolean isRamp) { + if (isRamp) { + switch (layerHeightOffset) { + case 0: + return 'L'; + case 1: + return 'H'; + case 2: + return 'X'; + default: + throw new IllegalArgumentException("Invalid ramp"); + } + } + else { + return (char) ('A' + layerHeightOffset); + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/TerrainShaders.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/TerrainShaders.java new file mode 100644 index 0000000..f54e4e7 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/TerrainShaders.java @@ -0,0 +1,380 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.environment; + +import com.etheller.warsmash.viewer5.Shaders; + +/** + * Mostly copied from HiveWE! + */ +public class TerrainShaders { + public static final class Cliffs { + private Cliffs() { + } + + public static final String vert = "#version 330 core\r\n" + // + "\r\n" + // + "in vec3 vPosition;\r\n" + // + "in vec2 vUV;\r\n" + // + "in vec3 vNormal;\r\n" + // + "in vec4 vOffset;\r\n" + // + "\r\n" + // + "uniform mat4 MVP;\r\n" + // + "\r\n" + // + "uniform sampler2D height_texture;\r\n" + // + "uniform sampler2D shadowMap;\r\n" + // + "uniform float centerOffsetX;\r\n" + // + "uniform float centerOffsetY;\r\n" + // + "uniform sampler2D lightTexture;\r\n" + // + "uniform float lightCount;\r\n" + // + "uniform float lightTextureHeight;\r\n" + // + "\r\n" + // + "out vec3 UV;\r\n" + // + "out vec3 Normal;\r\n" + // + "out vec2 pathing_map_uv;\r\n" + // + "out vec3 position;\r\n" + // + "out vec2 v_suv;\r\n" + // + "out vec3 shadeColor;\r\n" + // + "\r\n" + // + "void main() {\r\n" + // + " pathing_map_uv = (vec2(vPosition.y, -vPosition.x) / 128 + vOffset.xy) * 4;\r\n" + // + " \r\n" + // + " ivec2 size = textureSize(height_texture, 0);\r\n" + // + " ivec2 shadowSize = textureSize(shadowMap, 0);\r\n" + // + " v_suv = pathing_map_uv / shadowSize;\r\n" + // + " float value = texture(height_texture, (vOffset.xy + vec2(vPosition.y + 64, -vPosition.x + 64) / 128.0) / vec2(size)).r;\r\n" + + // + "\r\n" + // + " position = (vec3(vPosition.y, -vPosition.x, vPosition.z) + vec3(vOffset.xy, vOffset.z + value) * 128 );\r\n" + + // + " vec4 myposition = vec4(position, 1);\r\n" + // + " myposition.x += centerOffsetX;\r\n" + // + " myposition.y += centerOffsetY;\r\n" + // + " position.x /= (size.x * 128.0);\r\n" + // + " position.y /= (size.y * 128.0);\r\n" + // + " gl_Position = MVP * myposition;\r\n" + // + " UV = vec3(vUV, vOffset.a);\r\n" + // + "\r\n" + // + " ivec2 height_pos = ivec2(vOffset.xy + vec2(vPosition.y, -vPosition.x) / 128);\r\n" + // + " ivec3 off = ivec3(1, 1, 0);\r\n" + // + " float hL = texelFetch(height_texture, height_pos - off.xz, 0).r;\r\n" + // + " float hR = texelFetch(height_texture, height_pos + off.xz, 0).r;\r\n" + // + " float hD = texelFetch(height_texture, height_pos - off.zy, 0).r;\r\n" + // + " float hU = texelFetch(height_texture, height_pos + off.zy, 0).r;\r\n" + // + " bool edgeX = (vPosition.y) == float((int(vPosition.y))/128*128);\r\n" + // + " bool edgeY = (vPosition.x) == float((int(vPosition.x))/128*128);\r\n" + // + " bool edgeZ = (vPosition.z) == float((int(vPosition.z))/128*128);\r\n" + // + " vec3 terrain_normal = vec3(vNormal.y, -vNormal.x, vNormal.z);\r\n" + // + " if(edgeX) {\r\n" + // + " terrain_normal.x = hL - hR;\r\n" + // + " }\r\n" + // + " if(edgeY) {\r\n" + // + " terrain_normal.y = hD - hU;\r\n" + // + " }\r\n" + // + " if(edgeZ) {\r\n" + // + " terrain_normal.z = 2.0;\r\n" + // + " }\r\n" + // + " terrain_normal = normalize(terrain_normal);\r\n" + // + "\r\n" + // + " Normal = terrain_normal;\r\n" + // + Shaders.lightSystem("terrain_normal", "myposition.xyz", "lightTexture", "lightTextureHeight", + "lightCount", true) + + "\r\n" + // + " shadeColor = clamp(lightFactor, 0.0, 1.0);\r\n" + // + "}"; + + public static final String frag = "#version 330 core\r\n" + // + "\r\n" + // + "uniform sampler2DArray cliff_textures;\r\n" + // + "uniform sampler2D shadowMap;\r\n" + // + "\r\n" + // + "uniform bool show_lighting;\r\n" + // + "\r\n" + // + "in vec3 UV;\r\n" + // + "in vec3 Normal;\r\n" + // + "in vec2 pathing_map_uv;\r\n" + // + "in vec3 position;\r\n" + // + "in vec2 v_suv;\r\n" + // + "in vec3 shadeColor;\r\n" + // + "\r\n" + // + "out vec4 color;\r\n" + // + "\r\n" + // + "void main() {\r\n" + // + " color = texture(cliff_textures, UV);\r\n" + // + "\r\n" + // + " float shadow = texture2D(shadowMap, v_suv).r;\r\n" + // + " color.rgb *= (1.0 - shadow);\r\n" + // + " if (show_lighting) {\r\n" + // + " color.rgb *= shadeColor;\r\n" + // + " }\r\n" + // + "\r\n" + // + "}"; + } + + public static final class Terrain { + private Terrain() { + } + + public static final String vert = "#version 330 core\r\n" + // + "\r\n" + // + "in vec2 vPosition;\r\n" + // + "uniform mat4 MVP;\r\n" + // + "uniform mat4 DepthBiasMVP;\r\n" + // + "\r\n" + // + "uniform sampler2D height_texture;\r\n" + // + "uniform sampler2D height_cliff_texture;\r\n" + // + "uniform usampler2D terrain_texture_list;\r\n" + // + "uniform float centerOffsetX;\r\n" + // + "uniform float centerOffsetY;\r\n" + // + "uniform sampler2D lightTexture;\r\n" + // + "uniform float lightCount;\r\n" + // + "uniform float lightTextureHeight;\r\n" + // + "\r\n" + // + "out vec2 UV;\r\n" + // + "flat out uvec4 texture_indices;\r\n" + // + "out vec2 pathing_map_uv;\r\n" + // + "out vec3 position;\r\n" + // + "out vec3 ShadowCoord;\r\n" + // + "out vec2 v_suv;\r\n" + // + "out vec3 shadeColor;\r\n" + // + "\r\n" + // + "void main() { \r\n" + // + " ivec2 size = textureSize(terrain_texture_list, 0);\r\n" + // + " ivec2 pos = ivec2(gl_InstanceID % size.x, gl_InstanceID / size.x);\r\n" + // + "\r\n" + // + " ivec2 height_pos = ivec2(vPosition + pos);\r\n" + // + " vec4 height = texelFetch(height_cliff_texture, height_pos, 0);\r\n" + // + "\r\n" + // + " ivec3 off = ivec3(1, 1, 0);\r\n" + // + " float hL = texelFetch(height_texture, height_pos - off.xz, 0).r;\r\n" + // + " float hR = texelFetch(height_texture, height_pos + off.xz, 0).r;\r\n" + // + " float hD = texelFetch(height_texture, height_pos - off.zy, 0).r;\r\n" + // + " float hU = texelFetch(height_texture, height_pos + off.zy, 0).r;\r\n" + // + " vec3 normal = normalize(vec3(hL - hR, hD - hU, 2.0));\r\n" + // + "\r\n" + // + " UV = vec2(vPosition.x, 1 - vPosition.y);\r\n" + // + " texture_indices = texelFetch(terrain_texture_list, pos, 0);\r\n" + // + " pathing_map_uv = (vPosition + pos) * 4; \r\n" + // + "\r\n" + // + " // Cliff culling\r\n" + // + " vec3 positionWorld = vec3((vPosition.x + pos.x)*128.0 + centerOffsetX, (vPosition.y + pos.y)*128.0 + centerOffsetY, height.r*128.0);\r\n" + + // + " position = positionWorld;\r\n" + // + " gl_Position = ((texture_indices.a & 32768u) == 0u) ? MVP * vec4(position.xyz, 1) : vec4(2.0, 0.0, 0.0, 1.0);\r\n" + + // + " ShadowCoord = (((texture_indices.a & 32768u) == 0u) ? DepthBiasMVP * vec4(position.xyz, 1) : vec4(2.0, 0.0, 0.0, 1.0)).xyz;\r\n" + + // + " v_suv = (vPosition + pos) / size;\r\n" + // + " position.x = (position.x - centerOffsetX) / (size.x * 128.0);\r\n" + // + " position.y = (position.y - centerOffsetY) / (size.y * 128.0);\r\n" + // + Shaders.lightSystem("normal", "positionWorld", "lightTexture", "lightTextureHeight", "lightCount", true) + + "\r\n" + // + " shadeColor = clamp(lightFactor, 0.0, 1.0);\r\n" + // + "}"; + + public static final String frag = "#version 330 core\r\n" + // + "\r\n" + // + "uniform bool show_pathing_map;\r\n" + // + "uniform bool show_lighting;\r\n" + // + "\r\n" + // + "uniform sampler2DArray sample0;\r\n" + // + "uniform sampler2DArray sample1;\r\n" + // + "uniform sampler2DArray sample2;\r\n" + // + "uniform sampler2DArray sample3;\r\n" + // + "uniform sampler2DArray sample4;\r\n" + // + "uniform sampler2DArray sample5;\r\n" + // + "uniform sampler2DArray sample6;\r\n" + // + "uniform sampler2DArray sample7;\r\n" + // + "uniform sampler2DArray sample8;\r\n" + // + "uniform sampler2DArray sample9;\r\n" + // + "uniform sampler2DArray sample10;\r\n" + // + "uniform sampler2DArray sample11;\r\n" + // + "uniform sampler2DArray sample12;\r\n" + // + "uniform sampler2DArray sample13;\r\n" + // + "uniform sampler2DArray sample14;\r\n" + // + "uniform sampler2DArray sample15;\r\n" + // + "uniform sampler2DArray sample16;\r\n" + // + "\r\n" + // +// "layout (binding = 20) uniform usampler2D pathing_map_static;\r\n" + // +// "layout (binding = 21) uniform usampler2D pathing_map_dynamic;\r\n" + // + "uniform sampler2D shadowMap;\r\n" + // + "\r\n" + // + "in vec2 UV;\r\n" + // + "flat in uvec4 texture_indices;\r\n" + // + "in vec2 pathing_map_uv;\r\n" + // + "in vec3 position;\r\n" + // + "in vec3 ShadowCoord;\r\n" + // + "in vec2 v_suv;\r\n" + // + "in vec3 shadeColor;\r\n" + // + "\r\n" + // + "out vec4 color;\r\n" + // +// "layout (location = 1) out vec4 position;\r\n" + // + "\r\n" + // + "vec4 get_fragment(uint id, vec3 uv) {\r\n" + // + " vec2 dx = dFdx(uv.xy);\r\n" + // + " vec2 dy = dFdy(uv.xy);\r\n" + // + "\r\n" + // + " switch(id) {\r\n" + // + " case 0u:\r\n" + // + " return textureGrad(sample0, uv, dx, dy);\r\n" + // + " case 1u:\r\n" + // + " return textureGrad(sample1, uv, dx, dy);\r\n" + // + " case 2u:\r\n" + // + " return textureGrad(sample2, uv, dx, dy);\r\n" + // + " case 3u:\r\n" + // + " return textureGrad(sample3, uv, dx, dy);\r\n" + // + " case 4u:\r\n" + // + " return textureGrad(sample4, uv, dx, dy);\r\n" + // + " case 5u:\r\n" + // + " return textureGrad(sample5, uv, dx, dy);\r\n" + // + " case 6u:\r\n" + // + " return textureGrad(sample6, uv, dx, dy);\r\n" + // + " case 7u:\r\n" + // + " return textureGrad(sample7, uv, dx, dy);\r\n" + // + " case 8u:\r\n" + // + " return textureGrad(sample8, uv, dx, dy);\r\n" + // + " case 9u:\r\n" + // + " return textureGrad(sample9, uv, dx, dy);\r\n" + // + " case 10u:\r\n" + // + " return textureGrad(sample10, uv, dx, dy);\r\n" + // + " case 11u:\r\n" + // + " return textureGrad(sample11, uv, dx, dy);\r\n" + // + " case 12u:\r\n" + // + " return textureGrad(sample12, uv, dx, dy);\r\n" + // + " case 13u:\r\n" + // + " return textureGrad(sample13, uv, dx, dy);\r\n" + // + " case 14u:\r\n" + // + " return textureGrad(sample14, uv, dx, dy);\r\n" + // + " case 15u:\r\n" + // + " return textureGrad(sample15, uv, dx, dy);\r\n" + // + " case 16u:\r\n" + // + " return textureGrad(sample16, uv, dx, dy);\r\n" + // + " case 17u:\r\n" + // + " return vec4(0, 0, 0, 0);\r\n" + // + " }\r\n" + // + "}\r\n" + // + "\r\n" + // + "\r\n" + // + "void main() {\r\n" + // + " color = get_fragment(texture_indices.a & 31u, vec3(UV, texture_indices.a >> 5));\r\n" + // + " color = color * color.a + get_fragment(texture_indices.b & 31u, vec3(UV, texture_indices.b >> 5)) * (1 - color.a);\r\n" + + // + " color = color * color.a + get_fragment(texture_indices.g & 31u, vec3(UV, texture_indices.g >> 5)) * (1 - color.a);\r\n" + + // + " color = color * color.a + get_fragment(texture_indices.r & 31u, vec3(UV, texture_indices.r >> 5)) * (1 - color.a);\r\n" + + // + " float shadow = texture2D(shadowMap, v_suv).r;\r\n" + // +// " float visibility = 1.0;\r\n" + // +// " if ( texture2D(shadowMap, ShadowCoord.xy).z > ShadowCoord.z ) {\r\n" + // +// " visibility = 0.5;\r\n" + // +// " }\r\n" + // + "\r\n" + // + " if (show_lighting) {\r\n" + // + " color = vec4(color.xyz * (1.0 - shadow) * shadeColor, 1.0);\r\n" + // + " } else {\r\n" + // + " color = vec4(color.xyz * (1.0 - shadow), 1.0);\r\n" + // + " }\r\n" + // +// "\r\n" + // +// " if (show_pathing_map) {\r\n" + // +// " uint byte_static = texelFetch(pathing_map_static, ivec2(pathing_map_uv), 0).r;\r\n" + // +// " uint byte_dynamic = texelFetch(pathing_map_dynamic, ivec2(pathing_map_uv), 0).r;\r\n" + // +// " uint final = byte_static.r | byte_dynamic.r;\r\n" + // +// "\r\n" + // +// " vec4 pathing_static_color = vec4((final & 2) >> 1, (final & 4) >> 2, (final & 8) >> 3, 0.25);\r\n" +// + // +// "\r\n" + // +// " color = length(pathing_static_color.rgb) > 0 ? color * 0.75 + pathing_static_color * 0.5 : color;\r\n" +// + // +// " }\r\n" + // + "}"; + } + + public static final class Water { + private Water() { + } + + public static final String vert = "#version 330 core\r\n" + // + "\r\n" + // + "in vec2 vPosition;\r\n" + // + "\r\n" + // + "uniform sampler2D water_height_texture;\r\n" + // + "uniform sampler2D ground_height_texture;\r\n" + // + "uniform sampler2D water_exists_texture;\r\n" + // + "uniform float centerOffsetX;\r\n" + // + "uniform float centerOffsetY;\r\n" + // + "\r\n" + // + "uniform mat4 MVP;\r\n" + // + "uniform vec4 shallow_color_min;\r\n" + // + "uniform vec4 shallow_color_max;\r\n" + // + "uniform vec4 deep_color_min;\r\n" + // + "uniform vec4 deep_color_max;\r\n" + // + "uniform float water_offset;\r\n" + // + "uniform sampler2D lightTexture;\r\n" + // + "uniform float lightCount;\r\n" + // + "uniform float lightTextureHeight;\r\n" + // + "\r\n" + // + "out vec2 UV;\r\n" + // + "out vec4 Color;\r\n" + // + "out vec2 position;\r\n" + // + "out vec3 shadeColor;\r\n" + // + "\r\n" + // + "const float min_depth = 10.f / 128;\r\n" + // + "const float deeplevel = 64.f / 128;\r\n" + // + "const float maxdepth = 72.f / 128;\r\n" + // + "\r\n" + // + "void main() { \r\n" + // + " ivec2 size = textureSize(water_height_texture, 0) - 1;\r\n" + // + " ivec2 pos = ivec2(gl_InstanceID % size.x, gl_InstanceID / size.x);\r\n" + // + " ivec2 height_pos = ivec2(vPosition + pos);\r\n" + // + " float water_height = texelFetch(water_height_texture, height_pos, 0).r + water_offset;\r\n" + // + "\r\n" + // + " bool is_water = texelFetch(water_exists_texture, pos, 0).r > 0\r\n" + // + " || texelFetch(water_exists_texture, pos + ivec2(1, 0), 0).r > 0\r\n" + // + " || texelFetch(water_exists_texture, pos + ivec2(1, 1), 0).r > 0\r\n" + // + " || texelFetch(water_exists_texture, pos + ivec2(0, 1), 0).r > 0;\r\n" + // + "\r\n" + // + " position = vec2((vPosition.x + pos.x)*128.0 + centerOffsetX, (vPosition.y + pos.y)*128.0 + centerOffsetY);\r\n" + + // + " vec4 myposition = vec4(position.xy, water_height*128.0, 1);\r\n" + // + " vec3 Normal = vec3(0,0,1);\r\n" + // + " gl_Position = is_water ? MVP * myposition : vec4(2.0, 0.0, 0.0, 1.0);\r\n" + // + "\r\n" + // + " UV = vec2((vPosition.x + pos.x%2)/2.0, (vPosition.y + pos.y%2)/2.0);\r\n" + // + "\r\n" + // + " float ground_height = texelFetch(ground_height_texture, height_pos, 0).r;\r\n" + // + " float value = clamp(water_height - ground_height, 0.f, 1.f);\r\n" + // + " if (value <= deeplevel) {\r\n" + // + " value = max(0.f, value - min_depth) / (deeplevel - min_depth);\r\n" + // + " Color = shallow_color_min * (1.f - value) + shallow_color_max * value;\r\n" + // + " } else {\r\n" + // + " value = clamp(value - deeplevel, 0.f, maxdepth - deeplevel) / (maxdepth - deeplevel);\r\n" + // + " Color = deep_color_min * (1.f - value) + deep_color_max * value;\r\n" + // + " }\r\n" + // + Shaders.lightSystem("Normal", "myposition.xyz", "lightTexture", "lightTextureHeight", "lightCount", + true) + + "\r\n" + // + " shadeColor = clamp(lightFactor, 0.0, 1.0);\r\n" + // + " }"; + + public static final String frag = "#version 330 core\r\n" + // + "\r\n" + // + "uniform sampler2DArray water_textures;\r\n" + // + "uniform sampler2D water_exists_texture;\r\n" + // + "\r\n" + // + "\r\n" + // + "uniform int current_texture;\r\n" + // + "uniform vec4 mapBounds;\r\n" + // + "\r\n" + // + "in vec2 UV;\r\n" + // + "in vec4 Color;\r\n" + // + "in vec2 position;\r\n" + // + "in vec3 shadeColor;\r\n" + // + "\r\n" + // + "out vec4 outColor;\r\n" + // + "\r\n" + // + "void main() {\r\n" + // + " vec2 d2 = min(position - mapBounds.xy, mapBounds.zw - position);\r\n" + // + " float d1 = clamp(min(d2.x, d2.y) / 64.0 + 1.0, 0.0, 1.0) * 0.8 + 0.2;;\r\n" + // + " outColor = texture(water_textures, vec3(UV, current_texture)) * vec4(Color.rgb * d1 * shadeColor, Color.a);\r\n" + + // + "}"; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/WaveBuilder.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/WaveBuilder.java new file mode 100644 index 0000000..dfb639c --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/environment/WaveBuilder.java @@ -0,0 +1,152 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.environment; + +import java.util.HashMap; +import java.util.Map; + +import com.badlogic.gdx.math.Quaternion; +import com.etheller.warsmash.parsers.w3x.w3e.War3MapW3e; +import com.etheller.warsmash.parsers.w3x.w3i.War3MapW3i; +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.units.Element; +import com.etheller.warsmash.util.RenderMathUtils; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; + +public class WaveBuilder { + + private final int[] mapSize; + private final DataTable waterTable; + private final War3MapViewer viewer; + private final RenderCorner[][] corners; + private final float[] centerOffset; + private final float waterHeightOffset; + private float[] locations; + + private final Map models; + private final War3MapW3e w3eFile; + private final War3MapW3i w3iFile; + + public WaveBuilder(final int[] mapSize, final DataTable waterTable, final War3MapViewer viewer, + final RenderCorner[][] corners, final float[] centerOffset, final float waterHeightOffset, + final War3MapW3e w3eFile, final War3MapW3i w3iFile) { + this.mapSize = mapSize; + this.waterTable = waterTable; + this.viewer = viewer; + this.corners = corners; + this.centerOffset = centerOffset; + this.waterHeightOffset = waterHeightOffset; + this.w3eFile = w3eFile; + this.w3iFile = w3iFile; + this.models = new HashMap<>(); + } + + public void createWaves(final Terrain terrain) { + final int columns = this.mapSize[0]; + final int rows = this.mapSize[1]; + final float wavesDepth = 25f / 128f; + final char tileset = this.w3eFile.getTileset(); + final Element waterRow = this.waterTable.get(tileset + "Sha"); + + final long wavesCliff = (this.w3iFile.getFlags() & 0x0800); + final long wavesRolling = (this.w3iFile.getFlags() & 0x1000); + + final String shoreline = waterRow.getField("shoreDir") + "\\" + waterRow.getField("shoreSFile") + "\\" + + waterRow.getField("shoreSFile") + "0.mdx"; + final String outsideCorner = waterRow.getField("shoreDir") + "\\" + waterRow.getField("shoreOCFile") + "\\" + + waterRow.getField("shoreOCFile") + "0.mdx"; + final String insideCorner = waterRow.getField("shoreDir") + "\\" + waterRow.getField("shoreICFile") + "\\" + + waterRow.getField("shoreICFile") + "0.mdx"; +// final String shoreline = "Buildings\\Other\\TempArtB\\TempArtB.mdx"; +// final String outsideCorner = "Buildings\\Other\\TempArtB\\TempArtB.mdx"; +// final String insideCorner = "Buildings\\Other\\TempArtB\\TempArtB.mdx"; + + this.locations = new float[3]; + + for (int y = 0; y < (rows - 1); ++y) { + for (int x = 0; x < (columns - 1); ++x) { + final RenderCorner a = this.corners[x][y]; + final RenderCorner b = this.corners[x + 1][y]; + final RenderCorner c = this.corners[x + 1][y + 1]; + final RenderCorner d = this.corners[x][y + 1]; + if ((a.getWater() != 0) || (b.getWater() != 0) || (c.getWater() != 0) || (d.getWater() != 0)) { + final boolean isCliff = (a.getLayerHeight() != b.getLayerHeight()) + || (a.getLayerHeight() != c.getLayerHeight()) || (a.getLayerHeight() != d.getLayerHeight()); + if (isCliff && (wavesCliff == 0)) { + continue; + } + if (!isCliff && (wavesRolling == 0)) { + continue; + } + final int ad = (a.depth > wavesDepth) ? 1 : 0; + final int bd = (b.depth > wavesDepth) ? 1 : 0; + final int cd = (c.depth > wavesDepth) ? 1 : 0; + final int dd = (d.depth > wavesDepth) ? 1 : 0; + final int count = ad + bd + cd + dd; + this.locations[0] = (x * 128.0f) + this.centerOffset[0] + 64.0f; + this.locations[1] = (y * 128.0f) + this.centerOffset[1] + 64.0f; + this.locations[2] = ((((a.getWaterHeight() + b.getWaterHeight() + c.getWaterHeight() + + d.getWaterHeight()) / 4f) + this.waterHeightOffset) * 128.0f) + 1.0f; + if (count == 1) { + addModelInstance(terrain, insideCorner, rotation(ad, bd, cd/* , dd */) - ((3 * Math.PI) / 4)); + } + else if (count == 2) { + final double rot = rotation2(ad, bd, cd, dd); + if (!Double.isNaN(rot)) { + addModelInstance(terrain, shoreline, rot); + } + } + else if (count == 3) { + addModelInstance(terrain, outsideCorner, + rotation(1 ^ ad, 1 ^ bd, 1 ^ cd/* , 1 ^ dd */) + ((5 * Math.PI) / 4)); + } + } + } + } + } + + private void addModelInstance(final Terrain terrain, final String path, final double rotation) { + if (!this.models.containsKey(path)) { + this.models.put(path, + (MdxModel) this.viewer.load(path, this.viewer.wc3PathSolver, this.viewer.solverParams)); + } + final MdxModel model = this.models.get(path); + final MdxComplexInstance instance = (MdxComplexInstance) model.addInstance(); + instance.setLocation(this.locations); + instance.setLocalRotation(new Quaternion().setFromAxisRad(RenderMathUtils.VEC3_UNIT_Z, (float) rotation)); + instance.setScene(this.viewer.worldScene); + if (!terrain.inPlayableArea(this.locations[0], this.locations[1])) { + instance.setVertexColor(new float[] { 51 / 255f, 51 / 255f, 51 / 255f, 1.0f }); + } + this.viewer.standOnRepeat(instance); + } + + private static double rotation(final int a, final int b, final int c) { + if (a != 0) { + return (-3 * Math.PI) / 4; + } + if (b != 0) { + return -Math.PI / 4; + } + if (c != 0) { + return Math.PI / 4; + } + return (3 * Math.PI) / 4; + } + + private static double rotation2(final int a, final int b, final int c, final int d) { + if ((a != 0) && (b != 0)) { + return -Math.PI / 2; + } + if ((b != 0) && (c != 0)) { + return 0; + } + if ((c != 0) && (d != 0)) { + return Math.PI / 2; + } + if ((a != 0) && (d != 0)) { + return Math.PI; + } + return Double.NaN; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/OrientationInterpolation.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/OrientationInterpolation.java new file mode 100644 index 0000000..e761e7c --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/OrientationInterpolation.java @@ -0,0 +1,61 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim; + +/** + * We observe this table during gameplay but I haven't found it anywhere in the + * data yet. So, I'm making my own. + */ +public enum OrientationInterpolation { + OI0(0.07f, 0.2f, 999f), + OI1(0.03f, 0.1f, 0.04f), + OI2(0.015f, 1.0f, 999), + OI3(0.005f, 0.1f, 0.0043f), + OI4(0.04f, 0.15f, 0.01f), + OI5(0.05f, 0.18f, 0.015f), + OI6(0.1f, 0.3f, 0.1f), + OI7(0.003f, 0.08f, 0.0027f), + OI8(0.001f, 0.05f, 0.001f); + + public static OrientationInterpolation[] VALUES = values(); + + private float startingAcceleration; + private float maxVelocity; + private float endingNegativeAcceleration; + private float endingAccelCutoff; + private float startingAccelCutoff; + + private OrientationInterpolation(final float startingAcceleration, final float maxVelocity, + final float endingNegativeAcceleration) { + this.startingAcceleration = startingAcceleration; + this.maxVelocity = maxVelocity; + this.endingNegativeAcceleration = endingNegativeAcceleration; + this.endingAccelCutoff = endingAccelCutoff(maxVelocity, endingNegativeAcceleration); + this.startingAccelCutoff = endingAccelCutoff(maxVelocity, startingAcceleration); + } + + public float getStartingAcceleration() { + return this.startingAcceleration; + } + + public float getMaxVelocity() { + return this.maxVelocity; + } + + public float getEndingNegativeAcceleration() { + return this.endingNegativeAcceleration; + } + + public float getEndingAccelCutoff() { + return this.endingAccelCutoff; + } + + public float getStartingAccelCutoff() { + return this.startingAccelCutoff; + } + + private static float endingAccelCutoff(final float maxVelocity, final float endingAccel) { + final float endingAccelFinishingTime = maxVelocity / endingAccel; + final float endingDistanceRequired = (maxVelocity * endingAccelFinishingTime) + - ((endingAccel / 2) * (endingAccelFinishingTime * endingAccelFinishingTime)); + return endingDistanceRequired; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderAttackInstant.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderAttackInstant.java new file mode 100644 index 0000000..3129690 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderAttackInstant.java @@ -0,0 +1,40 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim; + +import java.util.List; + +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.mdx.Sequence; +import com.etheller.warsmash.viewer5.handlers.mdx.SequenceLoopMode; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.IndexedSequence; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; + +public class RenderAttackInstant implements RenderEffect { + private final MdxComplexInstance modelInstance; + + public RenderAttackInstant(final MdxComplexInstance modelInstance, final War3MapViewer war3MapViewer, + final float yaw) { + this.modelInstance = modelInstance; + final MdxModel model = (MdxModel) this.modelInstance.model; + final List sequences = model.getSequences(); + final IndexedSequence sequence = SequenceUtils.selectSequence(PrimaryTag.DEATH, SequenceUtils.EMPTY, sequences, + true); + if ((sequence != null) && (sequence.index != -1)) { + this.modelInstance.setSequenceLoopMode(SequenceLoopMode.NEVER_LOOP); + this.modelInstance.setSequence(sequence.index); + } + this.modelInstance.localRotation.setFromAxisRad(0, 0, 1, yaw); + } + + @Override + public boolean updateAnimations(final War3MapViewer war3MapViewer, final float deltaTime) { + + final boolean everythingDone = this.modelInstance.sequenceEnded; + if (everythingDone) { + war3MapViewer.worldScene.removeInstance(this.modelInstance); + } + return everythingDone; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderAttackProjectile.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderAttackProjectile.java new file mode 100644 index 0000000..4731aaf --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderAttackProjectile.java @@ -0,0 +1,139 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim; + +import java.util.List; + +import com.badlogic.gdx.math.Quaternion; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.mdx.Sequence; +import com.etheller.warsmash.viewer5.handlers.mdx.SequenceLoopMode; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.IndexedSequence; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetWidgetVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.projectile.CAttackProjectile; + +public class RenderAttackProjectile implements RenderEffect { + private static final Quaternion pitchHeap = new Quaternion(); + + private final CAttackProjectile simulationProjectile; + private final MdxComplexInstance modelInstance; + private float x; + private float y; + private float z; + private final float startingHeight; + private final float arcPeakHeight; + private float totalTravelDistance; + + private final float targetHeight; + + private float yaw; + + private float pitch; + private boolean done = false; + private float deathTimeElapsed; + + public RenderAttackProjectile(final CAttackProjectile simulationProjectile, final MdxComplexInstance modelInstance, + final float z, final float arc, final War3MapViewer war3MapViewer) { + this.simulationProjectile = simulationProjectile; + this.modelInstance = modelInstance; + this.x = simulationProjectile.getX(); + this.y = simulationProjectile.getY(); + this.z = z; + this.startingHeight = z; + final float targetX = this.simulationProjectile.getTarget().getX(); + final float targetY = this.simulationProjectile.getTarget().getY(); + final float dxToTarget = targetX - this.x; + final float dyToTarget = targetY - this.y; + final float d2DToTarget = (float) StrictMath.sqrt((dxToTarget * dxToTarget) + (dyToTarget * dyToTarget)); + final float startingDistance = d2DToTarget + this.totalTravelDistance; + final CWidget widgetTarget = this.simulationProjectile.getTarget().visit(AbilityTargetWidgetVisitor.INSTANCE); + float impactZ; + float flyHeight; + if ((simulationProjectile.getUnitAttack().getWeaponType() == CWeaponType.ARTILLERY) || (widgetTarget == null)) { + impactZ = 0; + flyHeight = 0; + } + else { + impactZ = widgetTarget.getImpactZ(); + flyHeight = widgetTarget.getFlyHeight(); + } + this.targetHeight = (war3MapViewer.terrain.getGroundHeight(targetX, targetY) + flyHeight + impactZ); + this.arcPeakHeight = arc * startingDistance; + this.yaw = (float) StrictMath.atan2(dyToTarget, dxToTarget); + } + + @Override + public boolean updateAnimations(final War3MapViewer war3MapViewer, final float deltaTime) { + final boolean wasDone = this.done; + if (this.done = this.simulationProjectile.isDone()) { + final MdxModel model = (MdxModel) this.modelInstance.model; + final List sequences = model.getSequences(); + final IndexedSequence sequence = SequenceUtils.selectSequence(PrimaryTag.DEATH, SequenceUtils.EMPTY, + sequences, true); + if ((sequence != null) && this.done && !wasDone) { + this.modelInstance.setSequenceLoopMode(SequenceLoopMode.NEVER_LOOP); + this.modelInstance.setSequence(sequence.index); + } + } + else { + if (this.modelInstance.sequenceEnded || (this.modelInstance.sequence == -1)) { + SequenceUtils.randomStandSequence(this.modelInstance); + } + } + final float simX = this.simulationProjectile.getX(); + final float simY = this.simulationProjectile.getY(); + final float simDx = simX - this.x; + final float simDy = simY - this.y; + final float simD = (float) StrictMath.sqrt((simDx * simDx) + (simDy * simDy)); + final float speed = StrictMath.min(simD, this.simulationProjectile.getSpeed() * deltaTime); + if (simD > 0) { + this.x = this.x + ((speed * simDx) / simD); + this.y = this.y + ((speed * simDy) / simD); + final float targetX = this.simulationProjectile.getTargetX(); + final float targetY = this.simulationProjectile.getTargetY(); + final float dxToTarget = targetX - this.x; + final float dyToTarget = targetY - this.y; + final float d2DToTarget = (float) StrictMath.sqrt((dxToTarget * dxToTarget) + (dyToTarget * dyToTarget)); + final float startingDistance = d2DToTarget + this.totalTravelDistance; + final float halfStartingDistance = startingDistance / 2f; + + final float dtsz = this.targetHeight - this.startingHeight; + final float d1z = dtsz / (halfStartingDistance * 2); + this.totalTravelDistance += speed; + final float dz = d1z * this.totalTravelDistance; + + final float distanceToPeak = this.totalTravelDistance - halfStartingDistance; + final float normPeakDist = distanceToPeak / halfStartingDistance; + final float currentHeightPercentage = 1 - (normPeakDist * normPeakDist); + final float arcCurrentHeight = currentHeightPercentage * this.arcPeakHeight; + this.z = this.startingHeight + dz + arcCurrentHeight; + + if (!this.done) { + this.yaw = (float) StrictMath.atan2(dyToTarget, dxToTarget); + + final float slope = (-2 * (normPeakDist) * this.arcPeakHeight) / halfStartingDistance; + this.pitch = (float) StrictMath.atan2(slope + d1z, 1); + } + } + if (this.done) { + this.pitch = 0; + this.deathTimeElapsed += deltaTime; + } + + this.modelInstance.setLocation(this.x, this.y, this.z); + this.modelInstance.localRotation.setFromAxisRad(0, 0, 1, this.yaw); + this.modelInstance.rotate(pitchHeap.setFromAxisRad(0, -1, 0, this.pitch)); + war3MapViewer.worldScene.instanceMoved(this.modelInstance, this.x, this.y); + + final boolean everythingDone = this.simulationProjectile.isDone() && (this.modelInstance.sequenceEnded + || (this.deathTimeElapsed >= war3MapViewer.simulation.getGameplayConstants().getBulletDeathTime())); + if (everythingDone) { + war3MapViewer.worldScene.removeInstance(this.modelInstance); + } + return everythingDone; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderDestructable.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderDestructable.java new file mode 100644 index 0000000..6da29d4 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderDestructable.java @@ -0,0 +1,145 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.math.Rectangle; +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.units.manager.MutableObjectData.WorldEditorDataType; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.SplatModel.SplatMover; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.BuildingShadow; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CDestructable; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; + +public class RenderDestructable extends RenderDoodad implements RenderWidget { + private static final War3ID TEX_FILE = War3ID.fromString("btxf"); + private static final War3ID TEX_ID = War3ID.fromString("btxi"); + private static final War3ID SEL_CIRCLE_SIZE = War3ID.fromString("bgsc"); + + private float life; + public Rectangle walkableBounds; + private final CDestructable simulationDestructable; + private SplatMover selectionCircle; + private final UnitAnimationListenerImpl unitAnimationListenerImpl; + private boolean dead; + private BuildingShadow destructableShadow; + + public RenderDestructable(final War3MapViewer map, final MdxModel model, final MutableGameObject row, + final com.etheller.warsmash.parsers.w3x.doo.Doodad doodad, final WorldEditorDataType type, + final float maxPitch, final float maxRoll, final float life, final BuildingShadow destructableShadow, + final CDestructable simulationDestructable) { + super(map, model, row, doodad, type, maxPitch, maxRoll); + this.life = simulationDestructable.getLife(); + this.destructableShadow = destructableShadow; + this.simulationDestructable = simulationDestructable; + String replaceableTextureFile = row.getFieldAsString(TEX_FILE, 0); + final int replaceableTextureId = row.getFieldAsInteger(TEX_ID, 0); + if ((replaceableTextureFile != null) && (replaceableTextureFile.length() > 1)) { + final int dotIndex = replaceableTextureFile.lastIndexOf('.'); + if (dotIndex != -1) { + replaceableTextureFile = replaceableTextureFile.substring(0, dotIndex); + } + replaceableTextureFile += ".blp"; + this.instance.setReplaceableTexture(replaceableTextureId, replaceableTextureFile); + } + this.selectionScale *= row.getFieldAsFloat(SEL_CIRCLE_SIZE, 0); + this.unitAnimationListenerImpl = new UnitAnimationListenerImpl((MdxComplexInstance) this.instance); + simulationDestructable.setUnitAnimationListener(this.unitAnimationListenerImpl); + this.unitAnimationListenerImpl.playAnimation(true, getAnimation(), SequenceUtils.EMPTY, 1.0f, true); + } + + @Override + public PrimaryTag getAnimation() { + if (this.life <= 0) { + return PrimaryTag.DEATH; + } + return super.getAnimation(); + } + + @Override + public MdxComplexInstance getInstance() { + return (MdxComplexInstance) this.instance; + } + + @Override + public CWidget getSimulationWidget() { + return this.simulationDestructable; + } + + @Override + public void updateAnimations(final War3MapViewer war3MapViewer) { + // TODO maybe move getAnimation behaviors to here and make this thing not a + // doodad + + final boolean dead = this.simulationDestructable.isDead(); + if (dead && !this.dead) { + this.unitAnimationListenerImpl.playAnimation(true, PrimaryTag.DEATH, SequenceUtils.EMPTY, 1.0f, true); + if (this.destructableShadow != null) { + this.destructableShadow.remove(); + this.destructableShadow = null; + } + if (this.selectionCircle != null) { + this.selectionCircle.destroy(Gdx.gl30, war3MapViewer.terrain.centerOffset); + this.selectionCircle = null; + } + } + else if (!dead) { + if (this.dead) { + this.unitAnimationListenerImpl.playAnimation(true, PrimaryTag.BIRTH, SequenceUtils.EMPTY, 1.0f, true); + // TODO add back shadow here + + } + else { + if (Math.abs(this.life - this.simulationDestructable.getLife()) > 0.003f) { + if (this.life > this.simulationDestructable.getLife()) { + this.unitAnimationListenerImpl.playAnimation(true, PrimaryTag.STAND, SequenceUtils.HIT, 1.0f, + true); + } + this.life = this.simulationDestructable.getLife(); + } + } + } + this.dead = dead; + this.unitAnimationListenerImpl.update(); + } + + @Override + public boolean isIntersectedOnMeshAlways() { + return false; + } + + @Override + public float getSelectionScale() { + return this.selectionScale; + } + + @Override + public float getX() { + return this.x; + } + + @Override + public float getY() { + return this.y; + } + + @Override + public float getZ() { + return this.instance.localLocation.z; + } + + @Override + public void unassignSelectionCircle() { + this.selectionCircle = null; + } + + @Override + public void assignSelectionCircle(final SplatMover selectionCircle) { + this.selectionCircle = selectionCircle; + + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderDoodad.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderDoodad.java new file mode 100644 index 0000000..0b56b77 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderDoodad.java @@ -0,0 +1,92 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim; + +import com.badlogic.gdx.math.Quaternion; +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.units.manager.MutableObjectData.WorldEditorDataType; +import com.etheller.warsmash.util.RenderMathUtils; +import com.etheller.warsmash.viewer5.ModelInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.mdx.SequenceLoopMode; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; + +public class RenderDoodad { + private static final int SAMPLE_RADIUS = 4; + public final ModelInstance instance; + private final MutableGameObject row; + private final float maxPitch; + private final float maxRoll; + protected float x; + protected float y; + protected float selectionScale; + + public RenderDoodad(final War3MapViewer map, final MdxModel model, final MutableGameObject row, + final com.etheller.warsmash.parsers.w3x.doo.Doodad doodad, final WorldEditorDataType type, + final float maxPitch, final float maxRoll) { + this.maxPitch = maxPitch; + this.maxRoll = maxRoll; + final boolean isSimple = row.readSLKTagBoolean("lightweight"); + ModelInstance instance; + + if (isSimple && false) { + instance = model.addInstance(1); + } + else { + instance = model.addInstance(); + ((MdxComplexInstance) instance).setSequenceLoopMode(SequenceLoopMode.NEVER_LOOP); + } + + instance.move(doodad.getLocation()); + // TODO: the following pitch/roll system is a heuristic, and we probably want to + // revisit it later. + // Specifically, I was pretty convinced that whichever is applied first + // (pitch/roll) should be used to do a projection onto the already-tilted plane + // to find the angle used for the other of the two + // (instead of measuring down from an imaginary flat ground plane, as we do + // currently). + final float facingRadians = doodad.getAngle(); + float pitch, roll; + this.x = doodad.getLocation()[0]; + this.y = doodad.getLocation()[1]; + final float pitchSampleForwardX = this.x + (SAMPLE_RADIUS * (float) Math.cos(facingRadians)); + final float pitchSampleForwardY = this.y + (SAMPLE_RADIUS * (float) Math.sin(facingRadians)); + final float pitchSampleBackwardX = this.x - (SAMPLE_RADIUS * (float) Math.cos(facingRadians)); + final float pitchSampleBackwardY = this.y - (SAMPLE_RADIUS * (float) Math.sin(facingRadians)); + final float pitchSampleGroundHeight1 = map.terrain.getGroundHeight(pitchSampleBackwardX, pitchSampleBackwardY); + final float pitchSampleGorundHeight2 = map.terrain.getGroundHeight(pitchSampleForwardX, pitchSampleForwardY); + pitch = Math.max(-maxPitch, Math.min(maxPitch, + (float) Math.atan2(pitchSampleGorundHeight2 - pitchSampleGroundHeight1, SAMPLE_RADIUS * 2))); + final double leftOfFacingAngle = facingRadians + (Math.PI / 2); + final float rollSampleForwardX = this.x + (SAMPLE_RADIUS * (float) Math.cos(leftOfFacingAngle)); + final float rollSampleForwardY = this.y + (SAMPLE_RADIUS * (float) Math.sin(leftOfFacingAngle)); + final float rollSampleBackwardX = this.x - (SAMPLE_RADIUS * (float) Math.cos(leftOfFacingAngle)); + final float rollSampleBackwardY = this.y - (SAMPLE_RADIUS * (float) Math.sin(leftOfFacingAngle)); + final float rollSampleGroundHeight1 = map.terrain.getGroundHeight(rollSampleBackwardX, rollSampleBackwardY); + final float rollSampleGroundHeight2 = map.terrain.getGroundHeight(rollSampleForwardX, rollSampleForwardY); + roll = Math.max(-maxRoll, Math.min(maxRoll, + (float) Math.atan2(rollSampleGroundHeight2 - rollSampleGroundHeight1, SAMPLE_RADIUS * 2))); + instance.rotate(new Quaternion().setFromAxisRad(RenderMathUtils.VEC3_UNIT_Z, facingRadians)); + instance.rotate(new Quaternion().setFromAxisRad(RenderMathUtils.VEC3_UNIT_Y, -pitch)); + instance.rotate(new Quaternion().setFromAxisRad(RenderMathUtils.VEC3_UNIT_X, roll)); +// instance.rotate(new Quaternion().setEulerAnglesRad(facingRadians, 0, 0)); + final float[] scale = doodad.getScale(); + instance.scale(scale); + if (type == WorldEditorDataType.DOODADS) { + final float defScale = row.readSLKTagFloat("defScale"); + instance.uniformScale(defScale); + this.selectionScale = defScale; + } + else { + this.selectionScale = (float) Math.sqrt((scale[0]) * (scale[1]) * (scale[2])); + } + instance.setScene(map.worldScene); + + this.instance = instance; + this.row = row; + } + + public PrimaryTag getAnimation() { + return PrimaryTag.STAND; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderEffect.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderEffect.java new file mode 100644 index 0000000..653b006 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderEffect.java @@ -0,0 +1,7 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim; + +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; + +public interface RenderEffect { + boolean updateAnimations(final War3MapViewer war3MapViewer, float deltaTime); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderItem.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderItem.java new file mode 100644 index 0000000..2dc82bd --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderItem.java @@ -0,0 +1,179 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim; + +import com.badlogic.gdx.math.Quaternion; +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.RenderMathUtils; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.SplatModel.SplatMover; +import com.etheller.warsmash.viewer5.handlers.w3x.UnitSoundset; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; + +public class RenderItem implements RenderWidget { + private static final War3ID ITEM_MODEL_SCALE = War3ID.fromString("isca"); + private static final War3ID ITEM_RED = War3ID.fromString("iclr"); + private static final War3ID ITEM_GREEN = War3ID.fromString("iclg"); + private static final War3ID ITEM_BLUE = War3ID.fromString("iclb"); + private final CItem simulationItem; + public final MdxComplexInstance instance; + public final MutableGameObject row; + public final float[] location = new float[3]; + public float radius; + public UnitSoundset soundset; + public final MdxModel portraitModel; + public SplatMover shadow; + public SplatMover selectionCircle; + private boolean hidden; + private boolean dead; + + public RenderItem(final War3MapViewer map, final MdxModel model, final MutableGameObject row, final float x, + final float y, final float z, final float angle, final UnitSoundset soundset, final MdxModel portraitModel, + final CItem simulationItem) { + this.portraitModel = portraitModel; + this.simulationItem = simulationItem; + final MdxComplexInstance instance = (MdxComplexInstance) model.addInstance(); + + this.location[0] = x; + this.location[1] = y; + this.location[2] = z; + instance.move(this.location); +// instance.localRotation.setFromAxisRad(RenderMathUtils.VEC3_UNIT_Z, angle); + instance.rotate(new Quaternion().setFromAxisRad(RenderMathUtils.VEC3_UNIT_Z, angle)); + instance.setScene(map.worldScene); + + if (row != null) { + War3ID red; + War3ID green; + War3ID blue; + War3ID scale; + scale = ITEM_MODEL_SCALE; + red = ITEM_RED; + green = ITEM_GREEN; + blue = ITEM_BLUE; + instance.setVertexColor(new float[] { (row.getFieldAsInteger(red, 0)) / 255f, + (row.getFieldAsInteger(green, 0)) / 255f, (row.getFieldAsInteger(blue, 0)) / 255f }); + instance.uniformScale(row.getFieldAsFloat(scale, 0)); + + this.radius = 1 * 36; + } + + this.instance = instance; + this.row = row; + this.soundset = soundset; + } + + @Override + public MdxComplexInstance getInstance() { + return this.instance; + } + + @Override + public CWidget getSimulationWidget() { + return this.simulationItem; + } + + @Override + public void updateAnimations(final War3MapViewer map) { + final boolean hidden = this.simulationItem.isHidden(); + if (hidden != this.hidden) { + this.hidden = hidden; + if (hidden) { + this.instance.hide(); + if (this.shadow != null) { + this.shadow.hide(); + } + } + else { + this.instance.show(); + if (this.shadow != null) { + this.shadow.show(map.terrain.centerOffset); + } + } + } + final boolean dead = this.simulationItem.isDead(); + final MdxComplexInstance mdxComplexInstance = this.instance; + if (dead) { + if (!this.dead) { + this.dead = dead; + SequenceUtils.randomDeathSequence(mdxComplexInstance); + } + } + else if (mdxComplexInstance.sequenceEnded || (mdxComplexInstance.sequence == -1)) { + SequenceUtils.randomStandSequence(mdxComplexInstance); + } + + final float prevX = this.location[0]; + final float prevY = this.location[1]; + final float simulationX = this.simulationItem.getX(); + final float simulationY = this.simulationItem.getY(); + final float simDx = simulationX - this.location[0]; + final float simDy = simulationY - this.location[1]; + this.location[0] = simulationX; + this.location[1] = simulationY; + final float dx = this.location[0] - prevX; + final float dy = this.location[1] - prevY; + final float groundHeight; + // land units will have their feet pass under the surface of the water, so items + // here are in the same place + final float groundHeightTerrainAndWater = map.terrain.getGroundHeight(this.location[0], this.location[1]); + MdxComplexInstance currentWalkableUnder; + currentWalkableUnder = map.getHighestWalkableUnder(this.location[0], this.location[1]); + War3MapViewer.gdxRayHeap.set(this.location[0], this.location[1], 4096, 0, 0, -8192); + if ((currentWalkableUnder != null) + && currentWalkableUnder.intersectRayWithCollision(War3MapViewer.gdxRayHeap, + War3MapViewer.intersectionHeap, true, true) + && (War3MapViewer.intersectionHeap.z > groundHeightTerrainAndWater)) { + groundHeight = War3MapViewer.intersectionHeap.z; + } + else { + groundHeight = groundHeightTerrainAndWater; + currentWalkableUnder = null; + } + this.location[2] = this.simulationItem.getFlyHeight() + groundHeight; + + this.instance.moveTo(this.location); + if (this.shadow != null) { + this.shadow.move(dx, dy, map.terrain.centerOffset); + this.shadow.setHeightAbsolute(currentWalkableUnder != null, groundHeight + map.imageWalkableZOffset); + } + } + + @Override + public boolean isIntersectedOnMeshAlways() { + return false; + } + + @Override + public float getSelectionScale() { + return 1.0f; + } + + @Override + public float getX() { + return this.location[0]; + } + + @Override + public float getY() { + return this.location[1]; + } + + @Override + public float getZ() { + return this.location[2]; + } + + @Override + public void unassignSelectionCircle() { + this.selectionCircle = null; + } + + @Override + public void assignSelectionCircle(final SplatMover t) { + this.selectionCircle = t; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderUnit.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderUnit.java new file mode 100644 index 0000000..346ee7a --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderUnit.java @@ -0,0 +1,498 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim; + +import java.util.EnumSet; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.math.Quaternion; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.RenderMathUtils; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.SecondaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.SplatModel.SplatMover; +import com.etheller.warsmash.viewer5.handlers.w3x.UnitSoundset; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.BuildingShadow; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.MovementType; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.ability.AbilityDataUI; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.commandbuttons.CommandButtonListener; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.commandbuttons.CommandCardPopulatingAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; + +public class RenderUnit implements RenderWidget { + public static final Quaternion tempQuat = new Quaternion(); + private static final War3ID RED = War3ID.fromString("uclr"); + private static final War3ID GREEN = War3ID.fromString("uclg"); + private static final War3ID BLUE = War3ID.fromString("uclb"); + private static final War3ID MODEL_SCALE = War3ID.fromString("usca"); + private static final War3ID MOVE_HEIGHT = War3ID.fromString("umvh"); + private static final War3ID ORIENTATION_INTERPOLATION = War3ID.fromString("uori"); + private static final War3ID ANIM_PROPS = War3ID.fromString("uani"); + private static final War3ID BLEND_TIME = War3ID.fromString("uble"); + private static final War3ID BUILD_SOUND_LABEL = War3ID.fromString("ubsl"); + private static final War3ID UNIT_SELECT_HEIGHT = War3ID.fromString("uslz"); + private static final float[] heapZ = new float[3]; + public final MdxComplexInstance instance; + public final MutableGameObject row; + public final float[] location = new float[3]; + public float selectionScale; + public UnitSoundset soundset; + public final MdxModel portraitModel; + public int playerIndex; + private final CUnit simulationUnit; + public SplatMover shadow; + private BuildingShadow buildingShadowInstance; + public SplatMover selectionCircle; + + private float facing; + + private boolean swimming; + private boolean working; + + private boolean dead = false; + + private final UnitAnimationListenerImpl unitAnimationListenerImpl; + private OrientationInterpolation orientationInterpolation; + private float currentTurnVelocity = 0; + public long lastUnitResponseEndTimeMillis; + private boolean corpse; + private boolean boneCorpse; + private final RenderUnitTypeData typeData; + public final MdxModel specialArtModel; + public SplatMover uberSplat; + private float selectionHeight; + + public RenderUnit(final War3MapViewer map, final MdxModel model, final MutableGameObject row, final float x, + final float y, final float z, final int playerIndex, final UnitSoundset soundset, + final MdxModel portraitModel, final CUnit simulationUnit, final RenderUnitTypeData typeData, + final MdxModel specialArtModel, final BuildingShadow buildingShadow, + final float selectionCircleScaleFactor) { + this.portraitModel = portraitModel; + this.simulationUnit = simulationUnit; + this.typeData = typeData; + this.specialArtModel = specialArtModel; + this.buildingShadowInstance = buildingShadow; + final MdxComplexInstance instance = (MdxComplexInstance) model.addInstance(); + + this.location[0] = x; + this.location[1] = y; + this.location[2] = z; + instance.move(this.location); + this.facing = simulationUnit.getFacing(); + final float angle = (float) Math.toRadians(this.facing); +// instance.localRotation.setFromAxisRad(RenderMathUtils.VEC3_UNIT_Z, angle); + instance.rotate(tempQuat.setFromAxisRad(RenderMathUtils.VEC3_UNIT_Z, angle)); + this.playerIndex = playerIndex & 0xFFFF; + instance.setTeamColor(this.playerIndex); + instance.setScene(map.worldScene); + this.unitAnimationListenerImpl = new UnitAnimationListenerImpl(instance); + simulationUnit.setUnitAnimationListener(this.unitAnimationListenerImpl); + final String requiredAnimationNames = row.getFieldAsString(ANIM_PROPS, 0); + TokenLoop: for (final String animationName : requiredAnimationNames.split(",")) { + final String upperCaseToken = animationName.toUpperCase(); + for (final SecondaryTag secondaryTag : SecondaryTag.values()) { + if (upperCaseToken.equals(secondaryTag.name())) { + this.unitAnimationListenerImpl.addSecondaryTag(secondaryTag); + continue TokenLoop; + } + } + } + + if (row != null) { + heapZ[2] = simulationUnit.getFlyHeight(); + this.location[2] += heapZ[2]; + + instance.move(heapZ); + War3ID red; + War3ID green; + War3ID blue; + War3ID scale; + scale = MODEL_SCALE; + red = RED; + green = GREEN; + blue = BLUE; + instance.setVertexColor(new float[] { (row.getFieldAsInteger(red, 0)) / 255f, + (row.getFieldAsInteger(green, 0)) / 255f, (row.getFieldAsInteger(blue, 0)) / 255f }); + instance.uniformScale(row.getFieldAsFloat(scale, 0)); + + this.selectionScale = row.getFieldAsFloat(War3MapViewer.UNIT_SELECT_SCALE, 0) * selectionCircleScaleFactor; + this.selectionHeight = row.getFieldAsFloat(UNIT_SELECT_HEIGHT, 0); + int orientationInterpolationOrdinal = row.getFieldAsInteger(ORIENTATION_INTERPOLATION, 0); + if ((orientationInterpolationOrdinal < 0) + || (orientationInterpolationOrdinal >= OrientationInterpolation.VALUES.length)) { + orientationInterpolationOrdinal = 0; + } + this.orientationInterpolation = OrientationInterpolation.VALUES[orientationInterpolationOrdinal]; + + final float blendTime = row.getFieldAsFloat(BLEND_TIME, 0); + instance.setBlendTime(blendTime * 1000.0f); + } + + this.instance = instance; + this.row = row; + this.soundset = soundset; + + } + + public void populateCommandCard(final CSimulation game, final GameUI gameUI, + final CommandButtonListener commandButtonListener, final AbilityDataUI abilityDataUI, + final int subMenuOrderId) { + final CommandCardPopulatingAbilityVisitor commandCardPopulatingVisitor = CommandCardPopulatingAbilityVisitor.INSTANCE + .reset(game, gameUI, this.simulationUnit, commandButtonListener, abilityDataUI, subMenuOrderId); + for (final CAbility ability : this.simulationUnit.getAbilities()) { + ability.visit(commandCardPopulatingVisitor); + } + } + + @Override + public void updateAnimations(final War3MapViewer map) { + final boolean wasHidden = this.instance.hidden(); + if (this.simulationUnit.isHidden()) { + if (!wasHidden) { + if (this.selectionCircle != null) { + this.selectionCircle.hide(); + } + if (this.shadow != null) { + this.shadow.hide(); + } + } + this.instance.hide(); + return; + } + else { + this.instance.show(); + if (wasHidden) { + if (this.selectionCircle != null) { + this.selectionCircle.show(map.terrain.centerOffset); + } + if (this.shadow != null) { + this.shadow.show(map.terrain.centerOffset); + } + } + } + final float prevX = this.location[0]; + final float prevY = this.location[1]; + final float simulationX = this.simulationUnit.getX(); + final float simulationY = this.simulationUnit.getY(); + final float deltaTime = Gdx.graphics.getDeltaTime(); + final float simDx = simulationX - this.location[0]; + final float simDy = simulationY - this.location[1]; + final float distanceToSimulation = (float) Math.sqrt((simDx * simDx) + (simDy * simDy)); + final int speed = this.simulationUnit.getSpeed(); + final float speedDelta = speed * deltaTime; + if ((distanceToSimulation > speedDelta) && (deltaTime < 1.0)) { + // The 1.0 here says that after 1 second of lag, units just teleport to show + // where they actually are + this.location[0] += (speedDelta * simDx) / distanceToSimulation; + this.location[1] += (speedDelta * simDy) / distanceToSimulation; + } + else { + this.location[0] = simulationX; + this.location[1] = simulationY; + } + final float dx = this.location[0] - prevX; + final float dy = this.location[1] - prevY; + final float groundHeight; + final MovementType movementType = this.simulationUnit.getUnitType().getMovementType(); + final short terrainPathing = map.terrain.pathingGrid.getPathing(this.location[0], this.location[1]); + boolean swimming = (movementType == MovementType.AMPHIBIOUS) + && PathingGrid.isPathingFlag(terrainPathing, PathingGrid.PathingType.SWIMMABLE) + && !PathingGrid.isPathingFlag(terrainPathing, PathingGrid.PathingType.WALKABLE); + final boolean working = this.simulationUnit.getBuildQueueTypes()[0] != null; + final float groundHeightTerrain = map.terrain.getGroundHeight(this.location[0], this.location[1]); + float groundHeightTerrainAndWater; + MdxComplexInstance currentWalkableUnder; + final boolean standingOnWater = (swimming) || (movementType == MovementType.FLOAT) + || (movementType == MovementType.FLY) || (movementType == MovementType.HOVER); + if (standingOnWater) { + groundHeightTerrainAndWater = Math.max(groundHeightTerrain, + map.terrain.getWaterHeight(this.location[0], this.location[1])); + } + else { + // land units will have their feet pass under the surface of the water + groundHeightTerrainAndWater = groundHeightTerrain; + } + if (movementType == MovementType.FLOAT) { + // boats cant go on bridges + groundHeight = groundHeightTerrainAndWater; + currentWalkableUnder = null; + } + else { + currentWalkableUnder = map.getHighestWalkableUnder(this.location[0], this.location[1]); + War3MapViewer.gdxRayHeap.set(this.location[0], this.location[1], 4096, 0, 0, -8192); + if ((currentWalkableUnder != null) + && currentWalkableUnder.intersectRayWithCollision(War3MapViewer.gdxRayHeap, + War3MapViewer.intersectionHeap, true, true) + && (War3MapViewer.intersectionHeap.z > groundHeightTerrainAndWater)) { + groundHeight = War3MapViewer.intersectionHeap.z; + swimming = false; // Naga Royal Guard should slither across a bridge, not swim in rock + } + else { + groundHeight = groundHeightTerrainAndWater; + currentWalkableUnder = null; + } + } + if (swimming && !this.swimming) { + this.unitAnimationListenerImpl.addSecondaryTag(AnimationTokens.SecondaryTag.SWIM); + } + else if (!swimming && this.swimming) { + this.unitAnimationListenerImpl.removeSecondaryTag(AnimationTokens.SecondaryTag.SWIM); + } + if (working && !this.working) { + this.unitAnimationListenerImpl.addSecondaryTag(AnimationTokens.SecondaryTag.WORK); + } + else if (!working && this.working) { + this.unitAnimationListenerImpl.removeSecondaryTag(AnimationTokens.SecondaryTag.WORK); + } + this.swimming = swimming; + this.working = working; + final boolean dead = this.simulationUnit.isDead(); + final boolean corpse = this.simulationUnit.isCorpse(); + final boolean boneCorpse = this.simulationUnit.isBoneCorpse(); + if (dead && !this.dead) { + this.unitAnimationListenerImpl.playAnimation(true, PrimaryTag.DEATH, SequenceUtils.EMPTY, 1.0f, true); + if (this.shadow != null) { + this.shadow.destroy(Gdx.gl30, map.terrain.centerOffset); + this.shadow = null; + } + if (this.buildingShadowInstance != null) { + this.buildingShadowInstance.remove(); + this.buildingShadowInstance = null; + } + if (this.uberSplat != null) { + this.uberSplat.destroy(Gdx.gl30, map.terrain.centerOffset); + this.uberSplat = null; + } + if (this.selectionCircle != null) { + this.selectionCircle.destroy(Gdx.gl30, map.terrain.centerOffset); + this.selectionCircle = null; + } + } + if (boneCorpse && !this.boneCorpse) { + this.unitAnimationListenerImpl.playAnimationWithDuration(true, PrimaryTag.DECAY, SequenceUtils.BONE, + this.simulationUnit.getEndingDecayTime(map.simulation), true); + } + else if (corpse && !this.corpse) { + this.unitAnimationListenerImpl.playAnimationWithDuration(true, PrimaryTag.DECAY, SequenceUtils.FLESH, + map.simulation.getGameplayConstants().getDecayTime(), true); + } + this.dead = dead; + this.corpse = corpse; + this.boneCorpse = boneCorpse; + this.location[2] = this.simulationUnit.getFlyHeight() + groundHeight; + final float selectionCircleHeight = this.selectionHeight + groundHeight; + this.instance.moveTo(this.location); + float simulationFacing = this.simulationUnit.getFacing(); + if (simulationFacing < 0) { + simulationFacing += 360; + } + float renderFacing = this.facing; + if (renderFacing < 0) { + renderFacing += 360; + } + float facingDelta = simulationFacing - renderFacing; + if (facingDelta < -180) { + facingDelta = 360 + facingDelta; + } + if (facingDelta > 180) { + facingDelta = -360 + facingDelta; + } + final float absoluteFacingDelta = Math.abs(facingDelta); + final float turningSign = Math.signum(facingDelta); + + final float absoluteFacingDeltaRadians = (float) Math.toRadians(absoluteFacingDelta); + float acceleration; + final boolean endPhase = (absoluteFacingDeltaRadians <= this.orientationInterpolation.getEndingAccelCutoff()) + && ((this.currentTurnVelocity * turningSign) > 0); + if (endPhase) { + this.currentTurnVelocity = (1 + - ((this.orientationInterpolation.getEndingAccelCutoff() - absoluteFacingDeltaRadians) + / this.orientationInterpolation.getEndingAccelCutoff())) + * (this.orientationInterpolation.getMaxVelocity()) * turningSign; + } + else { + acceleration = this.orientationInterpolation.getStartingAcceleration() * turningSign; + this.currentTurnVelocity = this.currentTurnVelocity + acceleration; + } + if ((this.currentTurnVelocity * turningSign) > this.orientationInterpolation.getMaxVelocity()) { + this.currentTurnVelocity = this.orientationInterpolation.getMaxVelocity() * turningSign; + } + float angleToAdd = (float) ((Math.toDegrees(this.currentTurnVelocity) * deltaTime) / 0.03f); + + if (absoluteFacingDelta < Math.abs(angleToAdd)) { + angleToAdd = facingDelta; + this.currentTurnVelocity = 0.0f; + } + this.facing = (((this.facing + angleToAdd) % 360) + 360) % 360; + this.instance.setLocalRotation(tempQuat.setFromAxis(RenderMathUtils.VEC3_UNIT_Z, this.facing)); + + final float facingRadians = (float) Math.toRadians(this.facing); + final float maxPitch = this.typeData.getMaxPitch(); + final float maxRoll = this.typeData.getMaxRoll(); + final float sampleRadius = this.typeData.getElevationSampleRadius(); + float pitch, roll; + final float pitchSampleForwardX = this.location[0] + (sampleRadius * (float) Math.cos(facingRadians)); + final float pitchSampleForwardY = this.location[1] + (sampleRadius * (float) Math.sin(facingRadians)); + final float pitchSampleBackwardX = this.location[0] - (sampleRadius * (float) Math.cos(facingRadians)); + final float pitchSampleBackwardY = this.location[1] - (sampleRadius * (float) Math.sin(facingRadians)); + final double leftOfFacingAngle = facingRadians + (Math.PI / 2); + final float rollSampleForwardX = this.location[0] + (sampleRadius * (float) Math.cos(leftOfFacingAngle)); + final float rollSampleForwardY = this.location[1] + (sampleRadius * (float) Math.sin(leftOfFacingAngle)); + final float rollSampleBackwardX = this.location[0] - (sampleRadius * (float) Math.cos(leftOfFacingAngle)); + final float rollSampleBackwardY = this.location[1] - (sampleRadius * (float) Math.sin(leftOfFacingAngle)); + final float pitchSampleGroundHeight1; + final float pitchSampleGroundHeight2; + final float rollSampleGroundHeight1; + final float rollSampleGroundHeight2; + if (currentWalkableUnder != null) { + pitchSampleGroundHeight1 = getGroundHeightSample(groundHeight, currentWalkableUnder, pitchSampleBackwardX, + pitchSampleBackwardY); + pitchSampleGroundHeight2 = getGroundHeightSample(groundHeight, currentWalkableUnder, pitchSampleForwardX, + pitchSampleForwardY); + rollSampleGroundHeight1 = getGroundHeightSample(groundHeight, currentWalkableUnder, rollSampleBackwardX, + rollSampleBackwardY); + rollSampleGroundHeight2 = getGroundHeightSample(groundHeight, currentWalkableUnder, rollSampleForwardX, + rollSampleForwardY); + } + else { + final float pitchGroundHeight1 = map.terrain.getGroundHeight(pitchSampleBackwardX, pitchSampleBackwardY); + final float pitchGroundHeight2 = map.terrain.getGroundHeight(pitchSampleForwardX, pitchSampleForwardY); + final float rollGroundHeight1 = map.terrain.getGroundHeight(rollSampleBackwardX, rollSampleBackwardY); + final float rollGroundHeight2 = map.terrain.getGroundHeight(rollSampleForwardX, rollSampleForwardY); + if (standingOnWater) { + pitchSampleGroundHeight1 = Math.max(pitchGroundHeight1, + map.terrain.getWaterHeight(pitchSampleBackwardX, pitchSampleBackwardY)); + pitchSampleGroundHeight2 = Math.max(pitchGroundHeight2, + map.terrain.getWaterHeight(pitchSampleForwardX, pitchSampleForwardY)); + rollSampleGroundHeight1 = Math.max(rollGroundHeight1, + map.terrain.getWaterHeight(rollSampleBackwardX, rollSampleBackwardY)); + rollSampleGroundHeight2 = Math.max(rollGroundHeight2, + map.terrain.getWaterHeight(rollSampleForwardX, rollSampleForwardY)); + } + else { + pitchSampleGroundHeight1 = pitchGroundHeight1; + pitchSampleGroundHeight2 = pitchGroundHeight2; + rollSampleGroundHeight1 = rollGroundHeight1; + rollSampleGroundHeight2 = rollGroundHeight2; + } + } + pitch = Math.max(-maxPitch, Math.min(maxPitch, + (float) Math.atan2(pitchSampleGroundHeight2 - pitchSampleGroundHeight1, sampleRadius * 2))); + roll = Math.max(-maxRoll, Math.min(maxRoll, + (float) Math.atan2(rollSampleGroundHeight2 - rollSampleGroundHeight1, sampleRadius * 2))); + this.instance.rotate(tempQuat.setFromAxisRad(RenderMathUtils.VEC3_UNIT_Y, -pitch)); + this.instance.rotate(tempQuat.setFromAxisRad(RenderMathUtils.VEC3_UNIT_X, roll)); + + map.worldScene.instanceMoved(this.instance, this.location[0], this.location[1]); + if (this.shadow != null) { + this.shadow.move(dx, dy, map.terrain.centerOffset); + this.shadow.setHeightAbsolute(currentWalkableUnder != null, groundHeight + map.imageWalkableZOffset); + } + if (this.selectionCircle != null) { + this.selectionCircle.move(dx, dy, map.terrain.centerOffset); + this.selectionCircle.setHeightAbsolute( + (currentWalkableUnder != null) + || ((movementType == MovementType.FLY) || (movementType == MovementType.HOVER)), + selectionCircleHeight + map.imageWalkableZOffset); + } + this.unitAnimationListenerImpl.update(); + if (!dead && this.simulationUnit.isConstructing()) { + this.instance.setFrameByRatio( + this.simulationUnit.getConstructionProgress() / this.simulationUnit.getUnitType().getBuildTime()); + } + } + + private float getGroundHeightSample(final float groundHeight, final MdxComplexInstance currentWalkableUnder, + final float sampleX, final float sampleY) { + final float sampleGroundHeight; + War3MapViewer.gdxRayHeap.origin.x = sampleX; + War3MapViewer.gdxRayHeap.origin.y = sampleY; + if (currentWalkableUnder.intersectRayWithCollision(War3MapViewer.gdxRayHeap, War3MapViewer.intersectionHeap, + true, true)) { + sampleGroundHeight = War3MapViewer.intersectionHeap.z; + } + else { + sampleGroundHeight = groundHeight; + } + return sampleGroundHeight; + } + + public CUnit getSimulationUnit() { + return this.simulationUnit; + } + + public EnumSet getSecondaryAnimationTags() { + return this.unitAnimationListenerImpl.secondaryAnimationTags; + } + + public void repositioned(final War3MapViewer map) { + final float prevX = this.location[0]; + final float prevY = this.location[1]; + final float simulationX = this.simulationUnit.getX(); + final float simulationY = this.simulationUnit.getY(); + final float dx = simulationX - prevX; + final float dy = simulationY - prevY; + if (this.shadow != null) { + this.shadow.move(dx, dy, map.terrain.centerOffset); + } + if (this.selectionCircle != null) { + this.selectionCircle.move(dx, dy, map.terrain.centerOffset); + } + this.location[0] = this.simulationUnit.getX(); + this.location[1] = this.simulationUnit.getY(); + } + + @Override + public MdxComplexInstance getInstance() { + return this.instance; + } + + @Override + public CWidget getSimulationWidget() { + return this.simulationUnit; + } + + @Override + public boolean isIntersectedOnMeshAlways() { + return this.simulationUnit.getUnitType().isBuilding(); + } + + @Override + public float getSelectionScale() { + return this.selectionScale; + } + + @Override + public float getX() { + return this.location[0]; + } + + @Override + public float getY() { + return this.location[1]; + } + + @Override + public float getZ() { + return this.location[2]; + } + + @Override + public void unassignSelectionCircle() { + this.selectionCircle = null; + } + + @Override + public void assignSelectionCircle(final SplatMover t) { + this.selectionCircle = t; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderUnitTypeData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderUnitTypeData.java new file mode 100644 index 0000000..e6c1c6b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderUnitTypeData.java @@ -0,0 +1,38 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim; + +public class RenderUnitTypeData { + private final float maxPitch; + private final float maxRoll; + private final float sampleRadius; + private final boolean allowCustomTeamColor; + private final int teamColor; + + public RenderUnitTypeData(final float maxPitch, final float maxRoll, final float sampleRadius, + final boolean allowCustomTeamColor, final int teamColor) { + this.maxPitch = maxPitch; + this.maxRoll = maxRoll; + this.sampleRadius = sampleRadius; + this.allowCustomTeamColor = allowCustomTeamColor; + this.teamColor = teamColor; + } + + public float getMaxPitch() { + return this.maxPitch; + } + + public float getMaxRoll() { + return this.maxRoll; + } + + public float getElevationSampleRadius() { + return this.sampleRadius; + } + + public boolean isAllowCustomTeamColor() { + return this.allowCustomTeamColor; + } + + public int getTeamColor() { + return this.teamColor; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderWidget.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderWidget.java new file mode 100644 index 0000000..3eeabd0 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/RenderWidget.java @@ -0,0 +1,162 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim; + +import java.util.EnumSet; +import java.util.LinkedList; +import java.util.Queue; + +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.mdx.Sequence; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.SecondaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.SplatModel.SplatMover; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitAnimationListener; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; + +public interface RenderWidget { + MdxComplexInstance getInstance(); + + CWidget getSimulationWidget(); + + void updateAnimations(War3MapViewer war3MapViewer); + + boolean isIntersectedOnMeshAlways(); + + float getSelectionScale(); + + float getX(); + + float getY(); + + float getZ(); + + void unassignSelectionCircle(); + + void assignSelectionCircle(SplatMover t); + + public static final class UnitAnimationListenerImpl implements CUnitAnimationListener { + private final MdxComplexInstance instance; + protected final EnumSet secondaryAnimationTags = EnumSet + .noneOf(AnimationTokens.SecondaryTag.class); + private final EnumSet recycleSet = EnumSet + .noneOf(AnimationTokens.SecondaryTag.class); + private PrimaryTag currentAnimation; + private EnumSet currentAnimationSecondaryTags = SequenceUtils.EMPTY; + private float currentSpeedRatio; + private boolean currentlyAllowingRarityVariations; + private final Queue animationQueue = new LinkedList<>(); + + public UnitAnimationListenerImpl(final MdxComplexInstance instance) { + this.instance = instance; + } + + @Override + public void addSecondaryTag(final AnimationTokens.SecondaryTag tag) { + if (!secondaryAnimationTags.contains(tag)) { + this.secondaryAnimationTags.add(tag); + if (!animationQueue.isEmpty()) { + final QueuedAnimation nextAnimation = animationQueue.poll(); + playAnimation(true, nextAnimation.animationName, nextAnimation.secondaryAnimationTags, 1.0f, + nextAnimation.allowRarityVariations); + } + else { + playAnimation(true, this.currentAnimation, this.currentAnimationSecondaryTags, + this.currentSpeedRatio, this.currentlyAllowingRarityVariations); + } + } + } + + @Override + public void removeSecondaryTag(final AnimationTokens.SecondaryTag tag) { + if (secondaryAnimationTags.contains(tag)) { + this.secondaryAnimationTags.remove(tag); + playAnimation(true, this.currentAnimation, this.currentAnimationSecondaryTags, this.currentSpeedRatio, + this.currentlyAllowingRarityVariations); + } + } + + @Override + public void playAnimation(final boolean force, final PrimaryTag animationName, + final EnumSet secondaryAnimationTags, final float speedRatio, + final boolean allowRarityVariations) { + this.animationQueue.clear(); + if (force || (animationName != this.currentAnimation) + || !secondaryAnimationTags.equals(this.currentAnimationSecondaryTags)) { + this.currentSpeedRatio = speedRatio; + this.recycleSet.clear(); + this.recycleSet.addAll(this.secondaryAnimationTags); + this.recycleSet.addAll(secondaryAnimationTags); + this.instance.setAnimationSpeed(speedRatio); + if (SequenceUtils.randomSequence(this.instance, animationName, this.recycleSet, + allowRarityVariations) != null) { + this.currentAnimation = animationName; + this.currentAnimationSecondaryTags = secondaryAnimationTags; + this.currentlyAllowingRarityVariations = allowRarityVariations; + } + } + } + + public void playAnimationWithDuration(final boolean force, final PrimaryTag animationName, + final EnumSet secondaryAnimationTags, final float duration, + final boolean allowRarityVariations) { + this.animationQueue.clear(); + if (force || (animationName != this.currentAnimation) + || !secondaryAnimationTags.equals(this.currentAnimationSecondaryTags)) { + this.recycleSet.clear(); + this.recycleSet.addAll(this.secondaryAnimationTags); + this.recycleSet.addAll(secondaryAnimationTags); + final Sequence sequence = SequenceUtils.randomSequence(this.instance, animationName, this.recycleSet, + allowRarityVariations); + if (sequence != null) { + this.currentAnimation = animationName; + this.currentAnimationSecondaryTags = secondaryAnimationTags; + this.currentlyAllowingRarityVariations = allowRarityVariations; + this.currentSpeedRatio = ((sequence.getInterval()[1] - sequence.getInterval()[0]) / 1000.0f) + / duration; + this.instance.setAnimationSpeed(this.currentSpeedRatio); + } + } + } + + @Override + public void queueAnimation(final PrimaryTag animationName, final EnumSet secondaryAnimationTags, + final boolean allowRarityVariations) { + this.animationQueue.add(new QueuedAnimation(animationName, secondaryAnimationTags, allowRarityVariations)); + } + + public void update() { + if (this.instance.sequenceEnded || (this.instance.sequence == -1)) { + // animation done + if ((this.instance.sequence != -1) && (((MdxModel) this.instance.model).getSequences() + .get(this.instance.sequence).getFlags() == 0)) { + // animation is a looping animation + playAnimation(true, this.currentAnimation, this.currentAnimationSecondaryTags, + this.currentSpeedRatio, this.currentlyAllowingRarityVariations); + } + else { + final QueuedAnimation nextAnimation = this.animationQueue.poll(); + if (nextAnimation != null) { + playAnimation(true, nextAnimation.animationName, nextAnimation.secondaryAnimationTags, 1.0f, + nextAnimation.allowRarityVariations); + } + } + } + } + } + + public static final class QueuedAnimation { + private final PrimaryTag animationName; + private final EnumSet secondaryAnimationTags; + private final boolean allowRarityVariations; + + public QueuedAnimation(final PrimaryTag animationName, final EnumSet secondaryAnimationTags, + final boolean allowRarityVariations) { + this.animationName = animationName; + this.secondaryAnimationTags = secondaryAnimationTags; + this.allowRarityVariations = allowRarityVariations; + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/ability/AbilityDataUI.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/ability/AbilityDataUI.java new file mode 100644 index 0000000..42585ef --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/ability/AbilityDataUI.java @@ -0,0 +1,307 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim.ability; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.badlogic.gdx.graphics.Texture; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.units.Element; +import com.etheller.warsmash.units.manager.MutableObjectData; +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; + +public class AbilityDataUI { + // Standard ability icon fields + private static final War3ID ICON_NORMAL_X = War3ID.fromString("abpx"); + private static final War3ID ICON_NORMAL_Y = War3ID.fromString("abpy"); + private static final War3ID ICON_NORMAL = War3ID.fromString("aart"); + private static final War3ID ICON_TURN_OFF = War3ID.fromString("auar"); + private static final War3ID ICON_TURN_OFF_X = War3ID.fromString("aubx"); + private static final War3ID ICON_TURN_OFF_Y = War3ID.fromString("auby"); + private static final War3ID ICON_RESEARCH = War3ID.fromString("arar"); + private static final War3ID ICON_RESEARCH_X = War3ID.fromString("arpx"); + private static final War3ID ICON_RESEARCH_Y = War3ID.fromString("arpy"); + private static final War3ID ABILITY_TIP = War3ID.fromString("atp1"); + private static final War3ID ABILITY_UBER_TIP = War3ID.fromString("aub1"); + private static final War3ID ABILITY_UN_TIP = War3ID.fromString("aut1"); + private static final War3ID ABILITY_UN_UBER_TIP = War3ID.fromString("auu1"); + private static final War3ID ABILITY_RESEARCH_TIP = War3ID.fromString("aret"); + private static final War3ID ABILITY_RESEARCH_UBER_TIP = War3ID.fromString("arut"); + + private static final War3ID CASTER_ART = War3ID.fromString("acat"); + private static final War3ID TARGET_ART = War3ID.fromString("atat"); + private static final War3ID SPECIAL_ART = War3ID.fromString("asat"); + private static final War3ID EFFECT_ART = War3ID.fromString("aeat"); + private static final War3ID AREA_EFFECT_ART = War3ID.fromString("aaea"); + private static final War3ID MISSILE_ART = War3ID.fromString("amat"); + + private static final War3ID UNIT_ICON_NORMAL_X = War3ID.fromString("ubpx"); + private static final War3ID UNIT_ICON_NORMAL_Y = War3ID.fromString("ubpy"); + private static final War3ID UNIT_ICON_NORMAL = War3ID.fromString("uico"); + private static final War3ID UNIT_TIP = War3ID.fromString("utip"); + private static final War3ID UNIT_UBER_TIP = War3ID.fromString("utub"); + + private static final War3ID ITEM_ICON_NORMAL_X = War3ID.fromString("ubpx"); + private static final War3ID ITEM_ICON_NORMAL_Y = War3ID.fromString("ubpy"); + private static final War3ID ITEM_ICON_NORMAL = War3ID.fromString("iico"); + private static final War3ID ITEM_TIP = War3ID.fromString("utip"); + private static final War3ID ITEM_UBER_TIP = War3ID.fromString("utub"); + private static final War3ID ITEM_DESCRIPTION = War3ID.fromString("ides"); + + private static final War3ID UPGRADE_ICON_NORMAL_X = War3ID.fromString("gbpx"); + private static final War3ID UPGRADE_ICON_NORMAL_Y = War3ID.fromString("gbpy"); + private static final War3ID UPGRADE_ICON_NORMAL = War3ID.fromString("gar1"); + private static final War3ID UPGRADE_LEVELS = War3ID.fromString("glvl"); + private static final War3ID UPGRADE_TIP = War3ID.fromString("gtp1"); + private static final War3ID UPGRADE_UBER_TIP = War3ID.fromString("gub1"); + + private final Map rawcodeToUI = new HashMap<>(); + private final Map rawcodeToUnitUI = new HashMap<>(); + private final Map rawcodeToItemUI = new HashMap<>(); + private final Map> rawcodeToUpgradeUI = new HashMap<>(); + private final IconUI moveUI; + private final IconUI stopUI; + private final IconUI holdPosUI; + private final IconUI patrolUI; + private final IconUI attackUI; + private final IconUI attackGroundUI; + private final IconUI buildHumanUI; + private final IconUI buildOrcUI; + private final IconUI buildNightElfUI; + private final IconUI buildUndeadUI; + private final IconUI buildNeutralUI; + private final IconUI buildNagaUI; + private final IconUI cancelUI; + private final IconUI cancelBuildUI; + private final IconUI cancelTrainUI; + private final IconUI rallyUI; + private final IconUI selectSkillUI; + + public AbilityDataUI(final MutableObjectData abilityData, final MutableObjectData unitData, + final MutableObjectData itemData, final MutableObjectData upgradeData, final GameUI gameUI, + final War3MapViewer viewer) { + final String disabledPrefix = gameUI.getSkinField("CommandButtonDisabledArtPath"); + for (final War3ID alias : abilityData.keySet()) { + final MutableGameObject abilityTypeData = abilityData.get(alias); + final String iconResearchPath = gameUI.trySkinField(abilityTypeData.getFieldAsString(ICON_RESEARCH, 0)); + final String iconNormalPath = gameUI.trySkinField(abilityTypeData.getFieldAsString(ICON_NORMAL, 0)); + final String iconTurnOffPath = gameUI.trySkinField(abilityTypeData.getFieldAsString(ICON_TURN_OFF, 0)); + final String iconTip = abilityTypeData.getFieldAsString(ABILITY_TIP, 1); + final String iconUberTip = abilityTypeData.getFieldAsString(ABILITY_UBER_TIP, 1); + final String iconTurnOffTip = abilityTypeData.getFieldAsString(ABILITY_UN_TIP, 1); + final String iconTurnOffUberTip = abilityTypeData.getFieldAsString(ABILITY_UN_UBER_TIP, 1); + final String iconResearchTip = abilityTypeData.getFieldAsString(ABILITY_RESEARCH_TIP, 1); + final String iconResearchUberTip = abilityTypeData.getFieldAsString(ABILITY_RESEARCH_UBER_TIP, 1); + final int iconResearchX = abilityTypeData.getFieldAsInteger(ICON_RESEARCH_X, 0); + final int iconResearchY = abilityTypeData.getFieldAsInteger(ICON_RESEARCH_Y, 0); + final int iconNormalX = abilityTypeData.getFieldAsInteger(ICON_NORMAL_X, 0); + final int iconNormalY = abilityTypeData.getFieldAsInteger(ICON_NORMAL_Y, 0); + final int iconTurnOffX = abilityTypeData.getFieldAsInteger(ICON_TURN_OFF_X, 0); + final int iconTurnOffY = abilityTypeData.getFieldAsInteger(ICON_TURN_OFF_Y, 0); + final Texture iconResearch = gameUI.loadTexture(iconResearchPath); + final Texture iconResearchDisabled = gameUI.loadTexture(disable(iconResearchPath, disabledPrefix)); + final Texture iconNormal = gameUI.loadTexture(iconNormalPath); + final Texture iconNormalDisabled = gameUI.loadTexture(disable(iconNormalPath, disabledPrefix)); + final Texture iconTurnOff = gameUI.loadTexture(iconTurnOffPath); + final Texture iconTurnOffDisabled = gameUI.loadTexture(disable(iconTurnOffPath, disabledPrefix)); + + final List casterArt = Arrays.asList(abilityTypeData.getFieldAsString(CASTER_ART, 0).split(",")); + final List targetArt = Arrays.asList(abilityTypeData.getFieldAsString(TARGET_ART, 0).split(",")); + final List specialArt = Arrays.asList(abilityTypeData.getFieldAsString(SPECIAL_ART, 0).split(",")); + final List effectArt = Arrays.asList(abilityTypeData.getFieldAsString(EFFECT_ART, 0).split(",")); + final List areaEffectArt = Arrays + .asList(abilityTypeData.getFieldAsString(AREA_EFFECT_ART, 0).split(",")); + final List missileArt = Arrays.asList(abilityTypeData.getFieldAsString(MISSILE_ART, 0).split(",")); + + this.rawcodeToUI.put(alias, + new AbilityUI( + new IconUI(iconResearch, iconResearchDisabled, iconResearchX, iconResearchY, + iconResearchTip, iconResearchUberTip), + new IconUI(iconNormal, iconNormalDisabled, iconNormalX, iconNormalY, iconTip, iconUberTip), + new IconUI(iconTurnOff, iconTurnOffDisabled, iconTurnOffX, iconTurnOffY, iconTurnOffTip, + iconTurnOffUberTip), + casterArt, targetArt, specialArt, effectArt, areaEffectArt, missileArt)); + } + for (final War3ID alias : unitData.keySet()) { + final MutableGameObject abilityTypeData = unitData.get(alias); + final String iconNormalPath = gameUI.trySkinField(abilityTypeData.getFieldAsString(UNIT_ICON_NORMAL, 0)); + final int iconNormalX = abilityTypeData.getFieldAsInteger(UNIT_ICON_NORMAL_X, 0); + final int iconNormalY = abilityTypeData.getFieldAsInteger(UNIT_ICON_NORMAL_Y, 0); + final String iconTip = abilityTypeData.getFieldAsString(UNIT_TIP, 0); + final String iconUberTip = abilityTypeData.getFieldAsString(UNIT_UBER_TIP, 0); + final Texture iconNormal = gameUI.loadTexture(iconNormalPath); + final Texture iconNormalDisabled = gameUI.loadTexture(disable(iconNormalPath, disabledPrefix)); + this.rawcodeToUnitUI.put(alias, + new IconUI(iconNormal, iconNormalDisabled, iconNormalX, iconNormalY, iconTip, iconUberTip)); + } + for (final War3ID alias : itemData.keySet()) { + final MutableGameObject abilityTypeData = itemData.get(alias); + final String iconNormalPath = gameUI.trySkinField(abilityTypeData.getFieldAsString(ITEM_ICON_NORMAL, 0)); + final int iconNormalX = abilityTypeData.getFieldAsInteger(ITEM_ICON_NORMAL_X, 0); + final int iconNormalY = abilityTypeData.getFieldAsInteger(ITEM_ICON_NORMAL_Y, 0); + final String iconTip = abilityTypeData.getFieldAsString(ITEM_TIP, 0); + final String iconUberTip = abilityTypeData.getFieldAsString(ITEM_UBER_TIP, 0); + final String iconDescription = abilityTypeData.getFieldAsString(ITEM_DESCRIPTION, 0); + final Texture iconNormal = gameUI.loadTexture(iconNormalPath); + final Texture iconNormalDisabled = gameUI.loadTexture(disable(iconNormalPath, disabledPrefix)); + this.rawcodeToItemUI.put(alias, + new ItemUI( + new IconUI(iconNormal, iconNormalDisabled, iconNormalX, iconNormalY, iconTip, iconUberTip), + abilityTypeData.getName(), iconDescription, iconNormalPath)); + } + for (final War3ID alias : upgradeData.keySet()) { + final MutableGameObject upgradeTypeData = upgradeData.get(alias); + final int upgradeLevels = upgradeTypeData.getFieldAsInteger(UPGRADE_LEVELS, 0); + final int iconNormalX = upgradeTypeData.getFieldAsInteger(UPGRADE_ICON_NORMAL_X, 0); + final int iconNormalY = upgradeTypeData.getFieldAsInteger(UPGRADE_ICON_NORMAL_Y, 0); + final List upgradeIconsByLevel = new ArrayList<>(); + for (int i = 0; i < upgradeLevels; i++) { + final String iconTip = upgradeTypeData.getFieldAsString(UPGRADE_TIP, 0); + final String iconUberTip = upgradeTypeData.getFieldAsString(UPGRADE_UBER_TIP, 0); + final String iconNormalPath = gameUI + .trySkinField(upgradeTypeData.getFieldAsString(UPGRADE_ICON_NORMAL, i)); + final Texture iconNormal = gameUI.loadTexture(iconNormalPath); + final Texture iconNormalDisabled = gameUI.loadTexture(disable(iconNormalPath, disabledPrefix)); + upgradeIconsByLevel.add( + new IconUI(iconNormal, iconNormalDisabled, iconNormalX, iconNormalY, iconTip, iconUberTip)); + } + this.rawcodeToUpgradeUI.put(alias, upgradeIconsByLevel); + } + this.moveUI = createBuiltInIconUI(gameUI, "CmdMove", disabledPrefix); + this.stopUI = createBuiltInIconUI(gameUI, "CmdStop", disabledPrefix); + this.holdPosUI = createBuiltInIconUI(gameUI, "CmdHoldPos", disabledPrefix); + this.patrolUI = createBuiltInIconUI(gameUI, "CmdPatrol", disabledPrefix); + this.attackUI = createBuiltInIconUI(gameUI, "CmdAttack", disabledPrefix); + this.buildHumanUI = createBuiltInIconUI(gameUI, "CmdBuildHuman", disabledPrefix); + this.buildOrcUI = createBuiltInIconUI(gameUI, "CmdBuildOrc", disabledPrefix); + this.buildNightElfUI = createBuiltInIconUI(gameUI, "CmdBuildNightElf", disabledPrefix); + this.buildUndeadUI = createBuiltInIconUI(gameUI, "CmdBuildUndead", disabledPrefix); + this.buildNagaUI = createBuiltInIconUI(gameUI, "CmdBuildNaga", disabledPrefix); + this.buildNeutralUI = createBuiltInIconUI(gameUI, "CmdBuild", disabledPrefix); + this.attackGroundUI = createBuiltInIconUI(gameUI, "CmdAttackGround", disabledPrefix); + this.cancelUI = createBuiltInIconUI(gameUI, "CmdCancel", disabledPrefix); + this.cancelBuildUI = createBuiltInIconUI(gameUI, "CmdCancelBuild", disabledPrefix); + this.cancelTrainUI = createBuiltInIconUI(gameUI, "CmdCancelTrain", disabledPrefix); + this.rallyUI = createBuiltInIconUI(gameUI, "CmdRally", disabledPrefix); + this.selectSkillUI = createBuiltInIconUI(gameUI, "CmdSelectSkill", disabledPrefix); + } + + private IconUI createBuiltInIconUI(final GameUI gameUI, final String key, final String disabledPrefix) { + final Element builtInAbility = gameUI.getSkinData().get(key); + final String iconPath = gameUI.trySkinField(builtInAbility.getField("Art")); + final Texture icon = gameUI.loadTexture(iconPath); + final Texture iconDisabled = gameUI.loadTexture(disable(iconPath, disabledPrefix)); + final int buttonPositionX = builtInAbility.getFieldValue("Buttonpos", 0); + final int buttonPositionY = builtInAbility.getFieldValue("Buttonpos", 1); + final String tip = builtInAbility.getField("Tip"); + final String uberTip = builtInAbility.getField("UberTip"); + return new IconUI(icon, iconDisabled, buttonPositionX, buttonPositionY, tip, uberTip); + } + + public AbilityUI getUI(final War3ID rawcode) { + return this.rawcodeToUI.get(rawcode); + } + + public IconUI getUnitUI(final War3ID rawcode) { + return this.rawcodeToUnitUI.get(rawcode); + } + + public ItemUI getItemUI(final War3ID rawcode) { + return this.rawcodeToItemUI.get(rawcode); + } + + public IconUI getUpgradeUI(final War3ID rawcode, final int level) { + final List upgradeUI = this.rawcodeToUpgradeUI.get(rawcode); + if (upgradeUI != null) { + if (level < upgradeUI.size()) { + return upgradeUI.get(level); + } + else { + return upgradeUI.get(upgradeUI.size() - 1); + } + } + return null; + } + + private static String disable(final String path, final String disabledPrefix) { + final int slashIndex = path.lastIndexOf('\\'); + String name = path; + if (slashIndex != -1) { + name = path.substring(slashIndex + 1); + } + return disabledPrefix + "DIS" + name; + } + + public IconUI getMoveUI() { + return this.moveUI; + } + + public IconUI getStopUI() { + return this.stopUI; + } + + public IconUI getHoldPosUI() { + return this.holdPosUI; + } + + public IconUI getPatrolUI() { + return this.patrolUI; + } + + public IconUI getAttackUI() { + return this.attackUI; + } + + public IconUI getAttackGroundUI() { + return this.attackGroundUI; + } + + public IconUI getBuildHumanUI() { + return this.buildHumanUI; + } + + public IconUI getBuildNightElfUI() { + return this.buildNightElfUI; + } + + public IconUI getBuildOrcUI() { + return this.buildOrcUI; + } + + public IconUI getBuildUndeadUI() { + return this.buildUndeadUI; + } + + public IconUI getBuildNagaUI() { + return this.buildNagaUI; + } + + public IconUI getBuildNeutralUI() { + return this.buildNeutralUI; + } + + public IconUI getCancelUI() { + return this.cancelUI; + } + + public IconUI getCancelBuildUI() { + return this.cancelBuildUI; + } + + public IconUI getCancelTrainUI() { + return this.cancelTrainUI; + } + + public IconUI getRallyUI() { + return this.rallyUI; + } + + public IconUI getSelectSkillUI() { + return this.selectSkillUI; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/ability/AbilityUI.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/ability/AbilityUI.java new file mode 100644 index 0000000..2be75c1 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/ability/AbilityUI.java @@ -0,0 +1,72 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim.ability; + +import java.util.List; + +public class AbilityUI { + private final IconUI learnIconUI; + private final IconUI onIconUI; + private final IconUI offIconUI; + private final List casterArt; + private final List targetArt; + private final List specialArt; + private final List effectArt; + private final List areaEffectArt; + private final List missileArt; + + public AbilityUI(final IconUI learnIconUI, final IconUI onIconUI, final IconUI offIconUI, + final List casterArt, final List targetArt, final List specialArt, + final List effectArt, final List areaEffectArt, final List missileArt) { + this.learnIconUI = learnIconUI; + this.onIconUI = onIconUI; + this.offIconUI = offIconUI; + this.casterArt = casterArt; + this.targetArt = targetArt; + this.specialArt = specialArt; + this.effectArt = effectArt; + this.areaEffectArt = areaEffectArt; + this.missileArt = missileArt; + } + + public IconUI getLearnIconUI() { + return this.learnIconUI; + } + + public IconUI getOnIconUI() { + return this.onIconUI; + } + + public IconUI getOffIconUI() { + return this.offIconUI; + } + + public String getCasterArt(final int index) { + return tryGet(this.casterArt, index); + } + + public String getTargetArt(final int index) { + return tryGet(this.targetArt, index); + } + + public String getSpecialArt(final int index) { + return tryGet(this.specialArt, index); + } + + public String getEffectArt(final int index) { + return tryGet(this.effectArt, index); + } + + public String getAreaEffectArt(final int index) { + return tryGet(this.areaEffectArt, index); + } + + public String getMissileArt(final int index) { + return tryGet(this.missileArt, index); + } + + private static String tryGet(final List items, final int index) { + if (index < items.size()) { + return items.get(index); + } + return items.get(items.size() - 1); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/ability/IconUI.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/ability/IconUI.java new file mode 100644 index 0000000..8d56183 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/ability/IconUI.java @@ -0,0 +1,46 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim.ability; + +import com.badlogic.gdx.graphics.Texture; + +public class IconUI { + private final Texture icon; + private final Texture iconDisabled; + private final int buttonPositionX; + private final int buttonPositionY; + private final String toolTip; + private final String uberTip; + + public IconUI(final Texture icon, final Texture iconDisabled, final int buttonPositionX, final int buttonPositionY, + final String toolTip, final String uberTip) { + this.icon = icon; + this.iconDisabled = iconDisabled; + this.buttonPositionX = buttonPositionX; + this.buttonPositionY = buttonPositionY; + this.toolTip = toolTip; + this.uberTip = uberTip; + } + + public Texture getIcon() { + return this.icon; + } + + public Texture getIconDisabled() { + return this.iconDisabled; + } + + public int getButtonPositionX() { + return this.buttonPositionX; + } + + public int getButtonPositionY() { + return this.buttonPositionY; + } + + public String getToolTip() { + return this.toolTip; + } + + public String getUberTip() { + return this.uberTip; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/ability/ItemUI.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/ability/ItemUI.java new file mode 100644 index 0000000..b1a6a4d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/ability/ItemUI.java @@ -0,0 +1,32 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim.ability; + +public class ItemUI { + private final IconUI iconUI; + private final String name; + private final String description; + private final String itemIconPathForDragging; + + public ItemUI(final IconUI iconUI, final String name, final String description, + final String itemIconPathForDragging) { + this.iconUI = iconUI; + this.name = name; + this.description = description; + this.itemIconPathForDragging = itemIconPathForDragging; + } + + public IconUI getIconUI() { + return this.iconUI; + } + + public String getName() { + return this.name; + } + + public String getDescription() { + return this.description; + } + + public String getItemIconPathForDragging() { + return this.itemIconPathForDragging; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/commandbuttons/AbilityCommandButton.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/commandbuttons/AbilityCommandButton.java new file mode 100644 index 0000000..5f3a73d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/commandbuttons/AbilityCommandButton.java @@ -0,0 +1,94 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim.commandbuttons; + +import com.badlogic.gdx.graphics.Texture; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.ability.AbilityUI; + +public class AbilityCommandButton implements CommandButton { + private final AbilityUI abilityIconUI; + private final int orderId; + + public AbilityCommandButton(final AbilityUI abilityIconUI, final int orderId) { + this.abilityIconUI = abilityIconUI; + this.orderId = orderId; + } + + @Override + public String getToolTip() { + return null; + } + + @Override + public String getUberTip() { + return null; + } + + @Override + public int getLumberCost() { + return 0; + } + + @Override + public int getGoldCost() { + return 0; + } + + @Override + public int getManaCost() { + return 0; + } + + @Override + public int getFoodCost() { + return 0; + } + + @Override + public Texture getIcon() { + return this.abilityIconUI.getOnIconUI().getIcon(); + } + + @Override + public Texture getDisabledIcon() { + return this.abilityIconUI.getOnIconUI().getIconDisabled(); + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public float getCooldown() { + return 0; + } + + @Override + public float getCooldownRemaining() { + return 0; + } + + @Override + public boolean isAutoCastCapable() { + return false; + } + + @Override + public boolean isAutoCastActive() { + return false; + } + + @Override + public int getButtonPositionX() { + return this.abilityIconUI.getOnIconUI().getButtonPositionX(); + } + + @Override + public int getButtonPositionY() { + return this.abilityIconUI.getOnIconUI().getButtonPositionY(); + } + + @Override + public int getOrderId() { + return this.orderId; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/commandbuttons/BasicCommandButton.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/commandbuttons/BasicCommandButton.java new file mode 100644 index 0000000..c9cc906 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/commandbuttons/BasicCommandButton.java @@ -0,0 +1,95 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim.commandbuttons; + +import com.badlogic.gdx.graphics.Texture; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.ability.IconUI; + +public class BasicCommandButton implements CommandButton { + private final IconUI iconUI; + private final int orderId; + + public BasicCommandButton(final IconUI iconUI, final int orderId) { + this.iconUI = iconUI; + this.orderId = orderId; + } + + @Override + public String getToolTip() { + return null; + } + + @Override + public String getUberTip() { + return null; + } + + @Override + public int getLumberCost() { + return 0; + } + + @Override + public int getGoldCost() { + return 0; + } + + @Override + public int getManaCost() { + return 0; + } + + @Override + public int getFoodCost() { + return 0; + } + + @Override + public Texture getIcon() { + return this.iconUI.getIcon(); + } + + @Override + public Texture getDisabledIcon() { + return this.iconUI.getIconDisabled(); + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public float getCooldown() { + return 0; + } + + @Override + public float getCooldownRemaining() { + return 0; + } + + @Override + public boolean isAutoCastCapable() { + return false; + } + + @Override + public boolean isAutoCastActive() { + return false; + } + + @Override + public int getButtonPositionX() { + return this.iconUI.getButtonPositionX(); + } + + @Override + public int getButtonPositionY() { + return this.iconUI.getButtonPositionY(); + } + + @Override + public int getOrderId() { + return this.orderId; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/commandbuttons/CommandButton.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/commandbuttons/CommandButton.java new file mode 100644 index 0000000..af4e9c9 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/commandbuttons/CommandButton.java @@ -0,0 +1,38 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim.commandbuttons; + +import com.badlogic.gdx.graphics.Texture; + +public interface CommandButton { + String getToolTip(); + + String getUberTip(); + + int getLumberCost(); + + int getGoldCost(); + + int getManaCost(); + + int getFoodCost(); + + Texture getIcon(); + + Texture getDisabledIcon(); + + boolean isEnabled(); + + float getCooldown(); + + float getCooldownRemaining(); + + boolean isAutoCastCapable(); + + boolean isAutoCastActive(); + + int getButtonPositionX(); + + int getButtonPositionY(); + + int getOrderId(); + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/commandbuttons/CommandButtonListener.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/commandbuttons/CommandButtonListener.java new file mode 100644 index 0000000..ad00601 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/commandbuttons/CommandButtonListener.java @@ -0,0 +1,40 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim.commandbuttons; + +import com.badlogic.gdx.graphics.Texture; + +public interface CommandButtonListener { +// String getToolTip(); +// +// String getUberTip(); +// +// int getLumberCost(); +// +// int getGoldCost(); +// +// int getManaCost(); +// +// int getFoodCost(); +// +// Texture getIcon(); +// +// Texture getDisabledIcon(); +// +// boolean isEnabled(); +// +// float getCooldown(); +// +// float getCooldownRemaining(); +// +// boolean isAutoCastCapable(); +// +// boolean isAutoCastActive(); +// +// int getButtonPositionX(); +// +// int getButtonPositionY(); +// +// int getOrderId(); + void commandButton(int buttonPositionX, int buttonPositionY, Texture icon, int abilityHandleId, int orderId, + int autoCastOrderId, boolean active, boolean autoCastActive, boolean menuButton, String tip, String uberTip, + int goldCost, int lumberCost, int foodCost); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/commandbuttons/CommandCardPopulatingAbilityVisitor.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/commandbuttons/CommandCardPopulatingAbilityVisitor.java new file mode 100644 index 0000000..fd181d2 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/rendersim/commandbuttons/CommandCardPopulatingAbilityVisitor.java @@ -0,0 +1,344 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.rendersim.commandbuttons; + +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.ability.AbilityDataUI; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.ability.AbilityUI; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.ability.IconUI; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityAttack; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityGeneric; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityMove; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.AbstractCAbilityBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityBuildInProgress; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityHumanBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityNagaBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityNeutralBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityNightElfBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityOrcBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityUndeadBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.combat.CAbilityColdArrows; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic.GenericNoIconAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic.GenericSingleIconActiveAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.hero.CAbilityHero; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.queue.CAbilityQueue; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.queue.CAbilityRally; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttack; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.ResourceType; + +public class CommandCardPopulatingAbilityVisitor implements CAbilityVisitor { + public static final CommandCardPopulatingAbilityVisitor INSTANCE = new CommandCardPopulatingAbilityVisitor(); + private CSimulation game; + private CUnit unit; + + private CommandButtonListener commandButtonListener; + private AbilityDataUI abilityDataUI; + private int menuBaseOrderId; + private boolean hasStop; + private final CommandCardActivationReceiverPreviewCallback previewCallback = new CommandCardActivationReceiverPreviewCallback(); + private GameUI gameUI; + + public CommandCardPopulatingAbilityVisitor reset(final CSimulation game, final GameUI gameUI, final CUnit unit, + final CommandButtonListener commandButtonListener, final AbilityDataUI abilityDataUI, + final int menuBaseOrderId) { + this.game = game; + this.gameUI = gameUI; + this.unit = unit; + this.commandButtonListener = commandButtonListener; + this.abilityDataUI = abilityDataUI; + this.menuBaseOrderId = menuBaseOrderId; + this.hasStop = false; + return this; + } + + @Override + public Void accept(final CAbilityAttack ability) { + if ((this.menuBaseOrderId == 0) && ability.isIconShowing()) { + addCommandButton(ability, this.abilityDataUI.getAttackUI(), ability.getHandleId(), OrderIds.attack, 0, + false, false); + boolean attackGroundEnabled = false; + for (final CUnitAttack attack : this.unit.getAttacks()) { + if (attack.getWeaponType().isAttackGroundSupported()) { + attackGroundEnabled = true; + break; + } + } + if (attackGroundEnabled) { + addCommandButton(ability, this.abilityDataUI.getAttackGroundUI(), ability.getHandleId(), + OrderIds.attackground, 0, false, false); + } + if (!this.hasStop) { + this.hasStop = true; + addCommandButton(ability, this.abilityDataUI.getStopUI(), 0, OrderIds.stop, 0, false, false); + } + } + return null; + } + + @Override + public Void accept(final CAbilityMove ability) { + if ((this.menuBaseOrderId == 0) && ability.isIconShowing()) { + addCommandButton(ability, this.abilityDataUI.getMoveUI(), ability.getHandleId(), OrderIds.move, 0, false, + false); + addCommandButton(ability, this.abilityDataUI.getHoldPosUI(), 0, OrderIds.holdposition, 0, false, false); + addCommandButton(ability, this.abilityDataUI.getPatrolUI(), ability.getHandleId(), OrderIds.patrol, 0, + false, false); + if (!this.hasStop) { + this.hasStop = true; + addCommandButton(ability, this.abilityDataUI.getStopUI(), 0, OrderIds.stop, 0, false, false); + } + } + return null; + } + + @Override + public Void accept(final CAbilityGeneric ability) { + if ((this.menuBaseOrderId == 0) && ability.isIconShowing()) { + final AbilityUI abilityUI = this.abilityDataUI.getUI(ability.getRawcode()); + if (abilityUI != null) { + addCommandButton(ability, abilityUI.getOnIconUI(), ability.getHandleId(), 0, 0, false, false); + } + else { + addCommandButton(ability, this.abilityDataUI.getStopUI(), ability.getHandleId(), 0, 0, false, false); + } + } + return null; + } + + @Override + public Void accept(final GenericSingleIconActiveAbility ability) { + if ((this.menuBaseOrderId == 0) && ability.isIconShowing()) { + final AbilityUI ui = this.abilityDataUI.getUI(ability.getAlias()); + addCommandButton(ability, ability.isToggleOn() ? ui.getOffIconUI() : ui.getOnIconUI(), + ability.getHandleId(), ability.getBaseOrderId(), 0, false, false); + } + return null; + } + + @Override + public Void accept(final GenericNoIconAbility ability) { + return null; + } + + @Override + public Void accept(final CAbilityRally ability) { + if (this.menuBaseOrderId == 0) { + addCommandButton(ability, this.abilityDataUI.getRallyUI(), ability.getHandleId(), ability.getBaseOrderId(), + 0, false, false); + } + return null; + } + + @Override + public Void accept(final CAbilityColdArrows ability) { + if ((this.menuBaseOrderId == 0) && ability.isIconShowing()) { + final boolean autoCastActive = ability.isAutoCastActive(); + int autoCastId; + if (autoCastActive) { + autoCastId = OrderIds.coldarrows; + } + else { + autoCastId = OrderIds.uncoldarrows; + } + final IconUI onIconUI = this.abilityDataUI.getUI(ability.getRawcode()).getOnIconUI(); + addCommandButton(ability, onIconUI, ability.getHandleId(), OrderIds.coldarrowstarg, autoCastId, + autoCastActive, false); + } + return null; + } + + @Override + public Void accept(final CAbilityOrcBuild ability) { + handleBuildMenu(ability, this.abilityDataUI.getBuildOrcUI()); + return null; + } + + @Override + public Void accept(final CAbilityHumanBuild ability) { + handleBuildMenu(ability, this.abilityDataUI.getBuildHumanUI()); + return null; + } + + @Override + public Void accept(final CAbilityNightElfBuild ability) { + handleBuildMenu(ability, this.abilityDataUI.getBuildNightElfUI()); + return null; + } + + @Override + public Void accept(final CAbilityUndeadBuild ability) { + handleBuildMenu(ability, this.abilityDataUI.getBuildUndeadUI()); + return null; + } + + @Override + public Void accept(final CAbilityNagaBuild ability) { + handleBuildMenu(ability, this.abilityDataUI.getBuildNagaUI()); + return null; + } + + @Override + public Void accept(final CAbilityNeutralBuild ability) { + handleBuildMenu(ability, this.abilityDataUI.getBuildNeutralUI()); + return null; + } + + private void handleBuildMenu(final AbstractCAbilityBuild ability, final IconUI buildUI) { + if ((this.menuBaseOrderId == ability.getBaseOrderId()) && ability.isIconShowing()) { + for (final War3ID unitType : ability.getStructuresBuilt()) { + final IconUI unitUI = this.abilityDataUI.getUnitUI(unitType); + if (unitUI != null) { + final CUnitType simulationUnitType = this.game.getUnitData().getUnitType(unitType); + addCommandButton(ability, unitUI, ability.getHandleId(), unitType.getValue(), 0, false, false, + simulationUnitType.getGoldCost(), simulationUnitType.getLumberCost(), + simulationUnitType.getFoodUsed()); + } + } + } + else { + addCommandButton(ability, buildUI, ability.getHandleId(), ability.getBaseOrderId(), 0, false, true); + } + } + + private void addCommandButton(final CAbility ability, final IconUI iconUI, final int handleId, final int orderId, + final int autoCastOrderId, final boolean autoCastActive, final boolean menuButton) { + addCommandButton(ability, iconUI, handleId, orderId, autoCastOrderId, autoCastActive, menuButton, 0, 0, 0); + } + + private void addCommandButton(final CAbility ability, final IconUI iconUI, final int handleId, final int orderId, + final int autoCastOrderId, final boolean autoCastActive, final boolean menuButton, int goldCost, + int lumberCost, int foodCost) { + ability.checkCanUse(this.game, this.unit, orderId, this.previewCallback.reset()); + final boolean active = ((this.unit.getCurrentBehavior() != null) + && (orderId == this.unit.getCurrentBehavior().getHighlightOrderId())); + final boolean disabled = ((ability != null) && ability.isDisabled()) || this.previewCallback.disabled; + String uberTip = iconUI.getUberTip(); + if (disabled) { + // dont show these on disabled + goldCost = 0; + lumberCost = 0; + foodCost = 0; + } + if (this.previewCallback.isShowingRequirements()) { + uberTip = this.previewCallback.getRequirementsText() + "|r" + uberTip; + } + this.commandButtonListener.commandButton(iconUI.getButtonPositionX(), iconUI.getButtonPositionY(), + disabled ? iconUI.getIconDisabled() : iconUI.getIcon(), handleId, disabled ? 0 : orderId, + autoCastOrderId, active, autoCastActive, menuButton, iconUI.getToolTip(), uberTip, goldCost, lumberCost, + foodCost); + } + + @Override + public Void accept(final CAbilityBuildInProgress ability) { + if (this.menuBaseOrderId == 0) { + addCommandButton(ability, this.abilityDataUI.getCancelBuildUI(), ability.getHandleId(), OrderIds.cancel, 0, + false, false); + } + return null; + } + + @Override + public Void accept(final CAbilityQueue ability) { + if ((this.menuBaseOrderId == 0) && ability.isIconShowing()) { + for (final War3ID unitType : ability.getUnitsTrained()) { + final IconUI unitUI = this.abilityDataUI.getUnitUI(unitType); + if (unitUI != null) { + final CUnitType simulationUnitType = this.game.getUnitData().getUnitType(unitType); + addCommandButton(ability, unitUI, ability.getHandleId(), unitType.getValue(), 0, false, false, + simulationUnitType.getGoldCost(), simulationUnitType.getLumberCost(), + simulationUnitType.getFoodUsed()); + } + } + for (final War3ID unitType : ability.getResearchesAvailable()) { + final CPlayer player = this.game.getPlayer(this.unit.getPlayerIndex()); + final IconUI unitUI = this.abilityDataUI.getUpgradeUI(unitType, player.getTechtreeUnlocked(unitType)); + if (unitUI != null) { + addCommandButton(ability, unitUI, ability.getHandleId(), unitType.getValue(), 0, false, false); + } + } + if (this.unit.getBuildQueueTypes()[0] != null) { + addCommandButton(ability, this.abilityDataUI.getCancelTrainUI(), ability.getHandleId(), OrderIds.cancel, + 0, false, false); + } + } + return null; + } + + private final class CommandCardActivationReceiverPreviewCallback implements AbilityActivationReceiver { + private boolean disabled; + private final StringBuilder requirementsTextBuilder = new StringBuilder(); + + public CommandCardActivationReceiverPreviewCallback reset() { + this.disabled = false; + this.requirementsTextBuilder.setLength(0); + return this; + } + + @Override + public void useOk() { + + } + + @Override + public void notEnoughResources(final ResourceType resource) { + + } + + @Override + public void notAnActiveAbility() { + + } + + @Override + public void missingRequirement(final War3ID type, final int level) { + this.disabled = true; + if (this.requirementsTextBuilder.length() == 0) { + this.requirementsTextBuilder.append(CommandCardPopulatingAbilityVisitor.this.gameUI.getTemplates() + .getDecoratedString("REQUIRESTOOLTIP")); + this.requirementsTextBuilder.append("|n - "); + } + else { + this.requirementsTextBuilder.append(" - "); + } + final CUnitType unitType = CommandCardPopulatingAbilityVisitor.this.game.getUnitData().getUnitType(type); + this.requirementsTextBuilder + .append(unitType == null ? "NOTEXTERN Unknown ('" + type + "')" : unitType.getName()); + this.requirementsTextBuilder.append("|n"); + } + + @Override + public void casterMovementDisabled() { + + } + + @Override + public void cargoCapacityUnavailable() { + } + + @Override + public void disabled() { + this.disabled = true; + } + + public boolean isShowingRequirements() { + return this.requirementsTextBuilder.length() != 0; + } + + public String getRequirementsText() { + return this.requirementsTextBuilder.toString(); + } + } + + @Override + public Void accept(final CAbilityHero ability) { + // TODO + return null; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CDestructable.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CDestructable.java new file mode 100644 index 0000000..d5f1e8f --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CDestructable.java @@ -0,0 +1,83 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.RemovablePathingMapInstance; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderWidget.UnitAnimationListenerImpl; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public class CDestructable extends CWidget { + + private final CDestructableType destType; + private final RemovablePathingMapInstance pathingInstance; + private final RemovablePathingMapInstance pathingInstanceDeath; + private UnitAnimationListenerImpl unitAnimationListenerImpl; + + public CDestructable(final int handleId, final float x, final float y, final float life, + final CDestructableType destTypeInstance, final RemovablePathingMapInstance pathingInstance, + final RemovablePathingMapInstance pathingInstanceDeath) { + super(handleId, x, y, life); + this.destType = destTypeInstance; + this.pathingInstance = pathingInstance; + this.pathingInstanceDeath = pathingInstanceDeath; + } + + @Override + public float getFlyHeight() { + return 0; + } + + @Override + public float getImpactZ() { + return 0; // TODO maybe from DestructableType + } + + @Override + public void damage(final CSimulation simulation, final CUnit source, final CAttackType attackType, + final String weaponType, final float damage) { + final boolean wasDead = isDead(); + this.life -= damage; + if (!wasDead && isDead()) { + if (this.pathingInstance != null) { + this.pathingInstance.remove(); + } + if (this.pathingInstanceDeath != null) { + this.pathingInstanceDeath.add(); + } + } + simulation.destructableDamageEvent(this, weaponType, this.destType.getArmorType()); + } + + @Override + public boolean canBeTargetedBy(final CSimulation simulation, final CUnit source, + final EnumSet targetsAllowed) { + if (targetsAllowed.containsAll(this.destType.getTargetedAs())) { + if (isDead()) { + return targetsAllowed.contains(CTargetType.DEAD); + } + else { + return !targetsAllowed.contains(CTargetType.DEAD) || targetsAllowed.contains(CTargetType.ALIVE); + } + } + else { + System.err.println("Not targeting because " + targetsAllowed + " does not contain all of " + + this.destType.getTargetedAs()); + } + return false; + } + + @Override + public T visit(final AbilityTargetVisitor visitor) { + return visitor.accept(this); + } + + public CDestructableType getDestType() { + return this.destType; + } + + public void setUnitAnimationListener(final UnitAnimationListenerImpl unitAnimationListenerImpl) { + this.unitAnimationListenerImpl = unitAnimationListenerImpl; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CDestructableType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CDestructableType.java new file mode 100644 index 0000000..c13bea6 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CDestructableType.java @@ -0,0 +1,57 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +import java.awt.image.BufferedImage; +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public class CDestructableType { + + private final String name; + private final float life; + private final EnumSet targetedAs; + private final String armorType; + private final int buildTime; + private final BufferedImage pathingPixelMap; + private final BufferedImage pathingDeathPixelMap; + + public CDestructableType(final String name, final float life, final EnumSet targetedAs, + final String armorType, final int buildTime, final BufferedImage pathingPixelMap, + final BufferedImage pathingDeathPixelMap) { + this.name = name; + this.life = life; + this.targetedAs = targetedAs; + this.armorType = armorType; + this.buildTime = buildTime; + this.pathingPixelMap = pathingPixelMap; + this.pathingDeathPixelMap = pathingDeathPixelMap; + } + + public String getName() { + return this.name; + } + + public float getLife() { + return this.life; + } + + public EnumSet getTargetedAs() { + return this.targetedAs; + } + + public String getArmorType() { + return this.armorType; + } + + public int getBuildTime() { + return this.buildTime; + } + + public BufferedImage getPathingPixelMap() { + return this.pathingPixelMap; + } + + public BufferedImage getPathingDeathPixelMap() { + return this.pathingDeathPixelMap; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CGameplayConstants.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CGameplayConstants.java new file mode 100644 index 0000000..303fd7d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CGameplayConstants.java @@ -0,0 +1,404 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +import java.util.Arrays; + +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.units.Element; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CDefenseType; + +/** + * Stores some gameplay constants at runtime in a java object (symbol table) to + * maybe be faster than a map. + */ +public class CGameplayConstants { + private final float attackHalfAngle; + private final float[][] damageBonusTable; + private final float maxCollisionRadius; + private final float decayTime; + private final float boneDecayTime; + private final float bulletDeathTime; + private final float closeEnoughRange; + private final float dawnTimeGameHours; + private final float duskTimeGameHours; + private final float gameDayHours; + private final float gameDayLength; + private final float structureDecayTime; + private final float buildingAngle; + private final float rootAngle; + + private final float defenseArmor; + + private final int heroExpRange; + + private final int maxHeroLevel; + private final int maxUnitLevel; + private final int[] needHeroXp; + private final int[] needHeroXpSum; + private final int[] grantHeroXp; + private final int[] grantNormalXp; + private final int[] heroFactorXp; + private final float summonedKillFactor; + private final float strAttackBonus; + private final float strHitPointBonus; + private final float strRegenBonus; + private final float intManaBonus; + private final float intRegenBonus; + private final float agiDefenseBonus; + private final float agiDefenseBase; + private final int agiMoveBonus; + private final float agiAttackSpeedBonus; + + private final int needHeroXPFormulaA; + private final int needHeroXPFormulaB; + private final int needHeroXPFormulaC; + private final int grantHeroXPFormulaA; + private final int grantHeroXPFormulaB; + private final int grantHeroXPFormulaC; + private final int grantNormalXPFormulaA; + private final int grantNormalXPFormulaB; + private final int grantNormalXPFormulaC; + + private final int heroAbilityLevelSkip; + + private final boolean globalExperience; + private final boolean maxLevelHeroesDrainExp; + private final boolean buildingKillsGiveExp; + + private final float dropItemRange; + private final float giveItemRange; + private final float pickupItemRange; + private final float pawnItemRange; + private final float pawnItemRate; + + private final float followRange; + private final float structureFollowRange; + private final float followItemRange; + private final float spellCastRangeBuffer; + + public CGameplayConstants(final DataTable parsedDataTable) { + final Element miscData = parsedDataTable.get("Misc"); + // TODO use radians for half angle + this.attackHalfAngle = (float) Math.toDegrees(miscData.getFieldFloatValue("AttackHalfAngle")); + this.maxCollisionRadius = miscData.getFieldFloatValue("MaxCollisionRadius"); + this.decayTime = miscData.getFieldFloatValue("DecayTime"); + this.boneDecayTime = miscData.getFieldFloatValue("BoneDecayTime"); + this.structureDecayTime = miscData.getFieldFloatValue("StructureDecayTime"); + this.bulletDeathTime = miscData.getFieldFloatValue("BulletDeathTime"); + this.closeEnoughRange = miscData.getFieldFloatValue("CloseEnoughRange"); + + this.dawnTimeGameHours = miscData.getFieldFloatValue("Dawn"); + this.duskTimeGameHours = miscData.getFieldFloatValue("Dusk"); + this.gameDayHours = miscData.getFieldFloatValue("DayHours"); + this.gameDayLength = miscData.getFieldFloatValue("DayLength"); + + this.buildingAngle = miscData.getFieldFloatValue("BuildingAngle"); + this.rootAngle = miscData.getFieldFloatValue("RootAngle"); + + final CDefenseType[] defenseTypeOrder = { CDefenseType.SMALL, CDefenseType.MEDIUM, CDefenseType.LARGE, + CDefenseType.FORT, CDefenseType.NORMAL, CDefenseType.HERO, CDefenseType.DIVINE, CDefenseType.NONE, }; + this.damageBonusTable = new float[CAttackType.values().length][defenseTypeOrder.length]; + for (int i = 0; i < CAttackType.VALUES.length; i++) { + Arrays.fill(this.damageBonusTable[i], 1.0f); + final CAttackType attackType = CAttackType.VALUES[i]; + final String damageBonus = miscData.getField("DamageBonus" + attackType.getDamageKey()); + final String[] damageComponents = damageBonus.split(","); + for (int j = 0; j < damageComponents.length; j++) { + if (damageComponents[j].length() > 0) { + final CDefenseType defenseType = defenseTypeOrder[j]; + try { + this.damageBonusTable[i][defenseType.ordinal()] = Float.parseFloat(damageComponents[j]); +// System.out.println(attackType + ":" + defenseType + ": " + damageComponents[j]); + } + catch (final NumberFormatException e) { + throw new RuntimeException("DamageBonus" + attackType.getDamageKey(), e); + } + } + } + } + + this.defenseArmor = miscData.getFieldFloatValue("DefenseArmor"); + + this.globalExperience = miscData.getFieldValue("GlobalExperience") != 0; + this.maxLevelHeroesDrainExp = miscData.getFieldValue("MaxLevelHeroesDrainExp") != 0; + this.buildingKillsGiveExp = miscData.getFieldValue("BuildingKillsGiveExp") != 0; + + this.heroExpRange = miscData.getFieldValue("HeroExpRange"); + + this.maxHeroLevel = miscData.getFieldValue("MaxHeroLevel"); + this.maxUnitLevel = miscData.getFieldValue("MaxUnitLevel"); + + this.needHeroXPFormulaA = miscData.getFieldValue("NeedHeroXPFormulaA"); + this.needHeroXPFormulaB = miscData.getFieldValue("NeedHeroXPFormulaB"); + this.needHeroXPFormulaC = miscData.getFieldValue("NeedHeroXPFormulaC"); + this.grantHeroXPFormulaA = miscData.getFieldValue("GrantHeroXPFormulaA"); + this.grantHeroXPFormulaB = miscData.getFieldValue("GrantHeroXPFormulaB"); + this.grantHeroXPFormulaC = miscData.getFieldValue("GrantHeroXPFormulaC"); + this.grantNormalXPFormulaA = miscData.getFieldValue("GrantNormalXPFormulaA"); + this.grantNormalXPFormulaB = miscData.getFieldValue("GrantNormalXPFormulaB"); + this.grantNormalXPFormulaC = miscData.getFieldValue("GrantNormalXPFormulaC"); + + this.needHeroXp = parseTable(miscData.getField("NeedHeroXP"), this.needHeroXPFormulaA, this.needHeroXPFormulaB, + this.needHeroXPFormulaC, this.maxHeroLevel); + this.needHeroXpSum = new int[this.needHeroXp.length]; + for (int i = 0; i < this.needHeroXpSum.length; i++) { + if (i == 0) { + this.needHeroXpSum[i] = this.needHeroXp[i]; + } + else { + this.needHeroXpSum[i] = this.needHeroXp[i] + this.needHeroXpSum[i - 1]; + } + } + this.grantHeroXp = parseTable(miscData.getField("GrantHeroXP"), this.grantHeroXPFormulaA, + this.grantHeroXPFormulaB, this.grantHeroXPFormulaC, this.maxHeroLevel); + this.grantNormalXp = parseTable(miscData.getField("GrantNormalXP"), this.grantNormalXPFormulaA, + this.grantNormalXPFormulaB, this.grantNormalXPFormulaC, this.maxUnitLevel); + this.heroFactorXp = parseIntArray(miscData.getField("HeroFactorXP")); + this.summonedKillFactor = miscData.getFieldFloatValue("SummonedKillFactor"); + this.strAttackBonus = miscData.getFieldFloatValue("StrAttackBonus"); + this.strHitPointBonus = miscData.getFieldFloatValue("StrHitPointBonus"); + this.strRegenBonus = miscData.getFieldFloatValue("StrRegenBonus"); + this.intManaBonus = miscData.getFieldFloatValue("IntManaBonus"); + this.intRegenBonus = miscData.getFieldFloatValue("IntRegenBonus"); + this.agiDefenseBonus = miscData.getFieldFloatValue("AgiDefenseBonus"); + this.agiDefenseBase = miscData.getFieldFloatValue("AgiDefenseBase"); + this.agiMoveBonus = miscData.getFieldValue("AgiMoveBonus"); + this.agiAttackSpeedBonus = miscData.getFieldFloatValue("AgiAttackSpeedBonus"); + + this.heroAbilityLevelSkip = miscData.getFieldValue("HeroAbilityLevelSkip"); + + this.dropItemRange = miscData.getFieldFloatValue("DropItemRange"); + this.giveItemRange = miscData.getFieldFloatValue("GiveItemRange"); + this.pickupItemRange = miscData.getFieldFloatValue("PickupItemRange"); + this.pawnItemRange = miscData.getFieldFloatValue("PawnItemRange"); + this.pawnItemRate = miscData.getFieldFloatValue("PawnItemRate"); + + this.followRange = miscData.getFieldFloatValue("FollowRange"); + this.structureFollowRange = miscData.getFieldFloatValue("StructureFollowRange"); + this.followItemRange = miscData.getFieldFloatValue("FollowItemRange"); + + this.spellCastRangeBuffer = miscData.getFieldFloatValue("SpellCastRangeBuffer"); + } + + public float getAttackHalfAngle() { + return this.attackHalfAngle; + } + + public float getDamageRatioAgainst(final CAttackType attackType, final CDefenseType defenseType) { + return this.damageBonusTable[attackType.ordinal()][defenseType.ordinal()]; + } + + public float getMaxCollisionRadius() { + return this.maxCollisionRadius; + } + + public float getDecayTime() { + return this.decayTime; + } + + public float getBoneDecayTime() { + return this.boneDecayTime; + } + + public float getBulletDeathTime() { + return this.bulletDeathTime; + } + + public float getCloseEnoughRange() { + return this.closeEnoughRange; + } + + public float getGameDayHours() { + return this.gameDayHours; + } + + public float getGameDayLength() { + return this.gameDayLength; + } + + public float getDawnTimeGameHours() { + return this.dawnTimeGameHours; + } + + public float getDuskTimeGameHours() { + return this.duskTimeGameHours; + } + + public float getStructureDecayTime() { + return this.structureDecayTime; + } + + public float getBuildingAngle() { + return this.buildingAngle; + } + + public float getRootAngle() { + return this.rootAngle; + } + + public float getDefenseArmor() { + return this.defenseArmor; + } + + public boolean isGlobalExperience() { + return this.globalExperience; + } + + public boolean isMaxLevelHeroesDrainExp() { + return this.maxLevelHeroesDrainExp; + } + + public boolean isBuildingKillsGiveExp() { + return this.buildingKillsGiveExp; + } + + public int getHeroAbilityLevelSkip() { + return this.heroAbilityLevelSkip; + } + + public int getHeroExpRange() { + return this.heroExpRange; + } + + public int getMaxHeroLevel() { + return this.maxHeroLevel; + } + + public int getMaxUnitLevel() { + return this.maxUnitLevel; + } + + public float getSummonedKillFactor() { + return this.summonedKillFactor; + } + + public float getStrAttackBonus() { + return this.strAttackBonus; + } + + public float getStrHitPointBonus() { + return this.strHitPointBonus; + } + + public float getStrRegenBonus() { + return this.strRegenBonus; + } + + public float getIntManaBonus() { + return this.intManaBonus; + } + + public float getIntRegenBonus() { + return this.intRegenBonus; + } + + public float getAgiDefenseBonus() { + return this.agiDefenseBonus; + } + + public float getAgiDefenseBase() { + return this.agiDefenseBase; + } + + public int getAgiMoveBonus() { + return this.agiMoveBonus; + } + + public float getAgiAttackSpeedBonus() { + return this.agiAttackSpeedBonus; + } + + public float getHeroFactorXp(final int level) { + return getTableValue(this.heroFactorXp, level) / 100f; + } + + public int getNeedHeroXP(final int level) { + return getTableValue(this.needHeroXp, level); + } + + public int getNeedHeroXPSum(final int level) { + return getTableValue(this.needHeroXpSum, level); + } + + public int getGrantHeroXP(final int level) { + return getTableValue(this.grantHeroXp, level); + } + + public int getGrantNormalXP(final int level) { + return getTableValue(this.grantNormalXp, level); + } + + public float getDropItemRange() { + return this.dropItemRange; + } + + public float getPickupItemRange() { + return this.pickupItemRange; + } + + public float getGiveItemRange() { + return this.giveItemRange; + } + + public float getPawnItemRange() { + return this.pawnItemRange; + } + + public float getPawnItemRate() { + return this.pawnItemRate; + } + + public float getFollowRange() { + return this.followRange; + } + + public float getStructureFollowRange() { + return this.structureFollowRange; + } + + public float getFollowItemRange() { + return this.followItemRange; + } + + public float getSpellCastRangeBuffer() { + return this.spellCastRangeBuffer; + } + + private static int getTableValue(final int[] table, int level) { + if (level <= 0) { + return 0; + } + if (level > table.length) { + level = table.length; + } + return table[level - 1]; + } + + /* + * This incorporates the function "f(x)" documented both on + * http://classic.battle.net/war3/basics/heroes.shtml and also on MiscGame.txt. + */ + private static int[] parseTable(final String txt, final int formulaA, final int formulaB, final int formulaC, + final int tableSize) { + final String[] splitTxt = txt.split(","); + final int[] result = new int[tableSize]; + for (int i = 0; i < tableSize; i++) { + if (i < splitTxt.length) { + result[i] = Integer.parseInt(splitTxt[i]); + } + else { + result[i] = (formulaA * result[i - 1]) + (formulaB * i) + formulaC; + } + } + return result; + } + + private static int[] parseIntArray(final String txt) { + final String[] splitTxt = txt.split(","); + final int[] result = new int[splitTxt.length]; + for (int i = 0; i < splitTxt.length; i++) { + result[i] = Integer.parseInt(splitTxt[i]); + } + return result; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CItem.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CItem.java new file mode 100644 index 0000000..2548e4d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CItem.java @@ -0,0 +1,103 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +import java.util.EnumSet; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.MovementType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public class CItem extends CWidget { + private final War3ID typeId; + private final CItemType itemType; + private boolean hidden; + + public CItem(final int handleId, final float x, final float y, final float life, final War3ID typeId, + final CItemType itemTypeInstance) { + super(handleId, x, y, life); + this.typeId = typeId; + this.itemType = itemTypeInstance; + } + + @Override + public float getFlyHeight() { + return 0; + } + + @Override + public float getImpactZ() { + return 0; // TODO probably from ItemType + } + + @Override + public void damage(final CSimulation simulation, final CUnit source, final CAttackType attackType, + final String weaponType, final float damage) { + this.life -= damage; + simulation.itemDamageEvent(this, weaponType, this.itemType.getArmorType()); + } + + @Override + public boolean canBeTargetedBy(final CSimulation simulation, final CUnit source, + final EnumSet targetsAllowed) { + return targetsAllowed.contains(CTargetType.ITEM); + } + + public void setX(final float x, final CWorldCollision collision) { + super.setX(x); + } + + public void setY(final float y, final CWorldCollision collision) { + super.setY(y); + } + + @Override + public T visit(final AbilityTargetVisitor visitor) { + return visitor.accept(this); + } + + public War3ID getTypeId() { + return this.typeId; + } + + public CItemType getItemType() { + return this.itemType; + } + + public void setHidden(final boolean hidden) { + this.hidden = hidden; + } + + public boolean isHidden() { + return this.hidden; + } + + public void setPointAndCheckUnstuck(final float newX, final float newY, final CSimulation game) { + final CWorldCollision collision = game.getWorldCollision(); + final PathingGrid pathingGrid = game.getPathingGrid(); + ; + float outputX = newX, outputY = newY; + int checkX = 0; + int checkY = 0; + float collisionSize; + tempRect.setSize(16, 16); + collisionSize = 16; + for (int i = 0; i < 300; i++) { + final float centerX = newX + (checkX * 64); + final float centerY = newY + (checkY * 64); + tempRect.setCenter(centerX, centerY); + if (pathingGrid.isPathable(centerX, centerY, MovementType.FOOT, collisionSize)) { + outputX = centerX; + outputY = centerY; + break; + } + final double angle = ((((int) Math.floor(Math.sqrt((4 * i) + 1))) % 4) * Math.PI) / 2; + checkX -= (int) Math.cos(angle); + checkY -= (int) Math.sin(angle); + } + setX(outputX); + setY(outputY); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CItemType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CItemType.java new file mode 100644 index 0000000..47de954 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CItemType.java @@ -0,0 +1,156 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +import java.util.List; + +import com.etheller.warsmash.util.War3ID; + +public class CItemType { + private final List abilityList; + private final War3ID cooldownGroup; + private final boolean ignoreCooldown; + private final int numberOfCharges; + private final boolean activelyUsed; + private final boolean perishable; + private final boolean useAutomaticallyWhenAcquired; + private final int goldCost; + private final int lumberCost; + private final int stockMax; + private final int stockReplenishInterval; + private final int stockStartDelay; + private final int hitPoints; + private final String armorType; + private final int level; + private final int levelUnclassified; + private final int priority; + private final boolean sellable; + private final boolean pawnable; + private final boolean droppedWhenCarrierDies; + private final boolean canBeDropped; + private final boolean validTargetForTransformation; + private final boolean includeAsRandomChoice; + + public CItemType(final List abilityList, final War3ID cooldownGroup, final boolean ignoreCooldown, + final int numberOfCharges, final boolean activelyUsed, final boolean perishable, + final boolean useAutomaticallyWhenAcquired, final int goldCost, final int lumberCost, final int stockMax, + final int stockReplenishInterval, final int stockStartDelay, final int hitPoints, final String armorType, + final int level, final int levelUnclassified, final int priority, final boolean sellable, + final boolean pawnable, final boolean droppedWhenCarrierDies, final boolean canBeDropped, + final boolean validTargetForTransformation, final boolean includeAsRandomChoice) { + this.abilityList = abilityList; + this.cooldownGroup = cooldownGroup; + this.ignoreCooldown = ignoreCooldown; + this.numberOfCharges = numberOfCharges; + this.activelyUsed = activelyUsed; + this.perishable = perishable; + this.useAutomaticallyWhenAcquired = useAutomaticallyWhenAcquired; + this.goldCost = goldCost; + this.lumberCost = lumberCost; + this.stockMax = stockMax; + this.stockReplenishInterval = stockReplenishInterval; + this.stockStartDelay = stockStartDelay; + this.hitPoints = hitPoints; + this.armorType = armorType; + this.level = level; + this.levelUnclassified = levelUnclassified; + this.priority = priority; + this.sellable = sellable; + this.pawnable = pawnable; + this.droppedWhenCarrierDies = droppedWhenCarrierDies; + this.canBeDropped = canBeDropped; + this.validTargetForTransformation = validTargetForTransformation; + this.includeAsRandomChoice = includeAsRandomChoice; + } + + public List getAbilityList() { + return this.abilityList; + } + + public War3ID getCooldownGroup() { + return this.cooldownGroup; + } + + public boolean isIgnoreCooldown() { + return this.ignoreCooldown; + } + + public int getNumberOfCharges() { + return this.numberOfCharges; + } + + public boolean isActivelyUsed() { + return this.activelyUsed; + } + + public boolean isPerishable() { + return this.perishable; + } + + public boolean isUseAutomaticallyWhenAcquired() { + return this.useAutomaticallyWhenAcquired; + } + + public int getGoldCost() { + return this.goldCost; + } + + public int getLumberCost() { + return this.lumberCost; + } + + public int getStockMax() { + return this.stockMax; + } + + public int getStockReplenishInterval() { + return this.stockReplenishInterval; + } + + public int getStockStartDelay() { + return this.stockStartDelay; + } + + public int getHitPoints() { + return this.hitPoints; + } + + public String getArmorType() { + return this.armorType; + } + + public int getLevel() { + return this.level; + } + + public int getLevelUnclassified() { + return this.levelUnclassified; + } + + public int getPriority() { + return this.priority; + } + + public boolean isSellable() { + return this.sellable; + } + + public boolean isPawnable() { + return this.pawnable; + } + + public boolean isDroppedWhenCarrierDies() { + return this.droppedWhenCarrierDies; + } + + public boolean isCanBeDropped() { + return this.canBeDropped; + } + + public boolean isValidTargetForTransformation() { + return this.validTargetForTransformation; + } + + public boolean isIncludeAsRandomChoice() { + return this.includeAsRandomChoice; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CPlayerStateListener.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CPlayerStateListener.java new file mode 100644 index 0000000..3fbea2e --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CPlayerStateListener.java @@ -0,0 +1,44 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +import com.etheller.warsmash.util.SubscriberSetNotifier; + +public interface CPlayerStateListener { + void goldChanged(); + + void lumberChanged(); + + void foodChanged(); + + void upkeepChanged(); + + public static final class CPlayerStateNotifier extends SubscriberSetNotifier + implements CPlayerStateListener { + @Override + public void goldChanged() { + for (final CPlayerStateListener listener : set) { + listener.goldChanged(); + } + } + + @Override + public void lumberChanged() { + for (final CPlayerStateListener listener : set) { + listener.lumberChanged(); + } + } + + @Override + public void foodChanged() { + for (final CPlayerStateListener listener : set) { + listener.foodChanged(); + } + } + + @Override + public void upkeepChanged() { + for (final CPlayerStateListener listener : set) { + listener.upkeepChanged(); + } + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CSimulation.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CSimulation.java new file mode 100644 index 0000000..e472ce9 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CSimulation.java @@ -0,0 +1,415 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +import java.awt.geom.Point2D; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Random; + +import com.badlogic.gdx.math.Rectangle; +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.units.manager.MutableObjectData; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.RemovablePathingMapInstance; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorMove; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackInstant; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackListener; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackMissile; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.projectile.CAttackProjectile; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.config.CBasePlayer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.config.CPlayerAPI; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.config.War3MapConfig; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.config.War3MapConfigStartLoc; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.data.CAbilityData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.data.CDestructableData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.data.CItemData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.data.CUnitData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.pathing.CPathfindingProcessor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CAllianceType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CRace; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.region.CRegionManager; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.timers.CTimer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.ResourceType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.SimulationRenderController; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.CommandErrorListener; + +public class CSimulation implements CPlayerAPI { + private final CAbilityData abilityData; + private final CUnitData unitData; + private final CDestructableData destructableData; + private final CItemData itemData; + private final List units; + private final List newUnits; + private final List destructables; + private final List items; + private final List players; + private final List projectiles; + private final List newProjectiles; + private final HandleIdAllocator handleIdAllocator; + private transient final SimulationRenderController simulationRenderController; + private int gameTurnTick = 0; + private final PathingGrid pathingGrid; + private final CWorldCollision worldCollision; + private final CPathfindingProcessor[] pathfindingProcessors; + private final List[] playerHeroes; + private final CGameplayConstants gameplayConstants; + private final Random seededRandom; + private float currentGameDayTimeElapsed; + private final Map handleIdToUnit = new HashMap<>(); + private final Map handleIdToDestructable = new HashMap<>(); + private final Map handleIdToItem = new HashMap<>(); + private final Map handleIdToAbility = new HashMap<>(); + private final LinkedList activeTimers = new LinkedList<>(); + private transient CommandErrorListener commandErrorListener; + private final CRegionManager regionManager; + + public CSimulation(final War3MapConfig config, final DataTable miscData, final MutableObjectData parsedUnitData, + final MutableObjectData parsedItemData, final MutableObjectData parsedDestructableData, + final MutableObjectData parsedAbilityData, final SimulationRenderController simulationRenderController, + final PathingGrid pathingGrid, final Rectangle entireMapBounds, final Random seededRandom, + final CommandErrorListener commandErrorListener) { + this.gameplayConstants = new CGameplayConstants(miscData); + this.simulationRenderController = simulationRenderController; + this.pathingGrid = pathingGrid; + this.abilityData = new CAbilityData(parsedAbilityData); + this.unitData = new CUnitData(this.gameplayConstants, parsedUnitData, this.abilityData, + this.simulationRenderController); + this.destructableData = new CDestructableData(parsedDestructableData, simulationRenderController); + this.itemData = new CItemData(parsedItemData); + this.units = new ArrayList<>(); + this.newUnits = new ArrayList<>(); + this.destructables = new ArrayList<>(); + this.items = new ArrayList<>(); + this.projectiles = new ArrayList<>(); + this.newProjectiles = new ArrayList<>(); + this.handleIdAllocator = new HandleIdAllocator(); + this.worldCollision = new CWorldCollision(entireMapBounds, this.gameplayConstants.getMaxCollisionRadius()); + this.regionManager = new CRegionManager(entireMapBounds, pathingGrid); + this.pathfindingProcessors = new CPathfindingProcessor[WarsmashConstants.MAX_PLAYERS]; + this.playerHeroes = new ArrayList[WarsmashConstants.MAX_PLAYERS]; + for (int i = 0; i < WarsmashConstants.MAX_PLAYERS; i++) { + this.pathfindingProcessors[i] = new CPathfindingProcessor(pathingGrid, this.worldCollision); + this.playerHeroes[i] = new ArrayList<>(); + } + this.seededRandom = seededRandom; + this.players = new ArrayList<>(); + for (int i = 0; i < WarsmashConstants.MAX_PLAYERS; i++) { + final CBasePlayer configPlayer = config.getPlayer(i); + final War3MapConfigStartLoc startLoc = config.getStartLoc(configPlayer.getStartLocationIndex()); + final CPlayer newPlayer = new CPlayer(CRace.OTHER, new float[] { startLoc.getX(), startLoc.getY() }, + configPlayer); + this.players.add(newPlayer); + } + this.players.get(this.players.size() - 4).setName(miscData.getLocalizedString("WESTRING_PLAYER_NA")); + this.players.get(this.players.size() - 3).setName(miscData.getLocalizedString("WESTRING_PLAYER_NV")); + this.players.get(this.players.size() - 2).setName(miscData.getLocalizedString("WESTRING_PLAYER_NE")); + final CPlayer neutralPassive = this.players.get(this.players.size() - 1); + neutralPassive.setName(miscData.getLocalizedString("WESTRING_PLAYER_NP")); + + for (int i = 0; i < WarsmashConstants.MAX_PLAYERS; i++) { + final CPlayer cPlayer = this.players.get(i); + cPlayer.setAlliance(neutralPassive, CAllianceType.PASSIVE, true); + neutralPassive.setAlliance(cPlayer, CAllianceType.PASSIVE, true); + } + + this.commandErrorListener = commandErrorListener; + + } + + public CUnitData getUnitData() { + return this.unitData; + } + + public CAbilityData getAbilityData() { + return this.abilityData; + } + + public CDestructableData getDestructableData() { + return this.destructableData; + } + + public CItemData getItemData() { + return this.itemData; + } + + public List getUnits() { + return this.units; + } + + public List getDestructables() { + return this.destructables; + } + + public void registerTimer(final CTimer timer) { + final ListIterator listIterator = this.activeTimers.listIterator(); + while (listIterator.hasNext()) { + final CTimer nextTimer = listIterator.next(); + if (nextTimer.getEngineFireTick() > timer.getEngineFireTick()) { + listIterator.previous(); + listIterator.add(timer); + } + } + this.activeTimers.add(timer); + } + + public void unregisterTimer(final CTimer time) { + this.activeTimers.remove(time); + } + + public CUnit createUnit(final War3ID typeId, final int playerIndex, final float x, final float y, + final float facing, final BufferedImage buildingPathingPixelMap, + final RemovablePathingMapInstance pathingInstance) { + final CUnit unit = this.unitData.create(this, playerIndex, typeId, x, y, facing, buildingPathingPixelMap, + this.handleIdAllocator, pathingInstance); + this.newUnits.add(unit); + this.handleIdToUnit.put(unit.getHandleId(), unit); + this.worldCollision.addUnit(unit); + return unit; + } + + public CDestructable createDestructable(final War3ID typeId, final float x, final float y, + final RemovablePathingMapInstance pathingInstance, final RemovablePathingMapInstance pathingInstanceDeath) { + final CDestructable dest = this.destructableData.create(this, typeId, x, y, this.handleIdAllocator, + pathingInstance, pathingInstanceDeath); + this.handleIdToDestructable.put(dest.getHandleId(), dest); + this.destructables.add(dest); + return dest; + } + + public CItem createItem(final War3ID alias, final float unitX, final float unitY) { + final CItem item = this.itemData.create(this, alias, unitX, unitY, this.handleIdAllocator.createId()); + this.handleIdToItem.put(item.getHandleId(), item); + this.items.add(item); + return item; + } + + public CUnit createUnit(final War3ID typeId, final int playerIndex, final float x, final float y, + final float facing) { + return this.simulationRenderController.createUnit(this, typeId, playerIndex, x, y, facing); + } + + public CUnit getUnit(final int handleId) { + return this.handleIdToUnit.get(handleId); + } + + public CAbility getAbility(final int handleId) { + return this.handleIdToAbility.get(handleId); + } + + protected void onAbilityAddedToUnit(final CUnit unit, final CAbility ability) { + this.handleIdToAbility.put(ability.getHandleId(), ability); + } + + public CAttackProjectile createProjectile(final CUnit source, final float launchX, final float launchY, + final float launchFacing, final CUnitAttackMissile attack, final AbilityTarget target, final float damage, + final int bounceIndex, final CUnitAttackListener attackListener) { + final CAttackProjectile projectile = this.simulationRenderController.createAttackProjectile(this, launchX, + launchY, launchFacing, source, attack, target, damage, bounceIndex, attackListener); + this.newProjectiles.add(projectile); + return projectile; + } + + public void createInstantAttackEffect(final CUnit source, final CUnitAttackInstant attack, final CWidget target) { + this.simulationRenderController.createInstantAttackEffect(this, source, attack, target); + } + + public PathingGrid getPathingGrid() { + return this.pathingGrid; + } + + public void findNaiveSlowPath(final CUnit ignoreIntersectionsWithThisUnit, + final CUnit ignoreIntersectionsWithThisSecondUnit, final float startX, final float startY, + final Point2D.Float goal, final PathingGrid.MovementType movementType, final float collisionSize, + final boolean allowSmoothing, final CBehaviorMove queueItem) { + final int playerIndex = queueItem.getUnit().getPlayerIndex(); + this.pathfindingProcessors[playerIndex].findNaiveSlowPath(ignoreIntersectionsWithThisUnit, + ignoreIntersectionsWithThisSecondUnit, startX, startY, goal, movementType, collisionSize, + allowSmoothing, queueItem); + } + + public void removeFromPathfindingQueue(final CBehaviorMove behaviorMove) { + final int playerIndex = behaviorMove.getUnit().getPlayerIndex(); + this.pathfindingProcessors[playerIndex].removeFromPathfindingQueue(behaviorMove); + } + + public void update() { + final Iterator unitIterator = this.units.iterator(); + while (unitIterator.hasNext()) { + final CUnit unit = unitIterator.next(); + if (unit.update(this)) { + unitIterator.remove(); + for (final CAbility ability : unit.getAbilities()) { + this.handleIdToAbility.remove(ability.getHandleId()); + } + this.handleIdToUnit.remove(unit.getHandleId()); + this.simulationRenderController.removeUnit(unit); + this.playerHeroes[unit.getPlayerIndex()].remove(unit); + } + } + finishAddingNewUnits(); + final Iterator projectileIterator = this.projectiles.iterator(); + while (projectileIterator.hasNext()) { + final CAttackProjectile projectile = projectileIterator.next(); + if (projectile.update(this)) { + projectileIterator.remove(); + } + } + this.projectiles.addAll(this.newProjectiles); + this.newProjectiles.clear(); + for (final CPathfindingProcessor pathfindingProcessor : this.pathfindingProcessors) { + pathfindingProcessor.update(this); + } + this.gameTurnTick++; + this.currentGameDayTimeElapsed = (this.currentGameDayTimeElapsed + WarsmashConstants.SIMULATION_STEP_TIME) + % this.gameplayConstants.getGameDayLength(); + while (!this.activeTimers.isEmpty() && (this.activeTimers.peek().getEngineFireTick() <= this.gameTurnTick)) { + this.activeTimers.pop().fire(this); + } + + } + + private void finishAddingNewUnits() { + this.units.addAll(this.newUnits); + this.newUnits.clear(); + } + + public float getGameTimeOfDay() { + return (this.currentGameDayTimeElapsed / this.gameplayConstants.getGameDayLength()) + * this.gameplayConstants.getGameDayHours(); + } + + public int getGameTurnTick() { + return this.gameTurnTick; + } + + public CWorldCollision getWorldCollision() { + return this.worldCollision; + } + + public CRegionManager getRegionManager() { + return this.regionManager; + } + + public CGameplayConstants getGameplayConstants() { + return this.gameplayConstants; + } + + public Random getSeededRandom() { + return this.seededRandom; + } + + public void unitDamageEvent(final CUnit damagedUnit, final String weaponSound, final String armorType) { + this.simulationRenderController.spawnDamageSound(damagedUnit, weaponSound, armorType); + } + + public void destructableDamageEvent(final CDestructable damagedDestructable, final String weaponSound, + final String armorType) { + this.simulationRenderController.spawnDamageSound(damagedDestructable, weaponSound, armorType); + } + + public void itemDamageEvent(final CItem damageItem, final String weaponSound, final String armorType) { + this.simulationRenderController.spawnDamageSound(damageItem, weaponSound, armorType); + } + + public void unitConstructedEvent(final CUnit constructingUnit, final CUnit constructedStructure) { + this.simulationRenderController.spawnUnitConstructionSound(constructingUnit, constructedStructure); + } + + @Override + public CPlayer getPlayer(final int index) { + return this.players.get(index); + } + + public CommandErrorListener getCommandErrorListener(final int playerIndex) { + return this.commandErrorListener; + } + + public void unitConstructFinishEvent(final CUnit constructedStructure) { + final CPlayer player = getPlayer(constructedStructure.getPlayerIndex()); + player.addTechtreeUnlocked(constructedStructure.getTypeId()); + this.simulationRenderController.spawnUnitConstructionFinishSound(constructedStructure); + } + + public void createBuildingDeathEffect(final CUnit cUnit) { + this.simulationRenderController.spawnBuildingDeathEffect(cUnit); + } + + public HandleIdAllocator getHandleIdAllocator() { + return this.handleIdAllocator; + } + + public void unitTrainedEvent(final CUnit trainingUnit, final CUnit trainedUnit) { + this.simulationRenderController.spawnUnitReadySound(trainedUnit); + } + + public void unitRepositioned(final CUnit cUnit) { + this.simulationRenderController.unitRepositioned(cUnit); + } + + public void unitGainResourceEvent(final CUnit unit, final ResourceType resourceType, final int amount) { + this.simulationRenderController.spawnGainResourceTextTag(unit, resourceType, amount); + } + + public void unitGainLevelEvent(final CUnit unit) { + this.simulationRenderController.spawnGainLevelEffect(unit); + } + + public void heroCreateEvent(final CUnit hero) { + this.playerHeroes[hero.getPlayerIndex()].add(hero); + } + + public void unitPickUpItemEvent(final CUnit cUnit, final CItem item) { + this.simulationRenderController.spawnUIUnitGetItemSound(cUnit, item); + } + + public void unitDropItemEvent(final CUnit cUnit, final CItem item) { + this.simulationRenderController.spawnUIUnitDropItemSound(cUnit, item); + } + + public List getPlayerHeroes(final int playerIndex) { + return this.playerHeroes[playerIndex]; + } + + public void unitsLoaded() { + // called on startup after the system loads the map's units layer, but not any + // custom scripts yet + finishAddingNewUnits(); + for (final CUnit unit : this.units) { + final CPlayer player = this.players.get(unit.getPlayerIndex()); + player.setUnitFoodUsed(unit, unit.getUnitType().getFoodUsed()); + player.setUnitFoodMade(unit, unit.getUnitType().getFoodMade()); + player.addTechtreeUnlocked(unit.getTypeId()); + } + } + + public CWidget getWidget(final int handleId) { + final CUnit unit = this.handleIdToUnit.get(handleId); + if (unit != null) { + return unit; + } + final CDestructable destructable = this.handleIdToDestructable.get(handleId); + if (destructable != null) { + return destructable; + } + final CItem item = this.handleIdToItem.get(handleId); + if (item != null) { + return item; + } + return null; + } + + public void createEffectOnUnit(final CUnit unit, final String effectPath) { + this.simulationRenderController.spawnEffectOnUnit(unit, effectPath); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CSimulationMapData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CSimulationMapData.java new file mode 100644 index 0000000..21c2147 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CSimulationMapData.java @@ -0,0 +1,10 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +/** + * Provides limited access to game map data when necessary for the game + * simulation logic. Do not add methods to this to query anything that isn't + * going to be network sync'ed. + */ +public interface CSimulationMapData { + short getTerrainPathing(float x, float y); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnit.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnit.java new file mode 100644 index 0000000..48fd227 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnit.java @@ -0,0 +1,1559 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Set; + +import com.badlogic.gdx.math.Rectangle; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.RemovablePathingMapInstance; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitStateListener.CUnitStateNotifier; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityBuildInProgress; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.hero.CAbilityHero; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.inventory.CAbilityInventory; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.mine.CAbilityGoldMine; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorAttack; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorAttackListener; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorFollow; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorHoldPosition; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorMove; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorPatrol; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorStop; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttack; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.COrder; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.COrderNoTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.COrderTargetPoint; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.COrderTargetWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CAllianceType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.region.CRegion; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.region.CRegionEnumFunction; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.region.CRegionManager; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.BooleanAbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.BooleanAbilityTargetCheckReceiver; + +public class CUnit extends CWidget { + private static RegionCheckerImpl regionCheckerImpl = new RegionCheckerImpl(); + + private War3ID typeId; + private float facing; // degrees + private float mana; + private int maximumLife; + private int maximumMana; + private int speed; + private int agilityDefenseBonus; + private int permanentDefenseBonus; + private int temporaryDefenseBonus; + + private int currentDefenseDisplay; + private int currentDefense; + + private int cooldownEndTime = 0; + private float flyHeight; + private int playerIndex; + + private final List abilities = new ArrayList<>(); + + private CBehavior currentBehavior; + private final Queue orderQueue = new LinkedList<>(); + private final CUnitType unitType; + + private Rectangle collisionRectangle; + private RemovablePathingMapInstance pathingInstance; + + private final EnumSet classifications = EnumSet.noneOf(CUnitClassification.class); + + private int deathTurnTick; + private boolean corpse; + private boolean boneCorpse; + + private transient CUnitAnimationListener unitAnimationListener; + + // if you use triggers for this then the transient tag here becomes really + // questionable -- it already was -- but I meant for those to inform us + // which fields shouldn't be persisted if we do game state save later + private transient CUnitStateNotifier stateNotifier = new CUnitStateNotifier(); + private final float acquisitionRange; + private transient static AutoAttackTargetFinderEnum autoAttackTargetFinderEnum = new AutoAttackTargetFinderEnum(); + + private transient CBehaviorMove moveBehavior; + private transient CBehaviorAttack attackBehavior; + private transient CBehaviorFollow followBehavior; + private transient CBehaviorPatrol patrolBehavior; + private transient CBehaviorStop stopBehavior; + private transient CBehaviorHoldPosition holdPositionBehavior; + private boolean constructing = false; + private float constructionProgress; + private boolean hidden = false; + private boolean paused = false; + private boolean acceptingOrders = true; + private boolean invulnerable = false; + private CBehavior defaultBehavior; + private COrder lastStartedOrder = null; + private CUnit workerInside; + private final War3ID[] buildQueue = new War3ID[WarsmashConstants.BUILD_QUEUE_SIZE]; + private final QueueItemType[] buildQueueTypes = new QueueItemType[WarsmashConstants.BUILD_QUEUE_SIZE]; + private boolean queuedUnitFoodPaid = false; + private AbilityTarget rallyPoint; + + private int foodMade; + private int foodUsed; + + private List unitSpecificAttacks; + private transient Set containingRegions = new LinkedHashSet<>(); + private transient Set priorContainingRegions = new LinkedHashSet<>(); + + public CUnit(final int handleId, final int playerIndex, final float x, final float y, final float life, + final War3ID typeId, final float facing, final float mana, final int maximumLife, final int maximumMana, + final int speed, final CUnitType unitType, final RemovablePathingMapInstance pathingInstance) { + super(handleId, x, y, life); + this.playerIndex = playerIndex; + this.typeId = typeId; + this.facing = facing; + this.mana = mana; + this.maximumLife = maximumLife; + this.maximumMana = maximumMana; + this.speed = speed; + this.pathingInstance = pathingInstance; + this.flyHeight = unitType.getDefaultFlyingHeight(); + this.unitType = unitType; + this.classifications.addAll(unitType.getClassifications()); + this.acquisitionRange = unitType.getDefaultAcquisitionRange(); + this.stopBehavior = new CBehaviorStop(this); + this.defaultBehavior = this.stopBehavior; + this.currentBehavior = this.defaultBehavior; + } + + private void computeDerivedFields() { + this.currentDefenseDisplay = this.unitType.getDefense() + this.agilityDefenseBonus + this.permanentDefenseBonus; + this.currentDefense = this.currentDefenseDisplay + this.temporaryDefenseBonus; + } + + public void setAgilityDefenseBonus(final int agilityDefenseBonus) { + this.agilityDefenseBonus = agilityDefenseBonus; + computeDerivedFields(); + } + + public void setPermanentDefenseBonus(final int permanentDefenseBonus) { + this.permanentDefenseBonus = permanentDefenseBonus; + computeDerivedFields(); + } + + public void setTemporaryDefenseBonus(final int temporaryDefenseBonus) { + this.temporaryDefenseBonus = temporaryDefenseBonus; + computeDerivedFields(); + } + + public int getTemporaryDefenseBonus() { + return this.temporaryDefenseBonus; + } + + public int getCurrentDefenseDisplay() { + return this.currentDefenseDisplay; + } + + public void setUnitAnimationListener(final CUnitAnimationListener unitAnimationListener) { + this.unitAnimationListener = unitAnimationListener; + this.unitAnimationListener.playAnimation(true, PrimaryTag.STAND, SequenceUtils.EMPTY, 1.0f, true); + } + + public CUnitAnimationListener getUnitAnimationListener() { + return this.unitAnimationListener; + } + + public void add(final CSimulation simulation, final CAbility ability) { + this.abilities.add(ability); + simulation.onAbilityAddedToUnit(this, ability); + ability.onAdd(simulation, this); + } + + public War3ID getTypeId() { + return this.typeId; + } + + /** + * @return facing in DEGREES + */ + public float getFacing() { + return this.facing; + } + + public float getMana() { + return this.mana; + } + + public int getMaximumLife() { + return this.maximumLife; + } + + public int getMaximumMana() { + return this.maximumMana; + } + + public void setTypeId(final War3ID typeId) { + this.typeId = typeId; + } + + public void setFacing(final float facing) { + // java modulo output can be negative, but not if we + // force positive and modulo again + this.facing = ((facing % 360) + 360) % 360; + } + + public void setMana(final float mana) { + this.mana = mana; + } + + public void setMaximumLife(final int maximumLife) { + this.maximumLife = maximumLife; + } + + public void setMaximumMana(final int maximumMana) { + this.maximumMana = maximumMana; + } + + public void setSpeed(final int speed) { + this.speed = speed; + } + + public int getSpeed() { + return this.speed; + } + + /** + * Updates one tick of simulation logic and return true if it's time to remove + * this unit from the game. + */ + public boolean update(final CSimulation game) { + if (isDead()) { + if (this.collisionRectangle != null) { + // Moved this here because doing it on "kill" was able to happen in some cases + // while also iterating over the units that are in the collision system, and + // then it hit the "writing while iterating" problem. + game.getWorldCollision().removeUnit(this); + } + final int gameTurnTick = game.getGameTurnTick(); + if (!this.corpse) { + if (gameTurnTick > (this.deathTurnTick + + (int) (this.unitType.getDeathTime() / WarsmashConstants.SIMULATION_STEP_TIME))) { + this.corpse = true; + if (!this.unitType.isRaise()) { + this.boneCorpse = true; + // start final phase immediately for "cant raise" case + } + if (!this.unitType.isDecay()) { + // if we dont raise AND dont decay, then now that death anim is over + // we just delete the unit + return true; + } + this.deathTurnTick = gameTurnTick; + } + } + else if (!this.boneCorpse) { + if (game.getGameTurnTick() > (this.deathTurnTick + (int) (game.getGameplayConstants().getDecayTime() + / WarsmashConstants.SIMULATION_STEP_TIME))) { + this.boneCorpse = true; + this.deathTurnTick = gameTurnTick; + } + } + else if (game.getGameTurnTick() > (this.deathTurnTick + + (int) (getEndingDecayTime(game) / WarsmashConstants.SIMULATION_STEP_TIME))) { + return true; + } + } + else if (!this.paused) { + if ((this.rallyPoint != this) && (this.rallyPoint instanceof CUnit) && ((CUnit) this.rallyPoint).isDead()) { + setRallyPoint(this); + } + if (this.constructing) { + this.constructionProgress += WarsmashConstants.SIMULATION_STEP_TIME; + final int buildTime = this.unitType.getBuildTime(); + final float healthGain = (WarsmashConstants.SIMULATION_STEP_TIME / buildTime) + * (this.maximumLife * (1.0f - WarsmashConstants.BUILDING_CONSTRUCT_START_LIFE)); + setLife(game, Math.min(this.life + healthGain, this.maximumLife)); + if (this.constructionProgress >= buildTime) { + this.constructing = false; + this.constructionProgress = 0; + popoutWorker(game); + final Iterator abilityIterator = this.abilities.iterator(); + while (abilityIterator.hasNext()) { + final CAbility ability = abilityIterator.next(); + if (ability instanceof CAbilityBuildInProgress) { + abilityIterator.remove(); + } + else { + ability.setDisabled(false); + ability.setIconShowing(true); + } + } + if (this.unitType.getFoodMade() != 0) { + final CPlayer player = game.getPlayer(this.playerIndex); + player.setFoodCap(player.getFoodCap() + this.unitType.getFoodMade()); + } + game.unitConstructFinishEvent(this); + this.stateNotifier.ordersChanged(); + } + } + else { + final War3ID queuedRawcode = this.buildQueue[0]; + if (queuedRawcode != null) { + // queue step forward + if (this.queuedUnitFoodPaid) { + this.constructionProgress += WarsmashConstants.SIMULATION_STEP_TIME; + } + else { + if (this.buildQueueTypes[0] == QueueItemType.UNIT) { + final CPlayer player = game.getPlayer(this.playerIndex); + final CUnitType trainedUnitType = game.getUnitData().getUnitType(queuedRawcode); + final int newFoodUsed = player.getFoodUsed() + trainedUnitType.getFoodUsed(); + if (newFoodUsed <= player.getFoodCap()) { + player.setFoodUsed(newFoodUsed); + this.queuedUnitFoodPaid = true; + } + } + else { + this.queuedUnitFoodPaid = true; + System.err.println( + "Unpaid food for non unit queue item ???? Attempting to correct this by setting paid=true"); + } + } + if (this.buildQueueTypes[0] == QueueItemType.UNIT) { + final CUnitType trainedUnitType = game.getUnitData().getUnitType(queuedRawcode); + if (this.constructionProgress >= trainedUnitType.getBuildTime()) { + this.constructionProgress = 0; + final CUnit trainedUnit = game.createUnit(queuedRawcode, this.playerIndex, getX(), getY(), + game.getGameplayConstants().getBuildingAngle()); + // dont add food cost to player 2x + trainedUnit.setFoodUsed(trainedUnitType.getFoodUsed()); + final CPlayer player = game.getPlayer(this.playerIndex); + player.setUnitFoodMade(trainedUnit, trainedUnitType.getFoodMade()); + player.addTechtreeUnlocked(queuedRawcode); + // nudge the trained unit out around us + trainedUnit.nudgeAround(game, this); + game.unitTrainedEvent(this, trainedUnit); + if (this.rallyPoint != null) { + final int rallyOrderId = OrderIds.smart; + this.rallyPoint.visit( + UseAbilityOnTargetByIdVisitor.INSTANCE.reset(game, trainedUnit, rallyOrderId)); + } + for (int i = 0; i < (this.buildQueue.length - 1); i++) { + setBuildQueueItem(game, i, this.buildQueue[i + 1], this.buildQueueTypes[i + 1]); + } + setBuildQueueItem(game, this.buildQueue.length - 1, null, null); + this.stateNotifier.queueChanged(); + } + } + else if (this.buildQueueTypes[0] == QueueItemType.RESEARCH) { + final CUnitType trainedUnitType = game.getUnitData().getUnitType(queuedRawcode); + if (this.constructionProgress >= trainedUnitType.getBuildTime()) { + this.constructionProgress = 0; + for (int i = 0; i < (this.buildQueue.length - 1); i++) { + setBuildQueueItem(game, i, this.buildQueue[i + 1], this.buildQueueTypes[i + 1]); + } + setBuildQueueItem(game, this.buildQueue.length - 1, null, null); + this.stateNotifier.queueChanged(); + } + } + } + for (final CAbility ability : this.abilities) { + ability.onTick(game, this); + } + if (this.currentBehavior != null) { + final CBehavior lastBehavior = this.currentBehavior; + final int lastBehaviorHighlightOrderId = lastBehavior.getHighlightOrderId(); + this.currentBehavior = this.currentBehavior.update(game); + if (lastBehavior != this.currentBehavior) { + lastBehavior.end(game, false); + this.currentBehavior.begin(game); + } + if (this.currentBehavior.getHighlightOrderId() != lastBehaviorHighlightOrderId) { + this.stateNotifier.ordersChanged(); + } + } + else { + // check to auto acquire targets + autoAcquireAttackTargets(game, false); + } + } + } + return false; + + } + + private void popoutWorker(final CSimulation game) { + if (this.workerInside != null) { + this.workerInside.setInvulnerable(false); + this.workerInside.setHidden(false); + this.workerInside.setPaused(false); + this.workerInside.nudgeAround(game, this); + this.workerInside = null; + } + } + + public boolean autoAcquireAttackTargets(final CSimulation game, final boolean disableMove) { + if (!this.getAttacks().isEmpty() && !this.unitType.getClassifications().contains(CUnitClassification.PEON)) { + if (this.collisionRectangle != null) { + tempRect.set(this.collisionRectangle); + } + else { + tempRect.set(this.getX(), this.getY(), 0, 0); + } + final float halfSize = this.acquisitionRange; + tempRect.x -= halfSize; + tempRect.y -= halfSize; + tempRect.width += halfSize * 2; + tempRect.height += halfSize * 2; + game.getWorldCollision().enumUnitsInRect(tempRect, + autoAttackTargetFinderEnum.reset(game, this, disableMove)); + return autoAttackTargetFinderEnum.foundAnyTarget; + } + return false; + } + + public float getEndingDecayTime(final CSimulation game) { + if (this.unitType.isBuilding()) { + return game.getGameplayConstants().getStructureDecayTime(); + } + return game.getGameplayConstants().getBoneDecayTime(); + } + + public void order(final CSimulation game, final COrder order, final boolean queue) { + if (isDead()) { + return; + } + + if (order != null) { + final CAbility ability = game.getAbility(order.getAbilityHandleId()); + if (ability != null) { + // Allow the ability to response to the order without actually placing itself in + // the queue, nor modifying (interrupting) the queue. + if (!ability.checkBeforeQueue(game, this, order.getOrderId(), order.getTarget(game))) { + this.stateNotifier.ordersChanged(); + return; + } + } + } + + if ((this.lastStartedOrder != null) && this.lastStartedOrder.equals(order) + && (this.lastStartedOrder.getOrderId() == OrderIds.smart)) { + // I skip your spammed move orders, TODO this will probably break some repeat + // attack order or something later + return; + } + if ((queue || !this.acceptingOrders) && ((this.currentBehavior != this.stopBehavior) + && (this.currentBehavior != this.holdPositionBehavior))) { + this.orderQueue.add(order); + this.stateNotifier.waypointsChanged(); + } + else { + setDefaultBehavior(this.stopBehavior); + if (this.currentBehavior != null) { + this.currentBehavior.end(game, true); + } + this.currentBehavior = beginOrder(game, order); + if (this.currentBehavior != null) { + this.currentBehavior.begin(game); + } + for (final COrder queuedOrder : this.orderQueue) { + final int abilityHandleId = queuedOrder.getAbilityHandleId(); + final CAbility ability = game.getAbility(abilityHandleId); + ability.onCancelFromQueue(game, this, queuedOrder.getOrderId()); + } + this.orderQueue.clear(); + this.stateNotifier.ordersChanged(); + this.stateNotifier.waypointsChanged(); + } + } + + public boolean order(final CSimulation simulation, final int orderId, final AbilityTarget target) { + if (orderId == OrderIds.stop) { + order(simulation, new COrderNoTarget(0, orderId, false), false); + return true; + } + for (final CAbility ability : this.abilities) { + final BooleanAbilityActivationReceiver activationReceiver = BooleanAbilityActivationReceiver.INSTANCE; + ability.checkCanUse(simulation, this, orderId, activationReceiver); + if (activationReceiver.isOk()) { + if (target == null) { + final BooleanAbilityTargetCheckReceiver booleanTargetReceiver = BooleanAbilityTargetCheckReceiver + .getInstance().reset(); + ability.checkCanTargetNoTarget(simulation, this, orderId, booleanTargetReceiver); + if (booleanTargetReceiver.isTargetable()) { + order(simulation, new COrderNoTarget(ability.getHandleId(), orderId, false), false); + return true; + } + } + final boolean targetable = target.visit(new AbilityTargetVisitor() { + @Override + public Boolean accept(final AbilityPointTarget target) { + final BooleanAbilityTargetCheckReceiver booleanTargetReceiver = BooleanAbilityTargetCheckReceiver + .getInstance().reset(); + ability.checkCanTarget(simulation, CUnit.this, orderId, target, booleanTargetReceiver); + final boolean pointTargetable = booleanTargetReceiver.isTargetable(); + if (pointTargetable) { + order(simulation, new COrderTargetPoint(ability.getHandleId(), orderId, target, false), + false); + } + return pointTargetable; + } + + public Boolean acceptWidget(final CWidget target) { + final BooleanAbilityTargetCheckReceiver booleanTargetReceiver = BooleanAbilityTargetCheckReceiver + .getInstance().reset(); + ability.checkCanTarget(simulation, CUnit.this, orderId, target, booleanTargetReceiver); + final boolean widgetTargetable = booleanTargetReceiver.isTargetable(); + if (widgetTargetable) { + order(simulation, + new COrderTargetWidget(ability.getHandleId(), orderId, target.getHandleId(), false), + false); + } + return widgetTargetable; + } + + @Override + public Boolean accept(final CUnit target) { + return acceptWidget(target); + } + + @Override + public Boolean accept(final CDestructable target) { + return acceptWidget(target); + } + + @Override + public Boolean accept(final CItem target) { + return acceptWidget(target); + } + }); + if (targetable) { + return true; + } + } + } + return false; + } + + private CBehavior beginOrder(final CSimulation game, final COrder order) { + this.lastStartedOrder = order; + CBehavior nextBehavior; + if (order != null) { + nextBehavior = order.begin(game, this); + } + else { + nextBehavior = this.defaultBehavior; + } + return nextBehavior; + } + + public CBehavior getCurrentBehavior() { + return this.currentBehavior; + } + + public List getAbilities() { + return this.abilities; + } + + public T getAbility(final CAbilityVisitor visitor) { + for (final CAbility ability : this.abilities) { + final T visited = ability.visit(visitor); + if (visited != null) { + return visited; + } + } + return null; + } + + public T getFirstAbilityOfType(final Class cAbilityClass) { + for (final CAbility ability : this.abilities) { + if (cAbilityClass.isAssignableFrom(ability.getClass())) { + return (T) ability; + } + } + return null; + } + + public void setCooldownEndTime(final int cooldownEndTime) { + this.cooldownEndTime = cooldownEndTime; + } + + public int getCooldownEndTime() { + return this.cooldownEndTime; + } + + @Override + public float getFlyHeight() { + return this.flyHeight; + } + + public void setFlyHeight(final float flyHeight) { + this.flyHeight = flyHeight; + } + + public int getPlayerIndex() { + return this.playerIndex; + } + + public void setPlayerIndex(final int playerIndex) { + this.playerIndex = playerIndex; + } + + public CUnitType getUnitType() { + return this.unitType; + } + + public void setCollisionRectangle(final Rectangle collisionRectangle) { + this.collisionRectangle = collisionRectangle; + } + + public Rectangle getCollisionRectangle() { + return this.collisionRectangle; + } + + public void setX(final float newX, final CWorldCollision collision, final CRegionManager regionManager) { + final float prevX = getX(); + if (!this.unitType.isBuilding()) { + setX(newX); + collision.translate(this, newX - prevX, 0); + } + checkRegionEvents(regionManager); + } + + public void setY(final float newY, final CWorldCollision collision, final CRegionManager regionManager) { + final float prevY = getY(); + if (!this.unitType.isBuilding()) { + setY(newY); + collision.translate(this, 0, newY - prevY); + } + checkRegionEvents(regionManager); + } + + public void setPointAndCheckUnstuck(final float newX, final float newY, final CSimulation game) { + final CWorldCollision collision = game.getWorldCollision(); + final PathingGrid pathingGrid = game.getPathingGrid(); + ; + float outputX = newX, outputY = newY; + int checkX = 0; + int checkY = 0; + float collisionSize; + if (this.unitType.getBuildingPathingPixelMap() != null) { + tempRect.setSize(this.unitType.getBuildingPathingPixelMap().getWidth() * 32, + this.unitType.getBuildingPathingPixelMap().getHeight() * 32); + collisionSize = tempRect.getWidth() / 2; + } + else if (this.collisionRectangle != null) { + tempRect.set(this.collisionRectangle); + collisionSize = this.unitType.getCollisionSize(); + } + else { + tempRect.setSize(16, 16); + collisionSize = this.unitType.getCollisionSize(); + } + final boolean repos = false; + for (int i = 0; i < 300; i++) { + final float centerX = newX + (checkX * 64); + final float centerY = newY + (checkY * 64); + tempRect.setCenter(centerX, centerY); + if (!collision.intersectsAnythingOtherThan(tempRect, this, this.unitType.getMovementType()) + && pathingGrid.isPathable(centerX, centerY, this.unitType.getMovementType(), collisionSize)) { + outputX = centerX; + outputY = centerY; + break; + } + final double angle = ((((int) Math.floor(Math.sqrt((4 * i) + 1))) % 4) * Math.PI) / 2; + checkX -= (int) Math.cos(angle); + checkY -= (int) Math.sin(angle); + } + setPoint(outputX, outputY, collision, game.getRegionManager()); + game.unitRepositioned(this); + } + + public void setPoint(final float newX, final float newY, final CWorldCollision collision, + final CRegionManager regionManager) { + final float prevX = getX(); + final float prevY = getY(); + setX(newX); + setY(newY); + if (!this.unitType.isBuilding()) { + collision.translate(this, newX - prevX, newY - prevY); + } + checkRegionEvents(regionManager); + } + + private void checkRegionEvents(final CRegionManager regionManager) { + final Set temp = this.containingRegions; + this.containingRegions = this.priorContainingRegions; + this.priorContainingRegions = temp; + this.containingRegions.clear(); + regionManager.checkRegions(this.collisionRectangle == null ? tempRect.set(this.getX(), this.getY(), 0, 0) + : this.collisionRectangle, regionCheckerImpl.reset(this)); + for (final CRegion region : this.priorContainingRegions) { + if (!this.containingRegions.contains(region)) { + onLeaveRegion(region); + } + } + } + + private void onEnterRegion(final CRegion region) { + + } + + private void onLeaveRegion(final CRegion region) { + + } + + public EnumSet getClassifications() { + return this.classifications; + } + + public int getDefense() { + return this.currentDefense; + } + + @Override + public float getImpactZ() { + return this.unitType.getImpactZ(); + } + + public double angleTo(final AbilityTarget target) { + final double dx = target.getX() - getX(); + final double dy = target.getY() - getY(); + return StrictMath.atan2(dy, dx); + } + + public double distance(final AbilityTarget target) { + double dx = StrictMath.abs(target.getX() - getX()); + double dy = StrictMath.abs(target.getY() - getY()); + final float thisCollisionSize = this.unitType.getCollisionSize(); + float targetCollisionSize; + if (target instanceof CUnit) { + final CUnitType targetUnitType = ((CUnit) target).getUnitType(); + targetCollisionSize = targetUnitType.getCollisionSize(); + } + else { + targetCollisionSize = 0; // TODO destructable collision size here + } + if (dx < 0) { + dx = 0; + } + if (dy < 0) { + dy = 0; + } + + double groundDistance = StrictMath.sqrt((dx * dx) + (dy * dy)) - thisCollisionSize - targetCollisionSize; + if (groundDistance < 0) { + groundDistance = 0; + } + return groundDistance; + } + + public double distance(final float x, final float y) { + double dx = Math.abs(x - getX()); + double dy = Math.abs(y - getY()); + final float thisCollisionSize = this.unitType.getCollisionSize(); + if (dx < 0) { + dx = 0; + } + if (dy < 0) { + dy = 0; + } + + double groundDistance = StrictMath.sqrt((dx * dx) + (dy * dy)) - thisCollisionSize; + if (groundDistance < 0) { + groundDistance = 0; + } + return groundDistance; + } + + @Override + public void damage(final CSimulation simulation, final CUnit source, final CAttackType attackType, + final String weaponType, final float damage) { + final boolean wasDead = isDead(); + if (!this.invulnerable) { + final float damageRatioFromArmorClass = simulation.getGameplayConstants().getDamageRatioAgainst(attackType, + this.unitType.getDefenseType()); + final float damageRatioFromDefense; + final int defense = this.currentDefense; + if (defense >= 0) { + damageRatioFromDefense = 1f - (((defense) * simulation.getGameplayConstants().getDefenseArmor()) + / (1 + (simulation.getGameplayConstants().getDefenseArmor() * defense))); + } + else { + damageRatioFromDefense = 2f - (float) StrictMath.pow(0.94, -defense); + } + final float trueDamage = damageRatioFromArmorClass * damageRatioFromDefense * damage; + this.life -= trueDamage; + this.stateNotifier.lifeChanged(); + } + simulation.unitDamageEvent(this, weaponType, this.unitType.getArmorType()); + if (!this.invulnerable && isDead()) { + if (!wasDead) { + kill(simulation, source); + } + } + else { + if ((this.currentBehavior == null) || (this.currentBehavior == this.defaultBehavior)) { + boolean foundMatchingReturnFireAttack = false; + if (!simulation.getPlayer(getPlayerIndex()).hasAlliance(source.getPlayerIndex(), CAllianceType.PASSIVE) + && !this.unitType.getClassifications().contains(CUnitClassification.PEON)) { + for (final CUnitAttack attack : this.getAttacks()) { + if (source.canBeTargetedBy(simulation, this, attack.getTargetsAllowed())) { + this.currentBehavior = getAttackBehavior().reset(OrderIds.attack, attack, source, false, + CBehaviorAttackListener.DO_NOTHING); + this.currentBehavior.begin(simulation); + foundMatchingReturnFireAttack = true; + break; + } + } + } + if (!foundMatchingReturnFireAttack && this.unitType.isCanFlee() && !isMovementDisabled() + && (this.moveBehavior != null) && (this.playerIndex != source.getPlayerIndex())) { + final double angleTo = source.angleTo(this); + final int distanceToFlee = getSpeed(); + this.currentBehavior = this.moveBehavior.reset(OrderIds.move, + new AbilityPointTarget((float) (getX() + (distanceToFlee * StrictMath.cos(angleTo))), + (float) (getY() + (distanceToFlee * StrictMath.sin(angleTo))))); + this.currentBehavior.begin(simulation); + } + } + } + } + + private void kill(final CSimulation simulation, final CUnit source) { + if (this.currentBehavior != null) { + this.currentBehavior.end(simulation, true); + } + this.currentBehavior = null; + this.orderQueue.clear(); + if (this.constructing) { + simulation.createBuildingDeathEffect(this); + } + else { + this.deathTurnTick = simulation.getGameTurnTick(); + } + if (this.pathingInstance != null) { + this.pathingInstance.remove(); + this.pathingInstance = null; + } + popoutWorker(simulation); + final CPlayer player = simulation.getPlayer(this.playerIndex); + if (this.foodMade != 0) { + player.setUnitFoodMade(this, 0); + } + if (this.foodUsed != 0) { + player.setUnitFoodUsed(this, 0); + } + + // Award hero experience + if (source != null) { + final CPlayer sourcePlayer = simulation.getPlayer(source.getPlayerIndex()); + if (!sourcePlayer.hasAlliance(this.playerIndex, CAllianceType.PASSIVE)) { + final CGameplayConstants gameplayConstants = simulation.getGameplayConstants(); + if (gameplayConstants.isBuildingKillsGiveExp() || !source.getUnitType().isBuilding()) { + final CUnit killedUnit = this; + final CAbilityHero killedUnitHeroData = getHeroData(); + final boolean killedUnitIsAHero = killedUnitHeroData != null; + int availableAwardXp; + if (killedUnitIsAHero) { + availableAwardXp = gameplayConstants.getGrantHeroXP(killedUnitHeroData.getHeroLevel()); + } + else { + availableAwardXp = gameplayConstants.getGrantNormalXP(this.unitType.getLevel()); + } + final List xpReceivingHeroes = new ArrayList<>(); + final int heroExpRange = gameplayConstants.getHeroExpRange(); + simulation.getWorldCollision().enumUnitsInRect(new Rectangle(this.getX() - heroExpRange, + this.getY() - heroExpRange, heroExpRange * 2, heroExpRange * 2), new CUnitEnumFunction() { + @Override + public boolean call(final CUnit unit) { + if ((unit.distance(killedUnit) <= heroExpRange) + && sourcePlayer.hasAlliance(unit.getPlayerIndex(), CAllianceType.SHARED_XP) + && (unit.getHeroData() != null)) { + xpReceivingHeroes.add(unit); + } + return false; + } + }); + if (xpReceivingHeroes.isEmpty()) { + if (gameplayConstants.isGlobalExperience()) { + for (int i = 0; i < WarsmashConstants.MAX_PLAYERS; i++) { + if (sourcePlayer.hasAlliance(i, CAllianceType.SHARED_XP)) { + xpReceivingHeroes.addAll(simulation.getPlayerHeroes(i)); + } + } + } + } + for (final CUnit receivingHero : xpReceivingHeroes) { + final CAbilityHero heroData = receivingHero.getHeroData(); + heroData.addXp(simulation, receivingHero, + (int) (availableAwardXp * (1f / xpReceivingHeroes.size()) + * gameplayConstants.getHeroFactorXp(heroData.getHeroLevel()))); + } + } + } + } + } + + public boolean canReach(final AbilityTarget target, final float range) { + final double distance = distance(target); + if (target instanceof CUnit) { + final CUnit targetUnit = (CUnit) target; + final CUnitType targetUnitType = targetUnit.getUnitType(); + if (targetUnitType.isBuilding() && (targetUnitType.getBuildingPathingPixelMap() != null)) { + final BufferedImage buildingPathingPixelMap = targetUnitType.getBuildingPathingPixelMap(); + final float targetX = target.getX(); + final float targetY = target.getY(); + if (canReachToPathing(range, targetUnit.getFacing(), buildingPathingPixelMap, targetX, targetY)) { + return true; + } + } + } + else if (target instanceof CDestructable) { + final CDestructable targetDest = (CDestructable) target; + final CDestructableType targetDestType = targetDest.getDestType(); + final BufferedImage pathingPixelMap = targetDest.isDead() ? targetDestType.getPathingDeathPixelMap() + : targetDestType.getPathingPixelMap(); + final float targetX = target.getX(); + final float targetY = target.getY(); + if ((pathingPixelMap != null) && canReachToPathing(range, 270, pathingPixelMap, targetX, targetY)) { + return true; + } + } + return distance <= range; + } + + public boolean canReach(final float x, final float y, final float range) { + return distance(x, y) <= range; // TODO use dist squared for performance + } + + public boolean canReachToPathing(final float range, final float rotationForPathing, + final BufferedImage buildingPathingPixelMap, final float targetX, final float targetY) { + final int rotation = ((int) rotationForPathing + 450) % 360; + final float relativeOffsetX = getX() - targetX; + final float relativeOffsetY = getY() - targetY; + final int gridWidth = ((rotation % 180) != 0) ? buildingPathingPixelMap.getHeight() + : buildingPathingPixelMap.getWidth(); + final int gridHeight = ((rotation % 180) != 0) ? buildingPathingPixelMap.getWidth() + : buildingPathingPixelMap.getHeight(); + final int relativeGridX = (int) Math.floor(relativeOffsetX / 32f) + (gridWidth / 2); + final int relativeGridY = (int) Math.floor(relativeOffsetY / 32f) + (gridHeight / 2); + final int rangeInCells = (int) Math.floor(range / 32f) + 1; + final int rangeInCellsSquare = rangeInCells * rangeInCells; + int minCheckX = relativeGridX - rangeInCells; + int minCheckY = relativeGridY - rangeInCells; + int maxCheckX = relativeGridX + rangeInCells; + int maxCheckY = relativeGridY + rangeInCells; + if ((minCheckX < gridWidth) && (maxCheckX >= 0)) { + if ((minCheckY < gridHeight) && (maxCheckY >= 0)) { + if (minCheckX < 0) { + minCheckX = 0; + } + if (minCheckY < 0) { + minCheckY = 0; + } + if (maxCheckX > (gridWidth - 1)) { + maxCheckX = gridWidth - 1; + } + if (maxCheckY > (gridHeight - 1)) { + maxCheckY = gridHeight - 1; + } + for (int checkX = minCheckX; checkX <= maxCheckX; checkX++) { + for (int checkY = minCheckY; checkY <= maxCheckY; checkY++) { + final int dx = relativeGridX - checkX; + final int dy = relativeGridY - checkY; + if (((dx * dx) + (dy * dy)) <= rangeInCellsSquare) { + if (((getRGBFromPixelData(buildingPathingPixelMap, checkX, checkY, rotation) + & 0xFF0000) >>> 16) > 127) { + return true; + } + } + } + } + } + } + return false; + } + + private int getRGBFromPixelData(final BufferedImage buildingPathingPixelMap, final int checkX, final int checkY, + final int rotation) { + + // Below: y is downwards (:() + int x; + int y; + switch (rotation) { + case 90: + x = checkY; + y = buildingPathingPixelMap.getWidth() - 1 - checkX; + break; + case 180: + x = buildingPathingPixelMap.getWidth() - 1 - checkX; + y = buildingPathingPixelMap.getHeight() - 1 - checkY; + break; + case 270: + x = buildingPathingPixelMap.getHeight() - 1 - checkY; + y = checkX; + break; + default: + case 0: + x = checkX; + y = checkY; + } + return buildingPathingPixelMap.getRGB(x, buildingPathingPixelMap.getHeight() - 1 - y); + } + + public void addStateListener(final CUnitStateListener listener) { + this.stateNotifier.subscribe(listener); + } + + public void removeStateListener(final CUnitStateListener listener) { + this.stateNotifier.unsubscribe(listener); + } + + public boolean isCorpse() { + return this.corpse; + } + + public boolean isBoneCorpse() { + return this.boneCorpse; + } + + @Override + public boolean canBeTargetedBy(final CSimulation simulation, final CUnit source, + final EnumSet targetsAllowed) { + if (targetsAllowed.containsAll(this.unitType.getTargetedAs())) { + final int sourcePlayerIndex = source.getPlayerIndex(); + final CPlayer sourcePlayer = simulation.getPlayer(sourcePlayerIndex); + if (!targetsAllowed.contains(CTargetType.ENEMIES) + || !sourcePlayer.hasAlliance(this.playerIndex, CAllianceType.PASSIVE)) { + if (isDead()) { + if (this.unitType.isRaise() && this.unitType.isDecay() && isBoneCorpse()) { + return targetsAllowed.contains(CTargetType.DEAD); + } + } + else { + return !targetsAllowed.contains(CTargetType.DEAD) || targetsAllowed.contains(CTargetType.ALIVE); + } + } + } + else { + System.err.println("No targeting because " + targetsAllowed + " does not contain all of " + + this.unitType.getTargetedAs()); + } + return false; + } + + public boolean isMovementDisabled() { + return this.unitType.isBuilding(); + } + + public float getAcquisitionRange() { + return this.acquisitionRange; + } + + private static final class AutoAttackTargetFinderEnum implements CUnitEnumFunction { + private CSimulation game; + private CUnit source; + private boolean disableMove; + private boolean foundAnyTarget; + + private AutoAttackTargetFinderEnum reset(final CSimulation game, final CUnit source, + final boolean disableMove) { + this.game = game; + this.source = source; + this.disableMove = disableMove; + this.foundAnyTarget = false; + return this; + } + + @Override + public boolean call(final CUnit unit) { + if (!this.game.getPlayer(this.source.getPlayerIndex()).hasAlliance(unit.getPlayerIndex(), + CAllianceType.PASSIVE)) { + for (final CUnitAttack attack : this.source.getAttacks()) { + if (this.source.canReach(unit, this.source.acquisitionRange) + && unit.canBeTargetedBy(this.game, this.source, attack.getTargetsAllowed()) + && (this.source.distance(unit) >= this.source.getUnitType().getMinimumAttackRange())) { + if (this.source.currentBehavior != null) { + this.source.currentBehavior.end(this.game, false); + } + this.source.currentBehavior = this.source.getAttackBehavior().reset(OrderIds.attack, attack, + unit, this.disableMove, CBehaviorAttackListener.DO_NOTHING); + this.source.currentBehavior.begin(this.game); + this.foundAnyTarget = true; + return true; + } + } + } + return false; + } + } + + public CBehaviorMove getMoveBehavior() { + return this.moveBehavior; + } + + public void setMoveBehavior(final CBehaviorMove moveBehavior) { + this.moveBehavior = moveBehavior; + } + + public CBehaviorAttack getAttackBehavior() { + return this.attackBehavior; + } + + public void setAttackBehavior(final CBehaviorAttack attackBehavior) { + this.attackBehavior = attackBehavior; + } + + public CBehaviorStop getStopBehavior() { + return this.stopBehavior; + } + + public void setFollowBehavior(final CBehaviorFollow followBehavior) { + this.followBehavior = followBehavior; + } + + public void setPatrolBehavior(final CBehaviorPatrol patrolBehavior) { + this.patrolBehavior = patrolBehavior; + } + + public void setHoldPositionBehavior(final CBehaviorHoldPosition holdPositionBehavior) { + this.holdPositionBehavior = holdPositionBehavior; + } + + public CBehaviorFollow getFollowBehavior() { + return this.followBehavior; + } + + public CBehaviorPatrol getPatrolBehavior() { + return this.patrolBehavior; + } + + public CBehaviorHoldPosition getHoldPositionBehavior() { + return this.holdPositionBehavior; + } + + public CBehavior pollNextOrderBehavior(final CSimulation game) { + if (this.defaultBehavior != this.stopBehavior) { + // kind of a stupid hack, meant to align in feel with some behaviors that were + // observed on War3 + return this.defaultBehavior; + } + final COrder order = this.orderQueue.poll(); + final CBehavior nextOrderBehavior = beginOrder(game, order); + this.stateNotifier.waypointsChanged(); + return nextOrderBehavior; + } + + public boolean isMoving() { + return getCurrentBehavior() instanceof CBehaviorMove; + } + + public void setConstructing(final boolean constructing) { + this.constructing = constructing; + if (constructing) { + this.unitAnimationListener.playAnimation(true, PrimaryTag.BIRTH, SequenceUtils.EMPTY, 0.0f, true); + } + } + + public void setConstructionProgress(final float constructionProgress) { + this.constructionProgress = constructionProgress; + } + + public boolean isConstructing() { + return this.constructing; + } + + public float getConstructionProgress() { + return this.constructionProgress; + } + + public void setHidden(final boolean hidden) { + this.hidden = hidden; + } + + public void setPaused(final boolean paused) { + this.paused = paused; + } + + public void setAcceptingOrders(final boolean acceptingOrders) { + this.acceptingOrders = acceptingOrders; + } + + public boolean isHidden() { + return this.hidden; + } + + public void setInvulnerable(final boolean invulnerable) { + this.invulnerable = invulnerable; + } + + public boolean isInvulnerable() { + return this.invulnerable; + } + + public void setWorkerInside(final CUnit unit) { + this.workerInside = unit; + } + + public CUnit getWorkerInside() { + return this.workerInside; + } + + private void nudgeAround(final CSimulation simulation, final CUnit structure) { + setPointAndCheckUnstuck(structure.getX(), structure.getY(), simulation); + } + + @Override + public void setLife(final CSimulation simulation, final float life) { + final boolean wasDead = isDead(); + super.setLife(simulation, life); + if (isDead() && !wasDead) { + kill(simulation, null); + } + this.stateNotifier.lifeChanged(); + } + + private boolean queue(final CSimulation game, final War3ID rawcode, final QueueItemType queueItemType) { + for (int i = 0; i < this.buildQueue.length; i++) { + if (this.buildQueue[i] == null) { + setBuildQueueItem(game, i, rawcode, queueItemType); + return true; + } + } + return false; + } + + public War3ID[] getBuildQueue() { + return this.buildQueue; + } + + public QueueItemType[] getBuildQueueTypes() { + return this.buildQueueTypes; + } + + public float getBuildQueueTimeRemaining(final CSimulation simulation) { + if (this.buildQueueTypes[0] == null) { + return 0; + } + switch (this.buildQueueTypes[0]) { + case RESEARCH: + return 999; // TODO + case UNIT: + final CUnitType trainedUnitType = simulation.getUnitData().getUnitType(this.buildQueue[0]); + return trainedUnitType.getBuildTime(); + default: + return 0; + } + } + + public void cancelBuildQueueItem(final CSimulation game, final int cancelIndex) { + if ((cancelIndex >= 0) && (cancelIndex < this.buildQueueTypes.length)) { + final QueueItemType cancelledType = this.buildQueueTypes[cancelIndex]; + if (cancelledType != null) { + // TODO refund here! + if (cancelIndex == 0) { + this.constructionProgress = 0.0f; + switch (cancelledType) { + case RESEARCH: + break; + case UNIT: + final CPlayer player = game.getPlayer(this.playerIndex); + final CUnitType unitType = game.getUnitData().getUnitType(this.buildQueue[cancelIndex]); + player.setFoodUsed(player.getFoodUsed() - unitType.getFoodUsed()); + break; + } + } + switch (cancelledType) { + case RESEARCH: + break; + case UNIT: + final CPlayer player = game.getPlayer(this.playerIndex); + final CUnitType unitType = game.getUnitData().getUnitType(this.buildQueue[cancelIndex]); + player.refundFor(unitType); + break; + } + for (int i = cancelIndex; i < (this.buildQueueTypes.length - 1); i++) { + setBuildQueueItem(game, i, this.buildQueue[i + 1], this.buildQueueTypes[i + 1]); + } + setBuildQueueItem(game, this.buildQueue.length - 1, null, null); + this.stateNotifier.queueChanged(); + } + } + } + + public void setBuildQueueItem(final CSimulation game, final int index, final War3ID rawcode, + final QueueItemType queueItemType) { + this.buildQueue[index] = rawcode; + this.buildQueueTypes[index] = queueItemType; + if (index == 0) { + this.queuedUnitFoodPaid = true; + if ((rawcode != null) && (queueItemType == QueueItemType.UNIT)) { + final CPlayer player = game.getPlayer(this.playerIndex); + final CUnitType unitType = game.getUnitData().getUnitType(this.buildQueue[index]); + if (unitType.getFoodUsed() != 0) { + final int newFoodUsed = player.getFoodUsed() + unitType.getFoodUsed(); + if (newFoodUsed <= player.getFoodCap()) { + player.setFoodUsed(newFoodUsed); + } + else { + this.queuedUnitFoodPaid = false; + game.getCommandErrorListener(this.playerIndex).showNoFoodError(); + } + } + } + } + } + + public void queueTrainingUnit(final CSimulation game, final War3ID rawcode) { + if (queue(game, rawcode, QueueItemType.UNIT)) { + final CPlayer player = game.getPlayer(this.playerIndex); + final CUnitType unitType = game.getUnitData().getUnitType(rawcode); + player.chargeFor(unitType); + } + } + + public void queueResearch(final CSimulation game, final War3ID rawcode) { + queue(game, rawcode, QueueItemType.RESEARCH); + } + + public static enum QueueItemType { + UNIT, + RESEARCH; + } + + public void setRallyPoint(final AbilityTarget target) { + this.rallyPoint = target; + this.stateNotifier.rallyPointChanged(); + } + + public void internalPublishHeroStatsChanged() { + this.stateNotifier.heroStatsChanged(); + } + + public AbilityTarget getRallyPoint() { + return this.rallyPoint; + } + + private static interface RallyProvider { + float getX(); + + float getY(); + } + + @Override + public T visit(final AbilityTargetVisitor visitor) { + return visitor.accept(this); + } + + private static final class UseAbilityOnTargetByIdVisitor implements AbilityTargetVisitor { + private static final UseAbilityOnTargetByIdVisitor INSTANCE = new UseAbilityOnTargetByIdVisitor(); + private CSimulation game; + private CUnit trainedUnit; + private int rallyOrderId; + + private UseAbilityOnTargetByIdVisitor reset(final CSimulation game, final CUnit trainedUnit, + final int rallyOrderId) { + this.game = game; + this.trainedUnit = trainedUnit; + this.rallyOrderId = rallyOrderId; + return this; + } + + @Override + public Void accept(final AbilityPointTarget target) { + CAbility abilityToUse = null; + for (final CAbility ability : this.trainedUnit.getAbilities()) { + ability.checkCanUse(this.game, this.trainedUnit, this.rallyOrderId, + BooleanAbilityActivationReceiver.INSTANCE); + if (BooleanAbilityActivationReceiver.INSTANCE.isOk()) { + final BooleanAbilityTargetCheckReceiver targetCheckReceiver = BooleanAbilityTargetCheckReceiver + .getInstance().reset(); + ability.checkCanTarget(this.game, this.trainedUnit, this.rallyOrderId, target, targetCheckReceiver); + if (targetCheckReceiver.isTargetable()) { + abilityToUse = ability; + } + } + } + if (abilityToUse != null) { + this.trainedUnit.order(this.game, + new COrderTargetPoint(abilityToUse.getHandleId(), this.rallyOrderId, target, false), false); + } + return null; + } + + @Override + public Void accept(final CUnit targetUnit) { + return acceptWidget(this.game, this.trainedUnit, this.rallyOrderId, targetUnit); + } + + private Void acceptWidget(final CSimulation game, final CUnit trainedUnit, final int rallyOrderId, + final CWidget target) { + CAbility abilityToUse = null; + for (final CAbility ability : trainedUnit.getAbilities()) { + ability.checkCanUse(game, trainedUnit, rallyOrderId, BooleanAbilityActivationReceiver.INSTANCE); + if (BooleanAbilityActivationReceiver.INSTANCE.isOk()) { + final BooleanAbilityTargetCheckReceiver targetCheckReceiver = BooleanAbilityTargetCheckReceiver + .getInstance().reset(); + ability.checkCanTarget(game, trainedUnit, rallyOrderId, target, targetCheckReceiver); + if (targetCheckReceiver.isTargetable()) { + abilityToUse = ability; + } + } + } + if (abilityToUse != null) { + trainedUnit.order(game, + new COrderTargetWidget(abilityToUse.getHandleId(), rallyOrderId, target.getHandleId(), false), + false); + } + return null; + } + + @Override + public Void accept(final CDestructable target) { + return acceptWidget(this.game, this.trainedUnit, this.rallyOrderId, target); + } + + @Override + public Void accept(final CItem target) { + return acceptWidget(this.game, this.trainedUnit, this.rallyOrderId, target); + } + } + + public int getFoodMade() { + return this.foodMade; + } + + public int getFoodUsed() { + return this.foodUsed; + } + + public int setFoodMade(final int foodMade) { + final int delta = foodMade - this.foodMade; + this.foodMade = foodMade; + return delta; + } + + public int setFoodUsed(final int foodUsed) { + final int delta = foodUsed - this.foodUsed; + this.foodUsed = foodUsed; + return delta; + } + + public void setDefaultBehavior(final CBehavior defaultBehavior) { + this.defaultBehavior = defaultBehavior; + } + + public int getGold() { + for (final CAbility ability : this.abilities) { + if (ability instanceof CAbilityGoldMine) { + return ((CAbilityGoldMine) ability).getGold(); + } + } + return 0; + } + + public void setGold(final int goldAmount) { + for (final CAbility ability : this.abilities) { + if (ability instanceof CAbilityGoldMine) { + ((CAbilityGoldMine) ability).setGold(goldAmount); + } + } + } + + public Queue getOrderQueue() { + return this.orderQueue; + } + + public COrder getCurrentOrder() { + return this.lastStartedOrder; + } + + public CAbilityHero getHeroData() { + for (final CAbility ability : this.abilities) { + if (ability instanceof CAbilityHero) { + return (CAbilityHero) ability; + } + } + return null; + } + + public CAbilityInventory getInventoryData() { + for (final CAbility ability : this.abilities) { + if (ability instanceof CAbilityInventory) { + return (CAbilityInventory) ability; + } + } + return null; + } + + public void setUnitSpecificAttacks(final List unitSpecificAttacks) { + this.unitSpecificAttacks = unitSpecificAttacks; + } + + public List getUnitSpecificAttacks() { + return this.unitSpecificAttacks; + } + + public List getAttacks() { + if (this.unitSpecificAttacks != null) { + return this.unitSpecificAttacks; + } + return this.unitType.getAttacks(); + } + + public void onPickUpItem(final CSimulation game, final CItem item, final boolean playUserUISounds) { + this.stateNotifier.inventoryChanged(); + if (playUserUISounds) { + game.unitPickUpItemEvent(this, item); + } + } + + public void onDropItem(final CSimulation game, final CItem droppedItem, final boolean playUserUISounds) { + this.stateNotifier.inventoryChanged(); + if (playUserUISounds) { + game.unitDropItemEvent(this, droppedItem); + } + } + + public boolean isInRegion(final CRegion region) { + return this.containingRegions.contains(region); + } + + private static final class RegionCheckerImpl implements CRegionEnumFunction { + private CUnit unit; + + public RegionCheckerImpl reset(final CUnit unit) { + this.unit = unit; + return this; + } + + @Override + public boolean call(final CRegion region) { + if (this.unit.containingRegions.add(region)) { + if (!this.unit.priorContainingRegions.contains(region)) { + this.unit.onEnterRegion(region); + } + } + return false; + } + + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitAnimationListener.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitAnimationListener.java new file mode 100644 index 0000000..2a0029b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitAnimationListener.java @@ -0,0 +1,18 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.SecondaryTag; + +public interface CUnitAnimationListener { + void playAnimation(boolean force, final PrimaryTag animationName, + final EnumSet secondaryAnimationTags, float speedRatio, boolean allowRarityVariations); + + void queueAnimation(final PrimaryTag animationName, final EnumSet secondaryAnimationTags, + boolean allowRarityVariations); + + void addSecondaryTag(SecondaryTag secondaryTag); + + void removeSecondaryTag(SecondaryTag secondaryTag); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitClassification.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitClassification.java new file mode 100644 index 0000000..d603983 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitClassification.java @@ -0,0 +1,67 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +import java.util.HashMap; +import java.util.Map; + +/** + * We think in the original WC3 sourcecode, these are probably referred to as + * "Unit Types", but the community turn of phrase "Unit Type" has come to refer + * to what WC3 sourcecode calls "Unit Class", hence this is now named "Unit + * Classification" instead of "Unit Type" or "Unit Class" to disambiguate. This + * is consistent with the World Editor naming: "Stats - Unit Classification". + */ +public enum CUnitClassification { + GIANT("giant", "GiantClass"), + UNDEAD("undead", "UndeadClass"), + SUMMONED("summoned"), + MECHANICAL("mechanical", "MechanicalClass"), + PEON("peon"), + SAPPER("sapper"), + TOWNHALL("townhall"), + TREE("tree"), + WARD("ward"), + ANCIENT("ancient"), + STANDON("standon"), + NEURAL("neutral"), + TAUREN("tauren", "TaurenClass"); + private static final Map UNIT_EDITOR_KEY_TO_CLASSIFICATION = new HashMap<>(); + static { + for (final CUnitClassification unitClassification : values()) { + UNIT_EDITOR_KEY_TO_CLASSIFICATION.put(unitClassification.getUnitDataKey(), unitClassification); + } + } + + private String localeKey; + private String unitDataKey; + private String displayName; + + private CUnitClassification(final String unitDataKey, final String localeKey) { + this.unitDataKey = unitDataKey; + this.localeKey = localeKey; + } + + private CUnitClassification(final String unitDataKey) { + this.unitDataKey = unitDataKey; + this.localeKey = null; + } + + public String getUnitDataKey() { + return this.unitDataKey; + } + + public String getLocaleKey() { + return this.localeKey; + } + + public String getDisplayName() { + return this.displayName; + } + + public void setDisplayName(final String displayName) { + this.displayName = displayName; + } + + public static CUnitClassification parseUnitClassification(final String unitEditorKey) { + return UNIT_EDITOR_KEY_TO_CLASSIFICATION.get(unitEditorKey.toLowerCase()); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitEnumFunction.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitEnumFunction.java new file mode 100644 index 0000000..3f0392d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitEnumFunction.java @@ -0,0 +1,11 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +public interface CUnitEnumFunction { + /** + * Operates on a unit, returning true if we should stop execution. + * + * @param unit + * @return + */ + boolean call(CUnit unit); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitStateListener.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitStateListener.java new file mode 100644 index 0000000..2f1356a --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitStateListener.java @@ -0,0 +1,71 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +import com.etheller.warsmash.util.SubscriberSetNotifier; + +public interface CUnitStateListener { + void lifeChanged(); // hp (current) changes + + void ordersChanged(); + + void queueChanged(); + + void rallyPointChanged(); + + void waypointsChanged(); + + void heroStatsChanged(); + + void inventoryChanged(); + + public static final class CUnitStateNotifier extends SubscriberSetNotifier + implements CUnitStateListener { + @Override + public void lifeChanged() { + for (final CUnitStateListener listener : set) { + listener.lifeChanged(); + } + } + + @Override + public void ordersChanged() { + for (final CUnitStateListener listener : set) { + listener.ordersChanged(); + } + } + + @Override + public void queueChanged() { + for (final CUnitStateListener listener : set) { + listener.queueChanged(); + } + } + + @Override + public void rallyPointChanged() { + for (final CUnitStateListener listener : set) { + listener.rallyPointChanged(); + } + } + + @Override + public void waypointsChanged() { + for (final CUnitStateListener listener : set) { + listener.waypointsChanged(); + } + } + + @Override + public void heroStatsChanged() { + for (final CUnitStateListener listener : set) { + listener.heroStatsChanged(); + } + } + + @Override + public void inventoryChanged() { + for (final CUnitStateListener listener : set) { + listener.inventoryChanged(); + } + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitType.java new file mode 100644 index 0000000..50b27f9 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitType.java @@ -0,0 +1,356 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +import java.awt.image.BufferedImage; +import java.util.EnumSet; +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.MovementType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.hero.CPrimaryAttribute; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CDefenseType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttack; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.data.CUnitRace; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.pathing.CBuildingPathingType; + +/** + * The quick (symbol table instead of map) lookup for unit type values that we + * probably cannot change per unit instance. + */ +public class CUnitType { + private final String name; + private final String legacyName; + private final War3ID typeId; + private final int life; + private final int manaInitial; + private final int manaMaximum; + private final int speed; + private final int defense; + private final String abilityList; + private final boolean building; + private final PathingGrid.MovementType movementType; + private final float defaultFlyingHeight; + private final float collisionSize; + private final EnumSet classifications; + private final List attacks; + private final String armorType; // used for audio + private final boolean raise; + private final boolean decay; + private final CDefenseType defenseType; + private final float impactZ; + private final float deathTime; + + // TODO: this should probably not be stored as game state, i.e., is it really + // game data? can we store it in a cleaner way? + private final BufferedImage buildingPathingPixelMap; + private final EnumSet targetedAs; + private final float defaultAcquisitionRange; + private final float minimumAttackRange; + private final List structuresBuilt; + private final List unitsTrained; + private final List researchesAvailable; + private final CUnitRace unitRace; + private final int goldCost; + private final int lumberCost; + private final int foodUsed; + private final int foodMade; + private final int buildTime; + private final EnumSet preventedPathingTypes; + private final EnumSet requiredPathingTypes; + private final float propWindow; + private final float turnRate; + private final List requirements; + private final int level; + private final boolean hero; + private final int startingStrength; + private final float strengthPerLevel; + private final int startingAgility; + private final float agilityPerLevel; + private final int startingIntelligence; + private final float intelligencePerLevel; + private final CPrimaryAttribute primaryAttribute; + private final List heroAbilityList; + private final List heroProperNames; + private final int properNamesCount; + private final boolean canFlee; + + public CUnitType(final String name, final String legacyName, final War3ID typeId, final int life, + final int manaInitial, final int manaMaximum, final int speed, final int defense, final String abilityList, + final boolean isBldg, final MovementType movementType, final float defaultFlyingHeight, + final float collisionSize, final EnumSet classifications, + final List attacks, final String armorType, final boolean raise, final boolean decay, + final CDefenseType defenseType, final float impactZ, final BufferedImage buildingPathingPixelMap, + final float deathTime, final EnumSet targetedAs, final float defaultAcquisitionRange, + final float minimumAttackRange, final List structuresBuilt, final List unitsTrained, + final List researchesAvailable, final CUnitRace unitRace, final int goldCost, final int lumberCost, + final int foodUsed, final int foodMade, final int buildTime, + final EnumSet preventedPathingTypes, + final EnumSet requiredPathingTypes, final float propWindow, final float turnRate, + final List requirements, final int level, final boolean hero, final int strength, + final float strengthPerLevel, final int agility, final float agilityPerLevel, final int intelligence, + final float intelligencePerLevel, final CPrimaryAttribute primaryAttribute, + final List heroAbilityList, final List heroProperNames, final int properNamesCount, + final boolean canFlee) { + this.name = name; + this.legacyName = legacyName; + this.typeId = typeId; + this.life = life; + this.manaInitial = manaInitial; + this.manaMaximum = manaMaximum; + this.speed = speed; + this.defense = defense; + this.abilityList = abilityList; + this.building = isBldg; + this.movementType = movementType; + this.defaultFlyingHeight = defaultFlyingHeight; + this.collisionSize = collisionSize; + this.classifications = classifications; + this.attacks = attacks; + this.armorType = armorType; + this.raise = raise; + this.decay = decay; + this.defenseType = defenseType; + this.impactZ = impactZ; + this.buildingPathingPixelMap = buildingPathingPixelMap; + this.deathTime = deathTime; + this.targetedAs = targetedAs; + this.defaultAcquisitionRange = defaultAcquisitionRange; + this.minimumAttackRange = minimumAttackRange; + this.structuresBuilt = structuresBuilt; + this.unitsTrained = unitsTrained; + this.researchesAvailable = researchesAvailable; + this.unitRace = unitRace; + this.goldCost = goldCost; + this.lumberCost = lumberCost; + this.foodUsed = foodUsed; + this.foodMade = foodMade; + this.buildTime = buildTime; + this.preventedPathingTypes = preventedPathingTypes; + this.requiredPathingTypes = requiredPathingTypes; + this.propWindow = propWindow; + this.turnRate = turnRate; + this.requirements = requirements; + this.level = level; + this.hero = hero; + this.startingStrength = strength; + this.strengthPerLevel = strengthPerLevel; + this.startingAgility = agility; + this.agilityPerLevel = agilityPerLevel; + this.startingIntelligence = intelligence; + this.intelligencePerLevel = intelligencePerLevel; + this.primaryAttribute = primaryAttribute; + this.heroAbilityList = heroAbilityList; + this.heroProperNames = heroProperNames; + this.properNamesCount = properNamesCount; + this.canFlee = canFlee; + } + + public String getName() { + return this.name; + } + + public String getLegacyName() { + return this.legacyName; + } + + public War3ID getTypeId() { + return this.typeId; + } + + public int getLife() { + return this.life; + } + + public int getManaInitial() { + return this.manaInitial; + } + + public int getManaMaximum() { + return this.manaMaximum; + } + + public int getSpeed() { + return this.speed; + } + + public int getDefense() { + return this.defense; + } + + public String getAbilityList() { + return this.abilityList; + } + + public float getDefaultFlyingHeight() { + return this.defaultFlyingHeight; + } + + public PathingGrid.MovementType getMovementType() { + return this.movementType; + } + + public float getCollisionSize() { + return this.collisionSize; + } + + public boolean isBuilding() { + return this.building; + } + + public EnumSet getClassifications() { + return this.classifications; + } + + public List getAttacks() { + return this.attacks; + } + + public boolean isRaise() { + return this.raise; + } + + public boolean isDecay() { + return this.decay; + } + + public String getArmorType() { + return this.armorType; + } + + public CDefenseType getDefenseType() { + return this.defenseType; + } + + public float getImpactZ() { + return this.impactZ; + } + + public BufferedImage getBuildingPathingPixelMap() { + return this.buildingPathingPixelMap; + } + + public float getDeathTime() { + return this.deathTime; + } + + public EnumSet getTargetedAs() { + return this.targetedAs; + } + + public float getDefaultAcquisitionRange() { + return this.defaultAcquisitionRange; + } + + public float getMinimumAttackRange() { + return this.minimumAttackRange; + } + + public List getStructuresBuilt() { + return this.structuresBuilt; + } + + public List getUnitsTrained() { + return this.unitsTrained; + } + + public List getResearchesAvailable() { + return this.researchesAvailable; + } + + public CUnitRace getRace() { + return this.unitRace; + } + + public int getGoldCost() { + return this.goldCost; + } + + public int getLumberCost() { + return this.lumberCost; + } + + public int getFoodUsed() { + return this.foodUsed; + } + + public int getFoodMade() { + return this.foodMade; + } + + public int getBuildTime() { + return this.buildTime; + } + + public EnumSet getPreventedPathingTypes() { + return this.preventedPathingTypes; + } + + public EnumSet getRequiredPathingTypes() { + return this.requiredPathingTypes; + } + + public float getPropWindow() { + return this.propWindow; + } + + public float getTurnRate() { + return this.turnRate; + } + + public List getRequirements() { + return this.requirements; + } + + public int getLevel() { + return this.level; + } + + public boolean isHero() { + return this.hero; + } + + public int getStartingStrength() { + return this.startingStrength; + } + + public float getStrengthPerLevel() { + return this.strengthPerLevel; + } + + public int getStartingAgility() { + return this.startingAgility; + } + + public float getAgilityPerLevel() { + return this.agilityPerLevel; + } + + public int getStartingIntelligence() { + return this.startingIntelligence; + } + + public float getIntelligencePerLevel() { + return this.intelligencePerLevel; + } + + public CPrimaryAttribute getPrimaryAttribute() { + return this.primaryAttribute; + } + + public List getHeroAbilityList() { + return this.heroAbilityList; + } + + public List getHeroProperNames() { + return this.heroProperNames; + } + + public int getProperNamesCount() { + return this.properNamesCount; + } + + public boolean isCanFlee() { + return this.canFlee; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitTypeRequirement.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitTypeRequirement.java new file mode 100644 index 0000000..63b810d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CUnitTypeRequirement.java @@ -0,0 +1,21 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +import com.etheller.warsmash.util.War3ID; + +public class CUnitTypeRequirement { + private final War3ID requirement; + private final int requiredLevel; + + public CUnitTypeRequirement(final War3ID requirement, final int requiredLevel) { + this.requirement = requirement; + this.requiredLevel = requiredLevel; + } + + public War3ID getRequirement() { + return this.requirement; + } + + public int getRequiredLevel() { + return this.requiredLevel; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWidget.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWidget.java new file mode 100644 index 0000000..1ae17c3 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWidget.java @@ -0,0 +1,73 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +import java.util.EnumSet; + +import com.badlogic.gdx.math.Rectangle; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public abstract class CWidget implements AbilityTarget { + protected static final Rectangle tempRect = new Rectangle(); + private final int handleId; + private float x; + private float y; + protected float life; + + public CWidget(final int handleId, final float x, final float y, final float life) { + this.handleId = handleId; + this.x = x; + this.y = y; + this.life = life; + } + + public int getHandleId() { + return this.handleId; + } + + @Override + public float getX() { + return this.x; + } + + @Override + public float getY() { + return this.y; + } + + public float getLife() { + return this.life; + } + + protected void setX(final float x) { + this.x = x; + } + + protected void setY(final float y) { + this.y = y; + } + + public void setLife(final CSimulation simulation, final float life) { + this.life = life; + } + + public abstract void damage(final CSimulation simulation, final CUnit source, final CAttackType attackType, + final String weaponType, final float damage); + + public abstract float getFlyHeight(); + + public abstract float getImpactZ(); + + public boolean isDead() { + return this.life <= 0; + } + + public abstract boolean canBeTargetedBy(CSimulation simulation, CUnit source, + final EnumSet targetsAllowed); + + public double distanceSquaredNoCollision(final AbilityTarget target) { + final double dx = Math.abs(target.getX() - getX()); + final double dy = Math.abs(target.getY() - getY()); + return (dx * dx) + (dy * dy); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWidgetFilterFunction.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWidgetFilterFunction.java new file mode 100644 index 0000000..dc1a7bd --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWidgetFilterFunction.java @@ -0,0 +1,19 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +public interface CWidgetFilterFunction { + boolean call(CWidget unit); + + CWidgetFilterFunction ACCEPT_ALL = new CWidgetFilterFunction() { + @Override + public boolean call(final CWidget unit) { + return true; + } + }; + + CWidgetFilterFunction ACCEPT_ALL_LIVING = new CWidgetFilterFunction() { + @Override + public boolean call(final CWidget unit) { + return !unit.isDead(); + } + }; +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWorldCollision.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWorldCollision.java new file mode 100644 index 0000000..8f6600d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/CWorldCollision.java @@ -0,0 +1,240 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +import java.util.HashSet; +import java.util.Set; + +import com.badlogic.gdx.math.Rectangle; +import com.etheller.warsmash.util.Quadtree; +import com.etheller.warsmash.util.QuadtreeIntersector; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.MovementType; + +public class CWorldCollision { + private static final float MINIMUM_COLLISION_SIZE = 0.001f /* THIS IS TO STOP QUADTREE FROM BUSTING */; + private final Quadtree groundUnitCollision; + private final Quadtree airUnitCollision; + private final Quadtree seaUnitCollision; + private final Quadtree buildingUnitCollision; + private final float maxCollisionRadius; + private final AnyUnitExceptTwoIntersector anyUnitExceptTwoIntersector; + private final EachUnitOnlyOnceIntersector eachUnitOnlyOnceIntersector; + + public CWorldCollision(final Rectangle entireMapBounds, final float maxCollisionRadius) { + this.groundUnitCollision = new Quadtree<>(entireMapBounds); + this.airUnitCollision = new Quadtree<>(entireMapBounds); + this.seaUnitCollision = new Quadtree<>(entireMapBounds); + this.buildingUnitCollision = new Quadtree<>(entireMapBounds); + this.maxCollisionRadius = maxCollisionRadius; + this.anyUnitExceptTwoIntersector = new AnyUnitExceptTwoIntersector(); + this.eachUnitOnlyOnceIntersector = new EachUnitOnlyOnceIntersector(); + } + + public void addUnit(final CUnit unit) { + Rectangle bounds = unit.getCollisionRectangle(); + if (bounds == null) { + final float collisionSize = Math.max(MINIMUM_COLLISION_SIZE, + Math.min(this.maxCollisionRadius, unit.getUnitType().getCollisionSize())); + bounds = new Rectangle(unit.getX() - collisionSize, unit.getY() - collisionSize, collisionSize * 2, + collisionSize * 2); + unit.setCollisionRectangle(bounds); + } + if (unit.getUnitType().isBuilding()) { + // buildings are here so that we can include them when enumerating all units in + // a rect, but they don't really move dynamically, this is kind of pointless + this.buildingUnitCollision.add(unit, bounds); + } + else { + final MovementType movementType = unit.getUnitType().getMovementType(); + if (movementType != null) { + switch (movementType) { + case AMPHIBIOUS: + this.seaUnitCollision.add(unit, bounds); + this.groundUnitCollision.add(unit, bounds); + break; + case FLOAT: + this.seaUnitCollision.add(unit, bounds); + break; + case FLY: + this.airUnitCollision.add(unit, bounds); + break; + case DISABLED: + break; + default: + case FOOT: + case FOOT_NO_COLLISION: + case HORSE: + case HOVER: + this.groundUnitCollision.add(unit, bounds); + break; + } + } + } + } + + public void removeUnit(final CUnit unit) { + final Rectangle bounds = unit.getCollisionRectangle(); + if (bounds != null) { + if (unit.getUnitType().isBuilding()) { + this.buildingUnitCollision.remove(unit, bounds); + } + else { + final MovementType movementType = unit.getUnitType().getMovementType(); + if (movementType != null) { + switch (movementType) { + case AMPHIBIOUS: + this.seaUnitCollision.remove(unit, bounds); + this.groundUnitCollision.remove(unit, bounds); + break; + case FLOAT: + this.seaUnitCollision.remove(unit, bounds); + break; + case FLY: + this.airUnitCollision.remove(unit, bounds); + break; + case DISABLED: + break; + default: + case FOOT: + case FOOT_NO_COLLISION: + case HORSE: + case HOVER: + this.groundUnitCollision.remove(unit, bounds); + break; + } + } + } + } + unit.setCollisionRectangle(null); + } + + public void enumUnitsInRect(final Rectangle rect, final CUnitEnumFunction callback) { + this.eachUnitOnlyOnceIntersector.reset(callback); + this.groundUnitCollision.intersect(rect, this.eachUnitOnlyOnceIntersector); + this.airUnitCollision.intersect(rect, this.eachUnitOnlyOnceIntersector); + this.seaUnitCollision.intersect(rect, this.eachUnitOnlyOnceIntersector); + this.buildingUnitCollision.intersect(rect, this.eachUnitOnlyOnceIntersector); + } + + public boolean intersectsAnythingOtherThan(final Rectangle newPossibleRectangle, final CUnit sourceUnitToIgnore, + final MovementType movementType) { + return intersectsAnythingOtherThan(newPossibleRectangle, sourceUnitToIgnore, null, movementType); + } + + public boolean intersectsAnythingOtherThan(final Rectangle newPossibleRectangle, final CUnit sourceUnitToIgnore, + final CUnit sourceSecondUnitToIgnore, final MovementType movementType) { + if (movementType != null) { + switch (movementType) { + case AMPHIBIOUS: + if (this.seaUnitCollision.intersect(newPossibleRectangle, + this.anyUnitExceptTwoIntersector.reset(sourceUnitToIgnore, sourceSecondUnitToIgnore))) { + return true; + } + if (this.groundUnitCollision.intersect(newPossibleRectangle, + this.anyUnitExceptTwoIntersector.reset(sourceUnitToIgnore, sourceSecondUnitToIgnore))) { + return true; + } + return false; + case FLOAT: + return this.seaUnitCollision.intersect(newPossibleRectangle, + this.anyUnitExceptTwoIntersector.reset(sourceUnitToIgnore, sourceSecondUnitToIgnore)); + case FLY: + return this.airUnitCollision.intersect(newPossibleRectangle, + this.anyUnitExceptTwoIntersector.reset(sourceUnitToIgnore, sourceSecondUnitToIgnore)); + case DISABLED: + case FOOT_NO_COLLISION: + return false; + default: + case FOOT: + case HORSE: + case HOVER: + return this.groundUnitCollision.intersect(newPossibleRectangle, + this.anyUnitExceptTwoIntersector.reset(sourceUnitToIgnore, sourceSecondUnitToIgnore)); + } + } + return false; + } + + public void translate(final CUnit unit, final float xShift, final float yShift) { + if (unit.getUnitType().isBuilding()) { + throw new IllegalArgumentException("Cannot add building to the CWorldCollision"); + } + final MovementType movementType = unit.getUnitType().getMovementType(); + final Rectangle bounds = unit.getCollisionRectangle(); + if (movementType != null) { + switch (movementType) { + case AMPHIBIOUS: + final float oldX = bounds.x; + final float oldY = bounds.y; + this.seaUnitCollision.translate(unit, bounds, xShift, yShift); + bounds.x = oldX; + bounds.y = oldY; + this.groundUnitCollision.translate(unit, bounds, xShift, yShift); + break; + case FLOAT: + this.seaUnitCollision.translate(unit, bounds, xShift, yShift); + break; + case FLY: + this.airUnitCollision.translate(unit, bounds, xShift, yShift); + break; + case DISABLED: + break; + default: + case FOOT: + case FOOT_NO_COLLISION: + case HORSE: + case HOVER: + this.groundUnitCollision.translate(unit, bounds, xShift, yShift); + break; + } + } + } + + private static final class AnyUnitExceptTwoIntersector implements QuadtreeIntersector { + private CUnit firstUnit; + private CUnit secondUnit; + + public AnyUnitExceptTwoIntersector reset(final CUnit firstUnit, final CUnit secondUnit) { + this.firstUnit = firstUnit; + this.secondUnit = secondUnit; + return this; + } + + @Override + public boolean onIntersect(final CUnit intersectingObject) { + if (intersectingObject.isHidden()) { + return false; + } + return (intersectingObject != this.firstUnit) && (intersectingObject != this.secondUnit); + } + } + + private static final class EachUnitOnlyOnceIntersector implements QuadtreeIntersector { + private CUnitEnumFunction consumerDelegate; + private final Set intersectedUnits = new HashSet<>(); + private boolean done; + + public EachUnitOnlyOnceIntersector reset(final CUnitEnumFunction consumerDelegate) { + this.consumerDelegate = consumerDelegate; + this.intersectedUnits.clear(); + this.done = false; + return this; + } + + @Override + public boolean onIntersect(final CUnit intersectingObject) { + if (intersectingObject.isHidden()) { + return false; + } + if (this.done) { + // This check is because we may use the intersector for multiple intersect + // calls, see "enumUnitsInRect" and how it uses this intersector first on the + // ground unit layer, then the flying unit layer, without recycling + return true; + } + if (this.intersectedUnits.add(intersectingObject)) { + this.done = this.consumerDelegate.call(intersectingObject); + return this.done; + } + return false; + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/HandleIdAllocator.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/HandleIdAllocator.java new file mode 100644 index 0000000..c1be65c --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/HandleIdAllocator.java @@ -0,0 +1,14 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +/** + * This class is not similar to how WC3 allocates handle IDs in any way. + * Changing this would probably be necessary to support TimerUtils madness, + * because I forget how it works but I know it uses subtraction on handle IDs. + */ +public class HandleIdAllocator { + private int next = 3412532; // bogus number + + public int createId() { + return this.next++; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/StringsToExternalizeLater.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/StringsToExternalizeLater.java new file mode 100644 index 0000000..8398643 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/StringsToExternalizeLater.java @@ -0,0 +1,6 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation; + +public class StringsToExternalizeLater { + public static final String MUST_TARGET_POINT = "Must target a point."; + public static final String MUST_TARGET_WIDGET = "Must target a unit with this action."; +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/AbstractCAbility.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/AbstractCAbility.java new file mode 100644 index 0000000..7aab18f --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/AbstractCAbility.java @@ -0,0 +1,54 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; + +public abstract class AbstractCAbility implements CAbility { + private final int handleId; + private boolean disabled = false; + private boolean iconShowing = true; + + public AbstractCAbility(final int handleId) { + this.handleId = handleId; + } + + @Override + public final int getHandleId() { + return this.handleId; + } + + @Override + public final boolean isDisabled() { + return this.disabled; + } + + @Override + public final void setDisabled(final boolean disabled) { + this.disabled = disabled; + } + + @Override + public final boolean isIconShowing() { + return this.iconShowing; + } + + @Override + public final void setIconShowing(final boolean iconShowing) { + this.iconShowing = iconShowing; + } + + @Override + public final void checkCanUse(final CSimulation game, final CUnit unit, final int orderId, + final AbilityActivationReceiver receiver) { + if (this.disabled) { + receiver.disabled(); + } + else { + innerCheckCanUse(game, unit, orderId, receiver); + } + } + + protected abstract void innerCheckCanUse(final CSimulation game, final CUnit unit, final int orderId, + final AbilityActivationReceiver receiver); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbility.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbility.java new file mode 100644 index 0000000..7664f44 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbility.java @@ -0,0 +1,34 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; + +public interface CAbility extends CAbilityView { + /* should fire when ability added to unit */ + void onAdd(CSimulation game, CUnit unit); + + /* should fire when ability removed from unit */ + void onRemove(CSimulation game, CUnit unit); + + void onTick(CSimulation game, CUnit unit); + + void onCancelFromQueue(CSimulation game, CUnit unit, int orderId); + + /* return false to not do anything, such as for toggling autocast */ + boolean checkBeforeQueue(CSimulation game, CUnit caster, int orderId, AbilityTarget target); + + CBehavior begin(CSimulation game, CUnit caster, int orderId, CWidget target); + + CBehavior begin(CSimulation game, CUnit caster, int orderId, AbilityPointTarget point); + + CBehavior beginNoTarget(CSimulation game, CUnit caster, int orderId); + + void setDisabled(boolean disabled); + + void setIconShowing(boolean iconShowing); + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityAttack.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityAttack.java new file mode 100644 index 0000000..89599aa --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityAttack.java @@ -0,0 +1,176 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorAttack; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorAttackListener; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttack; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CAllianceType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver.TargetType; + +public class CAbilityAttack extends AbstractCAbility { + + public CAbilityAttack(final int handleId) { + super(handleId); + } + + @Override + protected void innerCheckCanUse(final CSimulation game, final CUnit unit, final int orderId, + final AbilityActivationReceiver receiver) { + receiver.useOk(); + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, final CWidget target, + final AbilityTargetCheckReceiver receiver) { + if (orderId == OrderIds.smart) { + if (target instanceof CUnit) { + if (game.getPlayer(unit.getPlayerIndex()).hasAlliance(((CUnit) target).getPlayerIndex(), + CAllianceType.PASSIVE)) { + receiver.orderIdNotAccepted(); + return; + } + } + else { + receiver.orderIdNotAccepted(); + return; + } + } + if ((orderId == OrderIds.smart) || (orderId == OrderIds.attack)) { + boolean canTarget = false; + for (final CUnitAttack attack : unit.getAttacks()) { + if (target.canBeTargetedBy(game, unit, attack.getTargetsAllowed())) { + canTarget = true; + break; + } + } + if (canTarget) { + receiver.targetOk(target); + } + else { + // TODO obviously we should later support better warnings here + receiver.mustTargetType(TargetType.UNIT); + } + } + else { + receiver.orderIdNotAccepted(); + } + } + + @Override + public boolean checkBeforeQueue(final CSimulation game, final CUnit caster, final int orderId, AbilityTarget target) { + return true; + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + switch (orderId) { + case OrderIds.attack: + receiver.targetOk(target); + break; + case OrderIds.attackground: + boolean allowAttackGround = false; + for (final CUnitAttack attack : unit.getAttacks()) { + if (attack.getWeaponType().isAttackGroundSupported()) { + allowAttackGround = true; + break; + } + } + if (allowAttackGround) { + receiver.targetOk(target); + } + else { + receiver.orderIdNotAccepted(); + } + break; + default: + receiver.orderIdNotAccepted(); + break; + } + } + + @Override + public void checkCanTargetNoTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityTargetCheckReceiver receiver) { + receiver.mustTargetType(TargetType.UNIT); + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + unit.setAttackBehavior(new CBehaviorAttack(unit)); + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + } + + @Override + public void onTick(final CSimulation game, final CUnit unit) { + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + CBehavior behavior = null; + for (final CUnitAttack attack : caster.getAttacks()) { + if (target.canBeTargetedBy(game, caster, attack.getTargetsAllowed())) { + behavior = caster.getAttackBehavior().reset(OrderIds.attack, attack, target, false, + CBehaviorAttackListener.DO_NOTHING); + break; + } + } + if (behavior == null) { + behavior = caster.getMoveBehavior().reset(OrderIds.attack, target); + } + return behavior; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + switch (orderId) { + case OrderIds.attack: + if (caster.getMoveBehavior() == null) { + return caster.pollNextOrderBehavior(game); + } + return caster.getMoveBehavior().reset(OrderIds.attack, point); + case OrderIds.attackground: + CBehavior behavior = null; + for (final CUnitAttack attack : caster.getAttacks()) { + if (attack.getWeaponType().isAttackGroundSupported()) { + behavior = caster.getAttackBehavior().reset(OrderIds.attackground, attack, point, false, + CBehaviorAttackListener.DO_NOTHING); + break; + } + } + if (behavior == null) { + behavior = caster.getMoveBehavior().reset(OrderIds.attackground, point); + } + return behavior; + default: + return caster.pollNextOrderBehavior(game); + } + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public T visit(final CAbilityVisitor visitor) { + return visitor.accept(this); + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityGeneric.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityGeneric.java new file mode 100644 index 0000000..df4e170 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityGeneric.java @@ -0,0 +1,93 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; + +/** + * Represents an ability from the object data + */ +public class CAbilityGeneric extends AbstractCAbility { + private final War3ID rawcode; + + public CAbilityGeneric(final War3ID rawcode, final int handleId) { + super(handleId); + this.rawcode = rawcode; + } + + public War3ID getRawcode() { + return this.rawcode; + } + + @Override + protected void innerCheckCanUse(final CSimulation game, final CUnit unit, final int orderId, + final AbilityActivationReceiver receiver) { + receiver.notAnActiveAbility(); + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, final CWidget target, + final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public void checkCanTargetNoTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public T visit(final CAbilityVisitor visitor) { + return visitor.accept(this); + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + } + + @Override + public void onTick(final CSimulation game, final CUnit unit) { + } + + @Override + public boolean checkBeforeQueue(final CSimulation game, final CUnit caster, final int orderId, AbilityTarget target) { + return false; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityMove.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityMove.java new file mode 100644 index 0000000..232287d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityMove.java @@ -0,0 +1,134 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorFollow; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorHoldPosition; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorMove; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorPatrol; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver.TargetType; + +public class CAbilityMove extends AbstractCAbility { + + public CAbilityMove(final int handleId) { + super(handleId); + } + + @Override + protected void innerCheckCanUse(final CSimulation game, final CUnit unit, final int orderId, + final AbilityActivationReceiver receiver) { + receiver.useOk(); + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, final CWidget target, + final AbilityTargetCheckReceiver receiver) { + switch (orderId) { + case OrderIds.smart: + case OrderIds.patrol: + if ((target instanceof CUnit) && (target != unit)) { + receiver.targetOk(target); + } + else { + receiver.mustTargetType(TargetType.UNIT_OR_POINT); + } + break; + default: + receiver.orderIdNotAccepted(); + break; + } + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + switch (orderId) { + case OrderIds.smart: + case OrderIds.move: + case OrderIds.patrol: + receiver.targetOk(target); + break; + default: + receiver.orderIdNotAccepted(); + break; + } + } + + @Override + public void checkCanTargetNoTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityTargetCheckReceiver receiver) { + switch (orderId) { + case OrderIds.holdposition: + receiver.targetOk(null); + break; + default: + receiver.orderIdNotAccepted(); + break; + } + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + unit.setMoveBehavior(new CBehaviorMove(unit)); + unit.setFollowBehavior(new CBehaviorFollow(unit)); + unit.setPatrolBehavior(new CBehaviorPatrol(unit)); + unit.setHoldPositionBehavior(new CBehaviorHoldPosition(unit)); + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + + } + + @Override + public void onTick(final CSimulation game, final CUnit unit) { + } + + @Override + public boolean checkBeforeQueue(final CSimulation game, final CUnit caster, final int orderId, AbilityTarget target) { + return true; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + return caster.getFollowBehavior().reset(OrderIds.move, (CUnit) target); + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + if (orderId == OrderIds.patrol) { + final CBehavior patrolBehavior = caster.getPatrolBehavior().reset(point); + caster.setDefaultBehavior(patrolBehavior); + return patrolBehavior; + } + else { + return caster.getMoveBehavior().reset(OrderIds.move, point); + } + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + if (orderId == OrderIds.holdposition) { + caster.setDefaultBehavior(caster.getHoldPositionBehavior()); + } + return caster.pollNextOrderBehavior(game); + } + + @Override + public T visit(final CAbilityVisitor visitor) { + return visitor.accept(this); + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityToggleableView.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityToggleableView.java new file mode 100644 index 0000000..810dd9f --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityToggleableView.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities; + +public interface CAbilityToggleableView extends CAbilityView { + boolean isActive(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityView.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityView.java new file mode 100644 index 0000000..05cd811 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityView.java @@ -0,0 +1,28 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; + +public interface CAbilityView { + void checkCanUse(CSimulation game, CUnit unit, int orderId, AbilityActivationReceiver receiver); + + void checkCanTarget(CSimulation game, CUnit unit, int orderId, CWidget target, + AbilityTargetCheckReceiver receiver); + + void checkCanTarget(CSimulation game, CUnit unit, int orderId, AbilityPointTarget target, + AbilityTargetCheckReceiver receiver); + + void checkCanTargetNoTarget(CSimulation game, CUnit unit, int orderId, AbilityTargetCheckReceiver receiver); + + int getHandleId(); + + boolean isDisabled(); + + boolean isIconShowing(); + + T visit(CAbilityVisitor visitor); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityVisitor.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityVisitor.java new file mode 100644 index 0000000..b2ec6e3 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/CAbilityVisitor.java @@ -0,0 +1,57 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityBuildInProgress; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityHumanBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityNagaBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityNeutralBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityNightElfBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityOrcBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityUndeadBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.combat.CAbilityColdArrows; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic.GenericNoIconAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic.GenericSingleIconActiveAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.hero.CAbilityHero; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.queue.CAbilityQueue; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.queue.CAbilityRally; + +/** + * A visitor for the lowest level inherent types of an ability. It's a bit of a + * design clash to have the notion of an ability visitor pattern while also + * having any arbitrary number of "ability types" defined in config files. But + * the way that we will handle this for now will be with the notion of a generic + * ability (one whose UI information and behaviors come from a rawcode) versus + * abilities with engine-level type information (move, stop, attack). + */ +public interface CAbilityVisitor { + T accept(CAbilityAttack ability); + + T accept(CAbilityMove ability); + + T accept(CAbilityOrcBuild ability); + + T accept(CAbilityHumanBuild ability); + + T accept(CAbilityUndeadBuild ability); + + T accept(CAbilityNightElfBuild ability); + + T accept(CAbilityGeneric ability); + + T accept(CAbilityColdArrows ability); + + T accept(CAbilityNagaBuild ability); + + T accept(CAbilityNeutralBuild ability); + + T accept(CAbilityBuildInProgress ability); + + T accept(CAbilityQueue ability); + + T accept(GenericSingleIconActiveAbility ability); + + T accept(CAbilityRally ability); + + T accept(GenericNoIconAbility ability); + + T accept(CAbilityHero ability); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/AbstractCAbilityBuild.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/AbstractCAbilityBuild.java new file mode 100644 index 0000000..811ca64 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/AbstractCAbilityBuild.java @@ -0,0 +1,131 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitTypeRequirement; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.AbstractCAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.menu.CAbilityMenu; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.ResourceType; + +public abstract class AbstractCAbilityBuild extends AbstractCAbility implements CAbilityMenu { + private static boolean REFUND_ON_ORDER_CANCEL = false; + private final Set structuresBuilt; + + public AbstractCAbilityBuild(final int handleId, final List structuresBuilt) { + super(handleId); + this.structuresBuilt = new LinkedHashSet<>(structuresBuilt); + } + + public Collection getStructuresBuilt() { + return this.structuresBuilt; + } + + @Override + protected void innerCheckCanUse(final CSimulation game, final CUnit unit, final int orderId, + final AbilityActivationReceiver receiver) { + final War3ID orderIdAsRawtype = new War3ID(orderId); + if (this.structuresBuilt.contains(orderIdAsRawtype)) { + final CUnitType unitType = game.getUnitData().getUnitType(orderIdAsRawtype); + if (unitType != null) { + final CPlayer player = game.getPlayer(unit.getPlayerIndex()); + final List requirements = unitType.getRequirements(); + boolean requirementsMet = true; + for (final CUnitTypeRequirement requirement : requirements) { + if (player.getTechtreeUnlocked(requirement.getRequirement()) < requirement.getRequiredLevel()) { + requirementsMet = false; + } + } + if (requirementsMet) { + if (player.getGold() >= unitType.getGoldCost()) { + if (player.getLumber() >= unitType.getLumberCost()) { + if ((unitType.getFoodUsed() == 0) + || ((player.getFoodUsed() + unitType.getFoodUsed()) <= player.getFoodCap())) { + + receiver.useOk(); + } + else { + receiver.notEnoughResources(ResourceType.FOOD); + } + } + else { + receiver.notEnoughResources(ResourceType.LUMBER); + } + } + else { + receiver.notEnoughResources(ResourceType.GOLD); + } + } + else { + for (final CUnitTypeRequirement requirement : requirements) { + receiver.missingRequirement(requirement.getRequirement(), requirement.getRequiredLevel()); + } + } + } + else { + receiver.useOk(); + } + } + else { + /// ??? + receiver.useOk(); + } + } + + @Override + public final void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, final CWidget target, + final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public final void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + if (this.structuresBuilt.contains(new War3ID(orderId))) { + receiver.targetOk(target); + } + else { + receiver.orderIdNotAccepted(); + } + } + + @Override + public final void checkCanTargetNoTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public boolean checkBeforeQueue(final CSimulation game, final CUnit caster, final int orderId, AbilityTarget target) { + return true; + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + if (REFUND_ON_ORDER_CANCEL) { + final CPlayer player = game.getPlayer(unit.getPlayerIndex()); + final War3ID orderIdAsRawtype = new War3ID(orderId); + final CUnitType unitType = game.getUnitData().getUnitType(orderIdAsRawtype); + player.refundFor(unitType); + if (unitType.getFoodUsed() != 0) { + player.setFoodUsed(player.getFoodUsed() - unitType.getFoodUsed()); + } + } + } + + @Override + public void onTick(final CSimulation game, final CUnit unit) { + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityBuildInProgress.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityBuildInProgress.java new file mode 100644 index 0000000..7c9c085 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityBuildInProgress.java @@ -0,0 +1,100 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.AbstractCAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; + +public class CAbilityBuildInProgress extends AbstractCAbility { + + public CAbilityBuildInProgress(final int handleId) { + super(handleId); + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + + } + + @Override + public void onTick(final CSimulation game, final CUnit unit) { + } + + @Override + public boolean checkBeforeQueue(final CSimulation game, final CUnit caster, final int orderId, AbilityTarget target) { + final CPlayer player = game.getPlayer(caster.getPlayerIndex()); + player.refundFor(caster.getUnitType()); + caster.setLife(game, 0); + return false; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + return null; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + return null; + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + return null; + } + + @Override + protected void innerCheckCanUse(final CSimulation game, final CUnit unit, final int orderId, + final AbilityActivationReceiver receiver) { + receiver.useOk(); + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, final CWidget target, + final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public void checkCanTargetNoTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityTargetCheckReceiver receiver) { + if (orderId == OrderIds.cancel) { + receiver.targetOk(null); + } + else { + receiver.orderIdNotAccepted(); + } + } + + @Override + public T visit(final CAbilityVisitor visitor) { + return visitor.accept(this); + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + // TODO Auto-generated method stub + + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityHumanBuild.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityHumanBuild.java new file mode 100644 index 0000000..b6f420f --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityHumanBuild.java @@ -0,0 +1,68 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build; + +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; + +public class CAbilityHumanBuild extends AbstractCAbilityBuild { + + public CAbilityHumanBuild(final int handleId, final List structuresBuilt) { + super(handleId, structuresBuilt); + // TODO Auto-generated constructor stub + } + + @Override + public int getBaseOrderId() { + return OrderIds.humanbuild; + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + // TODO Auto-generated method stub + + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + // TODO Auto-generated method stub + + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { +// caster.getMoveBehavior().reset(point.x, point.y, ) + return null; + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + // TODO Auto-generated method stub + return null; + } + + @Override + public T visit(final CAbilityVisitor visitor) { + return visitor.accept(this); + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + // TODO Auto-generated method stub + + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityNagaBuild.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityNagaBuild.java new file mode 100644 index 0000000..57587a6 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityNagaBuild.java @@ -0,0 +1,60 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build; + +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; + +public class CAbilityNagaBuild extends AbstractCAbilityBuild { + + public CAbilityNagaBuild(final int handleId, final List structuresBuilt) { + super(handleId, structuresBuilt); + } + + @Override + public T visit(final CAbilityVisitor visitor) { + return visitor.accept(this); + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public int getBaseOrderId() { + return OrderIds.nagabuild; + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + // TODO Auto-generated method stub + + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityNeutralBuild.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityNeutralBuild.java new file mode 100644 index 0000000..bbfb9de --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityNeutralBuild.java @@ -0,0 +1,60 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build; + +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; + +public class CAbilityNeutralBuild extends AbstractCAbilityBuild { + + public CAbilityNeutralBuild(final int handleId, final List structuresBuilt) { + super(handleId, structuresBuilt); + } + + @Override + public T visit(final CAbilityVisitor visitor) { + return visitor.accept(this); + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public int getBaseOrderId() { + return OrderIds.buildmenu; + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + // TODO Auto-generated method stub + + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityNightElfBuild.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityNightElfBuild.java new file mode 100644 index 0000000..9fd0a2a --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityNightElfBuild.java @@ -0,0 +1,77 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build; + +import java.awt.image.BufferedImage; +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.build.CBehaviorOrcBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayer; + +public class CAbilityNightElfBuild extends AbstractCAbilityBuild { + private CBehaviorOrcBuild buildBehavior; + + public CAbilityNightElfBuild(final int handleId, final List structuresBuilt) { + super(handleId, structuresBuilt); + } + + @Override + public T visit(final CAbilityVisitor visitor) { + return visitor.accept(this); + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + this.buildBehavior = new CBehaviorOrcBuild(unit); + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + final War3ID orderIdAsRawtype = new War3ID(orderId); + final CUnitType unitType = game.getUnitData().getUnitType(orderIdAsRawtype); + final BufferedImage buildingPathingPixelMap = unitType.getBuildingPathingPixelMap(); + if (buildingPathingPixelMap != null) { + point.x = (float) Math.floor(point.x / 64f) * 64f; + point.y = (float) Math.floor(point.y / 64f) * 64f; + if (((buildingPathingPixelMap.getWidth() / 2) % 2) == 1) { + point.x += 32f; + } + if (((buildingPathingPixelMap.getHeight() / 2) % 2) == 1) { + point.y += 32f; + } + } + final CPlayer player = game.getPlayer(caster.getPlayerIndex()); + player.chargeFor(unitType); + if (unitType.getFoodUsed() != 0) { + player.setFoodUsed(player.getFoodUsed() + unitType.getFoodUsed()); + } + return this.buildBehavior.reset(point, orderId, getBaseOrderId()); + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public int getBaseOrderId() { + return OrderIds.nightelfbuild; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityOrcBuild.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityOrcBuild.java new file mode 100644 index 0000000..37a9f57 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityOrcBuild.java @@ -0,0 +1,77 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build; + +import java.awt.image.BufferedImage; +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.build.CBehaviorOrcBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayer; + +public class CAbilityOrcBuild extends AbstractCAbilityBuild { + private CBehaviorOrcBuild buildBehavior; + + public CAbilityOrcBuild(final int handleId, final List structuresBuilt) { + super(handleId, structuresBuilt); + } + + @Override + public T visit(final CAbilityVisitor visitor) { + return visitor.accept(this); + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + this.buildBehavior = new CBehaviorOrcBuild(unit); + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + final War3ID orderIdAsRawtype = new War3ID(orderId); + final CUnitType unitType = game.getUnitData().getUnitType(orderIdAsRawtype); + final BufferedImage buildingPathingPixelMap = unitType.getBuildingPathingPixelMap(); + if (buildingPathingPixelMap != null) { + point.x = (float) Math.floor(point.x / 64f) * 64f; + point.y = (float) Math.floor(point.y / 64f) * 64f; + if (((buildingPathingPixelMap.getWidth() / 2) % 2) == 1) { + point.x += 32f; + } + if (((buildingPathingPixelMap.getHeight() / 2) % 2) == 1) { + point.y += 32f; + } + } + final CPlayer player = game.getPlayer(caster.getPlayerIndex()); + player.chargeFor(unitType); + if (unitType.getFoodUsed() != 0) { + player.setFoodUsed(player.getFoodUsed() + unitType.getFoodUsed()); + } + return this.buildBehavior.reset(point, orderId, getBaseOrderId()); + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public int getBaseOrderId() { + return OrderIds.orcbuild; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityUndeadBuild.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityUndeadBuild.java new file mode 100644 index 0000000..8d5a8f7 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/build/CAbilityUndeadBuild.java @@ -0,0 +1,77 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build; + +import java.awt.image.BufferedImage; +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.build.CBehaviorUndeadBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayer; + +public class CAbilityUndeadBuild extends AbstractCAbilityBuild { + private CBehaviorUndeadBuild buildBehavior; + + public CAbilityUndeadBuild(final int handleId, final List structuresBuilt) { + super(handleId, structuresBuilt); + } + + @Override + public T visit(final CAbilityVisitor visitor) { + return visitor.accept(this); + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + this.buildBehavior = new CBehaviorUndeadBuild(unit); + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + final War3ID orderIdAsRawtype = new War3ID(orderId); + final CUnitType unitType = game.getUnitData().getUnitType(orderIdAsRawtype); + final BufferedImage buildingPathingPixelMap = unitType.getBuildingPathingPixelMap(); + if (buildingPathingPixelMap != null) { + point.x = (float) Math.floor(point.x / 64f) * 64f; + point.y = (float) Math.floor(point.y / 64f) * 64f; + if (((buildingPathingPixelMap.getWidth() / 2) % 2) == 1) { + point.x += 32f; + } + if (((buildingPathingPixelMap.getHeight() / 2) % 2) == 1) { + point.y += 32f; + } + } + final CPlayer player = game.getPlayer(caster.getPlayerIndex()); + player.chargeFor(unitType); + if (unitType.getFoodUsed() != 0) { + player.setFoodUsed(player.getFoodUsed() + unitType.getFoodUsed()); + } + return this.buildBehavior.reset(point, orderId, getBaseOrderId()); + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public int getBaseOrderId() { + return OrderIds.undeadbuild; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/combat/CAbilityColdArrows.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/combat/CAbilityColdArrows.java new file mode 100644 index 0000000..14dd9a0 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/combat/CAbilityColdArrows.java @@ -0,0 +1,136 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.combat; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.AbstractCAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorAttackListener; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttack; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; + +/** + * Represents an ability from the object data + */ +public class CAbilityColdArrows extends AbstractCAbility { + private final War3ID rawcode; + private boolean autoCastActive; + + public CAbilityColdArrows(final War3ID rawcode, final int handleId) { + super(handleId); + this.rawcode = rawcode; + } + + @Override + protected void innerCheckCanUse(final CSimulation game, final CUnit unit, final int orderId, + final AbilityActivationReceiver receiver) { + receiver.useOk(); + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, final CWidget target, + final AbilityTargetCheckReceiver receiver) { + switch (orderId) { + case OrderIds.coldarrowstarg: + receiver.targetOk(target); + break; + default: + receiver.orderIdNotAccepted(); + break; + } + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public void checkCanTargetNoTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityTargetCheckReceiver receiver) { + switch (orderId) { + case OrderIds.coldarrows: + case OrderIds.uncoldarrows: + receiver.targetOk(null); + break; + default: + receiver.orderIdNotAccepted(); + break; + } + } + + public War3ID getRawcode() { + return this.rawcode; + } + + public boolean isAutoCastActive() { + return this.autoCastActive; + } + + @Override + public T visit(final CAbilityVisitor visitor) { + return visitor.accept(this); + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + } + + @Override + public void onTick(final CSimulation game, final CUnit unit) { + } + + @Override + public boolean checkBeforeQueue(final CSimulation game, final CUnit caster, final int orderId, AbilityTarget target) { + switch (orderId) { + case OrderIds.coldarrows: + case OrderIds.uncoldarrows: + this.autoCastActive = !this.autoCastActive; + return false; + default: + return true; + } + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + CBehavior behavior = null; + for (final CUnitAttack attack : caster.getAttacks()) { + if (target.canBeTargetedBy(game, caster, attack.getTargetsAllowed())) { + behavior = caster.getAttackBehavior().reset(OrderIds.coldarrowstarg, attack, target, false, + CBehaviorAttackListener.DO_NOTHING); + break; + } + } + if (behavior != null) { + return behavior; + } + return caster.pollNextOrderBehavior(game); + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/generic/AbstractGenericNoIconAbility.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/generic/AbstractGenericNoIconAbility.java new file mode 100644 index 0000000..8abdcae --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/generic/AbstractGenericNoIconAbility.java @@ -0,0 +1,34 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.AbstractCAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; + +public abstract class AbstractGenericNoIconAbility extends AbstractCAbility implements GenericNoIconAbility { + private final War3ID alias; + + public AbstractGenericNoIconAbility(final int handleId, final War3ID alias) { + super(handleId); + this.alias = alias; + } + + @Override + public boolean checkBeforeQueue(final CSimulation game, final CUnit caster, final int orderId, + final AbilityTarget target) { + return true; + } + + @Override + public T visit(final CAbilityVisitor visitor) { + return visitor.accept(this); + } + + @Override + public War3ID getAlias() { + return this.alias; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/generic/AbstractGenericSingleIconActiveAbility.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/generic/AbstractGenericSingleIconActiveAbility.java new file mode 100644 index 0000000..169f4ce --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/generic/AbstractGenericSingleIconActiveAbility.java @@ -0,0 +1,92 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.AbstractCAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; + +public abstract class AbstractGenericSingleIconActiveAbility extends AbstractCAbility + implements GenericSingleIconActiveAbility { + private final War3ID alias; + + public AbstractGenericSingleIconActiveAbility(final int handleId, final War3ID alias) { + super(handleId); + this.alias = alias; + } + + @Override + public boolean checkBeforeQueue(final CSimulation game, final CUnit caster, final int orderId, AbilityTarget target) { + return true; + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, final CWidget target, + final AbilityTargetCheckReceiver receiver) { + if (orderId == getBaseOrderId()) { + innerCheckCanTarget(game, unit, orderId, target, receiver); + } + else if (orderId == OrderIds.smart) { + innerCheckCanSmartTarget(game, unit, orderId, target, receiver); + } + else { + receiver.orderIdNotAccepted(); + } + } + + protected abstract void innerCheckCanTarget(CSimulation game, CUnit unit, int orderId, CWidget target, + AbilityTargetCheckReceiver receiver); + + protected abstract void innerCheckCanSmartTarget(CSimulation game, CUnit unit, int orderId, CWidget target, + AbilityTargetCheckReceiver receiver); + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + if (orderId == getBaseOrderId()) { + innerCheckCanTarget(game, unit, orderId, target, receiver); + } + else if (orderId == OrderIds.smart) { + innerCheckCanSmartTarget(game, unit, orderId, target, receiver); + } + else { + receiver.orderIdNotAccepted(); + } + } + + protected abstract void innerCheckCanTarget(CSimulation game, CUnit unit, int orderId, AbilityPointTarget target, + AbilityTargetCheckReceiver receiver); + + protected abstract void innerCheckCanSmartTarget(CSimulation game, CUnit unit, int orderId, + AbilityPointTarget target, AbilityTargetCheckReceiver receiver); + + @Override + public void checkCanTargetNoTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityTargetCheckReceiver receiver) { + if (orderId == getBaseOrderId()) { + innerCheckCanTargetNoTarget(game, unit, orderId, receiver); + } + else { + receiver.orderIdNotAccepted(); + } + } + + protected abstract void innerCheckCanTargetNoTarget(CSimulation game, CUnit unit, int orderId, + AbilityTargetCheckReceiver receiver); + + @Override + public T visit(final CAbilityVisitor visitor) { + return visitor.accept(this); + } + + @Override + public War3ID getAlias() { + return this.alias; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/generic/AbstractGenericSingleIconNoSmartActiveAbility.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/generic/AbstractGenericSingleIconNoSmartActiveAbility.java new file mode 100644 index 0000000..aa26147 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/generic/AbstractGenericSingleIconNoSmartActiveAbility.java @@ -0,0 +1,28 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; + +public abstract class AbstractGenericSingleIconNoSmartActiveAbility extends AbstractGenericSingleIconActiveAbility { + + public AbstractGenericSingleIconNoSmartActiveAbility(final int handleId, final War3ID alias) { + super(handleId, alias); + } + + @Override + protected void innerCheckCanSmartTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + protected void innerCheckCanSmartTarget(final CSimulation game, final CUnit unit, final int orderId, + final CWidget target, final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/generic/GenericNoIconAbility.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/generic/GenericNoIconAbility.java new file mode 100644 index 0000000..fb6dd4c --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/generic/GenericNoIconAbility.java @@ -0,0 +1,8 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; + +public interface GenericNoIconAbility extends CAbility { + War3ID getAlias(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/generic/GenericSingleIconActiveAbility.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/generic/GenericSingleIconActiveAbility.java new file mode 100644 index 0000000..5a9f8ce --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/generic/GenericSingleIconActiveAbility.java @@ -0,0 +1,12 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; + +public interface GenericSingleIconActiveAbility extends CAbility { + War3ID getAlias(); + + int getBaseOrderId(); + + boolean isToggleOn(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/harvest/CAbilityHarvest.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/harvest/CAbilityHarvest.java new file mode 100644 index 0000000..92bf056 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/harvest/CAbilityHarvest.java @@ -0,0 +1,228 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.harvest; + +import java.util.EnumSet; +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CDestructable; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic.AbstractGenericSingleIconActiveAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.mine.CAbilityGoldMine; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.harvest.CBehaviorHarvest; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.harvest.CBehaviorReturnResources; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttack; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackNormal; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.ResourceType; + +public class CAbilityHarvest extends AbstractGenericSingleIconActiveAbility { + private final int damageToTree; + private final int goldCapacity; + private final int lumberCapacity; + private final float castRange; + private final float duration; + private CBehaviorHarvest behaviorHarvest; + private CBehaviorReturnResources behaviorReturnResources; + private int carriedResourceAmount; + private ResourceType carriedResourceType; + private CUnitAttack treeAttack; + private CWidget lastHarvestTarget; + + public CAbilityHarvest(final int handleId, final War3ID alias, final int damageToTree, final int goldCapacity, + final int lumberCapacity, final float castRange, final float duration) { + super(handleId, alias); + this.damageToTree = damageToTree; + this.goldCapacity = goldCapacity; + this.lumberCapacity = lumberCapacity; + this.castRange = castRange; + this.duration = duration; + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + this.behaviorHarvest = new CBehaviorHarvest(unit, this); + this.behaviorReturnResources = new CBehaviorReturnResources(unit, this); + + final List unitAttacks = unit.getAttacks(); + CUnitAttack bestFitTreeAttack = null; + for (final CUnitAttack attack : unitAttacks) { + if (attack.getTargetsAllowed().contains(CTargetType.TREE)) { + bestFitTreeAttack = attack; + } + } + this.treeAttack = new CUnitAttackNormal( + bestFitTreeAttack == null ? 0.433f : bestFitTreeAttack.getAnimationBackswingPoint(), + bestFitTreeAttack == null ? 0.433f : bestFitTreeAttack.getAnimationDamagePoint(), CAttackType.NORMAL, + this.duration, 0, 1, this.damageToTree * 2, 0, (int) this.castRange, + bestFitTreeAttack == null ? 250 : bestFitTreeAttack.getRangeMotionBuffer(), + bestFitTreeAttack == null ? false : bestFitTreeAttack.isShowUI(), + bestFitTreeAttack == null ? EnumSet.of(CTargetType.TREE) : bestFitTreeAttack.getTargetsAllowed(), + bestFitTreeAttack == null ? "AxeMediumChop" : bestFitTreeAttack.getWeaponSound(), + bestFitTreeAttack == null ? CWeaponType.NORMAL : bestFitTreeAttack.getWeaponType()); + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + } + + @Override + public void onTick(final CSimulation game, final CUnit unit) { + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + return this.behaviorHarvest.reset(target); + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + return caster.pollNextOrderBehavior(game); + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + if (isToggleOn() && (orderId == OrderIds.returnresources)) { + return this.behaviorReturnResources.reset(game); + } + return caster.pollNextOrderBehavior(game); + } + + @Override + public int getBaseOrderId() { + return isToggleOn() ? OrderIds.returnresources : OrderIds.harvest; + } + + @Override + public boolean isToggleOn() { + return this.carriedResourceAmount > 0; + } + + @Override + protected void innerCheckCanUse(final CSimulation game, final CUnit unit, final int orderId, + final AbilityActivationReceiver receiver) { + receiver.useOk(); + } + + @Override + protected void innerCheckCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final CWidget target, final AbilityTargetCheckReceiver receiver) { + if (target instanceof CUnit) { + final CUnit targetUnit = (CUnit) target; + for (final CAbility ability : targetUnit.getAbilities()) { + if (ability instanceof CAbilityGoldMine) { + receiver.targetOk(target); + return; + } + else if ((this.carriedResourceType != null) && (ability instanceof CAbilityReturnResources)) { + final CAbilityReturnResources abilityReturn = (CAbilityReturnResources) ability; + if (abilityReturn.accepts(this.carriedResourceType)) { + receiver.targetOk(target); + return; + } + } + } + receiver.mustTargetResources(); + } + else if (target instanceof CDestructable) { + if (target.canBeTargetedBy(game, unit, this.treeAttack.getTargetsAllowed())) { + receiver.targetOk(target); + } + else { + receiver.mustTargetResources(); + } + } + else { + receiver.mustTargetResources(); + } + } + + @Override + protected void innerCheckCanSmartTarget(final CSimulation game, final CUnit unit, final int orderId, + final CWidget target, final AbilityTargetCheckReceiver receiver) { + innerCheckCanTarget(game, unit, orderId, target, receiver); + } + + @Override + protected void innerCheckCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + protected void innerCheckCanSmartTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + protected void innerCheckCanTargetNoTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityTargetCheckReceiver receiver) { + if ((orderId == OrderIds.returnresources) && isToggleOn()) { + receiver.targetOk(null); + } + else { + receiver.orderIdNotAccepted(); + } + } + + public int getDamageToTree() { + return this.damageToTree; + } + + public int getGoldCapacity() { + return this.goldCapacity; + } + + public int getLumberCapacity() { + return this.lumberCapacity; + } + + public int getCarriedResourceAmount() { + return this.carriedResourceAmount; + } + + public ResourceType getCarriedResourceType() { + return this.carriedResourceType; + } + + public void setCarriedResources(final ResourceType carriedResourceType, final int carriedResourceAmount) { + this.carriedResourceType = carriedResourceType; + this.carriedResourceAmount = carriedResourceAmount; + } + + public CBehaviorHarvest getBehaviorHarvest() { + return this.behaviorHarvest; + } + + public CBehaviorReturnResources getBehaviorReturnResources() { + return this.behaviorReturnResources; + } + + public CUnitAttack getTreeAttack() { + return this.treeAttack; + } + + public void setLastHarvestTarget(final CWidget lastHarvestTarget) { + this.lastHarvestTarget = lastHarvestTarget; + } + + public CWidget getLastHarvestTarget() { + return this.lastHarvestTarget; + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/harvest/CAbilityReturnResources.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/harvest/CAbilityReturnResources.java new file mode 100644 index 0000000..c3d1051 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/harvest/CAbilityReturnResources.java @@ -0,0 +1,92 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.harvest; + +import java.util.EnumSet; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic.AbstractGenericNoIconAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.ResourceType; + +/** + * Was probably named CAbilityReturn in 2002, idk + */ +public class CAbilityReturnResources extends AbstractGenericNoIconAbility { + private final EnumSet acceptedResourceTypes; + + public CAbilityReturnResources(final int handleId, final War3ID alias, + final EnumSet acceptedResourceTypes) { + super(handleId, alias); + this.acceptedResourceTypes = acceptedResourceTypes; + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + + } + + @Override + public void onTick(final CSimulation game, final CUnit unit) { + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + return null; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + return null; + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + return null; + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, final CWidget target, + final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public void checkCanTargetNoTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + protected void innerCheckCanUse(final CSimulation game, final CUnit unit, final int orderId, + final AbilityActivationReceiver receiver) { + receiver.notAnActiveAbility(); + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + } + + public boolean accepts(final ResourceType resourceType) { + if (isDisabled()) { + return false; + } + return this.acceptedResourceTypes.contains(resourceType); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/hero/CAbilityHero.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/hero/CAbilityHero.java new file mode 100644 index 0000000..28e2e40 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/hero/CAbilityHero.java @@ -0,0 +1,276 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.hero; + +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CGameplayConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.AbstractCAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttack; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; + +public class CAbilityHero extends AbstractCAbility { + private final List skillsAvailable; + private int xp; + private int heroLevel; + private int skillPoints; + + private HeroStatValue strength; + private HeroStatValue agility; + private HeroStatValue intelligence; + private String properName; + + public CAbilityHero(final int handleId, final List skillsAvailable) { + super(handleId); + this.skillsAvailable = skillsAvailable; + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + this.heroLevel = 1; + this.xp = 0; + final CUnitType unitType = unit.getUnitType(); + this.strength = new HeroStatValue(unitType.getStartingStrength(), unitType.getStrengthPerLevel()); + this.agility = new HeroStatValue(unitType.getStartingAgility(), unitType.getAgilityPerLevel()); + this.intelligence = new HeroStatValue(unitType.getStartingIntelligence(), unitType.getIntelligencePerLevel()); + calculateDerivatedFields(game, unit); + + final int nameIndex = game.getSeededRandom().nextInt(unitType.getProperNamesCount()); + + String properName; + final List heroProperNames = unitType.getHeroProperNames(); + if (heroProperNames.size() > 0) { + if (nameIndex < heroProperNames.size()) { + properName = heroProperNames.get(nameIndex); + } + else { + properName = heroProperNames.get(heroProperNames.size() - 1); + } + } + else { + properName = WarsmashConstants.DEFAULT_STRING; + } + this.properName = properName; + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + + } + + @Override + public void onTick(final CSimulation game, final CUnit unit) { + + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + + } + + @Override + public boolean checkBeforeQueue(final CSimulation game, final CUnit caster, final int orderId, AbilityTarget target) { + return true; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + return null; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + return null; + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + return null; + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, final CWidget target, + final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public void checkCanTargetNoTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public T visit(final CAbilityVisitor visitor) { + return visitor.accept(this); + } + + @Override + protected void innerCheckCanUse(final CSimulation game, final CUnit unit, final int orderId, + final AbilityActivationReceiver receiver) { + receiver.useOk(); + } + + public int getSkillPoints() { + return this.skillPoints; + } + + public void setSkillPoints(final int skillPoints) { + this.skillPoints = skillPoints; + } + + public int getXp() { + return this.xp; + } + + public void setXp(final int xp) { + this.xp = xp; + } + + public int getHeroLevel() { + return this.heroLevel; + } + + public void setHeroLevel(final int level) { + this.heroLevel = level; + } + + public HeroStatValue getStrength() { + return this.strength; + } + + public HeroStatValue getAgility() { + return this.agility; + } + + public HeroStatValue getIntelligence() { + return this.intelligence; + } + + public String getProperName() { + return this.properName; + } + + public void addXp(final CSimulation simulation, final CUnit unit, final int xp) { + this.xp += xp; + final CGameplayConstants gameplayConstants = simulation.getGameplayConstants(); + while ((this.heroLevel < gameplayConstants.getMaxHeroLevel()) + && (this.xp >= gameplayConstants.getNeedHeroXPSum(this.heroLevel))) { + this.heroLevel++; + this.skillPoints++; + calculateDerivatedFields(simulation, unit); + simulation.unitGainLevelEvent(unit); + } + unit.internalPublishHeroStatsChanged(); + } + + private HeroStatValue getStat(final CPrimaryAttribute attribute) { + switch (attribute) { + case AGILITY: + return this.agility; + case INTELLIGENCE: + return this.intelligence; + default: + case STRENGTH: + return this.strength; + } + } + + private void calculateDerivatedFields(final CSimulation game, final CUnit unit) { + final CGameplayConstants gameplayConstants = game.getGameplayConstants(); + final int prevStrength = this.strength.getCurrent(); + final int prevAgility = this.agility.getCurrent(); + final int prevIntelligence = this.intelligence.getCurrent(); + this.strength.calculate(this.heroLevel); + this.agility.calculate(this.heroLevel); + this.intelligence.calculate(this.heroLevel); + final int deltaStrength = this.strength.getCurrent() - prevStrength; + final int deltaIntelligence = this.intelligence.getCurrent() - prevIntelligence; + final int currentAgility = this.agility.getCurrent(); + final int deltaAgility = currentAgility - prevAgility; + + final int primaryAttribute = getStat(unit.getUnitType().getPrimaryAttribute()).getCurrent(); + for (final CUnitAttack attack : unit.getUnitSpecificAttacks()) { + attack.setPrimaryAttributeDamageBonus((int) (primaryAttribute * gameplayConstants.getStrAttackBonus())); + } + + final float hitPointIncrease = gameplayConstants.getStrHitPointBonus() * deltaStrength; + final int oldMaximumLife = unit.getMaximumLife(); + final float oldLife = unit.getLife(); + final int newMaximumLife = Math.round(oldMaximumLife + hitPointIncrease); + final float newLife = (oldLife * (newMaximumLife)) / oldMaximumLife; + unit.setMaximumLife(newMaximumLife); + unit.setLife(game, newLife); + + final float manaPointIncrease = gameplayConstants.getIntManaBonus() * deltaIntelligence; + final int oldMaximumMana = unit.getMaximumMana(); + final float oldMana = unit.getMana(); + final int newMaximumMana = Math.round(oldMaximumMana + manaPointIncrease); + final float newMana = (oldMana * (newMaximumMana)) / oldMaximumMana; + unit.setMaximumMana(newMaximumMana); + unit.setMana(newMana); + + final int agilityDefenseBonus = Math.round( + gameplayConstants.getAgiDefenseBase() + (gameplayConstants.getAgiDefenseBonus() * currentAgility)); + unit.setAgilityDefenseBonus(agilityDefenseBonus); + } + + public static final class HeroStatValue { + private final float perLevelFactor; + private int base; + private int bonus; + private int currentBase; + private int current; + + private HeroStatValue(final int base, final float perLevelFactor) { + this.base = base; + this.perLevelFactor = perLevelFactor; + } + + public void calculate(final int level) { + this.currentBase = this.base + (int) ((level - 1) * this.perLevelFactor); + this.current = this.currentBase + this.bonus; + } + + public void setBase(final int base) { + this.base = base; + } + + public void setBonus(final int bonus) { + this.bonus = bonus; + } + + public int getCurrent() { + return this.current; + } + + public String getDisplayText() { + String text = Integer.toString(this.currentBase); + if (this.bonus != 0) { + if (this.bonus > 0) { + text += "|cFF00FF00 (+" + this.bonus + ")"; + } + else { + text += "|cFFFF0000 (+" + this.bonus + ")"; + } + } + return text; + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/hero/CPrimaryAttribute.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/hero/CPrimaryAttribute.java new file mode 100644 index 0000000..f00d4d7 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/hero/CPrimaryAttribute.java @@ -0,0 +1,23 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.hero; + +public enum CPrimaryAttribute { + STRENGTH, + INTELLIGENCE, + AGILITY; + + public static CPrimaryAttribute parsePrimaryAttribute(final String targetTypeString) { + if (targetTypeString == null) { + return STRENGTH; + } + switch (targetTypeString.toUpperCase()) { + case "STR": + return STRENGTH; + case "INT": + return INTELLIGENCE; + case "AGI": + return AGILITY; + default: + return STRENGTH; + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/inventory/CAbilityInventory.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/inventory/CAbilityInventory.java new file mode 100644 index 0000000..08da64d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/inventory/CAbilityInventory.java @@ -0,0 +1,240 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.inventory; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItemType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic.AbstractGenericNoIconAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.inventory.CBehaviorDropItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.inventory.CBehaviorGetItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; + +public class CAbilityInventory extends AbstractGenericNoIconAbility { + private final boolean canDropItems; + private final boolean canGetItems; + private final boolean canUseItems; + private final boolean dropItemsOnDeath; + private final CItem[] itemsHeld; + private CBehaviorGetItem behaviorGetItem; + private CBehaviorDropItem behaviorDropItem; + + public CAbilityInventory(final int handleId, final War3ID alias, final boolean canDropItems, + final boolean canGetItems, final boolean canUseItems, final boolean dropItemsOnDeath, + final int itemCapacity) { + super(handleId, alias); + this.canDropItems = canDropItems; + this.canGetItems = canGetItems; + this.canUseItems = canUseItems; + this.dropItemsOnDeath = dropItemsOnDeath; + this.itemsHeld = new CItem[itemCapacity]; + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + this.behaviorGetItem = new CBehaviorGetItem(unit, this); + this.behaviorDropItem = new CBehaviorDropItem(unit, this); + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + + } + + @Override + public void onTick(final CSimulation game, final CUnit unit) { + + } + + @Override + public boolean checkBeforeQueue(final CSimulation game, final CUnit caster, final int orderId, + final AbilityTarget target) { + if ((orderId >= OrderIds.itemdrag00) && (orderId <= OrderIds.itemdrag05)) { + for (int i = 0; i < this.itemsHeld.length; i++) { + if (this.itemsHeld[i] == target) { + final CItem temp = this.itemsHeld[i]; + final int dragDropDestinationIndex = orderId - OrderIds.itemdrag00; + this.itemsHeld[i] = this.itemsHeld[dragDropDestinationIndex]; + this.itemsHeld[dragDropDestinationIndex] = temp; + return false; + } + } + } + return super.checkBeforeQueue(game, caster, orderId, target); + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + + } + + public int getItemCapacity() { + return this.itemsHeld.length; + } + + public CItem getItemInSlot(final int slotIndex) { + if ((slotIndex < 0) || (slotIndex >= this.itemsHeld.length)) { + return null; + } + return this.itemsHeld[slotIndex]; + } + + public boolean isDropItemsOnDeath() { + return this.dropItemsOnDeath; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + return this.behaviorGetItem.reset((CItem) target); + } + + public CBehavior beginDropItem(final CSimulation game, final CUnit caster, final int orderId, + final CItem itemToDrop, final AbilityPointTarget target) { + return this.behaviorDropItem.reset(itemToDrop, target); + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, final CWidget target, + final AbilityTargetCheckReceiver receiver) { + if (((orderId == OrderIds.getitem) || (orderId == OrderIds.smart)) && !target.isDead()) { + if (target instanceof CItem) { + final CItem targetItem = (CItem) target; + if (!targetItem.isHidden()) { + receiver.targetOk(target); + } + else { + receiver.orderIdNotAccepted(); + } + } + else { + receiver.orderIdNotAccepted(); + } + } + else { + if ((orderId >= OrderIds.itemdrag00) && (orderId <= OrderIds.itemdrag05)) { + if (target instanceof CItem) { + final int slot = getSlot((CItem) target); + if (slot != -1) { + receiver.targetOk(target); + } + else { + receiver.orderIdNotAccepted(); + } + } + else { + receiver.orderIdNotAccepted(); + } + } + receiver.orderIdNotAccepted(); + } + } + + public int getSlot(final CItem target) { + int slot = -1; + for (int i = 0; i < this.itemsHeld.length; i++) { + if (this.itemsHeld[i] == target) { + slot = i; + } + } + return slot; + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + if (orderId == OrderIds.dropitem) { + receiver.orderIdNotAccepted(); + } + } + + @Override + public void checkCanTargetNoTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + protected void innerCheckCanUse(final CSimulation game, final CUnit unit, final int orderId, + final AbilityActivationReceiver receiver) { + receiver.useOk(); + } + + /** + * Attempts to give the hero the specified item, returning the item slot to + * which the item is added or -1 if no available slot is found + * + * @param item + * @return + */ + public int giveItem(final CSimulation simulation, final CUnit hero, final CItem item, + final boolean playUserUISounds) { + if ((item != null) && !item.isDead() && !item.isHidden()) { + final CItemType itemType = item.getItemType(); + if (itemType.isUseAutomaticallyWhenAcquired()) { + if (itemType.isActivelyUsed()) { + item.setLife(simulation, 0); + // TODO when we give unit ability here, then use ability + } + } + else { + for (int i = 0; i < this.itemsHeld.length; i++) { + if (this.itemsHeld[i] == null) { + this.itemsHeld[i] = item; + item.setHidden(true); + hero.onPickUpItem(simulation, item, true); + return i; + } + } + if (playUserUISounds) { + simulation.getCommandErrorListener(hero.getPlayerIndex()).showInventoryFullError(); + } + } + } + return -1; + } + + public void dropItem(final CSimulation simulation, final CUnit hero, final int slotIndex, final float x, + final float y, final boolean playUserUISounds) { + final CItem droppedItem = this.itemsHeld[slotIndex]; + hero.onDropItem(simulation, droppedItem, true); + this.itemsHeld[slotIndex] = null; + droppedItem.setHidden(false); + droppedItem.setPointAndCheckUnstuck(x, y, simulation); + } + + public void dropItem(final CSimulation simulation, final CUnit hero, final CItem itemToDrop, final float x, + final float y, final boolean playUserUISounds) { + boolean foundItem = false; + for (int i = 0; i < this.itemsHeld.length; i++) { + if (this.itemsHeld[i] == itemToDrop) { + this.itemsHeld[i] = null; + foundItem = true; + } + } + if (foundItem) { + hero.onDropItem(simulation, itemToDrop, true); + itemToDrop.setHidden(false); + itemToDrop.setPointAndCheckUnstuck(x, y, simulation); + } + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/menu/CAbilityMenu.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/menu/CAbilityMenu.java new file mode 100644 index 0000000..7344084 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/menu/CAbilityMenu.java @@ -0,0 +1,8 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.menu; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; + +public interface CAbilityMenu extends CAbility { + // the base order ID of the menu + int getBaseOrderId(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/mine/CAbilityGoldMine.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/mine/CAbilityGoldMine.java new file mode 100644 index 0000000..3a28cc6 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/mine/CAbilityGoldMine.java @@ -0,0 +1,140 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.mine; + +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.SecondaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic.AbstractGenericNoIconAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.harvest.CBehaviorHarvest; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; + +public class CAbilityGoldMine extends AbstractGenericNoIconAbility { + private int gold; + private final float miningDuration; + private final int miningCapacity; + private final List activeMiners; + private boolean wasEmpty; + + public CAbilityGoldMine(final int handleId, final War3ID alias, final int maxGold, final float miningDuration, + final int miningCapacity) { + super(handleId, alias); + this.gold = maxGold; + this.miningDuration = miningDuration; + this.miningCapacity = miningCapacity; + this.activeMiners = new ArrayList<>(); + this.wasEmpty = this.activeMiners.isEmpty(); + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + + } + + @Override + public void onTick(final CSimulation game, final CUnit unit) { + final boolean empty = this.activeMiners.isEmpty(); + if (empty != this.wasEmpty) { + if (empty) { + unit.getUnitAnimationListener().removeSecondaryTag(SecondaryTag.WORK); + } + else { + unit.getUnitAnimationListener().addSecondaryTag(SecondaryTag.WORK); + } + this.wasEmpty = empty; + } + for (int i = this.activeMiners.size() - 1; i >= 0; i--) { + final CBehaviorHarvest activeMiner = this.activeMiners.get(i); + if (game.getGameTurnTick() >= activeMiner.getPopoutFromMineTurnTick()) { + + final int goldMined = Math.min(this.gold, activeMiner.getGoldCapacity()); + this.gold -= goldMined; + if (this.gold <= 0) { + unit.setLife(game, 0); + } + activeMiner.popoutFromMine(goldMined); + this.activeMiners.remove(i); + } + } + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + return null; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + return null; + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + return null; + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, final CWidget target, + final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public void checkCanTargetNoTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + protected void innerCheckCanUse(final CSimulation game, final CUnit unit, final int orderId, + final AbilityActivationReceiver receiver) { + receiver.notAnActiveAbility(); + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + } + + public int getGold() { + return this.gold; + } + + public void setGold(final int gold) { + this.gold = gold; + } + + public int getActiveMinerCount() { + return this.activeMiners.size(); + } + + public void addMiner(final CBehaviorHarvest miner) { + this.activeMiners.add(miner); + } + + public int getMiningCapacity() { + return this.miningCapacity; + } + + public float getMiningDuration() { + return this.miningDuration; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/queue/CAbilityQueue.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/queue/CAbilityQueue.java new file mode 100644 index 0000000..7250841 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/queue/CAbilityQueue.java @@ -0,0 +1,173 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.queue; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitTypeRequirement; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.AbstractCAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.ResourceType; + +public final class CAbilityQueue extends AbstractCAbility { + private final Set unitsTrained; + private final Set researchesAvailable; + + public CAbilityQueue(final int handleId, final List unitsTrained, final List researchesAvailable) { + super(handleId); + this.unitsTrained = new LinkedHashSet<>(unitsTrained); + this.researchesAvailable = new LinkedHashSet<>(researchesAvailable); + } + + public Set getUnitsTrained() { + return this.unitsTrained; + } + + public Set getResearchesAvailable() { + return this.researchesAvailable; + } + + @Override + protected void innerCheckCanUse(final CSimulation game, final CUnit unit, final int orderId, + final AbilityActivationReceiver receiver) { + final War3ID orderIdAsRawtype = new War3ID(orderId); + if (this.unitsTrained.contains(orderIdAsRawtype) || this.researchesAvailable.contains(orderIdAsRawtype)) { + final CUnitType unitType = game.getUnitData().getUnitType(orderIdAsRawtype); + if (unitType != null) { + final CPlayer player = game.getPlayer(unit.getPlayerIndex()); + final List requirements = unitType.getRequirements(); + boolean requirementsMet = true; + for (final CUnitTypeRequirement requirement : requirements) { + if (player.getTechtreeUnlocked(requirement.getRequirement()) < requirement.getRequiredLevel()) { + requirementsMet = false; + } + } + if (requirementsMet) { + if (player.getGold() >= unitType.getGoldCost()) { + if (player.getLumber() >= unitType.getLumberCost()) { + if ((unitType.getFoodUsed() == 0) + || ((player.getFoodUsed() + unitType.getFoodUsed()) <= player.getFoodCap())) { + receiver.useOk(); + } + else { + receiver.notEnoughResources(ResourceType.FOOD); + } + } + else { + receiver.notEnoughResources(ResourceType.LUMBER); + } + } + else { + receiver.notEnoughResources(ResourceType.GOLD); + } + } + else { + for (final CUnitTypeRequirement requirement : requirements) { + receiver.missingRequirement(requirement.getRequirement(), requirement.getRequiredLevel()); + } + } + } + else { + receiver.useOk(); + } + } + else { + /// ??? + receiver.useOk(); + } + } + + @Override + public final void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, final CWidget target, + final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public final void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public final void checkCanTargetNoTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityTargetCheckReceiver receiver) { + if (this.unitsTrained.contains(new War3ID(orderId)) || this.researchesAvailable.contains(new War3ID(orderId))) { + receiver.targetOk(null); + } + else if (orderId == OrderIds.cancel) { + receiver.targetOk(null); + } + else { + receiver.orderIdNotAccepted(); + } + } + + @Override + public boolean checkBeforeQueue(final CSimulation game, final CUnit caster, final int orderId, AbilityTarget target) { + return true; + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + + } + + @Override + public void onTick(final CSimulation game, final CUnit unit) { + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + return null; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + return null; + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + if (orderId == OrderIds.cancel) { + caster.cancelBuildQueueItem(game, 0); + } + else { + final War3ID rawcode = new War3ID(orderId); + if (this.unitsTrained.contains(rawcode)) { + caster.queueTrainingUnit(game, rawcode); + } + else if (this.researchesAvailable.contains(rawcode)) { + caster.queueResearch(game, rawcode); + } + } + return null; + } + + @Override + public T visit(final CAbilityVisitor visitor) { + return visitor.accept(this); + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/queue/CAbilityRally.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/queue/CAbilityRally.java new file mode 100644 index 0000000..4f2fecc --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/queue/CAbilityRally.java @@ -0,0 +1,111 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.queue; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.AbstractCAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; + +public class CAbilityRally extends AbstractCAbility { + + public CAbilityRally(final int handleId) { + super(handleId); + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + unit.setRallyPoint(unit); + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + + } + + @Override + public void onTick(final CSimulation game, final CUnit unit) { + } + + @Override + protected void innerCheckCanUse(final CSimulation game, final CUnit unit, final int orderId, + final AbilityActivationReceiver receiver) { + receiver.useOk(); + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, final CWidget target, + final AbilityTargetCheckReceiver receiver) { + switch (orderId) { + case OrderIds.smart: + case OrderIds.setrally: + receiver.targetOk(target); + break; + default: + receiver.orderIdNotAccepted(); + break; + } + } + + @Override + public void checkCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + switch (orderId) { + case OrderIds.smart: + case OrderIds.setrally: + receiver.targetOk(target); + break; + default: + receiver.orderIdNotAccepted(); + break; + } + } + + @Override + public void checkCanTargetNoTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + public boolean checkBeforeQueue(final CSimulation game, final CUnit caster, final int orderId, AbilityTarget target) { + return true; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + caster.setRallyPoint(target); + return caster.pollNextOrderBehavior(game); + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + caster.setRallyPoint(point); + return caster.pollNextOrderBehavior(game); + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + return null; + } + + @Override + public T visit(final CAbilityVisitor visitor) { + return visitor.accept(this); + } + + public int getBaseOrderId() { + return OrderIds.setrally; + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityPointTarget.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityPointTarget.java new file mode 100644 index 0000000..bc443a6 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityPointTarget.java @@ -0,0 +1,34 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting; + +import com.badlogic.gdx.math.Vector2; + +public class AbilityPointTarget extends Vector2 implements AbilityTarget { + + public AbilityPointTarget() { + super(); + } + + public AbilityPointTarget(final float x, final float y) { + super(x, y); + } + + public AbilityPointTarget(final Vector2 v) { + super(v); + } + + @Override + public float getX() { + return this.x; + } + + @Override + public float getY() { + return this.y; + } + + @Override + public T visit(final AbilityTargetVisitor visitor) { + return visitor.accept(this); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTarget.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTarget.java new file mode 100644 index 0000000..c172d19 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTarget.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting; + +public interface AbilityTarget { + float getX(); + + float getY(); + + T visit(AbilityTargetVisitor visitor); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTargetItemVisitor.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTargetItemVisitor.java new file mode 100644 index 0000000..b1276ab --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTargetItemVisitor.java @@ -0,0 +1,30 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CDestructable; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; + +public class AbilityTargetItemVisitor implements AbilityTargetVisitor { + public static final AbilityTargetItemVisitor INSTANCE = new AbilityTargetItemVisitor(); + + @Override + public CItem accept(final AbilityPointTarget target) { + return null; + } + + @Override + public CItem accept(final CUnit target) { + return null; + } + + @Override + public CItem accept(final CDestructable target) { + return null; + } + + @Override + public CItem accept(final CItem target) { + return target; + } + +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTargetStillAliveAndTargetableVisitor.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTargetStillAliveAndTargetableVisitor.java new file mode 100644 index 0000000..571a3c1 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTargetStillAliveAndTargetableVisitor.java @@ -0,0 +1,45 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting; + +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CDestructable; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public final class AbilityTargetStillAliveAndTargetableVisitor implements AbilityTargetVisitor { + private CSimulation simulation; + private CUnit unit; + private EnumSet targetsAllowed; + + public AbilityTargetStillAliveAndTargetableVisitor reset(final CSimulation simulation, final CUnit unit, + final EnumSet targetsAllowed) { + this.simulation = simulation; + this.unit = unit; + this.targetsAllowed = targetsAllowed; + return this; + } + + @Override + public Boolean accept(final AbilityPointTarget target) { + return Boolean.TRUE; + } + + @Override + public Boolean accept(final CUnit target) { + return !target.isDead() && !target.isHidden() + && target.canBeTargetedBy(this.simulation, this.unit, this.targetsAllowed); + } + + @Override + public Boolean accept(final CDestructable target) { + return !target.isDead() && target.canBeTargetedBy(this.simulation, this.unit, this.targetsAllowed); + } + + @Override + public Boolean accept(final CItem target) { + return !target.isDead() && target.canBeTargetedBy(this.simulation, this.unit, this.targetsAllowed); + } + +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTargetStillAliveVisitor.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTargetStillAliveVisitor.java new file mode 100644 index 0000000..d6a7917 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTargetStillAliveVisitor.java @@ -0,0 +1,30 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CDestructable; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; + +public class AbilityTargetStillAliveVisitor implements AbilityTargetVisitor { + public static final AbilityTargetStillAliveVisitor INSTANCE = new AbilityTargetStillAliveVisitor(); + + @Override + public Boolean accept(final AbilityPointTarget target) { + return Boolean.TRUE; + } + + @Override + public Boolean accept(final CUnit target) { + return !target.isDead() && !target.isHidden(); + } + + @Override + public Boolean accept(final CDestructable target) { + return !target.isDead(); + } + + @Override + public Boolean accept(final CItem target) { + return !target.isDead() && !target.isHidden(); + } + +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTargetVisitor.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTargetVisitor.java new file mode 100644 index 0000000..4768f67 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTargetVisitor.java @@ -0,0 +1,15 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CDestructable; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; + +public interface AbilityTargetVisitor { + T accept(AbilityPointTarget target); + + T accept(CUnit target); + + T accept(CDestructable target); + + T accept(CItem target); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTargetWidgetVisitor.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTargetWidgetVisitor.java new file mode 100644 index 0000000..7a3deb5 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/targeting/AbilityTargetWidgetVisitor.java @@ -0,0 +1,31 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CDestructable; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; + +public class AbilityTargetWidgetVisitor implements AbilityTargetVisitor { + public static final AbilityTargetWidgetVisitor INSTANCE = new AbilityTargetWidgetVisitor(); + + @Override + public CWidget accept(final AbilityPointTarget target) { + return null; + } + + @Override + public CWidget accept(final CUnit target) { + return target; + } + + @Override + public CWidget accept(final CDestructable target) { + return target; + } + + @Override + public CWidget accept(final CItem target) { + return target; + } + +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/test/CAbilityChannelTest.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/test/CAbilityChannelTest.java new file mode 100644 index 0000000..db50488 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/test/CAbilityChannelTest.java @@ -0,0 +1,91 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.test; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic.AbstractGenericSingleIconNoSmartActiveAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.test.CBehaviorChannelTest; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityTargetCheckReceiver; + +public class CAbilityChannelTest extends AbstractGenericSingleIconNoSmartActiveAbility { + private CBehaviorChannelTest behaviorChannelTest; + private final float artDuration; + + public CAbilityChannelTest(final int handleId, final War3ID alias, final float artDuration) { + super(handleId, alias); + this.artDuration = artDuration; + } + + @Override + public int getBaseOrderId() { + return OrderIds.channel; + } + + @Override + public boolean isToggleOn() { + return false; + } + + @Override + public void onAdd(final CSimulation game, final CUnit unit) { + this.behaviorChannelTest = new CBehaviorChannelTest(unit, this.artDuration); + } + + @Override + public void onRemove(final CSimulation game, final CUnit unit) { + } + + @Override + public void onTick(final CSimulation game, final CUnit unit) { + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, final CWidget target) { + return this.behaviorChannelTest; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster, final int orderId, + final AbilityPointTarget point) { + return this.behaviorChannelTest; + } + + @Override + public CBehavior beginNoTarget(final CSimulation game, final CUnit caster, final int orderId) { + return this.behaviorChannelTest; + } + + @Override + protected void innerCheckCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final CWidget target, final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + protected void innerCheckCanTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityPointTarget target, final AbilityTargetCheckReceiver receiver) { + receiver.orderIdNotAccepted(); + } + + @Override + protected void innerCheckCanTargetNoTarget(final CSimulation game, final CUnit unit, final int orderId, + final AbilityTargetCheckReceiver receiver) { + receiver.targetOk(null); + } + + @Override + protected void innerCheckCanUse(final CSimulation game, final CUnit unit, final int orderId, + final AbilityActivationReceiver receiver) { + receiver.useOk(); + } + + @Override + public void onCancelFromQueue(final CSimulation game, final CUnit unit, final int orderId) { + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/CAbilityType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/CAbilityType.java new file mode 100644 index 0000000..5c8725b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/CAbilityType.java @@ -0,0 +1,42 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types; + +import java.util.EnumSet; +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public abstract class CAbilityType { + /* alias: defines which ability editor ability to use */ + private final War3ID alias; + /* code: defines which CAbility class to use */ + private final War3ID code; + + private final List levelData; + + public CAbilityType(final War3ID alias, final War3ID code, final List levelData) { + this.alias = alias; + this.code = code; + this.levelData = levelData; + } + + public War3ID getAlias() { + return this.alias; + } + + public War3ID getCode() { + return this.code; + } + + public EnumSet getTargetsAllowed(final int level) { + return getLevelData(level).getTargetsAllowed(); + } + + protected final TYPE_LEVEL_DATA getLevelData(final int level) { + return this.levelData.get(level); + } + + public abstract CAbility createAbility(int handleId); + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/CAbilityTypeLevelData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/CAbilityTypeLevelData.java new file mode 100644 index 0000000..d023452 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/CAbilityTypeLevelData.java @@ -0,0 +1,17 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types; + +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public class CAbilityTypeLevelData { + private final EnumSet targetsAllowed; + + public CAbilityTypeLevelData(final EnumSet targetsAllowed) { + this.targetsAllowed = targetsAllowed; + } + + public EnumSet getTargetsAllowed() { + return this.targetsAllowed; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/CAbilityTypeDefinition.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/CAbilityTypeDefinition.java new file mode 100644 index 0000000..361e244 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/CAbilityTypeDefinition.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions; + +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityType; + +public interface CAbilityTypeDefinition { + CAbilityType createAbilityType(War3ID rawcode, MutableGameObject abilityEditorData); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/AbstractCAbilityTypeDefinition.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/AbstractCAbilityTypeDefinition.java new file mode 100644 index 0000000..52386a4 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/AbstractCAbilityTypeDefinition.java @@ -0,0 +1,33 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl; + +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityTypeLevelData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.CAbilityTypeDefinition; + +public abstract class AbstractCAbilityTypeDefinition + implements CAbilityTypeDefinition { + protected static final War3ID TARGETS_ALLOWED = War3ID.fromString("atar"); + private static final War3ID LEVELS = War3ID.fromString("alev"); + protected static final War3ID CAST_RANGE = War3ID.fromString("aran"); + protected static final War3ID DURATION = War3ID.fromString("adur"); + + @Override + public CAbilityType createAbilityType(final War3ID alias, final MutableGameObject abilityEditorData) { + final int levels = abilityEditorData.getFieldAsInteger(LEVELS, 0); + final List levelData = new ArrayList<>(); + for (int level = 1; level <= levels; level++) { + levelData.add(createLevelData(abilityEditorData, level)); + } + return innerCreateAbilityType(alias, abilityEditorData, levelData); + } + + protected abstract TYPE_LEVEL_DATA createLevelData(MutableGameObject abilityEditorData, int level); + + protected abstract CAbilityType innerCreateAbilityType(War3ID alias, MutableGameObject abilityEditorData, + List levelData); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionChannelTest.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionChannelTest.java new file mode 100644 index 0000000..6d4808a --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionChannelTest.java @@ -0,0 +1,33 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl; + +import java.util.EnumSet; +import java.util.List; + +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.CAbilityTypeDefinition; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl.CAbilityTypeChannelTest; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl.CAbilityTypeChannelTestLevelData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public class CAbilityTypeDefinitionChannelTest extends AbstractCAbilityTypeDefinition + implements CAbilityTypeDefinition { + protected static final War3ID ART_DURATION = War3ID.fromString("Ncl4"); + + @Override + protected CAbilityTypeChannelTestLevelData createLevelData(final MutableGameObject abilityEditorData, + final int level) { + final String targetsAllowedAtLevelString = abilityEditorData.getFieldAsString(TARGETS_ALLOWED, level); + final float artDuration = abilityEditorData.getFieldAsFloat(ART_DURATION, level); + final EnumSet targetsAllowedAtLevel = CTargetType.parseTargetTypeSet(targetsAllowedAtLevelString); + return new CAbilityTypeChannelTestLevelData(targetsAllowedAtLevel, artDuration); + } + + @Override + protected CAbilityType innerCreateAbilityType(final War3ID alias, final MutableGameObject abilityEditorData, + final List levelData) { + return new CAbilityTypeChannelTest(alias, abilityEditorData.getCode(), levelData); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionColdArrows.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionColdArrows.java new file mode 100644 index 0000000..55a6557 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionColdArrows.java @@ -0,0 +1,27 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl; + +import java.util.List; + +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl.CAbilityTypeColdArrows; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl.CAbilityTypeColdArrowsLevelData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public class CAbilityTypeDefinitionColdArrows extends AbstractCAbilityTypeDefinition { + + @Override + protected CAbilityTypeColdArrowsLevelData createLevelData(final MutableGameObject abilityEditorData, + final int level) { + return new CAbilityTypeColdArrowsLevelData( + CTargetType.parseTargetTypeSet(abilityEditorData.getFieldAsString(TARGETS_ALLOWED, level))); + } + + @Override + protected CAbilityType innerCreateAbilityType(final War3ID alias, final MutableGameObject abilityEditorData, + final List levelData) { + return new CAbilityTypeColdArrows(alias, abilityEditorData.getCode(), levelData); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionGoldMine.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionGoldMine.java new file mode 100644 index 0000000..0b86b09 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionGoldMine.java @@ -0,0 +1,37 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl; + +import java.util.EnumSet; +import java.util.List; + +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.CAbilityTypeDefinition; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl.CAbilityTypeGoldMine; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl.CAbilityTypeGoldMineLevelData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public class CAbilityTypeDefinitionGoldMine extends AbstractCAbilityTypeDefinition + implements CAbilityTypeDefinition { + protected static final War3ID MAX_GOLD = War3ID.fromString("Gld1"); + protected static final War3ID MINING_DURATION = War3ID.fromString("Gld2"); + protected static final War3ID MINING_CAPACITY = War3ID.fromString("Gld3"); + + @Override + protected CAbilityTypeGoldMineLevelData createLevelData(final MutableGameObject abilityEditorData, + final int level) { + final String targetsAllowedAtLevelString = abilityEditorData.getFieldAsString(TARGETS_ALLOWED, level); + final EnumSet targetsAllowedAtLevel = CTargetType.parseTargetTypeSet(targetsAllowedAtLevelString); + final int maxGold = abilityEditorData.getFieldAsInteger(MAX_GOLD, level); + final float miningDuration = abilityEditorData.getFieldAsFloat(MINING_DURATION, level); + final int miningCapacity = abilityEditorData.getFieldAsInteger(MINING_CAPACITY, level); + return new CAbilityTypeGoldMineLevelData(targetsAllowedAtLevel, maxGold, miningDuration, miningCapacity); + } + + @Override + protected CAbilityType innerCreateAbilityType(final War3ID alias, final MutableGameObject abilityEditorData, + final List levelData) { + return new CAbilityTypeGoldMine(alias, abilityEditorData.getCode(), levelData); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionHarvest.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionHarvest.java new file mode 100644 index 0000000..4998ab9 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionHarvest.java @@ -0,0 +1,39 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl; + +import java.util.EnumSet; +import java.util.List; + +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.CAbilityTypeDefinition; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl.CAbilityTypeHarvest; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl.CAbilityTypeHarvestLevelData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public class CAbilityTypeDefinitionHarvest extends AbstractCAbilityTypeDefinition + implements CAbilityTypeDefinition { + protected static final War3ID DAMAGE_TO_TREE = War3ID.fromString("Har1"); + protected static final War3ID GOLD_CAPACITY = War3ID.fromString("Har2"); + protected static final War3ID LUMBER_CAPACITY = War3ID.fromString("Har3"); + + @Override + protected CAbilityTypeHarvestLevelData createLevelData(final MutableGameObject abilityEditorData, final int level) { + final String targetsAllowedAtLevelString = abilityEditorData.getFieldAsString(TARGETS_ALLOWED, level); + final EnumSet targetsAllowedAtLevel = CTargetType.parseTargetTypeSet(targetsAllowedAtLevelString); + final int damageToTree = abilityEditorData.getFieldAsInteger(DAMAGE_TO_TREE, level); + final int goldCapacity = abilityEditorData.getFieldAsInteger(GOLD_CAPACITY, level); + final int lumberCapacity = abilityEditorData.getFieldAsInteger(LUMBER_CAPACITY, level); + final float castRange = abilityEditorData.getFieldAsFloat(CAST_RANGE, level); + final float duration = abilityEditorData.getFieldAsFloat(DURATION, level); + return new CAbilityTypeHarvestLevelData(targetsAllowedAtLevel, damageToTree, goldCapacity, lumberCapacity, + castRange, duration); + } + + @Override + protected CAbilityType innerCreateAbilityType(final War3ID alias, final MutableGameObject abilityEditorData, + final List levelData) { + return new CAbilityTypeHarvest(alias, abilityEditorData.getCode(), levelData); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionInventory.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionInventory.java new file mode 100644 index 0000000..89d387b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionInventory.java @@ -0,0 +1,42 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl; + +import java.util.EnumSet; +import java.util.List; + +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.CAbilityTypeDefinition; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl.CAbilityTypeInventory; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl.CAbilityTypeInventoryLevelData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public class CAbilityTypeDefinitionInventory extends AbstractCAbilityTypeDefinition + implements CAbilityTypeDefinition { + protected static final War3ID ITEM_CAPACITY = War3ID.fromString("inv1"); + protected static final War3ID DROP_ITEMS_ON_DEATH = War3ID.fromString("inv2"); + protected static final War3ID CAN_USE_ITEMS = War3ID.fromString("inv3"); + protected static final War3ID CAN_GET_ITEMS = War3ID.fromString("inv4"); + protected static final War3ID CAN_DROP_ITEMS = War3ID.fromString("inv5"); + + @Override + protected CAbilityTypeInventoryLevelData createLevelData(final MutableGameObject abilityEditorData, + final int level) { + final String targetsAllowedAtLevelString = abilityEditorData.getFieldAsString(TARGETS_ALLOWED, level); + final EnumSet targetsAllowedAtLevel = CTargetType.parseTargetTypeSet(targetsAllowedAtLevelString); + final int itemCapacity = abilityEditorData.getFieldAsInteger(ITEM_CAPACITY, level); + final boolean dropItemsOnDeath = abilityEditorData.getFieldAsBoolean(DROP_ITEMS_ON_DEATH, level); + final boolean canUseItems = abilityEditorData.getFieldAsBoolean(CAN_USE_ITEMS, level); + final boolean canGetItems = abilityEditorData.getFieldAsBoolean(CAN_GET_ITEMS, level); + final boolean canDropItems = abilityEditorData.getFieldAsBoolean(CAN_DROP_ITEMS, level); + return new CAbilityTypeInventoryLevelData(targetsAllowedAtLevel, canDropItems, canGetItems, canUseItems, + dropItemsOnDeath, itemCapacity); + } + + @Override + protected CAbilityType innerCreateAbilityType(final War3ID alias, final MutableGameObject abilityEditorData, + final List levelData) { + return new CAbilityTypeInventory(alias, abilityEditorData.getCode(), levelData); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionReturnResources.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionReturnResources.java new file mode 100644 index 0000000..07c1178 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/definitions/impl/CAbilityTypeDefinitionReturnResources.java @@ -0,0 +1,35 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl; + +import java.util.EnumSet; +import java.util.List; + +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.CAbilityTypeDefinition; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl.CAbilityTypeReturnResources; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl.CAbilityTypeReturnResourcesLevelData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public class CAbilityTypeDefinitionReturnResources + extends AbstractCAbilityTypeDefinition implements CAbilityTypeDefinition { + protected static final War3ID ACCEPTS_GOLD = War3ID.fromString("Rtn1"); + protected static final War3ID ACCEPTS_LUMBER = War3ID.fromString("Rtn2"); + + @Override + protected CAbilityTypeReturnResourcesLevelData createLevelData(final MutableGameObject abilityEditorData, + final int level) { + final String targetsAllowedAtLevelString = abilityEditorData.getFieldAsString(TARGETS_ALLOWED, level); + final EnumSet targetsAllowedAtLevel = CTargetType.parseTargetTypeSet(targetsAllowedAtLevelString); + final boolean acceptsGold = abilityEditorData.getFieldAsBoolean(ACCEPTS_GOLD, level); + final boolean acceptsLumber = abilityEditorData.getFieldAsBoolean(ACCEPTS_LUMBER, level); + return new CAbilityTypeReturnResourcesLevelData(targetsAllowedAtLevel, acceptsGold, acceptsLumber); + } + + @Override + protected CAbilityType innerCreateAbilityType(final War3ID alias, final MutableGameObject abilityEditorData, + final List levelData) { + return new CAbilityTypeReturnResources(alias, abilityEditorData.getCode(), levelData); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeChannelTest.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeChannelTest.java new file mode 100644 index 0000000..3359527 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeChannelTest.java @@ -0,0 +1,23 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl; + +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.test.CAbilityChannelTest; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityType; + +public class CAbilityTypeChannelTest extends CAbilityType { + + public CAbilityTypeChannelTest(final War3ID alias, final War3ID code, + final List levelData) { + super(alias, code, levelData); + } + + @Override + public CAbility createAbility(final int handleId) { + final CAbilityTypeChannelTestLevelData levelData = getLevelData(0); + return new CAbilityChannelTest(handleId, getAlias(), levelData.getArtDuration()); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeChannelTestLevelData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeChannelTestLevelData.java new file mode 100644 index 0000000..0c30097 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeChannelTestLevelData.java @@ -0,0 +1,20 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl; + +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityTypeLevelData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public class CAbilityTypeChannelTestLevelData extends CAbilityTypeLevelData { + private final float artDuration; + + public CAbilityTypeChannelTestLevelData(final EnumSet targetsAllowed, final float artDuration) { + super(targetsAllowed); + this.artDuration = artDuration; + } + + public float getArtDuration() { + return this.artDuration; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeColdArrows.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeColdArrows.java new file mode 100644 index 0000000..5e86f3b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeColdArrows.java @@ -0,0 +1,22 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl; + +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.combat.CAbilityColdArrows; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityType; + +public class CAbilityTypeColdArrows extends CAbilityType { + + public CAbilityTypeColdArrows(final War3ID alias, final War3ID code, + final List levelData) { + super(alias, code, levelData); + } + + @Override + public CAbility createAbility(final int handleId) { + return new CAbilityColdArrows(getAlias(), handleId); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeColdArrowsLevelData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeColdArrowsLevelData.java new file mode 100644 index 0000000..7b16958 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeColdArrowsLevelData.java @@ -0,0 +1,14 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl; + +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityTypeLevelData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public class CAbilityTypeColdArrowsLevelData extends CAbilityTypeLevelData { + + public CAbilityTypeColdArrowsLevelData(final EnumSet targetsAllowed) { + super(targetsAllowed); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeGoldMine.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeGoldMine.java new file mode 100644 index 0000000..bd0f428 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeGoldMine.java @@ -0,0 +1,24 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl; + +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.mine.CAbilityGoldMine; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityType; + +public class CAbilityTypeGoldMine extends CAbilityType { + + public CAbilityTypeGoldMine(final War3ID alias, final War3ID code, + final List levelData) { + super(alias, code, levelData); + } + + @Override + public CAbility createAbility(final int handleId) { + final CAbilityTypeGoldMineLevelData levelData = getLevelData(0); + return new CAbilityGoldMine(handleId, getAlias(), levelData.getMaxGold(), levelData.getMiningDuration(), + levelData.getMiningCapacity()); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeGoldMineLevelData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeGoldMineLevelData.java new file mode 100644 index 0000000..522fd62 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeGoldMineLevelData.java @@ -0,0 +1,32 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl; + +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityTypeLevelData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public class CAbilityTypeGoldMineLevelData extends CAbilityTypeLevelData { + private final int maxGold; + private final float miningDuration; + private final int miningCapacity; + + public CAbilityTypeGoldMineLevelData(final EnumSet targetsAllowed, final int maxGold, + final float miningDuration, final int miningCapacity) { + super(targetsAllowed); + this.maxGold = maxGold; + this.miningDuration = miningDuration; + this.miningCapacity = miningCapacity; + } + + public int getMaxGold() { + return this.maxGold; + } + + public float getMiningDuration() { + return this.miningDuration; + } + + public int getMiningCapacity() { + return this.miningCapacity; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeHarvest.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeHarvest.java new file mode 100644 index 0000000..1e8ec15 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeHarvest.java @@ -0,0 +1,24 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl; + +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.harvest.CAbilityHarvest; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityType; + +public class CAbilityTypeHarvest extends CAbilityType { + + public CAbilityTypeHarvest(final War3ID alias, final War3ID code, + final List levelData) { + super(alias, code, levelData); + } + + @Override + public CAbility createAbility(final int handleId) { + final CAbilityTypeHarvestLevelData levelData = getLevelData(0); + return new CAbilityHarvest(handleId, getAlias(), levelData.getDamageToTree(), levelData.getGoldCapacity(), + levelData.getLumberCapacity(), levelData.getCastRange(), levelData.getDuration()); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeHarvestLevelData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeHarvestLevelData.java new file mode 100644 index 0000000..46854de --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeHarvestLevelData.java @@ -0,0 +1,45 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl; + +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityTypeLevelData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public class CAbilityTypeHarvestLevelData extends CAbilityTypeLevelData { + private final int damageToTree; + private final int goldCapacity; + private final int lumberCapacity; + private final float castRange; + private final float duration; + + public CAbilityTypeHarvestLevelData(final EnumSet targetsAllowed, final int damageToTree, + final int goldCapacity, final int lumberCapacity, final float castRange, final float duration) { + super(targetsAllowed); + this.damageToTree = damageToTree; + this.goldCapacity = goldCapacity; + this.lumberCapacity = lumberCapacity; + this.castRange = castRange; + this.duration = duration; + } + + public int getDamageToTree() { + return this.damageToTree; + } + + public int getGoldCapacity() { + return this.goldCapacity; + } + + public int getLumberCapacity() { + return this.lumberCapacity; + } + + public float getCastRange() { + return this.castRange; + } + + public float getDuration() { + return this.duration; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeInventory.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeInventory.java new file mode 100644 index 0000000..66b06f2 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeInventory.java @@ -0,0 +1,24 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl; + +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.inventory.CAbilityInventory; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityType; + +public class CAbilityTypeInventory extends CAbilityType { + + public CAbilityTypeInventory(final War3ID alias, final War3ID code, + final List levelData) { + super(alias, code, levelData); + } + + @Override + public CAbility createAbility(final int handleId) { + final CAbilityTypeInventoryLevelData levelData = getLevelData(0); + return new CAbilityInventory(handleId, getAlias(), levelData.isCanDropItems(), levelData.isCanGetItems(), + levelData.isCanUseItems(), levelData.isDropItemsOnDeath(), levelData.getItemCapacity()); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeInventoryLevelData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeInventoryLevelData.java new file mode 100644 index 0000000..518b95f --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeInventoryLevelData.java @@ -0,0 +1,47 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl; + +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityTypeLevelData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public class CAbilityTypeInventoryLevelData extends CAbilityTypeLevelData { + + private final boolean canDropItems; + private final boolean canGetItems; + private final boolean canUseItems; + private final boolean dropItemsOnDeath; + private final int itemCapacity; + + public CAbilityTypeInventoryLevelData(final EnumSet targetsAllowed, final boolean canDropItems, + final boolean canGetItems, final boolean canUseItems, final boolean dropItemsOnDeath, + final int itemCapacity) { + super(targetsAllowed); + this.canDropItems = canDropItems; + this.canGetItems = canGetItems; + this.canUseItems = canUseItems; + this.dropItemsOnDeath = dropItemsOnDeath; + this.itemCapacity = itemCapacity; + } + + public boolean isCanDropItems() { + return this.canDropItems; + } + + public boolean isCanGetItems() { + return this.canGetItems; + } + + public boolean isCanUseItems() { + return this.canUseItems; + } + + public boolean isDropItemsOnDeath() { + return this.dropItemsOnDeath; + } + + public int getItemCapacity() { + return this.itemCapacity; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeReturnResources.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeReturnResources.java new file mode 100644 index 0000000..a10ed6d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeReturnResources.java @@ -0,0 +1,32 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl; + +import java.util.EnumSet; +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.harvest.CAbilityReturnResources; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.ResourceType; + +public class CAbilityTypeReturnResources extends CAbilityType { + + public CAbilityTypeReturnResources(final War3ID alias, final War3ID code, + final List levelData) { + super(alias, code, levelData); + } + + @Override + public CAbility createAbility(final int handleId) { + final CAbilityTypeReturnResourcesLevelData levelData = getLevelData(0); + final EnumSet acceptedResourceTypes = EnumSet.noneOf(ResourceType.class); + if (levelData.isAcceptsGold()) { + acceptedResourceTypes.add(ResourceType.GOLD); + } + if (levelData.isAcceptsLumber()) { + acceptedResourceTypes.add(ResourceType.LUMBER); + } + return new CAbilityReturnResources(handleId, getAlias(), acceptedResourceTypes); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeReturnResourcesLevelData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeReturnResourcesLevelData.java new file mode 100644 index 0000000..2366f35 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/types/impl/CAbilityTypeReturnResourcesLevelData.java @@ -0,0 +1,28 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.impl; + +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityTypeLevelData; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; + +public class CAbilityTypeReturnResourcesLevelData extends CAbilityTypeLevelData { + + private final boolean acceptsGold; + private final boolean acceptsLumber; + + public CAbilityTypeReturnResourcesLevelData(final EnumSet targetsAllowed, final boolean acceptsGold, + final boolean acceptsLumber) { + super(targetsAllowed); + this.acceptsGold = acceptsGold; + this.acceptsLumber = acceptsLumber; + } + + public boolean isAcceptsGold() { + return this.acceptsGold; + } + + public boolean isAcceptsLumber() { + return this.acceptsLumber; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/upgrade/CAbilityUpgrade.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/upgrade/CAbilityUpgrade.java new file mode 100644 index 0000000..fb52922 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/abilities/upgrade/CAbilityUpgrade.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.upgrade; + +public class CAbilityUpgrade { + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/ai/AIDifficulty.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/ai/AIDifficulty.java new file mode 100644 index 0000000..681f710 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/ai/AIDifficulty.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.ai; + +public enum AIDifficulty { + NEWBIE, + NORMAL, + INSANE; + + public static AIDifficulty[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CAbstractRangedBehavior.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CAbstractRangedBehavior.java new file mode 100644 index 0000000..651f23c --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CAbstractRangedBehavior.java @@ -0,0 +1,115 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; + +public abstract class CAbstractRangedBehavior implements CRangedBehavior { + protected final CUnit unit; + + public CAbstractRangedBehavior(final CUnit unit) { + this.unit = unit; + } + + protected AbilityTarget target; + private boolean wasWithinPropWindow = false; + private boolean wasInRange = false; + private boolean disableMove = false; + private CBehaviorMove moveBehavior; + + protected final CAbstractRangedBehavior innerReset(final AbilityTarget target) { + return innerReset(target, false); + } + + protected final CAbstractRangedBehavior innerReset(final AbilityTarget target, final boolean disableCollision) { + this.target = target; + this.wasWithinPropWindow = false; + this.wasInRange = false; + CBehaviorMove moveBehavior; + if (!this.unit.isMovementDisabled()) { + moveBehavior = this.unit.getMoveBehavior().reset(this.target, this, disableCollision); + } + else { + moveBehavior = null; + } + this.moveBehavior = moveBehavior; + return this; + } + + protected abstract CBehavior update(CSimulation simulation, boolean withinRange); + + protected abstract CBehavior updateOnInvalidTarget(CSimulation simulation); + + protected abstract boolean checkTargetStillValid(CSimulation simulation); + + protected abstract void resetBeforeMoving(CSimulation simulation); + + @Override + public final CBehavior update(final CSimulation simulation) { + if (!checkTargetStillValid(simulation)) { + return updateOnInvalidTarget(simulation); + } + if (!isWithinRange(simulation)) { + if ((this.moveBehavior == null) || this.disableMove) { + return this.unit.pollNextOrderBehavior(simulation); + } + this.wasInRange = false; + resetBeforeMoving(simulation); + return this.unit.getMoveBehavior(); + } + this.wasInRange = true; + if (!this.unit.isMovementDisabled()) { + final float prevX = this.unit.getX(); + final float prevY = this.unit.getY(); + final float deltaX = this.target.getX() - prevX; + final float deltaY = this.target.getY() - prevY; + final double goalAngleRad = Math.atan2(deltaY, deltaX); + float goalAngle = (float) Math.toDegrees(goalAngleRad); + if (goalAngle < 0) { + goalAngle += 360; + } + float facing = this.unit.getFacing(); + float delta = goalAngle - facing; + final float propulsionWindow = simulation.getGameplayConstants().getAttackHalfAngle(); + final float turnRate = simulation.getUnitData().getTurnRate(this.unit.getTypeId()); + + if (delta < -180) { + delta = 360 + delta; + } + if (delta > 180) { + delta = -360 + delta; + } + final float absDelta = Math.abs(delta); + + if ((absDelta <= 1.0) && (absDelta != 0)) { + this.unit.setFacing(goalAngle); + } + else { + float angleToAdd = Math.signum(delta) * (float) Math.toDegrees(turnRate); + if (absDelta < Math.abs(angleToAdd)) { + angleToAdd = delta; + } + facing += angleToAdd; + this.unit.setFacing(facing); + } + if (absDelta < propulsionWindow) { + this.wasWithinPropWindow = true; + } + else { + // If this happens, the unit is facing the wrong way, and has to turn before + // moving. + this.wasWithinPropWindow = false; + } + } + else { + this.wasWithinPropWindow = true; + } + + return update(simulation, this.wasWithinPropWindow); + } + + public void setDisableMove(final boolean disableMove) { + this.disableMove = disableMove; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehavior.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehavior.java new file mode 100644 index 0000000..beb10a0 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehavior.java @@ -0,0 +1,19 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; + +public interface CBehavior { + /** + * Executes one step of game simulation of the current order, and then returns + * the next behavior for the unit after the result of the update cycle. + * + * @return + */ + CBehavior update(CSimulation game); + + void begin(CSimulation game); + + void end(CSimulation game, boolean interrupted); + + int getHighlightOrderId(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorAttack.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorAttack.java new file mode 100644 index 0000000..1750c82 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorAttack.java @@ -0,0 +1,145 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors; + +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetStillAliveAndTargetableVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttack; + +public class CBehaviorAttack extends CAbstractRangedBehavior { + + private int highlightOrderId; + private final AbilityTargetStillAliveAndTargetableVisitor abilityTargetStillAliveVisitor; + + public CBehaviorAttack(final CUnit unit) { + super(unit); + this.abilityTargetStillAliveVisitor = new AbilityTargetStillAliveAndTargetableVisitor(); + } + + private CUnitAttack unitAttack; + private int damagePointLaunchTime; + private int backSwingTime; + private int thisOrderCooldownEndTime; + private CBehaviorAttackListener attackListener; + + public CBehaviorAttack reset(final int highlightOrderId, final CUnitAttack unitAttack, final AbilityTarget target, + final boolean disableMove, final CBehaviorAttackListener attackListener) { + this.highlightOrderId = highlightOrderId; + this.attackListener = attackListener; + super.innerReset(target); + this.unitAttack = unitAttack; + this.damagePointLaunchTime = 0; + this.backSwingTime = 0; + this.thisOrderCooldownEndTime = 0; + setDisableMove(disableMove); + return this; + } + + @Override + public int getHighlightOrderId() { + return this.highlightOrderId; + } + + @Override + public boolean isWithinRange(final CSimulation simulation) { + float range = this.unitAttack.getRange(); + if (simulation.getGameTurnTick() < this.unit.getCooldownEndTime()) { + range += this.unitAttack.getRangeMotionBuffer(); + } + return this.unit.canReach(this.target, range) + && (this.unit.distance(this.target) >= this.unit.getUnitType().getMinimumAttackRange()); + } + + @Override + protected boolean checkTargetStillValid(final CSimulation simulation) { + return this.target.visit( + this.abilityTargetStillAliveVisitor.reset(simulation, this.unit, this.unitAttack.getTargetsAllowed())); + } + + @Override + protected void resetBeforeMoving(final CSimulation simulation) { + this.damagePointLaunchTime = 0; + this.thisOrderCooldownEndTime = 0; + } + + @Override + protected CBehavior updateOnInvalidTarget(final CSimulation simulation) { + return this.attackListener.onFinish(simulation, this.unit); + } + + @Override + public CBehavior update(final CSimulation simulation, final boolean withinRange) { + final int cooldownEndTime = this.unit.getCooldownEndTime(); + final int currentTurnTick = simulation.getGameTurnTick(); + if (withinRange) { + if (this.damagePointLaunchTime != 0) { + if (currentTurnTick >= this.damagePointLaunchTime) { + int minDamage = this.unitAttack.getMinDamage(); + final int maxDamage = Math.max(0, this.unitAttack.getMaxDamage()); + if (minDamage > maxDamage) { + minDamage = maxDamage; + } + final int damage; + if (maxDamage == 0) { + damage = 0; + } + else if (minDamage == maxDamage) { + damage = minDamage; + } + else { + damage = simulation.getSeededRandom().nextInt(maxDamage - minDamage) + minDamage; + } + this.unitAttack.launch(simulation, this.unit, this.target, damage, this.attackListener); + this.damagePointLaunchTime = 0; + } + } + else if (currentTurnTick >= cooldownEndTime) { + final float cooldownTime = this.unitAttack.getCooldownTime(); + final float animationBackswingPoint = this.unitAttack.getAnimationBackswingPoint(); + final int a1CooldownSteps = (int) (cooldownTime / WarsmashConstants.SIMULATION_STEP_TIME); + final int a1BackswingSteps = (int) (animationBackswingPoint / WarsmashConstants.SIMULATION_STEP_TIME); + final int a1DamagePointSteps = (int) (this.unitAttack.getAnimationDamagePoint() + / WarsmashConstants.SIMULATION_STEP_TIME); + this.unit.setCooldownEndTime(currentTurnTick + a1CooldownSteps); + this.thisOrderCooldownEndTime = currentTurnTick + a1CooldownSteps; + this.damagePointLaunchTime = currentTurnTick + a1DamagePointSteps; + this.backSwingTime = currentTurnTick + a1DamagePointSteps + a1BackswingSteps; + this.unit.getUnitAnimationListener().playAnimation(true, PrimaryTag.ATTACK, SequenceUtils.EMPTY, 1.0f, + true); + this.unit.getUnitAnimationListener().queueAnimation(PrimaryTag.STAND, SequenceUtils.READY, false); + } + else if ((currentTurnTick >= this.thisOrderCooldownEndTime)) { + this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.STAND, SequenceUtils.READY, 1.0f, + false); + } + } + else { + this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.STAND, SequenceUtils.READY, 1.0f, + false); + } + + if ((this.backSwingTime != 0) && (currentTurnTick >= this.backSwingTime)) { + this.backSwingTime = 0; + return this.attackListener.onFirstUpdateAfterBackswing(this); + } + return this; + } + + @Override + public void begin(final CSimulation game) { + + } + + @Override + public void end(final CSimulation game, final boolean interrupted) { + } + + @Override + public void endMove(final CSimulation game, final boolean interrupted) { + + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorAttackListener.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorAttackListener.java new file mode 100644 index 0000000..2c51f47 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorAttackListener.java @@ -0,0 +1,35 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackListener; + +public interface CBehaviorAttackListener extends CUnitAttackListener { + + // For this function, return the current attack behavior to keep attacking, or + // else return something else to interrupt it + CBehavior onFirstUpdateAfterBackswing(CBehaviorAttack currentAttackBehavior); + + CBehavior onFinish(CSimulation game, final CUnit finishingUnit); + + CBehaviorAttackListener DO_NOTHING = new CBehaviorAttackListener() { + @Override + public void onHit(final AbilityTarget target, final float damage) { + } + + @Override + public void onLaunch() { + } + + @Override + public CBehavior onFirstUpdateAfterBackswing(final CBehaviorAttack currentAttackBehavior) { + return currentAttackBehavior; + } + + @Override + public CBehavior onFinish(final CSimulation game, final CUnit finishingUnit) { + return finishingUnit.pollNextOrderBehavior(game); + } + }; +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorFollow.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorFollow.java new file mode 100644 index 0000000..2134745 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorFollow.java @@ -0,0 +1,67 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors; + +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetStillAliveVisitor; + +public class CBehaviorFollow extends CAbstractRangedBehavior { + + private int higlightOrderId; + + public CBehaviorFollow(final CUnit unit) { + super(unit); + } + + public CBehavior reset(final int higlightOrderId, final CUnit target) { + this.higlightOrderId = higlightOrderId; + return innerReset(target); + } + + @Override + public int getHighlightOrderId() { + return this.higlightOrderId; + } + + @Override + public boolean isWithinRange(final CSimulation simulation) { + return this.unit.canReach(this.target, this.unit.getAcquisitionRange()); + } + + @Override + protected CBehavior update(final CSimulation simulation, final boolean withinRange) { + this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.STAND, SequenceUtils.EMPTY, 1.0f, false); + return this; + } + + @Override + protected CBehavior updateOnInvalidTarget(final CSimulation simulation) { + return this.unit.pollNextOrderBehavior(simulation); + } + + @Override + protected boolean checkTargetStillValid(final CSimulation simulation) { + return this.target.visit(AbilityTargetStillAliveVisitor.INSTANCE); + } + + @Override + protected void resetBeforeMoving(final CSimulation simulation) { + } + + @Override + public void begin(final CSimulation game) { + + } + + @Override + public void end(final CSimulation game, final boolean interrupted) { + + } + + @Override + public void endMove(final CSimulation game, final boolean interrupted) { + + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorHoldPosition.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorHoldPosition.java new file mode 100644 index 0000000..91cbd53 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorHoldPosition.java @@ -0,0 +1,42 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors; + +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; + +public class CBehaviorHoldPosition implements CBehavior { + + private final CUnit unit; + + public CBehaviorHoldPosition(final CUnit unit) { + this.unit = unit; + } + + @Override + public int getHighlightOrderId() { + return OrderIds.holdposition; + } + + @Override + public CBehavior update(final CSimulation game) { + if (this.unit.autoAcquireAttackTargets(game, true)) { + // kind of a hack + return this.unit.getCurrentBehavior(); + } + this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.STAND, SequenceUtils.EMPTY, 1.0f, true); + return this.unit.pollNextOrderBehavior(game); + } + + @Override + public void begin(final CSimulation game) { + + } + + @Override + public void end(final CSimulation game, boolean interrupted) { + + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorMove.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorMove.java new file mode 100644 index 0000000..15099b1 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorMove.java @@ -0,0 +1,484 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors; + +import java.awt.geom.Point2D; +import java.awt.geom.Point2D.Float; +import java.util.List; + +import com.badlogic.gdx.math.Rectangle; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.MovementType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CDestructable; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWorldCollision; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.pathing.CPathfindingProcessor; + +public class CBehaviorMove implements CBehavior { + private static boolean ALWAYS_INTERRUPT_MOVE = false; + + private static final Rectangle tempRect = new Rectangle(); + private final CUnit unit; + private int highlightOrderId; + private final TargetVisitingResetter targetVisitingResetter; + + public CBehaviorMove(final CUnit unit) { + this.unit = unit; + this.targetVisitingResetter = new TargetVisitingResetter(); + } + + private boolean wasWithinPropWindow = false; + private List path = null; + private CPathfindingProcessor.GridMapping gridMapping; + private Point2D.Float target; + private int searchCycles = 0; + private CUnit followUnit; + private CRangedBehavior rangedBehavior; + private boolean firstUpdate = true; + private boolean disableCollision = false; + private boolean pathfindingActive = false; + private boolean firstPathfindJob = false; + private boolean pathfindingFailedGiveUp; + private int giveUpUntilTurnTick; + + public CBehaviorMove reset(final int highlightOrderId, final AbilityTarget target) { + target.visit(this.targetVisitingResetter.reset(highlightOrderId)); + this.rangedBehavior = null; + this.disableCollision = false; + return this; + } + + public CBehaviorMove reset(final AbilityTarget target, final CRangedBehavior rangedBehavior, + final boolean disableCollision) { + final int highlightOrderId = rangedBehavior.getHighlightOrderId(); + target.visit(this.targetVisitingResetter.reset(highlightOrderId)); + this.rangedBehavior = rangedBehavior; + this.disableCollision = disableCollision; + return this; + } + + private void internalResetMove(final int highlightOrderId, final float targetX, final float targetY) { + this.highlightOrderId = highlightOrderId; + this.wasWithinPropWindow = false; + this.gridMapping = CPathfindingProcessor.isCollisionSizeBetterSuitedForCorners( + this.unit.getUnitType().getCollisionSize()) ? CPathfindingProcessor.GridMapping.CORNERS + : CPathfindingProcessor.GridMapping.CELLS; + this.target = new Point2D.Float(targetX, targetY); + this.path = null; + this.searchCycles = 0; + this.followUnit = null; + this.firstUpdate = true; + this.pathfindingFailedGiveUp = false; + this.giveUpUntilTurnTick = 0; + } + + private void internalResetMove(final int highlightOrderId, final CUnit followUnit) { + this.highlightOrderId = highlightOrderId; + this.wasWithinPropWindow = false; + this.gridMapping = CPathfindingProcessor.isCollisionSizeBetterSuitedForCorners( + this.unit.getUnitType().getCollisionSize()) ? CPathfindingProcessor.GridMapping.CORNERS + : CPathfindingProcessor.GridMapping.CELLS; + this.target = new Float(followUnit.getX(), followUnit.getY()); + this.path = null; + this.searchCycles = 0; + this.followUnit = followUnit; + this.firstUpdate = true; + this.pathfindingFailedGiveUp = false; + this.giveUpUntilTurnTick = 0; + } + + @Override + public int getHighlightOrderId() { + return this.highlightOrderId; + } + + @Override + public CBehavior update(final CSimulation simulation) { + if ((this.rangedBehavior != null) && this.rangedBehavior.isWithinRange(simulation)) { + return this.rangedBehavior.update(simulation); + } + if (this.firstUpdate) { + // when units start moving, if they're on top of other units, maybe push them to + // the side + this.unit.setPointAndCheckUnstuck(this.unit.getX(), this.unit.getY(), simulation); + this.firstUpdate = false; + } + if (this.pathfindingFailedGiveUp) { + onMoveGiveUp(simulation); + return this.unit.pollNextOrderBehavior(simulation); + } + final float prevX = this.unit.getX(); + final float prevY = this.unit.getY(); + + MovementType movementType = this.unit.getUnitType().getMovementType(); + if (movementType == null) { + movementType = MovementType.DISABLED; + } + else if ((movementType == MovementType.FOOT) && this.disableCollision) { + movementType = MovementType.FOOT_NO_COLLISION; + } + final PathingGrid pathingGrid = simulation.getPathingGrid(); + final CWorldCollision worldCollision = simulation.getWorldCollision(); + final float collisionSize = this.unit.getUnitType().getCollisionSize(); + final float startFloatingX = prevX; + final float startFloatingY = prevY; + if (this.path == null) { + if (!this.pathfindingActive) { + if (this.followUnit != null) { + this.target.x = this.followUnit.getX(); + this.target.y = this.followUnit.getY(); + } + simulation.findNaiveSlowPath(this.unit, this.followUnit, startFloatingX, startFloatingY, this.target, + movementType, collisionSize, true, this); + this.pathfindingActive = true; + this.firstPathfindJob = true; + } + } + else if ((this.followUnit != null) && (this.path.size() > 1) && (this.target.distance(this.followUnit.getX(), + this.followUnit.getY()) > (0.1 * this.target.distance(this.unit.getX(), this.unit.getY())))) { + this.target.x = this.followUnit.getX(); + this.target.y = this.followUnit.getY(); + if (this.pathfindingActive) { + simulation.removeFromPathfindingQueue(this); + } + simulation.findNaiveSlowPath(this.unit, this.followUnit, startFloatingX, startFloatingY, this.target, + movementType, collisionSize, this.searchCycles < 4, this); + this.pathfindingActive = true; + } + float currentTargetX; + float currentTargetY; + if ((this.path == null) || this.path.isEmpty()) { + if (this.followUnit != null) { + currentTargetX = this.followUnit.getX(); + currentTargetY = this.followUnit.getY(); + } + else { + currentTargetX = this.target.x; + currentTargetY = this.target.y; + } + } + else { + if ((this.followUnit != null) && (this.path.size() == 1)) { + currentTargetX = this.followUnit.getX(); + currentTargetY = this.followUnit.getY(); + } + else { + final Point2D.Float nextPathElement = this.path.get(0); + currentTargetX = nextPathElement.x; + currentTargetY = nextPathElement.y; + } + } + + float deltaX = currentTargetX - prevX; + float deltaY = currentTargetY - prevY; + double goalAngleRad = Math.atan2(deltaY, deltaX); + float goalAngle = (float) Math.toDegrees(goalAngleRad); + if (goalAngle < 0) { + goalAngle += 360; + } + float facing = this.unit.getFacing(); + float delta = goalAngle - facing; + final float propulsionWindow = this.unit.getUnitType().getPropWindow(); + final float turnRate = this.unit.getUnitType().getTurnRate(); + final int speed = this.unit.getSpeed(); + + if (delta < -180) { + delta = 360 + delta; + } + if (delta > 180) { + delta = -360 + delta; + } + float absDelta = Math.abs(delta); + + if ((absDelta <= 1.0) && (absDelta != 0)) { + this.unit.setFacing(goalAngle); + } + else { + float angleToAdd = Math.signum(delta) * (float) Math.toDegrees(turnRate); + if (absDelta < Math.abs(angleToAdd)) { + angleToAdd = delta; + } + facing += angleToAdd; + this.unit.setFacing(facing); + } + final boolean blockedByGiveUpUntilTickDelay = simulation.getGameTurnTick() < this.giveUpUntilTurnTick; + if (!blockedByGiveUpUntilTickDelay && (this.path != null) && !this.pathfindingActive + && (absDelta < propulsionWindow)) { + final float speedTick = speed * WarsmashConstants.SIMULATION_STEP_TIME; + double continueDistance = speedTick; + do { + boolean done; + float nextX, nextY; + final double travelDistance = Math.sqrt((deltaX * deltaX) + (deltaY * deltaY)); + if (travelDistance <= continueDistance) { + nextX = currentTargetX; + nextY = currentTargetY; + continueDistance = continueDistance - travelDistance; + done = true; + } + else { + final double radianFacing = Math.toRadians(facing); + nextX = (prevX + (float) (Math.cos(radianFacing) * continueDistance)); + nextY = (prevY + (float) (Math.sin(radianFacing) * continueDistance)); + continueDistance = 0; +// done = (this.gridMapping.getX(pathingGrid, nextX) == this.gridMapping.getX(pathingGrid, +// currentTargetX)) +// && (this.gridMapping.getY(pathingGrid, nextY) == this.gridMapping.getY(pathingGrid, +// currentTargetY)); + done = false; + } + tempRect.set(this.unit.getCollisionRectangle()); + tempRect.setCenter(nextX, nextY); + if ((movementType == null) || (pathingGrid.isPathable(nextX, nextY, movementType, collisionSize)// ((int) + // collisionSize + // / 16) + // * 16 + && !worldCollision.intersectsAnythingOtherThan(tempRect, this.unit, movementType))) { + this.unit.setPoint(nextX, nextY, worldCollision, simulation.getRegionManager()); + if (done) { + // if we're making headway along the path then it's OK to start thinking fast + // again + if (travelDistance > 0) { + this.searchCycles = 0; + } + if (this.path.isEmpty()) { + onMoveGiveUp(simulation); + return this.unit.pollNextOrderBehavior(simulation); + } + else { + System.out.println(this.path); + final Float removed = this.path.remove(0); + System.out.println( + "We think we reached " + removed + " because we are at " + nextX + "," + nextY); + final boolean emptyPath = this.path.isEmpty(); + if (emptyPath) { + if (this.followUnit != null) { + currentTargetX = this.followUnit.getX(); + currentTargetY = this.followUnit.getY(); + } + else { + currentTargetX = this.target.x; + currentTargetY = this.target.y; + } + } + else { + if ((this.followUnit != null) && (this.path.size() == 1)) { + currentTargetX = this.followUnit.getX(); + currentTargetY = this.followUnit.getY(); + } + else { + final Point2D.Float firstPathElement = this.path.get(0); + currentTargetX = firstPathElement.x; + currentTargetY = firstPathElement.y; + } + } + deltaY = currentTargetY - nextY; + deltaX = currentTargetX - nextX; + if ((deltaX == 0.000f) && (deltaY == 0.000f) && this.path.isEmpty()) { + onMoveGiveUp(simulation); + return this.unit.pollNextOrderBehavior(simulation); + } + System.out.println("new target: " + currentTargetX + "," + currentTargetY); + System.out.println("new delta: " + deltaX + "," + deltaY); + goalAngleRad = Math.atan2(deltaY, deltaX); + goalAngle = (float) Math.toDegrees(goalAngleRad); + if (goalAngle < 0) { + goalAngle += 360; + } + facing = this.unit.getFacing(); + delta = goalAngle - facing; + + if (delta < -180) { + delta = 360 + delta; + } + if (delta > 180) { + delta = -360 + delta; + } + absDelta = Math.abs(delta); + if (absDelta >= propulsionWindow) { + if (this.wasWithinPropWindow) { + this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.STAND, + SequenceUtils.EMPTY, 1.0f, true); + } + this.wasWithinPropWindow = false; + return this; + } + } + } + } + else { + if (this.followUnit != null) { + this.target.x = this.followUnit.getX(); + this.target.y = this.followUnit.getY(); + } + if (!this.pathfindingActive) { + simulation.findNaiveSlowPath(this.unit, this.followUnit, startFloatingX, startFloatingY, + this.target, movementType, collisionSize, this.searchCycles < 4, this); + this.pathfindingActive = true; + this.searchCycles++; + return this; + } + } + this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.WALK, SequenceUtils.EMPTY, 1.0f, + true); + this.wasWithinPropWindow = true; + } + while (continueDistance > 0); + } + else { + // If this happens, the unit is facing the wrong way, and has to turn before + // moving. + if (this.wasWithinPropWindow) { + this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.STAND, SequenceUtils.EMPTY, 1.0f, + true); + } + this.wasWithinPropWindow = false; + } + + return this; + } + + private void onMoveGiveUp(final CSimulation simulation) { + if (this.rangedBehavior != null) { + this.rangedBehavior.endMove(simulation, true); + } + } + + private final class TargetVisitingResetter implements AbilityTargetVisitor { + private int highlightOrderId; + + private TargetVisitingResetter reset(final int highlightOrderId) { + this.highlightOrderId = highlightOrderId; + return this; + } + + @Override + public Void accept(final AbilityPointTarget target) { + internalResetMove(this.highlightOrderId, target.x, target.y); + return null; + } + + @Override + public Void accept(final CUnit target) { + internalResetMove(this.highlightOrderId, target); + return null; + } + + @Override + public Void accept(final CDestructable target) { + internalResetMove(this.highlightOrderId, target.getX(), target.getY()); + return null; + } + + @Override + public Void accept(final CItem target) { + internalResetMove(this.highlightOrderId, target.getX(), target.getY()); + return null; + } + } + + @Override + public void begin(final CSimulation game) { + + } + + @Override + public void end(final CSimulation game, final boolean interrupted) { + if (ALWAYS_INTERRUPT_MOVE) { + game.removeFromPathfindingQueue(this); + this.pathfindingActive = false; + } + if (this.rangedBehavior != null) { + this.rangedBehavior.endMove(game, interrupted); + } + } + + public CUnit getUnit() { + return this.unit; + } + + public void pathFound(final List waypoints, final CSimulation simulation) { + this.pathfindingActive = false; + + final float prevX = this.unit.getX(); + final float prevY = this.unit.getY(); + + MovementType movementType = this.unit.getUnitType().getMovementType(); + if (movementType == null) { + movementType = MovementType.DISABLED; + } + else if ((movementType == MovementType.FOOT) && this.disableCollision) { + movementType = MovementType.FOOT_NO_COLLISION; + } + final PathingGrid pathingGrid = simulation.getPathingGrid(); + final CWorldCollision worldCollision = simulation.getWorldCollision(); + final float collisionSize = this.unit.getUnitType().getCollisionSize(); + final float startFloatingX = prevX; + final float startFloatingY = prevY; + + this.path = waypoints; + if (this.firstPathfindJob) { + this.firstPathfindJob = false; + System.out.println("init path " + this.path); + // check for smoothing + if (!this.path.isEmpty()) { + float lastX = startFloatingX; + float lastY = startFloatingY; + float smoothingGroupStartX = startFloatingX; + float smoothingGroupStartY = startFloatingY; + final Point2D.Float firstPathElement = this.path.get(0); + double totalPathDistance = firstPathElement.distance(lastX, lastY); + lastX = firstPathElement.x; + lastY = firstPathElement.y; + int smoothingStartIndex = -1; + for (int i = 0; i < (this.path.size() - 1); i++) { + final Point2D.Float nextPossiblePathElement = this.path.get(i + 1); + totalPathDistance += nextPossiblePathElement.distance(lastX, lastY); + if ((totalPathDistance < (1.15 + * nextPossiblePathElement.distance(smoothingGroupStartX, smoothingGroupStartY))) + && pathingGrid.isPathable((smoothingGroupStartX + nextPossiblePathElement.x) / 2, + (smoothingGroupStartY + nextPossiblePathElement.y) / 2, movementType)) { + if (smoothingStartIndex == -1) { + smoothingStartIndex = i; + } + } + else { + if (smoothingStartIndex != -1) { + for (int j = i - 1; j >= smoothingStartIndex; j--) { + this.path.remove(j); + } + i = smoothingStartIndex; + } + smoothingStartIndex = -1; + final Point2D.Float smoothGroupNext = this.path.get(i); + smoothingGroupStartX = smoothGroupNext.x; + smoothingGroupStartY = smoothGroupNext.y; + totalPathDistance = nextPossiblePathElement.distance(smoothGroupNext); + } + lastX = nextPossiblePathElement.x; + lastY = nextPossiblePathElement.y; + } + if (smoothingStartIndex != -1) { + for (int j = smoothingStartIndex; j < (this.path.size() - 1); j++) { + final Point2D.Float removed = this.path.remove(j); + } + } + } + } + else if (this.path.isEmpty() || (this.searchCycles > 6)) { + if (this.searchCycles > 9) { + this.pathfindingFailedGiveUp = true; + } + else { + this.giveUpUntilTurnTick = simulation.getGameTurnTick() + + (int) (5 / WarsmashConstants.SIMULATION_STEP_TIME); + } + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorPatrol.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorPatrol.java new file mode 100644 index 0000000..b1edb7b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorPatrol.java @@ -0,0 +1,66 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; + +public class CBehaviorPatrol implements CRangedBehavior { + + private final CUnit unit; + private AbilityPointTarget target; + private AbilityPointTarget startPoint; + private boolean justAutoAttacked = false; + + public CBehaviorPatrol(final CUnit unit) { + this.unit = unit; + } + + public CBehavior reset(final AbilityPointTarget target) { + this.target = target; + this.startPoint = new AbilityPointTarget(this.unit.getX(), this.unit.getY()); + return this; + } + + @Override + public int getHighlightOrderId() { + return OrderIds.patrol; + } + + @Override + public boolean isWithinRange(final CSimulation simulation) { + if (this.justAutoAttacked = this.unit.autoAcquireAttackTargets(simulation, false)) { + // kind of a hack + return true; + } + return this.unit.distance(this.target.x, this.target.y) <= 16f; // TODO this is not how it was meant to be used + } + + @Override + public CBehavior update(final CSimulation simulation) { + if (this.justAutoAttacked) { + this.justAutoAttacked = false; + return this.unit.getCurrentBehavior(); + } + final AbilityPointTarget temp = this.target; + this.target = this.startPoint; + this.startPoint = temp; + return this.unit.getMoveBehavior().reset(this.target, this, false); + } + + @Override + public void begin(final CSimulation game) { + + } + + @Override + public void end(final CSimulation game, final boolean interrupted) { + + } + + @Override + public void endMove(final CSimulation game, final boolean interrupted) { + + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorStop.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorStop.java new file mode 100644 index 0000000..e45bc02 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CBehaviorStop.java @@ -0,0 +1,41 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors; + +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; + +public class CBehaviorStop implements CBehavior { + + private final CUnit unit; + + public CBehaviorStop(final CUnit unit) { + this.unit = unit; + } + + @Override + public int getHighlightOrderId() { + return OrderIds.stop; + } + + @Override + public CBehavior update(final CSimulation game) { + if (this.unit.autoAcquireAttackTargets(game, false)) { + return this.unit.getCurrentBehavior(); + } + this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.STAND, SequenceUtils.EMPTY, 1.0f, true); + return this.unit.pollNextOrderBehavior(game); + } + + @Override + public void begin(final CSimulation game) { + + } + + @Override + public void end(final CSimulation game, boolean interrupted) { + + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CRangedBehavior.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CRangedBehavior.java new file mode 100644 index 0000000..6b8ce91 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/CRangedBehavior.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; + +public interface CRangedBehavior extends CBehavior { + boolean isWithinRange(final CSimulation simulation); + + void endMove(CSimulation game, boolean interrupted); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/build/AbilityDisableWhileUnderConstructionVisitor.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/build/AbilityDisableWhileUnderConstructionVisitor.java new file mode 100644 index 0000000..06bc41d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/build/AbilityDisableWhileUnderConstructionVisitor.java @@ -0,0 +1,136 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.build; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityAttack; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityGeneric; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityMove; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityBuildInProgress; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityHumanBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityNagaBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityNeutralBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityNightElfBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityOrcBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityUndeadBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.combat.CAbilityColdArrows; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic.GenericNoIconAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic.GenericSingleIconActiveAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.hero.CAbilityHero; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.queue.CAbilityQueue; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.queue.CAbilityRally; + +public class AbilityDisableWhileUnderConstructionVisitor implements CAbilityVisitor { + public static final AbilityDisableWhileUnderConstructionVisitor INSTANCE = new AbilityDisableWhileUnderConstructionVisitor(); + + @Override + public Void accept(final CAbilityAttack ability) { + ability.setDisabled(true); + ability.setIconShowing(false); + return null; + } + + @Override + public Void accept(final CAbilityMove ability) { + ability.setDisabled(true); + ability.setIconShowing(false); + return null; + } + + @Override + public Void accept(final CAbilityOrcBuild ability) { + ability.setDisabled(true); + ability.setIconShowing(false); + return null; + } + + @Override + public Void accept(final CAbilityHumanBuild ability) { + ability.setDisabled(true); + ability.setIconShowing(false); + return null; + } + + @Override + public Void accept(final CAbilityUndeadBuild ability) { + ability.setDisabled(true); + ability.setIconShowing(false); + return null; + } + + @Override + public Void accept(final CAbilityNightElfBuild ability) { + ability.setDisabled(true); + ability.setIconShowing(false); + return null; + } + + @Override + public Void accept(final CAbilityGeneric ability) { + ability.setDisabled(true); + ability.setIconShowing(false); + return null; + } + + @Override + public Void accept(final CAbilityColdArrows ability) { + ability.setDisabled(true); + ability.setIconShowing(false); + return null; + } + + @Override + public Void accept(final CAbilityNagaBuild ability) { + ability.setDisabled(true); + ability.setIconShowing(false); + return null; + } + + @Override + public Void accept(final CAbilityNeutralBuild ability) { + ability.setDisabled(true); + ability.setIconShowing(false); + return null; + } + + @Override + public Void accept(final CAbilityBuildInProgress ability) { + ability.setDisabled(false); + ability.setIconShowing(false); + return null; + } + + @Override + public Void accept(final CAbilityQueue ability) { + ability.setDisabled(true); + ability.setIconShowing(false); + return null; + } + + @Override + public Void accept(final GenericSingleIconActiveAbility ability) { + ability.setDisabled(true); + ability.setIconShowing(false); + return null; + } + + @Override + public Void accept(final CAbilityRally ability) { + ability.setDisabled(false); + ability.setIconShowing(false); + return null; + } + + @Override + public Void accept(final GenericNoIconAbility ability) { + ability.setDisabled(true); + ability.setIconShowing(false); + return null; + } + + @Override + public Void accept(final CAbilityHero ability) { + ability.setDisabled(true); + ability.setIconShowing(false); + return null; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/build/CBehaviorOrcBuild.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/build/CBehaviorOrcBuild.java new file mode 100644 index 0000000..1b91123 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/build/CBehaviorOrcBuild.java @@ -0,0 +1,135 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.build; + +import java.awt.image.BufferedImage; +import java.util.EnumSet; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityBuildInProgress; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CAbstractRangedBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.pathing.CBuildingPathingType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayer; + +public class CBehaviorOrcBuild extends CAbstractRangedBehavior { + private int highlightOrderId; + private War3ID orderId; + private boolean unitCreated = false; + + public CBehaviorOrcBuild(final CUnit unit) { + super(unit); + } + + public CBehavior reset(final AbilityPointTarget target, final int orderId, final int highlightOrderId) { + this.highlightOrderId = highlightOrderId; + this.orderId = new War3ID(orderId); + this.unitCreated = false; + return innerReset(target); + } + + @Override + public boolean isWithinRange(final CSimulation simulation) { + final CUnitType unitType = simulation.getUnitData().getUnitType(this.orderId); + return this.unit.canReachToPathing(0, simulation.getGameplayConstants().getBuildingAngle(), + unitType.getBuildingPathingPixelMap(), this.target.getX(), this.target.getY()); + } + + @Override + public int getHighlightOrderId() { + return this.highlightOrderId; + } + + @Override + protected CBehavior update(final CSimulation simulation, final boolean withinRange) { + if (!this.unitCreated) { + this.unitCreated = true; + final CUnitType unitTypeToCreate = simulation.getUnitData().getUnitType(this.orderId); + final BufferedImage buildingPathingPixelMap = unitTypeToCreate.getBuildingPathingPixelMap(); + boolean buildLocationObstructed = false; + if (buildingPathingPixelMap != null) { + final EnumSet preventedPathingTypes = unitTypeToCreate.getPreventedPathingTypes(); + final EnumSet requiredPathingTypes = unitTypeToCreate.getRequiredPathingTypes(); + + if (!simulation.getPathingGrid().checkPathingTexture(this.target.getX(), this.target.getY(), + (int) simulation.getGameplayConstants().getBuildingAngle(), buildingPathingPixelMap, + preventedPathingTypes, requiredPathingTypes, simulation.getWorldCollision(), this.unit)) { + buildLocationObstructed = true; + } + } + final int playerIndex = this.unit.getPlayerIndex(); + if (!buildLocationObstructed) { + final CUnit constructedStructure = simulation.createUnit(this.orderId, playerIndex, this.target.getX(), + this.target.getY(), simulation.getGameplayConstants().getBuildingAngle()); + constructedStructure.setConstructing(true); + constructedStructure.setWorkerInside(this.unit); + constructedStructure.setLife(simulation, + constructedStructure.getMaximumLife() * WarsmashConstants.BUILDING_CONSTRUCT_START_LIFE); + constructedStructure.setFoodUsed(unitTypeToCreate.getFoodUsed()); + constructedStructure.add(simulation, + new CAbilityBuildInProgress(simulation.getHandleIdAllocator().createId())); + for (final CAbility ability : constructedStructure.getAbilities()) { + ability.visit(AbilityDisableWhileUnderConstructionVisitor.INSTANCE); + } + this.unit.setHidden(true); + this.unit.setPaused(true); + this.unit.setInvulnerable(true); + simulation.unitConstructedEvent(this.unit, constructedStructure); + } + else { + final CPlayer player = simulation.getPlayer(playerIndex); + refund(player, unitTypeToCreate); + simulation.getCommandErrorListener(playerIndex).showCantPlaceError(); + } + } + return this.unit.pollNextOrderBehavior(simulation); + } + + @Override + protected boolean checkTargetStillValid(final CSimulation simulation) { + return true; + } + + @Override + protected CBehavior updateOnInvalidTarget(final CSimulation simulation) { + return this.unit.pollNextOrderBehavior(simulation); + } + + @Override + protected void resetBeforeMoving(final CSimulation simulation) { + + } + + @Override + public void begin(final CSimulation game) { + + } + + @Override + public void end(final CSimulation game, final boolean interrupted) { + if (!this.unitCreated && interrupted) { + final CPlayer player = game.getPlayer(this.unit.getPlayerIndex()); + final CUnitType unitTypeToCreate = game.getUnitData().getUnitType(this.orderId); + refund(player, unitTypeToCreate); + } + } + + private void refund(final CPlayer player, final CUnitType unitTypeToCreate) { + player.setFoodUsed(player.getFoodUsed() - unitTypeToCreate.getFoodUsed()); + player.refundFor(unitTypeToCreate); + } + + @Override + public void endMove(final CSimulation game, final boolean interrupted) { + if (!this.unitCreated && interrupted) { + final CPlayer player = game.getPlayer(this.unit.getPlayerIndex()); + final CUnitType unitTypeToCreate = game.getUnitData().getUnitType(this.orderId); + refund(player, unitTypeToCreate); + } + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/build/CBehaviorUndeadBuild.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/build/CBehaviorUndeadBuild.java new file mode 100644 index 0000000..897d2c1 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/build/CBehaviorUndeadBuild.java @@ -0,0 +1,157 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.build; + +import java.awt.image.BufferedImage; +import java.util.EnumSet; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityBuildInProgress; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CAbstractRangedBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.pathing.CBuildingPathingType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayer; + +public class CBehaviorUndeadBuild extends CAbstractRangedBehavior { + private static int delayAnimationTicks = (int) (2.267f / WarsmashConstants.SIMULATION_STEP_TIME); + private int highlightOrderId; + private War3ID orderId; + private boolean unitCreated = false; + private int doneTick = 0; + + public CBehaviorUndeadBuild(final CUnit unit) { + super(unit); + } + + public CBehavior reset(final AbilityPointTarget target, final int orderId, final int highlightOrderId) { + this.highlightOrderId = highlightOrderId; + this.orderId = new War3ID(orderId); + this.unitCreated = false; + return innerReset(target); + } + + @Override + public boolean isWithinRange(final CSimulation simulation) { + if (this.doneTick != 0) { + return true; + } + final CUnitType unitType = simulation.getUnitData().getUnitType(this.orderId); + final BufferedImage buildingPathingPixelMap = unitType.getBuildingPathingPixelMap(); + if (buildingPathingPixelMap == null) { + return this.unit.canReach(this.target.getX(), this.target.getY(), unitType.getCollisionSize()); + } + return this.unit.canReachToPathing(0, simulation.getGameplayConstants().getBuildingAngle(), + buildingPathingPixelMap, this.target.getX(), this.target.getY()); + } + + @Override + public int getHighlightOrderId() { + return this.highlightOrderId; + } + + @Override + protected CBehavior update(final CSimulation simulation, final boolean withinRange) { + if (this.doneTick != 0) { + if (simulation.getGameTurnTick() > this.doneTick) { + return this.unit.pollNextOrderBehavior(simulation); + } + } + else if (!this.unitCreated) { + this.unitCreated = true; + final CUnitType unitTypeToCreate = simulation.getUnitData().getUnitType(this.orderId); + final BufferedImage buildingPathingPixelMap = unitTypeToCreate.getBuildingPathingPixelMap(); + boolean buildLocationObstructed = false; + if (buildingPathingPixelMap != null) { + final EnumSet preventedPathingTypes = unitTypeToCreate.getPreventedPathingTypes(); + final EnumSet requiredPathingTypes = unitTypeToCreate.getRequiredPathingTypes(); + + if (!simulation.getPathingGrid().checkPathingTexture(this.target.getX(), this.target.getY(), + (int) simulation.getGameplayConstants().getBuildingAngle(), buildingPathingPixelMap, + preventedPathingTypes, requiredPathingTypes, simulation.getWorldCollision(), this.unit)) { + buildLocationObstructed = true; + } + } + final int playerIndex = this.unit.getPlayerIndex(); + if (!buildLocationObstructed) { + final CUnit constructedStructure = simulation.createUnit(this.orderId, playerIndex, this.target.getX(), + this.target.getY(), simulation.getGameplayConstants().getBuildingAngle()); + constructedStructure.setConstructing(true); + constructedStructure.setLife(simulation, + constructedStructure.getMaximumLife() * WarsmashConstants.BUILDING_CONSTRUCT_START_LIFE); + constructedStructure.setFoodUsed(unitTypeToCreate.getFoodUsed()); + constructedStructure.add(simulation, + new CAbilityBuildInProgress(simulation.getHandleIdAllocator().createId())); + for (final CAbility ability : constructedStructure.getAbilities()) { + ability.visit(AbilityDisableWhileUnderConstructionVisitor.INSTANCE); + } + final float deltaX = this.unit.getX() - this.target.getX(); + final float deltaY = this.unit.getY() - this.target.getY(); + final float delta = (float) Math.sqrt((deltaX * deltaX) + (deltaY * deltaY)); + this.unit.setPoint(this.target.getX() + ((deltaX / delta) * unitTypeToCreate.getCollisionSize()), + this.target.getY() + ((deltaY / delta) * unitTypeToCreate.getCollisionSize()), + simulation.getWorldCollision(), simulation.getRegionManager()); + simulation.unitRepositioned(this.unit); + simulation.unitConstructedEvent(this.unit, constructedStructure); + this.doneTick = simulation.getGameTurnTick() + delayAnimationTicks; + } + else { + final CPlayer player = simulation.getPlayer(playerIndex); + refund(player, unitTypeToCreate); + simulation.getCommandErrorListener(playerIndex).showCantPlaceError(); + return this.unit.pollNextOrderBehavior(simulation); + } + } + this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.STAND, SequenceUtils.WORK, 1.0f, true); + return this; + } + + @Override + protected boolean checkTargetStillValid(final CSimulation simulation) { + return true; + } + + @Override + protected CBehavior updateOnInvalidTarget(final CSimulation simulation) { + return this.unit.pollNextOrderBehavior(simulation); + } + + @Override + protected void resetBeforeMoving(final CSimulation simulation) { + + } + + @Override + public void begin(final CSimulation game) { + + } + + @Override + public void end(final CSimulation game, final boolean interrupted) { + if (!this.unitCreated && interrupted) { + final CPlayer player = game.getPlayer(this.unit.getPlayerIndex()); + final CUnitType unitTypeToCreate = game.getUnitData().getUnitType(this.orderId); + refund(player, unitTypeToCreate); + } + } + + private void refund(final CPlayer player, final CUnitType unitTypeToCreate) { + player.setFoodUsed(player.getFoodUsed() - unitTypeToCreate.getFoodUsed()); + player.refundFor(unitTypeToCreate); + } + + @Override + public void endMove(final CSimulation game, final boolean interrupted) { + if (!this.unitCreated && interrupted) { + final CPlayer player = game.getPlayer(this.unit.getPlayerIndex()); + final CUnitType unitTypeToCreate = game.getUnitData().getUnitType(this.orderId); + refund(player, unitTypeToCreate); + } + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/harvest/CBehaviorHarvest.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/harvest/CBehaviorHarvest.java new file mode 100644 index 0000000..3833f81 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/harvest/CBehaviorHarvest.java @@ -0,0 +1,233 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.harvest; + +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.SecondaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CDestructable; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.harvest.CAbilityHarvest; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.mine.CAbilityGoldMine; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetStillAliveVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CAbstractRangedBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorAttack; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorAttackListener; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.ResourceType; + +public class CBehaviorHarvest extends CAbstractRangedBehavior + implements AbilityTargetVisitor, CBehaviorAttackListener { + private final CAbilityHarvest abilityHarvest; + private CSimulation simulation; + private int popoutFromMineTurnTick = 0; + + public CBehaviorHarvest(final CUnit unit, final CAbilityHarvest abilityHarvest) { + super(unit); + this.abilityHarvest = abilityHarvest; + } + + public CBehaviorHarvest reset(final CWidget target) { + innerReset(target, target instanceof CUnit); + this.abilityHarvest.setLastHarvestTarget(target); + if (this.popoutFromMineTurnTick != 0) { + // TODO this check is probably only for debug and should be removed after + // extensive testing + throw new IllegalStateException("A unit took action while within a gold mine."); + } + return this; + } + + @Override + public boolean isWithinRange(final CSimulation simulation) { + return this.unit.canReach(this.target, this.unit.getUnitType().getCollisionSize()); + } + + @Override + public int getHighlightOrderId() { + return OrderIds.harvest; + } + + @Override + protected CBehavior update(final CSimulation simulation, final boolean withinRange) { + this.simulation = simulation; + return this.target.visit(this); + } + + @Override + public CBehavior accept(final AbilityPointTarget target) { + return CBehaviorHarvest.this.unit.pollNextOrderBehavior(this.simulation); + } + + @Override + public CBehavior accept(final CUnit target) { + if ((this.abilityHarvest.getCarriedResourceAmount() == 0) + || (this.abilityHarvest.getCarriedResourceType() != ResourceType.GOLD)) { + for (final CAbility ability : target.getAbilities()) { + if (ability instanceof CAbilityGoldMine) { + final CAbilityGoldMine abilityGoldMine = (CAbilityGoldMine) ability; + final int activeMiners = abilityGoldMine.getActiveMinerCount(); + if (activeMiners < abilityGoldMine.getMiningCapacity()) { + abilityGoldMine.addMiner(this); + this.unit.setHidden(true); + this.unit.setInvulnerable(true); + this.unit.setPaused(true); + this.unit.setAcceptingOrders(false); + this.popoutFromMineTurnTick = this.simulation.getGameTurnTick() + + (int) (abilityGoldMine.getMiningDuration() / WarsmashConstants.SIMULATION_STEP_TIME); + } + else { + // we are stuck waiting to mine, let's make sure we play stand animation + this.unit.getUnitAnimationListener().playAnimation(false, PrimaryTag.STAND, SequenceUtils.EMPTY, + 1.0f, true); + } + return this; + } + } + // weird invalid target and we have no resources, consider harvesting done + if (this.abilityHarvest.getCarriedResourceAmount() == 0) { + return this.unit.pollNextOrderBehavior(this.simulation); + } + else { + return this.abilityHarvest.getBehaviorReturnResources().reset(this.simulation); + } + } + else { + // we have some GOLD and we're not in a mine (?) lets do a return resources + // order + return this.abilityHarvest.getBehaviorReturnResources().reset(this.simulation); + } + } + + public void popoutFromMine(final int goldMined) { + this.popoutFromMineTurnTick = 0; + this.unit.setHidden(false); + this.unit.setInvulnerable(false); + this.unit.setPaused(false); + this.unit.setAcceptingOrders(true); + dropResources(); + this.abilityHarvest.setCarriedResources(ResourceType.GOLD, goldMined); + this.unit.getUnitAnimationListener().addSecondaryTag(SecondaryTag.GOLD); + this.simulation.unitRepositioned(this.unit); + } + + @Override + public CBehavior accept(final CDestructable target) { + if ((this.abilityHarvest.getCarriedResourceType() != ResourceType.LUMBER) + || (this.abilityHarvest.getCarriedResourceAmount() < this.abilityHarvest.getLumberCapacity())) { + return this.unit.getAttackBehavior().reset(getHighlightOrderId(), this.abilityHarvest.getTreeAttack(), + target, false, this); + } + else { + // we have some LUMBER and we can't carry any more, time to return resources + return this.abilityHarvest.getBehaviorReturnResources().reset(this.simulation); + } + } + + @Override + public void onHit(final AbilityTarget target, final float damage) { + if (this.abilityHarvest.getCarriedResourceType() != ResourceType.LUMBER) { + dropResources(); + } + this.abilityHarvest.setCarriedResources(ResourceType.LUMBER, + Math.min(this.abilityHarvest.getCarriedResourceAmount() + this.abilityHarvest.getDamageToTree(), + this.abilityHarvest.getLumberCapacity())); + this.unit.getUnitAnimationListener().addSecondaryTag(SecondaryTag.LUMBER); + } + + @Override + public void onLaunch() { + + } + + @Override + public CBehavior onFirstUpdateAfterBackswing(final CBehaviorAttack currentAttackBehavior) { + if (this.abilityHarvest.getCarriedResourceAmount() >= this.abilityHarvest.getLumberCapacity()) { + return this.abilityHarvest.getBehaviorReturnResources().reset(this.simulation); + } + return currentAttackBehavior; + } + + @Override + public CBehavior onFinish(final CSimulation game, final CUnit finishingUnit) { + if (this.abilityHarvest.getCarriedResourceAmount() >= this.abilityHarvest.getLumberCapacity()) { + return this.abilityHarvest.getBehaviorReturnResources().reset(this.simulation); + } + return updateOnInvalidTarget(game); + } + + @Override + public CBehavior accept(final CItem target) { + return this.unit.pollNextOrderBehavior(this.simulation); + } + + @Override + protected boolean checkTargetStillValid(final CSimulation simulation) { + return this.target.visit(AbilityTargetStillAliveVisitor.INSTANCE); + } + + @Override + protected CBehavior updateOnInvalidTarget(final CSimulation simulation) { + if (this.target instanceof CDestructable) { + // wood + final CDestructable nearestTree = CBehaviorReturnResources.findNearestTree(this.unit, this.abilityHarvest, + simulation, this.unit); + if (nearestTree != null) { + return reset(nearestTree); + } + } + return this.unit.pollNextOrderBehavior(simulation); + } + + @Override + protected void resetBeforeMoving(final CSimulation simulation) { + + } + + @Override + public void begin(final CSimulation game) { + + } + + @Override + public void end(final CSimulation game, final boolean interrupted) { + + } + + public int getPopoutFromMineTurnTick() { + return this.popoutFromMineTurnTick; + } + + public int getGoldCapacity() { + return this.abilityHarvest.getGoldCapacity(); + } + + private void dropResources() { + if (this.abilityHarvest.getCarriedResourceType() != null) { + switch (this.abilityHarvest.getCarriedResourceType()) { + case FOOD: + throw new IllegalStateException("Unit used Harvest skill to carry FOOD resource!"); + case GOLD: + this.unit.getUnitAnimationListener().removeSecondaryTag(SecondaryTag.GOLD); + break; + case LUMBER: + this.unit.getUnitAnimationListener().removeSecondaryTag(SecondaryTag.LUMBER); + break; + } + } + this.abilityHarvest.setCarriedResources(null, 0); + } + + @Override + public void endMove(final CSimulation game, final boolean interrupted) { + + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/harvest/CBehaviorReturnResources.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/harvest/CBehaviorReturnResources.java new file mode 100644 index 0000000..f6b43cd --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/harvest/CBehaviorReturnResources.java @@ -0,0 +1,235 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.harvest; + +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.SecondaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CDestructable; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.harvest.CAbilityHarvest; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.harvest.CAbilityReturnResources; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.mine.CAbilityGoldMine; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetStillAliveVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CAbstractRangedBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayer; + +public class CBehaviorReturnResources extends CAbstractRangedBehavior implements AbilityTargetVisitor { + private final CAbilityHarvest abilityHarvest; + private CSimulation simulation; + + public CBehaviorReturnResources(final CUnit unit, final CAbilityHarvest abilityHarvest) { + super(unit); + this.abilityHarvest = abilityHarvest; + } + + public CBehavior reset(final CSimulation simulation) { + final CUnit nearestDropoffPoint = findNearestDropoffPoint(simulation); + if (nearestDropoffPoint == null) { + // TODO it is unconventional not to return self here + return this.unit.pollNextOrderBehavior(simulation); + } + innerReset(nearestDropoffPoint, true); + return this; + } + + @Override + public boolean isWithinRange(final CSimulation simulation) { + // TODO this is probably not what the CloseEnoughRange constant is for + return this.unit.canReach(this.target, this.unit.getUnitType().getCollisionSize()); + } + + @Override + public int getHighlightOrderId() { + return OrderIds.returnresources; + } + + @Override + protected CBehavior update(final CSimulation simulation, final boolean withinRange) { + this.simulation = simulation; + return this.target.visit(this); + } + + @Override + public CBehavior accept(final AbilityPointTarget target) { + return CBehaviorReturnResources.this.unit.pollNextOrderBehavior(this.simulation); + } + + @Override + public CBehavior accept(final CUnit target) { + for (final CAbility ability : target.getAbilities()) { + if (ability instanceof CAbilityReturnResources) { + final CAbilityReturnResources abilityReturnResources = (CAbilityReturnResources) ability; + if (abilityReturnResources.accepts(this.abilityHarvest.getCarriedResourceType())) { + final CPlayer player = this.simulation.getPlayer(this.unit.getPlayerIndex()); + CWidget nextTarget = null; + switch (this.abilityHarvest.getCarriedResourceType()) { + case FOOD: + throw new IllegalStateException("Unit used Harvest skill to carry FOOD resource!"); + case GOLD: + player.setGold(player.getGold() + this.abilityHarvest.getCarriedResourceAmount()); + this.unit.getUnitAnimationListener().removeSecondaryTag(SecondaryTag.GOLD); + if ((this.abilityHarvest.getLastHarvestTarget() != null) && this.abilityHarvest + .getLastHarvestTarget().visit(AbilityTargetStillAliveVisitor.INSTANCE)) { + nextTarget = this.abilityHarvest.getLastHarvestTarget(); + } + else { + nextTarget = findNearestMine(this.unit, this.simulation); + } + break; + case LUMBER: + player.setLumber(player.getLumber() + this.abilityHarvest.getCarriedResourceAmount()); + this.unit.getUnitAnimationListener().removeSecondaryTag(SecondaryTag.LUMBER); + if (this.abilityHarvest.getLastHarvestTarget() != null) { + if (this.abilityHarvest.getLastHarvestTarget() + .visit(AbilityTargetStillAliveVisitor.INSTANCE)) { + nextTarget = this.abilityHarvest.getLastHarvestTarget(); + } + else { + nextTarget = findNearestTree(this.unit, this.abilityHarvest, this.simulation, + this.abilityHarvest.getLastHarvestTarget()); + } + } + else { + nextTarget = findNearestTree(this.unit, this.abilityHarvest, this.simulation, this.unit); + } + break; + } + this.simulation.unitGainResourceEvent(this.unit, this.abilityHarvest.getCarriedResourceType(), + this.abilityHarvest.getCarriedResourceAmount()); + this.abilityHarvest.setCarriedResources(null, 0); + if (nextTarget != null) { + return this.abilityHarvest.getBehaviorHarvest().reset(nextTarget); + } + return this.unit.pollNextOrderBehavior(this.simulation); + } + } + } + return this; + } + + @Override + public CBehavior accept(final CDestructable target) { + // TODO cut trees! + return this.unit.pollNextOrderBehavior(this.simulation); + } + + @Override + public CBehavior accept(final CItem target) { + return this.unit.pollNextOrderBehavior(this.simulation); + } + + @Override + protected boolean checkTargetStillValid(final CSimulation simulation) { + return this.target.visit(AbilityTargetStillAliveVisitor.INSTANCE); + } + + @Override + protected CBehavior updateOnInvalidTarget(final CSimulation simulation) { + final CUnit nearestDropoff = findNearestDropoffPoint(simulation); + if (nearestDropoff != null) { + this.target = nearestDropoff; + return this; + } + return this.unit.pollNextOrderBehavior(simulation); + } + + @Override + protected void resetBeforeMoving(final CSimulation simulation) { + + } + + private CUnit findNearestDropoffPoint(final CSimulation simulation) { + CUnit nearestDropoffPoint = null; + double nearestDropoffDistance = Float.MAX_VALUE; + for (final CUnit unit : simulation.getUnits()) { + if (unit.getPlayerIndex() == this.unit.getPlayerIndex()) { + if (unit.visit(AbilityTargetStillAliveVisitor.INSTANCE)) { + boolean acceptedUnit = false; + for (final CAbility ability : unit.getAbilities()) { + if (ability instanceof CAbilityReturnResources) { + final CAbilityReturnResources abilityReturnResources = (CAbilityReturnResources) ability; + if (abilityReturnResources.accepts(this.abilityHarvest.getCarriedResourceType())) { + acceptedUnit = true; + break; + } + } + } + if (acceptedUnit) { + // TODO maybe use distance squared, problem is that we're using this + // inefficient more complex distance function on unit + final double distance = unit.distanceSquaredNoCollision(this.unit); + if (distance < nearestDropoffDistance) { + nearestDropoffDistance = distance; + nearestDropoffPoint = unit; + } + } + } + } + } + return nearestDropoffPoint; + } + + private static CUnit findNearestMine(final CUnit worker, final CSimulation simulation) { + CUnit nearestMine = null; + double nearestMineDistance = Float.MAX_VALUE; + for (final CUnit unit : simulation.getUnits()) { + boolean acceptedUnit = false; + for (final CAbility ability : unit.getAbilities()) { + if (ability instanceof CAbilityGoldMine) { + acceptedUnit = true; + break; + } + } + if (acceptedUnit) { + // TODO maybe use distance squared, problem is that we're using this + // inefficient more complex distance function on unit + final double distance = unit.distanceSquaredNoCollision(worker); + if (distance < nearestMineDistance) { + nearestMineDistance = distance; + nearestMine = unit; + } + } + } + return nearestMine; + } + + public static CDestructable findNearestTree(final CUnit worker, final CAbilityHarvest abilityHarvest, + final CSimulation simulation, final CWidget toObject) { + CDestructable nearestMine = null; + double nearestMineDistance = Float.MAX_VALUE; + for (final CDestructable unit : simulation.getDestructables()) { + if (!unit.isDead() + && unit.canBeTargetedBy(simulation, worker, abilityHarvest.getTreeAttack().getTargetsAllowed())) { + // TODO maybe use distance squared, problem is that we're using this + // inefficient more complex distance function on unit + final double distance = unit.distanceSquaredNoCollision(toObject); + if (distance < nearestMineDistance) { + nearestMineDistance = distance; + nearestMine = unit; + } + } + } + return nearestMine; + } + + @Override + public void begin(final CSimulation game) { + + } + + @Override + public void end(final CSimulation game, final boolean interrupted) { + + } + + @Override + public void endMove(final CSimulation game, final boolean interrupted) { + + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/inventory/CBehaviorDropItem.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/inventory/CBehaviorDropItem.java new file mode 100644 index 0000000..a74bd3d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/inventory/CBehaviorDropItem.java @@ -0,0 +1,71 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.inventory; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.inventory.CAbilityInventory; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetStillAliveVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CAbstractRangedBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; + +public class CBehaviorDropItem extends CAbstractRangedBehavior { + private final CAbilityInventory inventory; + private CItem targetItem; + + public CBehaviorDropItem(final CUnit unit, final CAbilityInventory inventory) { + super(unit); + this.inventory = inventory; + } + + public CBehaviorDropItem reset(final CItem targetItem, final AbilityPointTarget targetPoint) { + innerReset(targetPoint); + this.targetItem = targetItem; + return this; + } + + @Override + public boolean isWithinRange(final CSimulation simulation) { + return this.unit.canReach(this.target, simulation.getGameplayConstants().getDropItemRange()); + } + + @Override + public void endMove(final CSimulation game, final boolean interrupted) { + } + + @Override + public void begin(final CSimulation game) { + } + + @Override + public void end(final CSimulation game, final boolean interrupted) { + } + + @Override + public int getHighlightOrderId() { + return OrderIds.dropitem; + } + + @Override + protected CBehavior update(final CSimulation simulation, final boolean withinRange) { + this.inventory.dropItem(simulation, this.unit, this.targetItem, this.target.getX(), this.target.getY(), true); + return this.unit.pollNextOrderBehavior(simulation); + } + + @Override + protected CBehavior updateOnInvalidTarget(final CSimulation simulation) { + return this.unit.pollNextOrderBehavior(simulation); + } + + @Override + protected boolean checkTargetStillValid(final CSimulation simulation) { + return this.target.visit(AbilityTargetStillAliveVisitor.INSTANCE); + } + + @Override + protected void resetBeforeMoving(final CSimulation simulation) { + + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/inventory/CBehaviorGetItem.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/inventory/CBehaviorGetItem.java new file mode 100644 index 0000000..6568c68 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/inventory/CBehaviorGetItem.java @@ -0,0 +1,70 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.inventory; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.inventory.CAbilityInventory; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetItemVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetStillAliveVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CAbstractRangedBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; + +public class CBehaviorGetItem extends CAbstractRangedBehavior { + private final CAbilityInventory inventory; + + public CBehaviorGetItem(final CUnit unit, final CAbilityInventory inventory) { + super(unit); + this.inventory = inventory; + } + + public CBehaviorGetItem reset(final CItem targetItem) { + innerReset(targetItem); + return this; + } + + @Override + public boolean isWithinRange(final CSimulation simulation) { + return this.unit.canReach(this.target, simulation.getGameplayConstants().getPickupItemRange()); + } + + @Override + public void endMove(final CSimulation game, final boolean interrupted) { + } + + @Override + public void begin(final CSimulation game) { + } + + @Override + public void end(final CSimulation game, final boolean interrupted) { + } + + @Override + public int getHighlightOrderId() { + return OrderIds.getitem; + } + + @Override + protected CBehavior update(final CSimulation simulation, final boolean withinRange) { + final CItem targetItem = this.target.visit(AbilityTargetItemVisitor.INSTANCE); + this.inventory.giveItem(simulation, this.unit, targetItem, true); + return this.unit.pollNextOrderBehavior(simulation); + } + + @Override + protected CBehavior updateOnInvalidTarget(final CSimulation simulation) { + return this.unit.pollNextOrderBehavior(simulation); + } + + @Override + protected boolean checkTargetStillValid(final CSimulation simulation) { + return this.target.visit(AbilityTargetStillAliveVisitor.INSTANCE); + } + + @Override + protected void resetBeforeMoving(final CSimulation simulation) { + + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/test/CBehaviorChannelTest.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/test/CBehaviorChannelTest.java new file mode 100644 index 0000000..d64ecd9 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/behaviors/test/CBehaviorChannelTest.java @@ -0,0 +1,49 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.test; + +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; + +public class CBehaviorChannelTest implements CBehavior { + private final CUnit unit; + private final float artDuration; + private int nextArtTick; + + public CBehaviorChannelTest(final CUnit unit, final float artDuration) { + this.unit = unit; + this.artDuration = artDuration; + } + + public CBehaviorChannelTest reset() { + this.nextArtTick = 0; + return this; + } + + @Override + public CBehavior update(final CSimulation game) { + this.unit.getUnitAnimationListener().playAnimation(false, null, SequenceUtils.SPELL, 1.0f, true); + final int gameTurnTick = game.getGameTurnTick(); + if (gameTurnTick >= this.nextArtTick) { + game.createEffectOnUnit(this.unit, "Abilities\\Spells\\Undead\\DeathPact\\DeathPactTarget.mdl"); + this.nextArtTick = gameTurnTick + (int) (this.artDuration / WarsmashConstants.SIMULATION_STEP_TIME); + } + return this; + } + + @Override + public void begin(final CSimulation game) { + } + + @Override + public void end(final CSimulation game, boolean interrupted) { + } + + @Override + public int getHighlightOrderId() { + return OrderIds.channel; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CAttackType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CAttackType.java new file mode 100644 index 0000000..cd540b8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CAttackType.java @@ -0,0 +1,46 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat; + +public enum CAttackType implements CodeKeyType { + UNKNOWN, + NORMAL, + PIERCE, + SIEGE, + SPELLS, + CHAOS, + MAGIC, + HERO; + + public static CAttackType[] VALUES = values(); + + private String codeKey; + private String damageKey; + + private CAttackType() { + final String name = name(); + final String computedCodeKey = name.charAt(0) + name.substring(1).toLowerCase(); + if (computedCodeKey.equals("Spells")) { + this.codeKey = "Magic"; + } + else { + this.codeKey = computedCodeKey; + } + this.damageKey = this.codeKey; + } + + @Override + public String getCodeKey() { + return this.codeKey; + } + + public String getDamageKey() { + return this.damageKey; + } + + public static CAttackType parseAttackType(final String attackTypeString) { + final String upperCaseAttackType = attackTypeString.toUpperCase(); + if ("SEIGE".equals(upperCaseAttackType)) { + return SIEGE; + } + return valueOf(upperCaseAttackType); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CDefenseType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CDefenseType.java new file mode 100644 index 0000000..1360513 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CDefenseType.java @@ -0,0 +1,33 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat; + +public enum CDefenseType implements CodeKeyType { + NONE, + NORMAL, + SMALL, + MEDIUM, + LARGE, + FORT, + HERO, + DIVINE; + + public static CDefenseType[] VALUES = values(); + + private String codeKey; + + private CDefenseType() { + this.codeKey = name().charAt(0) + name().substring(1).toLowerCase(); + } + + @Override + public String getCodeKey() { + return this.codeKey; + } + + public static CDefenseType parseDefenseType(final String typeString) { + final String upperCaseTypeString = typeString.toUpperCase(); + if (upperCaseTypeString.equals("HEAVY")) { + return LARGE; + } + return valueOf(upperCaseTypeString); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CRegenType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CRegenType.java new file mode 100644 index 0000000..b303272 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CRegenType.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat; + +public enum CRegenType { + NONE, + ALWAYS, + BLIGHT, + DAY, + NIGHT; + + public static CRegenType parseRegenType(final String typeString) { + return valueOf(typeString.toUpperCase()); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CTargetType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CTargetType.java new file mode 100644 index 0000000..5fb484a --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CTargetType.java @@ -0,0 +1,139 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat; + +import java.util.EnumSet; + +public enum CTargetType { + AIR, + ALIVE, + ALLIES, + DEAD, + DEBRIS, + ENEMIES, + GROUND, + HERO, + INVULNERABLE, + ITEM, + MECHANICAL, + NEUTRAL, + NONE, + NONHERO, + NONSAPPER, + NOTSELF, + ORGANIC, + PLAYERUNITS, + SAPPER, + SELF, + STRUCTURE, + TERRAIN, + TREE, + VULNERABLE, + WALL, + WARD, + ANCIENT, + NONANCIENT, + FRIEND, + BRIDGE, + DECORATION; + + public static CTargetType parseTargetType(final String targetTypeString) { + if (targetTypeString == null) { + return null; + } + switch (targetTypeString.toLowerCase()) { + case "air": + return AIR; + case "alive": + case "aliv": + return ALIVE; + case "allies": + case "alli": + case "ally": + return ALLIES; + case "dead": + return DEAD; + case "debris": + case "debr": + return DEBRIS; + case "enemies": + case "enem": + case "enemy": + return ENEMIES; + case "ground": + case "grou": + return GROUND; + case "hero": + return HERO; + case "invulnerable": + case "invu": + return INVULNERABLE; + case "item": + return ITEM; + case "mechanical": + case "mech": + return MECHANICAL; + case "neutral": + case "neut": + return NEUTRAL; + case "none": + return NONE; + case "nonhero": + case "nonh": + return NONHERO; + case "nonsapper": + return NONSAPPER; + case "notself": + case "nots": + return NOTSELF; + case "organic": + case "orga": + return ORGANIC; + case "player": + case "play": + return PLAYERUNITS; + case "sapper": + return SAPPER; + case "self": + return SELF; + case "structure": + case "stru": + return STRUCTURE; + case "terrain": + case "terr": + return TERRAIN; + case "tree": + return TREE; + case "vulnerable": + case "vuln": + return VULNERABLE; + case "wall": + return WALL; + case "ward": + return WARD; + case "ancient": + return ANCIENT; + case "nonancient": + return NONANCIENT; + case "friend": + case "frie": + return FRIEND; + case "bridge": + return BRIDGE; + case "decoration": + case "deco": + return DECORATION; + default: + return null; + } + } + + public static EnumSet parseTargetTypeSet(final String targetTypeString) { + final EnumSet types = EnumSet.noneOf(CTargetType.class); + for (final String type : targetTypeString.split(",")) { + final CTargetType parsedType = parseTargetType(type); + if (parsedType != null) { + types.add(parsedType); + } + } + return types; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CUpgradeClass.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CUpgradeClass.java new file mode 100644 index 0000000..3112bf6 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CUpgradeClass.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat; + +public enum CUpgradeClass { + ARMOR, + ARTILLERY, + MELEE, + RANGED, + CASTER; + + public static CUpgradeClass parseUpgradeClass(final String upgradeClassString) { + return valueOf(upgradeClassString.toUpperCase()); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CWeaponType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CWeaponType.java new file mode 100644 index 0000000..b47162f --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CWeaponType.java @@ -0,0 +1,20 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat; + +public enum CWeaponType { + NORMAL, + INSTANT, + ARTILLERY, + ALINE, + MISSILE, + MSPLASH, + MBOUNCE, + MLINE; + + public static CWeaponType parseWeaponType(final String weaponTypeString) { + return valueOf(weaponTypeString.toUpperCase()); + } + + public boolean isAttackGroundSupported() { + return (this == CWeaponType.ARTILLERY) || (this == CWeaponType.ALINE); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CodeKeyType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CodeKeyType.java new file mode 100644 index 0000000..d727a02 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/CodeKeyType.java @@ -0,0 +1,7 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat; + +public interface CodeKeyType { + String getCodeKey(); + + int ordinal(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttack.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttack.java new file mode 100644 index 0000000..0b885f5 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttack.java @@ -0,0 +1,271 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks; + +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; + +/** + * The base class for unit-data-based combat attacks. + * + * I really wanted to split this out into sub classes based on weapon type, but + * I came to realize the Ballista in RoC probably had the spill distance effect + * & area of effect both after it upgrades Impaling Bolt, and this would point + * out that the behaviors were not mutually exclusive. + * + * Then I reviewed it and decided that in RoC, the Impaling Bolts upgrade did + * not interact with the damage spill combat settings from the UnitWeapons.slk, + * because many of those settings did not exist. So I will attempt to emulate + * these attacks as best as possible. + */ +public abstract class CUnitAttack { + private float animationBackswingPoint; + private float animationDamagePoint; + private CAttackType attackType; + private float cooldownTime; + private int damageBase; + private int damageDice; + private int damageSidesPerDie; + private int damageUpgradeAmount; + private int range; + private float rangeMotionBuffer; + private boolean showUI; + private EnumSet targetsAllowed; + private String weaponSound; + private CWeaponType weaponType; + + // calculate + private int minDamage; + private int maxDamage; + private int minDamageDisplay; + private int maxDamageDisplay; + + private int primaryAttributeDamageBonus; + private int permanentDamageBonus; + private int temporaryDamageBonus; + + public CUnitAttack(final float animationBackswingPoint, final float animationDamagePoint, + final CAttackType attackType, final float cooldownTime, final int damageBase, final int damageDice, + final int damageSidesPerDie, final int damageUpgradeAmount, final int range, final float rangeMotionBuffer, + final boolean showUI, final EnumSet targetsAllowed, final String weaponSound, + final CWeaponType weaponType) { + this.animationBackswingPoint = animationBackswingPoint; + this.animationDamagePoint = animationDamagePoint; + this.attackType = attackType; + this.cooldownTime = cooldownTime; + this.damageBase = damageBase; + this.damageDice = damageDice; + this.damageSidesPerDie = damageSidesPerDie; + this.damageUpgradeAmount = damageUpgradeAmount; + this.range = range; + this.rangeMotionBuffer = rangeMotionBuffer; + this.showUI = showUI; + this.targetsAllowed = targetsAllowed; + this.weaponSound = weaponSound; + this.weaponType = weaponType; + computeDerivedFields(); + } + + public CUnitAttack(final CUnitAttack other) { + this.animationBackswingPoint = other.animationBackswingPoint; + this.animationDamagePoint = other.animationDamagePoint; + this.attackType = other.attackType; + this.cooldownTime = other.cooldownTime; + this.damageBase = other.damageBase; + this.damageDice = other.damageDice; + this.damageSidesPerDie = other.damageSidesPerDie; + this.damageUpgradeAmount = other.damageUpgradeAmount; + this.range = other.range; + this.rangeMotionBuffer = other.rangeMotionBuffer; + this.showUI = other.showUI; + this.targetsAllowed = other.targetsAllowed; + this.weaponSound = other.weaponSound; + this.weaponType = other.weaponType; + + this.primaryAttributeDamageBonus = other.primaryAttributeDamageBonus; + this.permanentDamageBonus = other.permanentDamageBonus; + this.temporaryDamageBonus = other.temporaryDamageBonus; + computeDerivedFields(); + } + + public abstract CUnitAttack copy(); + + private void computeDerivedFields() { + final int baseDamage = this.damageBase + this.primaryAttributeDamageBonus + this.permanentDamageBonus; + this.minDamageDisplay = baseDamage + this.damageDice; + this.maxDamageDisplay = baseDamage + (this.damageDice * this.damageSidesPerDie); + if (this.minDamageDisplay < 0) { + this.minDamageDisplay = 0; + } + if (this.maxDamageDisplay < 0) { + this.maxDamageDisplay = 0; + } + this.minDamage = this.minDamageDisplay + this.temporaryDamageBonus; + this.maxDamage = this.maxDamageDisplay + this.temporaryDamageBonus; + } + + public float getAnimationBackswingPoint() { + return this.animationBackswingPoint; + } + + public float getAnimationDamagePoint() { + return this.animationDamagePoint; + } + + public CAttackType getAttackType() { + return this.attackType; + } + + public float getCooldownTime() { + return this.cooldownTime; + } + + public int getDamageBase() { + return this.damageBase; + } + + public int getDamageDice() { + return this.damageDice; + } + + public int getDamageSidesPerDie() { + return this.damageSidesPerDie; + } + + public int getDamageUpgradeAmount() { + return this.damageUpgradeAmount; + } + + public int getRange() { + return this.range; + } + + public float getRangeMotionBuffer() { + return this.rangeMotionBuffer; + } + + public boolean isShowUI() { + return this.showUI; + } + + public EnumSet getTargetsAllowed() { + return this.targetsAllowed; + } + + public String getWeaponSound() { + return this.weaponSound; + } + + public CWeaponType getWeaponType() { + return this.weaponType; + } + + public void setAnimationBackswingPoint(final float animationBackswingPoint) { + this.animationBackswingPoint = animationBackswingPoint; + } + + public void setAnimationDamagePoint(final float animationDamagePoint) { + this.animationDamagePoint = animationDamagePoint; + } + + public void setAttackType(final CAttackType attackType) { + this.attackType = attackType; + } + + public void setCooldownTime(final float cooldownTime) { + this.cooldownTime = cooldownTime; + } + + public void setDamageBase(final int damageBase) { + this.damageBase = damageBase; + computeDerivedFields(); + } + + public void setDamageDice(final int damageDice) { + this.damageDice = damageDice; + computeDerivedFields(); + } + + public void setDamageSidesPerDie(final int damageSidesPerDie) { + this.damageSidesPerDie = damageSidesPerDie; + computeDerivedFields(); + } + + public void setDamageUpgradeAmount(final int damageUpgradeAmount) { + this.damageUpgradeAmount = damageUpgradeAmount; + } + + public void setRange(final int range) { + this.range = range; + } + + public void setRangeMotionBuffer(final float rangeMotionBuffer) { + this.rangeMotionBuffer = rangeMotionBuffer; + } + + public void setShowUI(final boolean showUI) { + this.showUI = showUI; + } + + public void setTargetsAllowed(final EnumSet targetsAllowed) { + this.targetsAllowed = targetsAllowed; + } + + public void setWeaponSound(final String weaponSound) { + this.weaponSound = weaponSound; + } + + public void setWeaponType(final CWeaponType weaponType) { + this.weaponType = weaponType; + } + + public int getMinDamage() { + return this.minDamage; + } + + public int getMaxDamage() { + return this.maxDamage; + } + + public int getMinDamageDisplay() { + return this.minDamageDisplay; + } + + public int getMaxDamageDisplay() { + return this.maxDamageDisplay; + } + + public void setPrimaryAttributeDamageBonus(final int primaryAttributeDamageBonus) { + this.primaryAttributeDamageBonus = primaryAttributeDamageBonus; + computeDerivedFields(); + } + + public void setPermanentDamageBonus(final int permanentDamageBonus) { + this.permanentDamageBonus = permanentDamageBonus; + computeDerivedFields(); + } + + public void setTemporaryDamageBonus(final int temporaryDamageBonus) { + this.temporaryDamageBonus = temporaryDamageBonus; + computeDerivedFields(); + } + + public int getPrimaryAttributeDamageBonus() { + return this.primaryAttributeDamageBonus; + } + + public int getPermanentDamageBonus() { + return this.permanentDamageBonus; + } + + public int getTemporaryDamageBonus() { + return this.temporaryDamageBonus; + } + + public abstract void launch(CSimulation simulation, CUnit unit, AbilityTarget target, float damage, + CUnitAttackListener attackListener); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackInstant.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackInstant.java new file mode 100644 index 0000000..33bbec4 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackInstant.java @@ -0,0 +1,56 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks; + +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetWidgetVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; + +public class CUnitAttackInstant extends CUnitAttack { + private String projectileArt; + + public CUnitAttackInstant(final float animationBackswingPoint, final float animationDamagePoint, + final CAttackType attackType, final float cooldownTime, final int damageBase, final int damageDice, + final int damageSidesPerDie, final int damageUpgradeAmount, final int range, final float rangeMotionBuffer, + final boolean showUI, final EnumSet targetsAllowed, final String weaponSound, + final CWeaponType weaponType, final String projectileArt) { + super(animationBackswingPoint, animationDamagePoint, attackType, cooldownTime, damageBase, damageDice, + damageSidesPerDie, damageUpgradeAmount, range, rangeMotionBuffer, showUI, targetsAllowed, weaponSound, + weaponType); + this.projectileArt = projectileArt; + } + + @Override + public CUnitAttack copy() { + return new CUnitAttackInstant(getAnimationBackswingPoint(), getAnimationDamagePoint(), getAttackType(), + getCooldownTime(), getDamageBase(), getDamageDice(), getDamageSidesPerDie(), getDamageUpgradeAmount(), + getRange(), getRangeMotionBuffer(), isShowUI(), getTargetsAllowed(), getWeaponSound(), getWeaponType(), + this.projectileArt); + } + + public String getProjectileArt() { + return this.projectileArt; + } + + public void setProjectileArt(final String projectileArt) { + this.projectileArt = projectileArt; + } + + @Override + public void launch(final CSimulation simulation, final CUnit unit, final AbilityTarget target, final float damage, + final CUnitAttackListener attackListener) { + attackListener.onLaunch(); + final CWidget widget = target.visit(AbilityTargetWidgetVisitor.INSTANCE); + if (widget != null) { + simulation.createInstantAttackEffect(unit, this, widget); + widget.damage(simulation, unit, getAttackType(), getWeaponSound(), damage); + attackListener.onHit(target, damage); + } + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackListener.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackListener.java new file mode 100644 index 0000000..e833fc7 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackListener.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; + +public interface CUnitAttackListener { + void onLaunch(); + + void onHit(AbilityTarget target, float damage); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissile.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissile.java new file mode 100644 index 0000000..f3017d8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissile.java @@ -0,0 +1,92 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks; + +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetWidgetVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; + +public class CUnitAttackMissile extends CUnitAttack { + private float projectileArc; + private String projectileArt; + private boolean projectileHomingEnabled; + private int projectileSpeed; + + public CUnitAttackMissile(final float animationBackswingPoint, final float animationDamagePoint, + final CAttackType attackType, final float cooldownTime, final int damageBase, final int damageDice, + final int damageSidesPerDie, final int damageUpgradeAmount, final int range, final float rangeMotionBuffer, + final boolean showUI, final EnumSet targetsAllowed, final String weaponSound, + final CWeaponType weaponType, final float projectileArc, final String projectileArt, + final boolean projectileHomingEnabled, final int projectileSpeed) { + super(animationBackswingPoint, animationDamagePoint, attackType, cooldownTime, damageBase, damageDice, + damageSidesPerDie, damageUpgradeAmount, range, rangeMotionBuffer, showUI, targetsAllowed, weaponSound, + weaponType); + this.projectileArc = projectileArc; + this.projectileArt = projectileArt; + this.projectileHomingEnabled = projectileHomingEnabled; + this.projectileSpeed = projectileSpeed; + } + + @Override + public CUnitAttack copy() { + return new CUnitAttackMissile(this.getAnimationBackswingPoint(), getAnimationDamagePoint(), getAttackType(), + getCooldownTime(), getDamageBase(), getDamageDice(), getDamageSidesPerDie(), getDamageUpgradeAmount(), + getRange(), getRangeMotionBuffer(), isShowUI(), getTargetsAllowed(), getWeaponSound(), getWeaponType(), + this.projectileArc, this.projectileArt, this.projectileHomingEnabled, this.projectileSpeed); + } + + public float getProjectileArc() { + return this.projectileArc; + } + + public String getProjectileArt() { + return this.projectileArt; + } + + public boolean isProjectileHomingEnabled() { + return this.projectileHomingEnabled; + } + + public int getProjectileSpeed() { + return this.projectileSpeed; + } + + public void setProjectileArc(final float projectileArc) { + this.projectileArc = projectileArc; + } + + public void setProjectileArt(final String projectileArt) { + this.projectileArt = projectileArt; + } + + public void setProjectileHomingEnabled(final boolean projectileHomingEnabled) { + this.projectileHomingEnabled = projectileHomingEnabled; + } + + public void setProjectileSpeed(final int projectileSpeed) { + this.projectileSpeed = projectileSpeed; + } + + @Override + public void launch(final CSimulation simulation, final CUnit unit, final AbilityTarget target, final float damage, + final CUnitAttackListener attackListener) { + attackListener.onLaunch(); + simulation.createProjectile(unit, unit.getX(), unit.getY(), (float) Math.toRadians(unit.getFacing()), this, + target, damage, 0, attackListener); + } + + public void doDamage(final CSimulation cSimulation, final CUnit source, final AbilityTarget target, + final float damage, final float x, final float y, final int bounceIndex, + final CUnitAttackListener attackListener) { + final CWidget widget = target.visit(AbilityTargetWidgetVisitor.INSTANCE); + if (widget != null) { + widget.damage(cSimulation, source, getAttackType(), getWeaponSound(), damage); + attackListener.onHit(target, damage); + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileBounce.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileBounce.java new file mode 100644 index 0000000..b56f99b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileBounce.java @@ -0,0 +1,135 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks; + +import java.util.EnumSet; + +import com.badlogic.gdx.math.Rectangle; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitEnumFunction; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetWidgetVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; + +public class CUnitAttackMissileBounce extends CUnitAttackMissile { + private float damageLossFactor; + private int maximumNumberOfTargets; + private final int areaOfEffectFullDamage; + private final EnumSet areaOfEffectTargets; + + public CUnitAttackMissileBounce(final float animationBackswingPoint, final float animationDamagePoint, + final CAttackType attackType, final float cooldownTime, final int damageBase, final int damageDice, + final int damageSidesPerDie, final int damageUpgradeAmount, final int range, final float rangeMotionBuffer, + final boolean showUI, final EnumSet targetsAllowed, final String weaponSound, + final CWeaponType weaponType, final float projectileArc, final String projectileArt, + final boolean projectileHomingEnabled, final int projectileSpeed, final float damageLossFactor, + final int maximumNumberOfTargets, final int areaOfEffectFullDamage, + final EnumSet areaOfEffectTargets) { + super(animationBackswingPoint, animationDamagePoint, attackType, cooldownTime, damageBase, damageDice, + damageSidesPerDie, damageUpgradeAmount, range, rangeMotionBuffer, showUI, targetsAllowed, weaponSound, + weaponType, projectileArc, projectileArt, projectileHomingEnabled, projectileSpeed); + this.damageLossFactor = damageLossFactor; + this.maximumNumberOfTargets = maximumNumberOfTargets; + this.areaOfEffectFullDamage = areaOfEffectFullDamage; + this.areaOfEffectTargets = areaOfEffectTargets; + } + + @Override + public CUnitAttack copy() { + return new CUnitAttackMissileBounce(getAnimationBackswingPoint(), getAnimationDamagePoint(), getAttackType(), + getCooldownTime(), getDamageBase(), getDamageDice(), getDamageSidesPerDie(), getDamageUpgradeAmount(), + getRange(), getRangeMotionBuffer(), isShowUI(), getTargetsAllowed(), getWeaponSound(), getWeaponType(), + getProjectileArc(), getProjectileArt(), isProjectileHomingEnabled(), getProjectileSpeed(), + this.damageLossFactor, this.maximumNumberOfTargets, this.areaOfEffectFullDamage, + this.areaOfEffectTargets); + } + + public float getDamageLossFactor() { + return this.damageLossFactor; + } + + public int getMaximumNumberOfTargets() { + return this.maximumNumberOfTargets; + } + + public void setDamageLossFactor(final float damageLossFactor) { + this.damageLossFactor = damageLossFactor; + } + + public void setMaximumNumberOfTargets(final int maximumNumberOfTargets) { + this.maximumNumberOfTargets = maximumNumberOfTargets; + } + + @Override + public void doDamage(final CSimulation cSimulation, final CUnit source, final AbilityTarget target, + final float damage, final float x, final float y, final int bounceIndex, + final CUnitAttackListener attackListener) { + super.doDamage(cSimulation, source, target, damage, x, y, bounceIndex, attackListener); + final CWidget widget = target.visit(AbilityTargetWidgetVisitor.INSTANCE); + if (widget != null) { + final int nextBounceIndex = bounceIndex + 1; + if (nextBounceIndex != this.maximumNumberOfTargets) { + BounceMissileConsumer.INSTANCE.nextBounce(cSimulation, source, widget, this, x, y, damage, + nextBounceIndex, attackListener); + } + } + } + + private static final class BounceMissileConsumer implements CUnitEnumFunction { + private static final BounceMissileConsumer INSTANCE = new BounceMissileConsumer(); + private final Rectangle rect = new Rectangle(); + private CUnitAttackMissileBounce attack; + private CSimulation simulation; + private CUnit source; + private CWidget target; + private float x; + private float y; + private float damage; + private int bounceIndex; + private CUnitAttackListener attackListener; + private boolean launched = false; + + public void nextBounce(final CSimulation simulation, final CUnit source, final CWidget target, + final CUnitAttackMissileBounce attack, final float x, final float y, final float damage, + final int bounceIndex, final CUnitAttackListener attackListener) { + this.simulation = simulation; + this.source = source; + this.target = target; + this.attack = attack; + this.x = x; + this.y = y; + this.damage = damage; + this.bounceIndex = bounceIndex; + this.attackListener = attackListener; + this.launched = false; + final float doubleMaxArea = attack.areaOfEffectFullDamage + + (this.simulation.getGameplayConstants().getCloseEnoughRange() * 2); + final float maxArea = doubleMaxArea / 2; + this.rect.set(x - maxArea, y - maxArea, doubleMaxArea, doubleMaxArea); + simulation.getWorldCollision().enumUnitsInRect(this.rect, this); + + } + + @Override + public boolean call(final CUnit enumUnit) { + if (enumUnit == this.target) { + return false; + } + if (enumUnit.canBeTargetedBy(this.simulation, this.source, this.attack.areaOfEffectTargets)) { + if (this.launched) { + throw new IllegalStateException("already launched"); + } + final float dx = enumUnit.getX() - this.x; + final float dy = enumUnit.getY() - this.y; + final float angle = (float) Math.atan2(dy, dx); + this.simulation.createProjectile(this.source, this.x, this.y, angle, this.attack, enumUnit, + this.damage * (1.0f - this.attack.damageLossFactor), this.bounceIndex, this.attackListener); + this.launched = true; + return true; + } + return false; + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileLine.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileLine.java new file mode 100644 index 0000000..bd95e8a --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileLine.java @@ -0,0 +1,52 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks; + +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; + +public class CUnitAttackMissileLine extends CUnitAttackMissile { + private float damageSpillDistance; + private float damageSpillRadius; + + public CUnitAttackMissileLine(final float animationBackswingPoint, final float animationDamagePoint, + final CAttackType attackType, final float cooldownTime, final int damageBase, final int damageDice, + final int damageSidesPerDie, final int damageUpgradeAmount, final int range, final float rangeMotionBuffer, + final boolean showUI, final EnumSet targetsAllowed, final String weaponSound, + final CWeaponType weaponType, final float projectileArc, final String projectileArt, + final boolean projectileHomingEnabled, final int projectileSpeed, final float damageSpillDistance, + final float damageSpillRadius) { + super(animationBackswingPoint, animationDamagePoint, attackType, cooldownTime, damageBase, damageDice, + damageSidesPerDie, damageUpgradeAmount, range, rangeMotionBuffer, showUI, targetsAllowed, weaponSound, + weaponType, projectileArc, projectileArt, projectileHomingEnabled, projectileSpeed); + this.damageSpillDistance = damageSpillDistance; + this.damageSpillRadius = damageSpillRadius; + } + + @Override + public CUnitAttack copy() { + return new CUnitAttackMissileLine(getAnimationBackswingPoint(), getAnimationDamagePoint(), getAttackType(), + getCooldownTime(), getDamageBase(), getDamageDice(), getDamageSidesPerDie(), getDamageUpgradeAmount(), + getRange(), getRangeMotionBuffer(), isShowUI(), getTargetsAllowed(), getWeaponSound(), getWeaponType(), + getProjectileArc(), getProjectileArt(), isProjectileHomingEnabled(), getProjectileSpeed(), + this.damageSpillDistance, this.damageSpillRadius); + } + + public float getDamageSpillDistance() { + return this.damageSpillDistance; + } + + public float getDamageSpillRadius() { + return this.damageSpillRadius; + } + + public void setDamageSpillDistance(final float damageSpillDistance) { + this.damageSpillDistance = damageSpillDistance; + } + + public void setDamageSpillRadius(final float damageSpillRadius) { + this.damageSpillRadius = damageSpillRadius; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileSplash.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileSplash.java new file mode 100644 index 0000000..525efaf --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackMissileSplash.java @@ -0,0 +1,173 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks; + +import java.util.EnumSet; + +import com.badlogic.gdx.math.Rectangle; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitEnumFunction; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; + +public class CUnitAttackMissileSplash extends CUnitAttackMissile { + private int areaOfEffectFullDamage; + private int areaOfEffectMediumDamage; + private int areaOfEffectSmallDamage; + private EnumSet areaOfEffectTargets; + private float damageFactorMedium; + private float damageFactorSmall; + + public CUnitAttackMissileSplash(final float animationBackswingPoint, final float animationDamagePoint, + final CAttackType attackType, final float cooldownTime, final int damageBase, final int damageDice, + final int damageSidesPerDie, final int damageUpgradeAmount, final int range, final float rangeMotionBuffer, + final boolean showUI, final EnumSet targetsAllowed, final String weaponSound, + final CWeaponType weaponType, final float projectileArc, final String projectileArt, + final boolean projectileHomingEnabled, final int projectileSpeed, final int areaOfEffectFullDamage, + final int areaOfEffectMediumDamage, final int areaOfEffectSmallDamage, + final EnumSet areaOfEffectTargets, final float damageFactorMedium, + final float damageFactorSmall) { + super(animationBackswingPoint, animationDamagePoint, attackType, cooldownTime, damageBase, damageDice, + damageSidesPerDie, damageUpgradeAmount, range, rangeMotionBuffer, showUI, targetsAllowed, weaponSound, + weaponType, projectileArc, projectileArt, projectileHomingEnabled, projectileSpeed); + this.areaOfEffectFullDamage = areaOfEffectFullDamage; + this.areaOfEffectMediumDamage = areaOfEffectMediumDamage; + this.areaOfEffectSmallDamage = areaOfEffectSmallDamage; + this.areaOfEffectTargets = areaOfEffectTargets; + this.damageFactorMedium = damageFactorMedium; + this.damageFactorSmall = damageFactorSmall; + } + + @Override + public CUnitAttack copy() { + return new CUnitAttackMissileSplash(getAnimationBackswingPoint(), getAnimationDamagePoint(), getAttackType(), + getCooldownTime(), getDamageBase(), getDamageDice(), getDamageSidesPerDie(), getDamageUpgradeAmount(), + getRange(), getRangeMotionBuffer(), isShowUI(), getTargetsAllowed(), getWeaponSound(), getWeaponType(), + getProjectileArc(), getProjectileArt(), isProjectileHomingEnabled(), getProjectileSpeed(), + this.areaOfEffectFullDamage, this.areaOfEffectMediumDamage, this.areaOfEffectSmallDamage, + this.areaOfEffectTargets, this.damageFactorMedium, this.damageFactorSmall); + } + + @Override + public int getRange() { + return super.getRange(); + } + + public int getAreaOfEffectFullDamage() { + return this.areaOfEffectFullDamage; + } + + public int getAreaOfEffectMediumDamage() { + return this.areaOfEffectMediumDamage; + } + + public int getAreaOfEffectSmallDamage() { + return this.areaOfEffectSmallDamage; + } + + public EnumSet getAreaOfEffectTargets() { + return this.areaOfEffectTargets; + } + + public float getDamageFactorMedium() { + return this.damageFactorMedium; + } + + public float getDamageFactorSmall() { + return this.damageFactorSmall; + } + + public void setAreaOfEffectFullDamage(final int areaOfEffectFullDamage) { + this.areaOfEffectFullDamage = areaOfEffectFullDamage; + } + + public void setAreaOfEffectMediumDamage(final int areaOfEffectMediumDamage) { + this.areaOfEffectMediumDamage = areaOfEffectMediumDamage; + } + + public void setAreaOfEffectSmallDamage(final int areaOfEffectSmallDamage) { + this.areaOfEffectSmallDamage = areaOfEffectSmallDamage; + } + + public void setAreaOfEffectTargets(final EnumSet areaOfEffectTargets) { + this.areaOfEffectTargets = areaOfEffectTargets; + } + + public void setDamageFactorMedium(final float damageFactorMedium) { + this.damageFactorMedium = damageFactorMedium; + } + + public void setDamageFactorSmall(final float damageFactorSmall) { + this.damageFactorSmall = damageFactorSmall; + } + + @Override + public void doDamage(final CSimulation cSimulation, final CUnit source, final AbilityTarget target, + final float damage, final float x, final float y, final int bounceIndex, + final CUnitAttackListener attackListener) { + SplashDamageConsumer.INSTANCE.doDamage(cSimulation, source, target, this, x, y, damage, attackListener); + if ((getWeaponType() != CWeaponType.ARTILLERY) && !SplashDamageConsumer.INSTANCE.hitTarget) { + super.doDamage(cSimulation, source, target, damage * this.damageFactorSmall, x, y, bounceIndex, + attackListener); + } + } + + private static final class SplashDamageConsumer implements CUnitEnumFunction { + private static final SplashDamageConsumer INSTANCE = new SplashDamageConsumer(); + private final Rectangle rect = new Rectangle(); + private CUnitAttackMissileSplash attack; + private CSimulation simulation; + private CUnit source; + private AbilityTarget target; + private float x; + private float y; + private float damage; + private CUnitAttackListener attackListener; + private boolean hitTarget; + + public void doDamage(final CSimulation simulation, final CUnit source, final AbilityTarget target, + final CUnitAttackMissileSplash attack, final float x, final float y, final float damage, + final CUnitAttackListener attackListener) { + this.simulation = simulation; + this.source = source; + this.target = target; + this.attack = attack; + this.x = x; + this.y = y; + this.damage = damage; + this.attackListener = attackListener; + this.hitTarget = false; + final float doubleMaxArea = (attack.areaOfEffectSmallDamage) * 2; + final float maxArea = doubleMaxArea / 2; + this.rect.set(x - maxArea, y - maxArea, doubleMaxArea, doubleMaxArea); + simulation.getWorldCollision().enumUnitsInRect(this.rect, this); + } + + @Override + public boolean call(final CUnit enumUnit) { + if (enumUnit.canBeTargetedBy(this.simulation, this.source, this.attack.areaOfEffectTargets)) { + final double distance = enumUnit.distance(this.x, this.y); + if (distance <= (this.attack.areaOfEffectFullDamage)) { + enumUnit.damage(this.simulation, this.source, this.attack.getAttackType(), + this.attack.getWeaponSound(), this.damage); + this.attackListener.onHit(enumUnit, this.damage); + } + else if (distance <= (this.attack.areaOfEffectMediumDamage)) { + enumUnit.damage(this.simulation, this.source, this.attack.getAttackType(), + this.attack.getWeaponSound(), this.damage * this.attack.damageFactorMedium); + this.attackListener.onHit(enumUnit, this.damage); + } + else if (distance <= (this.attack.areaOfEffectSmallDamage)) { + enumUnit.damage(this.simulation, this.source, this.attack.getAttackType(), + this.attack.getWeaponSound(), this.damage * this.attack.damageFactorSmall); + this.attackListener.onHit(enumUnit, this.damage); + } + if (enumUnit == this.target) { + this.hitTarget = true; + } + } + return false; + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackNormal.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackNormal.java new file mode 100644 index 0000000..301070c --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/attacks/CUnitAttackNormal.java @@ -0,0 +1,44 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks; + +import java.util.EnumSet; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetWidgetVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; + +public class CUnitAttackNormal extends CUnitAttack { + + public CUnitAttackNormal(final float animationBackswingPoint, final float animationDamagePoint, + final CAttackType attackType, final float cooldownTime, final int damageBase, final int damageDice, + final int damageSidesPerDie, final int damageUpgradeAmount, final int range, final float rangeMotionBuffer, + final boolean showUI, final EnumSet targetsAllowed, final String weaponSound, + final CWeaponType weaponType) { + super(animationBackswingPoint, animationDamagePoint, attackType, cooldownTime, damageBase, damageDice, + damageSidesPerDie, damageUpgradeAmount, range, rangeMotionBuffer, showUI, targetsAllowed, weaponSound, + weaponType); + } + + @Override + public CUnitAttack copy() { + return new CUnitAttackNormal(getAnimationBackswingPoint(), getAnimationDamagePoint(), getAttackType(), + getCooldownTime(), getDamageBase(), getDamageDice(), getDamageSidesPerDie(), getDamageUpgradeAmount(), + getRange(), getRangeMotionBuffer(), isShowUI(), getTargetsAllowed(), getWeaponSound(), getWeaponType()); + } + + @Override + public void launch(final CSimulation simulation, final CUnit unit, final AbilityTarget target, final float damage, + final CUnitAttackListener attackListener) { + attackListener.onLaunch(); + final CWidget widget = target.visit(AbilityTargetWidgetVisitor.INSTANCE); + if (widget != null) { + widget.damage(simulation, unit, getAttackType(), getWeaponSound(), damage); + attackListener.onHit(target, damage); + } + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/projectile/CAttackProjectile.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/projectile/CAttackProjectile.java new file mode 100644 index 0000000..58013db --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/combat/projectile/CAttackProjectile.java @@ -0,0 +1,114 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.projectile; + +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackListener; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackMissile; + +public class CAttackProjectile { + private float x; + private float y; + private final float initialTargetX; + private final float initialTargetY; + private final float speed; + private final AbilityTarget target; + private boolean done; + private final CUnit source; + private final float damage; + private final CUnitAttackMissile unitAttack; + private final int bounceIndex; + private final CUnitAttackListener attackListener; + + public CAttackProjectile(final float x, final float y, final float speed, final AbilityTarget target, + final CUnit source, final float damage, final CUnitAttackMissile unitAttack, final int bounceIndex, + final CUnitAttackListener attackListener) { + this.x = x; + this.y = y; + this.speed = speed; + this.target = target; + this.source = source; + this.damage = damage; + this.unitAttack = unitAttack; + this.bounceIndex = bounceIndex; + this.attackListener = attackListener; + this.initialTargetX = target.getX(); + this.initialTargetY = target.getY(); + } + + public boolean update(final CSimulation cSimulation) { + final float tx = getTargetX(); + final float ty = getTargetY(); + final float sx = this.x; + final float sy = this.y; + final float dtsx = tx - sx; + final float dtsy = ty - sy; + final float c = (float) Math.sqrt((dtsx * dtsx) + (dtsy * dtsy)); + + final float d1x = dtsx / c; + final float d1y = dtsy / c; + + float travelDistance = Math.min(c, this.speed * WarsmashConstants.SIMULATION_STEP_TIME); + final boolean done = c <= travelDistance; + if (done) { + travelDistance = c; + } + + final float dx = d1x * travelDistance; + final float dy = d1y * travelDistance; + + this.x = this.x + dx; + this.y = this.y + dy; + + if (done && !this.done) { + this.unitAttack.doDamage(cSimulation, this.source, this.target, this.damage, this.x, this.y, + this.bounceIndex, this.attackListener); + this.done = true; + } + return this.done; + } + + public float getX() { + return this.x; + } + + public float getY() { + return this.y; + } + + public float getSpeed() { + return this.speed; + } + + public AbilityTarget getTarget() { + return this.target; + } + + public boolean isDone() { + return this.done; + } + + public CUnitAttackMissile getUnitAttack() { + return this.unitAttack; + } + + public float getTargetX() { + if (this.unitAttack.isProjectileHomingEnabled() && (this.unitAttack.getWeaponType() != CWeaponType.ARTILLERY)) { + return this.target.getX(); + } + else { + return this.initialTargetX; + } + } + + public float getTargetY() { + if (this.unitAttack.isProjectileHomingEnabled() && (this.unitAttack.getWeaponType() != CWeaponType.ARTILLERY)) { + return this.target.getY(); + } + else { + return this.initialTargetY; + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/config/CBasePlayer.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/config/CBasePlayer.java new file mode 100644 index 0000000..931d19a --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/config/CBasePlayer.java @@ -0,0 +1,185 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.config; + +import java.util.EnumMap; +import java.util.EnumSet; + +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CAllianceType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CMapControl; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayerJass; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayerState; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CRacePreference; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CPlayerSlotState; + +public class CBasePlayer implements CPlayerJass { + private final int id; + private String name; + private int team; + private int startLocationIndex; + private int forcedStartLocationIndex = -1; + private int color; + private final EnumSet racePrefs; + private final EnumSet[] alliances; + private final EnumMap[] taxRates; + private boolean onScoreScreen; + private boolean raceSelectable; + private CMapControl mapControl = CMapControl.NEUTRAL; + private CPlayerSlotState slotState = CPlayerSlotState.EMPTY; + + public CBasePlayer(final CBasePlayer other) { + this.id = other.id; + this.name = other.name; + this.team = other.team; + this.startLocationIndex = other.startLocationIndex; + this.forcedStartLocationIndex = other.forcedStartLocationIndex; + this.color = other.color; + this.racePrefs = other.racePrefs; + this.alliances = other.alliances; + this.taxRates = other.taxRates; + this.onScoreScreen = other.onScoreScreen; + this.raceSelectable = other.raceSelectable; + this.mapControl = other.mapControl; + this.slotState = other.slotState; + } + + public CBasePlayer(final int id) { + this.id = id; + this.alliances = new EnumSet[WarsmashConstants.MAX_PLAYERS]; + this.taxRates = new EnumMap[WarsmashConstants.MAX_PLAYERS]; + this.racePrefs = EnumSet.noneOf(CRacePreference.class); + for (int i = 0; i < this.alliances.length; i++) { + if (i == id) { + // player is fully allied with self + this.alliances[i] = EnumSet.allOf(CAllianceType.class); + } + else { + this.alliances[i] = EnumSet.noneOf(CAllianceType.class); + } + this.taxRates[i] = new EnumMap<>(CPlayerState.class); + } + } + + public int getId() { + return this.id; + } + + @Override + public void setName(final String name) { + this.name = name; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public void setOnScoreScreen(final boolean onScoreScreen) { + this.onScoreScreen = onScoreScreen; + } + + public boolean isOnScoreScreen() { + return this.onScoreScreen; + } + + @Override + public void setRaceSelectable(final boolean raceSelectable) { + this.raceSelectable = raceSelectable; + } + + @Override + public void setTeam(final int team) { + this.team = team; + } + + @Override + public int getTeam() { + return this.team; + } + + @Override + public int getStartLocationIndex() { + return this.startLocationIndex; + } + + @Override + public void setStartLocationIndex(final int startLocationIndex) { + this.startLocationIndex = startLocationIndex; + } + + @Override + public void setColor(final int color) { + this.color = color; + } + + @Override + public int getColor() { + return this.color; + } + + @Override + public boolean isRacePrefSet(final CRacePreference racePref) { + return this.racePrefs.contains(racePref); + } + + @Override + public void setRacePref(final CRacePreference racePref) { + this.racePrefs.add(racePref); + } + + @Override + public void setAlliance(final int otherPlayerIndex, final CAllianceType allianceType, final boolean value) { + final EnumSet alliancesWithOtherPlayer = this.alliances[otherPlayerIndex]; + if (value) { + alliancesWithOtherPlayer.add(allianceType); + } + else { + alliancesWithOtherPlayer.remove(allianceType); + } + } + + public boolean hasAlliance(final int otherPlayerIndex, final CAllianceType allianceType) { + final EnumSet alliancesWithOtherPlayer = this.alliances[otherPlayerIndex]; + return alliancesWithOtherPlayer.contains(allianceType); + } + + @Override + public void forceStartLocation(final int startLocIndex) { + this.forcedStartLocationIndex = startLocIndex; + } + + @Override + public void setTaxRate(final int otherPlayerIndex, final CPlayerState whichResource, final int rate) { + this.taxRates[otherPlayerIndex].put(whichResource, rate); + } + + @Override + public void setController(final CMapControl mapControl) { + this.mapControl = mapControl; + + } + + @Override + public boolean isSelectable() { + return this.raceSelectable; + } + + @Override + public CMapControl getController() { + return this.mapControl; + } + + @Override + public CPlayerSlotState getSlotState() { + return this.slotState; + } + + @Override + public int getTaxRate(final int otherPlayerIndex, final CPlayerState whichResource) { + final Integer taxRate = this.taxRates[otherPlayerIndex].get(whichResource); + if (taxRate == null) { + return 0; + } + return taxRate; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/config/CPlayerAPI.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/config/CPlayerAPI.java new file mode 100644 index 0000000..61baa4f --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/config/CPlayerAPI.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.config; + +public interface CPlayerAPI { + CBasePlayer getPlayer(int index); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/config/War3MapConfig.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/config/War3MapConfig.java new file mode 100644 index 0000000..dec0f41 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/config/War3MapConfig.java @@ -0,0 +1,153 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.config; + +import java.util.EnumMap; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CMapFlag; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CMapPlacement; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CGameSpeed; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CGameType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CMapDensity; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CMapDifficulty; + +public class War3MapConfig implements CPlayerAPI { + private String mapName; + private String mapDescription; + private int teamCount; + private int playerCount; + private final War3MapConfigStartLoc[] startLocations; + private final CBasePlayer[] players; + private final EnumMap gameTypeToSupported = new EnumMap<>(CGameType.class); + private final EnumMap mapFlagToEnabled = new EnumMap<>(CMapFlag.class); + private CMapPlacement placement; + private CGameSpeed gameSpeed; + private CMapDifficulty gameDifficulty; + private CMapDensity resourceDensity; + private CMapDensity creatureDensity; + private CGameType gameTypeSelected; + + public War3MapConfig(final int maxPlayers) { + this.startLocations = new War3MapConfigStartLoc[maxPlayers]; + this.players = new CBasePlayer[maxPlayers]; + for (int i = 0; i < maxPlayers; i++) { + this.startLocations[i] = new War3MapConfigStartLoc(); + this.players[i] = new CBasePlayer(i); + } + } + + public void setMapName(final String mapName) { + this.mapName = mapName; + } + + public void setMapDescription(final String mapDescription) { + this.mapDescription = mapDescription; + } + + public String getMapName() { + return this.mapName; + } + + public String getMapDescription() { + return this.mapDescription; + } + + public void setTeamCount(final int teamCount) { + this.teamCount = teamCount; + } + + public void setPlayerCount(final int playerCount) { + this.playerCount = playerCount; + } + + public void defineStartLocation(final int whichStartLoc, final float x, final float y) { + final War3MapConfigStartLoc startLoc = this.startLocations[whichStartLoc]; + startLoc.setX(x); + startLoc.setY(y); + } + + public War3MapConfigStartLoc getStartLoc(final int whichStartLoc) { + return this.startLocations[whichStartLoc]; + } + + public void setGameTypeSupported(final CGameType gameType, final boolean supported) { + this.gameTypeToSupported.put(gameType, supported); + } + + public void setMapFlag(final CMapFlag mapFlag, final boolean set) { + this.mapFlagToEnabled.put(mapFlag, set); + } + + public void setPlacement(final CMapPlacement placement) { + this.placement = placement; + } + + public void setGameSpeed(final CGameSpeed gameSpeed) { + this.gameSpeed = gameSpeed; + } + + public void setGameDifficulty(final CMapDifficulty gameDifficulty) { + this.gameDifficulty = gameDifficulty; + } + + public void setResourceDensity(final CMapDensity resourceDensity) { + this.resourceDensity = resourceDensity; + } + + public void setCreatureDensity(final CMapDensity creatureDensity) { + this.creatureDensity = creatureDensity; + } + + public int getTeamCount() { + return this.teamCount; + } + + public int getPlayerCount() { + return this.playerCount; + } + + public boolean isGameTypeSupported(final CGameType gameType) { + final Boolean supported = this.gameTypeToSupported.get(gameType); + return (supported != null) && supported; + } + + public CGameType getGameTypeSelected() { + return this.gameTypeSelected; + } + + public boolean isMapFlagSet(final CMapFlag mapFlag) { + final Boolean flag = this.mapFlagToEnabled.get(mapFlag); + return (flag != null) && flag; + } + + public CMapPlacement getPlacement() { + return this.placement; + } + + public CGameSpeed getGameSpeed() { + return this.gameSpeed; + } + + public CMapDifficulty getGameDifficulty() { + return this.gameDifficulty; + } + + public CMapDensity getResourceDensity() { + return this.resourceDensity; + } + + public CMapDensity getCreatureDensity() { + return this.creatureDensity; + } + + public float getStartLocationX(final int startLocIndex) { + return this.startLocations[startLocIndex].getX(); + } + + public float getStartLocationY(final int startLocIndex) { + return this.startLocations[startLocIndex].getY(); + } + + @Override + public CBasePlayer getPlayer(final int index) { + return this.players[index]; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/config/War3MapConfigStartLoc.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/config/War3MapConfigStartLoc.java new file mode 100644 index 0000000..281eb76 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/config/War3MapConfigStartLoc.java @@ -0,0 +1,44 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.config; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CStartLocPrio; + +public class War3MapConfigStartLoc { + private float x; + private float y; + private int[] otherStartIndices; + private CStartLocPrio[] otherStartLocPriorities; + + public void setX(final float x) { + this.x = x; + } + + public void setY(final float y) { + this.y = y; + } + + public float getX() { + return this.x; + } + + public float getY() { + return this.y; + } + + public int[] getOtherStartIndices() { + return this.otherStartIndices; + } + + public CStartLocPrio[] getOtherStartLocPriorities() { + return this.otherStartLocPriorities; + } + + public void setStartLocPrioCount(final int startLocPrioCount) { + this.otherStartIndices = new int[startLocPrioCount]; + this.otherStartLocPriorities = new CStartLocPrio[startLocPrioCount]; + } + + public void setStartLocPrio(final int prioSlotIndex, final int otherStartLocIndex, final CStartLocPrio priority) { + this.otherStartIndices[prioSlotIndex] = otherStartLocIndex; + this.otherStartLocPriorities[prioSlotIndex] = priority; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CAbilityData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CAbilityData.java new file mode 100644 index 0000000..d0cdcb5 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CAbilityData.java @@ -0,0 +1,66 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.data; + +import java.util.HashMap; +import java.util.Map; + +import com.etheller.warsmash.units.manager.MutableObjectData; +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityGeneric; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.CAbilityType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.CAbilityTypeDefinition; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.CAbilityTypeDefinitionChannelTest; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.CAbilityTypeDefinitionColdArrows; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.CAbilityTypeDefinitionGoldMine; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.CAbilityTypeDefinitionHarvest; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.CAbilityTypeDefinitionInventory; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.types.definitions.impl.CAbilityTypeDefinitionReturnResources; + +public class CAbilityData { + + private final MutableObjectData abilityData; + private Map> aliasToAbilityType = new HashMap<>(); + private final Map codeToAbilityTypeDefinition = new HashMap<>(); + + public CAbilityData(final MutableObjectData abilityData) { + this.abilityData = abilityData; + this.aliasToAbilityType = new HashMap<>(); + registerCodes(); + } + + private void registerCodes() { + this.codeToAbilityTypeDefinition.put(War3ID.fromString("ACcw"), new CAbilityTypeDefinitionColdArrows()); + this.codeToAbilityTypeDefinition.put(War3ID.fromString("Agld"), new CAbilityTypeDefinitionGoldMine()); + this.codeToAbilityTypeDefinition.put(War3ID.fromString("Artn"), new CAbilityTypeDefinitionReturnResources()); + this.codeToAbilityTypeDefinition.put(War3ID.fromString("Ahar"), new CAbilityTypeDefinitionHarvest()); + this.codeToAbilityTypeDefinition.put(War3ID.fromString("ANcl"), new CAbilityTypeDefinitionChannelTest()); + this.codeToAbilityTypeDefinition.put(War3ID.fromString("AInv"), new CAbilityTypeDefinitionInventory()); + } + + public CAbilityType getAbilityType(final War3ID alias) { + CAbilityType abilityType = this.aliasToAbilityType.get(alias); + if (abilityType == null) { + final MutableGameObject mutableGameObject = this.abilityData.get(alias); + if (mutableGameObject == null) { + return null; + } + final War3ID code = mutableGameObject.getCode(); + final CAbilityTypeDefinition abilityTypeDefinition = this.codeToAbilityTypeDefinition.get(code); + if (abilityTypeDefinition != null) { + abilityType = abilityTypeDefinition.createAbilityType(alias, mutableGameObject); + this.aliasToAbilityType.put(alias, abilityType); + } + } + return abilityType; + } + + public CAbility createAbility(final String ability, final int handleId) { + final War3ID war3Id = War3ID.fromString(ability); + final CAbilityType abilityType = getAbilityType(war3Id); + if (abilityType != null) { + return abilityType.createAbility(handleId); + } + return new CAbilityGeneric(war3Id, handleId); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CDestructableData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CDestructableData.java new file mode 100644 index 0000000..e26ff00 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CDestructableData.java @@ -0,0 +1,87 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.data; + +import java.awt.image.BufferedImage; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +import com.etheller.warsmash.units.manager.MutableObjectData; +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.RemovablePathingMapInstance; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CDestructable; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CDestructableType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.HandleIdAllocator; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.SimulationRenderController; + +public class CDestructableData { + private static final War3ID NAME = War3ID.fromString("bnam"); + private static final War3ID HIT_POINT_MAXIMUM = War3ID.fromString("bhps"); + private static final War3ID TARGETED_AS = War3ID.fromString("btar"); + private static final War3ID ARMOR_TYPE = War3ID.fromString("barm"); + + private static final War3ID BUILD_TIME = War3ID.fromString("bbut"); + private static final War3ID REPAIR_TIME = War3ID.fromString("bret"); + private static final War3ID GOLD_REPAIR = War3ID.fromString("breg"); + private static final War3ID LUMBER_REPAIR = War3ID.fromString("brel"); + + private final MutableObjectData unitData; + private final Map unitIdToUnitType = new HashMap<>(); + private final SimulationRenderController simulationRenderController; + + public CDestructableData(final MutableObjectData unitData, + final SimulationRenderController simulationRenderController) { + this.unitData = unitData; + this.simulationRenderController = simulationRenderController; + } + + public CDestructable create(final CSimulation simulation, final War3ID typeId, final float x, final float y, + final HandleIdAllocator handleIdAllocator, final RemovablePathingMapInstance pathingInstance, + final RemovablePathingMapInstance pathingInstanceDeath) { + final MutableGameObject unitType = this.unitData.get(typeId); + final int handleId = handleIdAllocator.createId(); + + final CDestructableType unitTypeInstance = getUnitTypeInstance(typeId, unitType); + + final float life = unitTypeInstance.getLife(); + + final CDestructable destructable = new CDestructable(handleId, x, y, life, unitTypeInstance, pathingInstance, + pathingInstanceDeath); + return destructable; + } + + private CDestructableType getUnitTypeInstance(final War3ID typeId, final MutableGameObject unitType) { + CDestructableType unitTypeInstance = this.unitIdToUnitType.get(typeId); + if (unitTypeInstance == null) { + final BufferedImage buildingPathingPixelMap = this.simulationRenderController + .getDestructablePathingPixelMap(typeId); + final BufferedImage buildingPathingDeathPixelMap = this.simulationRenderController + .getDestructablePathingDeathPixelMap(typeId); + final String name = unitType.getFieldAsString(NAME, 0); + final float life = unitType.getFieldAsFloat(HIT_POINT_MAXIMUM, 0); + final EnumSet targetedAs = CTargetType + .parseTargetTypeSet(unitType.getFieldAsString(TARGETED_AS, 0)); + final String armorType = unitType.getFieldAsString(ARMOR_TYPE, 0); + final int buildTime = unitType.getFieldAsInteger(BUILD_TIME, 0); + + unitTypeInstance = new CDestructableType(name, life, targetedAs, armorType, buildTime, + buildingPathingPixelMap, buildingPathingDeathPixelMap); + this.unitIdToUnitType.put(typeId, unitTypeInstance); + } + return unitTypeInstance; + } + + public CDestructableType getUnitType(final War3ID rawcode) { + final CDestructableType unitTypeInstance = this.unitIdToUnitType.get(rawcode); + if (unitTypeInstance != null) { + return unitTypeInstance; + } + final MutableGameObject unitType = this.unitData.get(rawcode); + if (unitType == null) { + return null; + } + return getUnitTypeInstance(rawcode, unitType); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CItemData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CItemData.java new file mode 100644 index 0000000..24ea711 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CItemData.java @@ -0,0 +1,125 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.data; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.etheller.warsmash.units.manager.MutableObjectData; +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItemType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; + +public class CItemData { + private static final War3ID ABILITY_LIST = War3ID.fromString("iabi"); + private static final War3ID COOLDOWN_GROUP = War3ID.fromString("icid"); + private static final War3ID IGNORE_COOLDOWN = War3ID.fromString("iicd"); + private static final War3ID NUMBER_OF_CHARGES = War3ID.fromString("iuse"); + private static final War3ID ACTIVELY_USED = War3ID.fromString("iusa"); + private static final War3ID PERISHABLE = War3ID.fromString("iper"); + private static final War3ID USE_AUTOMATICALLY_WHEN_ACQUIRED = War3ID.fromString("ipow"); + + private static final War3ID GOLD_COST = War3ID.fromString("igol"); + private static final War3ID LUMBER_COST = War3ID.fromString("ilum"); + private static final War3ID STOCK_MAX = War3ID.fromString("isto"); + private static final War3ID STOCK_REPLENISH_INTERVAL = War3ID.fromString("istr"); + private static final War3ID STOCK_START_DELAY = War3ID.fromString("isst"); + + private static final War3ID HIT_POINTS = War3ID.fromString("ihtp"); + private static final War3ID ARMOR_TYPE = War3ID.fromString("iarm"); + + private static final War3ID LEVEL = War3ID.fromString("ilev"); + private static final War3ID LEVEL_UNCLASSIFIED = War3ID.fromString("ilvo"); + private static final War3ID PRIORITY = War3ID.fromString("ipri"); + + private static final War3ID SELLABLE = War3ID.fromString("isel"); + private static final War3ID PAWNABLE = War3ID.fromString("ipaw"); + + private static final War3ID DROPPED_WHEN_CARRIER_DIES = War3ID.fromString("idrp"); + private static final War3ID CAN_BE_DROPPED = War3ID.fromString("idro"); + + private static final War3ID VALID_TARGET_FOR_TRANSFORMATION = War3ID.fromString("imor"); + private static final War3ID INCLUDE_AS_RANDOM_CHOICE = War3ID.fromString("iprn"); + + private final Map itemIdToItemType = new HashMap<>(); + private final MutableObjectData itemData; + + public CItemData(final MutableObjectData itemData) { + this.itemData = itemData; + } + + public CItem create(final CSimulation simulation, final War3ID typeId, final float x, final float y, + final int handleId) { + final MutableGameObject itemType = this.itemData.get(typeId); + final CItemType itemTypeInstance = getItemTypeInstance(typeId, itemType); + + return new CItem(handleId, x, y, itemTypeInstance.getHitPoints(), typeId, itemTypeInstance); + } + + public CItemType getItemType(final War3ID typeId) { + final MutableGameObject itemType = this.itemData.get(typeId); + if (itemType == null) { + return null; + } + return getItemTypeInstance(typeId, itemType); + } + + private CItemType getItemTypeInstance(final War3ID typeId, final MutableGameObject itemType) { + CItemType itemTypeInstance = this.itemIdToItemType.get(typeId); + if (itemTypeInstance == null) { + final String abilityListString = itemType.getFieldAsString(ABILITY_LIST, 0); + final String[] abilityListStringItems = abilityListString.split(","); + final List abilityList = new ArrayList<>(); + for (final String abilityListStringItem : abilityListStringItems) { + if (abilityListStringItem.length() == 4) { + abilityList.add(War3ID.fromString(abilityListStringItem)); + } + } + + final War3ID cooldownGroup; + final String cooldownGroupString = itemType.getFieldAsString(COOLDOWN_GROUP, 0); + if ((cooldownGroupString != null) && (cooldownGroupString.length() == 4)) { + cooldownGroup = War3ID.fromString(cooldownGroupString); + } + else { + cooldownGroup = null; + } + final boolean ignoreCooldown = itemType.getFieldAsBoolean(IGNORE_COOLDOWN, 0); + final int numberOfCharges = itemType.getFieldAsInteger(NUMBER_OF_CHARGES, 0); + final boolean activelyUsed = itemType.getFieldAsBoolean(ACTIVELY_USED, 0); + final boolean perishable = itemType.getFieldAsBoolean(PERISHABLE, 0); + final boolean useAutomaticallyWhenAcquired = itemType.getFieldAsBoolean(USE_AUTOMATICALLY_WHEN_ACQUIRED, 0); + + final int goldCost = itemType.getFieldAsInteger(GOLD_COST, 0); + final int lumberCost = itemType.getFieldAsInteger(LUMBER_COST, 0); + final int stockMax = itemType.getFieldAsInteger(STOCK_MAX, 0); + final int stockReplenishInterval = itemType.getFieldAsInteger(STOCK_REPLENISH_INTERVAL, 0); + final int stockStartDelay = itemType.getFieldAsInteger(STOCK_START_DELAY, 0); + + final int hitPoints = itemType.getFieldAsInteger(HIT_POINTS, 0); + final String armorType = itemType.getFieldAsString(ARMOR_TYPE, 0); + + final int level = itemType.getFieldAsInteger(LEVEL, 0); + final int levelUnclassified = itemType.getFieldAsInteger(LEVEL_UNCLASSIFIED, 0); + final int priority = itemType.getFieldAsInteger(PRIORITY, 0); + + final boolean sellable = itemType.getFieldAsBoolean(SELLABLE, 0); + final boolean pawnable = itemType.getFieldAsBoolean(PAWNABLE, 0); + + final boolean droppedWhenCarrierDies = itemType.getFieldAsBoolean(DROPPED_WHEN_CARRIER_DIES, 0); + final boolean canBeDropped = itemType.getFieldAsBoolean(CAN_BE_DROPPED, 0); + + final boolean validTargetForTransformation = itemType.getFieldAsBoolean(VALID_TARGET_FOR_TRANSFORMATION, 0); + final boolean includeAsRandomChoice = itemType.getFieldAsBoolean(INCLUDE_AS_RANDOM_CHOICE, 0); + + itemTypeInstance = new CItemType(abilityList, cooldownGroup, ignoreCooldown, numberOfCharges, activelyUsed, + perishable, useAutomaticallyWhenAcquired, goldCost, lumberCost, stockMax, stockReplenishInterval, + stockStartDelay, hitPoints, armorType, level, levelUnclassified, priority, sellable, pawnable, + droppedWhenCarrierDies, canBeDropped, validTargetForTransformation, includeAsRandomChoice); + this.itemIdToItemType.put(typeId, itemTypeInstance); + } + return itemTypeInstance; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CUnitData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CUnitData.java new file mode 100644 index 0000000..642c525 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CUnitData.java @@ -0,0 +1,748 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.data; + +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.etheller.warsmash.units.manager.MutableObjectData; +import com.etheller.warsmash.units.manager.MutableObjectData.MutableGameObject; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.MovementType; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.RemovablePathingMapInstance; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CGameplayConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitClassification; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitTypeRequirement; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.HandleIdAllocator; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityAttack; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityMove; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityHumanBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityNagaBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityNeutralBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityNightElfBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityOrcBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityUndeadBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.hero.CAbilityHero; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.hero.CPrimaryAttribute; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.queue.CAbilityQueue; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.queue.CAbilityRally; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CDefenseType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CTargetType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CWeaponType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttack; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackInstant; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackMissile; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackMissileBounce; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackMissileLine; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackMissileSplash; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackNormal; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.pathing.CBuildingPathingType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.SimulationRenderController; + +public class CUnitData { + private static final War3ID MANA_INITIAL_AMOUNT = War3ID.fromString("umpi"); + private static final War3ID MANA_MAXIMUM = War3ID.fromString("umpm"); + private static final War3ID HIT_POINT_MAXIMUM = War3ID.fromString("uhpm"); + private static final War3ID MOVEMENT_SPEED_BASE = War3ID.fromString("umvs"); + private static final War3ID PROPULSION_WINDOW = War3ID.fromString("uprw"); + private static final War3ID TURN_RATE = War3ID.fromString("umvr"); + private static final War3ID IS_BLDG = War3ID.fromString("ubdg"); + private static final War3ID NAME = War3ID.fromString("unam"); + private static final War3ID PROPER_NAMES = War3ID.fromString("upro"); + private static final War3ID PROPER_NAMES_COUNT = War3ID.fromString("upru"); + private static final War3ID PROJECTILE_LAUNCH_X = War3ID.fromString("ulpx"); + private static final War3ID PROJECTILE_LAUNCH_Y = War3ID.fromString("ulpy"); + private static final War3ID PROJECTILE_LAUNCH_Z = War3ID.fromString("ulpz"); + private static final War3ID ATTACKS_ENABLED = War3ID.fromString("uaen"); + private static final War3ID ATTACK1_BACKSWING_POINT = War3ID.fromString("ubs1"); + private static final War3ID ATTACK1_DAMAGE_POINT = War3ID.fromString("udp1"); + private static final War3ID ATTACK1_AREA_OF_EFFECT_FULL_DMG = War3ID.fromString("ua1f"); + private static final War3ID ATTACK1_AREA_OF_EFFECT_HALF_DMG = War3ID.fromString("ua1h"); + private static final War3ID ATTACK1_AREA_OF_EFFECT_QUARTER_DMG = War3ID.fromString("ua1q"); + private static final War3ID ATTACK1_AREA_OF_EFFECT_TARGETS = War3ID.fromString("ua1p"); + private static final War3ID ATTACK1_ATTACK_TYPE = War3ID.fromString("ua1t"); + private static final War3ID ATTACK1_COOLDOWN = War3ID.fromString("ua1c"); + private static final War3ID ATTACK1_DMG_BASE = War3ID.fromString("ua1b"); + private static final War3ID ATTACK1_DAMAGE_FACTOR_HALF = War3ID.fromString("uhd1"); + private static final War3ID ATTACK1_DAMAGE_FACTOR_QUARTER = War3ID.fromString("uqd1"); + private static final War3ID ATTACK1_DAMAGE_LOSS_FACTOR = War3ID.fromString("udl1"); + private static final War3ID ATTACK1_DMG_DICE = War3ID.fromString("ua1d"); + private static final War3ID ATTACK1_DMG_SIDES_PER_DIE = War3ID.fromString("ua1s"); + private static final War3ID ATTACK1_DMG_SPILL_DIST = War3ID.fromString("usd1"); + private static final War3ID ATTACK1_DMG_SPILL_RADIUS = War3ID.fromString("usr1"); + private static final War3ID ATTACK1_DMG_UPGRADE_AMT = War3ID.fromString("udu1"); + private static final War3ID ATTACK1_TARGET_COUNT = War3ID.fromString("utc1"); + private static final War3ID ATTACK1_PROJECTILE_ARC = War3ID.fromString("uma1"); + private static final War3ID ATTACK1_MISSILE_ART = War3ID.fromString("ua1m"); + private static final War3ID ATTACK1_PROJECTILE_HOMING_ENABLED = War3ID.fromString("umh1"); + private static final War3ID ATTACK1_PROJECTILE_SPEED = War3ID.fromString("ua1z"); + private static final War3ID ATTACK1_RANGE = War3ID.fromString("ua1r"); + private static final War3ID ATTACK1_RANGE_MOTION_BUFFER = War3ID.fromString("urb1"); + private static final War3ID ATTACK1_SHOW_UI = War3ID.fromString("uwu1"); + private static final War3ID ATTACK1_TARGETS_ALLOWED = War3ID.fromString("ua1g"); + private static final War3ID ATTACK1_WEAPON_SOUND = War3ID.fromString("ucs1"); + private static final War3ID ATTACK1_WEAPON_TYPE = War3ID.fromString("ua1w"); + + private static final War3ID ATTACK2_BACKSWING_POINT = War3ID.fromString("ubs2"); + private static final War3ID ATTACK2_DAMAGE_POINT = War3ID.fromString("udp2"); + private static final War3ID ATTACK2_AREA_OF_EFFECT_FULL_DMG = War3ID.fromString("ua2f"); + private static final War3ID ATTACK2_AREA_OF_EFFECT_HALF_DMG = War3ID.fromString("ua2h"); + private static final War3ID ATTACK2_AREA_OF_EFFECT_QUARTER_DMG = War3ID.fromString("ua2q"); + private static final War3ID ATTACK2_AREA_OF_EFFECT_TARGETS = War3ID.fromString("ua2p"); + private static final War3ID ATTACK2_ATTACK_TYPE = War3ID.fromString("ua2t"); + private static final War3ID ATTACK2_COOLDOWN = War3ID.fromString("ua2c"); + private static final War3ID ATTACK2_DMG_BASE = War3ID.fromString("ua2b"); + private static final War3ID ATTACK2_DAMAGE_FACTOR_HALF = War3ID.fromString("uhd2"); + private static final War3ID ATTACK2_DAMAGE_FACTOR_QUARTER = War3ID.fromString("uqd2"); + private static final War3ID ATTACK2_DAMAGE_LOSS_FACTOR = War3ID.fromString("udl2"); + private static final War3ID ATTACK2_DMG_DICE = War3ID.fromString("ua2d"); + private static final War3ID ATTACK2_DMG_SIDES_PER_DIE = War3ID.fromString("ua2s"); + private static final War3ID ATTACK2_DMG_SPILL_DIST = War3ID.fromString("usd2"); + private static final War3ID ATTACK2_DMG_SPILL_RADIUS = War3ID.fromString("usr2"); + private static final War3ID ATTACK2_DMG_UPGRADE_AMT = War3ID.fromString("udu2"); + private static final War3ID ATTACK2_TARGET_COUNT = War3ID.fromString("utc2"); + private static final War3ID ATTACK2_PROJECTILE_ARC = War3ID.fromString("uma2"); + private static final War3ID ATTACK2_MISSILE_ART = War3ID.fromString("ua2m"); + private static final War3ID ATTACK2_PROJECTILE_HOMING_ENABLED = War3ID.fromString("umh2"); + private static final War3ID ATTACK2_PROJECTILE_SPEED = War3ID.fromString("ua2z"); + private static final War3ID ATTACK2_RANGE = War3ID.fromString("ua2r"); + private static final War3ID ATTACK2_RANGE_MOTION_BUFFER = War3ID.fromString("urb2"); + private static final War3ID ATTACK2_SHOW_UI = War3ID.fromString("uwu2"); + private static final War3ID ATTACK2_TARGETS_ALLOWED = War3ID.fromString("ua2g"); + private static final War3ID ATTACK2_WEAPON_SOUND = War3ID.fromString("ucs2"); + private static final War3ID ATTACK2_WEAPON_TYPE = War3ID.fromString("ua2w"); + + private static final War3ID ACQUISITION_RANGE = War3ID.fromString("uacq"); + private static final War3ID MINIMUM_ATTACK_RANGE = War3ID.fromString("uamn"); + + private static final War3ID PROJECTILE_IMPACT_Z = War3ID.fromString("uimz"); + + private static final War3ID DEATH_TYPE = War3ID.fromString("udea"); + private static final War3ID ARMOR_TYPE = War3ID.fromString("uarm"); + + private static final War3ID DEFENSE = War3ID.fromString("udef"); + private static final War3ID DEFENSE_TYPE = War3ID.fromString("udty"); + private static final War3ID MOVE_HEIGHT = War3ID.fromString("umvh"); + private static final War3ID MOVE_TYPE = War3ID.fromString("umvt"); + private static final War3ID COLLISION_SIZE = War3ID.fromString("ucol"); + private static final War3ID CLASSIFICATION = War3ID.fromString("utyp"); + private static final War3ID DEATH_TIME = War3ID.fromString("udtm"); + private static final War3ID TARGETED_AS = War3ID.fromString("utar"); + + private static final War3ID ABILITIES_NORMAL = War3ID.fromString("uabi"); + private static final War3ID ABILITIES_HERO = War3ID.fromString("uhab"); + + private static final War3ID STRUCTURES_BUILT = War3ID.fromString("ubui"); + private static final War3ID UNITS_TRAINED = War3ID.fromString("utra"); + private static final War3ID RESEARCHES_AVAILABLE = War3ID.fromString("ures"); + private static final War3ID UNIT_RACE = War3ID.fromString("urac"); + + private static final War3ID REQUIRES = War3ID.fromString("ureq"); + private static final War3ID REQUIRES_AMOUNT = War3ID.fromString("urqc"); + + private static final War3ID GOLD_COST = War3ID.fromString("ugol"); + private static final War3ID LUMBER_COST = War3ID.fromString("ulum"); + private static final War3ID BUILD_TIME = War3ID.fromString("ubld"); + private static final War3ID FOOD_USED = War3ID.fromString("ufoo"); + private static final War3ID FOOD_MADE = War3ID.fromString("ufma"); + + private static final War3ID REQUIRE_PLACE = War3ID.fromString("upar"); + private static final War3ID PREVENT_PLACE = War3ID.fromString("upap"); + + private static final War3ID UNIT_LEVEL = War3ID.fromString("ulev"); + + private static final War3ID STR = War3ID.fromString("ustr"); + private static final War3ID STR_PLUS = War3ID.fromString("ustp"); + private static final War3ID AGI = War3ID.fromString("uagi"); + private static final War3ID AGI_PLUS = War3ID.fromString("uagp"); + private static final War3ID INT = War3ID.fromString("uint"); + private static final War3ID INT_PLUS = War3ID.fromString("uinp"); + private static final War3ID PRIMARY_ATTRIBUTE = War3ID.fromString("upra"); + + private static final War3ID CAN_FLEE = War3ID.fromString("ufle"); + + private final CGameplayConstants gameplayConstants; + private final MutableObjectData unitData; + private final Map unitIdToUnitType = new HashMap<>(); + private final Map jassLegacyNameToUnitId = new HashMap<>(); + private final CAbilityData abilityData; + private final SimulationRenderController simulationRenderController; + + public CUnitData(final CGameplayConstants gameplayConstants, final MutableObjectData unitData, + final CAbilityData abilityData, final SimulationRenderController simulationRenderController) { + this.gameplayConstants = gameplayConstants; + this.unitData = unitData; + this.abilityData = abilityData; + this.simulationRenderController = simulationRenderController; + } + + public CUnit create(final CSimulation simulation, final int playerIndex, final War3ID typeId, final float x, + final float y, final float facing, final BufferedImage buildingPathingPixelMap, + final HandleIdAllocator handleIdAllocator, final RemovablePathingMapInstance pathingInstance) { + final MutableGameObject unitType = this.unitData.get(typeId); + final int handleId = handleIdAllocator.createId(); + + final CUnitType unitTypeInstance = getUnitTypeInstance(typeId, buildingPathingPixelMap, unitType); + final int life = unitTypeInstance.getLife(); + final int manaInitial = unitTypeInstance.getManaInitial(); + final int manaMaximum = unitTypeInstance.getManaMaximum(); + final int speed = unitTypeInstance.getSpeed(); + + final CUnit unit = new CUnit(handleId, playerIndex, x, y, life, typeId, facing, manaInitial, life, manaMaximum, + speed, unitTypeInstance, pathingInstance); + if (speed > 0) { + unit.add(simulation, new CAbilityMove(handleIdAllocator.createId())); + } + if (unitTypeInstance.isHero()) { + final List heroAttacks = new ArrayList<>(); + for (final CUnitAttack attack : unitTypeInstance.getAttacks()) { + heroAttacks.add(attack.copy()); + } + unit.setUnitSpecificAttacks(heroAttacks); + } + if (!unit.getAttacks().isEmpty()) { + unit.add(simulation, new CAbilityAttack(handleIdAllocator.createId())); + } + final List structuresBuilt = unitTypeInstance.getStructuresBuilt(); + if (!structuresBuilt.isEmpty()) { + switch (unitTypeInstance.getRace()) { + case ORC: + unit.add(simulation, new CAbilityOrcBuild(handleIdAllocator.createId(), structuresBuilt)); + break; + case HUMAN: + unit.add(simulation, new CAbilityHumanBuild(handleIdAllocator.createId(), structuresBuilt)); + break; + case UNDEAD: + unit.add(simulation, new CAbilityUndeadBuild(handleIdAllocator.createId(), structuresBuilt)); + break; + case NIGHTELF: + unit.add(simulation, new CAbilityNightElfBuild(handleIdAllocator.createId(), structuresBuilt)); + break; + case NAGA: + unit.add(simulation, new CAbilityNagaBuild(handleIdAllocator.createId(), structuresBuilt)); + break; + case CREEPS: + case CRITTERS: + case DEMON: + case OTHER: + unit.add(simulation, new CAbilityNeutralBuild(handleIdAllocator.createId(), structuresBuilt)); + break; + } + } + final List unitsTrained = unitTypeInstance.getUnitsTrained(); + final List researchesAvailable = unitTypeInstance.getResearchesAvailable(); + if (!unitsTrained.isEmpty() || !researchesAvailable.isEmpty()) { + unit.add(simulation, new CAbilityQueue(handleIdAllocator.createId(), unitsTrained, researchesAvailable)); + } + if (!unitsTrained.isEmpty()) { + unit.add(simulation, new CAbilityRally(handleIdAllocator.createId())); + } + if (unitTypeInstance.isHero()) { + final List heroAbilityList = unitTypeInstance.getHeroAbilityList(); + unit.add(simulation, new CAbilityHero(handleIdAllocator.createId(), heroAbilityList)); + // reset initial mana after the value is adjusted for hero data + unit.setMana(manaInitial); + } + for (final String ability : unitTypeInstance.getAbilityList().split(",")) { + if ((ability.length() > 0) && !"_".equals(ability)) { + final CAbility createAbility = this.abilityData.createAbility(ability, handleIdAllocator.createId()); + if (createAbility != null) { + unit.add(simulation, createAbility); + } + } + } + return unit; + } + + private CUnitType getUnitTypeInstance(final War3ID typeId, final BufferedImage buildingPathingPixelMap, + final MutableGameObject unitType) { + CUnitType unitTypeInstance = this.unitIdToUnitType.get(typeId); + if (unitTypeInstance == null) { + final String legacyName = getLegacyName(unitType); + final int life = unitType.getFieldAsInteger(HIT_POINT_MAXIMUM, 0); + final int manaInitial = unitType.getFieldAsInteger(MANA_INITIAL_AMOUNT, 0); + final int manaMaximum = unitType.getFieldAsInteger(MANA_MAXIMUM, 0); + final int speed = unitType.getFieldAsInteger(MOVEMENT_SPEED_BASE, 0); + final int defense = unitType.getFieldAsInteger(DEFENSE, 0); + final String abilityList = unitType.getFieldAsString(ABILITIES_NORMAL, 0); + final String heroAbilityListString = unitType.getFieldAsString(ABILITIES_HERO, 0); + final int unitLevel = unitType.getFieldAsInteger(UNIT_LEVEL, 0); + + final float moveHeight = unitType.getFieldAsFloat(MOVE_HEIGHT, 0); + final String movetp = unitType.getFieldAsString(MOVE_TYPE, 0); + final float collisionSize = unitType.getFieldAsFloat(COLLISION_SIZE, 0); + final float propWindow = unitType.getFieldAsFloat(PROPULSION_WINDOW, 0); + final float turnRate = unitType.getFieldAsFloat(TURN_RATE, 0); + + final boolean canFlee = unitType.getFieldAsBoolean(CAN_FLEE, 0); + + final float strPlus = unitType.getFieldAsFloat(STR_PLUS, 0); + final float agiPlus = unitType.getFieldAsFloat(AGI_PLUS, 0); + final float intPlus = unitType.getFieldAsFloat(INT_PLUS, 0); + + final int strength = unitType.getFieldAsInteger(STR, 0); + final int agility = unitType.getFieldAsInteger(AGI, 0); + final int intelligence = unitType.getFieldAsInteger(INT, 0); + final CPrimaryAttribute primaryAttribute = CPrimaryAttribute + .parsePrimaryAttribute(unitType.getFieldAsString(PRIMARY_ATTRIBUTE, 0)); + + final String properNames = unitType.getFieldAsString(PROPER_NAMES, 0); + final int properNamesCount = unitType.getFieldAsInteger(PROPER_NAMES_COUNT, 0); + + final boolean isBldg = unitType.getFieldAsBoolean(IS_BLDG, 0); + PathingGrid.MovementType movementType = PathingGrid.getMovementType(movetp); + if (movementType == null) { + movementType = MovementType.DISABLED; + } + final String unitName = unitType.getFieldAsString(NAME, 0); + final float acquisitionRange = unitType.getFieldAsFloat(ACQUISITION_RANGE, 0); + final float minimumAttackRange = unitType.getFieldAsFloat(MINIMUM_ATTACK_RANGE, 0); + final EnumSet targetedAs = CTargetType + .parseTargetTypeSet(unitType.getFieldAsString(TARGETED_AS, 0)); + final String classificationString = unitType.getFieldAsString(CLASSIFICATION, 0); + final EnumSet classifications = EnumSet.noneOf(CUnitClassification.class); + if (classificationString != null) { + final String[] classificationValues = classificationString.split(","); + for (final String unitEditorKey : classificationValues) { + final CUnitClassification unitClassification = CUnitClassification + .parseUnitClassification(unitEditorKey); + if (unitClassification != null) { + classifications.add(unitClassification); + } + } + } + final List attacks = new ArrayList<>(); + final int attacksEnabled = unitType.getFieldAsInteger(ATTACKS_ENABLED, 0); + if ((attacksEnabled & 0x1) != 0) { + try { + // attack one + final float animationBackswingPoint = unitType.getFieldAsFloat(ATTACK1_BACKSWING_POINT, 0); + final float animationDamagePoint = unitType.getFieldAsFloat(ATTACK1_DAMAGE_POINT, 0); + final int areaOfEffectFullDamage = unitType.getFieldAsInteger(ATTACK1_AREA_OF_EFFECT_FULL_DMG, 0); + final int areaOfEffectMediumDamage = unitType.getFieldAsInteger(ATTACK1_AREA_OF_EFFECT_HALF_DMG, 0); + final int areaOfEffectSmallDamage = unitType.getFieldAsInteger(ATTACK1_AREA_OF_EFFECT_QUARTER_DMG, + 0); + final EnumSet areaOfEffectTargets = CTargetType + .parseTargetTypeSet(unitType.getFieldAsString(ATTACK1_AREA_OF_EFFECT_TARGETS, 0)); + final CAttackType attackType = CAttackType + .parseAttackType(unitType.getFieldAsString(ATTACK1_ATTACK_TYPE, 0)); + final float cooldownTime = unitType.getFieldAsFloat(ATTACK1_COOLDOWN, 0); + final int damageBase = unitType.getFieldAsInteger(ATTACK1_DMG_BASE, 0); + final float damageFactorMedium = unitType.getFieldAsFloat(ATTACK1_DAMAGE_FACTOR_HALF, 0); + final float damageFactorSmall = unitType.getFieldAsFloat(ATTACK1_DAMAGE_FACTOR_QUARTER, 0); + final float damageLossFactor = unitType.getFieldAsFloat(ATTACK1_DAMAGE_LOSS_FACTOR, 0); + final int damageDice = unitType.getFieldAsInteger(ATTACK1_DMG_DICE, 0); + final int damageSidesPerDie = unitType.getFieldAsInteger(ATTACK1_DMG_SIDES_PER_DIE, 0); + final float damageSpillDistance = unitType.getFieldAsFloat(ATTACK1_DMG_SPILL_DIST, 0); + final float damageSpillRadius = unitType.getFieldAsFloat(ATTACK1_DMG_SPILL_RADIUS, 0); + final int damageUpgradeAmount = unitType.getFieldAsInteger(ATTACK1_DMG_UPGRADE_AMT, 0); + final int maximumNumberOfTargets = unitType.getFieldAsInteger(ATTACK1_TARGET_COUNT, 0); + final float projectileArc = unitType.getFieldAsFloat(ATTACK1_PROJECTILE_ARC, 0); + final String projectileArt = unitType.getFieldAsString(ATTACK1_MISSILE_ART, 0); + final boolean projectileHomingEnabled = unitType + .getFieldAsBoolean(ATTACK1_PROJECTILE_HOMING_ENABLED, 0); + final int projectileSpeed = unitType.getFieldAsInteger(ATTACK1_PROJECTILE_SPEED, 0); + final int range = unitType.getFieldAsInteger(ATTACK1_RANGE, 0); + final float rangeMotionBuffer = unitType.getFieldAsFloat(ATTACK1_RANGE_MOTION_BUFFER, 0); + final boolean showUI = unitType.getFieldAsBoolean(ATTACK1_SHOW_UI, 0); + final EnumSet targetsAllowed = CTargetType + .parseTargetTypeSet(unitType.getFieldAsString(ATTACK1_TARGETS_ALLOWED, 0)); + final String weaponSound = unitType.getFieldAsString(ATTACK1_WEAPON_SOUND, 0); + final CWeaponType weaponType = CWeaponType + .parseWeaponType(unitType.getFieldAsString(ATTACK1_WEAPON_TYPE, 0)); + attacks.add(createAttack(animationBackswingPoint, animationDamagePoint, areaOfEffectFullDamage, + areaOfEffectMediumDamage, areaOfEffectSmallDamage, areaOfEffectTargets, attackType, + cooldownTime, damageBase, damageFactorMedium, damageFactorSmall, damageLossFactor, + damageDice, damageSidesPerDie, damageSpillDistance, damageSpillRadius, damageUpgradeAmount, + maximumNumberOfTargets, projectileArc, projectileArt, projectileHomingEnabled, + projectileSpeed, range, rangeMotionBuffer, showUI, targetsAllowed, weaponSound, + weaponType)); + } + catch (final Exception exc) { + System.err.println("Attack 1 failed to parse with: " + exc.getClass() + ":" + exc.getMessage()); + } + } + if ((attacksEnabled & 0x2) != 0) { + try { + // attack two + final float animationBackswingPoint = unitType.getFieldAsFloat(ATTACK2_BACKSWING_POINT, 0); + final float animationDamagePoint = unitType.getFieldAsFloat(ATTACK2_DAMAGE_POINT, 0); + final int areaOfEffectFullDamage = unitType.getFieldAsInteger(ATTACK2_AREA_OF_EFFECT_FULL_DMG, 0); + final int areaOfEffectMediumDamage = unitType.getFieldAsInteger(ATTACK2_AREA_OF_EFFECT_HALF_DMG, 0); + final int areaOfEffectSmallDamage = unitType.getFieldAsInteger(ATTACK2_AREA_OF_EFFECT_QUARTER_DMG, + 0); + final EnumSet areaOfEffectTargets = CTargetType + .parseTargetTypeSet(unitType.getFieldAsString(ATTACK2_AREA_OF_EFFECT_TARGETS, 0)); + final CAttackType attackType = CAttackType + .parseAttackType(unitType.getFieldAsString(ATTACK2_ATTACK_TYPE, 0)); + final float cooldownTime = unitType.getFieldAsFloat(ATTACK2_COOLDOWN, 0); + final int damageBase = unitType.getFieldAsInteger(ATTACK2_DMG_BASE, 0); + final float damageFactorMedium = unitType.getFieldAsFloat(ATTACK2_DAMAGE_FACTOR_HALF, 0); + final float damageFactorSmall = unitType.getFieldAsFloat(ATTACK2_DAMAGE_FACTOR_QUARTER, 0); + final float damageLossFactor = unitType.getFieldAsFloat(ATTACK2_DAMAGE_LOSS_FACTOR, 0); + final int damageDice = unitType.getFieldAsInteger(ATTACK2_DMG_DICE, 0); + final int damageSidesPerDie = unitType.getFieldAsInteger(ATTACK2_DMG_SIDES_PER_DIE, 0); + final float damageSpillDistance = unitType.getFieldAsFloat(ATTACK2_DMG_SPILL_DIST, 0); + final float damageSpillRadius = unitType.getFieldAsFloat(ATTACK2_DMG_SPILL_RADIUS, 0); + final int damageUpgradeAmount = unitType.getFieldAsInteger(ATTACK2_DMG_UPGRADE_AMT, 0); + final int maximumNumberOfTargets = unitType.getFieldAsInteger(ATTACK2_TARGET_COUNT, 0); + float projectileArc = unitType.getFieldAsFloat(ATTACK2_PROJECTILE_ARC, 0); + String projectileArt = unitType.getFieldAsString(ATTACK2_MISSILE_ART, 0); + int projectileSpeed = unitType.getFieldAsInteger(ATTACK2_PROJECTILE_SPEED, 0); + if ("_".equals(projectileArt) || projectileArt.isEmpty()) { + projectileArt = unitType.getFieldAsString(ATTACK1_MISSILE_ART, 0); + projectileSpeed = unitType.getFieldAsInteger(ATTACK1_PROJECTILE_SPEED, 0); + projectileArc = unitType.getFieldAsFloat(ATTACK1_PROJECTILE_ARC, 0); + } + final boolean projectileHomingEnabled = unitType + .getFieldAsBoolean(ATTACK2_PROJECTILE_HOMING_ENABLED, 0); + final int range = unitType.getFieldAsInteger(ATTACK2_RANGE, 0); + final float rangeMotionBuffer = unitType.getFieldAsFloat(ATTACK2_RANGE_MOTION_BUFFER, 0); + boolean showUI = unitType.getFieldAsBoolean(ATTACK2_SHOW_UI, 0); + final EnumSet targetsAllowed = CTargetType + .parseTargetTypeSet(unitType.getFieldAsString(ATTACK2_TARGETS_ALLOWED, 0)); + final String weaponSound = unitType.getFieldAsString(ATTACK2_WEAPON_SOUND, 0); + final CWeaponType weaponType = CWeaponType + .parseWeaponType(unitType.getFieldAsString(ATTACK2_WEAPON_TYPE, 0)); + if (!attacks.isEmpty()) { + final CUnitAttack otherAttack = attacks.get(0); + if ((otherAttack.getAttackType() == attackType) && (targetsAllowed.size() == 1) + && (targetsAllowed.contains(CTargetType.TREE) + || (targetsAllowed.contains(CTargetType.STRUCTURE) + && (otherAttack.getDamageBase() == damageBase) + && (otherAttack.getDamageSidesPerDie() == damageSidesPerDie) + && (otherAttack.getDamageDice() == damageDice)))) { + showUI = false; + } + } + attacks.add(createAttack(animationBackswingPoint, animationDamagePoint, areaOfEffectFullDamage, + areaOfEffectMediumDamage, areaOfEffectSmallDamage, areaOfEffectTargets, attackType, + cooldownTime, damageBase, damageFactorMedium, damageFactorSmall, damageLossFactor, + damageDice, damageSidesPerDie, damageSpillDistance, damageSpillRadius, damageUpgradeAmount, + maximumNumberOfTargets, projectileArc, projectileArt, projectileHomingEnabled, + projectileSpeed, range, rangeMotionBuffer, showUI, targetsAllowed, weaponSound, + weaponType)); + } + catch (final Exception exc) { + System.err.println("Attack 2 failed to parse with: " + exc.getClass() + ":" + exc.getMessage()); + } + } + final int deathType = unitType.getFieldAsInteger(DEATH_TYPE, 0); + final boolean raise = (deathType & 0x1) != 0; + final boolean decay = (deathType & 0x2) != 0; + final String armorType = unitType.getFieldAsString(ARMOR_TYPE, 0); + final float impactZ = unitType.getFieldAsFloat(PROJECTILE_IMPACT_Z, 0); + final CDefenseType defenseType = CDefenseType.parseDefenseType(unitType.getFieldAsString(DEFENSE_TYPE, 0)); + final float deathTime = unitType.getFieldAsFloat(DEATH_TIME, 0); + final int goldCost = unitType.getFieldAsInteger(GOLD_COST, 0); + final int lumberCost = unitType.getFieldAsInteger(LUMBER_COST, 0); + final int buildTime = unitType.getFieldAsInteger(BUILD_TIME, 0); + final int foodUsed = unitType.getFieldAsInteger(FOOD_USED, 0); + final int foodMade = unitType.getFieldAsInteger(FOOD_MADE, 0); + + final String unitsTrainedString = unitType.getFieldAsString(UNITS_TRAINED, 0); + final String[] unitsTrainedStringItems = unitsTrainedString.trim().split(","); + final List unitsTrained = new ArrayList<>(); + for (final String unitsTrainedStringItem : unitsTrainedStringItems) { + if (unitsTrainedStringItem.length() == 4) { + unitsTrained.add(War3ID.fromString(unitsTrainedStringItem)); + } + } + + final String researchesAvailableString = unitType.getFieldAsString(RESEARCHES_AVAILABLE, 0); + final String[] researchesAvailableStringItems = researchesAvailableString.trim().split(","); + final List researchesAvailable = new ArrayList<>(); + for (final String researchesAvailableStringItem : researchesAvailableStringItems) { + if (researchesAvailableStringItem.length() == 4) { + researchesAvailable.add(War3ID.fromString(researchesAvailableStringItem)); + } + } + + final String structuresBuiltString = unitType.getFieldAsString(STRUCTURES_BUILT, 0); + final String[] structuresBuiltStringItems = structuresBuiltString.split(","); + final List structuresBuilt = new ArrayList<>(); + for (final String structuresBuiltStringItem : structuresBuiltStringItems) { + if (structuresBuiltStringItem.length() == 4) { + structuresBuilt.add(War3ID.fromString(structuresBuiltStringItem)); + } + } + + final String[] heroAbilityListStringItems = heroAbilityListString.split(","); + final List heroAbilityList = new ArrayList<>(); + for (final String heroAbilityItem : heroAbilityListStringItems) { + if (heroAbilityItem.length() == 4) { + heroAbilityList.add(War3ID.fromString(heroAbilityItem)); + } + } + + final String requirementsString = unitType.getFieldAsString(REQUIRES, 0); + final String requirementsLevelsString = unitType.getFieldAsString(REQUIRES_AMOUNT, 0); + final String[] requirementsStringItems = requirementsString.split(","); + final String[] requirementsLevelsStringItems = requirementsLevelsString.split(","); + final List requirements = new ArrayList<>(); + for (int i = 0; i < requirementsStringItems.length; i++) { + final String item = requirementsStringItems[i]; + if (!item.isEmpty()) { + int level; + if (i < requirementsLevelsStringItems.length) { + if (requirementsLevelsStringItems[i].isEmpty()) { + level = 1; + } + else { + level = Integer.parseInt(requirementsLevelsStringItems[i]); + } + } + else if (requirementsLevelsStringItems.length > 0) { + final String requirementLevel = requirementsLevelsStringItems[requirementsLevelsStringItems.length + - 1]; + if (requirementLevel.isEmpty()) { + level = 1; + } + else { + level = Integer.parseInt(requirementLevel); + } + } + else { + level = 1; + } + requirements.add(new CUnitTypeRequirement(War3ID.fromString(item), level)); + } + } + + final EnumSet preventedPathingTypes = CBuildingPathingType + .parsePathingTypeListSet(unitType.getFieldAsString(PREVENT_PLACE, 0)); + final EnumSet requiredPathingTypes = CBuildingPathingType + .parsePathingTypeListSet(unitType.getFieldAsString(REQUIRE_PLACE, 0)); + + final String raceString = unitType.getFieldAsString(UNIT_RACE, 0); + final CUnitRace unitRace = CUnitRace.parseRace(raceString); + + final boolean hero = Character.isUpperCase(typeId.charAt(0)); + + final List heroProperNames = Arrays.asList(properNames.split(",")); + + unitTypeInstance = new CUnitType(unitName, legacyName, typeId, life, manaInitial, manaMaximum, speed, + defense, abilityList, isBldg, movementType, moveHeight, collisionSize, classifications, attacks, + armorType, raise, decay, defenseType, impactZ, buildingPathingPixelMap, deathTime, targetedAs, + acquisitionRange, minimumAttackRange, structuresBuilt, unitsTrained, researchesAvailable, unitRace, + goldCost, lumberCost, foodUsed, foodMade, buildTime, preventedPathingTypes, requiredPathingTypes, + propWindow, turnRate, requirements, unitLevel, hero, strength, strPlus, agility, agiPlus, + intelligence, intPlus, primaryAttribute, heroAbilityList, heroProperNames, properNamesCount, + canFlee); + this.unitIdToUnitType.put(typeId, unitTypeInstance); + this.jassLegacyNameToUnitId.put(legacyName, typeId); + } + return unitTypeInstance; + } + + private String getLegacyName(final MutableGameObject unitType) { + String legacyName; + if (unitType.isCustom()) { + legacyName = "custom_" + unitType.getAlias(); + } + else { + // ?? this might be correct here, not sure, legacy name is mostly only used + // for spawning hidden units in campaign secrets + legacyName = unitType.readSLKTag("name"); + } + return legacyName; + } + + private static int[] populateHeroStatTable(final int maxHeroLevel, final float statPerLevel) { + final int[] table = new int[maxHeroLevel]; + float sumBonusAtLevel = 0f; + for (int i = 0; i < table.length; i++) { + final float newSumBonusAtLevel = sumBonusAtLevel + statPerLevel; + if (i == 0) { + table[i] = (int) newSumBonusAtLevel; + } + else { + table[i] = (int) newSumBonusAtLevel - table[i - 1]; + } + sumBonusAtLevel = newSumBonusAtLevel; + } + return table; + } + + private CUnitAttack createAttack(final float animationBackswingPoint, final float animationDamagePoint, + final int areaOfEffectFullDamage, final int areaOfEffectMediumDamage, final int areaOfEffectSmallDamage, + final EnumSet areaOfEffectTargets, final CAttackType attackType, final float cooldownTime, + final int damageBase, final float damageFactorMedium, final float damageFactorSmall, + final float damageLossFactor, final int damageDice, final int damageSidesPerDie, + final float damageSpillDistance, final float damageSpillRadius, final int damageUpgradeAmount, + final int maximumNumberOfTargets, final float projectileArc, final String projectileArt, + final boolean projectileHomingEnabled, final int projectileSpeed, final int range, + final float rangeMotionBuffer, final boolean showUI, final EnumSet targetsAllowed, + final String weaponSound, final CWeaponType weaponType) { + final CUnitAttack attack; + switch (weaponType) { + case MISSILE: + attack = new CUnitAttackMissile(animationBackswingPoint, animationDamagePoint, attackType, cooldownTime, + damageBase, damageDice, damageSidesPerDie, damageUpgradeAmount, range, rangeMotionBuffer, showUI, + targetsAllowed, weaponSound, weaponType, projectileArc, projectileArt, projectileHomingEnabled, + projectileSpeed); + break; + case MBOUNCE: + attack = new CUnitAttackMissileBounce(animationBackswingPoint, animationDamagePoint, attackType, + cooldownTime, damageBase, damageDice, damageSidesPerDie, damageUpgradeAmount, range, + rangeMotionBuffer, showUI, targetsAllowed, weaponSound, weaponType, projectileArc, projectileArt, + projectileHomingEnabled, projectileSpeed, damageLossFactor, maximumNumberOfTargets, + areaOfEffectFullDamage, areaOfEffectTargets); + break; + case MSPLASH: + case ARTILLERY: + attack = new CUnitAttackMissileSplash(animationBackswingPoint, animationDamagePoint, attackType, + cooldownTime, damageBase, damageDice, damageSidesPerDie, damageUpgradeAmount, range, + rangeMotionBuffer, showUI, targetsAllowed, weaponSound, weaponType, projectileArc, projectileArt, + projectileHomingEnabled, projectileSpeed, areaOfEffectFullDamage, areaOfEffectMediumDamage, + areaOfEffectSmallDamage, areaOfEffectTargets, damageFactorMedium, damageFactorSmall); + break; + case MLINE: + case ALINE: + attack = new CUnitAttackMissileLine(animationBackswingPoint, animationDamagePoint, attackType, cooldownTime, + damageBase, damageDice, damageSidesPerDie, damageUpgradeAmount, range, rangeMotionBuffer, showUI, + targetsAllowed, weaponSound, weaponType, projectileArc, projectileArt, projectileHomingEnabled, + projectileSpeed, damageSpillDistance, damageSpillRadius); + break; + case INSTANT: + attack = new CUnitAttackInstant(animationBackswingPoint, animationDamagePoint, attackType, cooldownTime, + damageBase, damageDice, damageSidesPerDie, damageUpgradeAmount, range, rangeMotionBuffer, showUI, + targetsAllowed, weaponSound, weaponType, projectileArt); + break; + default: + case NORMAL: + attack = new CUnitAttackNormal(animationBackswingPoint, animationDamagePoint, attackType, cooldownTime, + damageBase, damageDice, damageSidesPerDie, damageUpgradeAmount, range, rangeMotionBuffer, showUI, + targetsAllowed, weaponSound, weaponType); + break; + } + return attack; + } + + public float getPropulsionWindow(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsFloat(PROPULSION_WINDOW, 0); + } + + public float getTurnRate(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsFloat(TURN_RATE, 0); + } + + public boolean isBuilding(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsBoolean(IS_BLDG, 0); + } + + public String getName(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getName(); + } + + public int getA1MinDamage(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsInteger(ATTACK1_DMG_BASE, 0) + + this.unitData.get(unitTypeId).getFieldAsInteger(ATTACK1_DMG_DICE, 0); + } + + public int getA1MaxDamage(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsInteger(ATTACK1_DMG_BASE, 0) + + (this.unitData.get(unitTypeId).getFieldAsInteger(ATTACK1_DMG_DICE, 0) + * this.unitData.get(unitTypeId).getFieldAsInteger(ATTACK1_DMG_SIDES_PER_DIE, 0)); + } + + public int getA2MinDamage(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsInteger(ATTACK2_DMG_BASE, 0) + + this.unitData.get(unitTypeId).getFieldAsInteger(ATTACK2_DMG_DICE, 0); + } + + public int getA2MaxDamage(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsInteger(ATTACK2_DMG_BASE, 0) + + (this.unitData.get(unitTypeId).getFieldAsInteger(ATTACK2_DMG_DICE, 0) + * this.unitData.get(unitTypeId).getFieldAsInteger(ATTACK2_DMG_SIDES_PER_DIE, 0)); + } + + public int getDefense(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsInteger(DEFENSE, 0); + } + + public int getA1ProjectileSpeed(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsInteger(ATTACK1_PROJECTILE_SPEED, 0); + } + + public float getA1ProjectileArc(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsFloat(ATTACK1_PROJECTILE_ARC, 0); + } + + public int getA2ProjectileSpeed(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsInteger(ATTACK2_PROJECTILE_SPEED, 0); + } + + public float getA2ProjectileArc(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsFloat(ATTACK2_PROJECTILE_ARC, 0); + } + + public String getA1MissileArt(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsString(ATTACK1_MISSILE_ART, 0); + } + + public String getA2MissileArt(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsString(ATTACK2_MISSILE_ART, 0); + } + + public float getA1Cooldown(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsFloat(ATTACK1_COOLDOWN, 0); + } + + public float getA2Cooldown(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsFloat(ATTACK2_COOLDOWN, 0); + } + + public float getProjectileLaunchX(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsFloat(PROJECTILE_LAUNCH_X, 0); + } + + public float getProjectileLaunchY(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsFloat(PROJECTILE_LAUNCH_Y, 0); + } + + public float getProjectileLaunchZ(final War3ID unitTypeId) { + return this.unitData.get(unitTypeId).getFieldAsFloat(PROJECTILE_LAUNCH_Z, 0); + } + + public CUnitType getUnitType(final War3ID rawcode) { + final CUnitType unitTypeInstance = this.unitIdToUnitType.get(rawcode); + if (unitTypeInstance != null) { + return unitTypeInstance; + } + final MutableGameObject unitType = this.unitData.get(rawcode); + if (unitType == null) { + return null; + } + final BufferedImage buildingPathingPixelMap = this.simulationRenderController + .getBuildingPathingPixelMap(rawcode); + return getUnitTypeInstance(rawcode, buildingPathingPixelMap, unitType); + } + + public CUnitType getUnitTypeByJassLegacyName(final String jassLegacyName) { + final War3ID typeId = this.jassLegacyNameToUnitId.get(jassLegacyName); + if (typeId == null) { + // VERY inefficient, but this is a crazy system anyway, they should not be using + // this! + System.err.println( + "We are doing a highly inefficient lookup for a non-cached unit type based on its legacy string ID that I am pretty sure is not used by modding community: " + + jassLegacyName); + for (final War3ID key : this.unitData.keySet()) { + final MutableGameObject mutableGameObject = this.unitData.get(key); + if (jassLegacyName.equals(getLegacyName(mutableGameObject))) { + return getUnitType(mutableGameObject.getAlias()); + } + } + } + return getUnitType(typeId); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CUnitRace.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CUnitRace.java new file mode 100644 index 0000000..57f77f8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/data/CUnitRace.java @@ -0,0 +1,32 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.data; + +import java.util.HashMap; +import java.util.Map; + +public enum CUnitRace { + HUMAN, + ORC, + UNDEAD, + NIGHTELF, + NAGA, + CREEPS, + DEMON, + CRITTERS, + OTHER; + + private static Map keyToRace = new HashMap<>(); + + static { + for (final CUnitRace race : CUnitRace.values()) { + keyToRace.put(race.name(), race); + } + } + + public static CUnitRace parseRace(final String raceString) { + final CUnitRace race = keyToRace.get(raceString.toUpperCase()); + if (race == null) { + return OTHER; + } + return race; + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/item/CItemTypeJass.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/item/CItemTypeJass.java new file mode 100644 index 0000000..bde3174 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/item/CItemTypeJass.java @@ -0,0 +1,15 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.item; + +public enum CItemTypeJass { + PERMANENT, + CHARGED, + POWERUP, + ARTIFACT, + PURCHASABLE, + CAMPAIGN, + MISCELLANEOUS, + UNKNOWN, + ANY; + + public static CItemTypeJass[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/COrder.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/COrder.java new file mode 100644 index 0000000..30706f8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/COrder.java @@ -0,0 +1,23 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.StringMsgAbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.StringMsgTargetCheckReceiver; + +public interface COrder { + int getAbilityHandleId(); + + int getOrderId(); + + CBehavior begin(final CSimulation game, CUnit caster); + + AbilityTarget getTarget(CSimulation game); + + boolean isQueued(); + + final StringMsgTargetCheckReceiver targetCheckReceiver = new StringMsgTargetCheckReceiver<>(); + final StringMsgAbilityActivationReceiver abilityActivationReceiver = new StringMsgAbilityActivationReceiver(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/COrderDropItemAtPoint.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/COrderDropItemAtPoint.java new file mode 100644 index 0000000..0084a25 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/COrderDropItemAtPoint.java @@ -0,0 +1,111 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.inventory.CAbilityInventory; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.StringMsgTargetCheckReceiver; + +public class COrderDropItemAtPoint implements COrder { + private final int abilityHandleId; + private final int orderId; + private final int itemHandleId; + private final AbilityPointTarget target; + private final boolean queued; + + public COrderDropItemAtPoint(final int abilityHandleId, final int orderId, final int itemHandleId, + final AbilityPointTarget target, final boolean queued) { + this.abilityHandleId = abilityHandleId; + this.orderId = orderId; + this.itemHandleId = itemHandleId; + this.target = target; + this.queued = queued; + } + + @Override + public int getAbilityHandleId() { + return this.abilityHandleId; + } + + @Override + public int getOrderId() { + return this.orderId; + } + + @Override + public AbilityPointTarget getTarget(final CSimulation game) { + return this.target; + } + + @Override + public boolean isQueued() { + return this.queued; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster) { + final CAbilityInventory ability = (CAbilityInventory) game.getAbility(this.abilityHandleId); + ability.checkCanUse(game, caster, this.orderId, this.abilityActivationReceiver.reset()); + if (this.abilityActivationReceiver.isUseOk()) { + final StringMsgTargetCheckReceiver targetReceiver = (StringMsgTargetCheckReceiver) targetCheckReceiver; + final CItem itemToDrop = (CItem) game.getWidget(this.itemHandleId); + return ability.beginDropItem(game, caster, this.orderId, itemToDrop, this.target); + } + else { + game.getCommandErrorListener(caster.getPlayerIndex()) + .showCommandError(this.abilityActivationReceiver.getMessage()); + return caster.pollNextOrderBehavior(game); + } + + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + this.abilityHandleId; + result = (prime * result) + this.itemHandleId; + result = (prime * result) + this.orderId; + result = (prime * result) + (this.queued ? 1231 : 1237); + result = (prime * result) + ((this.target == null) ? 0 : this.target.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final COrderDropItemAtPoint other = (COrderDropItemAtPoint) obj; + if (this.abilityHandleId != other.abilityHandleId) { + return false; + } + if (this.itemHandleId != other.itemHandleId) { + return false; + } + if (this.orderId != other.orderId) { + return false; + } + if (this.queued != other.queued) { + return false; + } + if (this.target == null) { + if (other.target != null) { + return false; + } + } + else if (!this.target.equals(other.target)) { + return false; + } + return true; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/COrderNoTarget.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/COrderNoTarget.java new file mode 100644 index 0000000..6b08cd9 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/COrderNoTarget.java @@ -0,0 +1,97 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.StringMsgTargetCheckReceiver; + +public class COrderNoTarget implements COrder { + private final int abilityHandleId; + private final int orderId; + private final boolean queued; + + public COrderNoTarget(final int abilityHandleId, final int orderId, final boolean queued) { + this.abilityHandleId = abilityHandleId; + this.orderId = orderId; + this.queued = queued; + } + + @Override + public int getAbilityHandleId() { + return this.abilityHandleId; + } + + @Override + public int getOrderId() { + return this.orderId; + } + + @Override + public boolean isQueued() { + return this.queued; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster) { + final CAbility ability = game.getAbility(this.abilityHandleId); + ability.checkCanUse(game, caster, this.orderId, this.abilityActivationReceiver.reset()); + if (this.abilityActivationReceiver.isUseOk()) { + final StringMsgTargetCheckReceiver targetReceiver = (StringMsgTargetCheckReceiver) targetCheckReceiver; + ability.checkCanTargetNoTarget(game, caster, this.orderId, targetReceiver); + if (targetReceiver.getMessage() == null) { + return ability.beginNoTarget(game, caster, this.orderId); + } + else { + game.getCommandErrorListener(caster.getPlayerIndex()).showCommandError(targetReceiver.getMessage()); + return caster.pollNextOrderBehavior(game); + } + } + else { + game.getCommandErrorListener(caster.getPlayerIndex()) + .showCommandError(this.abilityActivationReceiver.getMessage()); + return caster.pollNextOrderBehavior(game); + } + } + + @Override + public AbilityTarget getTarget(final CSimulation game) { + return null; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + this.abilityHandleId; + result = (prime * result) + this.orderId; + result = (prime * result) + (this.queued ? 1231 : 1237); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final COrderNoTarget other = (COrderNoTarget) obj; + if (this.abilityHandleId != other.abilityHandleId) { + return false; + } + if (this.orderId != other.orderId) { + return false; + } + if (this.queued != other.queued) { + return false; + } + return true; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/COrderTargetPoint.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/COrderTargetPoint.java new file mode 100644 index 0000000..89e25c8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/COrderTargetPoint.java @@ -0,0 +1,110 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.StringMsgTargetCheckReceiver; + +public class COrderTargetPoint implements COrder { + private final int abilityHandleId; + private final int orderId; + private final AbilityPointTarget target; + private final boolean queued; + + public COrderTargetPoint(final int abilityHandleId, final int orderId, final AbilityPointTarget target, + final boolean queued) { + this.abilityHandleId = abilityHandleId; + this.orderId = orderId; + this.target = target; + this.queued = queued; + } + + @Override + public int getAbilityHandleId() { + return this.abilityHandleId; + } + + @Override + public int getOrderId() { + return this.orderId; + } + + @Override + public AbilityPointTarget getTarget(final CSimulation game) { + return this.target; + } + + @Override + public boolean isQueued() { + return this.queued; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster) { + final CAbility ability = game.getAbility(this.abilityHandleId); + ability.checkCanUse(game, caster, this.orderId, this.abilityActivationReceiver.reset()); + if (this.abilityActivationReceiver.isUseOk()) { + final StringMsgTargetCheckReceiver targetReceiver = (StringMsgTargetCheckReceiver) targetCheckReceiver; + ability.checkCanTarget(game, caster, this.orderId, this.target, targetReceiver); + if (targetReceiver.getTarget() != null) { + return ability.begin(game, caster, this.orderId, this.target); + } + else { + game.getCommandErrorListener(caster.getPlayerIndex()).showCommandError(targetReceiver.getMessage()); + return caster.pollNextOrderBehavior(game); + } + } + else { + game.getCommandErrorListener(caster.getPlayerIndex()) + .showCommandError(this.abilityActivationReceiver.getMessage()); + return caster.pollNextOrderBehavior(game); + } + + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + this.abilityHandleId; + result = (prime * result) + this.orderId; + result = (prime * result) + (this.queued ? 1231 : 1237); + result = (prime * result) + ((this.target == null) ? 0 : this.target.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final COrderTargetPoint other = (COrderTargetPoint) obj; + if (this.abilityHandleId != other.abilityHandleId) { + return false; + } + if (this.orderId != other.orderId) { + return false; + } + if (this.queued != other.queued) { + return false; + } + if (this.target == null) { + if (other.target != null) { + return false; + } + } + else if (!this.target.equals(other.target)) { + return false; + } + return true; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/COrderTargetWidget.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/COrderTargetWidget.java new file mode 100644 index 0000000..0262d05 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/COrderTargetWidget.java @@ -0,0 +1,106 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehavior; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.StringMsgTargetCheckReceiver; + +public class COrderTargetWidget implements COrder { + private final int abilityHandleId; + private final int orderId; + private final int targetHandleId; + private final boolean queued; + + public COrderTargetWidget(final int abilityHandleId, final int orderId, final int targetHandleId, + final boolean queued) { + this.abilityHandleId = abilityHandleId; + this.orderId = orderId; + this.targetHandleId = targetHandleId; + this.queued = queued; + } + + @Override + public int getAbilityHandleId() { + return this.abilityHandleId; + } + + @Override + public int getOrderId() { + return this.orderId; + } + + @Override + public AbilityTarget getTarget(final CSimulation game) { + final CWidget target = game.getWidget(this.targetHandleId); + return target; + } + + @Override + public boolean isQueued() { + return this.queued; + } + + @Override + public CBehavior begin(final CSimulation game, final CUnit caster) { + final CAbility ability = game.getAbility(this.abilityHandleId); + ability.checkCanUse(game, caster, this.orderId, abilityActivationReceiver.reset()); + if (abilityActivationReceiver.isUseOk()) { + final CWidget target = game.getWidget(this.targetHandleId); + final StringMsgTargetCheckReceiver targetReceiver = (StringMsgTargetCheckReceiver) targetCheckReceiver; + ability.checkCanTarget(game, caster, this.orderId, target, targetReceiver); + if (targetReceiver.getTarget() != null) { + return ability.begin(game, caster, this.orderId, targetReceiver.getTarget()); + } + else { + game.getCommandErrorListener(caster.getPlayerIndex()).showCommandError(targetReceiver.getMessage()); + return caster.pollNextOrderBehavior(game); + } + } + else { + game.getCommandErrorListener(caster.getPlayerIndex()) + .showCommandError(this.abilityActivationReceiver.getMessage()); + return caster.pollNextOrderBehavior(game); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + this.abilityHandleId; + result = (prime * result) + this.orderId; + result = (prime * result) + (this.queued ? 1231 : 1237); + result = (prime * result) + this.targetHandleId; + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final COrderTargetWidget other = (COrderTargetWidget) obj; + if (this.abilityHandleId != other.abilityHandleId) { + return false; + } + if (this.orderId != other.orderId) { + return false; + } + if (this.queued != other.queued) { + return false; + } + if (this.targetHandleId != other.targetHandleId) { + return false; + } + return true; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/OrderIdUtils.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/OrderIdUtils.java new file mode 100644 index 0000000..90375e3 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/OrderIdUtils.java @@ -0,0 +1,38 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +public class OrderIdUtils { + private static Map orderIdToString = new HashMap<>(); + private static Map stringToOrderId = new HashMap<>(); + + static { + for (final Field field : OrderIds.class.getDeclaredFields()) { + final String name = field.getName(); + try { + final Object value = field.get(null); + if (value instanceof Integer) { + final Integer orderId = (Integer) value; + orderIdToString.put(orderId, name); + stringToOrderId.put(name, orderId); + } + } + catch (final IllegalArgumentException e) { + e.printStackTrace(); + } + catch (final IllegalAccessException e) { + e.printStackTrace(); + } + } + } + + public static int getOrderId(final String orderIdString) { + return stringToOrderId.get(orderIdString); + } + + public static String getStringFromOrderId(final Integer orderId) { + return orderIdToString.get(orderId); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/OrderIds.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/OrderIds.java new file mode 100644 index 0000000..ee9022d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/orders/OrderIds.java @@ -0,0 +1,457 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders; + +/** + * Thanks to the Wurst guys for this list of ids, taken from this link: + * https://github.com/wurstscript/WurstStdlib2/blob/master/wurst/_wurst/assets/Orders.wurst + * + * The original code ported to create this Java file is licensed under the + * Apache License; you can read more at the link above. + * + */ +public class OrderIds { + ;/** + * This is an order with no target that opens up the build menu of a unit that + * can build structures. + */ + ; + public static final int buildmenu = 851994; + /** + * 851976 (cancel): This is an order with no target that is like a click on a + * cancel button. We used to be able to catch cancel clicks with this id back + * then but this id doesn't seem to work any more. + */ + public static final int cancel = 851976; + /** + * An item targeted order that move the target item to a certain inventory slot + * of the ordered hero. + */ + public static final int itemdrag00 = 852002; + /** + * An item targeted order that move the target item to a certain inventory slot + * of the ordered hero. + */ + public static final int itemdrag01 = 852003; + /** + * An item targeted order that move the target item to a certain inventory slot + * of the ordered hero. + */ + public static final int itemdrag02 = 852004; + /** + * An item targeted order that move the target item to a certain inventory slot + * of the ordered hero. + */ + public static final int itemdrag03 = 852005; + /** + * An item targeted order that move the target item to a certain inventory slot + * of the ordered hero. + */ + public static final int itemdrag04 = 852006; + /** + * An item targeted order that move the target item to a certain inventory slot + * of the ordered hero. + */ + public static final int itemdrag05 = 852007; + /** + * An order that will make the ordered hero use the item in a certain inventory + * slot. If it's an order with no target or object or point targeted depends on + * the type of item. + */ + public static final int itemuse00 = 852008; + /** + * An order that will make the ordered hero use the item in a certain inventory + * slot. If it's an order with no target or object or point targeted depends on + * the type of item. + */ + public static final int itemuse01 = 852009; + /** + * An order that will make the ordered hero use the item in a certain inventory + * slot. If it's an order with no target or object or point targeted depends on + * the type of item. + */ + public static final int itemuse02 = 852010; + /** + * An order that will make the ordered hero use the item in a certain inventory + * slot. If it's an order with no target or object or point targeted depends on + * the type of item. + */ + public static final int itemuse03 = 852011; + /** + * An order that will make the ordered hero use the item in a certain inventory + * slot. If it's an order with no target or object or point targeted depends on + * the type of item. + */ + public static final int itemuse04 = 852012; + /** + * An order that will make the ordered hero use the item in a certain inventory + * slot. If it's an order with no target or object or point targeted depends on + * the type of item. + */ + public static final int itemuse05 = 852013; + /** + * Order for AIaa ability, which blizzard made for tome of attack, but never + * used it. But it can actually change caster's base attack! + */ + public static final int tomeOfAttack = 852259; + /** This is a point or object targeted order that is like a right click. */ + public static final int smart = 851971; + /** + * This is an order with no target that opens the skill menu of heroes. If it is + * issued for a normal unit with triggers it will black out the command card for + * this unit, the command card will revert to normal after reselecting the unit. + */ + public static final int skillmenu = 852000; + /** + * This order is issued to units that get stunned by a spell, for example War + * Stomp (AOws). This is probably a hold position + hold fire order. The ordered + * unit will be unable to move and attack. + */ + public static final int stunned = 851973; + public static final int wandOfIllusion = 852274; + public static final int absorb = 852529; + public static final int acidbomb = 852662; + public static final int acolyteharvest = 852185; + public static final int ambush = 852131; + public static final int ancestralspirit = 852490; + public static final int ancestralspirittarget = 852491; + public static final int animatedead = 852217; + public static final int antimagicshell = 852186; + public static final int attack = 851983; + public static final int attackground = 851984; + public static final int attackonce = 851985; + public static final int attributemodskill = 852576; + public static final int auraunholy = 852215; + public static final int auravampiric = 852216; + public static final int autodispel = 852132; + public static final int autodispeloff = 852134; + public static final int autodispelon = 852133; + public static final int autoentangle = 852505; + public static final int autoentangleinstant = 852506; + public static final int autoharvestgold = 852021; + public static final int autoharvestlumber = 852022; + public static final int avatar = 852086; + public static final int avengerform = 852531; + public static final int awaken = 852466; + public static final int banish = 852486; + public static final int barkskin = 852135; + public static final int barkskinoff = 852137; + public static final int barkskinon = 852136; + public static final int battleroar = 852599; + public static final int battlestations = 852099; + public static final int bearform = 852138; + public static final int berserk = 852100; + public static final int blackarrow = 852577; + public static final int blackarrowoff = 852579; + public static final int blackarrowon = 852578; + public static final int blight = 852187; + public static final int blink = 852525; + public static final int blizzard = 852089; + public static final int bloodlust = 852101; + public static final int bloodlustoff = 852103; + public static final int bloodluston = 852102; + public static final int board = 852043; + public static final int breathoffire = 852580; + public static final int breathoffrost = 852560; + public static final int build = 851994; + public static final int burrow = 852533; + public static final int cannibalize = 852188; + public static final int carrionscarabs = 852551; + public static final int carrionscarabsinstant = 852554; + public static final int carrionscarabsoff = 852553; + public static final int carrionscarabson = 852552; + public static final int carrionswarm = 852218; + public static final int chainlightning = 852119; + public static final int channel = 852600; + public static final int charm = 852581; + public static final int chemicalrage = 852663; + public static final int cloudoffog = 852473; + public static final int clusterrockets = 852652; + public static final int coldarrows = 852244; + public static final int coldarrowstarg = 852243; + public static final int controlmagic = 852474; + public static final int corporealform = 852493; + public static final int corrosivebreath = 852140; + public static final int coupleinstant = 852508; + public static final int coupletarget = 852507; + public static final int creepanimatedead = 852246; + public static final int creepdevour = 852247; + public static final int creepheal = 852248; + public static final int creephealoff = 852250; + public static final int creephealon = 852249; + public static final int creepthunderbolt = 852252; + public static final int creepthunderclap = 852253; + public static final int cripple = 852189; + public static final int curse = 852190; + public static final int curseoff = 852192; + public static final int curseon = 852191; + public static final int cyclone = 852144; + public static final int darkconversion = 852228; + public static final int darkportal = 852229; + public static final int darkritual = 852219; + public static final int darksummoning = 852220; + public static final int deathanddecay = 852221; + public static final int deathcoil = 852222; + public static final int deathpact = 852223; + public static final int decouple = 852509; + public static final int defend = 852055; + public static final int detectaoe = 852015; + public static final int detonate = 852145; + public static final int devour = 852104; + public static final int devourmagic = 852536; + public static final int disassociate = 852240; + public static final int disenchant = 852495; + public static final int dismount = 852470; + public static final int dispel = 852057; + public static final int divineshield = 852090; + public static final int doom = 852583; + public static final int drain = 852487; + public static final int dreadlordinferno = 852224; + public static final int dropitem = 852001; + public static final int drunkenhaze = 852585; + public static final int earthquake = 852121; + public static final int eattree = 852146; + public static final int elementalfury = 852586; + public static final int ensnare = 852106; + public static final int ensnareoff = 852108; + public static final int ensnareon = 852107; + public static final int entangle = 852147; + public static final int entangleinstant = 852148; + public static final int entanglingroots = 852171; + public static final int etherealform = 852496; + public static final int evileye = 852105; + public static final int faeriefire = 852149; + public static final int faeriefireoff = 852151; + public static final int faeriefireon = 852150; + public static final int fanofknives = 852526; + public static final int farsight = 852122; + public static final int fingerofdeath = 852230; + public static final int firebolt = 852231; + public static final int flamestrike = 852488; + public static final int flamingarrows = 852174; + public static final int flamingarrowstarg = 852173; + public static final int flamingattack = 852540; + public static final int flamingattacktarg = 852539; + public static final int flare = 852060; + public static final int forceboard = 852044; + public static final int forceofnature = 852176; + public static final int forkedlightning = 852587; + public static final int freezingbreath = 852195; + public static final int frenzy = 852561; + public static final int frenzyoff = 852563; + public static final int frenzyon = 852562; + public static final int frostarmor = 852225; + public static final int frostarmoroff = 852459; + public static final int frostarmoron = 852458; + public static final int frostnova = 852226; + public static final int getitem = 851981; + public static final int gold2lumber = 852233; + public static final int grabtree = 852511; + public static final int harvest = 852018; + public static final int heal = 852063; + public static final int healingspray = 852664; + public static final int healingward = 852109; + public static final int healingwave = 852501; + public static final int healoff = 852065; + public static final int healon = 852064; + public static final int hex = 852502; + public static final int holdposition = 851993; + public static final int holybolt = 852092; + public static final int howlofterror = 852588; + public static final int humanbuild = 851995; + public static final int immolation = 852177; + public static final int impale = 852555; + public static final int incineratearrow = 852670; + public static final int incineratearrowoff = 852672; + public static final int incineratearrowon = 852671; + public static final int inferno = 852232; + public static final int innerfire = 852066; + public static final int innerfireoff = 852068; + public static final int innerfireon = 852067; + public static final int instant = 852200; + public static final int invisibility = 852069; + public static final int lavamonster = 852667; + public static final int lightningshield = 852110; + public static final int load = 852046; + public static final int loadarcher = 852142; + public static final int loadcorpse = 852050; + public static final int loadcorpseinstant = 852053; + public static final int locustswarm = 852556; + public static final int lumber2gold = 852234; + public static final int magicdefense = 852478; + public static final int magicleash = 852480; + public static final int magicundefense = 852479; + public static final int manaburn = 852179; + public static final int manaflareoff = 852513; + public static final int manaflareon = 852512; + public static final int manashieldoff = 852590; + public static final int manashieldon = 852589; + public static final int massteleport = 852093; + public static final int mechanicalcritter = 852564; + public static final int metamorphosis = 852180; + public static final int militia = 852072; + public static final int militiaconvert = 852071; + public static final int militiaoff = 852073; + public static final int militiaunconvert = 852651; + public static final int mindrot = 852565; + public static final int mirrorimage = 852123; + public static final int monsoon = 852591; + public static final int mount = 852469; + public static final int mounthippogryph = 852143; + public static final int move = 851986; + public static final int moveAI = 851988; + public static final int nagabuild = 852467; + public static final int neutraldetectaoe = 852023; + public static final int neutralinteract = 852566; + public static final int neutralspell = 852630; + public static final int nightelfbuild = 851997; + public static final int orcbuild = 851996; + public static final int parasite = 852601; + public static final int parasiteoff = 852603; + public static final int parasiteon = 852602; + public static final int patrol = 851990; + public static final int phaseshift = 852514; + public static final int phaseshiftinstant = 852517; + public static final int phaseshiftoff = 852516; + public static final int phaseshifton = 852515; + public static final int phoenixfire = 852481; + public static final int phoenixmorph = 852482; + public static final int poisonarrows = 852255; + public static final int poisonarrowstarg = 852254; + public static final int polymorph = 852074; + public static final int possession = 852196; + public static final int preservation = 852568; + public static final int purge = 852111; + public static final int rainofchaos = 852237; + public static final int rainoffire = 852238; + public static final int raisedead = 852197; + public static final int raisedeadoff = 852199; + public static final int raisedeadon = 852198; + public static final int ravenform = 852155; + public static final int recharge = 852157; + public static final int rechargeoff = 852159; + public static final int rechargeon = 852158; + public static final int rejuvination = 852160; + public static final int renew = 852161; + public static final int renewoff = 852163; + public static final int renewon = 852162; + public static final int repair = 852024; + public static final int repairoff = 852026; + public static final int repairon = 852025; + public static final int replenish = 852542; + public static final int replenishlife = 852545; + public static final int replenishlifeoff = 852547; + public static final int replenishlifeon = 852546; + public static final int replenishmana = 852548; + public static final int replenishmanaoff = 852550; + public static final int replenishmanaon = 852549; + public static final int replenishoff = 852544; + public static final int replenishon = 852543; + public static final int request_hero = 852239; + public static final int requestsacrifice = 852201; + public static final int restoration = 852202; + public static final int restorationoff = 852204; + public static final int restorationon = 852203; + public static final int resumebuild = 851999; + public static final int resumeharvesting = 852017; + public static final int resurrection = 852094; + public static final int returnresources = 852020; + public static final int revenge = 852241; + public static final int revive = 852039; + public static final int roar = 852164; + public static final int robogoblin = 852656; + public static final int root = 852165; + public static final int sacrifice = 852205; + public static final int sanctuary = 852569; + public static final int scout = 852181; + public static final int selfdestruct = 852040; + public static final int selfdestructoff = 852042; + public static final int selfdestructon = 852041; + public static final int sentinel = 852182; + public static final int setrally = 851980; + public static final int shadowsight = 852570; + public static final int shadowstrike = 852527; + public static final int shockwave = 852125; + public static final int silence = 852592; + public static final int sleep = 852227; + public static final int slow = 852075; + public static final int slowoff = 852077; + public static final int slowon = 852076; + public static final int soulburn = 852668; + public static final int soulpreservation = 852242; + public static final int spellshield = 852571; + public static final int spellshieldaoe = 852572; + public static final int spellsteal = 852483; + public static final int spellstealoff = 852485; + public static final int spellstealon = 852484; + public static final int spies = 852235; + public static final int spiritlink = 852499; + public static final int spiritofvengeance = 852528; + public static final int spirittroll = 852573; + public static final int spiritwolf = 852126; + public static final int stampede = 852593; + public static final int standdown = 852113; + public static final int starfall = 852183; + public static final int stasistrap = 852114; + public static final int steal = 852574; + public static final int stomp = 852127; + public static final int stoneform = 852206; + public static final int stop = 851972; + public static final int submerge = 852604; + public static final int summonfactory = 852658; + public static final int summongrizzly = 852594; + public static final int summonphoenix = 852489; + public static final int summonquillbeast = 852595; + public static final int summonwareagle = 852596; + public static final int tankdroppilot = 852079; + public static final int tankloadpilot = 852080; + public static final int tankpilot = 852081; + public static final int taunt = 852520; + public static final int thunderbolt = 852095; + public static final int thunderclap = 852096; + public static final int tornado = 852597; + public static final int townbelloff = 852083; + public static final int townbellon = 852082; + public static final int tranquility = 852184; + public static final int transmute = 852665; + public static final int unavatar = 852087; + public static final int unavengerform = 852532; + public static final int unbearform = 852139; + public static final int unburrow = 852534; + public static final int uncoldarrows = 852245; + public static final int uncorporealform = 852494; + public static final int undeadbuild = 851998; + public static final int undefend = 852056; + public static final int undivineshield = 852091; + public static final int unetherealform = 852497; + public static final int unflamingarrows = 852175; + public static final int unflamingattack = 852541; + public static final int unholyfrenzy = 852209; + public static final int unimmolation = 852178; + public static final int unload = 852047; + public static final int unloadall = 852048; + public static final int unloadallcorpses = 852054; + public static final int unloadallinstant = 852049; + public static final int unpoisonarrows = 852256; + public static final int unravenform = 852156; + public static final int unrobogoblin = 852657; + public static final int unroot = 852166; + public static final int unstableconcoction = 852500; + public static final int unstoneform = 852207; + public static final int unsubmerge = 852605; + public static final int unsummon = 852210; + public static final int unwindwalk = 852130; + public static final int vengeance = 852521; + public static final int vengeanceinstant = 852524; + public static final int vengeanceoff = 852523; + public static final int vengeanceon = 852522; + public static final int volcano = 852669; + public static final int voodoo = 852503; + public static final int ward = 852504; + public static final int waterelemental = 852097; + public static final int wateryminion = 852598; + public static final int web = 852211; + public static final int weboff = 852213; + public static final int webon = 852212; + public static final int whirlwind = 852128; + public static final int windwalk = 852129; + public static final int wispharvest = 852214; +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/pathing/CBuildingPathingType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/pathing/CBuildingPathingType.java new file mode 100644 index 0000000..b64c72e --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/pathing/CBuildingPathingType.java @@ -0,0 +1,30 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.pathing; + +import java.util.EnumSet; + +public enum CBuildingPathingType { + BLIGHTED, + UNBUILDABLE, + UNFLYABLE, + UNWALKABLE, + UNAMPH, + UNFLOAT; + + public static CBuildingPathingType parsePathingType(final String typeString) { + if ("_".equals(typeString) || "".equals(typeString)) { + return null; + } + return valueOf(typeString.toUpperCase()); + } + + public static EnumSet parsePathingTypeListSet(final String pathingListString) { + final EnumSet types = EnumSet.noneOf(CBuildingPathingType.class); + for (final String type : pathingListString.split(",")) { + final CBuildingPathingType parsedType = parsePathingType(type); + if (parsedType != null) { + types.add(parsedType); + } + } + return types; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/pathing/CPathfindingProcessor.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/pathing/CPathfindingProcessor.java new file mode 100644 index 0000000..14913b5 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/pathing/CPathfindingProcessor.java @@ -0,0 +1,482 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.pathing; + +import java.awt.geom.Point2D; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.PriorityQueue; + +import com.badlogic.gdx.math.Rectangle; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.MovementType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWorldCollision; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorMove; + +public class CPathfindingProcessor { + private static final Rectangle tempRect = new Rectangle(); + private final PathingGrid pathingGrid; + private final CWorldCollision worldCollision; + private final LinkedList moveQueue = new LinkedList<>(); + // things with modified state per current job: + private final Node[][] nodes; + private final Node[][] cornerNodes; + private final Node[] goalSet = new Node[4]; + private int goals = 0; + private int pathfindJobId = 0; + private int totalIterations = 0; + private int totalJobLoops = 0; + + public CPathfindingProcessor(final PathingGrid pathingGrid, final CWorldCollision worldCollision) { + this.pathingGrid = pathingGrid; + this.worldCollision = worldCollision; + this.nodes = new Node[pathingGrid.getHeight()][pathingGrid.getWidth()]; + this.cornerNodes = new Node[pathingGrid.getHeight() + 1][pathingGrid.getWidth() + 1]; + for (int i = 0; i < this.nodes.length; i++) { + for (int j = 0; j < this.nodes[i].length; j++) { + this.nodes[i][j] = new Node(new Point2D.Float(pathingGrid.getWorldX(j), pathingGrid.getWorldY(i))); + } + } + for (int i = 0; i < this.cornerNodes.length; i++) { + for (int j = 0; j < this.cornerNodes[i].length; j++) { + this.cornerNodes[i][j] = new Node( + new Point2D.Float(pathingGrid.getWorldXFromCorner(j), pathingGrid.getWorldYFromCorner(i))); + } + } + } + + /** + * Finds the path to a point using a naive, slow, and unoptimized algorithm. + * Does not have optimizations yet, do this for a bunch of units and it will + * probably lag like a walrus. The implementation here was created by reading + * the wikipedia article on A* to jog my memory from data structures class back + * in college, and is meant only as a first draft to get things working. + * + * @param collisionSize + * + * + * @param start + * @param goal + * @param playerIndex + * @param queueItem + * @return + */ + public void findNaiveSlowPath(final CUnit ignoreIntersectionsWithThisUnit, + final CUnit ignoreIntersectionsWithThisSecondUnit, final float startX, final float startY, + final Point2D.Float goal, final PathingGrid.MovementType movementType, final float collisionSize, + final boolean allowSmoothing, final CBehaviorMove queueItem) { + this.moveQueue.offer(new PathfindingJob(ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, + startX, startY, goal, movementType, collisionSize, allowSmoothing, queueItem)); + } + + public void removeFromPathfindingQueue(final CBehaviorMove behaviorMove) { + // TODO because of silly java things, this remove is O(N) for now, + // we could do some refactors to make it O(1) but do we care? + final Iterator iterator = this.moveQueue.iterator(); + while (iterator.hasNext()) { + final PathfindingJob job = iterator.next(); + if (job.queueItem == behaviorMove) { + iterator.remove(); + } + } + + } + + private boolean pathableBetween(final CUnit ignoreIntersectionsWithThisUnit, + final CUnit ignoreIntersectionsWithThisSecondUnit, final float startX, final float startY, + final PathingGrid.MovementType movementType, final float collisionSize, final float x, final float y) { + return this.pathingGrid.isPathable(x, y, movementType, collisionSize) + && this.pathingGrid.isPathable(startX, y, movementType, collisionSize) + && this.pathingGrid.isPathable(x, startY, movementType, collisionSize) + && isPathableDynamically(x, y, ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, + movementType) + && isPathableDynamically(x, startY, ignoreIntersectionsWithThisUnit, + ignoreIntersectionsWithThisSecondUnit, movementType) + && isPathableDynamically(startX, y, ignoreIntersectionsWithThisUnit, + ignoreIntersectionsWithThisSecondUnit, movementType); + } + + private boolean isPathableDynamically(final float x, final float y, final CUnit ignoreIntersectionsWithThisUnit, + final CUnit ignoreIntersectionsWithThisSecondUnit, final PathingGrid.MovementType movementType) { + return !this.worldCollision.intersectsAnythingOtherThan(tempRect.setCenter(x, y), + ignoreIntersectionsWithThisUnit, ignoreIntersectionsWithThisSecondUnit, movementType); + } + + public static boolean isCollisionSizeBetterSuitedForCorners(final float collisionSize) { + return (((2 * (int) collisionSize) / 32) % 2) == 1; + } + + public double f(final Node n) { + return n.g + h(n); + } + + public double g(final Node n) { + return n.g; + } + + private boolean isGoal(final Node n) { + for (int i = 0; i < this.goals; i++) { + if (n == this.goalSet[i]) { + return true; + } + } + return false; + } + + public float h(final Node n) { + float bestDistance = 0; + for (int i = 0; i < this.goals; i++) { + final float possibleDistance = (float) n.point.distance(this.goalSet[i].point); + if (possibleDistance > bestDistance) { + bestDistance = possibleDistance; // always overestimate + } + } + return bestDistance; + } + + public static final class Node { + public Direction cameFromDirection; + private final Point2D.Float point; + private double f; + private double g; + private Node cameFrom; + private int pathfindJobId; + + private Node(final Point2D.Float point) { + this.point = point; + } + + private void touch(final int pathfindJobId) { + if (pathfindJobId != this.pathfindJobId) { + this.g = Float.POSITIVE_INFINITY; + this.f = Float.POSITIVE_INFINITY; + this.cameFrom = null; + this.cameFromDirection = null; + this.pathfindJobId = pathfindJobId; + } + } + } + + private static enum Direction { + NORTH_WEST(-1, 1), + NORTH(0, 1), + NORTH_EAST(1, 1), + EAST(1, 0), + SOUTH_EAST(1, -1), + SOUTH(0, -1), + SOUTH_WEST(-1, -1), + WEST(-1, 0); + + public static final Direction[] VALUES = values(); + + private final int xOffset; + private final int yOffset; + private final double length; + + private Direction(final int xOffset, final int yOffset) { + this.xOffset = xOffset; + this.yOffset = yOffset; + final double sqrt = Math.sqrt((xOffset * xOffset) + (yOffset * yOffset)); + this.length = sqrt; + } + } + + public static interface GridMapping { + int getX(PathingGrid grid, float worldX); + + int getY(PathingGrid grid, float worldY); + + public static final GridMapping CELLS = new GridMapping() { + @Override + public int getX(final PathingGrid grid, final float worldX) { + return grid.getCellX(worldX); + } + + @Override + public int getY(final PathingGrid grid, final float worldY) { + return grid.getCellY(worldY); + } + + }; + + public static final GridMapping CORNERS = new GridMapping() { + @Override + public int getX(final PathingGrid grid, final float worldX) { + return grid.getCornerX(worldX); + } + + @Override + public int getY(final PathingGrid grid, final float worldY) { + return grid.getCornerY(worldY); + } + + }; + } + + public void update(final CSimulation simulation) { + int workIterations = 0; + JobsLoop: while (!this.moveQueue.isEmpty()) { + this.totalJobLoops++; + final PathfindingJob job = this.moveQueue.peek(); + if (!job.jobStarted) { + this.pathfindJobId++; + this.totalIterations = 0; + this.totalJobLoops = 0; + job.jobStarted = true; + System.out.println("starting job with smoothing=" + job.allowSmoothing); + workIterations += 5; // setup of job predicted cost + job.goalX = job.goal.x; + job.goalY = job.goal.y; + job.weightForHittingWalls = 1E9f; + if (!this.pathingGrid.isPathable(job.goalX, job.goalY, job.movementType, job.collisionSize) + || !isPathableDynamically(job.goalX, job.goalY, job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, job.movementType)) { + job.weightForHittingWalls = 5E2f; + } + System.out.println("beginning findNaiveSlowPath for " + job.startX + "," + job.startY + "," + job.goalX + + "," + job.goalY); + if ((job.startX == job.goalX) && (job.startY == job.goalY)) { + job.queueItem.pathFound(Collections.emptyList(), simulation); + this.moveQueue.poll(); + continue JobsLoop; + } + tempRect.set(0, 0, job.collisionSize * 2, job.collisionSize * 2); + if (isCollisionSizeBetterSuitedForCorners(job.collisionSize)) { + job.searchGraph = this.cornerNodes; + job.gridMapping = GridMapping.CORNERS; + System.out.println("using corners"); + } + else { + job.searchGraph = this.nodes; + job.gridMapping = GridMapping.CELLS; + System.out.println("using cells"); + } + final int goalCellY = job.gridMapping.getY(this.pathingGrid, job.goalY); + final int goalCellX = job.gridMapping.getX(this.pathingGrid, job.goalX); + final Node mostLikelyGoal = job.searchGraph[goalCellY][goalCellX]; + mostLikelyGoal.touch(this.pathfindJobId); + final double bestGoalDistance = mostLikelyGoal.point.distance(job.goalX, job.goalY); + Arrays.fill(this.goalSet, null); + this.goals = 0; + for (int i = goalCellX - 1; i <= (goalCellX + 1); i++) { + for (int j = goalCellY - 1; j <= (goalCellY + 1); j++) { + final Node possibleGoal = job.searchGraph[j][i]; + possibleGoal.touch(this.pathfindJobId); + if (possibleGoal.point.distance(job.goalX, job.goalY) <= bestGoalDistance) { + this.goalSet[this.goals++] = possibleGoal; + } + } + } + final int startGridY = job.gridMapping.getY(this.pathingGrid, job.startY); + final int startGridX = job.gridMapping.getX(this.pathingGrid, job.startX); + job.openSet = new PriorityQueue<>(new Comparator() { + @Override + public int compare(final Node a, final Node b) { + return Double.compare(f(a), f(b)); + } + }); + + job.start = job.searchGraph[startGridY][startGridX]; + job.start.touch(this.pathfindJobId); + if (job.startX > job.start.point.x) { + job.startGridMinX = startGridX; + job.startGridMaxX = startGridX + 1; + } + else if (job.startX < job.start.point.x) { + job.startGridMinX = startGridX - 1; + job.startGridMaxX = startGridX; + } + else { + job.startGridMinX = startGridX; + job.startGridMaxX = startGridX; + } + if (job.startY > job.start.point.y) { + job.startGridMinY = startGridY; + job.startGridMaxY = startGridY + 1; + } + else if (job.startY < job.start.point.y) { + job.startGridMinY = startGridY - 1; + job.startGridMaxY = startGridY; + } + else { + job.startGridMinY = startGridY; + job.startGridMaxY = startGridY; + } + for (int cellX = job.startGridMinX; cellX <= job.startGridMaxX; cellX++) { + for (int cellY = job.startGridMinY; cellY <= job.startGridMaxY; cellY++) { + if ((cellX >= 0) && (cellX < this.pathingGrid.getWidth()) && (cellY >= 0) + && (cellY < this.pathingGrid.getHeight())) { + final Node possibleNode = job.searchGraph[cellY][cellX]; + possibleNode.touch(this.pathfindJobId); + final float x = possibleNode.point.x; + final float y = possibleNode.point.y; + if (pathableBetween(job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, job.startX, job.startY, job.movementType, + job.collisionSize, x, y)) { + + final double tentativeScore = possibleNode.point.distance(job.startX, job.startY); + possibleNode.g = tentativeScore; + possibleNode.f = tentativeScore + h(possibleNode); + job.openSet.add(possibleNode); + + } + else { + final double tentativeScore = job.weightForHittingWalls; + possibleNode.g = tentativeScore; + possibleNode.f = tentativeScore + h(possibleNode); + job.openSet.add(possibleNode); + + } + } + } + } + } + + while (!job.openSet.isEmpty()) { + Node current = job.openSet.poll(); + current.touch(this.pathfindJobId); + if (isGoal(current)) { + final LinkedList totalPath = new LinkedList<>(); + Direction lastCameFromDirection = null; + + if ((current.cameFrom != null) + && pathableBetween(job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, current.point.x, current.point.y, + job.movementType, job.collisionSize, job.goalX, job.goalY) + && pathableBetween(job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, current.cameFrom.point.x, + current.cameFrom.point.y, job.movementType, job.collisionSize, current.point.x, + current.point.y) + && pathableBetween(job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, current.cameFrom.point.x, + current.cameFrom.point.y, job.movementType, job.collisionSize, job.goalX, job.goalY) + && job.allowSmoothing) { + // do some basic smoothing to walk straight to the goal if it is not obstructed, + // skipping the last grid location + totalPath.addFirst(job.goal); + current = current.cameFrom; + } + else { + totalPath.addFirst(job.goal); + totalPath.addFirst(current.point); + } + lastCameFromDirection = current.cameFromDirection; + Node lastNode = null; + while (current.cameFrom != null) { + lastNode = current; + current = current.cameFrom; + if ((lastCameFromDirection == null) || (current.cameFromDirection != lastCameFromDirection) + || (current.cameFromDirection == null)) { + if ((current.cameFromDirection != null) || (lastNode == null) + || !pathableBetween(job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, job.startX, job.startY, + job.movementType, job.collisionSize, current.point.x, current.point.y) + || !pathableBetween(job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, current.point.x, current.point.y, + job.movementType, job.collisionSize, lastNode.point.x, lastNode.point.y) + || !pathableBetween(job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, job.startX, job.startY, + job.movementType, job.collisionSize, lastNode.point.x, lastNode.point.y) + || !job.allowSmoothing) { + // Add the point if it's not the first one, or if we can only complete + // the journey by specifically walking to the first one + totalPath.addFirst(current.point); + lastCameFromDirection = current.cameFromDirection; + } + } + } + job.queueItem.pathFound(totalPath, simulation); + this.moveQueue.poll(); + System.out.println("Task " + this.pathfindJobId + " took " + this.totalIterations + + " iterations and " + this.totalJobLoops + " job loops!"); + continue JobsLoop; + } + + for (final Direction direction : Direction.VALUES) { + final float x = current.point.x + (direction.xOffset * 32); + final float y = current.point.y + (direction.yOffset * 32); + if (this.pathingGrid.contains(x, y)) { + double turnCost; + if ((current.cameFromDirection != null) && (direction != current.cameFromDirection)) { + turnCost = 0.25; + } + else { + turnCost = 0; + } + double tentativeScore = current.g + ((direction.length + turnCost) * 32); + if (!pathableBetween(job.ignoreIntersectionsWithThisUnit, + job.ignoreIntersectionsWithThisSecondUnit, current.point.x, current.point.y, + job.movementType, job.collisionSize, x, y)) { + tentativeScore += (direction.length) * job.weightForHittingWalls; + } + final Node neighbor = job.searchGraph[job.gridMapping.getY(this.pathingGrid, y)][job.gridMapping + .getX(this.pathingGrid, x)]; + neighbor.touch(this.pathfindJobId); + if (tentativeScore < neighbor.g) { + neighbor.cameFrom = current; + neighbor.cameFromDirection = direction; + neighbor.g = tentativeScore; + neighbor.f = tentativeScore + h(neighbor); + if (!job.openSet.contains(neighbor)) { + job.openSet.add(neighbor); + } + } + } + } + workIterations++; + this.totalIterations++; + if (workIterations >= 7500) { + // breaking jobs loop will implicitly exit without calling pathFound() below + break JobsLoop; + } + } + job.queueItem.pathFound(Collections.emptyList(), simulation); + this.moveQueue.poll(); + System.out.println("Task " + this.pathfindJobId + " took " + this.totalIterations + " iterations and " + + this.totalJobLoops + " job loops!"); + } + } + + public static final class PathfindingJob { + private final CUnit ignoreIntersectionsWithThisUnit; + private final CUnit ignoreIntersectionsWithThisSecondUnit; + private final float startX; + private final float startY; + private final Point2D.Float goal; + private final MovementType movementType; + private final float collisionSize; + private final boolean allowSmoothing; + private final CBehaviorMove queueItem; + private boolean jobStarted; + public float goalY; + public float goalX; + public float weightForHittingWalls; + Node[][] searchGraph; + GridMapping gridMapping; + PriorityQueue openSet; + Node start; + int startGridMinX; + int startGridMinY; + int startGridMaxX; + int startGridMaxY; + + public PathfindingJob(final CUnit ignoreIntersectionsWithThisUnit, + final CUnit ignoreIntersectionsWithThisSecondUnit, final float startX, final float startY, + final Point2D.Float goal, final PathingGrid.MovementType movementType, final float collisionSize, + final boolean allowSmoothing, final CBehaviorMove queueItem) { + this.ignoreIntersectionsWithThisUnit = ignoreIntersectionsWithThisUnit; + this.ignoreIntersectionsWithThisSecondUnit = ignoreIntersectionsWithThisSecondUnit; + this.startX = startX; + this.startY = startY; + this.goal = goal; + this.movementType = movementType; + this.collisionSize = collisionSize; + this.allowSmoothing = allowSmoothing; + this.queueItem = queueItem; + this.jobStarted = false; + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CAllianceType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CAllianceType.java new file mode 100644 index 0000000..ea1f28a --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CAllianceType.java @@ -0,0 +1,16 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +public enum CAllianceType { + PASSIVE, + HELP_REQUEST, + HELP_RESPONSE, + SHARED_XP, + SHARED_SPELLS, + SHARED_VISION, + SHARED_CONTROL, + SHARED_ADVANCED_CONTROL, + RESCUABLE, + SHARED_VISION_FORCED; + + public static CAllianceType[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CMapControl.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CMapControl.java new file mode 100644 index 0000000..423e458 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CMapControl.java @@ -0,0 +1,12 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +public enum CMapControl { + USER, + COMPUTER, + RESCUABLE, + NEUTRAL, + CREEP, + NONE; + + public static CMapControl[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CMapFlag.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CMapFlag.java new file mode 100644 index 0000000..f001bc8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CMapFlag.java @@ -0,0 +1,44 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +public enum CMapFlag { + MAP_FOG_HIDE_TERRAIN, + MAP_FOG_MAP_EXPLORED, + MAP_FOG_ALWAYS_VISIBLE, + + MAP_USE_HANDICAPS, + MAP_OBSERVERS, + MAP_OBSERVERS_ON_DEATH, + + MAP_FIXED_COLORS, + + MAP_LOCK_RESOURCE_TRADING, + MAP_RESOURCE_TRADING_ALLIES_ONLY, + + MAP_LOCK_ALLIANCE_CHANGES, + MAP_ALLIANCE_CHANGES_HIDDEN, + + MAP_CHEATS, + MAP_CHEATS_HIDDEN, + + MAP_LOCK_SPEED, + MAP_LOCK_RANDOM_SEED, + MAP_SHARED_ADVANCED_CONTROL, + MAP_RANDOM_HERO, + MAP_RANDOM_RACES, + MAP_RELOADED; + + public static CMapFlag[] VALUES = values(); + + public static CMapFlag getById(final int id) { + for (final CMapFlag type : VALUES) { + if ((type.getId()) == id) { + return type; + } + } + return null; + } + + public int getId() { + return 1 << ordinal(); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CMapPlacement.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CMapPlacement.java new file mode 100644 index 0000000..29eb00e --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CMapPlacement.java @@ -0,0 +1,10 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +public enum CMapPlacement { + RANDOM, + FIXED, + USE_MAP_SETTINGS, + TEAMS_TOGETHER; + + public static CMapPlacement[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayer.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayer.java new file mode 100644 index 0000000..956a396 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayer.java @@ -0,0 +1,140 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +import java.util.HashMap; +import java.util.Map; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CPlayerStateListener; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CPlayerStateListener.CPlayerStateNotifier; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.config.CBasePlayer; + +public class CPlayer extends CBasePlayer { + private final CRace race; + private final float[] startLocation; + private int gold = 500; + private int lumber = 150; + private int foodCap; + private int foodUsed; + private final Map rawcodeToTechtreeUnlocked = new HashMap<>(); + + // if you use triggers for this then the transient tag here becomes really + // questionable -- it already was -- but I meant for those to inform us + // which fields shouldn't be persisted if we do game state save later + private transient CPlayerStateNotifier stateNotifier = new CPlayerStateNotifier(); + + public CPlayer(final CRace race, final float[] startLocation, final CBasePlayer configPlayer) { + super(configPlayer); + this.race = race; + this.startLocation = startLocation; + } + + public void setAlliance(final CPlayer other, final CAllianceType alliance, final boolean flag) { + setAlliance(other.getId(), alliance, flag); + } + + public CRace getRace() { + return this.race; + } + + public int getGold() { + return this.gold; + } + + public int getLumber() { + return this.lumber; + } + + public int getFoodCap() { + return this.foodCap; + } + + public int getFoodUsed() { + return this.foodUsed; + } + + public float[] getStartLocation() { + return this.startLocation; + } + + public void setGold(final int gold) { + this.gold = gold; + this.stateNotifier.goldChanged(); + } + + public void setLumber(final int lumber) { + this.lumber = lumber; + this.stateNotifier.lumberChanged(); + } + + public void setFoodCap(final int foodCap) { + this.foodCap = foodCap; + this.stateNotifier.foodChanged(); + } + + public void setFoodUsed(final int foodUsed) { + this.foodUsed = foodUsed; + this.stateNotifier.foodChanged(); + } + + public int getTechtreeUnlocked(final War3ID rawcode) { + final Integer techtreeUnlocked = this.rawcodeToTechtreeUnlocked.get(rawcode); + if (techtreeUnlocked == null) { + return 0; + } + return techtreeUnlocked; + } + + public void addTechtreeUnlocked(final War3ID rawcode) { + final Integer techtreeUnlocked = this.rawcodeToTechtreeUnlocked.get(rawcode); + if (techtreeUnlocked == null) { + this.rawcodeToTechtreeUnlocked.put(rawcode, 1); + } + else { + this.rawcodeToTechtreeUnlocked.put(rawcode, techtreeUnlocked + 1); + } + } + + public void removeTechtreeUnlocked(final War3ID rawcode) { + final Integer techtreeUnlocked = this.rawcodeToTechtreeUnlocked.get(rawcode); + if (techtreeUnlocked == null) { + this.rawcodeToTechtreeUnlocked.put(rawcode, -1); + } + else { + this.rawcodeToTechtreeUnlocked.put(rawcode, techtreeUnlocked - 1); + } + } + + public void addStateListener(final CPlayerStateListener listener) { + this.stateNotifier.subscribe(listener); + } + + public void removeStateListener(final CPlayerStateListener listener) { + this.stateNotifier.unsubscribe(listener); + } + + public void chargeFor(final CUnitType unitType) { + this.lumber -= unitType.getLumberCost(); + this.gold -= unitType.getGoldCost(); + this.stateNotifier.lumberChanged(); + this.stateNotifier.goldChanged(); + } + + public void refundFor(final CUnitType unitType) { + this.lumber += unitType.getLumberCost(); + this.gold += unitType.getGoldCost(); + this.stateNotifier.lumberChanged(); + this.stateNotifier.goldChanged(); + } + + public void setUnitFoodUsed(final CUnit unit, final int foodUsed) { + this.foodUsed += unit.setFoodUsed(foodUsed); + this.stateNotifier.foodChanged(); + } + + public void setUnitFoodMade(final CUnit unit, final int foodMade) { + this.foodCap += unit.setFoodMade(foodMade); + this.stateNotifier.foodChanged(); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerColor.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerColor.java new file mode 100644 index 0000000..20cfcf6 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerColor.java @@ -0,0 +1,25 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +public enum CPlayerColor { + RED, + BLUE, + CYAN, + PURPLE, + YELLOW, + ORANGE, + GREEN, + PINK, + LIGHT_GRAY, + LIGHT_BLUE, + AQUA, + BROWN; + + public static CPlayerColor[] VALUES = values(); + + public static CPlayerColor getColorByIndex(final int index) { + if ((index >= 0) && (index < VALUES.length)) { + return VALUES[index]; + } + return null; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerGameResult.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerGameResult.java new file mode 100644 index 0000000..e713d5c --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerGameResult.java @@ -0,0 +1,10 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +public enum CPlayerGameResult { + VICTORY, + DEFEAT, + TIE, + NEUTRAL; + + public static CPlayerGameResult[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerJass.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerJass.java new file mode 100644 index 0000000..9a14f0b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerJass.java @@ -0,0 +1,49 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes.CPlayerSlotState; + +public interface CPlayerJass { + int getId(); + + void setTeam(int team); + + void setStartLocationIndex(int startLocIndex); + + void forceStartLocation(int startLocIndex); + + void setColor(int colorIndex); + + void setAlliance(int otherPlayerIndex, CAllianceType whichAllianceSetting, boolean value); + + boolean hasAlliance(int otherPlayerIndex, CAllianceType allianceType); + + void setTaxRate(int otherPlayerIndex, CPlayerState whichResource, int rate); + + void setRacePref(CRacePreference whichRacePreference); + + void setRaceSelectable(boolean selectable); + + void setController(CMapControl mapControl); + + void setName(String name); + + void setOnScoreScreen(boolean flag); + + int getTeam(); + + int getStartLocationIndex(); + + int getColor(); + + boolean isSelectable(); + + CMapControl getController(); + + CPlayerSlotState getSlotState(); + + int getTaxRate(int otherPlayerIndex, CPlayerState whichResource); + + boolean isRacePrefSet(CRacePreference pref); + + String getName(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerScore.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerScore.java new file mode 100644 index 0000000..f0950d8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerScore.java @@ -0,0 +1,31 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +public enum CPlayerScore { + UNITS_TRAINED, + UNITS_KILLED, + STRUCT_BUILT, + STRUCT_RAZED, + TECH_PERCENT, + FOOD_MAXPROD, + FOOD_MAXUSED, + HEROES_KILLED, + ITEMS_GAINED, + MERCS_HIRED, + GOLD_MINED_TOTAL, + GOLD_MINED_UPKEEP, + GOLD_LOST_UPKEEP, + GOLD_LOST_TAX, + GOLD_GIVEN, + GOLD_RECEIVED, + LUMBER_TOTAL, + LUMBER_LOST_UPKEEP, + LUMBER_LOST_TAX, + LUMBER_GIVEN, + LUMBER_RECEIVED, + UNIT_TOTAL, + HERO_TOTAL, + RESOURCE_TOTAL, + TOTAL; + + public static CPlayerScore[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerState.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerState.java new file mode 100644 index 0000000..9965b4f --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerState.java @@ -0,0 +1,42 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +public enum CPlayerState { + // current resource levels + // + RESOURCE_GOLD, + RESOURCE_LUMBER, + RESOURCE_HERO_TOKENS, + RESOURCE_FOOD_CAP, + RESOURCE_FOOD_USED, + FOOD_CAP_CEILING, + + GIVES_BOUNTY, + ALLIED_VICTORY, + PLACED, + OBSERVER_ON_DEATH, + OBSERVER, + UNFOLLOWABLE, + + // taxation rate for each resource + // + GOLD_UPKEEP_RATE, + LUMBER_UPKEEP_RATE, + + // cumulative resources collected by the player during the mission + // + GOLD_GATHERED, + LUMBER_GATHERED, + + UNKNOWN_STATE_17, + UNKNOWN_STATE_18, + UNKNOWN_STATE_19, + UNKNOWN_STATE_20, + UNKNOWN_STATE_21, + UNKNOWN_STATE_22, + UNKNOWN_STATE_23, + UNKNOWN_STATE_24, + + NO_CREEP_SLEEP; + + public static CPlayerState[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerUnitOrderExecutor.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerUnitOrderExecutor.java new file mode 100644 index 0000000..5620203 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerUnitOrderExecutor.java @@ -0,0 +1,86 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.behaviors.CBehaviorHoldPosition; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.COrderDropItemAtPoint; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.COrderNoTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.COrderTargetPoint; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.COrderTargetWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.CommandErrorListener; + +public class CPlayerUnitOrderExecutor implements CPlayerUnitOrderListener { + private final CSimulation game; + private final int playerIndex; + private final CommandErrorListener errorListener; + + public CPlayerUnitOrderExecutor(final CSimulation game, final int playerIndex, + final CommandErrorListener errorListener) { + this.game = game; + this.playerIndex = playerIndex; + this.errorListener = errorListener; + } + + @Override + public void issueTargetOrder(final int unitHandleId, final int abilityHandleId, final int orderId, + final int targetHandleId, final boolean queue) { + final CUnit unit = this.game.getUnit(unitHandleId); + if (this.playerIndex == unit.getPlayerIndex()) { + unit.order(this.game, new COrderTargetWidget(abilityHandleId, orderId, targetHandleId, queue), queue); + } + } + + @Override + public void issueDropItemAtPointOrder(final int unitHandleId, final int abilityHandleId, final int orderId, + final int targetHandleId, final float x, final float y, final boolean queue) { + final CUnit unit = this.game.getUnit(unitHandleId); + if (this.playerIndex == unit.getPlayerIndex()) { + unit.order(this.game, new COrderDropItemAtPoint(abilityHandleId, orderId, targetHandleId, + new AbilityPointTarget(x, y), queue), queue); + } + } + + @Override + public void issuePointOrder(final int unitHandleId, final int abilityHandleId, final int orderId, final float x, + final float y, final boolean queue) { + final CUnit unit = this.game.getUnit(unitHandleId); + if (this.playerIndex == unit.getPlayerIndex()) { + unit.order(this.game, new COrderTargetPoint(abilityHandleId, orderId, new AbilityPointTarget(x, y), queue), + queue); + } + } + + @Override + public void issueImmediateOrder(final int unitHandleId, final int abilityHandleId, final int orderId, + final boolean queue) { + final CUnit unit = this.game.getUnit(unitHandleId); + if (this.playerIndex == unit.getPlayerIndex()) { + if (abilityHandleId == 0) { + if (orderId == OrderIds.stop) { + unit.order(this.game, null, queue); + } + else if (orderId == OrderIds.holdposition) { + unit.order(this.game, null, queue); + final CBehaviorHoldPosition holdPositionBehavior = unit.getHoldPositionBehavior(); + if (holdPositionBehavior != null) { + unit.setDefaultBehavior(holdPositionBehavior); + } + } + } + else { + unit.order(this.game, new COrderNoTarget(abilityHandleId, orderId, queue), queue); + } + } + } + + @Override + public void unitCancelTrainingItem(final int unitHandleId, final int cancelIndex) { + final CUnit unit = this.game.getUnit(unitHandleId); + if (this.playerIndex == unit.getPlayerIndex()) { + unit.cancelBuildQueueItem(this.game, cancelIndex); + } + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerUnitOrderListener.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerUnitOrderListener.java new file mode 100644 index 0000000..af64694 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CPlayerUnitOrderListener.java @@ -0,0 +1,15 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +public interface CPlayerUnitOrderListener { + void issueTargetOrder(int unitHandleId, int abilityHandleId, int orderId, int targetHandleId, boolean queue); + + void issuePointOrder(int unitHandleId, int abilityHandleId, int orderId, float x, float y, boolean queue); + + // Below: used for "DROP ITEM AT POINT" + void issueDropItemAtPointOrder(int unitHandleId, int abilityHandleId, int orderId, int targetHandleId, float x, + float y, final boolean queue); + + void issueImmediateOrder(int unitHandleId, int abilityHandleId, int orderId, boolean queue); + + void unitCancelTrainingItem(int unitHandleId, int cancelIndex); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CRace.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CRace.java new file mode 100644 index 0000000..9043d91 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CRace.java @@ -0,0 +1,32 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +public enum CRace { + HUMAN(1), + ORC(2), + UNDEAD(3), + NIGHTELF(4), + DEMON(5), + OTHER(7); + + private int id; + + private CRace(final int id) { + this.id = id; + } + + public static CRace[] VALUES = values(); + + public int getId() { + return this.id; + } + + public static CRace parseRace(final int race) { + // TODO: this is bad time complexity (slow) but we're only doing it on startup + for (final CRace raceEnum : values()) { + if (raceEnum.getId() == race) { + return raceEnum; + } + } + return null; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CRacePreference.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CRacePreference.java new file mode 100644 index 0000000..24ff6ac --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CRacePreference.java @@ -0,0 +1,26 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +public enum CRacePreference { + HUMAN, + ORC, + NIGHTELF, + UNDEAD, + DEMON, + RANDOM, + USER_SELECTABLE; + + public static CRacePreference[] VALUES = values(); + + public static CRacePreference getById(final int id) { + for (final CRacePreference type : VALUES) { + if ((type.getId()) == id) { + return type; + } + } + return null; + } + + public int getId() { + return 1 << ordinal(); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CStartLocPrio.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CStartLocPrio.java new file mode 100644 index 0000000..99d7008 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/players/CStartLocPrio.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.players; + +public enum CStartLocPrio { + LOW, + HIGH, + NOT; + + public static CStartLocPrio[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/region/CRegion.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/region/CRegion.java new file mode 100644 index 0000000..f215fac --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/region/CRegion.java @@ -0,0 +1,125 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.region; + +import com.badlogic.gdx.math.Rectangle; + +public class CRegion { + private Rectangle currentBounds; + private boolean complexRegion; + + public void addRect(final Rectangle rect, final CRegionManager regionManager) { + if (this.currentBounds == null) { + this.currentBounds = new Rectangle(rect); + regionManager.addRectForRegion(this, this.currentBounds); + } + else { + if (!this.complexRegion) { + convertToComplexRegionAndAddRect(rect, regionManager); + } + else { + complexRegionAddRect(rect, regionManager); + } + } + } + + public void clearRect(final Rectangle rect, final CRegionManager regionManager) { + if (this.currentBounds == null) { + return; + } + if (this.complexRegion) { + regionManager.removeRectForRegion(this, this.currentBounds); + regionManager.removeComplexRegionCells(this, rect); + regionManager.computeNewMinimumComplexRegionBounds(this, this.currentBounds); + regionManager.addRectForRegion(this, this.currentBounds); + } + else { + this.complexRegion = true; + regionManager.addComplexRegionCells(this, this.currentBounds); + regionManager.removeComplexRegionCells(this, rect); + } + } + + public void remove(final CRegionManager regionManager) { + if (this.currentBounds == null) { + return; + } + if (this.complexRegion) { + regionManager.removeComplexRegionCells(this, this.currentBounds); + } + regionManager.removeRectForRegion(this, this.currentBounds); + } + + public void addCell(final float x, final float y, final CRegionManager regionManager) { + if (this.currentBounds == null) { + this.complexRegion = true; + this.currentBounds = new Rectangle(x, y, 0, 0); + regionManager.addComplexRegionCell(this, x, y, this.currentBounds); + regionManager.addRectForRegion(this, this.currentBounds); + } + else { + regionManager.removeRectForRegion(this, this.currentBounds); + if (!this.complexRegion) { + regionManager.addComplexRegionCells(this, this.currentBounds); + this.complexRegion = true; + } + regionManager.addComplexRegionCell(this, x, y, this.currentBounds); + regionManager.addRectForRegion(this, this.currentBounds); + } + } + + public void clearCell(final float x, final float y, final CRegionManager regionManager) { + if (this.currentBounds == null) { + return; + } + else { + regionManager.removeRectForRegion(this, this.currentBounds); + if (!this.complexRegion) { + regionManager.addComplexRegionCells(this, this.currentBounds); + this.complexRegion = true; + } + regionManager.clearComplexRegionCell(this, x, y, this.currentBounds); + regionManager.addRectForRegion(this, this.currentBounds); + } + } + + private void complexRegionAddRect(final Rectangle rect, final CRegionManager regionManager) { + regionManager.removeRectForRegion(this, this.currentBounds); + regionManager.addComplexRegionCells(this, rect); + this.currentBounds = this.currentBounds.merge(rect); + regionManager.addRectForRegion(this, this.currentBounds); + } + + private void convertToComplexRegionAndAddRect(final Rectangle rect, final CRegionManager regionManager) { + regionManager.removeRectForRegion(this, this.currentBounds); + this.complexRegion = true; + regionManager.addComplexRegionCells(this, this.currentBounds); + regionManager.addComplexRegionCells(this, rect); + this.currentBounds = this.currentBounds.merge(rect); + regionManager.addRectForRegion(this, this.currentBounds); + } + + public Rectangle getCurrentBounds() { + return this.currentBounds; + } + + public void setCurrentBounds(final Rectangle currentBounds) { + this.currentBounds = currentBounds; + } + + public boolean isComplexRegion() { + return this.complexRegion; + } + + public void setComplexRegion(final boolean complexRegion) { + this.complexRegion = complexRegion; + } + + public boolean contains(final float x, final float y, final CRegionManager regionManager) { + if (this.currentBounds == null) { + return false; + } + if (this.complexRegion) { + return regionManager.isPointInComplexRegion(this, x, y); + } + return this.currentBounds.contains(x, y); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/region/CRegionEnumFunction.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/region/CRegionEnumFunction.java new file mode 100644 index 0000000..8cf6135 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/region/CRegionEnumFunction.java @@ -0,0 +1,11 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.region; + +public interface CRegionEnumFunction { + /** + * Operates on a region, returning true if we should stop execution. + * + * @param region + * @return + */ + boolean call(CRegion region); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/region/CRegionManager.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/region/CRegionManager.java new file mode 100644 index 0000000..6ed9c65 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/region/CRegionManager.java @@ -0,0 +1,189 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.region; + +import java.util.ArrayList; +import java.util.List; + +import com.badlogic.gdx.math.Rectangle; +import com.etheller.warsmash.util.Quadtree; +import com.etheller.warsmash.util.QuadtreeIntersector; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid; + +public class CRegionManager { + private static Rectangle tempRect = new Rectangle(); + private final Quadtree regionTree; + private final RegionChecker regionChecker = new RegionChecker(); + private final List[][] cellRegions; + private final PathingGrid pathingGrid; + + public CRegionManager(final Rectangle entireMapBounds, final PathingGrid pathingGrid) { + this.regionTree = new Quadtree<>(entireMapBounds); + this.cellRegions = new List[pathingGrid.getHeight()][pathingGrid.getWidth()]; + this.pathingGrid = pathingGrid; + } + + public void addRectForRegion(final CRegion region, final Rectangle rect) { + this.regionTree.add(region, rect); + } + + public void removeRectForRegion(final CRegion region, final Rectangle rect) { + this.regionTree.remove(region, rect); + } + + /** + * Calls back on the enum function for every region that touches the given area. + * Sometimes, for performance, this algorithm is designed to call the enum + * function twice for the same region, because our expected use case is to store + * the regions in a set that guarantees uniqueness anyway (see CUnit and/or + * other uses of this method). + */ + public void checkRegions(final Rectangle area, final CRegionEnumFunction enumFunction) { + this.regionTree.intersect(area, this.regionChecker.reset(enumFunction)); + if (this.regionChecker.includesComplex) { + final int minX = this.pathingGrid.getCellX(area.x); + final int minY = this.pathingGrid.getCellY(area.y); + final int maxX = this.pathingGrid.getCellX(area.x + area.width); + final int maxY = this.pathingGrid.getCellY(area.y + area.height); + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + final List cellRegionsAtPoint = this.cellRegions[y][x]; + if (cellRegionsAtPoint != null) { + for (final CRegion region : cellRegionsAtPoint) { + if (enumFunction.call(region)) { + return; + } + } + } + } + } + } + } + + private static final class RegionChecker implements QuadtreeIntersector { + private CRegionEnumFunction delegate; + private boolean includesComplex = false; + + public RegionChecker reset(final CRegionEnumFunction delegate) { + this.delegate = delegate; + return this; + } + + @Override + public boolean onIntersect(final CRegion intersectingObject) { + if (intersectingObject.isComplexRegion()) { + this.includesComplex = true; + // handle this type of region differently + return false; + } + return this.delegate.call(intersectingObject); + } + + } + + public void addComplexRegionCells(final CRegion region, final Rectangle currentBounds) { + final int minX = this.pathingGrid.getCellX(currentBounds.x); + final int minY = this.pathingGrid.getCellY(currentBounds.y); + final int maxX = this.pathingGrid.getCellX(currentBounds.x + currentBounds.width); + final int maxY = this.pathingGrid.getCellY(currentBounds.y + currentBounds.height); + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + List list = this.cellRegions[y][x]; + if (list == null) { + this.cellRegions[y][x] = list = new ArrayList<>(); + } + list.add(region); + } + } + } + + public void removeComplexRegionCells(final CRegion region, final Rectangle currentBounds) { + final int minX = this.pathingGrid.getCellX(currentBounds.x); + final int minY = this.pathingGrid.getCellY(currentBounds.y); + final int maxX = this.pathingGrid.getCellX(currentBounds.x + currentBounds.width); + final int maxY = this.pathingGrid.getCellY(currentBounds.y + currentBounds.height); + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + final List list = this.cellRegions[y][x]; + if (list != null) { + list.remove(region); + } + } + } + } + + public void computeNewMinimumComplexRegionBounds(final CRegion region, final Rectangle complexRegionBounds) { + final int minX = this.pathingGrid.getCellX(complexRegionBounds.x); + final int minY = this.pathingGrid.getCellY(complexRegionBounds.y); + final int maxX = this.pathingGrid.getCellX(complexRegionBounds.x + complexRegionBounds.width); + final int maxY = this.pathingGrid.getCellY(complexRegionBounds.y + complexRegionBounds.height); + float newMinX = this.pathingGrid.getWorldX(this.pathingGrid.getWidth() - 1); + float newMaxX = this.pathingGrid.getWorldX(0); + float newMinY = this.pathingGrid.getWorldY(this.pathingGrid.getHeight() - 1); + float newMaxY = this.pathingGrid.getWorldY(0); + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + final List list = this.cellRegions[y][x]; + if (list != null) { + if (list.contains(region)) { + final float worldX = this.pathingGrid.getWorldX(x); + final float worldY = this.pathingGrid.getWorldY(y); + final float wMinX = worldX - 16f; + final float wMinY = worldY - 16f; + final float wMaxX = worldX + 15f; + final float wMaxY = worldY + 15f; + if (wMinX < newMinX) { + newMinX = wMinX; + } + if (wMinY < newMinY) { + newMinY = wMinY; + } + if (wMaxX > newMaxX) { + newMaxX = wMaxX; + } + if (wMaxY > newMaxY) { + newMaxY = wMaxY; + } + } + } + } + } + complexRegionBounds.set(newMinX, newMinY, newMaxX - newMinX, newMaxY - newMinY); + } + + public void addComplexRegionCell(final CRegion region, final float x, final float y, + final Rectangle boundsToUpdate) { + final int cellX = this.pathingGrid.getCellX(x); + final int cellY = this.pathingGrid.getCellY(y); + List list = this.cellRegions[cellY][cellX]; + if (list == null) { + this.cellRegions[cellY][cellX] = list = new ArrayList<>(); + } + list.add(region); + final float worldX = this.pathingGrid.getWorldX(cellX); + final float worldY = this.pathingGrid.getWorldY(cellY); + final float wMinX = worldX - 16f; + final float wMinY = worldY - 16f; + boundsToUpdate.merge(tempRect.set(wMinX, wMinY, 31f, 31f)); + } + + public void clearComplexRegionCell(final CRegion region, final float x, final float y, + final Rectangle boundsToUpdate) { + final int cellX = this.pathingGrid.getCellX(x); + final int cellY = this.pathingGrid.getCellY(y); + final List list = this.cellRegions[cellY][cellX]; + if (list != null) { + list.remove(region); + } + computeNewMinimumComplexRegionBounds(region, boundsToUpdate); + } + + public boolean isPointInComplexRegion(final CRegion region, final float x, final float y) { + final int cellX = this.pathingGrid.getCellX(x); + final int cellY = this.pathingGrid.getCellY(y); + final List list = this.cellRegions[cellY][cellX]; + if (list != null) { + return list.contains(region); + } + return false; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/state/CGameState.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/state/CGameState.java new file mode 100644 index 0000000..0cad618 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/state/CGameState.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.state; + +public enum CGameState { + DIVINE_INTERVENTION, + DISCONNECTED, + TIME_OF_DAY; + + public static CGameState[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/state/CUnitState.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/state/CUnitState.java new file mode 100644 index 0000000..2272ad4 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/state/CUnitState.java @@ -0,0 +1,10 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.state; + +public enum CUnitState { + LIFE, + MAX_LIFE, + MANA, + MAX_MANA; + + public static CUnitState[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/timers/CTimer.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/timers/CTimer.java new file mode 100644 index 0000000..917f44e --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/timers/CTimer.java @@ -0,0 +1,89 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.timers; + +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; + +public abstract class CTimer { + private int engineFireTick; + private int scheduleTick; + private float timeoutTime; + private float remainingTimeAfterPause; + private boolean running = false; + private boolean repeats; + + public void setTimeoutTime(final float timeoutTime) { + this.timeoutTime = timeoutTime; + } + + public boolean isRepeats() { + return this.repeats; + } + + public boolean isRunning() { + return this.running; + } + + public float getTimeoutTime() { + return this.timeoutTime; + } + + /** + * @param simulation + */ + public void start(final CSimulation simulation) { + this.running = true; + final int currentTick = simulation.getGameTurnTick(); + this.scheduleTick = currentTick; + innerStart(this.timeoutTime, simulation, currentTick); + } + + private void innerStart(final float timeoutTime, final CSimulation simulation, final int currentTick) { + final int ticks = (int) (timeoutTime / WarsmashConstants.SIMULATION_STEP_TIME); + this.engineFireTick = currentTick + ticks; + simulation.registerTimer(this); + } + + public void pause(final CSimulation simulation) { + this.remainingTimeAfterPause = getRemaining(simulation); + simulation.unregisterTimer(this); + } + + public void resume(final CSimulation simulation) { + if (this.remainingTimeAfterPause == 0) { + start(simulation); + } + final int currentTick = simulation.getGameTurnTick(); + innerStart(this.remainingTimeAfterPause, simulation, currentTick); + this.remainingTimeAfterPause = 0; + } + + public float getElapsed(final CSimulation simulation) { + final int currentTick = simulation.getGameTurnTick(); + final int elapsedTicks = currentTick - this.scheduleTick; + return Math.max(elapsedTicks * WarsmashConstants.SIMULATION_STEP_TIME, this.timeoutTime); + + } + + public float getRemaining(final CSimulation simulation) { + return this.timeoutTime - getElapsed(simulation); + } + + public void setRepeats(final boolean repeats) { + this.repeats = repeats; + } + + public int getEngineFireTick() { + return this.engineFireTick; + } + + public abstract void onFire(); + + public void fire(final CSimulation simulation) { + // its implied that we will have "unregisterTimer" happen automatically + // before this is called + this.running = false; + if (this.repeats) { + start(simulation); + } + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/timers/CTimerJass.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/timers/CTimerJass.java new file mode 100644 index 0000000..33ba52e --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/timers/CTimerJass.java @@ -0,0 +1,25 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.timers; + +import java.util.Collections; + +import com.etheller.interpreter.ast.function.JassFunction; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; + +public class CTimerJass extends CTimer { + private JassFunction handlerFunc; + private final GlobalScope jassGlobalScope; + + public CTimerJass(final GlobalScope jassGlobalScope) { + this.jassGlobalScope = jassGlobalScope; + } + + public void setHandlerFunc(final JassFunction handlerFunc) { + this.handlerFunc = handlerFunc; + } + + @Override + public void onFire() { + this.handlerFunc.call(Collections.emptyList(), this.jassGlobalScope, TriggerExecutionScope.EMPTY); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/JassGameEventsWar3.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/JassGameEventsWar3.java new file mode 100644 index 0000000..7d78f30 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/JassGameEventsWar3.java @@ -0,0 +1,248 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger; + +//=================================================== +//Game, Player and Unit Events +// +//When an event causes a trigger to fire these +//values allow the action code to determine which +//event was dispatched and therefore which set of +//native functions should be used to get information +//about the event. +// +//Do NOT change the order or value of these constants +//without insuring that the JASS_GAME_EVENTS_WAR3 enum +//is changed to match. +// +//=================================================== + +public enum JassGameEventsWar3 { + // =================================================== + // For use with TriggerRegisterGameEvent + // =================================================== + EVENT_GAME_VICTORY, + EVENT_GAME_END_LEVEL, + + EVENT_GAME_VARIABLE_LIMIT, + EVENT_GAME_STATE_LIMIT, + + EVENT_GAME_TIMER_EXPIRED, + + EVENT_GAME_ENTER_REGION, + EVENT_GAME_LEAVE_REGION, + + EVENT_GAME_TRACKABLE_HIT, + EVENT_GAME_TRACKABLE_TRACK, + + EVENT_GAME_SHOW_SKILL, + EVENT_GAME_BUILD_SUBMENU, + + // =================================================== + // For use with TriggerRegisterPlayerEvent + // =================================================== + EVENT_PLAYER_STATE_LIMIT, + EVENT_PLAYER_ALLIANCE_CHANGED, + + EVENT_PLAYER_DEFEAT, + EVENT_PLAYER_VICTORY, + EVENT_PLAYER_LEAVE, + EVENT_PLAYER_CHAT, + EVENT_PLAYER_END_CINEMATIC, + + // =================================================== + // For use with TriggerRegisterPlayerUnitEvent + // =================================================== + EVENT_PLAYER_UNIT_ATTACKED, + EVENT_PLAYER_UNIT_RESCUED, + + EVENT_PLAYER_UNIT_DEATH, + EVENT_PLAYER_UNIT_DECAY, + + EVENT_PLAYER_UNIT_DETECTED, + EVENT_PLAYER_UNIT_HIDDEN, + + EVENT_PLAYER_UNIT_SELECTED, + EVENT_PLAYER_UNIT_DESELECTED, + + EVENT_PLAYER_UNIT_CONSTRUCT_START, + EVENT_PLAYER_UNIT_CONSTRUCT_CANCEL, + EVENT_PLAYER_UNIT_CONSTRUCT_FINISH, + + EVENT_PLAYER_UNIT_UPGRADE_START, + EVENT_PLAYER_UNIT_UPGRADE_CANCEL, + EVENT_PLAYER_UNIT_UPGRADE_FINISH, + + EVENT_PLAYER_UNIT_TRAIN_START, + EVENT_PLAYER_UNIT_TRAIN_CANCEL, + EVENT_PLAYER_UNIT_TRAIN_FINISH, + + EVENT_PLAYER_UNIT_RESEARCH_START, + EVENT_PLAYER_UNIT_RESEARCH_CANCEL, + EVENT_PLAYER_UNIT_RESEARCH_FINISH, + EVENT_PLAYER_UNIT_ISSUED_ORDER, + EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, + EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER, + + EVENT_PLAYER_HERO_LEVEL, + EVENT_PLAYER_HERO_SKILL, + + EVENT_PLAYER_HERO_REVIVABLE, + + EVENT_PLAYER_HERO_REVIVE_START, + EVENT_PLAYER_HERO_REVIVE_CANCEL, + EVENT_PLAYER_HERO_REVIVE_FINISH, + EVENT_PLAYER_UNIT_SUMMON, + EVENT_PLAYER_UNIT_DROP_ITEM, + EVENT_PLAYER_UNIT_PICKUP_ITEM, + EVENT_PLAYER_UNIT_USE_ITEM, + EVENT_PLAYER_UNIT_LOADED, + + // =================================================== + // For use with TriggerRegisterUnitEvent + // =================================================== + + EVENT_UNIT_DAMAGED, + EVENT_UNIT_DEATH, + EVENT_UNIT_DECAY, + EVENT_UNIT_DETECTED, + EVENT_UNIT_HIDDEN, + EVENT_UNIT_SELECTED, + EVENT_UNIT_DESELECTED, + + EVENT_UNIT_STATE_LIMIT, + + // Events which may have a filter for the "other unit" + // + EVENT_UNIT_ACQUIRED_TARGET, + EVENT_UNIT_TARGET_IN_RANGE, + EVENT_UNIT_ATTACKED, + EVENT_UNIT_RESCUED, + + EVENT_UNIT_CONSTRUCT_CANCEL, + EVENT_UNIT_CONSTRUCT_FINISH, + + EVENT_UNIT_UPGRADE_START, + EVENT_UNIT_UPGRADE_CANCEL, + EVENT_UNIT_UPGRADE_FINISH, + +// Events which involve the specified unit performing +// training of other units +// + EVENT_UNIT_TRAIN_START, + EVENT_UNIT_TRAIN_CANCEL, + EVENT_UNIT_TRAIN_FINISH, + + EVENT_UNIT_RESEARCH_START, + EVENT_UNIT_RESEARCH_CANCEL, + EVENT_UNIT_RESEARCH_FINISH, + + EVENT_UNIT_ISSUED_ORDER, + EVENT_UNIT_ISSUED_POINT_ORDER, + EVENT_UNIT_ISSUED_TARGET_ORDER, + + EVENT_UNIT_HERO_LEVEL, + EVENT_UNIT_HERO_SKILL, + + EVENT_UNIT_HERO_REVIVABLE, + EVENT_UNIT_HERO_REVIVE_START, + EVENT_UNIT_HERO_REVIVE_CANCEL, + EVENT_UNIT_HERO_REVIVE_FINISH, + + EVENT_UNIT_SUMMON, + + EVENT_UNIT_DROP_ITEM, + EVENT_UNIT_PICKUP_ITEM, + EVENT_UNIT_USE_ITEM, + + EVENT_UNIT_LOADED, + + EVENT_WIDGET_DEATH, + + EVENT_DIALOG_BUTTON_CLICK, + EVENT_DIALOG_CLICK, + + // =================================================== + // Frozen Throne Expansion Events + // Need to be added here to preserve compat + // =================================================== + + // =================================================== + // For use with TriggerRegisterGameEvent + // =================================================== + + EVENT_GAME_LOADED, + EVENT_GAME_TOURNAMENT_FINISH_SOON, + EVENT_GAME_TOURNAMENT_FINISH_NOW, + EVENT_GAME_SAVE, + + EVENT_UNKNOWN_TFT_CODE_260, + + // =================================================== + // For use with TriggerRegisterPlayerEvent + // =================================================== + + EVENT_PLAYER_ARROW_LEFT_DOWN, + EVENT_PLAYER_ARROW_LEFT_UP, + EVENT_PLAYER_ARROW_RIGHT_DOWN, + EVENT_PLAYER_ARROW_RIGHT_UP, + EVENT_PLAYER_ARROW_DOWN_DOWN, + EVENT_PLAYER_ARROW_DOWN_UP, + EVENT_PLAYER_ARROW_UP_DOWN, + EVENT_PLAYER_ARROW_UP_UP, + + // =================================================== + // For use with TriggerRegisterPlayerUnitEvent + // =================================================== + + EVENT_PLAYER_UNIT_SELL, + EVENT_PLAYER_UNIT_CHANGE_OWNER, + EVENT_PLAYER_UNIT_SELL_ITEM, + EVENT_PLAYER_UNIT_SPELL_CHANNEL, + EVENT_PLAYER_UNIT_SPELL_CAST, + EVENT_PLAYER_UNIT_SPELL_EFFECT, + EVENT_PLAYER_UNIT_SPELL_FINISH, + EVENT_PLAYER_UNIT_SPELL_ENDCAST, + EVENT_PLAYER_UNIT_PAWN_ITEM, + + EVENT_UNKNOWN_TFT_CODE_278, + EVENT_UNKNOWN_TFT_CODE_279, + EVENT_UNKNOWN_TFT_CODE_280, + EVENT_UNKNOWN_TFT_CODE_281, + EVENT_UNKNOWN_TFT_CODE_282, + EVENT_UNKNOWN_TFT_CODE_283, + EVENT_UNKNOWN_TFT_CODE_284, + EVENT_UNKNOWN_TFT_CODE_285, + + // =================================================== + // For use with TriggerRegisterUnitEvent + // =================================================== + + EVENT_UNIT_SELL, + EVENT_UNIT_CHANGE_OWNER, + EVENT_UNIT_SELL_ITEM, + EVENT_UNIT_SPELL_CHANNEL, + EVENT_UNIT_SPELL_CAST, + EVENT_UNIT_SPELL_EFFECT, + EVENT_UNIT_SPELL_FINISH, + EVENT_UNIT_SPELL_ENDCAST, + EVENT_UNIT_PAWN_ITEM,; + + private static final int TFT_CUTOFF = EVENT_GAME_LOADED.ordinal(); + + public static JassGameEventsWar3[] VALUES; + static { + final JassGameEventsWar3[] localValuesArray = values(); + final JassGameEventsWar3 endValue = localValuesArray[localValuesArray.length]; + VALUES = new JassGameEventsWar3[endValue.getEventId() + 1]; + for (final JassGameEventsWar3 event : localValuesArray) { + VALUES[event.getEventId()] = event; + } + } + + public int getEventId() { + final int ordinal = ordinal(); + if (ordinal >= TFT_CUTOFF) { + return (ordinal - TFT_CUTOFF) + 256; + } + return ordinal; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CAttackTypeJass.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CAttackTypeJass.java new file mode 100644 index 0000000..8fbd348 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CAttackTypeJass.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; + +public enum CAttackTypeJass { + ; + public static CAttackType[] VALUES = { CAttackType.SPELLS, CAttackType.NORMAL, CAttackType.PIERCE, + CAttackType.SIEGE, CAttackType.MAGIC, CAttackType.CHAOS, CAttackType.HERO }; +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CBlendMode.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CBlendMode.java new file mode 100644 index 0000000..8ea22de --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CBlendMode.java @@ -0,0 +1,12 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CBlendMode { + NONE, + KEYALPHA, + BLEND, + ADDITIVE, + MODULATE, + MODULATE_2X; + + public static CBlendMode[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CCameraField.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CCameraField.java new file mode 100644 index 0000000..b51f0a4 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CCameraField.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CCameraField { + TARGET_DISTANCE, + FARZ, + ANGLE_OF_ATTACK, + FIELD_OF_VIEW, + ROLL, + ROTATION, + ZOFFSET; + + public static CCameraField[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CDamageType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CDamageType.java new file mode 100644 index 0000000..2269db5 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CDamageType.java @@ -0,0 +1,32 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CDamageType { + UNKNOWN, + UNKNOWN_CODE_1, + UNKNOWN_CODE_2, + UNKNOWN_CODE_3, + NORMAL, + ENHANCED, + UNKNOWN_CODE_6, + UNKNOWN_CODE_7, + FIRE, + COLD, + LIGHTNING, + POISON, + DISEASE, + DIVINE, + MAGIC, + SONIC, + ACID, + FORCE, + DEATH, + MIND, + PLANT, + DEFENSIVE, + DEMOLITION, + SLOW_POISON, + SPIRIT_LINK, + SHADOW_STRIKE; + + public static CDamageType[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CEffectType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CEffectType.java new file mode 100644 index 0000000..1d02a97 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CEffectType.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CEffectType { + EFFECT, + TARGET, + CASTER, + SPECIAL, + AREA_EFFECT, + MISSILE, + LIGHTNING; + + public static CEffectType[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CFogState.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CFogState.java new file mode 100644 index 0000000..4e849f1 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CFogState.java @@ -0,0 +1,22 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CFogState { + MASKED, + FOGGED, + VISIBLE; + + public static CFogState[] VALUES = values(); + + public static CFogState getById(final int id) { + for (final CFogState type : VALUES) { + if ((type.getId()) == id) { + return type; + } + } + return null; + } + + public int getId() { + return 1 << ordinal(); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CGameSpeed.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CGameSpeed.java new file mode 100644 index 0000000..ee44d83 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CGameSpeed.java @@ -0,0 +1,11 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CGameSpeed { + SLOWEST, + SLOW, + NORMAL, + FAST, + FASTEST; + + public static CGameSpeed[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CGameType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CGameType.java new file mode 100644 index 0000000..6c068a3 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CGameType.java @@ -0,0 +1,27 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CGameType { + MELEE, + FFA, + USE_MAP_SETTINGS, + BLIZ, + ONE_ON_ONE, + TWO_TEAM_PLAY, + THREE_TEAM_PLAY, + FOUR_TEAM_PLAY; + + public static CGameType[] VALUES = values(); + + public static CGameType getById(final int id) { + for (final CGameType type : VALUES) { + if ((type.getId()) == id) { + return type; + } + } + return null; + } + + public int getId() { + return 1 << ordinal(); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CLimitOp.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CLimitOp.java new file mode 100644 index 0000000..1a36421 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CLimitOp.java @@ -0,0 +1,12 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CLimitOp { + LESS_THAN, + LESS_THAN_OR_EQUAL, + EQUAL, + GREATER_THAN_OR_EQUAL, + GREATER_THAN, + NOT_EQUAL; + + public static CLimitOp[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CMapDensity.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CMapDensity.java new file mode 100644 index 0000000..0e27016 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CMapDensity.java @@ -0,0 +1,10 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CMapDensity { + NONE, + LIGHT, + MEDIUM, + HEAVY; + + public static CMapDensity[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CMapDifficulty.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CMapDifficulty.java new file mode 100644 index 0000000..30b1e87 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CMapDifficulty.java @@ -0,0 +1,10 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CMapDifficulty { + EASY, + NORMAL, + HARD, + INSANE; + + public static CMapDifficulty[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CPathingTypeJass.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CPathingTypeJass.java new file mode 100644 index 0000000..2e82cbe --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CPathingTypeJass.java @@ -0,0 +1,14 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CPathingTypeJass { + ANY, + WALKABILITY, + FLYABILITY, + BUILDABILITY, + PEONHARVESTPATHING, + BLIGHTPATHING, + FLOATABILITY, + AMPHIBIOUSPATHING; + + public static CPathingTypeJass[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CPlayerSlotState.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CPlayerSlotState.java new file mode 100644 index 0000000..e8c219f --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CPlayerSlotState.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CPlayerSlotState { + EMPTY, + PLAYING, + LEFT; + + public static CPlayerSlotState[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CRarityControl.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CRarityControl.java new file mode 100644 index 0000000..d67015d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CRarityControl.java @@ -0,0 +1,8 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CRarityControl { + FREQUENT, + RARE; + + public static CRarityControl[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CSoundType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CSoundType.java new file mode 100644 index 0000000..14db526 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CSoundType.java @@ -0,0 +1,8 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CSoundType { + EFFECT, + EFFECT_LOOPED; + + public static CSoundType[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CSoundVolumeGroup.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CSoundVolumeGroup.java new file mode 100644 index 0000000..3f22881 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CSoundVolumeGroup.java @@ -0,0 +1,14 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CSoundVolumeGroup { + UNITMOVEMENT, + UNITSOUNDS, + COMBAT, + SPELLS, + UI, + MUSIC, + AMBIENTSOUNDS, + FIRE; + + public static CSoundVolumeGroup[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CTexMapFlags.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CTexMapFlags.java new file mode 100644 index 0000000..10dee18 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CTexMapFlags.java @@ -0,0 +1,10 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CTexMapFlags { + NONE, + WRAP_U, + WRAP_V, + WRAP_UV; + + public static CTexMapFlags[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CVersion.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CVersion.java new file mode 100644 index 0000000..6e8cf22 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CVersion.java @@ -0,0 +1,8 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CVersion { + REIGN_OF_CHAOS, + FROZEN_THRONE; + + public static CVersion[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CWeaponSoundTypeJass.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CWeaponSoundTypeJass.java new file mode 100644 index 0000000..f3a6b8f --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/trigger/enumtypes/CWeaponSoundTypeJass.java @@ -0,0 +1,40 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.trigger.enumtypes; + +public enum CWeaponSoundTypeJass { + WHOKNOWS(null), + METAL_LIGHT_CHOP("MetalLightChop"), + METAL_MEDIUM_CHOP("MetalMediumChop"), + METAL_HEAVY_CHOP("MetalHeavyChop"), + METAL_LIGHT_SLICE("MetalLightSlice"), + METAL_MEDIUM_SLICE("MetalMediumSlice"), + METAL_HEAVY_SLICE("MetalHeavySlice"), + METAL_MEDIUM_BASH("MetalMediumBash"), + METAL_HEAVY_BASH("MetalHeavyBash"), + METAL_MEDIUM_STAB("MetalMediumStab"), + METAL_HEAVY_STAB("MetalHeavyStab"), + WOOD_LIGHT_SLICE("WoodLightSlice"), + WOOD_MEDIUM_SLICE("WoodMediumSlice"), + WOOD_HEAVY_SLICE("WoodHeavySlice"), + WOOD_LIGHT_BASH("WoodLightBash"), + WOOD_MEDIUM_BASH("WoodMediumBash"), + WOOD_HEAVY_BASH("WoodHeavyBash"), + WOOD_LIGHT_STAB("WoodLightStab"), + WOOD_MEDIUM_STAB("WoodMediumStab"), + CLAW_LIGHT_SLICE("ClawLightSlice"), + CLAW_MEDIUM_SLICE("ClawMediumSlice"), + CLAW_HEAVY_SLICE("ClawHeavySlice"), + AXE_MEDIUM_CHOP("AxeMediumChop"), + ROCK_HEAVY_BASH("RockHeavyBash"); + + private final String soundKey; + + CWeaponSoundTypeJass(final String soundKey) { + this.soundKey = soundKey; + } + + public String getSoundKey() { + return this.soundKey; + } + + public static CWeaponSoundTypeJass[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/unit/CUnitTypeJass.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/unit/CUnitTypeJass.java new file mode 100644 index 0000000..3092f37 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/unit/CUnitTypeJass.java @@ -0,0 +1,39 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.unit; + +public enum CUnitTypeJass { + HERO, + DEAD, + STRUCTURE, + + FLYING, + GROUND, + + ATTACKS_FLYING, + ATTACKS_GROUND, + + MELEE_ATTACKER, + RANGED_ATTACKER, + + GIANT, + SUMMONED, + STUNNED, + PLAGUED, + SNARED, + + UNDEAD, + MECHANICAL, + PEON, + SAPPER, + TOWNHALL, + ANCIENT, + + TAUREN, + POISONED, + POLYMORPHED, + SLEEPING, + RESISTANT, + ETHEREAL, + MAGIC_IMMUNE; + + public static CUnitTypeJass[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/AbilityActivationErrorHandler.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/AbilityActivationErrorHandler.java new file mode 100644 index 0000000..fafc5b2 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/AbilityActivationErrorHandler.java @@ -0,0 +1,22 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.util; + +import com.etheller.warsmash.viewer5.AudioContext; +import com.etheller.warsmash.viewer5.handlers.w3x.UnitSound; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.CommandErrorListener; + +public class AbilityActivationErrorHandler { + private final String errorString; + private final UnitSound errorSound; + + public AbilityActivationErrorHandler(final String errorString, final UnitSound errorSound) { + this.errorString = errorString; + this.errorSound = errorSound; + } + + public void onClick(final CommandErrorListener commandErrorListener, final AudioContext worldSceneAudioContext, + final RenderUnit commandedUnit) { + commandErrorListener.showCommandError(this.errorString); + this.errorSound.playUnitResponse(worldSceneAudioContext, commandedUnit); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/AbilityActivationReceiver.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/AbilityActivationReceiver.java new file mode 100644 index 0000000..09d79a4 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/AbilityActivationReceiver.java @@ -0,0 +1,19 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.util; + +import com.etheller.warsmash.util.War3ID; + +public interface AbilityActivationReceiver { + void useOk(); + + void notEnoughResources(ResourceType resource); + + void notAnActiveAbility(); + + void missingRequirement(War3ID type, int level); + + void casterMovementDisabled(); + + void cargoCapacityUnavailable(); + + void disabled(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/AbilityTargetCheckReceiver.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/AbilityTargetCheckReceiver.java new file mode 100644 index 0000000..2959ee6 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/AbilityTargetCheckReceiver.java @@ -0,0 +1,36 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.util; + +public interface AbilityTargetCheckReceiver { + void targetOk(TARGET_TYPE target); + + void mustTargetTeamType(TeamType correctType); + + void mustTargetType(TargetType correctType); + + void mustTargetResources(); + + void targetOutsideRange(double howMuch); + + void notAnActiveAbility(); + + void targetNotVisible(); + + void targetTooComplicated(); + + void targetNotInPlayableMap(); + + void orderIdNotAccepted(); + + public static enum TeamType { + ALLIED, + ENEMY, + PLAYER_UNITS; + } + + public static enum TargetType { + UNIT, + POINT, + UNIT_OR_POINT, + NO_TARGET + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/BooleanAbilityActivationReceiver.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/BooleanAbilityActivationReceiver.java new file mode 100644 index 0000000..b6ae3b7 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/BooleanAbilityActivationReceiver.java @@ -0,0 +1,48 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.util; + +import com.etheller.warsmash.util.War3ID; + +public class BooleanAbilityActivationReceiver implements AbilityActivationReceiver { + public static final BooleanAbilityActivationReceiver INSTANCE = new BooleanAbilityActivationReceiver(); + private boolean ok; + + @Override + public void useOk() { + this.ok = true; + } + + @Override + public void notEnoughResources(final ResourceType resource) { + this.ok = false; + } + + @Override + public void notAnActiveAbility() { + this.ok = false; + } + + @Override + public void missingRequirement(final War3ID type, final int level) { + this.ok = false; + } + + @Override + public void casterMovementDisabled() { + this.ok = false; + } + + @Override + public void cargoCapacityUnavailable() { + this.ok = false; + } + + @Override + public void disabled() { + this.ok = false; + } + + public boolean isOk() { + return this.ok; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/BooleanAbilityTargetCheckReceiver.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/BooleanAbilityTargetCheckReceiver.java new file mode 100644 index 0000000..bd65215 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/BooleanAbilityTargetCheckReceiver.java @@ -0,0 +1,71 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.util; + +public final class BooleanAbilityTargetCheckReceiver implements AbilityTargetCheckReceiver { + private static final BooleanAbilityTargetCheckReceiver INSTANCE = new BooleanAbilityTargetCheckReceiver<>(); + + public static BooleanAbilityTargetCheckReceiver getInstance() { + return (BooleanAbilityTargetCheckReceiver) INSTANCE; + } + + private boolean targetable = false; + + public boolean isTargetable() { + return this.targetable; + } + + public BooleanAbilityTargetCheckReceiver reset() { + this.targetable = false; + return this; + } + + @Override + public void targetOk(final TARGET_TYPE target) { + this.targetable = true; + } + + @Override + public void mustTargetTeamType(final TeamType correctType) { + this.targetable = false; + } + + @Override + public void mustTargetType(final TargetType correctType) { + this.targetable = false; + } + + @Override + public void mustTargetResources() { + this.targetable = false; + } + + @Override + public void targetOutsideRange(final double howMuch) { + this.targetable = false; + } + + @Override + public void notAnActiveAbility() { + this.targetable = false; + } + + @Override + public void targetNotVisible() { + this.targetable = false; + } + + @Override + public void targetTooComplicated() { + this.targetable = false; + } + + @Override + public void targetNotInPlayableMap() { + this.targetable = false; + } + + @Override + public void orderIdNotAccepted() { + this.targetable = false; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/CWidgetAbilityTargetCheckReceiver.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/CWidgetAbilityTargetCheckReceiver.java new file mode 100644 index 0000000..3dd15f7 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/CWidgetAbilityTargetCheckReceiver.java @@ -0,0 +1,64 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.util; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; + +public class CWidgetAbilityTargetCheckReceiver implements AbilityTargetCheckReceiver { + public static final CWidgetAbilityTargetCheckReceiver INSTANCE = new CWidgetAbilityTargetCheckReceiver(); + + private CWidget target; + + @Override + public void targetOk(final CWidget target) { + this.target = target; + } + + @Override + public void mustTargetTeamType(final TeamType correctType) { + this.target = null; + } + + @Override + public void mustTargetType(final TargetType correctType) { + this.target = null; + } + + @Override + public void mustTargetResources() { + this.target = null; + } + + @Override + public void targetOutsideRange(final double howMuch) { + this.target = null; + } + + @Override + public void notAnActiveAbility() { + this.target = null; + } + + @Override + public void targetNotVisible() { + this.target = null; + } + + @Override + public void targetTooComplicated() { + this.target = null; + } + + @Override + public void targetNotInPlayableMap() { + this.target = null; + } + + @Override + public void orderIdNotAccepted() { + this.target = null; + } + + public CWidget getTarget() { + return this.target; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/MeleeUIAbilityActivationReceiver.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/MeleeUIAbilityActivationReceiver.java new file mode 100644 index 0000000..fe5420d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/MeleeUIAbilityActivationReceiver.java @@ -0,0 +1,86 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.util; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.AudioContext; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.CommandErrorListener; + +public class MeleeUIAbilityActivationReceiver implements AbilityActivationReceiver { + private final AbilityActivationErrorHandler noGoldError; + private final AbilityActivationErrorHandler noLumberError; + private final AbilityActivationErrorHandler noFoodError; + private final AbilityActivationErrorHandler genericError; + + private boolean ok = false; + private CommandErrorListener commandErrorListener; + private AudioContext worldSceneAudioContext; + private RenderUnit commandedUnit; + + public MeleeUIAbilityActivationReceiver(final AbilityActivationErrorHandler noGoldError, + final AbilityActivationErrorHandler noLumberError, final AbilityActivationErrorHandler noFoodError, + final AbilityActivationErrorHandler genericError) { + this.noGoldError = noGoldError; + this.noLumberError = noLumberError; + this.noFoodError = noFoodError; + this.genericError = genericError; + } + + public MeleeUIAbilityActivationReceiver reset(final CommandErrorListener commandErrorListener, + final AudioContext worldSceneAudioContext, final RenderUnit commandedUnit) { + this.commandErrorListener = commandErrorListener; + this.worldSceneAudioContext = worldSceneAudioContext; + this.commandedUnit = commandedUnit; + this.ok = false; + return this; + } + + @Override + public void useOk() { + this.ok = true; + } + + @Override + public void notEnoughResources(final ResourceType resource) { + switch (resource) { + case GOLD: + this.noGoldError.onClick(this.commandErrorListener, this.worldSceneAudioContext, this.commandedUnit); + break; + case LUMBER: + this.noLumberError.onClick(this.commandErrorListener, this.worldSceneAudioContext, this.commandedUnit); + break; + case FOOD: + this.noFoodError.onClick(this.commandErrorListener, this.worldSceneAudioContext, this.commandedUnit); + break; + } + } + + @Override + public void notAnActiveAbility() { + this.genericError.onClick(this.commandErrorListener, this.worldSceneAudioContext, this.commandedUnit); + } + + @Override + public void missingRequirement(final War3ID type, final int level) { + this.genericError.onClick(this.commandErrorListener, this.worldSceneAudioContext, this.commandedUnit); + } + + @Override + public void casterMovementDisabled() { + this.genericError.onClick(this.commandErrorListener, this.worldSceneAudioContext, this.commandedUnit); + } + + @Override + public void cargoCapacityUnavailable() { + this.genericError.onClick(this.commandErrorListener, this.worldSceneAudioContext, this.commandedUnit); + } + + @Override + public void disabled() { + this.genericError.onClick(this.commandErrorListener, this.worldSceneAudioContext, this.commandedUnit); + } + + public boolean isUseOk() { + return this.ok; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/PointAbilityTargetCheckReceiver.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/PointAbilityTargetCheckReceiver.java new file mode 100644 index 0000000..0a17401 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/PointAbilityTargetCheckReceiver.java @@ -0,0 +1,64 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.util; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; + +public class PointAbilityTargetCheckReceiver implements AbilityTargetCheckReceiver { + public static final PointAbilityTargetCheckReceiver INSTANCE = new PointAbilityTargetCheckReceiver(); + + private AbilityPointTarget target; + + @Override + public void targetOk(final AbilityPointTarget target) { + this.target = target; + } + + @Override + public void mustTargetTeamType(final TeamType correctType) { + this.target = null; + } + + @Override + public void mustTargetType(final TargetType correctType) { + this.target = null; + } + + @Override + public void mustTargetResources() { + this.target = null; + } + + @Override + public void targetOutsideRange(final double howMuch) { + this.target = null; + } + + @Override + public void notAnActiveAbility() { + this.target = null; + } + + @Override + public void targetNotVisible() { + this.target = null; + } + + @Override + public void targetTooComplicated() { + this.target = null; + } + + @Override + public void targetNotInPlayableMap() { + this.target = null; + } + + @Override + public void orderIdNotAccepted() { + this.target = null; + } + + public AbilityPointTarget getTarget() { + return this.target; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/ResourceType.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/ResourceType.java new file mode 100644 index 0000000..6e7977c --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/ResourceType.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.util; + +public enum ResourceType { + GOLD, + LUMBER, + FOOD; + + public static final ResourceType[] VALUES = values(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/SimulationRenderController.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/SimulationRenderController.java new file mode 100644 index 0000000..05b9350 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/SimulationRenderController.java @@ -0,0 +1,58 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.util; + +import java.awt.image.BufferedImage; + +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CDestructable; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackInstant; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackListener; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackMissile; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.projectile.CAttackProjectile; + +public interface SimulationRenderController { + CAttackProjectile createAttackProjectile(CSimulation simulation, float launchX, float launchY, float launchFacing, + CUnit source, CUnitAttackMissile attack, AbilityTarget target, float damage, int bounceIndex, + CUnitAttackListener attackListener); + + CUnit createUnit(CSimulation simulation, final War3ID typeId, final int playerIndex, final float x, final float y, + final float facing); + + void createInstantAttackEffect(CSimulation cSimulation, CUnit source, CUnitAttackInstant attack, CWidget target); + + void spawnDamageSound(CWidget damagedDestructable, String weaponSound, String armorType); + + void spawnUnitConstructionSound(CUnit constructingUnit, CUnit constructedStructure); + + void removeUnit(CUnit unit); + + void removeDestructable(CDestructable dest); + + BufferedImage getBuildingPathingPixelMap(War3ID rawcode); + + BufferedImage getDestructablePathingPixelMap(War3ID rawcode); + + BufferedImage getDestructablePathingDeathPixelMap(War3ID rawcode); + + void spawnUnitConstructionFinishSound(CUnit constructedStructure); + + void spawnBuildingDeathEffect(CUnit cUnit); + + void spawnGainLevelEffect(CUnit cUnit); + + void spawnUnitReadySound(CUnit trainedUnit); + + void unitRepositioned(CUnit cUnit); + + void spawnGainResourceTextTag(CUnit gainingUnit, ResourceType resourceType, int amount); + + void spawnEffectOnUnit(CUnit unit, String effectPath); + + void spawnUIUnitGetItemSound(CUnit cUnit, CItem item); + + void spawnUIUnitDropItemSound(CUnit cUnit, CItem item); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/StringMsgAbilityActivationReceiver.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/StringMsgAbilityActivationReceiver.java new file mode 100644 index 0000000..5f8697c --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/StringMsgAbilityActivationReceiver.java @@ -0,0 +1,58 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.util; + +import com.etheller.warsmash.util.War3ID; + +public class StringMsgAbilityActivationReceiver implements AbilityActivationReceiver { + private String message; + private boolean useOk = false; + + public StringMsgAbilityActivationReceiver reset() { + this.message = null; + this.useOk = false; + return this; + } + + public String getMessage() { + return this.message; + } + + public boolean isUseOk() { + return this.useOk; + } + + @Override + public void useOk() { + this.useOk = true; + } + + @Override + public void notEnoughResources(final ResourceType resource) { + this.message = "NOTEXTERN: Requires more " + resource.name().toLowerCase() + "."; + } + + @Override + public void notAnActiveAbility() { + this.message = "NOTEXTERN: Not an active ability."; + } + + @Override + public void missingRequirement(final War3ID type, final int level) { + this.message = "NOTEXTERN: Requires " + type; + } + + @Override + public void cargoCapacityUnavailable() { + this.message = "NOTEXTERN: Cargo capacity unavailable."; + } + + @Override + public void casterMovementDisabled() { + this.message = "NOTEXTERN: Caster movement disabled."; + } + + @Override + public void disabled() { + this.message = "NOTEXTERN: Ability is disabled."; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/StringMsgTargetCheckReceiver.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/StringMsgTargetCheckReceiver.java new file mode 100644 index 0000000..15008bc --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/simulation/util/StringMsgTargetCheckReceiver.java @@ -0,0 +1,101 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.simulation.util; + +public final class StringMsgTargetCheckReceiver implements AbilityTargetCheckReceiver { + private static final StringMsgTargetCheckReceiver INSTANCE = new StringMsgTargetCheckReceiver<>(); + + public static StringMsgTargetCheckReceiver getInstance() { + return (StringMsgTargetCheckReceiver) INSTANCE; + } + + private TARGET_TYPE target; + private String message; + + public TARGET_TYPE getTarget() { + return this.target; + } + + public String getMessage() { + return this.message; + } + + public StringMsgTargetCheckReceiver reset() { + this.target = null; + this.message = null; + return this; + } + + @Override + public void targetOk(final TARGET_TYPE target) { + this.target = target; + } + + @Override + public void mustTargetTeamType(final TeamType correctType) { + switch (correctType) { + case ALLIED: + this.message = "NOTEXTERN: Must target an allied unit."; + break; + case ENEMY: + this.message = "NOTEXTERN: Must target an enemy unit."; + break; + case PLAYER_UNITS: + this.message = "NOTEXTERN: Unable to target a unit you do not control."; + break; + default: + this.message = "NOTEXTERN: Must target team type: " + correctType; + } + } + + @Override + public void mustTargetType(final TargetType correctType) { + switch (correctType) { + case POINT: + this.message = "NOTEXTERN: Must target a point."; + break; + case UNIT: + this.message = "NOTEXTERN: Must target a unit."; + break; + case UNIT_OR_POINT: + this.message = "NOTEXTERN: Must target a unit or point."; + break; + default: + this.message = "NOTEXTERN: Must target type: " + correctType; + } + } + + @Override + public void mustTargetResources() { + this.message = "NOTEXTERN: Must target resources."; + } + + @Override + public void targetOutsideRange(final double howMuch) { + this.message = "NOTEXTERN: Target is outside range."; + } + + @Override + public void notAnActiveAbility() { + this.message = "NOTEXTERN: Not an active ability."; + } + + @Override + public void targetNotVisible() { + this.message = "NOTEXTERN: Target is not visible."; + } + + @Override + public void targetTooComplicated() { + this.message = "NOTEXTERN: Target is too complicated."; + } + + @Override + public void targetNotInPlayableMap() { + this.message = "NOTEXTERN: Target is not within the designed combat area."; + } + + @Override + public void orderIdNotAccepted() { + this.message = "NOTEXTERN: OrderID not accepted."; + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/CommandCardIcon.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/CommandCardIcon.java new file mode 100644 index 0000000..15bf620 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/CommandCardIcon.java @@ -0,0 +1,227 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui; + +import com.badlogic.gdx.Input; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.frames.AbstractRenderableFrame; +import com.etheller.warsmash.parsers.fdf.frames.SpriteFrame; +import com.etheller.warsmash.parsers.fdf.frames.TextureFrame; +import com.etheller.warsmash.parsers.fdf.frames.UIFrame; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.ClickableActionFrame; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.CommandCardCommandListener; + +public class CommandCardIcon extends AbstractRenderableFrame implements ClickableActionFrame { + + private TextureFrame iconFrame; + private TextureFrame activeHighlightFrame; + private SpriteFrame cooldownFrame; + private SpriteFrame autocastFrame; + private float defaultWidth; + private float defaultHeight; + private int abilityHandleId; + private int orderId; + private int autoCastOrderId; + private boolean autoCastActive; + private final CommandCardCommandListener commandCardCommandListener; + private boolean menuButton; + private String tip; + private String uberTip; + private int tipGoldCost; + private int tipLumberCost; + private int tipFoodCost; + + public CommandCardIcon(final String name, final UIFrame parent, + final CommandCardCommandListener commandCardCommandListener) { + super(name, parent); + this.commandCardCommandListener = commandCardCommandListener; + } + + public void set(final TextureFrame iconFrame, final TextureFrame activeHighlightFrame, + final SpriteFrame cooldownFrame, final SpriteFrame autocastFrame) { + this.iconFrame = iconFrame; + this.activeHighlightFrame = activeHighlightFrame; + this.cooldownFrame = cooldownFrame; + this.autocastFrame = autocastFrame; + } + + public void clear() { + this.iconFrame.setVisible(false); + if (this.activeHighlightFrame != null) { + this.activeHighlightFrame.setVisible(false); + } + this.cooldownFrame.setVisible(false); + if (this.autocastFrame != null) { + this.autocastFrame.setVisible(false); + } + setVisible(false); + } + + public void setCommandButtonData(final Texture texture, final int abilityHandleId, final int orderId, + final int autoCastOrderId, final boolean active, final boolean autoCastActive, final boolean menuButton, + final String tip, final String uberTip, final int goldCost, final int lumberCost, final int foodCost) { + this.menuButton = menuButton; + setVisible(true); + this.iconFrame.setVisible(true); + if (this.activeHighlightFrame != null) { + this.activeHighlightFrame.setVisible(active); + } + this.cooldownFrame.setVisible(false); + if (this.autocastFrame != null) { + this.autocastFrame.setVisible(autoCastOrderId != 0); + if (autoCastOrderId != 0) { + if (this.autoCastActive != autoCastActive) { + if (autoCastActive) { + this.autocastFrame.setSequence(PrimaryTag.STAND); + } + else { + this.autocastFrame.setSequence(-1); + } + } + this.autoCastActive = autoCastActive; + } + } + this.iconFrame.setTexture(texture); + this.abilityHandleId = abilityHandleId; + this.orderId = orderId; + this.autoCastOrderId = autoCastOrderId; + this.tip = tip; + this.uberTip = uberTip; + this.tipGoldCost = goldCost; + this.tipLumberCost = lumberCost; + this.tipFoodCost = foodCost; + } + + @Override + protected void innerPositionBounds(final GameUI gameUI, final Viewport viewport) { + this.iconFrame.positionBounds(gameUI, viewport); + if (this.activeHighlightFrame != null) { + this.activeHighlightFrame.positionBounds(gameUI, viewport); + } + this.cooldownFrame.positionBounds(gameUI, viewport); + if (this.autocastFrame != null) { + this.autocastFrame.positionBounds(gameUI, viewport); + } + } + + @Override + protected void internalRender(final SpriteBatch batch, final BitmapFont baseFont, final GlyphLayout glyphLayout) { + this.iconFrame.render(batch, baseFont, glyphLayout); + if (this.activeHighlightFrame != null) { + this.activeHighlightFrame.render(batch, baseFont, glyphLayout); + } + this.cooldownFrame.render(batch, baseFont, glyphLayout); + if (this.autocastFrame != null) { + this.autocastFrame.render(batch, baseFont, glyphLayout); + } + } + + @Override + public UIFrame touchDown(final float screenX, final float screenY, final int button) { + if (isVisible() && this.renderBounds.contains(screenX, screenY)) { + if (((button == Input.Buttons.LEFT) && (this.orderId != 0)) + || ((button == Input.Buttons.RIGHT) && (this.autoCastOrderId != 0)) || this.menuButton) { + return this; + } + } + return super.touchDown(screenX, screenY, button); + } + + @Override + public UIFrame touchUp(final float screenX, final float screenY, final int button) { + if (isVisible() && this.renderBounds.contains(screenX, screenY)) { + return this; + } + return super.touchUp(screenX, screenY, button); + } + + public boolean isMenuButton() { + return this.menuButton; + } + + @Override + public void onClick(final int button) { + if (button == Input.Buttons.LEFT) { + if (this.menuButton) { + this.commandCardCommandListener.openMenu(this.orderId); + } + else { + this.commandCardCommandListener.onClick(this.abilityHandleId, this.orderId, false); + } + } + else if (button == Input.Buttons.RIGHT) { + this.commandCardCommandListener.onClick(this.abilityHandleId, this.autoCastOrderId, true); + } + } + + @Override + public void mouseDown(final GameUI gameUI, final Viewport uiViewport) { + this.iconFrame.setWidth(this.defaultWidth * 0.95f); + this.iconFrame.setHeight(this.defaultHeight * 0.95f); + positionBounds(gameUI, uiViewport); + } + + @Override + public void mouseUp(final GameUI gameUI, final Viewport uiViewport) { + this.iconFrame.setWidth(this.defaultWidth); + this.iconFrame.setHeight(this.defaultHeight); + positionBounds(gameUI, uiViewport); + } + + @Override + public void setWidth(final float width) { + this.defaultWidth = width; + super.setWidth(width); + } + + @Override + public void setHeight(final float height) { + this.defaultHeight = height; + super.setHeight(height); + } + + @Override + public void mouseEnter(final GameUI gameUI, final Viewport uiViewport) { + } + + @Override + public void mouseExit(final GameUI gameUI, final Viewport uiViewport) { + } + + @Override + public UIFrame getFrameChildUnderMouse(final float screenX, final float screenY) { + if (isVisible() && this.renderBounds.contains(screenX, screenY)) { + return this; + } + return null; + } + + @Override + public String getToolTip() { + return this.tip; + } + + @Override + public String getUberTip() { + return this.uberTip; + } + + @Override + public int getToolTipGoldCost() { + return this.tipGoldCost; + } + + @Override + public int getToolTipLumberCost() { + return this.tipLumberCost; + } + + @Override + public int getToolTipFoodCost() { + return this.tipFoodCost; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/MeleeUI.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/MeleeUI.java new file mode 100644 index 0000000..84064df --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/MeleeUI.java @@ -0,0 +1,2753 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.TimeUnit; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Input; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.Pixmap; +import com.badlogic.gdx.graphics.Pixmap.Blending; +import com.badlogic.gdx.graphics.Pixmap.Format; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator.FreeTypeFontParameter; +import com.badlogic.gdx.graphics.glutils.PixmapTextureData; +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.math.Vector3; +import com.badlogic.gdx.utils.TimeUtils; +import com.badlogic.gdx.utils.viewport.ExtendViewport; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.datamodel.AnchorDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.FramePoint; +import com.etheller.warsmash.parsers.fdf.datamodel.TextJustify; +import com.etheller.warsmash.parsers.fdf.datamodel.Vector4Definition; +import com.etheller.warsmash.parsers.fdf.frames.AbstractUIFrame; +import com.etheller.warsmash.parsers.fdf.frames.FilterModeTextureFrame; +import com.etheller.warsmash.parsers.fdf.frames.GlueTextButtonFrame; +import com.etheller.warsmash.parsers.fdf.frames.SetPoint; +import com.etheller.warsmash.parsers.fdf.frames.SimpleButtonFrame; +import com.etheller.warsmash.parsers.fdf.frames.SimpleFrame; +import com.etheller.warsmash.parsers.fdf.frames.SimpleStatusBarFrame; +import com.etheller.warsmash.parsers.fdf.frames.SpriteFrame; +import com.etheller.warsmash.parsers.fdf.frames.StringFrame; +import com.etheller.warsmash.parsers.fdf.frames.TextureFrame; +import com.etheller.warsmash.parsers.fdf.frames.UIFrame; +import com.etheller.warsmash.parsers.jass.Jass2.RootFrameListener; +import com.etheller.warsmash.units.Element; +import com.etheller.warsmash.units.manager.MutableObjectData; +import com.etheller.warsmash.util.FastNumberFormat; +import com.etheller.warsmash.util.ImageUtils; +import com.etheller.warsmash.util.RenderMathUtils; +import com.etheller.warsmash.util.War3ID; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.ViewerTextureRenderable; +import com.etheller.warsmash.viewer5.handlers.mdx.Attachment; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxModel; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxNode; +import com.etheller.warsmash.viewer5.handlers.mdx.ReplaceableIds; +import com.etheller.warsmash.viewer5.handlers.mdx.SequenceLoopMode; +import com.etheller.warsmash.viewer5.handlers.tga.TgaFile; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.PrimaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.AnimationTokens.SecondaryTag; +import com.etheller.warsmash.viewer5.handlers.w3x.SequenceUtils; +import com.etheller.warsmash.viewer5.handlers.w3x.SplatModel; +import com.etheller.warsmash.viewer5.handlers.w3x.SplatModel.SplatMover; +import com.etheller.warsmash.viewer5.handlers.w3x.TextTag; +import com.etheller.warsmash.viewer5.handlers.w3x.UnitSound; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; +import com.etheller.warsmash.viewer5.handlers.w3x.camera.CameraPreset; +import com.etheller.warsmash.viewer5.handlers.w3x.camera.CameraRates; +import com.etheller.warsmash.viewer5.handlers.w3x.camera.GameCameraManager; +import com.etheller.warsmash.viewer5.handlers.w3x.camera.PortraitCameraManager; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid; +import com.etheller.warsmash.viewer5.handlers.w3x.environment.PathingGrid.PathingFlags; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.ability.AbilityDataUI; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.ability.IconUI; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.ability.ItemUI; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.commandbuttons.CommandButtonListener; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CDestructable; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CGameplayConstants; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItem; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CItemType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CPlayerStateListener; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit.QueueItemType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitClassification; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitStateListener; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnitType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CWidgetFilterFunction; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityAttack; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityGeneric; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityMove; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityView; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.CAbilityVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.AbstractCAbilityBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityBuildInProgress; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityHumanBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityNagaBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityNeutralBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityNightElfBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityOrcBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.build.CAbilityUndeadBuild; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.combat.CAbilityColdArrows; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic.GenericNoIconAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.generic.GenericSingleIconActiveAbility; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.hero.CAbilityHero; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.hero.CPrimaryAttribute; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.inventory.CAbilityInventory; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.queue.CAbilityQueue; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.queue.CAbilityRally; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityPointTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTarget; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.abilities.targeting.AbilityTargetVisitor; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CAttackType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CDefenseType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.CodeKeyType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttack; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.combat.attacks.CUnitAttackMissileSplash; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.COrder; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.orders.OrderIds; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.pathing.CBuildingPathingType; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayerUnitOrderListener; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CRace; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.AbilityActivationErrorHandler; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.BooleanAbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.BooleanAbilityTargetCheckReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.CWidgetAbilityTargetCheckReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.MeleeUIAbilityActivationReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.PointAbilityTargetCheckReceiver; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.util.ResourceType; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.ClickableActionFrame; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.ClickableFrame; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.CommandCardCommandListener; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.CommandErrorListener; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.QueueIconListener; +import com.hiveworkshop.rms.parsers.mdlx.MdlxLayer.FilterMode; + +public class MeleeUI implements CUnitStateListener, CommandButtonListener, CommandCardCommandListener, + QueueIconListener, CommandErrorListener, CPlayerStateListener { + private static final long WORLD_FRAME_MESSAGE_FADEOUT_MILLIS = TimeUnit.SECONDS.toMillis(9); + private static final long WORLD_FRAME_MESSAGE_EXPIRE_MILLIS = TimeUnit.SECONDS.toMillis(10); + private static final long WORLD_FRAME_MESSAGE_FADE_DURATION = WORLD_FRAME_MESSAGE_EXPIRE_MILLIS + - WORLD_FRAME_MESSAGE_FADEOUT_MILLIS; + private static final String BUILDING_PATHING_PREVIEW_KEY = "buildingPathingPreview"; + public static final float DEFAULT_COMMAND_CARD_ICON_WIDTH = 0.039f; + public static final float DEFAULT_INVENTORY_ICON_WIDTH = 0.03125f; + private static final int COMMAND_CARD_WIDTH = 4; + private static final int COMMAND_CARD_HEIGHT = 3; + private static final int INVENTORY_WIDTH = 2; + private static final int INVENTORY_HEIGHT = 3; + + private static final Vector2 screenCoordsVector = new Vector2(); + private static final Vector3 clickLocationTemp = new Vector3(); + private static final AbilityPointTarget clickLocationTemp2 = new AbilityPointTarget(); + private final DataSource dataSource; + private final ExtendViewport uiViewport; + private final Scene uiScene; + private final Scene portraitScene; + private final GameCameraManager cameraManager; + private final War3MapViewer war3MapViewer; + private final RootFrameListener rootFrameListener; + private GameUI rootFrame; + private UIFrame consoleUI; + private UIFrame resourceBar; + private StringFrame resourceBarGoldText; + private StringFrame resourceBarLumberText; + private StringFrame resourceBarSupplyText; + private StringFrame resourceBarUpkeepText; + private SpriteFrame timeIndicator; + private UIFrame unitPortrait; + private StringFrame unitLifeText; + private StringFrame unitManaText; + private Portrait portrait; + private final Rectangle tempRect = new Rectangle(); + private final Vector2 projectionTemp1 = new Vector2(); + private final Vector2 projectionTemp2 = new Vector2(); + + // tooltip + private UIFrame tooltipFrame; + private StringFrame tooltipText; + private StringFrame tooltipUberTipText; + private UIFrame[] tooltipResourceFrames; + private TextureFrame[] tooltipResourceIconFrames; + private StringFrame[] tooltipResourceTextFrames; + + private UIFrame simpleInfoPanelUnitDetail; + private StringFrame simpleNameValue; + private StringFrame simpleClassValue; + private StringFrame simpleBuildingActionLabel; + private SimpleStatusBarFrame simpleBuildTimeIndicator; + private SimpleStatusBarFrame simpleHeroLevelBar; + + private UIFrame simpleInfoPanelBuildingDetail; + private StringFrame simpleBuildingNameValue; + private StringFrame simpleBuildingDescriptionValue; + private StringFrame simpleBuildingBuildingActionLabel; + private SimpleStatusBarFrame simpleBuildingBuildTimeIndicator; + private final QueueIcon[] queueIconFrames = new QueueIcon[WarsmashConstants.BUILD_QUEUE_SIZE]; + private QueueIcon selectWorkerInsideFrame; + + private UIFrame attack1Icon; + private TextureFrame attack1IconBackdrop; + private StringFrame attack1InfoPanelIconValue; + private StringFrame attack1InfoPanelIconLevel; + private UIFrame attack2Icon; + private TextureFrame attack2IconBackdrop; + private StringFrame attack2InfoPanelIconValue; + private StringFrame attack2InfoPanelIconLevel; + private UIFrame armorIcon; + private TextureFrame armorIconBackdrop; + private StringFrame armorInfoPanelIconValue; + private StringFrame armorInfoPanelIconLevel; + private InfoPanelIconBackdrops damageBackdrops; + private InfoPanelIconBackdrops defenseBackdrops; + + private UIFrame heroInfoPanel; + + private SimpleFrame inventoryBarFrame; + private StringFrame inventoryTitleFrame; + private final CommandCardIcon[][] inventoryIcons = new CommandCardIcon[INVENTORY_HEIGHT][INVENTORY_WIDTH]; + private Texture consoleInventoryNoCapacityTexture; + + private final CommandCardIcon[][] commandCard = new CommandCardIcon[COMMAND_CARD_HEIGHT][COMMAND_CARD_WIDTH]; + + private RenderUnit selectedUnit; + private final List subMenuOrderIdStack = new ArrayList<>(); + + // TODO remove this & replace with FDF + private final Texture activeButtonTexture; + private UIFrame inventoryCover; + private SpriteFrame cursorFrame; + private MeleeUIMinimap meleeUIMinimap; + private final CPlayerUnitOrderListener unitOrderListener; + private StringFrame errorMessageFrame; + private long lastErrorMessageExpireTime; + private long lastErrorMessageFadeTime; + + private CAbilityView activeCommand; + private int activeCommandOrderId; + private RenderUnit activeCommandUnit; + private MdxComplexInstance cursorModelInstance = null; + private MdxComplexInstance rallyPointInstance = null; + private BufferedImage cursorModelPathing; + private Pixmap cursorModelUnderneathPathingRedGreenPixmap; + private Texture cursorModelUnderneathPathingRedGreenPixmapTexture; + private PixmapTextureData cursorModelUnderneathPathingRedGreenPixmapTextureData; + private SplatModel cursorModelUnderneathPathingRedGreenSplatModel; + private CUnitType cursorBuildingUnitType; + private SplatMover placementCursor = null; + private final CursorTargetSetupVisitor cursorTargetSetupVisitor; + + private int selectedSoundCount = 0; + private final ActiveCommandUnitTargetFilter activeCommandUnitTargetFilter; + + // TODO these corrections are used for old hardcoded UI stuff, we should + // probably remove them later + private final float widthRatioCorrection; + private final float heightRatioCorrection; + private ClickableFrame mouseDownUIFrame; + private ClickableFrame mouseOverUIFrame; + private UIFrame smashSimpleInfoPanel; + private SimpleFrame smashAttack1IconWrapper; + private SimpleFrame smashAttack2IconWrapper; + private SimpleFrame smashArmorIconWrapper; + private final RallyPositioningVisitor rallyPositioningVisitor; + private final CPlayer localPlayer; + private MeleeUIAbilityActivationReceiver meleeUIAbilityActivationReceiver; + private MdxModel waypointModel; + private final List waypointModelInstances = new ArrayList<>(); + private List selectedUnits; + private BitmapFont textTagFont; + private SetPoint uberTipNoResourcesSetPoint; + private SetPoint uberTipWithResourcesSetPoint; + private TextureFrame primaryAttributeIcon; + private StringFrame strengthValue; + private StringFrame agilityValue; + private StringFrame intelligenceValue; + private SimpleFrame smashHeroInfoPanelWrapper; + + private final StringBuilder recycleStringBuilder = new StringBuilder(); + private CItem draggingItem; + private final ItemCommandCardCommandListener itemCommandCardCommandListener; + private SimpleButtonFrame questsButton; + private SimpleButtonFrame menuButton; + private SimpleButtonFrame alliesButton; + private SimpleButtonFrame chatButton; + private final Runnable exitGameRunnable; + private SimpleFrame smashEscMenu; + + public MeleeUI(final DataSource dataSource, final ExtendViewport uiViewport, final Scene uiScene, + final Scene portraitScene, final CameraPreset[] cameraPresets, final CameraRates cameraRates, + final War3MapViewer war3MapViewer, final RootFrameListener rootFrameListener, + final CPlayerUnitOrderListener unitOrderListener, final Runnable exitGameRunnable) { + this.dataSource = dataSource; + this.uiViewport = uiViewport; + this.uiScene = uiScene; + this.portraitScene = portraitScene; + this.war3MapViewer = war3MapViewer; + this.rootFrameListener = rootFrameListener; + this.unitOrderListener = unitOrderListener; + this.exitGameRunnable = exitGameRunnable; + + this.cameraManager = new GameCameraManager(cameraPresets, cameraRates); + + this.cameraManager.setupCamera(war3MapViewer.worldScene); + this.localPlayer = this.war3MapViewer.simulation.getPlayer(war3MapViewer.getLocalPlayerIndex()); + final float[] startLocation = this.localPlayer.getStartLocation(); + this.cameraManager.target.x = startLocation[0]; + this.cameraManager.target.y = startLocation[1]; + + this.activeButtonTexture = ImageUtils.getAnyExtensionTexture(war3MapViewer.mapMpq, + "UI\\Widgets\\Console\\Human\\CommandButton\\human-activebutton.blp"); + this.activeCommandUnitTargetFilter = new ActiveCommandUnitTargetFilter(); + this.widthRatioCorrection = this.uiViewport.getMinWorldWidth() / 1600f; + this.heightRatioCorrection = this.uiViewport.getMinWorldHeight() / 1200f; + this.rallyPositioningVisitor = new RallyPositioningVisitor(); + this.cursorTargetSetupVisitor = new CursorTargetSetupVisitor(); + + this.localPlayer.addStateListener(this); + + this.itemCommandCardCommandListener = new ItemCommandCardCommandListener(); + } + + private MeleeUIMinimap createMinimap(final War3MapViewer war3MapViewer) { + final Rectangle minimapDisplayArea = new Rectangle(18.75f * this.widthRatioCorrection, + 13.75f * this.heightRatioCorrection, 278.75f * this.widthRatioCorrection, + 276.25f * this.heightRatioCorrection); + Texture minimapTexture = null; + if (war3MapViewer.dataSource.has("war3mapMap.tga")) { + try { + minimapTexture = ImageUtils.getTextureNoColorCorrection(TgaFile.readTGA("war3mapMap.tga", + war3MapViewer.dataSource.getResourceAsStream("war3mapMap.tga"))); + } + catch (final IOException e) { + System.err.println("Could not load minimap TGA file"); + e.printStackTrace(); + } + } + else if (war3MapViewer.dataSource.has("war3mapMap.blp")) { + minimapTexture = ImageUtils.getAnyExtensionTexture(war3MapViewer.dataSource, "war3mapMap.blp"); + } + final Texture[] teamColors = new Texture[WarsmashConstants.MAX_PLAYERS]; + for (int i = 0; i < teamColors.length; i++) { + teamColors[i] = ImageUtils.getAnyExtensionTexture(war3MapViewer.dataSource, + "ReplaceableTextures\\" + ReplaceableIds.getPathString(1) + ReplaceableIds.getIdString(i) + ".blp"); + } + final Rectangle playableMapArea = war3MapViewer.terrain.getPlayableMapArea(); + return new MeleeUIMinimap(minimapDisplayArea, playableMapArea, minimapTexture, teamColors); + } + + /** + * Called "main" because this was originally written in JASS so that maps could + * override it, and I may convert it back to the JASS at some point. + */ + public void main() { + // ================================= + // Load skins and templates + // ================================= + final CRace race = this.localPlayer.getRace(); + final String racialSkinKey; + int racialCommandIndex; + if (race == null) { + racialSkinKey = "Human"; + racialCommandIndex = 0; + } + else { + switch (race) { + case HUMAN: + racialSkinKey = "Human"; + racialCommandIndex = 0; + break; + case ORC: + racialSkinKey = "Orc"; + racialCommandIndex = 1; + break; + case NIGHTELF: + racialSkinKey = "NightElf"; + racialCommandIndex = 3; + break; + case UNDEAD: + racialSkinKey = "Undead"; + racialCommandIndex = 2; + break; + case DEMON: + case OTHER: + default: + racialSkinKey = "Human"; + racialCommandIndex = 0; + break; + } + } + this.rootFrame = new GameUI(this.dataSource, GameUI.loadSkin(this.dataSource, racialSkinKey), this.uiViewport, + this.uiScene, this.war3MapViewer, racialCommandIndex, this.war3MapViewer.getAllObjectData().getWts()); + this.rootFrameListener.onCreate(this.rootFrame); + try { + this.rootFrame.loadTOCFile("UI\\FrameDef\\FrameDef.toc"); + } + catch (final IOException exc) { + throw new IllegalStateException("Unable to load FrameDef.toc", exc); + } + try { + this.rootFrame.loadTOCFile("UI\\FrameDef\\SmashFrameDef.toc"); + } + catch (final IOException exc) { + throw new IllegalStateException("Unable to load SmashFrameDef.toc", exc); + } + this.damageBackdrops = new InfoPanelIconBackdrops(CAttackType.values(), this.rootFrame, "Damage", "Neutral"); + this.defenseBackdrops = new InfoPanelIconBackdrops(CDefenseType.values(), this.rootFrame, "Armor", "Neutral"); + + // ================================= + // Load major UI components + // ================================= + // Console UI is the background with the racial theme + this.consoleUI = this.rootFrame.createSimpleFrame("ConsoleUI", this.rootFrame, 0); + this.consoleUI.setSetAllPoints(true); + + // Resource bar is a 3 part bar with Gold, Lumber, and Food. + // Its template does not specify where to put it, so we must + // put it in the "TOPRIGHT" corner. + this.resourceBar = this.rootFrame.createSimpleFrame("ResourceBarFrame", this.consoleUI, 0); + this.resourceBar.addSetPoint(new SetPoint(FramePoint.TOPRIGHT, this.consoleUI, FramePoint.TOPRIGHT, 0, 0)); + this.resourceBarGoldText = (StringFrame) this.rootFrame.getFrameByName("ResourceBarGoldText", 0); + goldChanged(); + this.resourceBarLumberText = (StringFrame) this.rootFrame.getFrameByName("ResourceBarLumberText", 0); + lumberChanged(); + this.resourceBarSupplyText = (StringFrame) this.rootFrame.getFrameByName("ResourceBarSupplyText", 0); + foodChanged(); + this.resourceBarUpkeepText = (StringFrame) this.rootFrame.getFrameByName("ResourceBarUpkeepText", 0); + upkeepChanged(); + + final UIFrame upperButtonBar = this.rootFrame.createSimpleFrame("UpperButtonBarFrame", this.consoleUI, 0); + upperButtonBar.addSetPoint(new SetPoint(FramePoint.TOPLEFT, this.consoleUI, FramePoint.TOPLEFT, 0, 0)); + + this.questsButton = (SimpleButtonFrame) this.rootFrame.getFrameByName("UpperButtonBarQuestsButton", 0); + this.questsButton.setEnabled(false); + this.menuButton = (SimpleButtonFrame) this.rootFrame.getFrameByName("UpperButtonBarMenuButton", 0); + this.alliesButton = (SimpleButtonFrame) this.rootFrame.getFrameByName("UpperButtonBarAlliesButton", 0); + this.alliesButton.setEnabled(false); + this.chatButton = (SimpleButtonFrame) this.rootFrame.getFrameByName("UpperButtonBarChatButton", 0); + this.chatButton.setEnabled(false); + + this.smashEscMenu = (SimpleFrame) this.rootFrame.createSimpleFrame("SmashEscMenu", this.rootFrame, 0); + this.smashEscMenu.addAnchor(new AnchorDefinition(FramePoint.TOP, 0, GameUI.convertY(this.uiViewport, -0.05f))); + final UIFrame escMenuBackdrop = this.rootFrame.createFrame("EscMenuBackdrop", this.smashEscMenu, 0, 0); + escMenuBackdrop.setVisible(false); + final UIFrame escMenuMainPanel = this.rootFrame.createFrame("EscMenuMainPanel", this.smashEscMenu, 0, 0); + escMenuMainPanel.setVisible(false); + this.smashEscMenu.add(escMenuBackdrop); + this.smashEscMenu.add(escMenuMainPanel); + + final UIFrame escMenuInnerMainPanel = this.rootFrame.getFrameByName("MainPanel", 0); + final GlueTextButtonFrame pauseButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("PauseButton", 0); + pauseButton.setEnabled(false); + final GlueTextButtonFrame saveGameButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("SaveGameButton", + 0); + saveGameButton.setEnabled(false); + final GlueTextButtonFrame loadGameButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("LoadGameButton", + 0); + loadGameButton.setEnabled(false); + final GlueTextButtonFrame optionsButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("OptionsButton", + 0); + optionsButton.setEnabled(false); + final GlueTextButtonFrame helpButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("HelpButton", 0); + helpButton.setEnabled(false); + final GlueTextButtonFrame tipsButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("TipsButton", 0); + tipsButton.setEnabled(false); + final GlueTextButtonFrame endGameButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("EndGameButton", + 0); + final GlueTextButtonFrame returnButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("ReturnButton", 0); + + final UIFrame escMenuInnerEndGamePanel = this.rootFrame.getFrameByName("EndGamePanel", 0); + final GlueTextButtonFrame endGamePreviousButton = (GlueTextButtonFrame) this.rootFrame + .getFrameByName("PreviousButton", 0); + final GlueTextButtonFrame endGameQuitButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("QuitButton", + 0); + final GlueTextButtonFrame endGameRestartButton = (GlueTextButtonFrame) this.rootFrame + .getFrameByName("RestartButton", 0); + endGameRestartButton.setEnabled(false); + final GlueTextButtonFrame endGameExitButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("ExitButton", + 0); + + final UIFrame escMenuInnerConfirmQuitPanel = this.rootFrame.getFrameByName("ConfirmQuitPanel", 0); + final GlueTextButtonFrame confirmQuitCancelButton = (GlueTextButtonFrame) this.rootFrame + .getFrameByName("ConfirmQuitCancelButton", 0); + final GlueTextButtonFrame confirmQuitQuitButton = (GlueTextButtonFrame) this.rootFrame + .getFrameByName("ConfirmQuitQuitButton", 0); + final UIFrame escMenuInnerHelpPanel = this.rootFrame.getFrameByName("HelpPanel", 0); + final UIFrame escMenuInnerTipsPanel = this.rootFrame.getFrameByName("TipsPanel", 0); + escMenuInnerMainPanel.setVisible(false); + escMenuInnerEndGamePanel.setVisible(false); + escMenuInnerConfirmQuitPanel.setVisible(false); + escMenuInnerHelpPanel.setVisible(false); + escMenuInnerTipsPanel.setVisible(false); + + this.menuButton.setOnClick(new Runnable() { + @Override + public void run() { + escMenuBackdrop.setVisible(true); + escMenuMainPanel.setVisible(true); + MeleeUI.this.smashEscMenu.setVisible(true); + escMenuInnerMainPanel.setVisible(true); + updateEscMenuCurrentPanel(escMenuBackdrop, escMenuMainPanel, escMenuInnerMainPanel); + } + }); + returnButton.setOnClick(new Runnable() { + @Override + public void run() { + escMenuBackdrop.setVisible(false); + escMenuMainPanel.setVisible(false); + MeleeUI.this.smashEscMenu.setVisible(false); + escMenuInnerMainPanel.setVisible(false); + } + }); + endGameButton.setOnClick(new Runnable() { + @Override + public void run() { + escMenuInnerMainPanel.setVisible(false); + escMenuInnerEndGamePanel.setVisible(true); + updateEscMenuCurrentPanel(escMenuBackdrop, escMenuMainPanel, escMenuInnerEndGamePanel); + } + }); + endGamePreviousButton.setOnClick(new Runnable() { + @Override + public void run() { + escMenuInnerEndGamePanel.setVisible(false); + escMenuInnerMainPanel.setVisible(true); + updateEscMenuCurrentPanel(escMenuBackdrop, escMenuMainPanel, escMenuInnerMainPanel); + } + }); + endGameQuitButton.setOnClick(new Runnable() { + @Override + public void run() { + escMenuInnerEndGamePanel.setVisible(false); + MeleeUI.this.exitGameRunnable.run(); + } + }); + endGameExitButton.setOnClick(new Runnable() { + @Override + public void run() { + escMenuInnerEndGamePanel.setVisible(false); + escMenuInnerConfirmQuitPanel.setVisible(true); + updateEscMenuCurrentPanel(escMenuBackdrop, escMenuMainPanel, escMenuInnerConfirmQuitPanel); + } + }); + confirmQuitCancelButton.setOnClick(new Runnable() { + @Override + public void run() { + escMenuInnerEndGamePanel.setVisible(true); + escMenuInnerConfirmQuitPanel.setVisible(false); + updateEscMenuCurrentPanel(escMenuBackdrop, escMenuMainPanel, escMenuInnerEndGamePanel); + } + }); + confirmQuitQuitButton.setOnClick(new Runnable() { + @Override + public void run() { + Gdx.app.exit(); + } + }); + + // Create the Time Indicator (clock) + this.timeIndicator = (SpriteFrame) this.rootFrame.createFrame("TimeOfDayIndicator", this.rootFrame, 0, 0); + this.timeIndicator.setSequence(0); // play the stand + this.timeIndicator.setAnimationSpeed(0.0f); // do not advance automatically + + // Create the unit portrait stuff + this.portrait = new Portrait(this.war3MapViewer, this.portraitScene); + positionPortrait(); + this.unitPortrait = this.rootFrame.createSimpleFrame("UnitPortrait", this.consoleUI, 0); + this.unitLifeText = (StringFrame) this.rootFrame.getFrameByName("UnitPortraitHitPointText", 0); + this.unitManaText = (StringFrame) this.rootFrame.getFrameByName("UnitPortraitManaPointText", 0); + + final float infoPanelUnitDetailWidth = GameUI.convertY(this.uiViewport, 0.180f); + final float infoPanelUnitDetailHeight = GameUI.convertY(this.uiViewport, 0.112f); + this.smashSimpleInfoPanel = this.rootFrame.createSimpleFrame("SmashSimpleInfoPanel", this.rootFrame, 0); + this.smashSimpleInfoPanel + .addAnchor(new AnchorDefinition(FramePoint.BOTTOM, 0, GameUI.convertY(this.uiViewport, 0.0f))); + this.smashSimpleInfoPanel.setWidth(infoPanelUnitDetailWidth); + this.smashSimpleInfoPanel.setHeight(infoPanelUnitDetailHeight); + + // Create Simple Info Unit Detail + this.simpleInfoPanelUnitDetail = this.rootFrame.createSimpleFrame("SimpleInfoPanelUnitDetail", + this.smashSimpleInfoPanel, 0); + this.simpleNameValue = (StringFrame) this.rootFrame.getFrameByName("SimpleNameValue", 0); + this.simpleClassValue = (StringFrame) this.rootFrame.getFrameByName("SimpleClassValue", 0); + this.simpleBuildingActionLabel = (StringFrame) this.rootFrame.getFrameByName("SimpleBuildingActionLabel", 0); + this.simpleBuildTimeIndicator = (SimpleStatusBarFrame) this.rootFrame.getFrameByName("SimpleBuildTimeIndicator", + 0); + final TextureFrame simpleBuildTimeIndicatorBar = this.simpleBuildTimeIndicator.getBarFrame(); + simpleBuildTimeIndicatorBar.setTexture("SimpleBuildTimeIndicator", this.rootFrame); + final TextureFrame simpleBuildTimeIndicatorBorder = this.simpleBuildTimeIndicator.getBorderFrame(); + simpleBuildTimeIndicatorBorder.setTexture("SimpleBuildTimeIndicatorBorder", this.rootFrame); + final float buildTimeIndicatorWidth = GameUI.convertX(this.uiViewport, 0.10538f); + final float buildTimeIndicatorHeight = GameUI.convertY(this.uiViewport, 0.0103f); + this.simpleBuildTimeIndicator.setWidth(buildTimeIndicatorWidth); + this.simpleBuildTimeIndicator.setHeight(buildTimeIndicatorHeight); + + this.simpleHeroLevelBar = (SimpleStatusBarFrame) this.rootFrame.getFrameByName("SimpleHeroLevelBar", 0); + final TextureFrame simpleHeroLevelBarBar = this.simpleHeroLevelBar.getBarFrame(); + simpleHeroLevelBarBar.setTexture("SimpleXpBarConsole", this.rootFrame); + simpleHeroLevelBarBar.setColor(new Color(138f / 255f, 0, 131f / 255f, 1f)); + final TextureFrame simpleHeroLevelBarBorder = this.simpleHeroLevelBar.getBorderFrame(); + simpleHeroLevelBarBorder.setTexture("SimpleXpBarBorder", this.rootFrame); + this.simpleHeroLevelBar.setWidth(infoPanelUnitDetailWidth); + + // Create Simple Info Panel Building Detail + this.simpleInfoPanelBuildingDetail = this.rootFrame.createSimpleFrame("SimpleInfoPanelBuildingDetail", + this.smashSimpleInfoPanel, 0); + this.simpleBuildingNameValue = (StringFrame) this.rootFrame.getFrameByName("SimpleBuildingNameValue", 0); + this.simpleBuildingDescriptionValue = (StringFrame) this.rootFrame + .getFrameByName("SimpleBuildingDescriptionValue", 0); + this.simpleBuildingBuildingActionLabel = (StringFrame) this.rootFrame + .getFrameByName("SimpleBuildingActionLabel", 0); + this.simpleBuildingBuildTimeIndicator = (SimpleStatusBarFrame) this.rootFrame + .getFrameByName("SimpleBuildTimeIndicator", 0); + final TextureFrame simpleBuildingBuildTimeIndicatorBar = this.simpleBuildingBuildTimeIndicator.getBarFrame(); + simpleBuildingBuildTimeIndicatorBar.setTexture("SimpleBuildTimeIndicator", this.rootFrame); + final TextureFrame simpleBuildingBuildTimeIndicatorBorder = this.simpleBuildingBuildTimeIndicator + .getBorderFrame(); + simpleBuildingBuildTimeIndicatorBorder.setTexture("SimpleBuildTimeIndicatorBorder", this.rootFrame); + this.simpleBuildingBuildTimeIndicator.setWidth(buildTimeIndicatorWidth); + this.simpleBuildingBuildTimeIndicator.setHeight(buildTimeIndicatorHeight); + this.simpleInfoPanelBuildingDetail.setVisible(false); + final TextureFrame simpleBuildQueueBackdrop = (TextureFrame) this.rootFrame + .getFrameByName("SimpleBuildQueueBackdrop", 0); + simpleBuildQueueBackdrop.setWidth(infoPanelUnitDetailWidth); + simpleBuildQueueBackdrop.setHeight(infoPanelUnitDetailWidth * 0.5f); + + this.queueIconFrames[0] = new QueueIcon("SmashBuildQueueIcon0", this.smashSimpleInfoPanel, this, 0); + final TextureFrame queueIconFrameBackdrop0 = new TextureFrame("SmashBuildQueueIcon0Backdrop", + this.queueIconFrames[0], false, new Vector4Definition(0, 1, 0, 1)); + queueIconFrameBackdrop0 + .addSetPoint(new SetPoint(FramePoint.CENTER, this.queueIconFrames[0], FramePoint.CENTER, 0, 0)); + this.queueIconFrames[0].set(queueIconFrameBackdrop0); + this.queueIconFrames[0] + .addSetPoint(new SetPoint(FramePoint.CENTER, this.smashSimpleInfoPanel, FramePoint.BOTTOMLEFT, + (infoPanelUnitDetailWidth * (15 + 19f)) / 256, (infoPanelUnitDetailWidth * (66 + 19f)) / 256)); + final float frontQueueIconWidth = (infoPanelUnitDetailWidth * 38) / 256; + this.queueIconFrames[0].setWidth(frontQueueIconWidth); + this.queueIconFrames[0].setHeight(frontQueueIconWidth); + queueIconFrameBackdrop0.setWidth(frontQueueIconWidth); + queueIconFrameBackdrop0.setHeight(frontQueueIconWidth); + this.rootFrame.add(this.queueIconFrames[0]); + + for (int i = 1; i < this.queueIconFrames.length; i++) { + this.queueIconFrames[i] = new QueueIcon("SmashBuildQueueIcon" + i, this.smashSimpleInfoPanel, this, i); + final TextureFrame queueIconFrameBackdrop = new TextureFrame("SmashBuildQueueIcon" + i + "Backdrop", + this.queueIconFrames[i], false, new Vector4Definition(0, 1, 0, 1)); + this.queueIconFrames[i].set(queueIconFrameBackdrop); + queueIconFrameBackdrop + .addSetPoint(new SetPoint(FramePoint.CENTER, this.queueIconFrames[i], FramePoint.CENTER, 0, 0)); + this.queueIconFrames[i].addSetPoint(new SetPoint(FramePoint.CENTER, this.smashSimpleInfoPanel, + FramePoint.BOTTOMLEFT, (infoPanelUnitDetailWidth * (13 + 14.5f + (40 * (i - 1)))) / 256, + (infoPanelUnitDetailWidth * (24 + 14.5f)) / 256)); + final float queueIconWidth = (infoPanelUnitDetailWidth * 29) / 256; + this.queueIconFrames[i].setWidth(queueIconWidth); + this.queueIconFrames[i].setHeight(queueIconWidth); + queueIconFrameBackdrop.setWidth(queueIconWidth); + queueIconFrameBackdrop.setHeight(queueIconWidth); + this.rootFrame.add(this.queueIconFrames[i]); + } + this.selectWorkerInsideFrame = new QueueIcon("SmashBuildQueueWorkerIcon", this.smashSimpleInfoPanel, this, 1); + final TextureFrame selectWorkerInsideIconFrameBackdrop = new TextureFrame("SmashBuildQueueWorkerIconBackdrop", + this.queueIconFrames[0], false, new Vector4Definition(0, 1, 0, 1)); + this.selectWorkerInsideFrame.set(selectWorkerInsideIconFrameBackdrop); + selectWorkerInsideIconFrameBackdrop + .addSetPoint(new SetPoint(FramePoint.CENTER, this.selectWorkerInsideFrame, FramePoint.CENTER, 0, 0)); + this.selectWorkerInsideFrame + .addSetPoint(new SetPoint(FramePoint.TOPLEFT, this.queueIconFrames[1], FramePoint.TOPLEFT, 0, 0)); + this.selectWorkerInsideFrame.setWidth(frontQueueIconWidth); + this.selectWorkerInsideFrame.setHeight(frontQueueIconWidth); + selectWorkerInsideIconFrameBackdrop.setWidth(frontQueueIconWidth); + selectWorkerInsideIconFrameBackdrop.setHeight(frontQueueIconWidth); + this.rootFrame.add(this.selectWorkerInsideFrame); + + this.smashAttack1IconWrapper = (SimpleFrame) this.rootFrame.createSimpleFrame("SmashSimpleInfoPanelIconDamage", + this.simpleInfoPanelUnitDetail, 0); + this.smashAttack1IconWrapper.addSetPoint(new SetPoint(FramePoint.TOPLEFT, this.simpleInfoPanelUnitDetail, + FramePoint.TOPLEFT, 0, GameUI.convertY(this.uiViewport, -0.032f))); + this.smashAttack1IconWrapper.setWidth(GameUI.convertX(this.uiViewport, 0.1f)); + this.smashAttack1IconWrapper.setHeight(GameUI.convertY(this.uiViewport, 0.030125f)); + this.attack1Icon = this.rootFrame.createSimpleFrame("SimpleInfoPanelIconDamage", this.smashAttack1IconWrapper, + 0); + this.attack1IconBackdrop = (TextureFrame) this.rootFrame.getFrameByName("InfoPanelIconBackdrop", 0); + this.attack1InfoPanelIconValue = (StringFrame) this.rootFrame.getFrameByName("InfoPanelIconValue", 0); + this.attack1InfoPanelIconLevel = (StringFrame) this.rootFrame.getFrameByName("InfoPanelIconLevel", 0); + + this.smashAttack2IconWrapper = (SimpleFrame) this.rootFrame.createSimpleFrame("SmashSimpleInfoPanelIconDamage", + this.simpleInfoPanelUnitDetail, 0); + this.smashAttack2IconWrapper + .addSetPoint(new SetPoint(FramePoint.TOPLEFT, this.simpleInfoPanelUnitDetail, FramePoint.TOPLEFT, + GameUI.convertX(this.uiViewport, 0.1f), GameUI.convertY(this.uiViewport, -0.03125f))); + this.smashAttack2IconWrapper.setWidth(GameUI.convertX(this.uiViewport, 0.1f)); + this.smashAttack2IconWrapper.setHeight(GameUI.convertY(this.uiViewport, 0.030125f)); + this.attack2Icon = this.rootFrame.createSimpleFrame("SimpleInfoPanelIconDamage", this.smashAttack2IconWrapper, + 1); + this.attack2IconBackdrop = (TextureFrame) this.rootFrame.getFrameByName("InfoPanelIconBackdrop", 1); + this.attack2InfoPanelIconValue = (StringFrame) this.rootFrame.getFrameByName("InfoPanelIconValue", 1); + this.attack2InfoPanelIconLevel = (StringFrame) this.rootFrame.getFrameByName("InfoPanelIconLevel", 1); + + this.smashArmorIconWrapper = (SimpleFrame) this.rootFrame.createSimpleFrame("SmashSimpleInfoPanelIconArmor", + this.simpleInfoPanelUnitDetail, 0); + this.smashArmorIconWrapper.addSetPoint(new SetPoint(FramePoint.TOPLEFT, this.simpleInfoPanelUnitDetail, + FramePoint.TOPLEFT, GameUI.convertX(this.uiViewport, 0f), GameUI.convertY(this.uiViewport, -0.0625f))); + this.smashArmorIconWrapper.setWidth(GameUI.convertX(this.uiViewport, 0.1f)); + this.smashArmorIconWrapper.setHeight(GameUI.convertY(this.uiViewport, 0.030125f)); + this.armorIcon = this.rootFrame.createSimpleFrame("SimpleInfoPanelIconArmor", this.smashArmorIconWrapper, 0); + this.armorIconBackdrop = (TextureFrame) this.rootFrame.getFrameByName("InfoPanelIconBackdrop", 0); + this.armorInfoPanelIconValue = (StringFrame) this.rootFrame.getFrameByName("InfoPanelIconValue", 0); + this.armorInfoPanelIconLevel = (StringFrame) this.rootFrame.getFrameByName("InfoPanelIconLevel", 0); + + this.smashHeroInfoPanelWrapper = (SimpleFrame) this.rootFrame.createSimpleFrame("SmashSimpleInfoPanelIconHero", + this.simpleInfoPanelUnitDetail, 0); + this.smashHeroInfoPanelWrapper.addSetPoint(new SetPoint(FramePoint.TOPLEFT, this.simpleInfoPanelUnitDetail, + FramePoint.TOPLEFT, GameUI.convertX(this.uiViewport, 0.1f), GameUI.convertY(this.uiViewport, -0.029f))); + this.smashHeroInfoPanelWrapper.setWidth(GameUI.convertX(this.uiViewport, 0.1f)); + this.smashHeroInfoPanelWrapper.setHeight(GameUI.convertY(this.uiViewport, 0.0625f)); + this.heroInfoPanel = this.rootFrame.createSimpleFrame("SimpleInfoPanelIconHero", this.smashHeroInfoPanelWrapper, + 0); + this.primaryAttributeIcon = (TextureFrame) this.rootFrame.getFrameByName("InfoPanelIconHeroIcon", 0); + this.strengthValue = (StringFrame) this.rootFrame.getFrameByName("InfoPanelIconHeroStrengthValue", 0); + this.agilityValue = (StringFrame) this.rootFrame.getFrameByName("InfoPanelIconHeroAgilityValue", 0); + this.intelligenceValue = (StringFrame) this.rootFrame.getFrameByName("InfoPanelIconHeroIntellectValue", 0); + + this.inventoryBarFrame = (SimpleFrame) this.rootFrame.createSimpleFrame("SmashSimpleInventoryBar", + this.rootFrame, 0); + this.inventoryBarFrame.setWidth(GameUI.convertX(this.uiViewport, 0.079f)); + this.inventoryBarFrame.setHeight(GameUI.convertY(this.uiViewport, 0.115f)); + this.inventoryBarFrame.addSetPoint(new SetPoint(FramePoint.BOTTOMRIGHT, this.consoleUI, FramePoint.BOTTOMLEFT, + GameUI.convertX(this.uiViewport, 0.591f), GameUI.convertY(this.uiViewport, 0.0f))); + + if (GameUI.DEBUG) { + final FilterModeTextureFrame placeholderPreview = new FilterModeTextureFrame(null, this.inventoryBarFrame, + false, null); + placeholderPreview.setFilterMode(FilterMode.ADDALPHA); + placeholderPreview.setTexture("ReplaceableTextures\\TeamColor\\TeamColor06.blp", this.rootFrame); + placeholderPreview.setSetAllPoints(true); + this.inventoryBarFrame.add(placeholderPreview); + } + + int commandButtonIndex = 0; + for (int j = 0; j < INVENTORY_HEIGHT; j++) { + for (int i = 0; i < INVENTORY_WIDTH; i++) { + final CommandCardIcon commandCardIcon = new CommandCardIcon( + "SmashInventoryButton_" + commandButtonIndex, this.inventoryBarFrame, + this.itemCommandCardCommandListener); + this.inventoryBarFrame.add(commandCardIcon); + final TextureFrame iconFrame = new TextureFrame( + "SmashInventoryButton_" + (commandButtonIndex) + "_Icon", this.rootFrame, false, null); + final SpriteFrame cooldownFrame = (SpriteFrame) this.rootFrame.createFrameByType("SPRITE", + "SmashInventoryButton_" + (commandButtonIndex) + "_Cooldown", this.rootFrame, "", 0); + commandCardIcon.addSetPoint(new SetPoint(FramePoint.TOPLEFT, this.inventoryBarFrame, FramePoint.TOPLEFT, + GameUI.convertX(this.uiViewport, 0.0037f + (0.04f * i)), + GameUI.convertY(this.uiViewport, -0.0021f - (0.03815f * j)))); + commandCardIcon.setWidth(GameUI.convertX(this.uiViewport, DEFAULT_INVENTORY_ICON_WIDTH)); + commandCardIcon.setHeight(GameUI.convertY(this.uiViewport, DEFAULT_INVENTORY_ICON_WIDTH)); + iconFrame.addSetPoint(new SetPoint(FramePoint.CENTER, commandCardIcon, FramePoint.CENTER, 0, 0)); + iconFrame.setWidth(GameUI.convertX(this.uiViewport, DEFAULT_INVENTORY_ICON_WIDTH)); + iconFrame.setHeight(GameUI.convertY(this.uiViewport, DEFAULT_INVENTORY_ICON_WIDTH)); + iconFrame.setTexture(ImageUtils.DEFAULT_ICON_PATH, this.rootFrame); + cooldownFrame.addSetPoint(new SetPoint(FramePoint.CENTER, commandCardIcon, FramePoint.CENTER, 0, 0)); + this.rootFrame.setSpriteFrameModel(cooldownFrame, this.rootFrame.getSkinField("CommandButtonCooldown")); + cooldownFrame.setWidth(GameUI.convertX(this.uiViewport, DEFAULT_INVENTORY_ICON_WIDTH)); + cooldownFrame.setHeight(GameUI.convertY(this.uiViewport, DEFAULT_INVENTORY_ICON_WIDTH)); + commandCardIcon.set(iconFrame, null, cooldownFrame, null); + this.inventoryIcons[j][i] = commandCardIcon; + commandCardIcon.clear(); + commandButtonIndex++; + } + } + this.inventoryTitleFrame = this.rootFrame.createStringFrame("SmashInventoryText", this.inventoryBarFrame, + new Color(0xFCDE12FF), TextJustify.CENTER, TextJustify.MIDDLE, 0.0109f); + this.rootFrame.setText(this.inventoryTitleFrame, this.rootFrame.getTemplates().getDecoratedString("INVENTORY")); + this.inventoryTitleFrame + .addSetPoint(new SetPoint(FramePoint.TOPLEFT, this.inventoryBarFrame, FramePoint.TOPLEFT, + GameUI.convertX(this.uiViewport, 0.004f), GameUI.convertY(this.uiViewport, 0.0165625f))); + this.inventoryTitleFrame.setWidth(GameUI.convertX(this.uiViewport, 0.071f)); + this.inventoryTitleFrame.setHeight(GameUI.convertX(this.uiViewport, 0.01125f)); + this.inventoryTitleFrame.setFontShadowColor(new Color(0f, 0f, 0f, 0.9f)); + this.inventoryTitleFrame.setFontShadowOffsetX(GameUI.convertX(this.uiViewport, 0.001f)); + this.inventoryTitleFrame.setFontShadowOffsetY(GameUI.convertY(this.uiViewport, -0.001f)); + this.consoleInventoryNoCapacityTexture = ImageUtils.getAnyExtensionTexture(this.dataSource, + this.rootFrame.getSkinField("ConsoleInventoryNoCapacity")); + + this.inventoryCover = this.rootFrame.createSimpleFrame("SmashConsoleInventoryCover", this.rootFrame, 0); + + final Element fontHeights = this.war3MapViewer.miscData.get("FontHeights"); + final float worldFrameMessageFontHeight = fontHeights.getFieldFloatValue("WorldFrameMessage"); + this.errorMessageFrame = this.rootFrame.createStringFrame("SmashErrorMessageFrame", this.rootFrame, + new Color(0xFFCC00FF), TextJustify.LEFT, TextJustify.MIDDLE, worldFrameMessageFontHeight); + this.errorMessageFrame.addAnchor(new AnchorDefinition(FramePoint.BOTTOMLEFT, + GameUI.convertX(this.uiViewport, 0.212f), GameUI.convertY(this.uiViewport, 0.182f))); + this.errorMessageFrame.setWidth(GameUI.convertX(this.uiViewport, 0.35f)); + this.errorMessageFrame.setHeight(GameUI.convertY(this.uiViewport, worldFrameMessageFontHeight)); + + this.errorMessageFrame.setFontShadowColor(new Color(0f, 0f, 0f, 0.9f)); + this.errorMessageFrame.setFontShadowOffsetX(GameUI.convertX(this.uiViewport, 0.001f)); + this.errorMessageFrame.setFontShadowOffsetY(GameUI.convertY(this.uiViewport, -0.001f)); + this.errorMessageFrame.setVisible(false); + + commandButtonIndex = 0; + for (int j = 0; j < COMMAND_CARD_HEIGHT; j++) { + for (int i = 0; i < COMMAND_CARD_WIDTH; i++) { + final CommandCardIcon commandCardIcon = new CommandCardIcon("SmashCommandButton_" + commandButtonIndex, + this.rootFrame, this); + this.rootFrame.add(commandCardIcon); + final TextureFrame iconFrame = new TextureFrame("SmashCommandButton_" + (commandButtonIndex) + "_Icon", + this.rootFrame, false, null); + final FilterModeTextureFrame activeHighlightFrame = new FilterModeTextureFrame( + "SmashCommandButton_" + (commandButtonIndex) + "_ActiveHighlight", this.rootFrame, true, null); + activeHighlightFrame.setFilterMode(FilterMode.ADDALPHA); + final SpriteFrame cooldownFrame = (SpriteFrame) this.rootFrame.createFrameByType("SPRITE", + "SmashCommandButton_" + (commandButtonIndex) + "_Cooldown", this.rootFrame, "", 0); + final SpriteFrame autocastFrame = (SpriteFrame) this.rootFrame.createFrameByType("SPRITE", + "SmashCommandButton_" + (commandButtonIndex) + "_Autocast", this.rootFrame, "", 0); + commandCardIcon.addAnchor(new AnchorDefinition(FramePoint.BOTTOMLEFT, + GameUI.convertX(this.uiViewport, 0.6175f + (0.0434f * i)), + GameUI.convertY(this.uiViewport, 0.095f - (0.044f * j)))); + commandCardIcon.setWidth(GameUI.convertX(this.uiViewport, DEFAULT_COMMAND_CARD_ICON_WIDTH)); + commandCardIcon.setHeight(GameUI.convertY(this.uiViewport, DEFAULT_COMMAND_CARD_ICON_WIDTH)); + iconFrame.addSetPoint(new SetPoint(FramePoint.CENTER, commandCardIcon, FramePoint.CENTER, 0, 0)); + iconFrame.setWidth(GameUI.convertX(this.uiViewport, DEFAULT_COMMAND_CARD_ICON_WIDTH)); + iconFrame.setHeight(GameUI.convertY(this.uiViewport, DEFAULT_COMMAND_CARD_ICON_WIDTH)); + iconFrame.setTexture(ImageUtils.DEFAULT_ICON_PATH, this.rootFrame); + activeHighlightFrame + .addSetPoint(new SetPoint(FramePoint.CENTER, commandCardIcon, FramePoint.CENTER, 0, 0)); + activeHighlightFrame.setWidth(GameUI.convertX(this.uiViewport, DEFAULT_COMMAND_CARD_ICON_WIDTH)); + activeHighlightFrame.setHeight(GameUI.convertY(this.uiViewport, DEFAULT_COMMAND_CARD_ICON_WIDTH)); + activeHighlightFrame.setTexture("CommandButtonActiveHighlight", this.rootFrame); + cooldownFrame.addSetPoint(new SetPoint(FramePoint.CENTER, commandCardIcon, FramePoint.CENTER, 0, 0)); + this.rootFrame.setSpriteFrameModel(cooldownFrame, this.rootFrame.getSkinField("CommandButtonCooldown")); + cooldownFrame.setWidth(GameUI.convertX(this.uiViewport, DEFAULT_COMMAND_CARD_ICON_WIDTH)); + cooldownFrame.setHeight(GameUI.convertY(this.uiViewport, DEFAULT_COMMAND_CARD_ICON_WIDTH)); + autocastFrame.addSetPoint(new SetPoint(FramePoint.CENTER, commandCardIcon, FramePoint.CENTER, 0, 0)); + this.rootFrame.setSpriteFrameModel(autocastFrame, this.rootFrame.getSkinField("CommandButtonAutocast")); + autocastFrame.setWidth(GameUI.convertX(this.uiViewport, DEFAULT_COMMAND_CARD_ICON_WIDTH)); + autocastFrame.setHeight(GameUI.convertY(this.uiViewport, DEFAULT_COMMAND_CARD_ICON_WIDTH)); + commandCardIcon.set(iconFrame, activeHighlightFrame, cooldownFrame, autocastFrame); + this.commandCard[j][i] = commandCardIcon; + commandCardIcon.clear(); + commandButtonIndex++; + } + } + + this.tooltipFrame = this.rootFrame.createFrame("SmashToolTip", this.rootFrame, 0, 0); + this.tooltipFrame.addAnchor(new AnchorDefinition(FramePoint.BOTTOMRIGHT, GameUI.convertX(this.uiViewport, 0.f), + GameUI.convertY(this.uiViewport, 0.176f))); + this.tooltipFrame.setWidth(GameUI.convertX(this.uiViewport, 0.280f)); + this.tooltipText = (StringFrame) this.rootFrame.getFrameByName("SmashToolTipText", 0); + this.tooltipText.setWidth(GameUI.convertX(this.uiViewport, 0.274f)); + this.tooltipText.addAnchor(new AnchorDefinition(FramePoint.TOPLEFT, GameUI.convertX(this.uiViewport, 0.003f), + GameUI.convertY(this.uiViewport, -0.003f))); + this.tooltipFrame.setVisible(false); + this.tooltipUberTipText = (StringFrame) this.rootFrame.getFrameByName("SmashUberTipText", 0); + this.tooltipUberTipText.setWidth(GameUI.convertX(this.uiViewport, 0.274f)); + this.uberTipNoResourcesSetPoint = new SetPoint(FramePoint.TOPLEFT, this.tooltipText, FramePoint.BOTTOMLEFT, 0, + GameUI.convertY(this.uiViewport, -0.004f)); + this.uberTipWithResourcesSetPoint = new SetPoint(FramePoint.TOPLEFT, this.tooltipText, FramePoint.BOTTOMLEFT, 0, + GameUI.convertY(this.uiViewport, -0.014f)); + this.tooltipUberTipText.addSetPoint(this.uberTipNoResourcesSetPoint); + this.tooltipResourceFrames = new UIFrame[ResourceType.VALUES.length]; + this.tooltipResourceIconFrames = new TextureFrame[ResourceType.VALUES.length]; + this.tooltipResourceTextFrames = new StringFrame[ResourceType.VALUES.length]; + for (int i = 0; i < this.tooltipResourceFrames.length; i++) { + this.tooltipResourceFrames[i] = this.rootFrame.createFrame("SmashToolTipIconResource", this.tooltipFrame, 0, + i); + this.tooltipResourceIconFrames[i] = (TextureFrame) this.rootFrame + .getFrameByName("SmashToolTipIconResourceBackdrop", i); + this.tooltipResourceTextFrames[i] = (StringFrame) this.rootFrame + .getFrameByName("SmashToolTipIconResourceLabel", i); + this.tooltipResourceFrames[i].addSetPoint(new SetPoint(FramePoint.TOPLEFT, this.tooltipText, + FramePoint.BOTTOMLEFT, GameUI.convertX(this.uiViewport, 0.004f + (0.032f * i)), + GameUI.convertY(this.uiViewport, -0.001f))); + // have we really no better API than the below??? + ((AbstractUIFrame) this.tooltipFrame).add(this.tooltipResourceFrames[i]); + this.rootFrame.remove(this.tooltipResourceFrames[i]); + } +// this.tooltipFrame = this.rootFrame.createFrameByType("BACKDROP", "SmashToolTipBackdrop", this.rootFrame, "", 0); + + this.cursorFrame = (SpriteFrame) this.rootFrame.createFrameByType("SPRITE", "SmashCursorFrame", this.rootFrame, + "", 0); + this.rootFrame.setSpriteFrameModel(this.cursorFrame, this.rootFrame.getSkinField("Cursor")); + this.cursorFrame.setSequence("Normal"); + this.cursorFrame.setZDepth(-1.0f); + Gdx.input.setCursorCatched(true); + + this.meleeUIMinimap = createMinimap(this.war3MapViewer); + + this.meleeUIAbilityActivationReceiver = new MeleeUIAbilityActivationReceiver( + new AbilityActivationErrorHandler(this.rootFrame.getErrorString("NoGold"), + this.war3MapViewer.getUiSounds().getSound(this.rootFrame.getSkinField("NoGoldSound"))), + new AbilityActivationErrorHandler(this.rootFrame.getErrorString("NoLumber"), + this.war3MapViewer.getUiSounds().getSound(this.rootFrame.getSkinField("NoLumberSound"))), + new AbilityActivationErrorHandler(this.rootFrame.getErrorString("NoFood"), + this.war3MapViewer.getUiSounds().getSound(this.rootFrame.getSkinField("NoFoodSound"))), + new AbilityActivationErrorHandler("", this.war3MapViewer.getUiSounds().getSound("InterfaceError"))); + + final MdxModel rallyModel = (MdxModel) this.war3MapViewer.load( + War3MapViewer.mdx(this.rootFrame.getSkinField("RallyIndicatorDst")), this.war3MapViewer.mapPathSolver, + this.war3MapViewer.solverParams); + this.rallyPointInstance = (MdxComplexInstance) rallyModel.addInstance(); + this.rallyPointInstance.rotate(RenderUnit.tempQuat.setFromAxis(RenderMathUtils.VEC3_UNIT_Z, + this.war3MapViewer.simulation.getGameplayConstants().getBuildingAngle())); + this.rallyPointInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + SequenceUtils.randomStandSequence(this.rallyPointInstance); + this.rallyPointInstance.hide(); + this.waypointModel = (MdxModel) this.war3MapViewer.load( + War3MapViewer.mdx(this.rootFrame.getSkinField("WaypointIndicator")), this.war3MapViewer.mapPathSolver, + this.war3MapViewer.solverParams); + + final FreeTypeFontParameter fontParam = new FreeTypeFontParameter(); + fontParam.size = (int) GameUI.convertY(this.uiViewport, 0.012f); + this.textTagFont = this.rootFrame.getFontGenerator().generateFont(fontParam); + + this.rootFrame.positionBounds(this.rootFrame, this.uiViewport); + + selectUnit(null); + + } + + private void updateEscMenuCurrentPanel(final UIFrame escMenuBackdrop, final UIFrame escMenuMainPanel, + final UIFrame escMenuInnerMainPanel) { + this.smashEscMenu.setWidth(escMenuInnerMainPanel.getAssignedWidth()); + this.smashEscMenu.setHeight(escMenuInnerMainPanel.getAssignedHeight()); + escMenuBackdrop.setWidth(escMenuInnerMainPanel.getAssignedWidth()); + escMenuBackdrop.setHeight(escMenuInnerMainPanel.getAssignedHeight()); + this.smashEscMenu.positionBounds(this.rootFrame, this.uiViewport); + + } + + @Override + public void onClick(final int abilityHandleId, final int orderId, final boolean rightClick) { + // TODO not O(N) + if (this.selectedUnit == null) { + return; + } + if (orderId == 0) { + return; + } + CAbilityView abilityToUse = null; + for (final CAbility ability : this.selectedUnit.getSimulationUnit().getAbilities()) { + if (ability.getHandleId() == abilityHandleId) { + abilityToUse = ability; + break; + } + } + if (abilityToUse != null) { + abilityToUse.checkCanUse(this.war3MapViewer.simulation, this.selectedUnit.getSimulationUnit(), orderId, + this.meleeUIAbilityActivationReceiver.reset(this, this.war3MapViewer.worldScene.audioContext, + this.selectedUnit)); + if (this.meleeUIAbilityActivationReceiver.isUseOk()) { + final BooleanAbilityTargetCheckReceiver noTargetReceiver = BooleanAbilityTargetCheckReceiver + .getInstance().reset(); + abilityToUse.checkCanTargetNoTarget(this.war3MapViewer.simulation, + this.selectedUnit.getSimulationUnit(), orderId, noTargetReceiver); + if (noTargetReceiver.isTargetable()) { + this.unitOrderListener.issueImmediateOrder(this.selectedUnit.getSimulationUnit().getHandleId(), + abilityHandleId, orderId, isShiftDown()); + } + else { + this.activeCommand = abilityToUse; + this.activeCommandOrderId = orderId; + this.activeCommandUnit = this.selectedUnit; + clearAndRepopulateCommandCard(); + } + } + } + else { + this.unitOrderListener.issueImmediateOrder(this.selectedUnit.getSimulationUnit().getHandleId(), + abilityHandleId, orderId, isShiftDown()); + } + if (rightClick) { + this.war3MapViewer.getUiSounds().getSound("AutoCastButtonClick").play(this.uiScene.audioContext, 0, 0, 0); + } + } + + @Override + public void openMenu(final int orderId) { + if (orderId == 0) { + this.subMenuOrderIdStack.clear(); + this.activeCommandUnit = null; + this.activeCommand = null; + this.activeCommandOrderId = -1; + } + else { + this.subMenuOrderIdStack.add(orderId); + } + clearAndRepopulateCommandCard(); + } + + @Override + public void showCommandError(final String message) { + this.rootFrame.setText(this.errorMessageFrame, message); + this.errorMessageFrame.setVisible(true); + final long millis = TimeUtils.millis(); + this.lastErrorMessageExpireTime = millis + WORLD_FRAME_MESSAGE_EXPIRE_MILLIS; + this.lastErrorMessageFadeTime = millis + WORLD_FRAME_MESSAGE_FADEOUT_MILLIS; + this.errorMessageFrame.setAlpha(1.0f); + } + + @Override + public void showCantPlaceError() { + showCommandError(this.rootFrame.getErrorString("Cantplace")); + this.war3MapViewer.getUiSounds().getSound(this.rootFrame.getSkinField("CantPlaceSound")) + .play(this.uiScene.audioContext, 0, 0, 0); + } + + @Override + public void showNoFoodError() { + showCommandError(this.rootFrame.getErrorString("NoFood")); + this.war3MapViewer.getUiSounds().getSound(this.rootFrame.getSkinField("NoFoodSound")) + .play(this.uiScene.audioContext, 0, 0, 0); + } + + @Override + public void showInventoryFullError() { + showCommandError(this.rootFrame.getErrorString("InventoryFull")); + this.war3MapViewer.getUiSounds().getSound(this.rootFrame.getSkinField("InventoryFullSound")) + .play(this.uiScene.audioContext, 0, 0, 0); + } + + public void update(final float deltaTime) { + this.portrait.update(); + + final int baseMouseX = Gdx.input.getX(); + int mouseX = baseMouseX; + final int baseMouseY = Gdx.input.getY(); + int mouseY = baseMouseY; + final int minX = this.uiViewport.getScreenX(); + final int maxX = minX + this.uiViewport.getScreenWidth(); + final int minY = this.uiViewport.getScreenY(); + final int maxY = minY + this.uiViewport.getScreenHeight(); + final boolean left = mouseX <= (minX + 3); + final boolean right = mouseX >= (maxX - 3); + final boolean up = mouseY <= (minY + 3); + final boolean down = mouseY >= (maxY - 3); + this.cameraManager.applyVelocity(deltaTime, up, down, left, right); + + mouseX = Math.max(minX, Math.min(maxX, mouseX)); + mouseY = Math.max(minY, Math.min(maxY, mouseY)); + if (Gdx.input.isCursorCatched()) { + Gdx.input.setCursorPosition(mouseX, mouseY); + } + + screenCoordsVector.set(mouseX, mouseY); + this.uiViewport.unproject(screenCoordsVector); + this.cursorFrame.setFramePointX(FramePoint.LEFT, screenCoordsVector.x); + this.cursorFrame.setFramePointY(FramePoint.BOTTOM, screenCoordsVector.y); + + if (this.activeCommand != null) { + if (this.draggingItem != null) { + this.cursorFrame.setSequence("HoldItem"); + } + else { + this.activeCommand.visit(this.cursorTargetSetupVisitor.reset(baseMouseX, baseMouseY)); + } + } + else { + if (this.cursorModelInstance != null) { + this.cursorModelInstance.detach(); + this.cursorModelInstance = null; + this.cursorFrame.setVisible(true); + } + if (this.placementCursor != null) { + this.placementCursor.destroy(Gdx.gl30, this.war3MapViewer.terrain.centerOffset); + this.placementCursor = null; + this.cursorFrame.setVisible(true); + } + if (this.cursorModelUnderneathPathingRedGreenSplatModel != null) { + this.war3MapViewer.terrain.removeSplatBatchModel(BUILDING_PATHING_PREVIEW_KEY); + this.cursorModelUnderneathPathingRedGreenSplatModel = null; + } + if (down) { + if (left) { + this.cursorFrame.setSequence("Scroll Down Left"); + } + else if (right) { + this.cursorFrame.setSequence("Scroll Down Right"); + } + else { + this.cursorFrame.setSequence("Scroll Down"); + } + } + else if (up) { + if (left) { + this.cursorFrame.setSequence("Scroll Up Left"); + } + else if (right) { + this.cursorFrame.setSequence("Scroll Up Right"); + } + else { + this.cursorFrame.setSequence("Scroll Up"); + } + } + else if (left) { + this.cursorFrame.setSequence("Scroll Left"); + } + else if (right) { + this.cursorFrame.setSequence("Scroll Right"); + } + else { + this.cursorFrame.setSequence("Normal"); + } + } + if (this.selectedUnit != null) { + if (this.simpleBuildTimeIndicator.isVisible()) { + this.simpleBuildTimeIndicator + .setValue(Math.min(this.selectedUnit.getSimulationUnit().getConstructionProgress() + / this.selectedUnit.getSimulationUnit().getUnitType().getBuildTime(), 0.99f)); + } + if (this.simpleBuildingBuildTimeIndicator.isVisible()) { + this.simpleBuildingBuildTimeIndicator + .setValue(Math.min( + this.selectedUnit.getSimulationUnit().getConstructionProgress() / this.selectedUnit + .getSimulationUnit().getBuildQueueTimeRemaining(this.war3MapViewer.simulation), + 0.99f)); + } + } + + final float groundHeight = Math.max( + this.war3MapViewer.terrain.getGroundHeight(this.cameraManager.target.x, this.cameraManager.target.y), + this.war3MapViewer.terrain.getWaterHeight(this.cameraManager.target.x, this.cameraManager.target.y)); + this.cameraManager.updateTargetZ(groundHeight); + this.cameraManager.updateCamera(); + final long currentMillis = TimeUtils.millis(); + if (currentMillis > this.lastErrorMessageExpireTime) { + this.errorMessageFrame.setVisible(false); + } + else if (currentMillis > this.lastErrorMessageFadeTime) { + final float fadeAlpha = (this.lastErrorMessageExpireTime - currentMillis) + / (float) WORLD_FRAME_MESSAGE_FADE_DURATION; + this.errorMessageFrame.setAlpha(fadeAlpha); + } + } + + public void render(final SpriteBatch batch, final GlyphLayout glyphLayout) { + final BitmapFont font = this.rootFrame.getFont(); + font.setColor(Color.YELLOW); + final String fpsString = "FPS: " + Gdx.graphics.getFramesPerSecond(); + glyphLayout.setText(font, fpsString); + font.draw(batch, fpsString, (this.uiViewport.getMinWorldWidth() - glyphLayout.width) / 2, + 1100 * this.heightRatioCorrection); + this.rootFrame.render(batch, this.rootFrame.getFont20(), glyphLayout); + if (this.selectedUnit != null) { + this.rootFrame.getFont20().setColor(Color.WHITE); + + } + + this.meleeUIMinimap.render(batch, this.war3MapViewer.units); + this.timeIndicator.setFrameByRatio(this.war3MapViewer.simulation.getGameTimeOfDay() + / this.war3MapViewer.simulation.getGameplayConstants().getGameDayHours()); + for (final TextTag textTag : this.war3MapViewer.textTags) { + this.war3MapViewer.worldScene.camera.worldToScreen(screenCoordsVector, textTag.getPosition()); + if (this.war3MapViewer.worldScene.camera.rect.contains(screenCoordsVector.x, + (Gdx.graphics.getHeight() - screenCoordsVector.y) + textTag.getScreenCoordsZHeight())) { + final Vector2 unprojected = this.uiViewport.unproject(screenCoordsVector); + final float remainingLife = textTag.getRemainingLife(); + final float alpha = (remainingLife > 1.0f ? 1.0f : remainingLife); + this.textTagFont.setColor(textTag.getColor().r, textTag.getColor().g, textTag.getColor().b, + textTag.getColor().a * alpha); + glyphLayout.setText(this.textTagFont, textTag.getText()); + this.textTagFont.draw(batch, textTag.getText(), unprojected.x - (glyphLayout.width / 2), + (unprojected.y - (glyphLayout.height / 2)) + textTag.getScreenCoordsZHeight()); + } + } + } + + public void portraitTalk() { + this.portrait.talk(); + } + + private final class CursorTargetSetupVisitor implements CAbilityVisitor { + private int baseMouseX; + private int baseMouseY; + + private CursorTargetSetupVisitor reset(final int baseMouseX, final int baseMouseY) { + this.baseMouseX = baseMouseX; + this.baseMouseY = baseMouseY; + return this; + } + + @Override + public Void accept(final CAbilityAttack ability) { + if (MeleeUI.this.activeCommandOrderId == OrderIds.attackground) { + float radius = 0; + for (final CUnitAttack attack : MeleeUI.this.activeCommandUnit.getSimulationUnit().getAttacks()) { + if (attack.getWeaponType().isAttackGroundSupported()) { + if (attack instanceof CUnitAttackMissileSplash) { + final int areaOfEffectSmallDamage = ((CUnitAttackMissileSplash) attack) + .getAreaOfEffectSmallDamage(); + radius = areaOfEffectSmallDamage; + } + } + } + handlePlacementCursor(ability, radius); + } + else { + handleTargetCursor(ability); + } + return null; + } + + @Override + public Void accept(final CAbilityMove ability) { + handleTargetCursor(ability); + return null; + } + + @Override + public Void accept(final CAbilityOrcBuild ability) { + handleBuildCursor(ability); + return null; + } + + @Override + public Void accept(final CAbilityHumanBuild ability) { + handleBuildCursor(ability); + return null; + } + + @Override + public Void accept(final CAbilityUndeadBuild ability) { + handleBuildCursor(ability); + return null; + } + + @Override + public Void accept(final CAbilityNightElfBuild ability) { + handleBuildCursor(ability); + return null; + } + + @Override + public Void accept(final CAbilityGeneric ability) { + handleTargetCursor(ability); + return null; + } + + @Override + public Void accept(final CAbilityColdArrows ability) { + handleTargetCursor(ability); + return null; + } + + @Override + public Void accept(final CAbilityNagaBuild ability) { + handleBuildCursor(ability); + return null; + } + + @Override + public Void accept(final CAbilityNeutralBuild ability) { + handleBuildCursor(ability); + return null; + } + + @Override + public Void accept(final CAbilityBuildInProgress ability) { + handleTargetCursor(ability); + return null; + } + + @Override + public Void accept(final CAbilityQueue ability) { + handleTargetCursor(ability); + return null; + } + + @Override + public Void accept(final GenericSingleIconActiveAbility ability) { + handleTargetCursor(ability); + return null; + } + + @Override + public Void accept(final GenericNoIconAbility ability) { + // this should probably never happen + handleTargetCursor(ability); + return null; + } + + @Override + public Void accept(final CAbilityRally ability) { + handleTargetCursor(ability); + return null; + } + + @Override + public Void accept(final CAbilityHero ability) { + handleTargetCursor(ability); + return null; + } + + private void handleTargetCursor(final CAbility ability) { + if (MeleeUI.this.cursorModelInstance != null) { + MeleeUI.this.cursorModelInstance.detach(); + MeleeUI.this.cursorModelInstance = null; + MeleeUI.this.cursorFrame.setVisible(true); + } + MeleeUI.this.cursorFrame.setSequence("Target"); + } + + private void handleBuildCursor(final AbstractCAbilityBuild ability) { + boolean justLoaded = false; + final War3MapViewer viewer = MeleeUI.this.war3MapViewer; + if (MeleeUI.this.cursorModelInstance == null) { + final MutableObjectData unitData = viewer.getAllObjectData().getUnits(); + final War3ID buildingTypeId = new War3ID(MeleeUI.this.activeCommandOrderId); + MeleeUI.this.cursorBuildingUnitType = viewer.simulation.getUnitData().getUnitType(buildingTypeId); + final String unitModelPath = viewer.getUnitModelPath(unitData.get(buildingTypeId)); + final MdxModel model = (MdxModel) viewer.load(unitModelPath, viewer.mapPathSolver, viewer.solverParams); + MeleeUI.this.cursorModelInstance = (MdxComplexInstance) model.addInstance(); +// MeleeUI.this.cursorModelInstance.setVertexColor(new float[] { 1, 1, 1, 0.5f }); + final int playerColorIndex = viewer.simulation + .getPlayer(MeleeUI.this.activeCommandUnit.getSimulationUnit().getPlayerIndex()).getColor(); + MeleeUI.this.cursorModelInstance.setTeamColor(playerColorIndex); + MeleeUI.this.cursorModelInstance.rotate(RenderUnit.tempQuat.setFromAxis(RenderMathUtils.VEC3_UNIT_Z, + viewer.simulation.getGameplayConstants().getBuildingAngle())); + MeleeUI.this.cursorModelInstance.setAnimationSpeed(0f); + justLoaded = true; + final CUnitType buildingUnitType = MeleeUI.this.cursorBuildingUnitType; + MeleeUI.this.cursorModelPathing = buildingUnitType.getBuildingPathingPixelMap(); + + if (MeleeUI.this.cursorModelPathing != null) { + MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmap = new Pixmap( + MeleeUI.this.cursorModelPathing.getWidth(), MeleeUI.this.cursorModelPathing.getHeight(), + Format.RGBA8888); + MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmap.setBlending(Blending.None); + MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmapTextureData = new PixmapTextureData( + MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmap, Format.RGBA8888, false, false); + MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmapTexture = new Texture( + MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmapTextureData); + final ViewerTextureRenderable greenPixmap = new ViewerTextureRenderable.GdxViewerTextureRenderable( + MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmapTexture); + MeleeUI.this.cursorModelUnderneathPathingRedGreenSplatModel = new SplatModel(Gdx.gl30, greenPixmap, + new ArrayList<>(), viewer.terrain.centerOffset, new ArrayList<>(), true, false, true); + MeleeUI.this.cursorModelUnderneathPathingRedGreenSplatModel.color[3] = 0.20f; + } + } + viewer.getClickLocation(clickLocationTemp, this.baseMouseX, Gdx.graphics.getHeight() - this.baseMouseY); + if (MeleeUI.this.cursorModelPathing != null) { + clickLocationTemp.x = (float) Math.floor(clickLocationTemp.x / 64f) * 64f; + clickLocationTemp.y = (float) Math.floor(clickLocationTemp.y / 64f) * 64f; + if (((MeleeUI.this.cursorModelPathing.getWidth() / 2) % 2) == 1) { + clickLocationTemp.x += 32f; + } + if (((MeleeUI.this.cursorModelPathing.getHeight() / 2) % 2) == 1) { + clickLocationTemp.y += 32f; + } + clickLocationTemp.z = viewer.terrain.getGroundHeight(clickLocationTemp.x, clickLocationTemp.y); + + final int cursorWidthCells = MeleeUI.this.cursorModelPathing.getWidth(); + final int halfCursorWidthCells = cursorWidthCells / 2; + final float halfRenderWidth = cursorWidthCells * 16; + final int cursorHeightCells = MeleeUI.this.cursorModelPathing.getHeight(); + final int halfCursorHeightCells = cursorHeightCells / 2; + final float halfRenderHeight = cursorHeightCells * 16; + final PathingGrid pathingGrid = viewer.simulation.getPathingGrid(); + boolean blockAll = false; + final int cellX = pathingGrid.getCellX(clickLocationTemp.x); + final int cellY = pathingGrid.getCellY(clickLocationTemp.y); + if ((cellX < halfCursorWidthCells) || (cellX > (pathingGrid.getWidth() - halfCursorWidthCells)) + || (cellY < halfCursorHeightCells) + || (cellY > (pathingGrid.getHeight() - halfCursorHeightCells))) { + blockAll = true; + } + if (blockAll) { + for (int i = 0; i < MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmap.getWidth(); i++) { + for (int j = 0; j < MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmap.getHeight(); j++) { + MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmap.drawPixel(i, + MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmap.getHeight() - 1 - j, + Color.rgba8888(1, 0, 0, 1.0f)); + } + } + } + else { + for (int i = 0; i < MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmap.getWidth(); i++) { + for (int j = 0; j < MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmap.getHeight(); j++) { + boolean blocked = false; + final short pathing = pathingGrid.getPathing( + (clickLocationTemp.x + (i * 32)) - halfRenderWidth, + (clickLocationTemp.y + (j * 32)) - halfRenderHeight); + for (final CBuildingPathingType preventedType : MeleeUI.this.cursorBuildingUnitType + .getPreventedPathingTypes()) { + if (PathingFlags.isPathingFlag(pathing, preventedType)) { + blocked = true; + } + } + for (final CBuildingPathingType requiredType : MeleeUI.this.cursorBuildingUnitType + .getRequiredPathingTypes()) { + if (!PathingFlags.isPathingFlag(pathing, requiredType)) { + blocked = true; + } + } + final int color = blocked ? Color.rgba8888(1, 0, 0, 1.0f) : Color.rgba8888(0, 1, 0, 1.0f); + MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmap.drawPixel(i, + MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmap.getHeight() - 1 - j, color); + } + } + } + MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmapTexture + .load(MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmapTextureData); + + if (justLoaded) { + viewer.terrain.addSplatBatchModel(BUILDING_PATHING_PREVIEW_KEY, + MeleeUI.this.cursorModelUnderneathPathingRedGreenSplatModel); + MeleeUI.this.placementCursor = MeleeUI.this.cursorModelUnderneathPathingRedGreenSplatModel.add( + clickLocationTemp.x - halfRenderWidth, clickLocationTemp.y - halfRenderHeight, + clickLocationTemp.x + halfRenderWidth, clickLocationTemp.y + halfRenderHeight, 10, + viewer.terrain.centerOffset); + } + MeleeUI.this.placementCursor.setLocation(clickLocationTemp.x, clickLocationTemp.y, + viewer.terrain.centerOffset); + } + MeleeUI.this.cursorModelInstance.setLocation(clickLocationTemp); + SequenceUtils.randomSequence(MeleeUI.this.cursorModelInstance, PrimaryTag.STAND); + MeleeUI.this.cursorFrame.setVisible(false); + if (justLoaded) { + MeleeUI.this.cursorModelInstance.setScene(viewer.worldScene); + } + } + + private void handlePlacementCursor(final CAbility ability, final float radius) { + final War3MapViewer viewer = MeleeUI.this.war3MapViewer; + viewer.getClickLocation(clickLocationTemp, this.baseMouseX, Gdx.graphics.getHeight() - this.baseMouseY); + if (MeleeUI.this.placementCursor == null) { + MeleeUI.this.placementCursor = viewer.terrain.addUberSplat( + MeleeUI.this.rootFrame.getSkinField("PlacementCursor"), clickLocationTemp.x, + clickLocationTemp.y, 10, radius, true, true, true); + } + MeleeUI.this.placementCursor.setLocation(clickLocationTemp.x, clickLocationTemp.y, + viewer.terrain.centerOffset); + MeleeUI.this.cursorFrame.setVisible(false); + } + } + + private final class RallyPositioningVisitor implements AbilityTargetVisitor { + private MdxComplexInstance rallyPointInstance = null; + + public RallyPositioningVisitor reset(final MdxComplexInstance rallyPointInstance) { + this.rallyPointInstance = rallyPointInstance; + return this; + } + + @Override + public Void accept(final AbilityPointTarget target) { + this.rallyPointInstance.setParent(null); + final float rallyPointX = target.getX(); + final float rallyPointY = target.getY(); + this.rallyPointInstance.setLocation(rallyPointX, rallyPointY, + MeleeUI.this.war3MapViewer.terrain.getGroundHeight(rallyPointX, rallyPointY)); + return null; + } + + @Override + public Void accept(final CUnit target) { + final RenderUnit renderUnit = MeleeUI.this.war3MapViewer.getRenderPeer(target); + final MdxModel model = (MdxModel) renderUnit.instance.model; + int index = -1; + for (int i = 0; i < model.attachments.size(); i++) { + final Attachment attachment = model.attachments.get(i); + if (attachment.getName().startsWith("sprite")) { + index = i; + break; + } + } + if (index == -1) { + for (int i = 0; i < model.attachments.size(); i++) { + final Attachment attachment = model.attachments.get(i); + if (attachment.getName().startsWith("overhead ref")) { + index = i; + } + } + } + if (index != -1) { + final MdxNode attachment = renderUnit.instance.getAttachment(index); + this.rallyPointInstance.setParent(attachment); + this.rallyPointInstance.setLocation(0, 0, 0); + } + else { + this.rallyPointInstance.setParent(null); + final float rallyPointX = target.getX(); + final float rallyPointY = target.getY(); + this.rallyPointInstance.setLocation(rallyPointX, rallyPointY, + MeleeUI.this.war3MapViewer.terrain.getGroundHeight(rallyPointX, rallyPointY)); + } + return null; + } + + @Override + public Void accept(final CDestructable target) { + this.rallyPointInstance.setParent(null); + final float rallyPointX = target.getX(); + final float rallyPointY = target.getY(); + this.rallyPointInstance.setLocation(rallyPointX, rallyPointY, + MeleeUI.this.war3MapViewer.terrain.getGroundHeight(rallyPointX, rallyPointY) + 192); + return null; + } + + @Override + public Void accept(final CItem target) { + this.rallyPointInstance.setParent(null); + final float rallyPointX = target.getX(); + final float rallyPointY = target.getY(); + this.rallyPointInstance.setLocation(rallyPointX, rallyPointY, + MeleeUI.this.war3MapViewer.terrain.getGroundHeight(rallyPointX, rallyPointY)); + return null; + } + } + + private final class ActiveCommandUnitTargetFilter implements CWidgetFilterFunction { + @Override + public boolean call(final CWidget unit) { + final BooleanAbilityTargetCheckReceiver targetReceiver = BooleanAbilityTargetCheckReceiver + .getInstance(); + MeleeUI.this.activeCommand.checkCanTarget(MeleeUI.this.war3MapViewer.simulation, + MeleeUI.this.activeCommandUnit.getSimulationUnit(), MeleeUI.this.activeCommandOrderId, unit, + targetReceiver); + return targetReceiver.isTargetable(); + } + } + + private static final class Portrait { + private MdxComplexInstance modelInstance; + private final PortraitCameraManager portraitCameraManager; + private final Scene portraitScene; + private final EnumSet recycleSet = EnumSet + .noneOf(AnimationTokens.SecondaryTag.class); + private RenderUnit unit; + + public Portrait(final War3MapViewer war3MapViewer, final Scene portraitScene) { + this.portraitScene = portraitScene; + this.portraitCameraManager = new PortraitCameraManager(); + this.portraitCameraManager.setupCamera(this.portraitScene); + this.portraitScene.camera.viewport(new Rectangle(100, 0, 6400, 48)); + } + + public void update() { + this.portraitCameraManager.updateCamera(); + if ((this.modelInstance != null) + && (this.modelInstance.sequenceEnded || (this.modelInstance.sequence == -1))) { + this.recycleSet.clear(); + this.recycleSet.addAll(this.unit.getSecondaryAnimationTags()); + SequenceUtils.randomSequence(this.modelInstance, PrimaryTag.PORTRAIT, this.recycleSet, true); + } + } + + public void talk() { + this.recycleSet.clear(); + this.recycleSet.addAll(this.unit.getSecondaryAnimationTags()); + this.recycleSet.add(SecondaryTag.TALK); + SequenceUtils.randomSequence(this.modelInstance, PrimaryTag.PORTRAIT, this.recycleSet, true); + } + + public void setSelectedUnit(final RenderUnit unit) { + this.unit = unit; + if (unit == null) { + if (this.modelInstance != null) { + this.portraitScene.removeInstance(this.modelInstance); + } + this.modelInstance = null; + this.portraitCameraManager.setModelInstance(null, null); + } + else { + final MdxModel portraitModel = unit.portraitModel; + if (portraitModel != null) { + if (this.modelInstance != null) { + this.portraitScene.removeInstance(this.modelInstance); + } + this.modelInstance = (MdxComplexInstance) portraitModel.addInstance(); + this.portraitCameraManager.setModelInstance(this.modelInstance, portraitModel); + this.modelInstance.setSequenceLoopMode(SequenceLoopMode.NEVER_LOOP); + this.modelInstance.setScene(this.portraitScene); + this.modelInstance.setVertexColor(unit.instance.vertexColor); + this.modelInstance.setTeamColor(unit.playerIndex); + } + } + } + } + + public void setDraggingItem(final CItem itemInSlot) { + this.draggingItem = itemInSlot; + if (itemInSlot != null) { + final String iconPath = this.war3MapViewer.getAbilityDataUI().getItemUI(itemInSlot.getTypeId()) + .getItemIconPathForDragging(); + this.cursorFrame.setReplaceableId(21, this.war3MapViewer.blp(iconPath)); + + int index = 0; + final CAbilityInventory inventory = this.selectedUnit.getSimulationUnit().getInventoryData(); + for (int i = 0; i < INVENTORY_HEIGHT; i++) { + for (int j = 0; j < INVENTORY_WIDTH; j++) { + final CommandCardIcon inventoryIcon = this.inventoryIcons[i][j]; + final CItem item = inventory.getItemInSlot(index); + if (item == null) { + if (index < inventory.getItemCapacity()) { + inventoryIcon.setCommandButtonData(null, 0, 0, index + 1, true, false, false, null, null, 0, + 0, 0); + } + } + index++; + } + } + } + else { + if (this.selectedUnit != null) { + final CAbilityInventory inventory = this.selectedUnit.getSimulationUnit().getInventoryData(); + if (inventory != null) { + int index = 0; + for (int i = 0; i < INVENTORY_HEIGHT; i++) { + for (int j = 0; j < INVENTORY_WIDTH; j++) { + final CommandCardIcon inventoryIcon = this.inventoryIcons[i][j]; + final CItem item = inventory.getItemInSlot(index); + if (item == null) { + if (index < inventory.getItemCapacity()) { + inventoryIcon.clear(); + } + } + index++; + } + } + } + } + + } + } + + public void selectUnit(RenderUnit unit) { + this.subMenuOrderIdStack.clear(); + if ((unit != null) && unit.getSimulationUnit().isDead()) { + unit = null; + } + if (this.selectedUnit != null) { + this.selectedUnit.getSimulationUnit().removeStateListener(this); + } + this.portrait.setSelectedUnit(unit); + this.selectedUnit = unit; + setDraggingItem(null); + if (unit == null) { + clearCommandCard(); + this.rootFrame.setText(this.simpleNameValue, ""); + this.rootFrame.setText(this.unitLifeText, ""); + this.rootFrame.setText(this.unitManaText, ""); + this.rootFrame.setText(this.simpleClassValue, ""); + this.rootFrame.setText(this.simpleBuildingActionLabel, ""); + this.attack1Icon.setVisible(false); + this.attack2Icon.setVisible(false); + this.rootFrame.setText(this.attack1InfoPanelIconLevel, ""); + this.rootFrame.setText(this.attack2InfoPanelIconLevel, ""); + this.rootFrame.setText(this.simpleBuildingBuildingActionLabel, ""); + this.rootFrame.setText(this.simpleBuildingNameValue, ""); + this.armorIcon.setVisible(false); + this.rootFrame.setText(this.armorInfoPanelIconLevel, ""); + this.simpleBuildTimeIndicator.setVisible(false); + this.simpleHeroLevelBar.setVisible(false); + this.simpleBuildingBuildTimeIndicator.setVisible(false); + this.simpleInfoPanelBuildingDetail.setVisible(false); + this.simpleInfoPanelUnitDetail.setVisible(false); + for (final QueueIcon queueIconFrame : this.queueIconFrames) { + queueIconFrame.setVisible(false); + } + this.selectWorkerInsideFrame.setVisible(false); + this.heroInfoPanel.setVisible(false); + this.rallyPointInstance.hide(); + this.rallyPointInstance.detach(); + this.inventoryCover.setVisible(true); + this.inventoryBarFrame.setVisible(false); + repositionWaypointFlags(null); + } + else { + unit.getSimulationUnit().addStateListener(this); + reloadSelectedUnitUI(unit); + } + } + + @Override + public void rallyPointChanged() { + if (this.selectedUnit != null) { + final CUnit simulationUnit = this.selectedUnit.getSimulationUnit(); + repositionRallyPoint(simulationUnit); + } + } + + private void repositionRallyPoint(final CUnit simulationUnit) { + final AbilityTarget rallyPoint = simulationUnit.getRallyPoint(); + if (rallyPoint != null) { + this.rallyPointInstance + .setTeamColor(this.war3MapViewer.simulation.getPlayer(simulationUnit.getPlayerIndex()).getColor()); + this.rallyPointInstance.show(); + this.rallyPointInstance.detach(); + rallyPoint.visit(this.rallyPositioningVisitor.reset(this.rallyPointInstance)); + this.rallyPointInstance.setScene(this.war3MapViewer.worldScene); + } + else { + this.rallyPointInstance.hide(); + this.rallyPointInstance.detach(); + } + } + + @Override + public void waypointsChanged() { + if (this.selectedUnit != null) { + final CUnit simulationUnit = this.selectedUnit.getSimulationUnit(); + repositionWaypointFlags(simulationUnit); + } + else { + repositionWaypointFlags(null); + } + } + + private void repositionWaypointFlags(final CUnit simulationUnit) { + final Iterator iterator; + int orderIndex = 0; + if (simulationUnit != null) { + final Queue orderQueue = simulationUnit.getOrderQueue(); + iterator = orderQueue.iterator(); + final COrder order = simulationUnit.getCurrentOrder(); + if ((order != null) && order.isQueued()) { + final MdxComplexInstance waypointModelInstance = getOrCreateWaypointIndicator(orderIndex); + final AbilityTarget target = order.getTarget(this.war3MapViewer.simulation); + if (target != null) { + waypointModelInstance.show(); + waypointModelInstance.detach(); + target.visit(this.rallyPositioningVisitor.reset(waypointModelInstance)); + waypointModelInstance.setScene(this.war3MapViewer.worldScene); + } + else { + waypointModelInstance.hide(); + waypointModelInstance.detach(); + } + orderIndex++; + } + } + else { + iterator = Collections.emptyIterator(); + } + for (; (orderIndex < this.waypointModelInstances.size()) || (iterator.hasNext()); orderIndex++) { + final MdxComplexInstance waypointModelInstance = getOrCreateWaypointIndicator(orderIndex); + if (iterator.hasNext()) { + final COrder order = iterator.next(); + final AbilityTarget target = order.getTarget(this.war3MapViewer.simulation); + if (target != null) { + waypointModelInstance.show(); + waypointModelInstance.detach(); + target.visit(this.rallyPositioningVisitor.reset(waypointModelInstance)); + waypointModelInstance.setScene(this.war3MapViewer.worldScene); + } + else { + waypointModelInstance.hide(); + waypointModelInstance.detach(); + } + } + else { + waypointModelInstance.hide(); + waypointModelInstance.detach(); + } + } + } + + private MdxComplexInstance getOrCreateWaypointIndicator(final int index) { + while (index >= this.waypointModelInstances.size()) { + final MdxComplexInstance waypointModelInstance = (MdxComplexInstance) this.waypointModel.addInstance(); + waypointModelInstance.rotate(RenderUnit.tempQuat.setFromAxis(RenderMathUtils.VEC3_UNIT_Z, + this.war3MapViewer.simulation.getGameplayConstants().getBuildingAngle())); + waypointModelInstance.setSequenceLoopMode(SequenceLoopMode.ALWAYS_LOOP); + SequenceUtils.randomStandSequence(waypointModelInstance); + waypointModelInstance.hide(); + this.waypointModelInstances.add(waypointModelInstance); + } + return this.waypointModelInstances.get(index); + } + + private void reloadSelectedUnitUI(final RenderUnit unit) { + final CUnit simulationUnit = unit.getSimulationUnit(); + this.rootFrame.setText(this.unitLifeText, + FastNumberFormat.formatWholeNumber(simulationUnit.getLife()) + " / " + simulationUnit.getMaximumLife()); + final int maximumMana = simulationUnit.getMaximumMana(); + if (maximumMana > 0) { + this.rootFrame.setText(this.unitManaText, + FastNumberFormat.formatWholeNumber(simulationUnit.getMana()) + " / " + maximumMana); + } + else { + this.rootFrame.setText(this.unitManaText, ""); + } + repositionRallyPoint(simulationUnit); + repositionWaypointFlags(simulationUnit); + if ((simulationUnit.getBuildQueue()[0] != null) + && (simulationUnit.getPlayerIndex() == this.war3MapViewer.getLocalPlayerIndex())) { + for (int i = 0; i < this.queueIconFrames.length; i++) { + final QueueItemType queueItemType = simulationUnit.getBuildQueueTypes()[i]; + if (queueItemType == null) { + this.queueIconFrames[i].setVisible(false); + } + else { + this.queueIconFrames[i].setVisible(true); + switch (queueItemType) { + case RESEARCH: + final IconUI upgradeUI = this.war3MapViewer.getAbilityDataUI() + .getUpgradeUI(simulationUnit.getBuildQueue()[i], 0); + this.queueIconFrames[i].setTexture(upgradeUI.getIcon()); + this.queueIconFrames[i].setToolTip(upgradeUI.getToolTip()); + this.queueIconFrames[i].setUberTip(upgradeUI.getUberTip()); + break; + case UNIT: + default: + final IconUI unitUI = this.war3MapViewer.getAbilityDataUI() + .getUnitUI(simulationUnit.getBuildQueue()[i]); + this.queueIconFrames[i].setTexture(unitUI.getIcon()); + this.queueIconFrames[i].setToolTip(unitUI.getToolTip()); + this.queueIconFrames[i].setUberTip(unitUI.getUberTip()); + break; + } + } + } + this.simpleInfoPanelBuildingDetail.setVisible(true); + this.simpleInfoPanelUnitDetail.setVisible(false); + this.rootFrame.setText(this.simpleBuildingNameValue, simulationUnit.getUnitType().getName()); + this.rootFrame.setText(this.simpleBuildingDescriptionValue, ""); + + this.simpleBuildingBuildTimeIndicator.setVisible(true); + this.simpleBuildTimeIndicator.setVisible(false); + this.simpleHeroLevelBar.setVisible(false); + if (simulationUnit.getBuildQueueTypes()[0] == QueueItemType.UNIT) { + this.rootFrame.setText(this.simpleBuildingBuildingActionLabel, + this.rootFrame.getTemplates().getDecoratedString("TRAINING")); + } + else { + this.rootFrame.setText(this.simpleBuildingBuildingActionLabel, + this.rootFrame.getTemplates().getDecoratedString("RESEARCHING")); + } + this.attack1Icon.setVisible(false); + this.attack2Icon.setVisible(false); + this.armorIcon.setVisible(false); + this.heroInfoPanel.setVisible(false); + this.selectWorkerInsideFrame.setVisible(false); + } + else { + for (final QueueIcon queueIconFrame : this.queueIconFrames) { + queueIconFrame.setVisible(false); + } + this.simpleInfoPanelBuildingDetail.setVisible(false); + this.simpleInfoPanelUnitDetail.setVisible(true); + final String unitTypeName = simulationUnit.getUnitType().getName(); + + final boolean anyAttacks = simulationUnit.getAttacks().size() > 0; + final boolean constructing = simulationUnit.isConstructing(); + final UIFrame localArmorIcon = this.armorIcon; + final TextureFrame localArmorIconBackdrop = this.armorIconBackdrop; + final StringFrame localArmorInfoPanelIconValue = this.armorInfoPanelIconValue; + if (anyAttacks && !constructing) { + final CUnitAttack attackOne = simulationUnit.getAttacks().get(0); + this.attack1Icon.setVisible(attackOne.isShowUI()); + this.attack1IconBackdrop.setTexture(this.damageBackdrops.getTexture(attackOne.getAttackType())); + String attackOneDmgText = attackOne.getMinDamageDisplay() + " - " + attackOne.getMaxDamageDisplay(); + final int attackOneTemporaryDamageBonus = attackOne.getTemporaryDamageBonus(); + if (attackOneTemporaryDamageBonus != 0) { + attackOneDmgText += (attackOneTemporaryDamageBonus > 0 ? "|cFF00FF00 (+" : "|cFFFF0000 (+") + + attackOneTemporaryDamageBonus + ")"; + } + this.rootFrame.setText(this.attack1InfoPanelIconValue, attackOneDmgText); + if (simulationUnit.getAttacks().size() > 1) { + final CUnitAttack attackTwo = simulationUnit.getAttacks().get(1); + this.attack2Icon.setVisible(attackTwo.isShowUI()); + this.attack2IconBackdrop.setTexture(this.damageBackdrops.getTexture(attackTwo.getAttackType())); + String attackTwoDmgText = attackTwo.getMinDamage() + " - " + attackTwo.getMaxDamage(); + final int attackTwoTemporaryDamageBonus = attackTwo.getTemporaryDamageBonus(); + if (attackTwoTemporaryDamageBonus != 0) { + attackTwoDmgText += (attackTwoTemporaryDamageBonus > 0 ? "|cFF00FF00 (+" : "|cFFFF0000 (+") + + attackTwoTemporaryDamageBonus + ")"; + } + this.rootFrame.setText(this.attack2InfoPanelIconValue, attackTwoDmgText); + } + else { + this.attack2Icon.setVisible(false); + } + + this.smashArmorIconWrapper.addSetPoint( + new SetPoint(FramePoint.TOPLEFT, this.simpleInfoPanelUnitDetail, FramePoint.TOPLEFT, + GameUI.convertX(this.uiViewport, 0f), GameUI.convertY(this.uiViewport, -0.0625f))); + this.smashArmorIconWrapper.positionBounds(this.rootFrame, this.uiViewport); + this.armorIcon.positionBounds(this.rootFrame, this.uiViewport); + } + else { + this.attack1Icon.setVisible(false); + this.attack2Icon.setVisible(false); + + this.smashArmorIconWrapper.addSetPoint( + new SetPoint(FramePoint.TOPLEFT, this.simpleInfoPanelUnitDetail, FramePoint.TOPLEFT, + GameUI.convertX(this.uiViewport, 0f), GameUI.convertY(this.uiViewport, -0.032f))); + this.smashArmorIconWrapper.positionBounds(this.rootFrame, this.uiViewport); + this.armorIcon.positionBounds(this.rootFrame, this.uiViewport); + } + + final CAbilityHero heroData = simulationUnit.getHeroData(); + final boolean hero = heroData != null; + this.heroInfoPanel.setVisible(hero); + if (hero) { + final CPrimaryAttribute primaryAttribute = simulationUnit.getUnitType().getPrimaryAttribute(); + String iconKey; + switch (primaryAttribute) { + case AGILITY: + iconKey = "InfoPanelIconHeroIconAGI"; + break; + case INTELLIGENCE: + iconKey = "InfoPanelIconHeroIconINT"; + break; + default: + case STRENGTH: + iconKey = "InfoPanelIconHeroIconSTR"; + break; + } + this.primaryAttributeIcon.setTexture(iconKey, this.rootFrame); + + this.rootFrame.setText(this.strengthValue, heroData.getStrength().getDisplayText()); + this.rootFrame.setText(this.agilityValue, heroData.getAgility().getDisplayText()); + this.rootFrame.setText(this.intelligenceValue, heroData.getIntelligence().getDisplayText()); + final String infopanelLevelClass = this.rootFrame.getTemplates() + .getDecoratedString("INFOPANEL_LEVEL_CLASS").replace("%u", "%d"); // :( + final int heroLevel = heroData.getHeroLevel(); + this.rootFrame.setText(this.simpleClassValue, + String.format(infopanelLevelClass, heroLevel, unitTypeName)); + this.rootFrame.setText(this.simpleNameValue, heroData.getProperName()); + this.simpleHeroLevelBar.setVisible(true); + final CGameplayConstants gameplayConstants = this.war3MapViewer.simulation.getGameplayConstants(); + this.simpleHeroLevelBar.setValue((heroData.getXp() - gameplayConstants.getNeedHeroXPSum(heroLevel - 1)) + / (float) gameplayConstants.getNeedHeroXP(heroLevel)); + } + else { + this.rootFrame.setText(this.simpleNameValue, unitTypeName); + String classText = null; + for (final CUnitClassification classification : simulationUnit.getClassifications()) { + if ((classification == CUnitClassification.MECHANICAL) + && simulationUnit.getUnitType().isBuilding()) { + // buildings dont display MECHANICAL + continue; + } + if (classification.getDisplayName() != null) { + classText = classification.getDisplayName(); + } + } + if (classText != null) { + this.rootFrame.setText(this.simpleClassValue, classText); + } + else { + this.rootFrame.setText(this.simpleClassValue, ""); + } + this.simpleHeroLevelBar.setVisible(false); + } + final CAbilityInventory inventory = simulationUnit.getInventoryData(); + this.inventoryCover.setVisible(inventory == null); + if (inventory != null) { + this.inventoryBarFrame.setVisible(true); + int index = 0; + for (int i = 0; i < INVENTORY_HEIGHT; i++) { + for (int j = 0; j < INVENTORY_WIDTH; j++) { + final CommandCardIcon inventoryIcon = this.inventoryIcons[i][j]; + final CItem item = inventory.getItemInSlot(index); + if (item != null) { + final ItemUI itemUI = this.war3MapViewer.getAbilityDataUI().getItemUI(item.getTypeId()); + final IconUI iconUI = itemUI.getIconUI(); + final CItemType itemType = item.getItemType(); + // TODO: below we set menu=false, this is bad, item should be based on item abil + final boolean activelyUsed = itemType.isActivelyUsed(); + final boolean pawnable = itemType.isPawnable(); + final String uberTip = iconUI.getUberTip(); + this.recycleStringBuilder.setLength(0); + if (pawnable) { + this.recycleStringBuilder + .append(this.rootFrame.getTemplates().getDecoratedString("ITEM_PAWN_TOOLTIP")); + this.recycleStringBuilder.append("|n"); + } + if (activelyUsed) { + this.recycleStringBuilder + .append(this.rootFrame.getTemplates().getDecoratedString("ITEM_USE_TOOLTIP")); + this.recycleStringBuilder.append("|n"); + } + this.recycleStringBuilder.append(uberTip); + inventoryIcon.setCommandButtonData(iconUI.getIcon(), 0, + activelyUsed ? itemType.getCooldownGroup().getValue() : 0, index + 1, activelyUsed, + false, false, itemUI.getName(), this.recycleStringBuilder.toString(), + itemType.getGoldCost(), itemType.getLumberCost(), 0); + } + else { + if (index >= inventory.getItemCapacity()) { + inventoryIcon.setCommandButtonData(this.consoleInventoryNoCapacityTexture, 0, 0, 0, + false, false, false, null, null, 0, 0, 0); + } + else { + if (this.draggingItem != null) { + inventoryIcon.setCommandButtonData(null, 0, 0, index + 1, true, false, false, null, + null, 0, 0, 0); + } + else { + inventoryIcon.clear(); + } + } + } + index++; + } + } + } + + localArmorIcon.setVisible(!constructing); + this.simpleBuildTimeIndicator.setVisible(constructing); + this.simpleBuildingBuildTimeIndicator.setVisible(false); + if (constructing) { + this.rootFrame.setText(this.simpleBuildingActionLabel, + this.rootFrame.getTemplates().getDecoratedString("CONSTRUCTING")); + this.queueIconFrames[0].setVisible(true); + this.queueIconFrames[0].setTexture( + this.war3MapViewer.getAbilityDataUI().getUnitUI(simulationUnit.getTypeId()).getIcon()); + + if (simulationUnit.getWorkerInside() != null) { + this.selectWorkerInsideFrame.setVisible(true); + this.selectWorkerInsideFrame.setTexture(this.war3MapViewer.getAbilityDataUI() + .getUnitUI(simulationUnit.getWorkerInside().getTypeId()).getIcon()); + } + else { + this.selectWorkerInsideFrame.setVisible(false); + } + } + else { + this.rootFrame.setText(this.simpleBuildingActionLabel, ""); + this.selectWorkerInsideFrame.setVisible(false); + } + final Texture defenseTexture = this.defenseBackdrops + .getTexture(simulationUnit.getUnitType().getDefenseType()); + if (defenseTexture == null) { + throw new RuntimeException(simulationUnit.getUnitType().getDefenseType() + " can't find texture!"); + } + localArmorIconBackdrop.setTexture(defenseTexture); + + String defenseDisplayString = Integer.toString(simulationUnit.getCurrentDefenseDisplay()); + final int temporaryDefenseBonus = simulationUnit.getTemporaryDefenseBonus(); + if (temporaryDefenseBonus != 0) { + if (temporaryDefenseBonus > 0) { + defenseDisplayString += "|cFF00FF00 (+" + temporaryDefenseBonus + ")"; + } + else { + defenseDisplayString += "|cFFFF0000 (+" + temporaryDefenseBonus + ")"; + } + } + this.rootFrame.setText(localArmorInfoPanelIconValue, defenseDisplayString); + } + clearAndRepopulateCommandCard(); + } + + private void clearCommandCard() { + for (int j = 0; j < COMMAND_CARD_HEIGHT; j++) { + for (int i = 0; i < COMMAND_CARD_WIDTH; i++) { + this.commandCard[j][i].clear(); + } + } + } + + @Override + public void commandButton(final int buttonPositionX, final int buttonPositionY, final Texture icon, + final int abilityHandleId, final int orderId, final int autoCastId, final boolean active, + final boolean autoCastActive, final boolean menuButton, final String tip, final String uberTip, + final int goldCost, final int lumberCost, final int foodCost) { + int x = Math.max(0, Math.min(COMMAND_CARD_WIDTH - 1, buttonPositionX)); + int y = Math.max(0, Math.min(COMMAND_CARD_HEIGHT - 1, buttonPositionY)); + while ((x < COMMAND_CARD_WIDTH) && (y < COMMAND_CARD_HEIGHT) && this.commandCard[y][x].isVisible()) { + x++; + if (x >= COMMAND_CARD_WIDTH) { + x = 0; + y++; + } + } + if ((x < COMMAND_CARD_WIDTH) && (y < COMMAND_CARD_HEIGHT)) { + this.commandCard[y][x].setCommandButtonData(icon, abilityHandleId, orderId, autoCastId, active, + autoCastActive, menuButton, tip, uberTip, goldCost, lumberCost, foodCost); + } + } + + public void resize(final Rectangle viewport) { + this.cameraManager.resize(viewport); + positionPortrait(); + } + + public void positionPortrait() { + this.projectionTemp1.x = 422 * this.widthRatioCorrection; + this.projectionTemp1.y = 57 * this.heightRatioCorrection; + this.projectionTemp2.x = (422 + 167) * this.widthRatioCorrection; + this.projectionTemp2.y = (57 + 170) * this.heightRatioCorrection; + this.uiViewport.project(this.projectionTemp1); + this.uiViewport.project(this.projectionTemp2); + + this.tempRect.x = this.projectionTemp1.x + this.uiViewport.getScreenX(); + this.tempRect.y = this.projectionTemp1.y + this.uiViewport.getScreenY(); + this.tempRect.width = this.projectionTemp2.x - this.projectionTemp1.x; + this.tempRect.height = this.projectionTemp2.y - this.projectionTemp1.y; + this.portrait.portraitScene.camera.viewport(this.tempRect); + } + + private static final class InfoPanelIconBackdrops { + private final Texture[] damageBackdropTextures; + + public InfoPanelIconBackdrops(final CodeKeyType[] attackTypes, final GameUI gameUI, final String prefix, + final String suffix) { + this.damageBackdropTextures = new Texture[attackTypes.length]; + for (int index = 0; index < attackTypes.length; index++) { + final CodeKeyType attackType = attackTypes[index]; + String skinLookupKey = "InfoPanelIcon" + prefix + attackType.getCodeKey() + suffix; + final Texture suffixTexture = gameUI.loadTexture(gameUI.getSkinField(skinLookupKey)); + if (suffixTexture != null) { + this.damageBackdropTextures[index] = suffixTexture; + } + else { + skinLookupKey = "InfoPanelIcon" + prefix + attackType.getCodeKey(); + this.damageBackdropTextures[index] = gameUI.loadTexture(gameUI.getSkinField(skinLookupKey)); + } + } + } + + public Texture getTexture(final CodeKeyType attackType) { + if (attackType != null) { + final int ordinal = attackType.ordinal(); + if ((ordinal >= 0) && (ordinal < this.damageBackdropTextures.length)) { + return this.damageBackdropTextures[ordinal]; + } + } + return this.damageBackdropTextures[0]; + } + + private static String getSuffix(final CAttackType attackType) { + switch (attackType) { + case CHAOS: + return "Chaos"; + case HERO: + return "Hero"; + case MAGIC: + return "Magic"; + case NORMAL: + return "Normal"; + case PIERCE: + return "Pierce"; + case SIEGE: + return "Siege"; + case SPELLS: + return "Magic"; + case UNKNOWN: + return "Unknown"; + default: + throw new IllegalArgumentException("Unknown attack type: " + attackType); + } + + } + } + + @Override + public void lifeChanged() { + if (this.selectedUnit.getSimulationUnit().isDead()) { + selectUnit(null); + } + else { + this.rootFrame.setText(this.unitLifeText, + FastNumberFormat.formatWholeNumber(this.selectedUnit.getSimulationUnit().getLife()) + " / " + + this.selectedUnit.getSimulationUnit().getMaximumLife()); + } + } + + @Override + public void goldChanged() { + this.rootFrame.setText(this.resourceBarGoldText, Integer.toString(this.localPlayer.getGold())); + } + + @Override + public void lumberChanged() { + this.rootFrame.setText(this.resourceBarLumberText, Integer.toString(this.localPlayer.getLumber())); + } + + @Override + public void foodChanged() { + final int foodCap = this.localPlayer.getFoodCap(); + if (foodCap == 0) { + this.rootFrame.setText(this.resourceBarSupplyText, Integer.toString(this.localPlayer.getFoodUsed())); + this.resourceBarSupplyText.setColor(Color.WHITE); + } + else { + this.rootFrame.setText(this.resourceBarSupplyText, this.localPlayer.getFoodUsed() + "/" + foodCap); + this.resourceBarSupplyText.setColor(this.localPlayer.getFoodUsed() > foodCap ? Color.RED : Color.WHITE); + } + } + + @Override + public void upkeepChanged() { + this.rootFrame.setText(this.resourceBarUpkeepText, "Upkeep NYI"); + this.resourceBarUpkeepText.setColor(Color.CYAN); + } + + @Override + public void ordersChanged() { + reloadSelectedUnitUI(this.selectedUnit); + if (this.mouseOverUIFrame instanceof ClickableActionFrame) { + loadTooltip((ClickableActionFrame) this.mouseOverUIFrame); + } + } + + @Override + public void heroStatsChanged() { + reloadSelectedUnitUI(this.selectedUnit); + } + + @Override + public void inventoryChanged() { + reloadSelectedUnitUI(this.selectedUnit); + } + + @Override + public void queueChanged() { + reloadSelectedUnitUI(this.selectedUnit); + } + + private void clearAndRepopulateCommandCard() { + clearCommandCard(); + if (this.selectedUnit.getSimulationUnit().getPlayerIndex() == this.war3MapViewer.getLocalPlayerIndex()) { + final AbilityDataUI abilityDataUI = this.war3MapViewer.getAbilityDataUI(); + final int menuOrderId = getSubMenuOrderId(); + if ((this.activeCommand != null) && (this.draggingItem == null)) { + final IconUI cancelUI = abilityDataUI.getCancelUI(); + this.commandButton(cancelUI.getButtonPositionX(), cancelUI.getButtonPositionY(), cancelUI.getIcon(), 0, + menuOrderId, 0, false, false, true, cancelUI.getToolTip(), cancelUI.getUberTip(), 0, 0, 0); + } + else { + if (menuOrderId != 0) { + final int exitOrderId = this.subMenuOrderIdStack.size() > 1 + ? this.subMenuOrderIdStack.get(this.subMenuOrderIdStack.size() - 2) + : 0; + final IconUI cancelUI = abilityDataUI.getCancelUI(); + this.commandButton(cancelUI.getButtonPositionX(), cancelUI.getButtonPositionY(), cancelUI.getIcon(), + 0, exitOrderId, 0, false, false, true, cancelUI.getToolTip(), cancelUI.getUberTip(), 0, 0, + 0); + } + this.selectedUnit.populateCommandCard(this.war3MapViewer.simulation, this.rootFrame, this, + abilityDataUI, menuOrderId); + } + } + } + + private int getSubMenuOrderId() { + return this.subMenuOrderIdStack.isEmpty() ? 0 + : this.subMenuOrderIdStack.get(this.subMenuOrderIdStack.size() - 1); + } + + public RenderUnit getSelectedUnit() { + return this.selectedUnit; + } + + public boolean keyDown(final int keycode) { + return this.cameraManager.keyDown(keycode); + } + + public boolean keyUp(final int keycode) { + return this.cameraManager.keyUp(keycode); + } + + public void scrolled(final int amount) { + this.cameraManager.scrolled(amount); + } + + public boolean touchDown(final int screenX, final int screenY, final float worldScreenY, final int button) { + screenCoordsVector.set(screenX, screenY); + this.uiViewport.unproject(screenCoordsVector); + if (this.meleeUIMinimap.containsMouse(screenCoordsVector.x, screenCoordsVector.y)) { + final Vector2 worldPoint = this.meleeUIMinimap.getWorldPointFromScreen(screenCoordsVector.x, + screenCoordsVector.y); + this.cameraManager.target.x = worldPoint.x; + this.cameraManager.target.y = worldPoint.y; + return true; + } + final UIFrame clickedUIFrame = this.rootFrame.touchDown(screenCoordsVector.x, screenCoordsVector.y, button); + if (clickedUIFrame == null) { + // try to interact with world + if (this.activeCommand != null) { + if (button == Input.Buttons.RIGHT) { + this.activeCommandUnit = null; + this.activeCommand = null; + this.activeCommandOrderId = -1; + if (this.draggingItem != null) { + setDraggingItem(null); + } + clearAndRepopulateCommandCard(); + } + else { + final RenderWidget rayPickUnit = this.war3MapViewer.rayPickUnit(screenX, worldScreenY, + this.activeCommandUnitTargetFilter); + final boolean shiftDown = isShiftDown(); + if (rayPickUnit != null) { + this.unitOrderListener.issueTargetOrder( + this.activeCommandUnit.getSimulationUnit().getHandleId(), + this.activeCommand.getHandleId(), this.activeCommandOrderId, + rayPickUnit.getSimulationWidget().getHandleId(), shiftDown); + final UnitSound yesSound = (this.activeCommand instanceof CAbilityAttack) + ? getSelectedUnit().soundset.yesAttack + : getSelectedUnit().soundset.yes; + if (yesSound.playUnitResponse(this.war3MapViewer.worldScene.audioContext, getSelectedUnit())) { + portraitTalk(); + } + this.selectedSoundCount = 0; + if (this.activeCommand instanceof CAbilityRally) { + this.war3MapViewer.getUiSounds().getSound("RallyPointPlace").play(this.uiScene.audioContext, + 0, 0, 0); + } + if (!shiftDown) { + this.subMenuOrderIdStack.clear(); + this.activeCommandUnit = null; + this.activeCommand = null; + this.activeCommandOrderId = -1; + clearAndRepopulateCommandCard(); + } + } + else { + this.war3MapViewer.getClickLocation(clickLocationTemp, screenX, (int) worldScreenY); + clickLocationTemp2.set(clickLocationTemp.x, clickLocationTemp.y); + + if (this.draggingItem != null) { + this.war3MapViewer.showConfirmation(clickLocationTemp, 0, 1, 0); + + this.unitOrderListener.issueDropItemAtPointOrder( + this.activeCommandUnit.getSimulationUnit().getHandleId(), + this.activeCommand.getHandleId(), this.activeCommandOrderId, + this.draggingItem.getHandleId(), clickLocationTemp2.x, clickLocationTemp2.y, + shiftDown); + if (getSelectedUnit().soundset.yes + .playUnitResponse(this.war3MapViewer.worldScene.audioContext, getSelectedUnit())) { + portraitTalk(); + } + this.activeCommandUnit = null; + this.activeCommand = null; + this.activeCommandOrderId = -1; + setDraggingItem(null); + clearAndRepopulateCommandCard(); + } + else { + this.activeCommand.checkCanTarget(this.war3MapViewer.simulation, + this.activeCommandUnit.getSimulationUnit(), this.activeCommandOrderId, + clickLocationTemp2, PointAbilityTargetCheckReceiver.INSTANCE); + final Vector2 target = PointAbilityTargetCheckReceiver.INSTANCE.getTarget(); + if (target != null) { + if ((this.activeCommand instanceof CAbilityAttack) + && (this.activeCommandOrderId == OrderIds.attack)) { + this.war3MapViewer.showConfirmation(clickLocationTemp, 1, 0, 0); + } + else { + this.war3MapViewer.showConfirmation(clickLocationTemp, 0, 1, 0); + } + this.unitOrderListener.issuePointOrder( + this.activeCommandUnit.getSimulationUnit().getHandleId(), + this.activeCommand.getHandleId(), this.activeCommandOrderId, + clickLocationTemp2.x, clickLocationTemp2.y, shiftDown); + if (getSelectedUnit().soundset.yes.playUnitResponse( + this.war3MapViewer.worldScene.audioContext, getSelectedUnit())) { + portraitTalk(); + } + this.selectedSoundCount = 0; + if (this.activeCommand instanceof AbstractCAbilityBuild) { + this.war3MapViewer.getUiSounds().getSound("PlaceBuildingDefault") + .play(this.uiScene.audioContext, 0, 0, 0); + } + else if (this.activeCommand instanceof CAbilityRally) { + this.war3MapViewer.getUiSounds().getSound("RallyPointPlace") + .play(this.uiScene.audioContext, 0, 0, 0); + } + if (!shiftDown) { + this.subMenuOrderIdStack.clear(); + this.activeCommandUnit = null; + this.activeCommand = null; + this.activeCommandOrderId = -1; + clearAndRepopulateCommandCard(); + } + + } + } + + } + } + } + else { + if (button == Input.Buttons.RIGHT) { + if ((getSelectedUnit() != null) && (getSelectedUnit().getSimulationUnit() + .getPlayerIndex() == this.war3MapViewer.getLocalPlayerIndex())) { + final RenderWidget rayPickUnit = this.war3MapViewer.rayPickUnit(screenX, worldScreenY); + if ((rayPickUnit != null) && !rayPickUnit.getSimulationWidget().isDead()) { + boolean ordered = false; + boolean rallied = false; + boolean attacked = false; + for (final RenderUnit unit : this.selectedUnits) { + for (final CAbility ability : unit.getSimulationUnit().getAbilities()) { + ability.checkCanTarget(this.war3MapViewer.simulation, unit.getSimulationUnit(), + OrderIds.smart, rayPickUnit.getSimulationWidget(), + CWidgetAbilityTargetCheckReceiver.INSTANCE); + final CWidget targetWidget = CWidgetAbilityTargetCheckReceiver.INSTANCE.getTarget(); + if (targetWidget != null) { + this.unitOrderListener.issueTargetOrder(unit.getSimulationUnit().getHandleId(), + ability.getHandleId(), OrderIds.smart, targetWidget.getHandleId(), + isShiftDown()); + rallied |= ability instanceof CAbilityRally; + attacked |= ability instanceof CAbilityAttack; + ordered = true; + } + } + + } + if (ordered) { + final UnitSound yesSound = attacked ? getSelectedUnit().soundset.yesAttack + : getSelectedUnit().soundset.yes; + if (yesSound.playUnitResponse(this.war3MapViewer.worldScene.audioContext, + getSelectedUnit())) { + portraitTalk(); + } + if (rallied) { + this.war3MapViewer.getUiSounds().getSound("RallyPointPlace") + .play(this.uiScene.audioContext, 0, 0, 0); + } + this.selectedSoundCount = 0; + } + else { + rightClickMove(screenX, worldScreenY); + } + } + else { + rightClickMove(screenX, worldScreenY); + } + } + } + else { + final List selectedUnits = this.war3MapViewer.selectUnit(screenX, worldScreenY, + false); + if (!selectedUnits.isEmpty()) { + selectWidgets(selectedUnits); + } + } + } + } + else { + if (clickedUIFrame instanceof ClickableFrame) { + this.mouseDownUIFrame = (ClickableFrame) clickedUIFrame; + this.mouseDownUIFrame.mouseDown(this.rootFrame, this.uiViewport); + } + } + return false; + } + + private void rightClickMove(final int screenX, final float worldScreenY) { + this.war3MapViewer.getClickLocation(clickLocationTemp, screenX, (int) worldScreenY); + this.war3MapViewer.showConfirmation(clickLocationTemp, 0, 1, 0); + clickLocationTemp2.set(clickLocationTemp.x, clickLocationTemp.y); + + boolean ordered = false; + boolean rallied = false; + for (final RenderUnit unit : this.selectedUnits) { + if (unit.getSimulationUnit().getPlayerIndex() == this.war3MapViewer.getLocalPlayerIndex()) { + for (final CAbility ability : unit.getSimulationUnit().getAbilities()) { + ability.checkCanUse(this.war3MapViewer.simulation, unit.getSimulationUnit(), OrderIds.smart, + BooleanAbilityActivationReceiver.INSTANCE); + if (BooleanAbilityActivationReceiver.INSTANCE.isOk()) { + ability.checkCanTarget(this.war3MapViewer.simulation, unit.getSimulationUnit(), OrderIds.smart, + clickLocationTemp2, PointAbilityTargetCheckReceiver.INSTANCE); + final Vector2 target = PointAbilityTargetCheckReceiver.INSTANCE.getTarget(); + if (target != null) { + this.unitOrderListener.issuePointOrder(unit.getSimulationUnit().getHandleId(), + ability.getHandleId(), OrderIds.smart, clickLocationTemp2.x, clickLocationTemp2.y, + isShiftDown()); + rallied |= ability instanceof CAbilityRally; + ordered = true; + } + } + } + } + + } + + if (ordered) { + if (getSelectedUnit().soundset.yes.playUnitResponse(this.war3MapViewer.worldScene.audioContext, + getSelectedUnit())) { + portraitTalk(); + } + if (rallied) { + this.war3MapViewer.getUiSounds().getSound("RallyPointPlace").play(this.uiScene.audioContext, 0, 0, 0); + } + this.selectedSoundCount = 0; + } + } + + private void selectWidgets(final List selectedUnits) { + final List units = new ArrayList<>(); + for (final RenderWidget widget : selectedUnits) { + if (widget instanceof RenderUnit) { + units.add((RenderUnit) widget); + } + } + selectUnits(units); + } + + private void selectUnits(final List selectedUnits) { + this.selectedUnits = selectedUnits; + if (!selectedUnits.isEmpty()) { + final RenderUnit unit = selectedUnits.get(0); + final boolean selectionChanged = getSelectedUnit() != unit; + boolean playedNewSound = false; + if (selectionChanged) { + this.selectedSoundCount = 0; + } + if (unit.getSimulationUnit().getPlayerIndex() == this.war3MapViewer.getLocalPlayerIndex()) { + if (unit.soundset != null) { + UnitSound ackSoundToPlay = unit.soundset.what; + int soundIndex; + final int pissedSoundCount = unit.soundset.pissed.getSoundCount(); + if (unit.getSimulationUnit().isConstructing()) { + ackSoundToPlay = this.war3MapViewer.getUiSounds() + .getSound(this.rootFrame.getSkinField("ConstructingBuilding")); + soundIndex = (int) (Math.random() * ackSoundToPlay.getSoundCount()); + } + else { + if ((this.selectedSoundCount >= 3) && (pissedSoundCount > 0)) { + soundIndex = this.selectedSoundCount - 3; + ackSoundToPlay = unit.soundset.pissed; + } + else { + soundIndex = (int) (Math.random() * ackSoundToPlay.getSoundCount()); + } + } + if ((ackSoundToPlay != null) && ackSoundToPlay + .playUnitResponse(this.war3MapViewer.worldScene.audioContext, unit, soundIndex)) { + this.selectedSoundCount++; + if ((this.selectedSoundCount - 3) >= pissedSoundCount) { + this.selectedSoundCount = 0; + } + playedNewSound = true; + } + } + } + else { + this.war3MapViewer.getUiSounds().getSound("InterfaceClick").play(this.uiScene.audioContext, 0, 0, 0); + } + if (selectionChanged) { + selectUnit(unit); + } + if (playedNewSound) { + portraitTalk(); + } + } + else { + selectUnit(null); + } + } + + public boolean touchUp(final int screenX, final int screenY, final float worldScreenY, final int button) { + screenCoordsVector.set(screenX, screenY); + this.uiViewport.unproject(screenCoordsVector); + final UIFrame clickedUIFrame = this.rootFrame.touchUp(screenCoordsVector.x, screenCoordsVector.y, button); + if (this.mouseDownUIFrame != null) { + if (clickedUIFrame == this.mouseDownUIFrame) { + this.mouseDownUIFrame.onClick(button); + if (this.mouseDownUIFrame instanceof ClickableActionFrame) { + this.war3MapViewer.getUiSounds().getSound("InterfaceClick").play(this.uiScene.audioContext, 0, 0, + 0); + } + else { + this.war3MapViewer.getUiSounds().getSound("MenuButtonClick").play(this.uiScene.audioContext, 0, 0, + 0); + } + } + this.mouseDownUIFrame.mouseUp(this.rootFrame, this.uiViewport); + } + this.mouseDownUIFrame = null; + return false; + } + + private static boolean isShiftDown() { + return Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT) || Gdx.input.isKeyPressed(Input.Keys.SHIFT_RIGHT); + } + + public boolean touchDragged(final int screenX, final int screenY, final float worldScreenY, final int pointer) { + screenCoordsVector.set(screenX, screenY); + this.uiViewport.unproject(screenCoordsVector); + + if (this.meleeUIMinimap.containsMouse(screenCoordsVector.x, screenCoordsVector.y)) { + final Vector2 worldPoint = this.meleeUIMinimap.getWorldPointFromScreen(screenCoordsVector.x, + screenCoordsVector.y); + this.cameraManager.target.x = worldPoint.x; + this.cameraManager.target.y = worldPoint.y; + } + return false; + } + + public boolean mouseMoved(final int screenX, final int screenY, final float worldScreenY) { + screenCoordsVector.set(screenX, screenY); + this.uiViewport.unproject(screenCoordsVector); + final UIFrame mousedUIFrame = this.rootFrame.getFrameChildUnderMouse(screenCoordsVector.x, + screenCoordsVector.y); + if (mousedUIFrame != this.mouseOverUIFrame) { + if (this.mouseOverUIFrame != null) { + this.mouseOverUIFrame.mouseExit(this.rootFrame, this.uiViewport); + } + if (mousedUIFrame instanceof ClickableFrame) { + this.mouseOverUIFrame = (ClickableFrame) mousedUIFrame; + if (this.mouseOverUIFrame != null) { + this.mouseOverUIFrame.mouseEnter(this.rootFrame, this.uiViewport); + } + if (mousedUIFrame instanceof ClickableActionFrame) { + loadTooltip((ClickableActionFrame) mousedUIFrame); + } + } + else { + this.mouseOverUIFrame = null; + this.tooltipFrame.setVisible(false); + } + } + return false; + } + + private void loadTooltip(final ClickableActionFrame mousedUIFrame) { + final int goldCost = mousedUIFrame.getToolTipGoldCost(); + final int lumberCost = mousedUIFrame.getToolTipLumberCost(); + final int foodCost = mousedUIFrame.getToolTipFoodCost(); + final String toolTip = mousedUIFrame.getToolTip(); + final String uberTip = mousedUIFrame.getUberTip(); + if ((toolTip == null) || (uberTip == null)) { + this.tooltipFrame.setVisible(false); + } + else { + this.rootFrame.setText(this.tooltipUberTipText, uberTip); + int resourceIndex = 0; + if (goldCost != 0) { + this.tooltipResourceFrames[resourceIndex].setVisible(true); + this.tooltipResourceIconFrames[resourceIndex].setTexture("ToolTipGoldIcon", this.rootFrame); + this.rootFrame.setText(this.tooltipResourceTextFrames[resourceIndex], Integer.toString(goldCost)); + resourceIndex++; + } + if (lumberCost != 0) { + this.tooltipResourceFrames[resourceIndex].setVisible(true); + this.tooltipResourceIconFrames[resourceIndex].setTexture("ToolTipLumberIcon", this.rootFrame); + this.rootFrame.setText(this.tooltipResourceTextFrames[resourceIndex], Integer.toString(lumberCost)); + resourceIndex++; + } + if (foodCost != 0) { + this.tooltipResourceFrames[resourceIndex].setVisible(true); + this.tooltipResourceIconFrames[resourceIndex].setTexture("ToolTipSupplyIcon", this.rootFrame); + this.rootFrame.setText(this.tooltipResourceTextFrames[resourceIndex], Integer.toString(foodCost)); + resourceIndex++; + } + for (int i = resourceIndex; i < this.tooltipResourceFrames.length; i++) { + this.tooltipResourceFrames[i].setVisible(false); + } + float resourcesHeight; + if (resourceIndex != 0) { + this.tooltipUberTipText.addSetPoint(this.uberTipWithResourcesSetPoint); + resourcesHeight = 0.014f; + } + else { + this.tooltipUberTipText.addSetPoint(this.uberTipNoResourcesSetPoint); + resourcesHeight = 0.004f; + } + this.rootFrame.setText(this.tooltipText, toolTip); + final float predictedViewportHeight = this.tooltipText.getPredictedViewportHeight() + + GameUI.convertY(this.uiViewport, resourcesHeight) + + this.tooltipUberTipText.getPredictedViewportHeight() + GameUI.convertY(this.uiViewport, 0.003f); + this.tooltipFrame.setHeight(predictedViewportHeight); + this.tooltipFrame.positionBounds(this.rootFrame, this.uiViewport); + this.tooltipFrame.setVisible(true); + } + } + + public float getHeightRatioCorrection() { + return this.heightRatioCorrection; + } + + @Override + public void queueIconClicked(final int index) { + final CUnit simulationUnit = this.selectedUnit.getSimulationUnit(); + if (simulationUnit.isConstructing()) { + switch (index) { + case 0: + for (final CAbility ability : simulationUnit.getAbilities()) { + ability.checkCanUse(this.war3MapViewer.simulation, simulationUnit, OrderIds.cancel, + BooleanAbilityActivationReceiver.INSTANCE); + if (BooleanAbilityActivationReceiver.INSTANCE.isOk()) { + + final BooleanAbilityTargetCheckReceiver targetCheckReceiver = BooleanAbilityTargetCheckReceiver + .getInstance().reset(); + ability.checkCanTargetNoTarget(this.war3MapViewer.simulation, simulationUnit, OrderIds.cancel, + targetCheckReceiver); + if (targetCheckReceiver.isTargetable()) { + this.unitOrderListener.issueImmediateOrder(simulationUnit.getHandleId(), + ability.getHandleId(), OrderIds.cancel, false); + } + } + } + break; + case 1: + final List unitList = Arrays.asList( + this.war3MapViewer.getRenderPeer(this.selectedUnit.getSimulationUnit().getWorkerInside())); + this.war3MapViewer.doSelectUnit(unitList); + selectWidgets(unitList); + break; + } + } + else { + this.unitOrderListener.unitCancelTrainingItem(simulationUnit.getHandleId(), index); + } + } + + public void dispose() { + if (this.rootFrame != null) { + this.rootFrame.dispose(); + } + } + + private class ItemCommandCardCommandListener implements CommandCardCommandListener { + @Override + public void onClick(final int abilityHandleId, final int orderId, final boolean rightClick) { + if (rightClick) { + final RenderUnit selectedUnit2 = MeleeUI.this.selectedUnit; + final CUnit simulationUnit = selectedUnit2.getSimulationUnit(); + final CAbilityInventory inventoryData = simulationUnit.getInventoryData(); + final int slot = orderId - 1; + final CItem itemInSlot = inventoryData.getItemInSlot(slot); + if (MeleeUI.this.draggingItem != null) { + final CUnit activeCmdSimUnit = MeleeUI.this.activeCommandUnit.getSimulationUnit(); + MeleeUI.this.unitOrderListener.issueTargetOrder(activeCmdSimUnit.getHandleId(), + activeCmdSimUnit.getInventoryData().getHandleId(), OrderIds.itemdrag00 + slot, + MeleeUI.this.draggingItem.getHandleId(), false); + setDraggingItem(null); + MeleeUI.this.activeCommand = null; + MeleeUI.this.activeCommandUnit = null; + } + else { + if (itemInSlot != null) { + setDraggingItem(itemInSlot); + MeleeUI.this.activeCommand = inventoryData; + MeleeUI.this.activeCommandUnit = selectedUnit2; + } + } + } + } + + @Override + public void openMenu(final int orderId) { + MeleeUI.this.openMenu(orderId); + } + + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/MeleeUIMinimap.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/MeleeUIMinimap.java new file mode 100644 index 0000000..c3af169 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/MeleeUIMinimap.java @@ -0,0 +1,62 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui; + +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.math.Vector2; +import com.etheller.warsmash.viewer5.handlers.w3x.rendersim.RenderUnit; + +public class MeleeUIMinimap { + private final Rectangle minimap; + private final Rectangle minimapFilledArea; + private final Texture minimapTexture; + private final Rectangle playableMapArea; + private final Texture[] teamColors; + + public MeleeUIMinimap(final Rectangle displayArea, final Rectangle playableMapArea, final Texture minimapTexture, + final Texture[] teamColors) { + this.playableMapArea = playableMapArea; + this.minimapTexture = minimapTexture; + this.teamColors = teamColors; + this.minimap = displayArea; + final float worldWidth = playableMapArea.getWidth(); + final float worldHeight = playableMapArea.getHeight(); + final float worldSize = Math.max(worldWidth, worldHeight); + final float minimapFilledWidth = (worldWidth / worldSize) * this.minimap.width; + final float minimapFilledHeight = (worldHeight / worldSize) * this.minimap.height; + + this.minimapFilledArea = new Rectangle(this.minimap.x + ((this.minimap.width - minimapFilledWidth) / 2), + this.minimap.y + ((this.minimap.height - minimapFilledHeight) / 2), minimapFilledWidth, + minimapFilledHeight); + } + + public void render(final SpriteBatch batch, final Iterable units) { + batch.draw(this.minimapTexture, this.minimap.x, this.minimap.y, this.minimap.width, this.minimap.height); + + for (final RenderUnit unit : units) { + final Texture minimapIcon = this.teamColors[unit.getSimulationUnit().getPlayerIndex()]; + batch.draw(minimapIcon, + this.minimapFilledArea.x + + (((unit.location[0] - this.playableMapArea.getX()) / (this.playableMapArea.getWidth())) + * this.minimapFilledArea.width), + this.minimapFilledArea.y + + (((unit.location[1] - this.playableMapArea.getY()) / (this.playableMapArea.getHeight())) + * this.minimapFilledArea.height), + 4, 4); + } + } + + public Vector2 getWorldPointFromScreen(final float screenX, final float screenY) { + final Rectangle filledArea = this.minimapFilledArea; + final float clickX = (screenX - filledArea.x) / filledArea.width; + final float clickY = (screenY - filledArea.y) / filledArea.height; + final float worldX = (clickX * this.playableMapArea.width) + this.playableMapArea.x; + final float worldY = (clickY * this.playableMapArea.height) + this.playableMapArea.y; + return new Vector2(worldX, worldY); + + } + + public boolean containsMouse(final float x, final float y) { + return this.minimapFilledArea.contains(x, y); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/MenuUI.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/MenuUI.java new file mode 100644 index 0000000..32308c8 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/MenuUI.java @@ -0,0 +1,1121 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui; + +import java.io.IOException; +import java.io.InputStream; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.utils.viewport.ExtendViewport; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.SingleModelScreen; +import com.etheller.warsmash.WarsmashGdxMapScreen; +import com.etheller.warsmash.WarsmashGdxMenuScreen; +import com.etheller.warsmash.WarsmashGdxMultiScreenGame; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.datamodel.FramePoint; +import com.etheller.warsmash.parsers.fdf.frames.EditBoxFrame; +import com.etheller.warsmash.parsers.fdf.frames.GlueButtonFrame; +import com.etheller.warsmash.parsers.fdf.frames.GlueTextButtonFrame; +import com.etheller.warsmash.parsers.fdf.frames.ListBoxFrame; +import com.etheller.warsmash.parsers.fdf.frames.SetPoint; +import com.etheller.warsmash.parsers.fdf.frames.SimpleFrame; +import com.etheller.warsmash.parsers.fdf.frames.SpriteFrame; +import com.etheller.warsmash.parsers.fdf.frames.StringFrame; +import com.etheller.warsmash.parsers.fdf.frames.UIFrame; +import com.etheller.warsmash.parsers.jass.Jass2.RootFrameListener; +import com.etheller.warsmash.parsers.w3x.War3Map; +import com.etheller.warsmash.parsers.w3x.w3i.War3MapW3i; +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.units.Element; +import com.etheller.warsmash.units.custom.WTS; +import com.etheller.warsmash.util.StringBundle; +import com.etheller.warsmash.util.WarsmashConstants; +import com.etheller.warsmash.util.WorldEditStrings; +import com.etheller.warsmash.viewer5.Scene; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxViewer; +import com.etheller.warsmash.viewer5.handlers.w3x.UnitSound; +import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.config.War3MapConfig; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CRace; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.ClickableFrame; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.FocusableFrame; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.menu.CampaignMenuData; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.menu.CampaignMenuUI; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.menu.CampaignMission; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.sound.KeyedSounds; + +public class MenuUI { + private static final Vector2 screenCoordsVector = new Vector2(); + private static boolean ENABLE_NOT_YET_IMPLEMENTED_BUTTONS = false; + + private final DataSource dataSource; + private final Scene uiScene; + private final Viewport uiViewport; + private final MdxViewer viewer; + private final RootFrameListener rootFrameListener; + private final float widthRatioCorrection; + private final float heightRatioCorrection; + private GameUI rootFrame; + private SpriteFrame cursorFrame; + + private ClickableFrame mouseDownUIFrame; + private ClickableFrame mouseOverUIFrame; + private FocusableFrame focusUIFrame; + + private UIFrame mainMenuFrame; + + private SpriteFrame glueSpriteLayerTopRight; + + private SpriteFrame glueSpriteLayerTopLeft; + + private WorldEditStrings worldEditStrings; + + private DataTable uiSoundsTable; + + private KeyedSounds uiSounds; + + private GlueTextButtonFrame singlePlayerButton; + private GlueTextButtonFrame battleNetButton; + private GlueTextButtonFrame localAreaNetworkButton; + private GlueTextButtonFrame optionsButton; + private GlueTextButtonFrame creditsButton; + private GlueButtonFrame realmButton; + private GlueTextButtonFrame exitButton; + + private final boolean quitting = false; + + private MenuState menuState; + + private UIFrame singlePlayerMenu; + private UIFrame singlePlayerMainPanel; + + private UIFrame skirmish; + + private UIFrame profilePanel; + private EditBoxFrame newProfileEditBox; + + private GlueButtonFrame profileButton; + private GlueTextButtonFrame campaignButton; + private GlueTextButtonFrame loadSavedButton; + private GlueTextButtonFrame viewReplayButton; + private GlueTextButtonFrame customCampaignButton; + private GlueTextButtonFrame skirmishButton; + private GlueTextButtonFrame singlePlayerCancelButton; + private GlueButtonFrame editionButton; + + private GlueTextButtonFrame skirmishCancelButton; + + private final WarsmashGdxMultiScreenGame screenManager; + + private final DataTable warsmashIni; + + private UnitSound glueScreenLoop; + + private SpriteFrame warcraftIIILogo; + // Campaign + private UIFrame campaignMenu; + private SpriteFrame campaignFade; + private GlueTextButtonFrame campaignBackButton; + private UIFrame missionSelectFrame; + private UIFrame campaignSelectFrame; + private final DataTable campaignStrings; + private SpriteFrame campaignWarcraftIIILogo; + private final SingleModelScreen menuScreen; + + private CampaignMenuData currentCampaign; + private String[] campaignList; + private CampaignMenuData[] campaignDatas; + private UnitSound mainMenuGlueScreenLoop; + private GlueTextButtonFrame addProfileButton; + private GlueTextButtonFrame deleteProfileButton; + private GlueTextButtonFrame selectProfileButton; + private final PlayerProfileManager profileManager; + private StringFrame profileNameText; + private UIFrame confirmDialog; + private CampaignMenuUI campaignRootMenuUI; + private CampaignMenuUI currentMissionSelectMenuUI; + private UIFrame loadingFrame; + private UIFrame loadingCustomPanel; + private UIFrame loadingMeleePanel; + private StringFrame loadingTitleText; + private StringFrame loadingSubtitleText; + private StringFrame loadingText; + private SpriteFrame loadingBar; + private String mapFilepathToStart; + private LoadingMap loadingMap; + private SpriteFrame loadingBackground; + + public MenuUI(final DataSource dataSource, final Viewport uiViewport, final Scene uiScene, final MdxViewer viewer, + final WarsmashGdxMultiScreenGame screenManager, final SingleModelScreen menuScreen, + final DataTable warsmashIni, final RootFrameListener rootFrameListener) { + this.dataSource = dataSource; + this.uiViewport = uiViewport; + this.uiScene = uiScene; + this.viewer = viewer; + this.screenManager = screenManager; + this.menuScreen = menuScreen; + this.warsmashIni = warsmashIni; + this.rootFrameListener = rootFrameListener; + + this.widthRatioCorrection = getMinWorldWidth() / 1600f; + this.heightRatioCorrection = getMinWorldHeight() / 1200f; + + this.campaignStrings = new DataTable(StringBundle.EMPTY); + final String campaignStringPath = "UI\\CampaignStrings" + (WarsmashConstants.GAME_VERSION == 1 ? "_exp" : "") + + ".txt"; + if (dataSource.has(campaignStringPath)) { + try (InputStream campaignStringStream = dataSource.getResourceAsStream(campaignStringPath)) { + this.campaignStrings.readTXT(campaignStringStream, true); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + else { + try (InputStream campaignStringStream = dataSource.getResourceAsStream("UI\\CampaignInfoClassic.txt")) { + this.campaignStrings.readTXT(campaignStringStream, true); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + + this.profileManager = PlayerProfileManager.loadFromGdx(); + } + + public float getHeightRatioCorrection() { + return this.heightRatioCorrection; + } + + /** + * Called "main" because this was originally written in JASS so that maps could + * override it, and I may convert it back to the JASS at some point. + */ + public void main() { + // ================================= + // Load skins and templates + // ================================= + this.rootFrame = new GameUI(this.dataSource, GameUI.loadSkin(this.dataSource, WarsmashConstants.GAME_VERSION), + this.uiViewport, this.uiScene, this.viewer, 0, WTS.DO_NOTHING); + + this.rootFrameListener.onCreate(this.rootFrame); + try { + this.rootFrame.loadTOCFile("UI\\FrameDef\\FrameDef.toc"); + } + catch (final IOException exc) { + throw new IllegalStateException("Unable to load FrameDef.toc", exc); + } + try { + this.rootFrame.loadTOCFile("UI\\FrameDef\\SmashFrameDef.toc"); + } + catch (final IOException exc) { + throw new IllegalStateException("Unable to load SmashFrameDef.toc", exc); + } + + // Create main menu + this.mainMenuFrame = this.rootFrame.createFrame("MainMenuFrame", this.rootFrame, 0, 0); + + this.warcraftIIILogo = (SpriteFrame) this.rootFrame.getFrameByName("WarCraftIIILogo", 0); + this.rootFrame.setSpriteFrameModel(this.warcraftIIILogo, + this.rootFrame.getSkinField("MainMenuLogo_V" + WarsmashConstants.GAME_VERSION)); + this.warcraftIIILogo.addSetPoint(new SetPoint(FramePoint.TOPLEFT, this.mainMenuFrame, FramePoint.TOPLEFT, + GameUI.convertX(this.uiViewport, 0.13f), GameUI.convertY(this.uiViewport, -0.08f))); + setMainMenuVisible(false); + this.rootFrame.getFrameByName("RealmSelect", 0).setVisible(false); + + this.glueSpriteLayerTopRight = (SpriteFrame) this.rootFrame.createFrameByType("SPRITE", + "SmashGlueSpriteLayerTopRight", this.rootFrame, "", 0); + this.glueSpriteLayerTopRight.setSetAllPoints(true); + final String topRightModel = this.rootFrame + .getSkinField("GlueSpriteLayerTopRight_V" + WarsmashConstants.GAME_VERSION); + this.rootFrame.setSpriteFrameModel(this.glueSpriteLayerTopRight, topRightModel); + this.glueSpriteLayerTopRight.setSequence("MainMenu Birth"); + + this.glueSpriteLayerTopLeft = (SpriteFrame) this.rootFrame.createFrameByType("SPRITE", + "SmashGlueSpriteLayerTopLeft", this.rootFrame, "", 0); + this.glueSpriteLayerTopLeft.setSetAllPoints(true); + final String topLeftModel = this.rootFrame + .getSkinField("GlueSpriteLayerTopLeft_V" + WarsmashConstants.GAME_VERSION); + this.rootFrame.setSpriteFrameModel(this.glueSpriteLayerTopLeft, topLeftModel); + this.glueSpriteLayerTopLeft.setSequence("MainMenu Birth"); + + this.cursorFrame = (SpriteFrame) this.rootFrame.createFrameByType("SPRITE", "SmashCursorFrame", this.rootFrame, + "", 0); + this.rootFrame.setSpriteFrameModel(this.cursorFrame, this.rootFrame.getSkinField("Cursor")); + this.cursorFrame.setSequence("Normal"); + this.cursorFrame.setZDepth(-1.0f); + Gdx.input.setCursorCatched(true); + + // Main Menu interactivity + this.singlePlayerButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("SinglePlayerButton", 0); + this.battleNetButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("BattleNetButton", 0); + this.realmButton = (GlueButtonFrame) this.rootFrame.getFrameByName("RealmButton", 0); + this.localAreaNetworkButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("LocalAreaNetworkButton", 0); + this.optionsButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("OptionsButton", 0); + this.creditsButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("CreditsButton", 0); + this.exitButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("ExitButton", 0); + this.editionButton = (GlueButtonFrame) this.rootFrame.getFrameByName("EditionButton", 0); + + if (this.editionButton != null) { + this.editionButton.setOnClick(new Runnable() { + @Override + public void run() { + WarsmashConstants.GAME_VERSION = (WarsmashConstants.GAME_VERSION == 1 ? 0 : 1); + MenuUI.this.glueSpriteLayerTopLeft.setSequence("MainMenu Death"); + MenuUI.this.glueSpriteLayerTopRight.setSequence("MainMenu Death"); + setMainMenuVisible(false); + MenuUI.this.menuState = MenuState.RESTARTING; + } + }); + } + + this.battleNetButton.setEnabled(false); + this.realmButton.setEnabled(false); + this.localAreaNetworkButton.setEnabled(false); + this.optionsButton.setEnabled(false); + this.creditsButton.setEnabled(false); + + this.exitButton.setOnClick(new Runnable() { + @Override + public void run() { + MenuUI.this.glueSpriteLayerTopLeft.setSequence("MainMenu Death"); + MenuUI.this.glueSpriteLayerTopRight.setSequence("MainMenu Death"); + setMainMenuVisible(false); + MenuUI.this.menuState = MenuState.QUITTING; + } + }); + + this.singlePlayerButton.setOnClick(new Runnable() { + @Override + public void run() { + MenuUI.this.glueSpriteLayerTopLeft.setSequence("MainMenu Death"); + MenuUI.this.glueSpriteLayerTopRight.setSequence("MainMenu Death"); + setMainMenuVisible(false); + MenuUI.this.menuState = MenuState.GOING_TO_SINGLE_PLAYER; + } + }); + + // Create single player + this.singlePlayerMenu = this.rootFrame.createFrame("SinglePlayerMenu", this.rootFrame, 0, 0); + this.singlePlayerMenu.setVisible(false); + + this.profilePanel = this.rootFrame.getFrameByName("ProfilePanel", 0); + this.profilePanel.setVisible(false); + + this.newProfileEditBox = (EditBoxFrame) this.rootFrame.getFrameByName("NewProfileEditBox", 0); + this.newProfileEditBox.setOnChange(new Runnable() { + @Override + public void run() { + MenuUI.this.addProfileButton + .setEnabled(!MenuUI.this.profileManager.hasProfile(MenuUI.this.newProfileEditBox.getText())); + } + }); + final StringFrame profileListText = (StringFrame) this.rootFrame.getFrameByName("ProfileListText", 0); + final SimpleFrame profileListContainer = (SimpleFrame) this.rootFrame.getFrameByName("ProfileListContainer", 0); + final ListBoxFrame profileListBox = (ListBoxFrame) this.rootFrame.createFrameByType("LISTBOX", "ListBoxWar3", + profileListContainer, "WITHCHILDREN", 0); + profileListBox.setSetAllPoints(true); + profileListBox.setFrameFont(profileListText.getFrameFont()); + for (final PlayerProfile profile : this.profileManager.getProfiles()) { + profileListBox.addItem(profile.getName(), this.rootFrame, this.uiViewport); + } + profileListContainer.add(profileListBox); + + this.addProfileButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("AddProfileButton", 0); + this.deleteProfileButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("DeleteProfileButton", 0); + this.selectProfileButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("SelectProfileButton", 0); + this.selectProfileButton.setEnabled(false); + this.deleteProfileButton.setEnabled(false); + this.addProfileButton.setOnClick(new Runnable() { + @Override + public void run() { + final String newProfileName = MenuUI.this.newProfileEditBox.getText(); + if (!newProfileName.isEmpty() && !MenuUI.this.profileManager.hasProfile(newProfileName)) { + MenuUI.this.profileManager.addProfile(newProfileName); + profileListBox.addItem(newProfileName, MenuUI.this.rootFrame, MenuUI.this.uiViewport); + MenuUI.this.addProfileButton.setEnabled(false); + } + } + }); + this.deleteProfileButton.setOnClick(new Runnable() { + @Override + public void run() { + final int selectedIndex = profileListBox.getSelectedIndex(); + final boolean validSelect = (selectedIndex >= 0) + && (selectedIndex < MenuUI.this.profileManager.getProfiles().size()); + if (validSelect) { + if (MenuUI.this.profileManager.getProfiles().size() > 1) { + final PlayerProfile profileToRemove = MenuUI.this.profileManager.getProfiles() + .get(selectedIndex); + final String removeProfileName = profileToRemove.getName(); + final boolean deletingCurrentProfile = removeProfileName + .equals(MenuUI.this.profileManager.getCurrentProfile()); + MenuUI.this.profileManager.removeProfile(profileToRemove); + profileListBox.removeItem(selectedIndex, MenuUI.this.rootFrame, MenuUI.this.uiViewport); + if (deletingCurrentProfile) { + setCurrentProfile(MenuUI.this.profileManager.getProfiles().get(0).getName()); + } + } + } + } + }); + this.selectProfileButton.setOnClick(new Runnable() { + @Override + public void run() { + final int selectedIndex = profileListBox.getSelectedIndex(); + final boolean validSelect = (selectedIndex >= 0) + && (selectedIndex < MenuUI.this.profileManager.getProfiles().size()); + if (validSelect) { + final PlayerProfile profileToSelect = MenuUI.this.profileManager.getProfiles().get(selectedIndex); + final String selectedProfileName = profileToSelect.getName(); + setCurrentProfile(selectedProfileName); + + MenuUI.this.glueSpriteLayerTopLeft.setSequence("RealmSelection Death"); + MenuUI.this.profilePanel.setVisible(false); + MenuUI.this.menuState = MenuState.SINGLE_PLAYER; + setSinglePlayerButtonsEnabled(false); + } + + } + + }); + profileListBox.setOnSelect(new Runnable() { + @Override + public void run() { + final int selectedIndex = profileListBox.getSelectedIndex(); + final boolean validSelect = (selectedIndex >= 0) + && (selectedIndex < MenuUI.this.profileManager.getProfiles().size()); + MenuUI.this.selectProfileButton.setEnabled(validSelect); + MenuUI.this.deleteProfileButton.setEnabled(validSelect); + } + }); + + this.singlePlayerMainPanel = this.rootFrame.getFrameByName("MainPanel", 0); + + // Single Player Interactivity + this.profileButton = (GlueButtonFrame) this.rootFrame.getFrameByName("ProfileButton", 0); + this.campaignButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("CampaignButton", 0); + this.loadSavedButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("LoadSavedButton", 0); + this.viewReplayButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("ViewReplayButton", 0); + this.customCampaignButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("CustomCampaignButton", 0); + this.skirmishButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("SkirmishButton", 0); + + this.singlePlayerCancelButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("CancelButton", 0); + + this.profileNameText = (StringFrame) this.rootFrame.getFrameByName("ProfileNameText", 0); + this.rootFrame.setText(this.profileNameText, this.profileManager.getCurrentProfile()); + + setSinglePlayerButtonsEnabled(true); + + this.profileButton.setOnClick(new Runnable() { + @Override + public void run() { + MenuUI.this.glueSpriteLayerTopLeft.setSequence("RealmSelection Birth"); + setSinglePlayerButtonsEnabled(false); + MenuUI.this.menuState = MenuState.SINGLE_PLAYER_PROFILE; + } + }); + + this.campaignButton.setOnClick(new Runnable() { + @Override + public void run() { + MenuUI.this.glueSpriteLayerTopLeft.setSequence("SinglePlayer Death"); + MenuUI.this.glueSpriteLayerTopRight.setSequence("SinglePlayer Death"); + MenuUI.this.singlePlayerMenu.setVisible(false); + MenuUI.this.profilePanel.setVisible(false); + MenuUI.this.menuState = MenuState.GOING_TO_CAMPAIGN; + } + }); + + this.skirmishButton.setOnClick(new Runnable() { + @Override + public void run() { + MenuUI.this.glueSpriteLayerTopLeft.setSequence("SinglePlayer Death"); + MenuUI.this.glueSpriteLayerTopRight.setSequence("SinglePlayer Death"); + MenuUI.this.singlePlayerMenu.setVisible(false); + MenuUI.this.profilePanel.setVisible(false); + MenuUI.this.menuState = MenuState.GOING_TO_SINGLE_PLAYER_SKIRMISH; + } + }); + + this.singlePlayerCancelButton.setOnClick(new Runnable() { + @Override + public void run() { + if (MenuUI.this.menuState == MenuState.SINGLE_PLAYER_PROFILE) { + MenuUI.this.glueSpriteLayerTopLeft.setSequence("RealmSelection Death"); + MenuUI.this.profilePanel.setVisible(false); + } + else { + MenuUI.this.glueSpriteLayerTopLeft.setSequence("SinglePlayer Death"); + } + MenuUI.this.glueSpriteLayerTopRight.setSequence("SinglePlayer Death"); + MenuUI.this.singlePlayerMenu.setVisible(false); + MenuUI.this.menuState = MenuState.GOING_TO_MAIN_MENU; + } + }); + + // Create skirmish UI + this.skirmish = this.rootFrame.createFrame("Skirmish", this.rootFrame, 0, 0); + this.skirmish.setVisible(false); + + this.skirmishCancelButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("CancelButton", 0); + this.skirmishCancelButton.setOnClick(new Runnable() { + @Override + public void run() { + MenuUI.this.glueSpriteLayerTopLeft.setSequence("SinglePlayerSkirmish Death"); + MenuUI.this.glueSpriteLayerTopRight.setSequence("SinglePlayerSkirmish Death"); + MenuUI.this.skirmish.setVisible(false); + MenuUI.this.menuState = MenuState.GOING_TO_SINGLE_PLAYER; + + } + }); + + // Create Campaign UI + + this.campaignMenu = this.rootFrame.createFrame("CampaignMenu", this.rootFrame, 0, 0); + this.campaignMenu.setVisible(false); + this.campaignFade = (SpriteFrame) this.rootFrame.getFrameByName("SlidingDoors", 0); + this.campaignFade.setVisible(false); + this.campaignBackButton = (GlueTextButtonFrame) this.rootFrame.getFrameByName("BackButton", 0); + this.campaignBackButton.setVisible(false); + this.missionSelectFrame = this.rootFrame.getFrameByName("MissionSelectFrame", 0); + this.missionSelectFrame.setVisible(false); + final StringFrame missionName = (StringFrame) this.rootFrame.getFrameByName("MissionName", 0); + final StringFrame missionNameHeader = (StringFrame) this.rootFrame.getFrameByName("MissionNameHeader", 0); + + this.campaignSelectFrame = this.rootFrame.getFrameByName("CampaignSelectFrame", 0); + this.campaignSelectFrame.setVisible(false); + + this.campaignWarcraftIIILogo = (SpriteFrame) this.rootFrame.getFrameByName("WarCraftIIILogo", 0); + this.rootFrame.setSpriteFrameModel(this.campaignWarcraftIIILogo, + this.rootFrame.getSkinField("MainMenuLogo_V" + WarsmashConstants.GAME_VERSION)); + this.campaignWarcraftIIILogo.setVisible(false); + this.campaignWarcraftIIILogo + .addSetPoint(new SetPoint(FramePoint.TOPRIGHT, this.campaignMenu, FramePoint.TOPRIGHT, + GameUI.convertX(this.uiViewport, -0.13f), GameUI.convertY(this.uiViewport, -0.08f))); + this.campaignRootMenuUI = new CampaignMenuUI(null, this.campaignMenu, this.rootFrame, this.uiViewport); + this.campaignRootMenuUI.setVisible(false); + this.campaignRootMenuUI.addSetPoint(new SetPoint(FramePoint.TOPRIGHT, this.campaignMenu, FramePoint.TOPRIGHT, + GameUI.convertX(this.uiViewport, -0.0f), GameUI.convertY(this.uiViewport, -0.12f))); + this.campaignRootMenuUI.setWidth(GameUI.convertX(this.uiViewport, 0.30f)); + this.campaignRootMenuUI.setHeight(GameUI.convertY(this.uiViewport, 0.42f)); + this.rootFrame.add(this.campaignRootMenuUI); + + this.campaignBackButton.setOnClick(new Runnable() { + @Override + public void run() { + if (MenuUI.this.currentMissionSelectMenuUI != null) { + MenuUI.this.currentMissionSelectMenuUI.setVisible(false); + MenuUI.this.missionSelectFrame.setVisible(false); + MenuUI.this.menuState = MenuState.CAMPAIGN; + MenuUI.this.currentMissionSelectMenuUI = null; + } + else { + MenuUI.this.campaignMenu.setVisible(false); + MenuUI.this.campaignBackButton.setVisible(false); + MenuUI.this.missionSelectFrame.setVisible(false); + MenuUI.this.campaignSelectFrame.setVisible(false); + MenuUI.this.campaignWarcraftIIILogo.setVisible(false); + MenuUI.this.campaignRootMenuUI.setVisible(false); + MenuUI.this.campaignFade.setSequence("Birth"); + MenuUI.this.menuState = MenuState.LEAVING_CAMPAIGN; + } + } + }); + final Element campaignIndex = this.campaignStrings.get("Index"); + this.campaignList = campaignIndex.getField("CampaignList").split(","); + this.campaignDatas = new CampaignMenuData[this.campaignList.length]; + for (int i = 0; i < this.campaignList.length; i++) { + final String campaign = this.campaignList[i]; + final Element campaignElement = this.campaignStrings.get(campaign); + if (campaignElement != null) { + final CampaignMenuData newCampaign = new CampaignMenuData(campaignElement); + this.campaignDatas[i] = newCampaign; + if (this.currentCampaign == null) { + this.currentCampaign = newCampaign; + } + + } + } + for (final CampaignMenuData campaign : this.campaignDatas) { + if (campaign != null) { + final CampaignMenuUI missionSelectMenuUI = new CampaignMenuUI(null, this.campaignMenu, this.rootFrame, + this.uiViewport); + missionSelectMenuUI.setVisible(false); + missionSelectMenuUI + .addSetPoint(new SetPoint(FramePoint.TOPRIGHT, this.campaignMenu, FramePoint.TOPRIGHT, + GameUI.convertX(this.uiViewport, -0.0f), GameUI.convertY(this.uiViewport, -0.12f))); + missionSelectMenuUI.setWidth(GameUI.convertX(this.uiViewport, 0.30f)); + missionSelectMenuUI.setHeight(GameUI.convertY(this.uiViewport, 0.42f)); + this.rootFrame.add(missionSelectMenuUI); + + for (final CampaignMission mission : campaign.getMissions()) { + missionSelectMenuUI.addButton(mission.getHeader(), mission.getMissionName(), new Runnable() { + @Override + public void run() { + MenuUI.this.campaignMenu.setVisible(false); + MenuUI.this.campaignBackButton.setVisible(false); + MenuUI.this.missionSelectFrame.setVisible(false); + MenuUI.this.campaignSelectFrame.setVisible(false); + MenuUI.this.campaignWarcraftIIILogo.setVisible(false); + MenuUI.this.campaignRootMenuUI.setVisible(false); + MenuUI.this.currentMissionSelectMenuUI.setVisible(false); + MenuUI.this.campaignFade.setSequence("Birth"); + MenuUI.this.mapFilepathToStart = mission.getMapFilename(); + } + }); + } + + this.campaignRootMenuUI.addButton(campaign.getHeader(), campaign.getName(), new Runnable() { + @Override + public void run() { + if (campaign != MenuUI.this.currentCampaign) { + MenuUI.this.campaignMenu.setVisible(false); + MenuUI.this.campaignBackButton.setVisible(false); + MenuUI.this.missionSelectFrame.setVisible(false); + MenuUI.this.campaignSelectFrame.setVisible(false); + MenuUI.this.campaignWarcraftIIILogo.setVisible(false); + MenuUI.this.campaignRootMenuUI.setVisible(false); + MenuUI.this.campaignFade.setSequence("Birth"); + MenuUI.this.currentCampaign = campaign; + MenuUI.this.currentMissionSelectMenuUI = missionSelectMenuUI; + MenuUI.this.menuState = MenuState.GOING_TO_MISSION_SELECT; + } + else { + MenuUI.this.campaignSelectFrame.setVisible(false); + MenuUI.this.campaignRootMenuUI.setVisible(false); + MenuUI.this.currentMissionSelectMenuUI.setVisible(true); + MenuUI.this.missionSelectFrame.setVisible(true); + MenuUI.this.menuState = MenuState.MISSION_SELECT; + } + MenuUI.this.rootFrame.setText(missionName, campaign.getName()); + MenuUI.this.rootFrame.setText(missionNameHeader, campaign.getHeader()); + } + }); + if (campaign == MenuUI.this.currentCampaign) { + MenuUI.this.currentMissionSelectMenuUI = missionSelectMenuUI; + } + } + } + + this.confirmDialog = this.rootFrame.createFrame("DialogWar3", this.rootFrame, 0, 0); + this.confirmDialog.setVisible(false); + + this.loadingFrame = this.rootFrame.createFrame("Loading", this.rootFrame, 0, 0); + this.loadingFrame.setVisible(false); + this.loadingCustomPanel = this.rootFrame.getFrameByName("LoadingCustomPanel", 0); + this.loadingCustomPanel.setVisible(false); + this.loadingTitleText = (StringFrame) this.rootFrame.getFrameByName("LoadingTitleText", 0); + this.loadingSubtitleText = (StringFrame) this.rootFrame.getFrameByName("LoadingSubtitleText", 0); + this.loadingText = (StringFrame) this.rootFrame.getFrameByName("LoadingText", 0); + this.loadingBar = (SpriteFrame) this.rootFrame.getFrameByName("LoadingBar", 0); + this.loadingBackground = (SpriteFrame) this.rootFrame.getFrameByName("LoadingBackground", 0); + + this.loadingMeleePanel = this.rootFrame.getFrameByName("LoadingMeleePanel", 0); + this.loadingMeleePanel.setVisible(false); + + // position all + this.rootFrame.positionBounds(this.rootFrame, this.uiViewport); + + this.menuState = MenuState.GOING_TO_MAIN_MENU; + + loadSounds(); + + final String glueLoopField = this.rootFrame.getSkinField("GlueScreenLoop_V" + WarsmashConstants.GAME_VERSION); + this.mainMenuGlueScreenLoop = this.uiSounds.getSound(glueLoopField); + this.glueScreenLoop = this.mainMenuGlueScreenLoop; + this.glueScreenLoop.play(this.uiScene.audioContext, 0f, 0f, 0f); + } + + private void internalStartMap(final String mapFilename) { + this.loadingFrame.setVisible(true); + this.loadingBar.setVisible(true); + this.loadingCustomPanel.setVisible(true); + final DataSource codebase = WarsmashGdxMapScreen.parseDataSources(this.warsmashIni); + final War3MapViewer viewer = new War3MapViewer(codebase, this.screenManager, + new War3MapConfig(WarsmashConstants.MAX_PLAYERS)); + + if (WarsmashGdxMapScreen.ENABLE_AUDIO) { + viewer.worldScene.enableAudio(); + viewer.enableAudio(); + } + try { + final War3Map map = viewer.beginLoadingMap(mapFilename); + final War3MapW3i mapInfo = map.readMapInformation(); + final DataTable worldEditData = viewer.loadWorldEditData(map); + final WTS wts = viewer.preloadWTS(map); + + final int loadingScreen = mapInfo.getLoadingScreen(); + System.out.println("LOADING SCREEN INT: " + loadingScreen); + final int campaignBackground = mapInfo.getCampaignBackground(); + final Element loadingScreens = worldEditData.get("LoadingScreens"); + final String key = String.format("%2s", Integer.toString(campaignBackground)).replace(' ', '0'); + final int animationSequenceIndex = loadingScreens.getFieldValue(key, 2); + final String campaignScreenModel = loadingScreens.getField(key, 3); + + this.menuScreen.setModel(null); + this.rootFrame.setSpriteFrameModel(this.loadingBackground, campaignScreenModel); + this.loadingBackground.setSequence(animationSequenceIndex); + this.rootFrame.setSpriteFrameModel(this.loadingBar, this.rootFrame.getSkinField("LoadingProgressBar")); + this.loadingBar.setSequence(0); + this.loadingBar.setFrameByRatio(0.5f); + this.loadingBar.setZDepth(1.0f); + this.rootFrame.setText(this.loadingTitleText, getStringWithWTS(wts, mapInfo.getLoadingScreenTitle())); + this.rootFrame.setText(this.loadingSubtitleText, getStringWithWTS(wts, mapInfo.getLoadingScreenSubtitle())); + this.rootFrame.setText(this.loadingText, getStringWithWTS(wts, mapInfo.getLoadingScreenText())); + this.loadingMap = new LoadingMap(viewer, map, mapInfo); + + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + + private static String getStringWithWTS(final WTS wts, String string) { + if (string.startsWith("TRIGSTR_")) { + string = wts.get(Integer.parseInt(string.substring(8))); + } + return string; + } + + public void startMap(final String mapFilename) { + this.mainMenuFrame.setVisible(false); + internalStartMap(mapFilename); + } + + private void setCurrentProfile(final String selectedProfileName) { + this.profileManager.setCurrentProfile(selectedProfileName); + this.rootFrame.setText(MenuUI.this.profileNameText, selectedProfileName); + } + + protected void setSinglePlayerButtonsEnabled(final boolean b) { + this.profileButton.setEnabled(b); + this.campaignButton.setEnabled(b); + this.loadSavedButton.setEnabled(b && ENABLE_NOT_YET_IMPLEMENTED_BUTTONS); + this.viewReplayButton.setEnabled(b && ENABLE_NOT_YET_IMPLEMENTED_BUTTONS); + this.customCampaignButton.setEnabled(b && ENABLE_NOT_YET_IMPLEMENTED_BUTTONS); + this.skirmishButton.setEnabled(b); + this.singlePlayerCancelButton.setEnabled(b); + } + + private void setMainMenuVisible(final boolean visible) { + this.mainMenuFrame.setVisible(visible); + this.warcraftIIILogo.setVisible(visible); + } + + public void resize() { + + } + + public void render(final SpriteBatch batch, final GlyphLayout glyphLayout) { + final BitmapFont font = this.rootFrame.getFont(); + final BitmapFont font20 = this.rootFrame.getFont20(); + font.setColor(Color.YELLOW); + final String fpsString = "FPS: " + Gdx.graphics.getFramesPerSecond(); + glyphLayout.setText(font, fpsString); + font.draw(batch, fpsString, (getMinWorldWidth() - glyphLayout.width) / 2, 1100 * this.heightRatioCorrection); + this.rootFrame.render(batch, font20, glyphLayout); + } + + private float getMinWorldWidth() { + if (this.uiViewport instanceof ExtendViewport) { + return ((ExtendViewport) this.uiViewport).getMinWorldWidth(); + } + return this.uiViewport.getWorldWidth(); + } + + private float getMinWorldHeight() { + if (this.uiViewport instanceof ExtendViewport) { + return ((ExtendViewport) this.uiViewport).getMinWorldHeight(); + } + return this.uiViewport.getWorldHeight(); + } + + public void update(final float deltaTime) { + if (this.mapFilepathToStart != null) { + this.campaignFade.setVisible(false); + internalStartMap(this.mapFilepathToStart); + this.mapFilepathToStart = null; + return; + } + else if (this.loadingMap != null) { + try { + this.loadingMap.viewer.loadMap(this.loadingMap.map, this.loadingMap.mapInfo, 0); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + // TODO not cast menu screen + MenuUI.this.screenManager.setScreen(new WarsmashGdxMapScreen(this.loadingMap.viewer, this.screenManager, + (WarsmashGdxMenuScreen) this.menuScreen)); + this.loadingMap = null; + + this.loadingBar.setVisible(false); + this.loadingFrame.setVisible(false); + this.loadingBackground.setVisible(false); + return; + } + if ((this.focusUIFrame != null) && !this.focusUIFrame.isVisibleOnScreen()) { + setFocusFrame(getNextFocusFrame()); + } + + final int baseMouseX = Gdx.input.getX(); + int mouseX = baseMouseX; + final int baseMouseY = Gdx.input.getY(); + int mouseY = baseMouseY; + final int minX = this.uiViewport.getScreenX(); + final int maxX = minX + this.uiViewport.getScreenWidth(); + final int minY = this.uiViewport.getScreenY(); + final int maxY = minY + this.uiViewport.getScreenHeight(); + + mouseX = Math.max(minX, Math.min(maxX, mouseX)); + mouseY = Math.max(minY, Math.min(maxY, mouseY)); + if (Gdx.input.isCursorCatched()) { + Gdx.input.setCursorPosition(mouseX, mouseY); + } + + screenCoordsVector.set(mouseX, mouseY); + this.uiViewport.unproject(screenCoordsVector); + this.cursorFrame.setFramePointX(FramePoint.LEFT, screenCoordsVector.x); + this.cursorFrame.setFramePointY(FramePoint.BOTTOM, screenCoordsVector.y); + this.cursorFrame.setSequence("Normal"); + + if (this.glueSpriteLayerTopRight.isSequenceEnded() && this.glueSpriteLayerTopLeft.isSequenceEnded() + && (!this.campaignFade.isVisible() || this.campaignFade.isSequenceEnded())) { + switch (this.menuState) { + case GOING_TO_MAIN_MENU: + this.glueSpriteLayerTopLeft.setSequence("MainMenu Birth"); + this.glueSpriteLayerTopRight.setSequence("MainMenu Birth"); + this.menuState = MenuState.MAIN_MENU; + break; + case MAIN_MENU: + setMainMenuVisible(true); + this.glueSpriteLayerTopLeft.setSequence("MainMenu Stand"); + this.glueSpriteLayerTopRight.setSequence("MainMenu Stand"); + break; + case GOING_TO_SINGLE_PLAYER: + this.glueSpriteLayerTopLeft.setSequence("SinglePlayer Birth"); + this.glueSpriteLayerTopRight.setSequence("SinglePlayer Birth"); + this.menuState = MenuState.SINGLE_PLAYER; + break; + case LEAVING_CAMPAIGN: + this.glueSpriteLayerTopLeft.setSequence("Birth"); + this.glueSpriteLayerTopRight.setSequence("Birth"); + if (this.campaignFade.isVisible()) { + this.campaignFade.setSequence("Death"); + } + this.glueScreenLoop.stop(); + this.glueScreenLoop = this.mainMenuGlueScreenLoop; + this.glueScreenLoop.play(this.uiScene.audioContext, 0f, 0f, 0f); + this.menuScreen.setModel( + this.rootFrame.getSkinField("GlueSpriteLayerBackground_V" + WarsmashConstants.GAME_VERSION)); + this.rootFrame.setSpriteFrameModel(this.cursorFrame, this.rootFrame.getSkinField("Cursor")); + this.menuState = MenuState.GOING_TO_SINGLE_PLAYER; + break; + case SINGLE_PLAYER: + this.singlePlayerMenu.setVisible(true); + this.campaignFade.setVisible(false); + setSinglePlayerButtonsEnabled(true); + this.glueSpriteLayerTopLeft.setSequence("SinglePlayer Stand"); + this.glueSpriteLayerTopRight.setSequence("SinglePlayer Stand"); + break; + case GOING_TO_SINGLE_PLAYER_SKIRMISH: + this.glueSpriteLayerTopLeft.setSequence("SinglePlayerSkirmish Birth"); + this.glueSpriteLayerTopRight.setSequence("SinglePlayerSkirmish Birth"); + this.menuState = MenuState.SINGLE_PLAYER_SKIRMISH; + break; + case SINGLE_PLAYER_SKIRMISH: + this.skirmish.setVisible(true); + this.glueSpriteLayerTopLeft.setSequence("SinglePlayerSkirmish Stand"); + this.glueSpriteLayerTopRight.setSequence("SinglePlayerSkirmish Stand"); + break; + case GOING_TO_CAMPAIGN: + this.glueSpriteLayerTopLeft.setSequence("Death"); + this.glueSpriteLayerTopRight.setSequence("Death"); + this.campaignMenu.setVisible(true); + this.campaignFade.setVisible(true); + this.campaignFade.setSequence("Birth"); + this.menuState = MenuState.GOING_TO_CAMPAIGN_PART2; + break; + case GOING_TO_CAMPAIGN_PART2: { + final String currentCampaignBackgroundModel = this.rootFrame + .getSkinField(this.currentCampaign.getBackground() + "_V" + WarsmashConstants.GAME_VERSION); + final String currentCampaignAmbientSound = this.rootFrame + .trySkinField(this.currentCampaign.getAmbientSound()); + this.menuScreen.setModel(currentCampaignBackgroundModel); + this.glueScreenLoop.stop(); + this.glueScreenLoop = this.uiSounds.getSound(currentCampaignAmbientSound); + this.glueScreenLoop.play(this.uiScene.audioContext, 0f, 0f, 0f); + final DataTable skinData = this.rootFrame.getSkinData(); + final String cursorSkin = CRace.VALUES[this.currentCampaign.getCursor()].name(); + this.rootFrame.setSpriteFrameModel(this.cursorFrame, skinData.get(cursorSkin).getField("Cursor")); + + this.campaignFade.setSequence("Death"); + this.menuState = MenuState.CAMPAIGN; + break; + } + case CAMPAIGN: + this.campaignMenu.setVisible(true); + this.campaignBackButton.setVisible(true); + this.campaignWarcraftIIILogo.setVisible(true); + this.campaignSelectFrame.setVisible(true); + this.campaignRootMenuUI.setVisible(true); + break; + case GOING_TO_MISSION_SELECT: { + final String currentCampaignBackgroundModel = this.rootFrame + .getSkinField(this.currentCampaign.getBackground() + "_V" + WarsmashConstants.GAME_VERSION); + final String currentCampaignAmbientSound = this.rootFrame + .trySkinField(this.currentCampaign.getAmbientSound()); + this.menuScreen.setModel(currentCampaignBackgroundModel); + this.glueScreenLoop.stop(); + this.glueScreenLoop = this.uiSounds.getSound(currentCampaignAmbientSound); + this.glueScreenLoop.play(this.uiScene.audioContext, 0f, 0f, 0f); + final DataTable skinData = this.rootFrame.getSkinData(); + final String cursorSkin = CRace.VALUES[this.currentCampaign.getCursor()].name(); + this.rootFrame.setSpriteFrameModel(this.cursorFrame, skinData.get(cursorSkin).getField("Cursor")); + + this.campaignFade.setSequence("Death"); + this.menuState = MenuState.MISSION_SELECT; + break; + } + case MISSION_SELECT: + this.campaignMenu.setVisible(true); + this.campaignBackButton.setVisible(true); + this.campaignWarcraftIIILogo.setVisible(true); + this.currentMissionSelectMenuUI.setVisible(true); + this.missionSelectFrame.setVisible(true); + break; + case GOING_TO_SINGLE_PLAYER_PROFILE: + this.glueSpriteLayerTopLeft.setSequence("RealmSelection Birth"); + this.menuState = MenuState.SINGLE_PLAYER_PROFILE; + break; + case SINGLE_PLAYER_PROFILE: + this.profilePanel.setVisible(true); + setSinglePlayerButtonsEnabled(true); + this.glueSpriteLayerTopLeft.setSequence("RealmSelection Stand"); + // TODO the below should probably be some generic focusing thing when we enter a + // new view? + if ((this.newProfileEditBox != null) && this.newProfileEditBox.isFocusable()) { + setFocusFrame(this.newProfileEditBox); + } + break; + case QUITTING: + Gdx.app.exit(); + break; + case RESTARTING: + MenuUI.this.screenManager + .setScreen(new WarsmashGdxMenuScreen(MenuUI.this.warsmashIni, this.screenManager)); + break; + default: + break; + } + } + + } + + private FocusableFrame getNextFocusFrame() { + return this.rootFrame.getNextFocusFrame(); + } + + public boolean touchDown(final int screenX, final int screenY, final float worldScreenY, final int button) { + screenCoordsVector.set(screenX, screenY); + this.uiViewport.unproject(screenCoordsVector); + final UIFrame clickedUIFrame = this.rootFrame.touchDown(screenCoordsVector.x, screenCoordsVector.y, button); + if (clickedUIFrame != null) { + if (clickedUIFrame instanceof ClickableFrame) { + this.mouseDownUIFrame = (ClickableFrame) clickedUIFrame; + this.mouseDownUIFrame.mouseDown(this.rootFrame, this.uiViewport); + } + if (clickedUIFrame instanceof FocusableFrame) { + final FocusableFrame clickedFocusableFrame = (FocusableFrame) clickedUIFrame; + if (clickedFocusableFrame.isFocusable()) { + setFocusFrame(clickedFocusableFrame); + } + } + } + return false; + } + + private void setFocusFrame(final FocusableFrame clickedFocusableFrame) { + if (this.focusUIFrame != null) { + this.focusUIFrame.onFocusLost(); + } + this.focusUIFrame = clickedFocusableFrame; + if (this.focusUIFrame != null) { + this.focusUIFrame.onFocusGained(); + } + } + + public boolean touchUp(final int screenX, final int screenY, final float worldScreenY, final int button) { + screenCoordsVector.set(screenX, screenY); + this.uiViewport.unproject(screenCoordsVector); + final UIFrame clickedUIFrame = this.rootFrame.touchUp(screenCoordsVector.x, screenCoordsVector.y, button); + if (this.mouseDownUIFrame != null) { + if (clickedUIFrame == this.mouseDownUIFrame) { + this.mouseDownUIFrame.onClick(button); + this.uiSounds.getSound("GlueScreenClick").play(this.uiScene.audioContext, 0, 0, 0); + } + this.mouseDownUIFrame.mouseUp(this.rootFrame, this.uiViewport); + } + this.mouseDownUIFrame = null; + return false; + } + + public boolean touchDragged(final int screenX, final int screenY, final float worldScreenY, final int pointer) { + mouseMoved(screenX, screenY, worldScreenY); + return false; + } + + public boolean mouseMoved(final int screenX, final int screenY, final float worldScreenY) { + screenCoordsVector.set(screenX, screenY); + this.uiViewport.unproject(screenCoordsVector); + final UIFrame mousedUIFrame = this.rootFrame.getFrameChildUnderMouse(screenCoordsVector.x, + screenCoordsVector.y); + if (mousedUIFrame != this.mouseOverUIFrame) { + if (this.mouseOverUIFrame != null) { + this.mouseOverUIFrame.mouseExit(this.rootFrame, this.uiViewport); + } + if (mousedUIFrame instanceof ClickableFrame) { + this.mouseOverUIFrame = (ClickableFrame) mousedUIFrame; + if (this.mouseOverUIFrame != null) { + this.mouseOverUIFrame.mouseEnter(this.rootFrame, this.uiViewport); + } + } + else { + this.mouseOverUIFrame = null; + } + } + return false; + } + + private void loadSounds() { + this.worldEditStrings = new WorldEditStrings(this.dataSource); + this.uiSoundsTable = new DataTable(this.worldEditStrings); + try { + try (InputStream miscDataTxtStream = this.dataSource.getResourceAsStream("UI\\SoundInfo\\UISounds.slk")) { + this.uiSoundsTable.readSLK(miscDataTxtStream); + } + try (InputStream miscDataTxtStream = this.dataSource + .getResourceAsStream("UI\\SoundInfo\\AmbienceSounds.slk")) { + this.uiSoundsTable.readSLK(miscDataTxtStream); + } + } + catch (final IOException e) { + e.printStackTrace(); + } + this.uiSounds = new KeyedSounds(this.uiSoundsTable, this.dataSource); + } + + public KeyedSounds getUiSounds() { + return this.uiSounds; + } + + private static enum MenuState { + GOING_TO_MAIN_MENU, + MAIN_MENU, + GOING_TO_SINGLE_PLAYER, + LEAVING_CAMPAIGN, + SINGLE_PLAYER, + GOING_TO_SINGLE_PLAYER_SKIRMISH, + SINGLE_PLAYER_SKIRMISH, + GOING_TO_CAMPAIGN, + GOING_TO_CAMPAIGN_PART2, + GOING_TO_MISSION_SELECT, + MISSION_SELECT, + CAMPAIGN, + GOING_TO_SINGLE_PLAYER_PROFILE, + SINGLE_PLAYER_PROFILE, + GOING_TO_LOADING_SCREEN, + QUITTING, + RESTARTING; + } + + public void hide() { + this.glueScreenLoop.stop(); + } + + public void dispose() { + if (this.rootFrame != null) { + this.rootFrame.dispose(); + } + } + + public boolean keyDown(final int keycode) { + if (this.focusUIFrame != null) { + this.focusUIFrame.keyDown(keycode); + } + return false; + } + + public boolean keyUp(final int keycode) { + if (this.focusUIFrame != null) { + this.focusUIFrame.keyUp(keycode); + } + return false; + } + + public boolean keyTyped(final char character) { + if (this.focusUIFrame != null) { + this.focusUIFrame.keyTyped(character); + } + return false; + } + + public void onReturnFromGame() { +// MenuUI.this.campaignMenu.setVisible(true); +// MenuUI.this.campaignBackButton.setVisible(true); +// MenuUI.this.missionSelectFrame.setVisible(true); +// MenuUI.this.campaignSelectFrame.setVisible(false); +// MenuUI.this.campaignWarcraftIIILogo.setVisible(true); +// MenuUI.this.campaignRootMenuUI.setVisible(false); +// MenuUI.this.currentMissionSelectMenuUI.setVisible(true); + switch (this.menuState) { + default: + case GOING_TO_MAIN_MENU: + case MAIN_MENU: + this.glueScreenLoop.stop(); + this.glueScreenLoop = this.mainMenuGlueScreenLoop; + this.glueScreenLoop.play(this.uiScene.audioContext, 0f, 0f, 0f); + this.menuScreen.setModel( + this.rootFrame.getSkinField("GlueSpriteLayerBackground_V" + WarsmashConstants.GAME_VERSION)); + this.rootFrame.setSpriteFrameModel(this.cursorFrame, this.rootFrame.getSkinField("Cursor")); + break; + case CAMPAIGN: + case MISSION_SELECT: + final String currentCampaignBackgroundModel = this.rootFrame + .getSkinField(this.currentCampaign.getBackground() + "_V" + WarsmashConstants.GAME_VERSION); + final String currentCampaignAmbientSound = this.rootFrame + .trySkinField(this.currentCampaign.getAmbientSound()); + this.menuScreen.setModel(currentCampaignBackgroundModel); + this.glueScreenLoop.stop(); + this.glueScreenLoop = this.uiSounds.getSound(currentCampaignAmbientSound); + this.glueScreenLoop.play(this.uiScene.audioContext, 0f, 0f, 0f); + final DataTable skinData = this.rootFrame.getSkinData(); + final String cursorSkin = CRace.VALUES[this.currentCampaign.getCursor()].name(); + this.rootFrame.setSpriteFrameModel(this.cursorFrame, skinData.get(cursorSkin).getField("Cursor")); + break; + } +// MenuUI.this.campaignFade.setSequence("Death"); +// this.campaignFade.setVisible(true); +// this.menuState = MenuState.MISSION_SELECT; + } + + private static final class LoadingMap { + + private final War3MapViewer viewer; + private final War3Map map; + private final War3MapW3i mapInfo; + + public LoadingMap(final War3MapViewer viewer, final War3Map map, final War3MapW3i mapInfo) { + this.viewer = viewer; + this.map = map; + this.mapInfo = mapInfo; + } + + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/PlayerProfile.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/PlayerProfile.java new file mode 100644 index 0000000..932da41 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/PlayerProfile.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui; + +public class PlayerProfile { + private final String name; + + public PlayerProfile(final String name) { + this.name = name; + } + + public String getName() { + return this.name; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/PlayerProfileManager.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/PlayerProfileManager.java new file mode 100644 index 0000000..5314e74 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/PlayerProfileManager.java @@ -0,0 +1,93 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui; + +import java.util.ArrayList; +import java.util.List; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Preferences; + +public class PlayerProfileManager { + private static final String CURRENT_PROFILE = "CurrentProfile"; + private static final String PROFILE_COUNT = "ProfileCount"; + private final Preferences preferences; + private final List profiles; + private String currentProfile; + + public static PlayerProfileManager loadFromGdx() { + final Preferences preferences = Gdx.app.getPreferences("WarsmashWC3Engine"); + final int profileCount = preferences.getInteger(PROFILE_COUNT); + final List profiles = new ArrayList<>(); + for (int i = 0; i < profileCount; i++) { + final String name = preferences.getString("Profile" + i + "_Name"); + profiles.add(new PlayerProfile(name)); + } + final String currentProfile = preferences.getString(CURRENT_PROFILE, "WorldEdit"); + if (profiles.isEmpty()) { + final PlayerProfile worldEditDefaultProfile = new PlayerProfile("WorldEdit"); + saveProfile(preferences, profiles.size(), worldEditDefaultProfile); + profiles.add(worldEditDefaultProfile); + preferences.putInteger(PROFILE_COUNT, profiles.size()); + preferences.flush(); + } + return new PlayerProfileManager(preferences, profiles, currentProfile); + } + + public PlayerProfileManager(final Preferences preferences, final List profiles, + final String currentProfile) { + this.preferences = preferences; + this.profiles = profiles; + this.currentProfile = currentProfile; + } + + public List getProfiles() { + return this.profiles; + } + + public PlayerProfile addProfile(final String name) { + final PlayerProfile playerProfile = new PlayerProfile(name); + saveProfile(this.preferences, this.profiles.size(), playerProfile); + this.profiles.add(playerProfile); + this.preferences.putInteger(PROFILE_COUNT, this.profiles.size()); + this.preferences.flush(); + return playerProfile; + } + + public void setCurrentProfile(final String currentProfile) { + this.currentProfile = currentProfile; + this.preferences.putString(CURRENT_PROFILE, this.currentProfile); + this.preferences.flush(); + } + + public String getCurrentProfile() { + return this.currentProfile; + } + + public void saveAll() { + final int size = this.profiles.size(); + this.preferences.putInteger(PROFILE_COUNT, size); + this.preferences.putString(CURRENT_PROFILE, this.currentProfile); + for (int i = 0; i < size; i++) { + final PlayerProfile playerProfile = this.profiles.get(i); + saveProfile(this.preferences, i, playerProfile); + } + this.preferences.flush(); + } + + private static void saveProfile(final Preferences preferences, final int i, final PlayerProfile playerProfile) { + preferences.putString("Profile" + i + "_Name", playerProfile.getName()); + } + + public boolean hasProfile(final String text) { + for (final PlayerProfile profile : this.profiles) { + if (profile.getName().equals(text)) { + return true; + } + } + return false; + } + + public void removeProfile(final PlayerProfile profileToRemove) { + this.profiles.remove(profileToRemove); + saveAll(); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/QueueIcon.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/QueueIcon.java new file mode 100644 index 0000000..fe090d2 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/QueueIcon.java @@ -0,0 +1,151 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui; + +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.frames.AbstractRenderableFrame; +import com.etheller.warsmash.parsers.fdf.frames.TextureFrame; +import com.etheller.warsmash.parsers.fdf.frames.UIFrame; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.ClickableActionFrame; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.QueueIconListener; + +public class QueueIcon extends AbstractRenderableFrame implements ClickableActionFrame { + + private TextureFrame iconFrame; + private final QueueIconListener clickListener; + private float defaultWidth; + private float defaultHeight; + private final int queueIconIndexId; + + private String toolTip; + private String uberTip; + + public QueueIcon(final String name, final UIFrame parent, final QueueIconListener clickListener, + final int queueIconIndexId) { + super(name, parent); + this.clickListener = clickListener; + this.queueIconIndexId = queueIconIndexId; + } + + public void set(final TextureFrame iconFrame) { + this.iconFrame = iconFrame; + setVisible(true); + } + + public void clear() { + setVisible(false); + } + + public void setTexture(final Texture texture) { + this.iconFrame.setTexture(texture); + } + + @Override + protected void innerPositionBounds(final GameUI gameUI, final Viewport viewport) { + this.iconFrame.positionBounds(gameUI, viewport); + } + + @Override + protected void internalRender(final SpriteBatch batch, final BitmapFont baseFont, final GlyphLayout glyphLayout) { + this.iconFrame.render(batch, baseFont, glyphLayout); + } + + @Override + public UIFrame touchDown(final float screenX, final float screenY, final int button) { + if (isVisible() && this.renderBounds.contains(screenX, screenY)) { + return this; + } + return super.touchDown(screenX, screenY, button); + } + + @Override + public UIFrame touchUp(final float screenX, final float screenY, final int button) { + if (isVisible() && this.renderBounds.contains(screenX, screenY)) { + return this; + } + return super.touchUp(screenX, screenY, button); + } + + @Override + public void onClick(final int button) { + this.clickListener.queueIconClicked(this.queueIconIndexId); + } + + @Override + public void setWidth(final float width) { + this.defaultWidth = width; + super.setWidth(width); + } + + @Override + public void setHeight(final float height) { + this.defaultHeight = height; + super.setHeight(height); + } + + @Override + public void mouseDown(final GameUI gameUI, final Viewport uiViewport) { + this.iconFrame.setWidth(this.defaultWidth * 0.95f); + this.iconFrame.setHeight(this.defaultHeight * 0.95f); + positionBounds(gameUI, uiViewport); + } + + @Override + public void mouseUp(final GameUI gameUI, final Viewport uiViewport) { + this.iconFrame.setWidth(this.defaultWidth); + this.iconFrame.setHeight(this.defaultHeight); + positionBounds(gameUI, uiViewport); + } + + @Override + public void mouseEnter(final GameUI gameUI, final Viewport uiViewport) { + } + + @Override + public void mouseExit(final GameUI gameUI, final Viewport uiViewport) { + } + + @Override + public UIFrame getFrameChildUnderMouse(final float screenX, final float screenY) { + if (isVisible() && this.renderBounds.contains(screenX, screenY)) { + return this; + } + return null; + } + + public void setToolTip(final String toolTip) { + this.toolTip = toolTip; + } + + public void setUberTip(final String uberTip) { + this.uberTip = uberTip; + } + + @Override + public String getToolTip() { + return this.toolTip; + } + + @Override + public String getUberTip() { + return this.uberTip; + } + + @Override + public int getToolTipFoodCost() { + return 0; + } + + @Override + public int getToolTipGoldCost() { + return 0; + } + + @Override + public int getToolTipLumberCost() { + return 0; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/ActiveCommand.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/ActiveCommand.java new file mode 100644 index 0000000..269d9bd --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/ActiveCommand.java @@ -0,0 +1,8 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui.command; + +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CSimulation; +import com.etheller.warsmash.viewer5.handlers.w3x.simulation.CUnit; + +public interface ActiveCommand { + boolean finish(CSimulation simulation, CUnit selectedUnit, float mouseScreenX, float mouseScreenY); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/ClickableActionFrame.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/ClickableActionFrame.java new file mode 100644 index 0000000..25f1110 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/ClickableActionFrame.java @@ -0,0 +1,25 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui.command; + +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; + +public interface ClickableActionFrame extends ClickableFrame { + @Override + void mouseDown(final GameUI gameUI, final Viewport uiViewport); + + @Override + void mouseUp(final GameUI gameUI, final Viewport uiViewport); + + @Override + void onClick(int button); + + String getToolTip(); + + String getUberTip(); + + int getToolTipGoldCost(); + + int getToolTipLumberCost(); + + int getToolTipFoodCost(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/ClickableFrame.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/ClickableFrame.java new file mode 100644 index 0000000..15e90e9 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/ClickableFrame.java @@ -0,0 +1,17 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui.command; + +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.frames.UIFrame; + +public interface ClickableFrame extends UIFrame { + void mouseDown(final GameUI gameUI, final Viewport uiViewport); + + void mouseUp(final GameUI gameUI, final Viewport uiViewport); + + void mouseEnter(final GameUI gameUI, final Viewport uiViewport); + + void mouseExit(final GameUI gameUI, final Viewport uiViewport); + + void onClick(int button); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/CommandCardCommandListener.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/CommandCardCommandListener.java new file mode 100644 index 0000000..fe003ab --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/CommandCardCommandListener.java @@ -0,0 +1,7 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui.command; + +public interface CommandCardCommandListener { + void onClick(int abilityHandleId, int orderId, boolean rightClick); + + void openMenu(int orderId); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/CommandErrorListener.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/CommandErrorListener.java new file mode 100644 index 0000000..70fa4f3 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/CommandErrorListener.java @@ -0,0 +1,11 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui.command; + +public interface CommandErrorListener { + void showCommandError(String message); + + void showCantPlaceError(); + + void showNoFoodError(); + + void showInventoryFullError(); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/FocusableFrame.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/FocusableFrame.java new file mode 100644 index 0000000..0300fc3 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/FocusableFrame.java @@ -0,0 +1,17 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui.command; + +import com.etheller.warsmash.parsers.fdf.frames.UIFrame; + +public interface FocusableFrame extends UIFrame { + boolean isFocusable(); + + void onFocusGained(); + + void onFocusLost(); + + boolean keyDown(int keycode); + + boolean keyUp(int keycode); + + boolean keyTyped(char character); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/QueueIconListener.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/QueueIconListener.java new file mode 100644 index 0000000..b6a1537 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/QueueIconListener.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui.command; + +public interface QueueIconListener { + void queueIconClicked(int index); +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/SettableCommandErrorListener.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/SettableCommandErrorListener.java new file mode 100644 index 0000000..7bed78b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/command/SettableCommandErrorListener.java @@ -0,0 +1,29 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui.command; + +public class SettableCommandErrorListener implements CommandErrorListener { + private CommandErrorListener delegate; + + @Override + public void showCommandError(final String message) { + this.delegate.showCommandError(message); + } + + @Override + public void showCantPlaceError() { + this.delegate.showCantPlaceError(); + } + + @Override + public void showNoFoodError() { + this.delegate.showNoFoodError(); + } + + @Override + public void showInventoryFullError() { + this.delegate.showInventoryFullError(); + } + + public void setDelegate(final CommandErrorListener delegate) { + this.delegate = delegate; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/menu/CampaignButtonUI.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/menu/CampaignButtonUI.java new file mode 100644 index 0000000..becd2b1 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/menu/CampaignButtonUI.java @@ -0,0 +1,104 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui.menu; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.frames.AbstractUIFrame; +import com.etheller.warsmash.parsers.fdf.frames.GlueButtonFrame; +import com.etheller.warsmash.parsers.fdf.frames.StringFrame; +import com.etheller.warsmash.parsers.fdf.frames.UIFrame; +import com.etheller.warsmash.viewer5.handlers.w3x.ui.command.ClickableFrame; + +public class CampaignButtonUI extends AbstractUIFrame implements ClickableFrame { + + private GlueButtonFrame buttonArt; + private boolean enabled = true; + private StringFrame headerText; + private StringFrame nameText; + private Color defaultNameColor; + private Color defaultHeaderColor; + private boolean artHighlight; + + public CampaignButtonUI(final String name, final UIFrame parent) { + super(name, parent); + } + + public void setButtonArt(final GlueButtonFrame buttonArt) { + this.buttonArt = buttonArt; + } + + public void setEnabled(final boolean enabled) { + this.enabled = enabled; + this.buttonArt.setEnabled(enabled); + } + + public void setOnClick(final Runnable onClick) { + this.buttonArt.setOnClick(onClick); + } + + @Override + public UIFrame touchUp(final float screenX, final float screenY, final int button) { + if (isVisible() && this.enabled && this.renderBounds.contains(screenX, screenY)) { + return this; + } + return super.touchUp(screenX, screenY, button); + } + + @Override + public UIFrame touchDown(final float screenX, final float screenY, final int button) { + if (isVisible() && this.enabled && this.renderBounds.contains(screenX, screenY)) { + return this; + } + return super.touchDown(screenX, screenY, button); + } + + @Override + public UIFrame getFrameChildUnderMouse(final float screenX, final float screenY) { + if (isVisible() && this.enabled && this.renderBounds.contains(screenX, screenY)) { + final UIFrame childResult = this.buttonArt.getFrameChildUnderMouse(screenX, screenY); + if (childResult != null) { + return childResult; + } + return this; + } + return super.getFrameChildUnderMouse(screenX, screenY); + } + + @Override + public void mouseDown(final GameUI gameUI, final Viewport uiViewport) { + this.buttonArt.mouseDown(gameUI, uiViewport); + } + + @Override + public void mouseUp(final GameUI gameUI, final Viewport uiViewport) { + this.buttonArt.mouseUp(gameUI, uiViewport); + } + + @Override + public void mouseEnter(final GameUI gameUI, final Viewport uiViewport) { + this.headerText.setColor(Color.WHITE); + this.nameText.setColor(Color.WHITE); + } + + @Override + public void mouseExit(final GameUI gameUI, final Viewport uiViewport) { + this.headerText.setColor(this.defaultHeaderColor); + this.nameText.setColor(this.defaultNameColor); + } + + @Override + public void onClick(final int button) { + this.buttonArt.onClick(button); + } + + public void setHeaderText(final StringFrame headerText) { + this.headerText = headerText; + this.defaultHeaderColor = headerText.getColor(); + } + + public void setNameText(final StringFrame nameText) { + this.nameText = nameText; + this.defaultNameColor = nameText.getColor(); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/menu/CampaignMenuData.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/menu/CampaignMenuData.java new file mode 100644 index 0000000..f92d18e --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/menu/CampaignMenuData.java @@ -0,0 +1,118 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui.menu; + +import java.util.ArrayList; +import java.util.List; + +import com.badlogic.gdx.graphics.Color; +import com.etheller.warsmash.units.Element; + +public class CampaignMenuData { + private final String header; + private final String name; + private final boolean defaultOpen; + private final String background; + private final int backgroundFogStyle; + private final Color backgroundFogColor; + private final float backgroundFogDensity; + private final float backgroundFogStart; + private final float backgroundFogEnd; + private final int cursor; + private final String ambientSound; + private final CampaignMission introCinematic; + private final CampaignMission openCinematic; + private final CampaignMission endCinematic; + private final List missions = new ArrayList<>(); + + public CampaignMenuData(final Element element) { + this.header = element.getField("Header"); + this.name = element.getField("Name"); + this.defaultOpen = element.getFieldValue("DefaultOpen") == 1; + this.background = element.getField("Background"); + this.backgroundFogStyle = element.getFieldValue("BackgroundFogStyle"); + this.backgroundFogColor = new Color(element.getFieldFloatValue("BackgroundFogColor", 1) / 255f, + element.getFieldFloatValue("BackgroundFogColor", 2) / 255f, + element.getFieldFloatValue("BackgroundFogColor", 3) / 255f, + element.getFieldFloatValue("BackgroundFogColor", 0) / 255f); + this.backgroundFogDensity = element.getFieldFloatValue("BackgroundFogDensity"); + this.backgroundFogStart = element.getFieldFloatValue("BackgroundFogStart"); + this.backgroundFogEnd = element.getFieldFloatValue("BackgroundFogEnd"); + this.cursor = element.getFieldValue("Cursor"); + this.ambientSound = element.getField("AmbientSound"); + this.introCinematic = readMission(element, "IntroCinematic"); + this.openCinematic = readMission(element, "OpenCinematic"); + this.endCinematic = readMission(element, "EndCinematic"); + int missionIndex = 0; + CampaignMission currentMission; + while ((currentMission = readMission(element, "Mission" + missionIndex)) != null) { + this.missions.add(currentMission); + missionIndex++; + } + } + + public String getHeader() { + return this.header; + } + + public String getName() { + return this.name; + } + + public boolean isDefaultOpen() { + return this.defaultOpen; + } + + public String getBackground() { + return this.background; + } + + public int getBackgroundFogStyle() { + return this.backgroundFogStyle; + } + + public Color getBackgroundFogColor() { + return this.backgroundFogColor; + } + + public float getBackgroundFogDensity() { + return this.backgroundFogDensity; + } + + public float getBackgroundFogStart() { + return this.backgroundFogStart; + } + + public float getBackgroundFogEnd() { + return this.backgroundFogEnd; + } + + public int getCursor() { + return this.cursor; + } + + public String getAmbientSound() { + return this.ambientSound; + } + + public CampaignMission getIntroCinematic() { + return this.introCinematic; + } + + public CampaignMission getOpenCinematic() { + return this.openCinematic; + } + + public CampaignMission getEndCinematic() { + return this.endCinematic; + } + + public List getMissions() { + return this.missions; + } + + private static CampaignMission readMission(final Element element, final String field) { + if ("".equals(element.getField(field, 0))) { + return null; + } + return new CampaignMission(element.getField(field, 0), element.getField(field, 1), element.getField(field, 2)); + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/menu/CampaignMenuUI.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/menu/CampaignMenuUI.java new file mode 100644 index 0000000..8aeee26 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/menu/CampaignMenuUI.java @@ -0,0 +1,81 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui.menu; + +import java.util.ArrayList; +import java.util.List; + +import com.badlogic.gdx.utils.viewport.Viewport; +import com.etheller.warsmash.parsers.fdf.GameUI; +import com.etheller.warsmash.parsers.fdf.datamodel.FramePoint; +import com.etheller.warsmash.parsers.fdf.frames.SetPoint; +import com.etheller.warsmash.parsers.fdf.frames.SimpleFrame; +import com.etheller.warsmash.parsers.fdf.frames.StringFrame; +import com.etheller.warsmash.parsers.fdf.frames.TextButtonFrame; +import com.etheller.warsmash.parsers.fdf.frames.UIFrame; + +public class CampaignMenuUI extends SimpleFrame { + private final GameUI rootFrame; + private final Viewport uiViewport; + private final List buttonUIs = new ArrayList<>(); + + public CampaignMenuUI(final String name, final UIFrame parent, final GameUI rootFrame, final Viewport uiViewport) { + super(name, parent); + this.rootFrame = rootFrame; + this.uiViewport = uiViewport; + } + + public void addButton(final String header, final String name, final Runnable onClick) { + final CampaignButtonUI campaignButtonUI = new CampaignButtonUI(null, this); + final TextButtonFrame campaignArrowButton = (TextButtonFrame) this.rootFrame + .createFrame("CampaignArrowButtonTemplate", campaignButtonUI, 0, 0); + campaignButtonUI.setButtonArt(campaignArrowButton); + campaignButtonUI.add(campaignArrowButton); + campaignArrowButton.addSetPoint(new SetPoint(FramePoint.TOPLEFT, campaignButtonUI, FramePoint.TOPLEFT, 0, 0)); + campaignArrowButton.setOnClick(onClick); + + final StringFrame headerText = (StringFrame) this.rootFrame.createFrame("StandardSmallTextTemplate", + campaignButtonUI, 0, 0); + this.rootFrame.setText(headerText, header); + campaignButtonUI.add(headerText); + final StringFrame nameText = (StringFrame) this.rootFrame.createFrame("StandardValueTextTemplate", + campaignButtonUI, 0, 0); + this.rootFrame.setText(nameText, name); + headerText.addSetPoint(new SetPoint(FramePoint.TOPLEFT, campaignArrowButton, FramePoint.TOPRIGHT, 0, 0)); + nameText.addSetPoint(new SetPoint(FramePoint.TOPLEFT, headerText, FramePoint.BOTTOMLEFT, 0, 0)); + campaignButtonUI.add(nameText); + campaignButtonUI.setHeaderText(headerText); + campaignButtonUI.setNameText(nameText); + campaignButtonUI.setHeight(GameUI.convertY(this.uiViewport, 0.032f)); + + add(campaignButtonUI); + this.buttonUIs.add(campaignButtonUI); + + } + + @Override + protected void innerPositionBounds(final GameUI gameUI, final Viewport viewport) { + super.innerPositionBounds(gameUI, viewport); + final float myHeight = this.renderBounds.height; + final int buttonCount = this.buttonUIs.size(); + final float buttonSpacing = Math.min(myHeight / buttonCount, GameUI.convertY(this.uiViewport, 0.056f)); + final float buttonsHeight = buttonSpacing * buttonCount; + final float expectedHeight = Math.min(buttonsHeight, myHeight); + final float yOffset = (myHeight - expectedHeight) / 2; + + UIFrame lastButton = null; + for (final CampaignButtonUI campaignButtonUI : this.buttonUIs) { + if (lastButton == null) { + campaignButtonUI.addSetPoint(new SetPoint(FramePoint.TOPLEFT, this, FramePoint.TOPLEFT, 0, -yOffset)); + campaignButtonUI.addSetPoint(new SetPoint(FramePoint.TOPRIGHT, this, FramePoint.TOPRIGHT, 0, -yOffset)); + } + else { + campaignButtonUI.addSetPoint( + new SetPoint(FramePoint.TOPLEFT, lastButton, FramePoint.TOPLEFT, 0, -buttonSpacing)); + campaignButtonUI.addSetPoint( + new SetPoint(FramePoint.TOPRIGHT, lastButton, FramePoint.TOPRIGHT, 0, -buttonSpacing)); + } + lastButton = campaignButtonUI; + } + super.innerPositionBounds(gameUI, viewport); + } + +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/menu/CampaignMission.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/menu/CampaignMission.java new file mode 100644 index 0000000..d69f149 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/menu/CampaignMission.java @@ -0,0 +1,25 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui.menu; + +public class CampaignMission { + private final String header; + private final String missionName; + private final String mapFilename; + + public CampaignMission(final String header, final String missionName, final String mapFilename) { + this.header = header; + this.missionName = missionName; + this.mapFilename = mapFilename; + } + + public String getHeader() { + return this.header; + } + + public String getMissionName() { + return this.missionName; + } + + public String getMapFilename() { + return this.mapFilename; + } +} diff --git a/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/sound/KeyedSounds.java b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/sound/KeyedSounds.java new file mode 100644 index 0000000..ee8fab3 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer5/handlers/w3x/ui/sound/KeyedSounds.java @@ -0,0 +1,29 @@ +package com.etheller.warsmash.viewer5.handlers.w3x.ui.sound; + +import java.util.HashMap; +import java.util.Map; + +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.viewer5.handlers.w3x.UnitSound; + +public class KeyedSounds { + private final DataTable uiSoundsTable; + private final DataSource dataSource; + private final Map keyToSound; + + public KeyedSounds(final DataTable uiSoundsTable, final DataSource dataSource) { + this.uiSoundsTable = uiSoundsTable; + this.dataSource = dataSource; + this.keyToSound = new HashMap<>(); + } + + public UnitSound getSound(final String key) { + UnitSound sound = this.keyToSound.get(key); + if (sound == null) { + sound = UnitSound.create(this.dataSource, this.uiSoundsTable, key, ""); + this.keyToSound.put(key, sound); + } + return sound; + } +} diff --git a/core/src/com/hiveworkshop/ReteraCASCUtils.java b/core/src/com/hiveworkshop/ReteraCASCUtils.java new file mode 100644 index 0000000..8599c37 --- /dev/null +++ b/core/src/com/hiveworkshop/ReteraCASCUtils.java @@ -0,0 +1,53 @@ +package com.hiveworkshop; + +public class ReteraCASCUtils { + + public static boolean arraysEquals(final byte[] a, final int aFromIndex, final int aToIndex, final byte[] b, + final int bFromIndex, final int bToIndex) { + if (a == null) { + if (b == null) { + return true; + } else { + return false; + } + } + if (b == null) { + return false; + } + if ((aToIndex - aFromIndex) != (bToIndex - bFromIndex)) { + return false; + } + int j = bFromIndex; + for (int i = aFromIndex; i < aToIndex; i++) { + if (a[i] != b[j++]) { + return false; + } + } + return true; + } + + public static int arraysCompareUnsigned(final byte[] a, final int aFromIndex, final int aToIndex, final byte[] b, + final int bFromIndex, final int bToIndex) { + final int i = arraysMismatch(a, aFromIndex, aToIndex, b, bFromIndex, bToIndex); + if ((i >= 0) && (i < Math.min(aToIndex - aFromIndex, bToIndex - bFromIndex))) { + return byteCompareUnsigned(a[aFromIndex + i], b[bFromIndex + i]); + } + return (aToIndex - aFromIndex) - (bToIndex - bFromIndex); + } + + private static int byteCompareUnsigned(final byte b, final byte c) { + return Integer.compare(b & 0xFF, c & 0xFF); + } + + private static int arraysMismatch(final byte[] a, final int aFromIndex, final int aToIndex, final byte[] b, + final int bFromIndex, final int bToIndex) { + final int aLength = aToIndex - aFromIndex; + final int bLength = bToIndex - bFromIndex; + for (int i = 0; (i < aLength) && (i < bLength); i++) { + if (a[aFromIndex + i] != b[bFromIndex + i]) { + return i; + } + } + return Math.min(aLength, bLength); + } +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/ConfigurationFile.java b/core/src/com/hiveworkshop/blizzard/casc/ConfigurationFile.java new file mode 100644 index 0000000..a5f56df --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/ConfigurationFile.java @@ -0,0 +1,118 @@ +package com.hiveworkshop.blizzard.casc; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +import com.hiveworkshop.blizzard.casc.nio.MalformedCASCStructureException; +import com.hiveworkshop.nio.ByteBufferInputStream; + +/** + * File containing CASC configuration information. This is basically a + * collection of keys with their assigned value. What the values mean depends on + * the purpose of the key. + */ +public class ConfigurationFile { + /** + * The name of the data folder containing the configuration files. + */ + public static final String CONFIGURATION_FOLDER_NAME = "config"; + + /** + * Character encoding used by configuration files. + */ + public static final Charset FILE_ENCODING = Charset.forName("UTF8"); + + /** + * Length of the configuration bucket folder names. + */ + public static final int BUCKET_NAME_LENGTH = 2; + + /** + * Number of configuration bucket folder tiers. + */ + public static final int BUCKET_TIERS = 2; + + /** + * Retrieve a configuration file from the data folder by its key. + * + * @param dataFolder Path of the CASC data folder. + * @param keyHex Key for configuration file as a hexadecimal string. + * @return The requested configuration file. + * @throws IOException If an exception occurs when retrieving the file. + */ + public static ConfigurationFile lookupConfigurationFile(final Path dataFolder, final String keyHex) + throws IOException { + Path file = dataFolder.resolve(CONFIGURATION_FOLDER_NAME); + for (int tier = 0; tier < BUCKET_TIERS; tier += 1) { + final int keyOffset = tier * BUCKET_NAME_LENGTH; + final String bucketFolderName = keyHex.substring(keyOffset, keyOffset + BUCKET_NAME_LENGTH); + file = file.resolve(bucketFolderName); + } + + file = file.resolve(keyHex); + + final ByteBuffer fileBuffer = ByteBuffer.wrap(Files.readAllBytes(file)); + + return new ConfigurationFile(fileBuffer); + } + + /** + * Underlying map holding the configuration data. + */ + private final Map configuration = new HashMap<>(); + + /** + * Construct a configuration file by decoding a file buffer. + * + * @param fileBuffer File buffer to decode from. + * @throws IOException If one or more IO errors occur. + */ + public ConfigurationFile(final ByteBuffer fileBuffer) throws IOException { + try (final ByteBufferInputStream fileStream = new ByteBufferInputStream(fileBuffer); + final Scanner lineScanner = new Scanner(new InputStreamReader(fileStream, FILE_ENCODING))) { + while (lineScanner.hasNextLine()) { + final String line = lineScanner.nextLine().trim(); + final int lineLength = line.indexOf('#'); + final String record; + if (lineLength != -1) { + record = line.substring(0, lineLength); + } else { + record = line; + } + + if (!record.equals("")) { + final int assignmentIndex = record.indexOf('='); + if (assignmentIndex == -1) { + throw new MalformedCASCStructureException( + "configuration file line contains record with no assignment"); + } + + final String key = record.substring(0, assignmentIndex).trim(); + final String value = record.substring(assignmentIndex + 1).trim(); + + if (configuration.putIfAbsent(key, value) != null) { + throw new MalformedCASCStructureException( + "configuration file contains duplicate key declarations"); + } + } + } + } + } + + /** + * Get the configuration defined by the file. + * + * @return Configuration map. + */ + public Map getConfiguration() { + return configuration; + } + +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/Key.java b/core/src/com/hiveworkshop/blizzard/casc/Key.java new file mode 100644 index 0000000..1a3f00c --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/Key.java @@ -0,0 +1,78 @@ +package com.hiveworkshop.blizzard.casc; + +import com.hiveworkshop.ReteraCASCUtils; +import com.hiveworkshop.lang.Hex; + +/** + * Class representing a CASC related key such as an encoding key. + *

+ * When testing equality and comparing the length of the shortest key is used. + */ +public final class Key implements Comparable { + /** + * Key array. + */ + private final byte[] key; + + /** + * Wraps a byte array into a key. The array is used directly so must not be + * modified. + * + * @param key Key array. + */ + public Key(final byte[] key) { + this.key = key; + } + + /** + * Constructs a key from a hexadecimal key string. Bytes are order in the order + * they appear in the string, which can be considered big endian. + * + * @param keyString hexadecimal key form of key. + */ + public Key(final CharSequence key) { + this.key = Hex.decodeHex(key); + } + + @Override + public int compareTo(final Key o) { + final int commonLength = Math.min(key.length, o.key.length); + return ReteraCASCUtils.arraysCompareUnsigned(key, 0, commonLength, o.key, 0, commonLength); + } + + @Override + public boolean equals(final Object obj) { + if ((obj == null) || !(obj instanceof Key)) { + return false; + } + + final Key otherKey = (Key) obj; + final int commonLength = Math.min(key.length, otherKey.key.length); + if (!ReteraCASCUtils.arraysEquals(key, 0, commonLength, otherKey.key, 0, commonLength)) { + return false; + } + + return true; + } + + /** + * Return the wrapped key array for low level interaction. + * + * @return Key array. + */ + public byte[] getKey() { + return key.clone(); + } + + @Override + public int hashCode() { + throw new UnsupportedOperationException("key hash code not safe to use due to variable sizes between systems"); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder((key.length + 1)); + Hex.stringBufferAppendHex(builder, key); + return builder.toString(); + } +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/StorageReference.java b/core/src/com/hiveworkshop/blizzard/casc/StorageReference.java new file mode 100644 index 0000000..e0c76e6 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/StorageReference.java @@ -0,0 +1,80 @@ +package com.hiveworkshop.blizzard.casc; + +import java.util.Map; + +/** + * A reference to a file extracted from a configuration file. + */ +public class StorageReference { + /** + * Suffix for sizes mapping entry in configuration files. + */ + private static final String SIZES_SUFFIX = "-size"; + + private final long storedSize; + private final long size; + private final Key encodingKey; + private final Key contentKey; + + /** + * Decodes a storage reference from a configuration file. + * + * @param name Name of reference. + * @param configuration Map of configuration file content. + */ + public StorageReference(final String name, final Map configuration) { + final String keys = configuration.get(name); + if (keys == null) { + throw new IllegalArgumentException("name does not exist in configuration"); + } + final String sizes = configuration.get(name + SIZES_SUFFIX); + if (sizes == null) { + throw new IllegalArgumentException("size missing in configuration"); + } + + final String[] keyStrings = keys.split(" "); + contentKey = new Key(keyStrings[0]); + encodingKey = new Key(keyStrings[1]); + + final String[] sizeStrings = sizes.split(" "); + size = Long.parseLong(sizeStrings[0]); + storedSize = Long.parseLong(sizeStrings[1]); + } + + /** + * Content key? + * + * @return Content key. + */ + public Key getContentKey() { + return contentKey; + } + + /** + * Encoding key used to lookup the file from CASC storage. + * + * @return Encoding key. + */ + public Key getEncodingKey() { + return encodingKey; + } + + /** + * File size. + * + * @return File size in bytes of the file. + */ + public long getSize() { + return size; + } + + /** + * Size of file content in CASC storage. + * + * @return Approximate byte usage of file in CASC storage. + */ + public long getStoredSize() { + return storedSize; + } + +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/info/FieldDataType.java b/core/src/com/hiveworkshop/blizzard/casc/info/FieldDataType.java new file mode 100644 index 0000000..bd5cb54 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/info/FieldDataType.java @@ -0,0 +1,25 @@ +package com.hiveworkshop.blizzard.casc.info; + +/** + * Field data types to help with decoding values. + */ +public enum FieldDataType { + /** + * Field contains textual data. Size ignored. + */ + STRING, + /** + * Field is a decimal number. Size determines number of bytes used to represent + * it. + */ + DEC, + /** + * Field is a hexadecimal string. Size is number of bytes used to represent it + * with every 2 characters representing 1 byte. + */ + HEX, + /** + * This field type is currently not supported. + */ + UNSUPPORTED +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/info/FieldDescriptor.java b/core/src/com/hiveworkshop/blizzard/casc/info/FieldDescriptor.java new file mode 100644 index 0000000..599e20f --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/info/FieldDescriptor.java @@ -0,0 +1,65 @@ +package com.hiveworkshop.blizzard.casc.info; + +public class FieldDescriptor { + private static final int NAME_TERMINATOR = '!'; + private static final int DATA_TYPE_TERMINATOR = ':'; + + private final String name; + private FieldDataType dataType; + private final int size; + + /** + * Constructs a field descriptor from a field declaration string. + * + * @param encoded Field declaration string. + */ + public FieldDescriptor(final String encoded) { + final int nameEnd = encoded.indexOf(NAME_TERMINATOR); + if (nameEnd == -1) { + throw new IllegalArgumentException("missing name terminator"); + } + final int dataTypeEnd = encoded.indexOf(DATA_TYPE_TERMINATOR, nameEnd + 1); + if (dataTypeEnd == -1) { + throw new IllegalArgumentException("missing data type terminator"); + } + + name = encoded.substring(0, nameEnd); + + try { + dataType = FieldDataType.valueOf(encoded.substring(nameEnd + 1, dataTypeEnd)); + } catch (final IllegalArgumentException e) { + dataType = FieldDataType.UNSUPPORTED; + } + + size = Integer.parseInt(encoded.substring(dataTypeEnd + 1)); + } + + /** + * Get the field name. + * + * @return Name of the field. + */ + public String getName() { + return name; + } + + /** + * Get the field data type. + * + * @return Field data type. + */ + public FieldDataType getDataType() { + return dataType; + } + + /** + * Get the field size. Field size is the number of bytes required to represent + * the field in native form. A value of 0 means the field is variable length. + * + * @return Field size in bytes. + */ + public int getSize() { + return size; + } + +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/info/Info.java b/core/src/com/hiveworkshop/blizzard/casc/info/Info.java new file mode 100644 index 0000000..24bdaab --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/info/Info.java @@ -0,0 +1,149 @@ +package com.hiveworkshop.blizzard.casc.info; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.Scanner; + +import com.hiveworkshop.blizzard.casc.nio.MalformedCASCStructureException; +import com.hiveworkshop.nio.ByteBufferInputStream; + +/** + * Top level CASC information file containing configuration information and + * entry point references. + */ +public class Info { + /** + * Name of the CASC build info file located in the install root (parent of the + * data folder). + */ + public static final String BUILD_INFO_FILE_NAME = ".build.info"; + + /** + * Character encoding used by info files. + */ + public static final Charset FILE_ENCODING = Charset.forName("UTF8"); + + /** + * Field separator used by CASC info files. + */ + private static final String FIELD_SEPARATOR_REGEX = "\\|"; + + /** + * Helper method to separate a single line of info file into separate field + * strings. + * + * @param encodedLine Line of info file. + * @return Array of separate fields. + */ + private static String[] separateFields(final String encodedLine) { + return encodedLine.split(FIELD_SEPARATOR_REGEX); + } + + private final ArrayList fieldDescriptors = new ArrayList<>(); + + private final ArrayList> records = new ArrayList<>(); + + /** + * Construct an info file from an array of encoded lines. + * + * @param encodedLines Encoded lines. + * @throws IOException + */ + public Info(final ByteBuffer fileBuffer) throws IOException { + try (final ByteBufferInputStream fileStream = new ByteBufferInputStream(fileBuffer); + final Scanner lineScanner = new Scanner(new InputStreamReader(fileStream, FILE_ENCODING))) { + final String[] encodedFieldDescriptors = separateFields(lineScanner.nextLine()); + for (final String encodedFieldDescriptor : encodedFieldDescriptors) { + fieldDescriptors.add(new FieldDescriptor(encodedFieldDescriptor)); + } + + while (lineScanner.hasNextLine()) { + records.add(new ArrayList<>(Arrays.asList(separateFields(lineScanner.nextLine())))); + } + } catch (final NoSuchElementException e) { + throw new MalformedCASCStructureException("missing headers"); + } + } + + /** + * Retrieves a specific field of a record. + * + * @param recordIndex Record index to lookup. + * @param fieldIndex Field index to retrieve of record. + * @return Field value. + * @throws IndexOutOfBoundsException When recordIndex or fieldIndex are out of + * bounds. + */ + public String getField(final int recordIndex, final int fieldIndex) { + return records.get(recordIndex).get(fieldIndex); + } + + /** + * Retrieves a specific field of a record. + * + * @param recordIndex Record index to lookup. + * @param fieldName Field name to retrieve of record. + * @return Field value, or null if field does not exist. + * @throws IndexOutOfBoundsException When recordIndex is out of bounds. + */ + public String getField(final int recordIndex, final String fieldName) { + // resolve field + final int fieldIndex = getFieldIndex(fieldName); + if (fieldIndex == -1) { + // field does not exist + return null; + } + + return getField(recordIndex, fieldIndex); + } + + /** + * Get the number of fields that make up each record. + * + * @return Field count. + */ + public int getFieldCount() { + return fieldDescriptors.size(); + } + + /** + * Retrieve the field descriptor of a field index. + * + * @param fieldIndex Field index to retrieve descriptor from. + * @return Field descriptor for field index. + */ + public FieldDescriptor getFieldDescriptor(final int fieldIndex) { + return fieldDescriptors.get(fieldIndex); + } + + /** + * Lookup the index of a named field. Returns the field index for the field name + * if found, otherwise returns -1. + * + * @param name Name of the field to find. + * @return Field index of field. + */ + public int getFieldIndex(final String name) { + for (int i = 0; i < fieldDescriptors.size(); i += 1) { + if (fieldDescriptors.get(i).getName().equals(name)) { + return i; + } + } + + return -1; + } + + /** + * Get the number of records in this file. + * + * @return Record count. + */ + public int getRecordCount() { + return records.size(); + } +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/io/WarcraftIIICASC.java b/core/src/com/hiveworkshop/blizzard/casc/io/WarcraftIIICASC.java new file mode 100644 index 0000000..2dbd8a5 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/io/WarcraftIIICASC.java @@ -0,0 +1,291 @@ +package com.hiveworkshop.blizzard.casc.io; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import com.hiveworkshop.blizzard.casc.ConfigurationFile; +import com.hiveworkshop.blizzard.casc.info.Info; +import com.hiveworkshop.blizzard.casc.nio.MalformedCASCStructureException; +import com.hiveworkshop.blizzard.casc.storage.Storage; +import com.hiveworkshop.blizzard.casc.vfs.VirtualFileSystem; +import com.hiveworkshop.blizzard.casc.vfs.VirtualFileSystem.PathResult; + +/** + * A convenient access to locally stored Warcraft III data files. Intended for + * use with CASC versions of Warcraft III including classic and Reforged. + */ +public class WarcraftIIICASC implements AutoCloseable { + /** + * File system view for accessing files from file paths. + */ + public class FileSystem { + /** + * Private constructor, currently not used. + */ + private FileSystem() { + + } + + /** + * Enumerate all file paths contained in this file system. + *

+ * This operation might be quite slow. + * + * @return A list containing all file paths contained in this file system. + * @throws IOException In an exception occurs when resolving files. + */ + public List enumerateFiles() throws IOException { + final List pathResults = vfs.getAllFiles(); + final ArrayList filePathStrings = new ArrayList(pathResults.size()); + + for (final PathResult pathResult : pathResults) { + filePathStrings.add(pathResult.getPath()); + } + + return filePathStrings; + } + + /** + * Test if the specified file path is a file. + * + * @param filePath Path of file to test. + * @return True if path represents a file, otherwise false. + * @throws IOException In an exception occurs when resolving files. + */ + public boolean isFile(final String filePath) throws IOException { + final byte[][] pathFragments = VirtualFileSystem.convertFilePath(filePath); + try { + final PathResult resolveResult = vfs.resolvePath(pathFragments); + return resolveResult.isFile(); + } catch (final FileNotFoundException e) { + return false; + } + } + + /** + * Test if the specified file path is available from local storage. + * + * @param filePath Path of file to test. + * @return True if path represents a file inside local storage, otherwise false. + * @throws IOException In an exception occurs when resolving files. + */ + public boolean isFileAvailable(final String filePath) throws IOException { + final byte[][] pathFragments = VirtualFileSystem.convertFilePath(filePath); + final PathResult resolveResult = vfs.resolvePath(pathFragments); + return resolveResult.existsInStorage(); + } + + /** + * Test if the specified file path is a nested file system. + *

+ * If true a file system can be resolved from the file path which files can be + * resolved from more efficiently than from higher up file systems. + *

+ * Support for this feature is not yet implemented. Please resolve everything + * from the root. + * + * @param filePath Path of file to test. + * @return True if file is a nested file system, otherwise false. + * @throws IOException In an exception occurs when resolving files. + */ + public boolean isNestedFileSystem(final String filePath) throws IOException { + final byte[][] pathFragments = VirtualFileSystem.convertFilePath(filePath); + try { + final PathResult resolveResult = vfs.resolvePath(pathFragments); + return resolveResult.isTVFS(); + } catch (final FileNotFoundException e) { + return false; + } + } + + /** + * Fully read the file at the specified file path into memory. + * + * @param filePath File path of file to read. + * @return Buffer containing file data. + * @throws IOException If an error occurs when reading the file. + */ + public ByteBuffer readFileData(final String filePath) throws IOException { + final byte[][] pathFragments = VirtualFileSystem.convertFilePath(filePath); + final PathResult resolveResult = vfs.resolvePath(pathFragments); + + if (!resolveResult.isFile()) { + throw new FileNotFoundException("the specified file path does not resolve to a file"); + } else if (!resolveResult.existsInStorage()) { + throw new FileNotFoundException("the specified file is not in local storage"); + } + + final ByteBuffer fileBuffer = resolveResult.readFile(null); + fileBuffer.flip(); + return fileBuffer; + } + } + + /** + * Name of the CASC data folder used by Warcraft III. + */ + private static final String WC3_DATA_FOLDER_NAME = "Data"; + + /** + * Warcraft III build information. + */ + private final Info buildInfo; + + /** + * Detected active build information record. + */ + private final int activeInfoRecord; + + /** + * Warcraft III build configuration. + */ + private final ConfigurationFile buildConfiguration; + + /** + * Warcraft III CASC data folder path. + */ + private final Path dataPath; + + /** + * Warcraft III local storage. + */ + private final Storage localStorage; + + /** + * TVFS file system to resolve file paths. + */ + private final VirtualFileSystem vfs; + + /** + * Construct an interface to the CASC local storage used by Warcraft III. Can be + * used to read data files from the local storage. + *

+ * The active build record is used for local storage details. + *

+ * Install folder is the Warcraft III installation folder where the + * .build.info file is located. For example + * C:\Program Files (x86)\Warcraft III. + *

+ * Memory mapped IO can be used instead of conventional channel based IO. This + * should improve IO performance considerably by avoiding excessive memory copy + * operations and system calls. However it may place considerable strain on the + * Java VM application virtual memory address space. As such memory mapping + * should only be used with large address aware VMs. + * + * @param installFolder Warcraft III installation folder. + * @param useMemoryMapping If memory mapped IO should be used to read file data. + * @throws IOException If an exception occurs while mounting. + */ + public WarcraftIIICASC(final Path installFolder, final boolean useMemoryMapping) throws IOException { + final Path infoFilePath = installFolder.resolve(Info.BUILD_INFO_FILE_NAME); + buildInfo = new Info(ByteBuffer.wrap(Files.readAllBytes(infoFilePath))); + + final int recordCount = buildInfo.getRecordCount(); + if (recordCount < 1) { + throw new MalformedCASCStructureException("build info contains no records"); + } + + // resolve the active record + final int activeFiledIndex = buildInfo.getFieldIndex("Active"); + if (activeFiledIndex == -1) { + throw new MalformedCASCStructureException("build info contains no active field"); + } + int recordIndex = 0; + for (; recordIndex < recordCount; recordIndex += 1) { + if (Integer.parseInt(buildInfo.getField(recordIndex, activeFiledIndex)) == 1) { + break; + } + } + if (recordIndex == recordCount) { + throw new MalformedCASCStructureException("build info contains no active record"); + } + activeInfoRecord = recordIndex; + + // resolve build configuration file + final int buildKeyFieldIndex = buildInfo.getFieldIndex("Build Key"); + if (buildKeyFieldIndex == -1) { + throw new MalformedCASCStructureException("build info contains no build key field"); + } + final String buildKey = buildInfo.getField(activeInfoRecord, buildKeyFieldIndex); + + // resolve data folder + dataPath = installFolder.resolve(WC3_DATA_FOLDER_NAME); + if (!Files.isDirectory(dataPath)) { + throw new MalformedCASCStructureException("data folder is missing"); + } + + // resolve build configuration file + buildConfiguration = ConfigurationFile.lookupConfigurationFile(dataPath, buildKey); + + // mounting local storage + localStorage = new Storage(dataPath, false, useMemoryMapping); + + // mounting virtual file system + VirtualFileSystem vfs = null; + try { + vfs = new VirtualFileSystem(localStorage, buildConfiguration.getConfiguration()); + } finally { + if (vfs == null) { + // storage must be closed to prevent resource leaks + localStorage.close(); + } + } + this.vfs = vfs; + } + + @Override + public void close() throws IOException { + localStorage.close(); + } + + /** + * Returns the active record index of the build information. This is the index + * of the record that is mounted. + * + * @return Active record index of build information. + */ + public int getActiveRecordIndex() { + return activeInfoRecord; + } + + /** + * Returns the active branch name which is currently mounted. + *

+ * This might reflect the locale that has been cached to local storage. + * + * @return Branch name. + * @throws IOException If no branch information is available. + */ + public String getBranch() throws IOException { + // resolve branch + final int branchFieldIndex = buildInfo.getFieldIndex("Branch"); + if (branchFieldIndex == -1) { + throw new MalformedCASCStructureException("build info contains no branch field"); + } + return buildInfo.getField(activeInfoRecord, branchFieldIndex); + } + + /** + * Returns the build information of the archive. + * + * @return Build information. + */ + public Info getBuildInfo() { + return buildInfo; + } + + /** + * Get the root file system of Warcraft III. From this all locally stored data + * files can be accessed. + * + * @return Root file system containing all files. + */ + public FileSystem getRootFileSystem() { + return new FileSystem(); + } +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/io/package-info.java b/core/src/com/hiveworkshop/blizzard/casc/io/package-info.java new file mode 100644 index 0000000..bbc9c07 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/io/package-info.java @@ -0,0 +1,6 @@ +/** + * High level APIs for CASC file system interaction. + *

+ * These are intended for ease of use, dealing away with many of the low level details required to work with CASC. + */ +package com.hiveworkshop.blizzard.casc.io; \ No newline at end of file diff --git a/core/src/com/hiveworkshop/blizzard/casc/nio/HashMismatchException.java b/core/src/com/hiveworkshop/blizzard/casc/nio/HashMismatchException.java new file mode 100644 index 0000000..dbba7c3 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/nio/HashMismatchException.java @@ -0,0 +1,15 @@ +package com.hiveworkshop.blizzard.casc.nio; + +import java.io.IOException; + +public class HashMismatchException extends IOException { + private static final long serialVersionUID = -7133950344327038673L; + + public HashMismatchException() { + } + + public HashMismatchException(String message) { + super(message); + } + +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/nio/LittleHashBlockProcessor.java b/core/src/com/hiveworkshop/blizzard/casc/nio/LittleHashBlockProcessor.java new file mode 100644 index 0000000..fbbf295 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/nio/LittleHashBlockProcessor.java @@ -0,0 +1,76 @@ +package com.hiveworkshop.blizzard.casc.nio; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class LittleHashBlockProcessor { + + /** + * + * @param encoded + * @return The size of the block + * @throws MalformedCASCStructureException If file is malformed. + */ + public int processBlock(final ByteBuffer encoded) throws MalformedCASCStructureException { + encoded.order(ByteOrder.LITTLE_ENDIAN); + final int length; + final int expectedHash; + try { + length = encoded.getInt(); + expectedHash = encoded.getInt(); + } catch (final BufferUnderflowException e) { + throw new MalformedCASCStructureException("little hash block header out of bounds"); + } + + final int actualHash = expectedHash; // TODO generate actual hash + + if (actualHash != expectedHash) { + return -length; + } + + return length; + } + + /** + * Get a little hash guarded block from the source buffer. + * + * @param sourceBuffer Buffer to retrieve block from. + * @return Guarded block. + * @throws MalformedCASCStructureException If the file is malformed. + * @throws HashMismatchException If the block is corrupt. + */ + public ByteBuffer getBlock(final ByteBuffer sourceBuffer) throws IOException { + final ByteBuffer workingBuffer = sourceBuffer.slice(); + + workingBuffer.order(ByteOrder.LITTLE_ENDIAN); + final int length; + final int expectedHash; + try { + length = workingBuffer.getInt(); + expectedHash = workingBuffer.getInt(); + } catch (final BufferUnderflowException e) { + throw new MalformedCASCStructureException("little hash block header out of bounds"); + } + + if (workingBuffer.remaining() < length) { + throw new MalformedCASCStructureException("little hash block out of bounds"); + } + + workingBuffer.limit(workingBuffer.position() + length); + final ByteBuffer blockBuffer = workingBuffer.slice(); + workingBuffer.position(workingBuffer.limit()); + workingBuffer.limit(workingBuffer.capacity()); + + final int actualHash = expectedHash; // TODO generate actual hash + + if (actualHash != expectedHash) { + throw new HashMismatchException("little hash block"); + } + + sourceBuffer.position(sourceBuffer.position() + workingBuffer.position()); + + return blockBuffer; + } +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/nio/MalformedCASCStructureException.java b/core/src/com/hiveworkshop/blizzard/casc/nio/MalformedCASCStructureException.java new file mode 100644 index 0000000..0e0db77 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/nio/MalformedCASCStructureException.java @@ -0,0 +1,15 @@ +package com.hiveworkshop.blizzard.casc.nio; + +import java.io.IOException; + +public class MalformedCASCStructureException extends IOException { + private static final long serialVersionUID = -5323382445554597608L; + + public MalformedCASCStructureException(String message) { + super(message); + } + + public MalformedCASCStructureException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/storage/BLTEContent.java b/core/src/com/hiveworkshop/blizzard/casc/storage/BLTEContent.java new file mode 100644 index 0000000..e2d8426 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/storage/BLTEContent.java @@ -0,0 +1,132 @@ +package com.hiveworkshop.blizzard.casc.storage; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import com.hiveworkshop.blizzard.casc.nio.MalformedCASCStructureException; +import com.hiveworkshop.lang.Hex; + +/** + * BLTE content entry, used to decode BLTE file data that follows it. + */ +public class BLTEContent { + /** + * BLTE content identifier. + */ + private static final ByteBuffer IDENTIFIER = ByteBuffer.wrap(new byte[] { 'B', 'L', 'T', 'E' }); + + /** + * Hash length in bytes. Should be fetched from appropriate digest length. + */ + private static final int HASH_LENGTH = 16; + + private final long compressedSize; + private final long decompressedSize; + private final byte[] hash = new byte[HASH_LENGTH]; + + public BLTEContent(final ByteBuffer blteBuffer) { + compressedSize = Integer.toUnsignedLong(blteBuffer.getInt()); + decompressedSize = Integer.toUnsignedLong(blteBuffer.getInt()); + blteBuffer.get(hash); + } + + public static BLTEContent[] decodeContent(final ByteBuffer storageBuffer) throws IOException { + final ByteBuffer contentBuffer = storageBuffer.slice(); + + // check identifier + + if ((contentBuffer.remaining() < IDENTIFIER.remaining()) + || !contentBuffer.limit(IDENTIFIER.remaining()).equals(IDENTIFIER)) { + throw new MalformedCASCStructureException("missing BLTE identifier"); + } + + // decode header + + contentBuffer.limit(contentBuffer.capacity()); + contentBuffer.position(contentBuffer.position() + IDENTIFIER.remaining()); + contentBuffer.order(ByteOrder.BIG_ENDIAN); + + final long headerSize; + try { + headerSize = Integer.toUnsignedLong(contentBuffer.getInt()); + } catch (final BufferUnderflowException e) { + throw new MalformedCASCStructureException("header preamble goes out of bounds"); + } + + if (headerSize == 0L) { + storageBuffer.position(storageBuffer.position() + contentBuffer.position()); + return new BLTEContent[0]; + } else if (headerSize > contentBuffer.capacity()) { + throw new MalformedCASCStructureException("BLTE header extends beyond storage buffer bounds"); + } + + contentBuffer.limit((int) headerSize); + final ByteBuffer blteBuffer = contentBuffer.slice(); + blteBuffer.order(ByteOrder.BIG_ENDIAN); + contentBuffer.position(contentBuffer.limit()); + contentBuffer.limit(contentBuffer.capacity()); + + final byte flags; + final int entryCount; + try { + flags = blteBuffer.get(); + if (flags != 0xF) { + throw new MalformedCASCStructureException("unknown flags"); + } + // BE24 read + final int be24Bytes = 3; + final ByteBuffer be24Buffer = ByteBuffer.allocate(Integer.BYTES); + be24Buffer.order(ByteOrder.BIG_ENDIAN); + blteBuffer.get(be24Buffer.array(), Integer.BYTES - be24Bytes, be24Bytes); + entryCount = be24Buffer.getInt(0); + if (entryCount == 0) { + throw new MalformedCASCStructureException("explicit zero entry count"); + } + } catch (final BufferUnderflowException e) { + throw new MalformedCASCStructureException("header goes out of bounds"); + } + + final BLTEContent[] content = new BLTEContent[entryCount]; + + for (int index = 0; index < content.length; index += 1) { + content[index] = new BLTEContent(blteBuffer); + } + + if (blteBuffer.hasRemaining()) { + throw new MalformedCASCStructureException("unprocessed BLTE bytes"); + } + + storageBuffer.position(storageBuffer.position() + contentBuffer.position()); + + return content; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("BLTEChunk{compressedSize="); + builder.append(compressedSize); + builder.append(", decompressedSize="); + builder.append(decompressedSize); + builder.append(", hash="); + Hex.stringBufferAppendHex(builder, hash); + builder.append("}"); + + return builder.toString(); + } + + public long getCompressedSize() { + return compressedSize; + } + + public long getDecompressedSize() { + return decompressedSize; + } + + public byte[] getHash() { + return hash.clone(); + } + +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/storage/BankStream.java b/core/src/com/hiveworkshop/blizzard/casc/storage/BankStream.java new file mode 100644 index 0000000..c7b67f8 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/storage/BankStream.java @@ -0,0 +1,185 @@ +package com.hiveworkshop.blizzard.casc.storage; + +import java.io.EOFException; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +import com.hiveworkshop.blizzard.casc.Key; +import com.hiveworkshop.blizzard.casc.nio.MalformedCASCStructureException; + +/** + * Allows high level access to stored file data banks. These data banks can be + * assembled using higher level logic into a continuous file. + */ +public class BankStream { + private final StorageContainer container; + private final BLTEContent[] content; + private final ByteBuffer streamBuffer; + private int bank = 0; + private boolean hasBanks; + + /** + * Constructs a bank steam from the given buffer. An optional key can be used to + * verify the right file is being processed. If a key is provided it is assumed + * the remaining size of the buffer exactly matches the container size. + * + * @param storageBuffer Storage buffer, as specified by an index file. + * @param key File encoding key to check contents with, or null if no + * such check is required. + * @throws IOException If an exception occurs during decoding of the + * storageBuffer. + */ + public BankStream(final ByteBuffer storageBuffer, final Key encodingKey) throws IOException { + ByteBuffer streamBuffer = storageBuffer.slice(); + container = new StorageContainer(streamBuffer); + if ((encodingKey != null) && !container.getKey().equals(encodingKey)) { + throw new MalformedCASCStructureException("container encoding key mismatch"); + } + + final int storageSize = (int) container.getSize(); + final int storageSizeDiff = Integer.compare(streamBuffer.capacity(), storageSize); + + if (storageSizeDiff < 0) { + throw new MalformedCASCStructureException("container buffer smaller than container"); + } else if ((encodingKey != null) && (storageSizeDiff != 0)) { + throw new MalformedCASCStructureException("container buffer size mismatch"); + } else if (storageSizeDiff > 0) { + // resize buffer to match file + final int streamPos = streamBuffer.position(); + streamBuffer.limit(storageSize); + streamBuffer.position(0); + streamBuffer = streamBuffer.slice(); + streamBuffer.position(streamPos); + } + + if (streamBuffer.hasRemaining()) { + content = BLTEContent.decodeContent(streamBuffer); + hasBanks = true; + } else { + content = null; + hasBanks = false; + } + + this.streamBuffer = streamBuffer; + storageBuffer.position(storageBuffer.position() + streamBuffer.capacity()); + } + + /** + * Get the length of the next bank in bytes. + * + * @return Length of bank in bytes. + * @throws EOFException If there are no more banks in this stream. + */ + public long getNextBankLength() throws EOFException { + if (!hasNextBank()) { + throw new EOFException("no more banks to decode"); + } + + return content.length != 0 ? content[bank].getDecompressedSize() : streamBuffer.remaining(); + } + + /** + * Decode a bank from the stream. The bank buffer must be large enough to + * receive the bank data as specified by getNextBankLength. A null buffer will + * automatically allocate one large enough. The position of the bank buffer will + * be advanced as appropriate, potentially allowing for many banks to be fetched + * in sequence. + * + * @param bankBuffer Buffer to receive bank data. + * @return If null then a new suitable buffer, otherwise bankBuffer. + * @throws IOException If something goes wrong during bank extraction. + * @throws EOFException If there are no more banks in this stream. + */ + public ByteBuffer getBank(ByteBuffer bankBuffer) throws IOException { + if (!hasNextBank()) { + throw new EOFException("no more banks to decode"); + } + + if (content.length != 0) { + final BLTEContent blteEntry = content[bank]; + final long encodedSize = blteEntry.getCompressedSize(); + final long decodedSize = blteEntry.getDecompressedSize(); + + if (streamBuffer.remaining() < encodedSize) { + throw new MalformedCASCStructureException("encoded data beyond end of file"); + } else if (bankBuffer == null) { + if (decodedSize > Integer.MAX_VALUE) { + throw new MalformedCASCStructureException("bank too large for Java to manipulate"); + } + bankBuffer = ByteBuffer.allocate((int) decodedSize); + } else if (bankBuffer.remaining() < decodedSize) { + throw new BufferOverflowException(); + } + + final ByteBuffer encodedBuffer = ((ByteBuffer) streamBuffer.slice().limit((int) encodedSize)).slice(); + final ByteBuffer decodedBuffer = ((ByteBuffer) bankBuffer.slice().limit((int) decodedSize)).slice(); + final byte[] intermediateEncodedCopy = new byte[encodedBuffer.remaining()]; + final byte[] intermediateDecodedCopy = new byte[decodedBuffer.remaining()]; + + final char encodingMode = (char) encodedBuffer.get(); + switch (encodingMode) { + case 'N': + // uncompressed data + if (encodedBuffer.remaining() != decodedSize) { + throw new MalformedCASCStructureException("not enough uncompressed bytes"); + } + decodedBuffer.put(encodedBuffer); + break; + case 'Z': + // zlib compressed data + final Inflater zlib = new Inflater(); + encodedBuffer.get(intermediateEncodedCopy, 0, encodedBuffer.remaining()); + zlib.setInput(intermediateEncodedCopy); + final int resultSize; + try { + resultSize = zlib.inflate(intermediateDecodedCopy); + decodedBuffer.put(intermediateDecodedCopy, 0, resultSize); + } catch (final DataFormatException e) { + throw new MalformedCASCStructureException("zlib inflate exception", e); + } + if (resultSize != decodedSize) { + throw new MalformedCASCStructureException("not enough bytes generated: " + resultSize + "B"); + } else if (!zlib.finished()) { + throw new MalformedCASCStructureException("unfinished inflate operation"); + } + break; + default: + throw new UnsupportedEncodingException("unsupported encoding mode: " + encodingMode); + } + + streamBuffer.position(streamBuffer.position() + encodedBuffer.position()); + bankBuffer.position(bankBuffer.position() + decodedBuffer.position()); + + bank += 1; + if (bank == content.length) { + hasBanks = false; + } + } else { + // this logic is guessed and requires confirmation + if (bankBuffer == null) { + bankBuffer = ByteBuffer.allocate(streamBuffer.remaining()); + } else if (bankBuffer.remaining() < streamBuffer.remaining()) { + throw new MalformedCASCStructureException("bank buffer too small"); + } + + bankBuffer.put(streamBuffer); + hasBanks = false; + } + + return bankBuffer; + } + + /** + * Returns true while one or more banks are remaining to be streamed. Only valid + * if hasBanks returns true. + * + * @return True if another bank can be decoded, otherwise false. + */ + public boolean hasNextBank() { + return hasBanks; + } +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/storage/IndexEntry.java b/core/src/com/hiveworkshop/blizzard/casc/storage/IndexEntry.java new file mode 100644 index 0000000..02ff05c --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/storage/IndexEntry.java @@ -0,0 +1,60 @@ +package com.hiveworkshop.blizzard.casc.storage; + +import com.hiveworkshop.blizzard.casc.Key; + +public class IndexEntry { + /** + * Index encoding key. + */ + private final Key key; + + /** + * Logical offset of storage container. + */ + private final long dataOffset; + + /** + * Size of storage container. + */ + private final long fileSize; + + public IndexEntry(final byte[] key, final long dataOffset, final long fileSize) { + this.key = new Key(key); + this.dataOffset = dataOffset; + this.fileSize = fileSize; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("IndexEntry{key="); + builder.append(key); + builder.append(", dataOffset="); + builder.append(dataOffset); + builder.append(", fileSize="); + builder.append(fileSize); + builder.append("}"); + + return builder.toString(); + } + + public long getDataOffset() { + return dataOffset; + } + + public long getFileSize() { + return fileSize; + } + + public String getKeyString() { + return key.toString(); + } + + public Key getKey() { + return key; + } + + public int compareKey(final Key otherKey) { + return otherKey.compareTo(key); + } +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/storage/IndexFile.java b/core/src/com/hiveworkshop/blizzard/casc/storage/IndexFile.java new file mode 100644 index 0000000..e61d9b6 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/storage/IndexFile.java @@ -0,0 +1,159 @@ +package com.hiveworkshop.blizzard.casc.storage; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Collections; + +import com.hiveworkshop.blizzard.casc.Key; +import com.hiveworkshop.blizzard.casc.nio.HashMismatchException; +import com.hiveworkshop.blizzard.casc.nio.LittleHashBlockProcessor; +import com.hiveworkshop.blizzard.casc.nio.MalformedCASCStructureException; + +public class IndexFile { + /** + * Alignment of the index entry block in bytes. + */ + private static final int ENTRY_BLOCK_ALIGNMENT = 16; + + private int bucketIndex; + + private int fileSizeLength; + + private int dataOffsetLength; + + private int encodingKeyLength; + + private int dataFileSizeBits; + + private long dataSizeMaximum; + + private final ArrayList entries = new ArrayList<>(); + + public IndexFile(final ByteBuffer fileBuffer) throws IOException { + decode(fileBuffer); + } + + private void decode(final ByteBuffer fileBuffer) throws IOException { + final ByteBuffer sourceBuffer = fileBuffer.slice(); + + // decode header + + final LittleHashBlockProcessor hashBlockProcessor = new LittleHashBlockProcessor(); + + final ByteBuffer headerBuffer; + try { + headerBuffer = hashBlockProcessor.getBlock(sourceBuffer); + } catch (final HashMismatchException e) { + throw new MalformedCASCStructureException("header block corrupt", e); + } + + headerBuffer.order(ByteOrder.LITTLE_ENDIAN); + + try { + if (headerBuffer.getShort() != 7) { + // possibly malformed + } + bucketIndex = Byte.toUnsignedInt(headerBuffer.get()); + if (headerBuffer.get() != 0) { + // possibly malformed + } + fileSizeLength = Byte.toUnsignedInt(headerBuffer.get()); + dataOffsetLength = Byte.toUnsignedInt(headerBuffer.get()); + encodingKeyLength = Byte.toUnsignedInt(headerBuffer.get()); + dataFileSizeBits = Byte.toUnsignedInt(headerBuffer.get()); + dataSizeMaximum = headerBuffer.getLong(); + } catch (final BufferUnderflowException e) { + throw new MalformedCASCStructureException("header block too small"); + } + + // decode entries + + final int entriesAlignmentMask = ENTRY_BLOCK_ALIGNMENT - 1; + sourceBuffer.position((sourceBuffer.position() + entriesAlignmentMask) & ~entriesAlignmentMask); + + final ByteBuffer entryBuffer; + try { + entryBuffer = hashBlockProcessor.getBlock(sourceBuffer); + } catch (final HashMismatchException e) { + throw new MalformedCASCStructureException("entries block corrupt", e); + } + + final int entryLength = fileSizeLength + dataOffsetLength + encodingKeyLength; + final int entryCount = entryBuffer.remaining() / entryLength; + + entries.ensureCapacity(entryCount); + + final ByteBuffer decodeDataOffsetBuffer = ByteBuffer.allocate(Long.BYTES); + final int decodeDataOffsetOffset = Long.BYTES - dataOffsetLength; + final ByteBuffer decodeFileSizeBuffer = ByteBuffer.allocate(Long.BYTES); + decodeFileSizeBuffer.order(ByteOrder.LITTLE_ENDIAN); + for (int i = 0; i < entryCount; i += 1) { + final byte[] key = new byte[encodingKeyLength]; + entryBuffer.get(key); + + entryBuffer.get(decodeDataOffsetBuffer.array(), decodeDataOffsetOffset, dataOffsetLength); + final long dataOffset = decodeDataOffsetBuffer.getLong(0); + + entryBuffer.get(decodeFileSizeBuffer.array(), 0, fileSizeLength); + final long fileSize = decodeFileSizeBuffer.getLong(0); + + // this can be used to detect special cross linking entries + // if (getIndexNumber(entry.key, entry.key.length) != bucketIndex); + // System.out.println("Bad key index: index=" + i + ", entry=" + entry + ", + // bucket=" + getIndexNumber(entry.key, entry.key.length)); + + entries.add(new IndexEntry(key, dataOffset, fileSize)); + } + + if (entryBuffer.hasRemaining()) { + throw new MalformedCASCStructureException("unable to fully process entries block"); + } + + fileBuffer.position(fileBuffer.position() + sourceBuffer.position()); + } + + public int getBucketIndex() { + return bucketIndex; + } + + public int getStoreIndex(final long dataOffset) { + return (int) (dataOffset >>> dataFileSizeBits); + } + + public long getStoreOffset(final long dataOffset) { + return dataOffset & ((1L << dataFileSizeBits) - 1L); + } + + public long getDataSizeMaximum() { + return dataSizeMaximum; + } + + public IndexEntry getEntry(final Key encodingKey) { + final int index = Collections.binarySearch(entries, encodingKey, (left, right) -> { + if ((left instanceof IndexEntry) && (right instanceof Key)) { + final IndexEntry entry = (IndexEntry) left; + final Key ekey = (Key) right; + return entry.getKey().compareTo(ekey); + } + throw new IllegalArgumentException("binary search comparing in inverted order"); + }); + + return index >= 0 ? entries.get(index) : null; + } + + public IndexEntry getEntry(final int index) { + return entries.get(index); + } + + public int getEntryCount() { + return entries.size(); + } + + public int getEncodingKeyLength() { + return encodingKeyLength; + } + +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/storage/Storage.java b/core/src/com/hiveworkshop/blizzard/casc/storage/Storage.java new file mode 100644 index 0000000..a5b75d2 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/storage/Storage.java @@ -0,0 +1,334 @@ +package com.hiveworkshop.blizzard.casc.storage; + +import java.io.EOFException; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; + +import com.hiveworkshop.blizzard.casc.Key; +import com.hiveworkshop.blizzard.casc.nio.MalformedCASCStructureException; + +/** + * Main data storage of a CASC archive. It consists of index files which point + * to storage containers in data files. + */ +public class Storage implements AutoCloseable { + /** + * The name of the data folder containing the configuration files. + */ + public static final String DATA_FOLDER_NAME = "data"; + + /** + * Number of index files used by a data store. + */ + private static final int INDEX_COUNT = 16; + + /** + * Usual number of copies of a specific index located in the folder. This is an + * estimate only used to increase search performance and will not effect + * results. + */ + private static final int INDEX_COPIES = 2; + + /** + * File extension used by storage index files. + */ + public static final String INDEX_FILE_EXTENSION = "idx"; + + /** + * File name of data files. 3 character extension is the index. + */ + public static final String DATA_FILE_NAME = "data"; + + /** + * Largest permitted data file index. + */ + public static final int DATA_FILE_INDEX_MAXIMUM = 999; + + /** + * Extension length used by data files. Defined by the length needed to store + * DATA_FILE_INDEX_MAXIMUM as a decimal string. + */ + public static final int DATA_FILE_EXTENSION_LENGTH = 3; + + /** + * Converts an encoding key into an index file number. + * + * @param encodingKey Input encoding key. + * @param keyLength Length of key to be processed. + * @return Index number. + */ + public static int getBucketIndex(final byte[] encodingKey, final int keyLength) { + int accumulator = 0; + for (int i = 0; i < keyLength; i += 1) { + accumulator ^= encodingKey[i]; + } + final int nibbleMask = (1 << 4) - 1; + return (accumulator & nibbleMask) ^ ((accumulator >> 4) & nibbleMask); + } + + private Path folder; + + private final HashMap channelMap = new HashMap<>(); + + private final IndexFile[] indicies = new IndexFile[INDEX_COUNT]; + + /** + * Index file versions loaded. Possibly useful for debugging. + */ + private final long[] idxVersions = new long[INDEX_COUNT]; + + /** + * Used to track closed status of the store. + */ + private boolean closed = false; + + private boolean useMemoryMapping; + + private int encodingKeyLength; + + /** + * Construct a storage object from the provided data folder. + *

+ * Using memory mapping should give the best performance. However some platforms + * or file systems might not support it. + * + * @param dataFolder Path of the CASC data folder. + * @param useOld Use other (old?) version of index files. + * @param useMemoryMapping If IO should be memory mapped. + * @throws IOException If there was a problem loading from the data folder. + */ + public Storage(final Path dataFolder, final boolean useOld, final boolean useMemoryMapping) throws IOException { + folder = dataFolder.resolve(DATA_FOLDER_NAME); + this.useMemoryMapping = useMemoryMapping; + + final ArrayList indexFiles = new ArrayList(INDEX_COUNT * INDEX_COPIES); + try (final DirectoryStream indexFileIterator = Files.newDirectoryStream(folder, + "*." + INDEX_FILE_EXTENSION)) { + for (final Path indexFile : indexFileIterator) { + indexFiles.add(indexFile); + } + } + + class IndexFileNameMeta { + private Path filePath; + private int index; + private long version; + } + + final HashMap> metaMap = new HashMap>( + INDEX_COUNT); + + for (final Path indexFile : indexFiles) { + final String fileName = indexFile.getFileName().toString(); + + final IndexFileNameMeta fileMeta = new IndexFileNameMeta(); + fileMeta.filePath = indexFile; + fileMeta.index = Integer.parseUnsignedInt(fileName.substring(0, 2), 16); + fileMeta.version = Long.parseUnsignedLong(fileName.substring(2, 10), 16); + + ArrayList bucketList = metaMap.get(fileMeta.index); + if (bucketList == null) { + bucketList = new ArrayList<>(); + metaMap.put(fileMeta.index, bucketList); + } + + bucketList.add(fileMeta); + } + + Comparator bucketOrder = (left, right) -> { + return (int) (left.version - right.version); + }; + if (!useOld) { + bucketOrder = Collections.reverseOrder(bucketOrder); + } + + for (int index = 0; index < indicies.length; index += 1) { + final ArrayList bucketList = metaMap.get(index); + if (bucketList == null) { + throw new MalformedCASCStructureException("storage index file missing"); + } + + Collections.sort(bucketList, bucketOrder); + + final IndexFileNameMeta fileMeta = bucketList.get(0); + idxVersions[index] = fileMeta.version; + indicies[index] = new IndexFile(loadFileFully(fileMeta.filePath)); + } + + // resolve index key length being used + int index = 0; + encodingKeyLength = indicies[index++].getEncodingKeyLength(); + for (; index < indicies.length; index += 1) { + if (encodingKeyLength != indicies[index].getEncodingKeyLength()) { + throw new MalformedCASCStructureException("inconsistent encoding key length between index files"); + } + } + } + + @Override + public synchronized void close() throws IOException { + if (closed) { + return; + } + + IOException exception = null; + for (final Map.Entry channelEntry : channelMap.entrySet()) { + try { + channelEntry.getValue().close(); + } catch (final IOException e) { + if (exception != null) { + exception.addSuppressed(e); + } else { + exception = e; + } + } + } + + closed = true; + + if (exception != null) { + throw new IOException("IOExceptions occured during closure", exception); + } + } + + public boolean hasBanks(final Key encodingKey) { + final int bucketIndex = getBucketIndex(encodingKey.getKey(), encodingKeyLength); + final IndexFile index = indicies[bucketIndex]; + final IndexEntry indexEntry = index.getEntry(encodingKey); + + return indexEntry != null; + } + + public BankStream getBanks(final Key encodingKey) throws IOException { + final int bucketIndex = getBucketIndex(encodingKey.getKey(), encodingKeyLength); + final IndexFile index = indicies[bucketIndex]; + final IndexEntry indexEntry = index.getEntry(encodingKey); + + if (indexEntry == null) { + throw new FileNotFoundException("encoding key not in store indicies"); + } + + final long dataOffset = indexEntry.getDataOffset(); + final int storeIndex = index.getStoreIndex(dataOffset); + final long storeOffset = index.getStoreOffset(dataOffset); + + final ByteBuffer storageBuffer = getStorageBuffer(storeIndex, storeOffset, indexEntry.getFileSize()); + + return new BankStream(storageBuffer, indexEntry.getKey()); + } + + private synchronized FileChannel getDataFileChannel(final int index) throws IOException { + if (closed) { + throw new ClosedChannelException(); + } + + FileChannel fileChannel = channelMap.get(index); + if (fileChannel == null) { + if (index > DATA_FILE_INDEX_MAXIMUM) { + throw new MalformedCASCStructureException("storage data file index too large"); + } + + final StringBuilder builder = new StringBuilder(); + builder.append(DATA_FILE_NAME); + builder.append('.'); + final String extensionNumber = Integer.toUnsignedString(index); + final int extensionZeroCount = DATA_FILE_EXTENSION_LENGTH - extensionNumber.length(); + for (int i = 0; i < extensionZeroCount; i += 1) { + builder.append('0'); + } + builder.append(extensionNumber); + + final Path filePath = folder.resolve(builder.toString()); + fileChannel = FileChannel.open(filePath, StandardOpenOption.READ); + channelMap.put(index, fileChannel); + } + + return fileChannel; + } + + /** + * Fetch a buffer from storage. + * + * @param index Data file index. + * @param offset Data file offset. + * @param length Requested buffer length. + * @return Storage buffer. + * @throws IOException If a problem occurs when preparing the storage buffer. + */ + private ByteBuffer getStorageBuffer(final int index, final long offset, final long length) throws IOException { + final FileChannel fileChannel = getDataFileChannel(index); + if (length > Integer.MAX_VALUE) { + throw new MalformedCASCStructureException("data buffer too large to process"); + } + + final ByteBuffer storageBuffer; + if (useMemoryMapping) { + final MappedByteBuffer mappedBuffer = fileChannel.map(MapMode.READ_ONLY, offset, length); + mappedBuffer.load(); + storageBuffer = mappedBuffer; + } else { + storageBuffer = ByteBuffer.allocate((int) length); + while (storageBuffer.hasRemaining() + && (fileChannel.read(storageBuffer, offset + storageBuffer.position()) != -1)) { + ; + } + + if (storageBuffer.hasRemaining()) { + throw new EOFException("unexpected end of file"); + } + storageBuffer.clear(); + } + + return storageBuffer; + } + + /** + * Loads a file fully into memory. Memory mapping is used if allowed. + * + * @param file Path of file to load into memory. + * @return File buffered into memory. + * @throws IOException If an IO exception occurs. + */ + private ByteBuffer loadFileFully(final Path file) throws IOException { + final ByteBuffer fileBuffer; + try (final FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) { + final long fileLength = channel.size(); + if (fileLength > Integer.MAX_VALUE) { + throw new MalformedCASCStructureException("file too large to process"); + } + + if (useMemoryMapping) { + final MappedByteBuffer mappedBuffer = channel.map(MapMode.READ_ONLY, 0, fileLength); + mappedBuffer.load(); + fileBuffer = mappedBuffer; + } else { + fileBuffer = ByteBuffer.allocate((int) fileLength); + while (fileBuffer.hasRemaining() && (channel.read(fileBuffer, fileBuffer.position()) != -1)) { + ; + } + + if (fileBuffer.hasRemaining()) { + throw new EOFException("unexpected end of file"); + } + fileBuffer.clear(); + } + } + + return fileBuffer; + } + +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/storage/StorageContainer.java b/core/src/com/hiveworkshop/blizzard/casc/storage/StorageContainer.java new file mode 100644 index 0000000..8bd0885 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/storage/StorageContainer.java @@ -0,0 +1,92 @@ +package com.hiveworkshop.blizzard.casc.storage; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import com.hiveworkshop.blizzard.casc.Key; +import com.hiveworkshop.blizzard.casc.nio.HashMismatchException; +import com.hiveworkshop.blizzard.casc.nio.MalformedCASCStructureException; + +/** + * High level storage container representing a storage entry. + */ +public class StorageContainer { + /** + * Size of storage encoding key in bytes. + */ + private static final int ENCODING_KEY_SIZE = 16; + + /** + * Container encoding key. + */ + private Key key; + private long size; + private short flags; + + public StorageContainer(final ByteBuffer storageBuffer) throws IOException { + final ByteBuffer containerBuffer = storageBuffer.slice(); + containerBuffer.order(ByteOrder.LITTLE_ENDIAN); + + // key is in reversed byte order + final int checksumA; + final int checksumB; + try { + final byte[] keyArray = new byte[ENCODING_KEY_SIZE]; + final int keyEnd = containerBuffer.position() + keyArray.length; + for (int writeIndex = 0, readIndex = keyEnd + - 1; writeIndex < keyArray.length; writeIndex += 1, readIndex -= 1) { + keyArray[writeIndex] = containerBuffer.get(readIndex); + } + containerBuffer.position(keyEnd); + + key = new Key(keyArray); + size = Integer.toUnsignedLong(containerBuffer.getInt()); + flags = containerBuffer.getShort(); + + checksumA = containerBuffer.getInt(); + checksumB = containerBuffer.getInt(); + } catch (final BufferUnderflowException e) { + throw new MalformedCASCStructureException("storage buffer too small"); + } + + final int computedA = checksumA; // TODO compute this + final int computedB = checksumB; // TODO compute this + if (checksumA != computedA) { + throw new HashMismatchException("container checksum A mismatch"); + } + if (checksumB != computedB) { + throw new HashMismatchException("container checksum B mismatch"); + } + + storageBuffer.position(storageBuffer.position() + containerBuffer.position()); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("FileEntry{key="); + builder.append(key); + builder.append(", size="); + builder.append(size); + builder.append(", flags="); + builder.append(Integer.toBinaryString(flags)); + builder.append("}"); + + return builder.toString(); + } + + public long getSize() { + return size; + } + + public short getFlags() { + return flags; + } + + public Key getKey() { + return key; + } + +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/trash/LocalDataFiles.java b/core/src/com/hiveworkshop/blizzard/casc/trash/LocalDataFiles.java new file mode 100644 index 0000000..51f348c --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/trash/LocalDataFiles.java @@ -0,0 +1,343 @@ +package com.hiveworkshop.blizzard.casc.trash; + +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +import com.hiveworkshop.blizzard.casc.nio.HashMismatchException; +import com.hiveworkshop.blizzard.casc.nio.MalformedCASCStructureException; +import com.hiveworkshop.lang.Hex; + +public class LocalDataFiles implements Closeable { + private static final int FRAGMENTATION_SIZE_BITS = 30; + + private static final int FILE_ENTRY_HEADER_SIZE = 30; + + private static final byte[] BLTE_MIME = new byte[] { 'B', 'L', 'T', 'E' }; + + private final HashMap dataFiles = new HashMap(); + + public LocalDataFiles(final Path dataPath) throws IOException { + mountDataFiles(dataPath); + } + + public void mountDataFiles(final Path dataPath) throws IOException { + try (final DirectoryStream dataFileIterator = Files.newDirectoryStream(dataPath, "data.*")) { + for (final Path dataFile : dataFileIterator) { + System.out.println(dataFile); + final String fileName = dataFile.getFileName().toString(); + final int number = Integer.parseInt(fileName.substring(5, 8)); + dataFiles.put(number, FileChannel.open(dataFile, StandardOpenOption.READ)); + } + + } catch (final Exception e) { + close(); + } + } + + @Override + public void close() throws IOException { + IOException exception = null; + for (final Map.Entry dataFileEntry : dataFiles.entrySet()) { + try { + dataFileEntry.getValue().close(); + } catch (final IOException e) { + exception = e; + } + } + + if (exception != null) { + throw new IOException("one or more IOExceptions occured during closure", exception); + } + } + + public FileEntry getFileEntry(final LocalIndexFile.IndexEntry indexEntry) throws IOException { + final ByteBuffer fileHeader = ByteBuffer.allocate(FILE_ENTRY_HEADER_SIZE); + + final long dataOffset = indexEntry.getDataOffset(); + final int dataFile = (int) (dataOffset >>> FRAGMENTATION_SIZE_BITS); + final long fileOffset = dataOffset & (1L << FRAGMENTATION_SIZE_BITS) - 1L; + final FileChannel channel = dataFiles.get(dataFile); + if (channel.read(fileHeader, fileOffset) != fileHeader.limit()) { + throw new EOFException("unexpected incomplete read"); + } + fileHeader.flip(); + + fileHeader.order(ByteOrder.LITTLE_ENDIAN); + final FileEntry fileEntry = new FileEntry(); + fileEntry.dataFile = dataFile; + fileEntry.fileOffset = fileOffset; + + // key is in reversed byte order + final byte[] key = new byte[16]; + int keyPos = fileHeader.position() + key.length; + fileHeader.position(keyPos); + for (int i = 0; i < key.length; i++) { + key[i] = fileHeader.get(--keyPos); + } + if (!indexEntry.compareKey(key)) { + throw new HashMismatchException("file entry does not match index entry"); + } + fileEntry.key = key; + + fileEntry.size = Integer.toUnsignedLong(fileHeader.getInt()); + fileEntry.flags = fileHeader.getShort(); + + // TODO actually check these + final int ChecksumA = fileHeader.getInt(); + final int ChecksumB = fileHeader.getInt(); + + return fileEntry; + } + + public BLTEChunk[] getBLTEChunks(final FileEntry file) throws IOException { + final FileChannel channel = dataFiles.get(file.dataFile); + final ByteBuffer blteDeclareHeader = ByteBuffer.allocate(8); + + final long blteOffset = file.fileOffset + FILE_ENTRY_HEADER_SIZE; + long currentOffset = blteOffset; + final long blteLimit = file.fileOffset + file.size; + if (blteLimit - currentOffset < blteDeclareHeader.capacity()) { + throw new MalformedCASCStructureException("BLTE header extends beyond file limits"); + } + + currentOffset += channel.read(blteDeclareHeader, currentOffset); + if (blteDeclareHeader.hasRemaining()) { + throw new EOFException("unexpected incomplete read"); + } + blteDeclareHeader.flip(); + + blteDeclareHeader.order(ByteOrder.BIG_ENDIAN); + final byte[] mime = new byte[BLTE_MIME.length]; + blteDeclareHeader.get(mime); + if (!Arrays.equals(mime, BLTE_MIME)) { + throw new MalformedCASCStructureException("expected BLTE mime"); + } + final long headerSize = Integer.toUnsignedLong(blteDeclareHeader.getInt()); + + BLTEChunk[] chunks; + if (headerSize > 0) { + final long headerBodySize = headerSize - blteDeclareHeader.capacity(); + + if (headerBodySize > Integer.MAX_VALUE) { + throw new MalformedCASCStructureException("BLTE header too large to process"); + } else if (blteOffset + headerBodySize > blteLimit) { + throw new MalformedCASCStructureException("BLTE header extends beyond file limits"); + } + + final ByteBuffer blteHeaderBody = ByteBuffer.allocate((int) headerBodySize); + + currentOffset += channel.read(blteHeaderBody, currentOffset); + if (blteHeaderBody.hasRemaining()) { + throw new EOFException("unexpected incomplete read"); + } + blteHeaderBody.flip(); + + blteHeaderBody.order(ByteOrder.BIG_ENDIAN); + blteHeaderBody.mark(); + final byte flags = blteHeaderBody.get(); + if (flags != 0xF) { + throw new MalformedCASCStructureException("unknown BLTE flags"); + } + // BE24 read + blteHeaderBody.reset(); + blteHeaderBody.put((byte) 0); + blteHeaderBody.reset(); + + final int chunkCount = blteHeaderBody.getInt(); + if (chunkCount < 0) { + throw new MalformedCASCStructureException("BLTE chunk count too large to process"); + } else if (chunkCount == 0) { + throw new MalformedCASCStructureException("invalid BLTE chunk count"); + } + + chunks = new BLTEChunk[chunkCount]; + long decompressedOffset = 0; + long compressedOffset = currentOffset - file.fileOffset; + for (int i = 0; i < chunks.length; i += 1) { + final long compressedSize = Integer.toUnsignedLong(blteHeaderBody.getInt()); + final long decompressedSize = Integer.toUnsignedLong(blteHeaderBody.getInt()); + final byte[] checksumHash = new byte[16]; + blteHeaderBody.get(checksumHash); + + final BLTEChunk chunk = new BLTEChunk(); + chunk.compressedOffset = compressedOffset; + chunk.compressedSize = compressedSize; + chunk.decompressedOffset = decompressedOffset; + chunk.decompressedSize = decompressedSize; + chunk.checksumHash = checksumHash; + chunks[i] = chunk; + + compressedOffset += compressedSize; + decompressedOffset += decompressedSize; + } + } else { + chunks = new BLTEChunk[1]; + final BLTEChunk chunk = new BLTEChunk(); + chunk.compressedOffset = currentOffset - file.fileOffset; + chunk.compressedSize = blteLimit - currentOffset; + chunk.decompressedOffset = 0; + + // unknown values, needs experimentation + chunk.decompressedSize = chunk.compressedSize; + chunk.checksumHash = null; + chunks[0] = chunk; + } + return chunks; + } + + public ByteBuffer getBLTEData(final FileEntry file, final BLTEChunk chunk, ByteBuffer blteDataBuffer) + throws IOException { + if (chunk.compressedSize + chunk.compressedOffset > file.size) { + throw new MalformedCASCStructureException("BLTE data extends beyond file data"); + } + + if (blteDataBuffer == null || blteDataBuffer.remaining() < chunk.compressedSize) { + blteDataBuffer = ByteBuffer.allocate((int) chunk.compressedSize); + } + + final int blteDataBufferLimit = blteDataBuffer.limit(); + blteDataBuffer.limit(blteDataBuffer.position() + (int) chunk.compressedSize); + try { + final FileChannel channel = dataFiles.get(file.dataFile); + channel.read(blteDataBuffer, file.fileOffset + chunk.compressedOffset); + if (blteDataBuffer.hasRemaining()) { + throw new EOFException("unexpected incomplete read"); + } + } finally { + blteDataBuffer.position(blteDataBuffer.limit()); + blteDataBuffer.limit(blteDataBufferLimit); + } + + return blteDataBuffer; + } + + public ByteBuffer getFileData(final BLTEChunk chunk, final ByteBuffer blteDataBuffer, ByteBuffer fileDataBuffer) + throws IOException { + if (blteDataBuffer.remaining() < chunk.compressedSize) { + throw new MalformedCASCStructureException("BLTE data too small"); + } + + if (fileDataBuffer == null || fileDataBuffer.remaining() < chunk.decompressedSize) { + fileDataBuffer = ByteBuffer.allocate((int) chunk.decompressedSize); + } + + final int blteDataBufferLimit = blteDataBuffer.limit(); + final int fileDataBufferLimit = fileDataBuffer.limit(); + blteDataBuffer.limit(blteDataBuffer.position() + (int) chunk.compressedSize); + fileDataBuffer.limit(fileDataBuffer.position() + (int) chunk.decompressedSize); + try { + final char encodingMode = (char) blteDataBuffer.get(); + switch (encodingMode) { + case 'N': + if (blteDataBuffer.remaining() != chunk.decompressedSize) { + throw new MalformedCASCStructureException("not enough uncompressed bytes"); + } + fileDataBuffer.put(blteDataBuffer); + break; + case 'Z': + final Inflater zlib = new Inflater(); + zlib.setInput(blteDataBuffer.array(), blteDataBuffer.position(), blteDataBuffer.remaining()); + int resultSize; + try { + resultSize = zlib.inflate(fileDataBuffer.array(), fileDataBuffer.position(), + fileDataBuffer.remaining()); + } catch (final DataFormatException e) { + throw new MalformedCASCStructureException("zlib inflate exception", e); + } + if (resultSize != chunk.decompressedSize) { + throw new MalformedCASCStructureException("not enough bytes generated: " + resultSize + "B"); + } else if (!zlib.finished()) { + throw new MalformedCASCStructureException("unfinished inflate operation"); + } + break; + default: + throw new UnsupportedEncodingException("unsupported encoding mode: " + encodingMode); + } + } finally { + blteDataBuffer.position(blteDataBuffer.limit()); + blteDataBuffer.limit(blteDataBufferLimit); + fileDataBuffer.position(fileDataBuffer.limit()); + fileDataBuffer.limit(fileDataBufferLimit); + } + + return fileDataBuffer; + } + + public static class FileEntry { + private byte[] key; + private int dataFile; + private long fileOffset; + private long size; + private short flags; + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("FileEntry{key=0x"); + Hex.stringBufferAppendHex(builder, key); + builder.append(", dataFile="); + builder.append(dataFile); + builder.append(", fileOffset="); + builder.append(fileOffset); + builder.append(", size="); + builder.append(size); + builder.append(", flags="); + builder.append(Integer.toBinaryString(flags)); + builder.append("}"); + + return builder.toString(); + } + + public boolean hasBLTE() { + return size > FILE_ENTRY_HEADER_SIZE; + } + } + + public static class BLTEChunk { + private long compressedOffset; + private long compressedSize; + private long decompressedOffset; + private long decompressedSize; + private byte[] checksumHash; + + public long getSize() { + return decompressedSize; + } + + public long getOffset() { + return decompressedOffset; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("BLTEChunk{compressedSize="); + builder.append(compressedSize); + builder.append(", decompressedSize="); + builder.append(decompressedSize); + builder.append(", compressedOffset="); + builder.append(compressedOffset); + builder.append(", decompressedOffset="); + builder.append(decompressedOffset); + builder.append(", checksumHash=0x"); + Hex.stringBufferAppendHex(builder, checksumHash); + builder.append("}"); + + return builder.toString(); + } + } +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/trash/LocalIndexFile.java b/core/src/com/hiveworkshop/blizzard/casc/trash/LocalIndexFile.java new file mode 100644 index 0000000..d0ae136 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/trash/LocalIndexFile.java @@ -0,0 +1,171 @@ +package com.hiveworkshop.blizzard.casc.trash; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; + +import com.hiveworkshop.ReteraCASCUtils; +import com.hiveworkshop.blizzard.casc.nio.HashMismatchException; +import com.hiveworkshop.blizzard.casc.nio.LittleHashBlockProcessor; +import com.hiveworkshop.lang.Hex; + +public class LocalIndexFile { + private byte bucketIndex; + + private byte entryFileSizeLength; + + private byte entryDataOffsetLength; + + private byte entryKeyLength; + + private byte dataFileSizeBits; + + private long dataSizeMaximum; + + private final ArrayList entries = new ArrayList<>(); + + public static int getIndexNumber(final byte[] key, final int keyLength) { + int accumulator = 0; + for (int i = 0; i < keyLength; i += 1) { + accumulator ^= key[i]; + } + final int nibbleMask = (1 << 4) - 1; + return accumulator & nibbleMask ^ accumulator >> 4 & nibbleMask; + } + + public LocalIndexFile(final ByteBuffer encodedFileBuffer) throws IOException { + decode(encodedFileBuffer); + } + + public void decode(final ByteBuffer encodedFileBuffer) throws IOException { + final LittleHashBlockProcessor hashBlockProcessor = new LittleHashBlockProcessor(); + final int fileLength = encodedFileBuffer.limit(); + + final int headerLength = hashBlockProcessor.processBlock(encodedFileBuffer); + if (headerLength < 0) { + throw new HashMismatchException("index header corrupt"); + } + + encodedFileBuffer.limit(encodedFileBuffer.position() + headerLength); + encodedFileBuffer.order(ByteOrder.LITTLE_ENDIAN); + + if (encodedFileBuffer.getShort() != 7) { + // possibly malformed + } + bucketIndex = encodedFileBuffer.get(); + if (encodedFileBuffer.get() != 0) { + // possibly malformed + } + entryFileSizeLength = encodedFileBuffer.get(); + entryDataOffsetLength = encodedFileBuffer.get(); + entryKeyLength = encodedFileBuffer.get(); + dataFileSizeBits = encodedFileBuffer.get(); + dataSizeMaximum = encodedFileBuffer.getLong(); + + encodedFileBuffer.limit(fileLength); + final int entriesAlignmentMask = (1 << 4) - 1; + encodedFileBuffer.position((encodedFileBuffer.position() + entriesAlignmentMask) & ~entriesAlignmentMask); + + final int entriesLength = hashBlockProcessor.processBlock(encodedFileBuffer); + if (entriesLength < 0) { + throw new HashMismatchException("index entries corrupt"); + } + + encodedFileBuffer.limit(encodedFileBuffer.position() + entriesLength); + final int entryLength = entryFileSizeLength + entryDataOffsetLength + entryKeyLength; + final int entryCount = encodedFileBuffer.remaining() / entryLength; + + entries.ensureCapacity(entryCount); + + final ByteBuffer decodeDataOffsetBuffer = ByteBuffer.allocate(Long.BYTES); + final int decodeDataOffsetOffset = Long.BYTES - entryDataOffsetLength; + final ByteBuffer decodeFileSizeBuffer = ByteBuffer.allocate(Long.BYTES); + decodeFileSizeBuffer.order(ByteOrder.LITTLE_ENDIAN); + for (int i = 0; i < entryCount; i += 1) { + final IndexEntry entry = new IndexEntry(); + + entry.key = new byte[entryKeyLength]; + encodedFileBuffer.get(entry.key); + + encodedFileBuffer.get(decodeDataOffsetBuffer.array(), decodeDataOffsetOffset, entryDataOffsetLength); + entry.dataOffset = decodeDataOffsetBuffer.getLong(0); + + encodedFileBuffer.get(decodeFileSizeBuffer.array(), 0, entryFileSizeLength); + entry.fileSize = decodeFileSizeBuffer.getLong(0); + + // this can be used to detect special cross linking entries + // if (getIndexNumber(entry.key, entry.key.length) != bucketIndex); + // System.out.println("Bad key index: index=" + i + ", entry=" + entry + ", + // bucket=" + getIndexNumber(entry.key, entry.key.length)); + + entries.add(entry); + } + + encodedFileBuffer.limit(fileLength); + } + + public IndexEntry getEntry(final byte[] key) { + for (final LocalIndexFile.IndexEntry indexEntry : entries) { + if (ReteraCASCUtils.arraysEquals(indexEntry.key, 0, entryKeyLength, key, 0, entryKeyLength)) { + return indexEntry; + } + } + + return null; + } + + public IndexEntry getEntry(final int index) { + return entries.get(index); + } + + public int getEntryCount() { + return entries.size(); + } + + public long getDataFileOffset(final long dataOffset) { + return dataOffset & (1L << dataFileSizeBits) - 1L; + } + + public int getDataFileNumber(final long dataOffset) { + return (int) (dataOffset >>> dataFileSizeBits); + } + + public static class IndexEntry { + private byte[] key; + private long dataOffset; + private long fileSize; + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("IndexEntry{key=0x"); + Hex.stringBufferAppendHex(builder, key); + builder.append(", dataOffset="); + builder.append(dataOffset); + builder.append(", fileSize="); + builder.append(fileSize); + builder.append("}"); + + return builder.toString(); + } + + public long getDataOffset() { + return dataOffset; + } + + public long getFileSize() { + return fileSize; + } + + public String getKeyString() { + final StringBuilder builder = new StringBuilder(); + Hex.stringBufferAppendHex(builder, key); + return builder.toString(); + } + + public boolean compareKey(final byte[] otherKey) { + return ReteraCASCUtils.arraysEquals(key, 0, key.length, otherKey, 0, key.length); + } + } +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/trash/VirtualFileSystem.java b/core/src/com/hiveworkshop/blizzard/casc/trash/VirtualFileSystem.java new file mode 100644 index 0000000..c83c48a --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/trash/VirtualFileSystem.java @@ -0,0 +1,86 @@ +package com.hiveworkshop.blizzard.casc.trash; + +import java.io.IOException; +import java.io.PrintStream; +import java.nio.ByteBuffer; + +import com.hiveworkshop.blizzard.casc.nio.MalformedCASCStructureException; +//import com.hiveworkshop.blizzard.casc.vfs.path.Container; +//import com.hiveworkshop.blizzard.casc.vfs.path.FileFactory; +//import com.hiveworkshop.blizzard.casc.vfs.path.Node; + +public class VirtualFileSystem { + + private static final ByteBuffer IDENTIFIER = ByteBuffer.wrap(new byte[] { 'T', 'V', 'F', 'S' }); + + // private final ArrayList root; + + public VirtualFileSystem(final ByteBuffer fileBuffer) throws IOException { + final ByteBuffer localBuffer = fileBuffer.slice(); + + // check identifier + + if (localBuffer.remaining() < IDENTIFIER.remaining() + || !localBuffer.limit(IDENTIFIER.remaining()).equals(IDENTIFIER)) { + throw new MalformedCASCStructureException("missing TVFS identifier"); + } + + // decode header + + localBuffer.limit(localBuffer.capacity()); + localBuffer.position(IDENTIFIER.remaining()); + + final byte version = localBuffer.get(); + if (version != 1) { + throw new UnsupportedOperationException("unsupported vfs version: " + version); + } + final int headerSize = Byte.toUnsignedInt(localBuffer.get()); + if (headerSize < 0x26) { + throw new MalformedCASCStructureException("vfs header too small"); + } + localBuffer.limit(headerSize); + + final int encodingKeySize = Byte.toUnsignedInt(localBuffer.get()); + final int patchKeySize = Byte.toUnsignedInt(localBuffer.get()); + + final int flags = localBuffer.getInt(); + + final int pathOffset = localBuffer.getInt(); + final int pathSize = localBuffer.getInt(); + final int fileReferenceOffset = localBuffer.getInt(); + final int fileReferenceSize = localBuffer.getInt(); + final int contentOffset = localBuffer.getInt(); + final int contentSize = localBuffer.getInt(); + + final int maximumPathDepth = Short.toUnsignedInt(localBuffer.getShort()); + + final int containerTableOffsetSize = Math.max(1, + Integer.BYTES - Integer.numberOfLeadingZeros(contentSize) / Byte.SIZE); + + localBuffer.limit(pathOffset + pathSize); + localBuffer.position(pathOffset); + final ByteBuffer pathBuffer = localBuffer.slice(); + localBuffer.clear(); + + localBuffer.limit(fileReferenceOffset + fileReferenceSize); + localBuffer.position(fileReferenceOffset); + final ByteBuffer fileReferenceBuffer = localBuffer.slice(); + localBuffer.clear(); + + localBuffer.limit(contentOffset + contentSize); + localBuffer.position(contentOffset); + final ByteBuffer contentBuffer = localBuffer.slice(); + localBuffer.clear(); + + // final var fileFactory = new FileFactory(fileReferenceBuffer, contentBuffer, + // encodingKeySize, containerTableOffsetSize); + + // root = Container.decodeContainer(pathBuffer, fileFactory); + } + + public void printPaths(final PrintStream out) { + /* + * for (final var node : root) { node.printPaths(out, ""); } + */ + } +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/vfs/FileNode.java b/core/src/com/hiveworkshop/blizzard/casc/vfs/FileNode.java new file mode 100644 index 0000000..a7c3fd9 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/vfs/FileNode.java @@ -0,0 +1,24 @@ +package com.hiveworkshop.blizzard.casc.vfs; + +import java.util.List; + +/** + * A file system node containing a logical file. + */ +public class FileNode extends PathNode { + private final StorageReference[] references; + + protected FileNode(final List pathFragments, final List references) { + super(pathFragments); + this.references = references.toArray(new StorageReference[0]); + } + + public int getFileReferenceCount() { + return references.length; + } + + public StorageReference getFileReference(final int index) { + return references[index]; + } + +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/vfs/PathNode.java b/core/src/com/hiveworkshop/blizzard/casc/vfs/PathNode.java new file mode 100644 index 0000000..007b9d3 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/vfs/PathNode.java @@ -0,0 +1,27 @@ +package com.hiveworkshop.blizzard.casc.vfs; + +import java.util.List; + +/** + * Represents a path node. Path nodes can either be prefix nodes or file nodes. + */ +public abstract class PathNode { + /** + * Array of path fragments. Each fragment represents part of a path string. Due + * to the potential for multi byte encoding, one cannot assume that each + * fragment can be assembled into a valid string. + */ + private final byte[][] pathFragments; + + protected PathNode(final List pathFragments) { + this.pathFragments = pathFragments.toArray(new byte[0][]); + } + + public int getPathFragmentCount() { + return pathFragments.length; + } + + public byte[] getFragment(final int index) { + return pathFragments[index]; + } +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/vfs/PrefixNode.java b/core/src/com/hiveworkshop/blizzard/casc/vfs/PrefixNode.java new file mode 100644 index 0000000..938dd93 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/vfs/PrefixNode.java @@ -0,0 +1,26 @@ +package com.hiveworkshop.blizzard.casc.vfs; + +import java.util.List; + +/** + * Prefix nodes generate a path prefix for other nodes. + */ +public class PrefixNode extends PathNode { + /** + * Array of child node that this node forms a prefix of. + */ + private final PathNode[] nodes; + + protected PrefixNode(final List pathFragments, final List nodes) { + super(pathFragments); + this.nodes = nodes.toArray(new PathNode[0]); + } + + public int getNodeCount() { + return nodes.length; + } + + public PathNode getNode(final int index) { + return nodes[index]; + } +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/vfs/StorageReference.java b/core/src/com/hiveworkshop/blizzard/casc/vfs/StorageReference.java new file mode 100644 index 0000000..aeeef6a --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/vfs/StorageReference.java @@ -0,0 +1,81 @@ +package com.hiveworkshop.blizzard.casc.vfs; + +import com.hiveworkshop.blizzard.casc.Key; + +/** + * A reference to a file in CASC storage. + */ +public class StorageReference { + /** + * Logical offset of this chunk. + */ + private long offset = 0; + + /** + * Logical size of this chunk. + */ + private long size = 0; + + /** + * Encoding key of chunk. + */ + private Key encodingKey = null; + + /** + * Physical size of stored data. + */ + private long physicalSize = 0; + + /** + * Total size of all decompressed data banks. + */ + private long actualSize = 0; + + public StorageReference(final long offset, final long size, final Key encodingKey, final int physicalSize, + final int actualSize) { + this.offset = offset; + this.size = size; + this.encodingKey = encodingKey; + this.physicalSize = physicalSize; + this.actualSize = actualSize; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("FileReference{encodingKey="); + builder.append(encodingKey); + builder.append(", offset="); + builder.append(offset); + builder.append(", size="); + builder.append(size); + builder.append(", physicalSize="); + builder.append(physicalSize); + builder.append(", actualSize="); + builder.append(actualSize); + builder.append("}"); + + return builder.toString(); + } + + public long getOffset() { + return offset; + } + + public long getSize() { + return size; + } + + public Key getEncodingKey() { + return encodingKey; + } + + public long getPhysicalSize() { + return physicalSize; + } + + public long getActualSize() { + return actualSize; + } + +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/vfs/TVFSDecoder.java b/core/src/com/hiveworkshop/blizzard/casc/vfs/TVFSDecoder.java new file mode 100644 index 0000000..d84c2dd --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/vfs/TVFSDecoder.java @@ -0,0 +1,253 @@ +package com.hiveworkshop.blizzard.casc.vfs; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.hiveworkshop.blizzard.casc.Key; +import com.hiveworkshop.blizzard.casc.nio.MalformedCASCStructureException; + +/** + * Decodes file data for file value nodes. + *

+ * This is done by collating together data. First the file is resolved from the + * file buffer then the data describing its contents is resolved from the + * contents buffer. + */ +public class TVFSDecoder { + /** + * TVFS file identifier located at start of TVFS files. + */ + private static final ByteBuffer IDENTIFIER = ByteBuffer.wrap(new byte[] { 'T', 'V', 'F', 'S' }); + + /** + * Flag for container values. If set inside a value then the value is a + * container of other nodes otherwise it is a file. + */ + private static final int VALUE_CONTAINER_FLAG = 0x80000000; + + /** + * Specifier for path node value. If path string length is this then value + * follows. + */ + private static final int VALUE_PATH_STRING_LENGTH = 0xFF; + + private byte version = 0; + private int flags = 0; + private int encodingKeySize = 0; + private int patchKeySize = 0; + private int pathOffset = 0; + private int pathSize = 0; + private int fileReferenceOffset = 0; + private int fileReferenceSize = 0; + private int cascReferenceOffset = 0; + private int cascReferenceSize = 0; + private int maximumPathDepth = 0; + + private int contentsOffsetSize = 0; + private ByteBuffer pathBuffer = null; + private ByteBuffer logicalBuffer = null; + private ByteBuffer storageBuffer = null; + + /** + * The offset into the content buffer is a special type that uses the minimum + * number of bytes to hold the largest offset. Hence non-standard types such as + * 3 bytes long big-endian integer are possible so a special buffer is needed to + * decode these numbers. + */ + private final ByteBuffer contentsOffsetDecoder; + + public TVFSDecoder() { + contentsOffsetDecoder = ByteBuffer.allocate(Integer.BYTES); + } + + public List decodeContainer() throws MalformedCASCStructureException { + return decodeContainer(pathBuffer); + } + + private List decodeContainer(final ByteBuffer pathBuffer) throws MalformedCASCStructureException { + final ArrayList nodes = new ArrayList(); + + while (pathBuffer.hasRemaining()) { + final PathNode node = decodeNode(pathBuffer); + nodes.add(node); + } + + return nodes; + } + + private PathNode decodeNode(final ByteBuffer pathBuffer) throws MalformedCASCStructureException { + final ArrayList pathFragments = new ArrayList(); + + PathNode node; + try { + int pathStringLength; + while ((pathStringLength = Byte.toUnsignedInt(pathBuffer.get())) != VALUE_PATH_STRING_LENGTH) { + final byte[] pathFragment = new byte[pathStringLength]; + pathBuffer.get(pathFragment); + pathFragments.add(pathFragment); + } + + final int value = pathBuffer.getInt(); + if ((value & VALUE_CONTAINER_FLAG) != 0) { + // prefix node + final int containerSize = value & ~VALUE_CONTAINER_FLAG; + pathBuffer.position(pathBuffer.position() - Integer.BYTES); + + if (containerSize > pathBuffer.remaining()) { + throw new MalformedCASCStructureException("prefix node container extends beyond path container"); + } + + pathBuffer.limit(pathBuffer.position() + containerSize); + + final ByteBuffer containerBuffer = pathBuffer.slice(); + + pathBuffer.position(pathBuffer.limit()); + pathBuffer.limit(pathBuffer.capacity()); + + containerBuffer.position(Integer.BYTES); + + final List nodes = decodeContainer(containerBuffer); + + node = new PrefixNode(pathFragments, nodes); + } else { + // file value + final StorageReference[] fileReferences = getFileReferences(value); + + node = new FileNode(pathFragments, Arrays.asList(fileReferences)); + } + } catch (final BufferUnderflowException e) { + throw new MalformedCASCStructureException("path stream goes beyond path container"); + } + + return node; + } + + private StorageReference[] getFileReferences(final int fileOffset) throws MalformedCASCStructureException { + if (fileOffset > logicalBuffer.limit()) { + throw new MalformedCASCStructureException("logical offset beyond file reference chunk"); + } + logicalBuffer.position(fileOffset); + + StorageReference[] references; + + try { + final int referenceCount = Byte.toUnsignedInt(logicalBuffer.get()); + references = new StorageReference[referenceCount]; + for (int i = 0; i < referenceCount; i += 1) { + final long offset = Integer.toUnsignedLong(logicalBuffer.getInt()); + final long size = Integer.toUnsignedLong(logicalBuffer.getInt()); + + logicalBuffer.get(contentsOffsetDecoder.array(), Integer.BYTES - contentsOffsetSize, + contentsOffsetSize); + final int cascReferenceOffset = contentsOffsetDecoder.getInt(0); + + if (cascReferenceOffset > storageBuffer.limit()) { + throw new MalformedCASCStructureException("storage offset beyond casc reference chunk"); + } + storageBuffer.position(cascReferenceOffset); + + try { + final byte[] encodingKeyDecoder = new byte[encodingKeySize]; + storageBuffer.get(encodingKeyDecoder); + + final int physicalSize = storageBuffer.getInt(); + final int actualSize = storageBuffer.getInt(); + + final StorageReference reference = new StorageReference(offset, size, new Key(encodingKeyDecoder), + physicalSize, actualSize); + references[i] = reference; + } catch (final BufferUnderflowException e) { + throw new MalformedCASCStructureException("storage goes out of bounds"); + } + } + } catch (final BufferUnderflowException e) { + throw new MalformedCASCStructureException("logical reference goes out of bounds"); + } + + return references; + } + + public TVFSFile loadFile(final ByteBuffer fileBuffer) throws IOException { + final ByteBuffer localBuffer = fileBuffer.slice(); + + // check identifier + + if ((localBuffer.remaining() < IDENTIFIER.remaining()) + || !localBuffer.limit(IDENTIFIER.remaining()).equals(IDENTIFIER)) { + throw new MalformedCASCStructureException("missing TVFS identifier"); + } + + // decode header + + localBuffer.limit(localBuffer.capacity()); + localBuffer.position(IDENTIFIER.remaining()); + + try { + version = localBuffer.get(); + if (version != 1) { + throw new UnsupportedOperationException("unsupported TVFS version: " + version); + } + final int headerSize = Byte.toUnsignedInt(localBuffer.get()); + if (headerSize > localBuffer.capacity()) { + throw new MalformedCASCStructureException("TVFS header extends past end of file"); + } + localBuffer.limit(headerSize); + + encodingKeySize = Byte.toUnsignedInt(localBuffer.get()); + patchKeySize = Byte.toUnsignedInt(localBuffer.get()); + flags = localBuffer.getInt(); + + pathOffset = localBuffer.getInt(); + pathSize = localBuffer.getInt(); + if ((Integer.toUnsignedLong(pathOffset) + Integer.toUnsignedLong(pathSize)) > localBuffer.capacity()) { + throw new MalformedCASCStructureException("path stream extends past end of file"); + } + + fileReferenceOffset = localBuffer.getInt(); + fileReferenceSize = localBuffer.getInt(); + if ((Integer.toUnsignedLong(fileReferenceOffset) + Integer.toUnsignedLong(fileReferenceSize)) > localBuffer + .capacity()) { + throw new MalformedCASCStructureException("logical data extends past end of file"); + } + + cascReferenceOffset = localBuffer.getInt(); + cascReferenceSize = localBuffer.getInt(); + if ((Integer.toUnsignedLong(cascReferenceOffset) + Integer.toUnsignedLong(cascReferenceSize)) > localBuffer + .capacity()) { + throw new MalformedCASCStructureException("storage data extends past end of file"); + } + + maximumPathDepth = Short.toUnsignedInt(localBuffer.getShort()); + } catch (final BufferUnderflowException e) { + throw new MalformedCASCStructureException("header goes out of bounds"); + } + + contentsOffsetSize = Math.max(1, Integer.BYTES - (Integer.numberOfLeadingZeros(cascReferenceSize) / Byte.SIZE)); + contentsOffsetDecoder.putInt(0, 0); + + localBuffer.limit(pathOffset + pathSize); + localBuffer.position(pathOffset); + pathBuffer = localBuffer.slice(); + localBuffer.clear(); + + localBuffer.limit(fileReferenceOffset + fileReferenceSize); + localBuffer.position(fileReferenceOffset); + logicalBuffer = localBuffer.slice(); + localBuffer.clear(); + + localBuffer.limit(cascReferenceOffset + cascReferenceSize); + localBuffer.position(cascReferenceOffset); + storageBuffer = localBuffer.slice(); + localBuffer.clear(); + + final List rootNodes = decodeContainer(); + final TVFSFile tvfsFile = new TVFSFile(version, flags, encodingKeySize, patchKeySize, maximumPathDepth, + rootNodes); + + return tvfsFile; + } +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/vfs/TVFSFile.java b/core/src/com/hiveworkshop/blizzard/casc/vfs/TVFSFile.java new file mode 100644 index 0000000..6d76f27 --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/vfs/TVFSFile.java @@ -0,0 +1,54 @@ +package com.hiveworkshop.blizzard.casc.vfs; + +import java.util.List; + +/** + * TVFS file containing file system path nodes. + */ +public class TVFSFile { + private final byte version; + private final int flags; + private final int encodingKeySize; + private final int patchKeySize; + private final int maximumPathDepth; + + private final PathNode[] rootNodes; + + public TVFSFile(final byte version, final int flags, final int encodingKeySize, final int patchKeySize, final int maximumPathDepth, + final List rootNodeList) { + this.version = version; + this.flags = flags; + this.encodingKeySize = encodingKeySize; + this.patchKeySize = patchKeySize; + this.maximumPathDepth = maximumPathDepth; + this.rootNodes = rootNodeList.toArray(new PathNode[0]); + } + + public int getEncodingKeySize() { + return encodingKeySize; + } + + public int getFlags() { + return flags; + } + + public int getMaximumPathDepth() { + return maximumPathDepth; + } + + public int getPatchKeySize() { + return patchKeySize; + } + + public PathNode getRootNode(final int index) { + return rootNodes[index]; + } + + public int getRootNodeCount() { + return rootNodes.length; + } + + public byte getVersion() { + return version; + } +} diff --git a/core/src/com/hiveworkshop/blizzard/casc/vfs/VirtualFileSystem.java b/core/src/com/hiveworkshop/blizzard/casc/vfs/VirtualFileSystem.java new file mode 100644 index 0000000..037162b --- /dev/null +++ b/core/src/com/hiveworkshop/blizzard/casc/vfs/VirtualFileSystem.java @@ -0,0 +1,681 @@ +package com.hiveworkshop.blizzard.casc.vfs; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CodingErrorAction; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; + +import com.hiveworkshop.ReteraCASCUtils; +import com.hiveworkshop.blizzard.casc.Key; +import com.hiveworkshop.blizzard.casc.nio.MalformedCASCStructureException; +import com.hiveworkshop.blizzard.casc.storage.BankStream; +import com.hiveworkshop.blizzard.casc.storage.Storage; + +/** + * High level file system API using TVFS directories to extract files from a + * store. + */ +public final class VirtualFileSystem { + /** + * A result of a file path lookup operation in a TVFS file system. + *

+ * Can be used to fetch the data of a file. + */ + public final class PathResult { + private final PathNode node; + private final byte[][] pathFragments; + + /** + * Internal constructor for path results. + * + * @param node Resolved node. + * @param pathFragments Path of resolved node. + */ + private PathResult(final PathNode node, final byte[][] pathFragments) { + this.node = node; + this.pathFragments = pathFragments; + } + + /** + * Returns true if this file completely exists in storage. + *

+ * The virtual file system structure lists all files, even ones that may not be + * in storage. Only files that are in storage can have their file buffer read. + *

+ * If this result is not a file then it exists in storage as it has no storage + * footprint. + * + * @return True if the file exists in storage. + */ + public boolean existsInStorage() { + boolean exists = true; + + if (isFile()) { + final FileNode fileNode = (FileNode) node; + final int fileReferenceCount = fileNode.getFileReferenceCount(); + for (int fileReferenceIndex = 0; fileReferenceIndex < fileReferenceCount; fileReferenceIndex += 1) { + final StorageReference fileReference = fileNode.getFileReference(fileReferenceIndex); + exists = exists && storage.hasBanks(fileReference.getEncodingKey()); + } + } + + return exists; + } + + /** + * Get the size of the file in bytes. + *

+ * If this result is not a file a value of 0 is returned. + * + * @return File size in bytes. + */ + public long getFileSize() { + long size = 0L; + + if (isFile()) { + final FileNode fileNode = (FileNode) node; + final int fileReferenceCount = fileNode.getFileReferenceCount(); + for (int fileReferenceIndex = 0; fileReferenceIndex < fileReferenceCount; fileReferenceIndex += 1) { + final StorageReference fileReference = fileNode.getFileReference(fileReferenceIndex); + size = Math.max(size, fileReference.getOffset() + fileReference.getSize()); + } + } + + return size; + } + + public String getPath() throws CharacterCodingException { + return convertPathFragments(pathFragments); + } + + public byte[][] getPathFragments() { + return pathFragments; + } + + public boolean isFile() { + return node instanceof FileNode; + } + + /** + * Returns if this path result represents a TVFS file node used by this file + * system. + *

+ * Such nodes logically act as folders in the file path but also contain file + * data used by this file system. Such behaviour may be incompatible with + * standard file systems which do not support both a folder and file at the same + * path. + *

+ * Results that are not files cannot be a TVFS file. + * + * @return If this node is a TVFS file used by this file system. + */ + public boolean isTVFS() { + if (!isFile()) { + return false; + } + + final FileNode fileNode = (FileNode) node; + final StorageReference fileReference = fileNode.getFileReference(0); + return tvfsStorageReferences.containsKey(fileReference.getEncodingKey()); + } + + /** + * Fully read this file into the specified destination buffer. If no buffer is + * specified a new one will be allocated. + *

+ * The specified buffer must have at least getFileSize bytes remaining. + * + * @param destBuffer Buffer to be written to. + * @return Buffer that was written to. + * @throws IOException If an error occurs during reading. + * @throws OutOfMemoryError If no buffer is specified and the file is too big + * for a single buffer. + */ + public ByteBuffer readFile(ByteBuffer destBuffer) throws IOException { + if (!isFile()) { + throw new FileNotFoundException("result is not a file"); + } + + final long fileSize = getFileSize(); + if (fileSize > Integer.MAX_VALUE) { + throw new OutOfMemoryError("file too big to process"); + } + + if (destBuffer == null) { + destBuffer = ByteBuffer.allocate((int) fileSize); + } else if (destBuffer.remaining() < fileSize) { + throw new BufferOverflowException(); + } + + final ByteBuffer fileBuffer = destBuffer.slice(); + + final FileNode fileNode = (FileNode) node; + final int fileReferenceCount = fileNode.getFileReferenceCount(); + for (int fileReferenceIndex = 0; fileReferenceIndex < fileReferenceCount; fileReferenceIndex += 1) { + final StorageReference fileReference = fileNode.getFileReference(fileReferenceIndex); + + final long logicalSize = fileReference.getSize(); + if (logicalSize != fileReference.getActualSize()) { + throw new MalformedCASCStructureException("inconsistent size"); + } + final long logicalOffset = fileReference.getOffset(); + + final BankStream bankStream = storage.getBanks(fileReference.getEncodingKey()); + // TODO test if compressed and logical sizes match stored sizes. + + fileBuffer.limit((int) (logicalOffset + logicalSize)); + fileBuffer.position((int) logicalOffset); + while (bankStream.hasNextBank()) { + bankStream.getBank(fileBuffer); + } + } + + destBuffer.position(destBuffer.position() + (int) fileSize); + return destBuffer; + } + } + + /** + * VFS storage reference key prefix. + */ + public static final String CONFIGURATION_KEY_PREFIX = "vfs-"; + + /** + * Root VFS storage reference. + */ + public static final String ROOT_KEY = "root"; + + /** + * Character encoding used internally by file paths. + */ + public static final Charset PATH_ENCODING = Charset.forName("UTF8"); + + /** + * Path separator used by path strings. + */ + public static final String PATH_SEPERATOR = "\\"; + + /** + * Compares the path fragments of a node with a section of file path fragments. + * This is useful for performing a binary search on a node's children. + *

+ * A return value of 0 does not mean that the node is in the path fragments. + * Only that if it were, it would be this node. This is because the children of + * a node all have unique first fragment sequences so only the first fragment is + * tested. + * + * @param pathFragments Path fragments of a file path. + * @param fragmentIndex Index of fragment where to start comparing at. + * @param fragmentOffset Offset into fragment to start comparing at. + * @param node Node which is being compared. + * @return Similar to standard comparator value (see above). + */ + private static int compareNodePathFragments(final byte[][] pathFragments, final int fragmentIndex, + final int fragmentOffset, final PathNode node) { + final int nodeFragmentCount = node.getPathFragmentCount(); + if (nodeFragmentCount == 0) { + // nodes without fragments have no path fragment presence so always match + return 0; + } + + final byte[] nodeFragment = node.getFragment(0); + final byte[] fragment = pathFragments[fragmentIndex]; + if ((nodeFragment.length == 0) && ((fragment.length - fragmentOffset) > 0)) { + // node with termination fragment are always before all other child nodes + return 1; + } + return ReteraCASCUtils.arraysCompareUnsigned(fragment, fragmentOffset, + Math.min(fragmentOffset + nodeFragment.length, fragment.length), nodeFragment, 0, nodeFragment.length); + } + + /** + * Convert a path string into path fragments for resolution in the VFS. + * + * @param filePath Path string to convert. + * @return Path fragments. + * @throws CharacterCodingException If the path string cannot be encoded into + * fragments. + */ + public static byte[][] convertFilePath(final String filePath) throws CharacterCodingException { + final String[] fragmentStrings = filePath.toLowerCase(Locale.ROOT).split("\\" + PATH_SEPERATOR); + final byte[][] pathFragments = new byte[fragmentStrings.length][]; + + final CharsetEncoder encoder = PATH_ENCODING.newEncoder(); + encoder.onMalformedInput(CodingErrorAction.REPORT); + encoder.onUnmappableCharacter(CodingErrorAction.REPORT); + for (int index = 0; index < fragmentStrings.length; index += 1) { + final ByteBuffer fragmentBuffer = encoder.encode(CharBuffer.wrap(fragmentStrings[index])); + if (fragmentBuffer.hasArray() && (fragmentBuffer.limit() == fragmentBuffer.capacity()) + && (fragmentBuffer.position() == 0)) { + // can use underlying array + pathFragments[index] = fragmentBuffer.array(); + } else { + // copy into array + final byte[] pathFragment = new byte[fragmentBuffer.remaining()]; + fragmentBuffer.get(pathFragment); + pathFragments[index] = pathFragment; + } + } + + return pathFragments; + } + + /** + * Convert path fragments used internally by VFS into a path string. + * + * @param pathFragments Path fragments to convert. + * @return Path string. + * @throws CharacterCodingException If the path fragments cannot be decoded into + * a valid String. + */ + public static String convertPathFragments(final byte[][] pathFragments) throws CharacterCodingException { + final String[] fragmentStrings = new String[pathFragments.length]; + + final CharsetDecoder decoder = PATH_ENCODING.newDecoder(); + decoder.onMalformedInput(CodingErrorAction.REPORT); + decoder.onUnmappableCharacter(CodingErrorAction.REPORT); + + for (int index = 0; index < fragmentStrings.length; index += 1) { + fragmentStrings[index] = decoder.decode(ByteBuffer.wrap(pathFragments[index])).toString(); + } + + return String.join(PATH_SEPERATOR, fragmentStrings); + } + + /** + * Test the path fragments of a node form a section of file path fragments. + * + * @param pathFragments Path fragments of a file path. + * @param fragmentIndex Index of fragment where to start testing at. + * @param fragmentOffset Offset into fragment to start testing at. + * @param node Node which is being tested. + * @return True if the node is contained in the path fragments, otherwise false. + */ + private static boolean equalNodePathFragments(final byte[][] pathFragments, final int fragmentIndex, + int fragmentOffset, final PathNode node) { + final int nodeFragmentCount = node.getPathFragmentCount(); + if (nodeFragmentCount == 0) { + // nodes without fragments have no path fragment presence so always match + return true; + } + + if ((nodeFragmentCount == 1) && (node.getFragment(0).length == 0)) { + // node with termination fragment + return fragmentOffset == pathFragments[fragmentIndex].length; + } else if (pathFragments.length < (fragmentIndex + nodeFragmentCount)) { + // fragment too short + return false; + } + + boolean result = true; + int nodeFragmentIndex = 0; + while (result && (nodeFragmentIndex < nodeFragmentCount)) { + final byte[] fragment = pathFragments[fragmentIndex + nodeFragmentIndex]; + final byte[] nodeFragment = node.getFragment(nodeFragmentIndex); + result = result && ReteraCASCUtils.arraysEquals(fragment, fragmentOffset, + Math.min(fragmentOffset + nodeFragment.length, fragment.length), nodeFragment, 0, + nodeFragment.length); + fragmentOffset = 0; + nodeFragmentIndex += 1; + } + + return result; + } + + /** + * Local CASC storage. Used to retrieve file data. + */ + private final Storage storage; + + /** + * Decoder used to load TVFS files in the TVFS tree. + */ + private final TVFSDecoder decoder = new TVFSDecoder(); + + /** + * TVFS file containing the root directory for the file system. + */ + private final TVFSFile tvfsRoot; + + /** + * TVFS file cache. Holds all loaded TVFS files for this file system. This + * allows the TVFS files to be loaded lazily which could potentially reduce + * loading times and memory usage when only some branches of the TVFS file tree + * are accessed. + */ + private final TreeMap tvfsCache = new TreeMap<>(); + + /** + * Map of all TVFS files used by the TVFS file tree. Keys that are not in this + * map are treated as leaf files rather than a nested TVFS file. + */ + private final TreeMap tvfsStorageReferences = new TreeMap<>(); + + /** + * Construct a TVFS file system from a CASC local storage and build + * configuration. + * + * @param storage CASC local storage to source files from. + * @param buildConfiguration Build configuration of CASC archive. + * @throws IOException If an exception occurs when loading the file system. + */ + public VirtualFileSystem(final Storage storage, final Map buildConfiguration) throws IOException { + this.storage = storage; + + int vfsNumber = 0; + String configurationKey; + while (buildConfiguration + .containsKey(configurationKey = CONFIGURATION_KEY_PREFIX + Integer.toUnsignedString(++vfsNumber))) { + final com.hiveworkshop.blizzard.casc.StorageReference storageReference = new com.hiveworkshop.blizzard.casc.StorageReference( + configurationKey, buildConfiguration); + tvfsStorageReferences.put(storageReference.getEncodingKey(), storageReference); + } + + final com.hiveworkshop.blizzard.casc.StorageReference rootReference = new com.hiveworkshop.blizzard.casc.StorageReference( + CONFIGURATION_KEY_PREFIX + ROOT_KEY, buildConfiguration); + final ByteBuffer rootBuffer = fetchStoredBuffer(rootReference); + tvfsRoot = decoder.loadFile(rootBuffer); + + tvfsCache.put(rootReference.getEncodingKey(), tvfsRoot); + } + + /** + * Resolves a TVFS storage reference into a data buffer from the local storage. + * + * @param storageReference TVFS storage reference. + * @return Data buffer containing refered content. + * @throws IOException If an exception occurs when fetching the data buffer. + */ + private ByteBuffer fetchStoredBuffer(final com.hiveworkshop.blizzard.casc.StorageReference storageReference) + throws IOException { + final long size = storageReference.getSize(); + if (size > Integer.MAX_VALUE) { + throw new MalformedCASCStructureException("stored data too large to process"); + } + + final BankStream bankStream = storage.getBanks(storageReference.getEncodingKey()); + final ByteBuffer storedBuffer = ByteBuffer.allocate((int) size); + try { + while (bankStream.hasNextBank()) { + bankStream.getBank(storedBuffer); + } + } catch (final BufferOverflowException e) { + throw new MalformedCASCStructureException("stored data is bigger than expected"); + } + + if (storedBuffer.hasRemaining()) { + throw new MalformedCASCStructureException("stored data is smaller than expected"); + } + + storedBuffer.rewind(); + return storedBuffer; + } + + /** + * Method to get all files in the file system. + * + * @return List of file path results for every file in the file system. + * @throws IOException If an exception is thrown when loading a TVFS file or + * decoding path fragments into a path string. + */ + public List getAllFiles() throws IOException { + final ArrayList pathStringList = new ArrayList(); + + final int rootCount = tvfsRoot.getRootNodeCount(); + for (int rootIndex = 0; rootIndex < rootCount; rootIndex += 1) { + final PathNode root = tvfsRoot.getRootNode(rootIndex); + recursiveFilePathRetrieve(new byte[1][0], pathStringList, root); + } + + return pathStringList; + } + + /** + * Recursive function to traverse the TVFS tree and resolve all files in the + * file system. + * + * @param parentPathFragments Path fragments of parent node. + * @param resultList Result list. + * @param currentNode The child node to process. + * @throws IOException If an exception occurs when processing the node. + */ + private void recursiveFilePathRetrieve(final byte[][] parentPathFragments, final ArrayList resultList, + final PathNode currentNode) throws IOException { + byte[][] currentPathFragments = parentPathFragments; + + // process path fragments + final int fragmentCount = currentNode.getPathFragmentCount(); + if (fragmentCount > 0) { + int fragmentIndex = 0; + final byte[] fragment = currentNode.getFragment(fragmentIndex++); + + // expand path fragment array + int basePathFragmentsIndex = currentPathFragments.length; + if ((fragmentCount > 1) || (fragment.length > 0)) { + // first fragment of the node gets merged with last path fragment + basePathFragmentsIndex -= 1; + } + currentPathFragments = Arrays.copyOf(currentPathFragments, basePathFragmentsIndex + fragmentCount); + + // merge fragment + final byte[] sourceFragment = currentPathFragments[basePathFragmentsIndex]; + byte[] joinedFragment = fragment; + if (sourceFragment != null) { + joinedFragment = sourceFragment; + if (fragment.length != 0) { + final int joinOffset = sourceFragment.length; + joinedFragment = Arrays.copyOf(sourceFragment, joinOffset + fragment.length); + System.arraycopy(fragment, 0, joinedFragment, joinOffset, fragment.length); + } + } + + // append path fragments + currentPathFragments[basePathFragmentsIndex] = joinedFragment; + for (; fragmentIndex < fragmentCount; fragmentIndex += 1) { + currentPathFragments[basePathFragmentsIndex + fragmentIndex] = currentNode.getFragment(fragmentIndex); + } + } + + if (currentNode instanceof PrefixNode) { + final PrefixNode prefixNode = (PrefixNode) currentNode; + + final int childCount = prefixNode.getNodeCount(); + for (int index = 0; index < childCount; index += 1) { + recursiveFilePathRetrieve(currentPathFragments, resultList, prefixNode.getNode(index)); + } + } else if (currentNode instanceof FileNode) { + final FileNode fileNode = (FileNode) currentNode; + + final int fileReferenceCount = fileNode.getFileReferenceCount(); + if (fileReferenceCount == 1) { + // check if nested VFS + final Key encodingKey = fileNode.getFileReference(0).getEncodingKey(); + final TVFSFile tvfsFile = resolveTVFS(encodingKey); + + if (tvfsFile != null) { + // file is also a folder + final byte[][] folderPathFragments = Arrays.copyOf(currentPathFragments, + currentPathFragments.length + 1); + folderPathFragments[currentPathFragments.length] = new byte[0]; + + final int rootCount = tvfsFile.getRootNodeCount(); + for (int rootIndex = 0; rootIndex < rootCount; rootIndex += 1) { + final PathNode root = tvfsFile.getRootNode(rootIndex); + recursiveFilePathRetrieve(folderPathFragments, resultList, root); + } + } + + resultList.add(new PathResult(currentNode, currentPathFragments)); + } + } else { + throw new IllegalArgumentException("unsupported node type"); + } + } + + /** + * Recursive function to resolve a file node in a TVFS tree from path fragments + * representing a file system file path. + * + * @param pathFragments Path fragments of a file path. + * @param fragmentIndex Index of fragment where currently testing. + * @param fragmentOffset Offset into fragment where currently testing. + * @param node Node which is being tested. + * @return Resolved file node. + * @throws IOException If an exception occurs when testing the node. + */ + private FileNode recursiveResolvePathFragments(final byte[][] pathFragments, int fragmentIndex, int fragmentOffset, + final PathNode node) throws IOException { + if (!equalNodePathFragments(pathFragments, fragmentIndex, fragmentOffset, node)) { + // node not on path + return null; + } + + // advance fragment position + final int nodeFragmentCount = node.getPathFragmentCount(); + if (nodeFragmentCount == 1) { + final byte[] nodeFragment = node.getFragment(0); + if (nodeFragment.length == 0) { + // node with termination fragment + fragmentIndex += 1; + fragmentOffset = 0; + } else { + // node with less than a whole fragment + fragmentOffset += nodeFragment.length; + } + + } else if (nodeFragmentCount > 1) { + // node which completes 1 or more fragments. + fragmentIndex += nodeFragmentCount - 1; + fragmentOffset = node.getFragment(nodeFragmentCount - 1).length; + } + + // process node + if (node instanceof PrefixNode) { + // apply binary search to prefix node to find next node + final PrefixNode prefixNode = (PrefixNode) node; + final int childCount = prefixNode.getNodeCount(); + + int low = 0; + int high = childCount - 1; + while (low <= high) { + final int middle = (low + high) / 2; + final PathNode searchNode = prefixNode.getNode(middle); + final int result = compareNodePathFragments(pathFragments, fragmentIndex, fragmentOffset, searchNode); + + if (result == 0) { + // possible match + return recursiveResolvePathFragments(pathFragments, fragmentIndex, fragmentOffset, searchNode); + } else if (result < 0) { + high = middle - 1; + } else { + low = middle + 1; + } + } + + } else if (node instanceof FileNode) { + final FileNode fileNode = (FileNode) node; + + if ((fragmentIndex == (pathFragments.length - 1)) + && (fragmentOffset == pathFragments[pathFragments.length - 1].length)) { + // file found + return fileNode; + } else if (fragmentOffset == pathFragments[fragmentIndex].length) { + // nested TVFS file + final int fileReferenceCount = fileNode.getFileReferenceCount(); + if (fileReferenceCount == 1) { + // check if nested VFS + final Key encodingKey = fileNode.getFileReference(0).getEncodingKey(); + final TVFSFile tvfsFile = resolveTVFS(encodingKey); + + if (tvfsFile != null) { + // TVFS file to recursively resolve + if (tvfsFile.getRootNodeCount() != 1) { + throw new MalformedCASCStructureException("logic only defined for 1 TVFS root node"); + } + + fragmentIndex += 1; + fragmentOffset = 0; + return recursiveResolvePathFragments(pathFragments, fragmentIndex, fragmentOffset, + tvfsFile.getRootNode(0)); + } + } + } + + } else { + throw new IllegalArgumentException("unsupported node type"); + } + + // file not found + return null; + } + + /** + * Resolves a file from the specified path fragments representing a file system + * file path. + * + * @param pathFragments File path fragments. + * @return Path result for a file. + * @throws FileNotFoundException If the file does not exist in the file system. + * @throws IOException If an exception occurs when resolving the path + * fragments. + * + */ + public PathResult resolvePath(final byte[][] pathFragments) throws IOException { + if (pathFragments.length == 0) { + throw new IllegalArgumentException("pathFragments.length must be greater than 0"); + } + + if (tvfsRoot.getRootNodeCount() != 1) { + throw new MalformedCASCStructureException("logic only defined for 1 root node"); + } + + final FileNode result = recursiveResolvePathFragments(pathFragments, 0, 0, tvfsRoot.getRootNode(0)); + if (result == null) { + throw new FileNotFoundException("path not in storage"); + } + + return new PathResult(result, pathFragments); + } + + /** + * Resolves a TVFS file from an encoding key. The key is checked if it is a TVFS + * file in this file system and then resolved in local storage. The resulting + * file is then decoded as a TVFS file and returned. Decoded TVFS files are + * cached for improved performance. This method can be called concurrently. + * + * @param encodingKey Encoding key of TVFS file to resolve. + * @return The resolved TVFS file, or null if the encoding key is not for a TVFS + * file of this file system. + * @throws IOException If an error occurs when resolving the TVFS file. + */ + private TVFSFile resolveTVFS(final Key encodingKey) throws IOException { + TVFSFile tvfsFile = null; + final com.hiveworkshop.blizzard.casc.StorageReference storageReference = tvfsStorageReferences.get(encodingKey); + if (storageReference != null) { + // is a TVFS file of this file system + synchronized (this) { + tvfsFile = tvfsCache.get(encodingKey); + if (tvfsFile == null) { + // decode TVFS from storage + final ByteBuffer rootBuffer = fetchStoredBuffer(storageReference); + tvfsFile = decoder.loadFile(rootBuffer); + + tvfsCache.put(storageReference.getEncodingKey(), tvfsFile); + } + } + } + return tvfsFile; + } +} diff --git a/core/src/com/hiveworkshop/json/JSONArray.java b/core/src/com/hiveworkshop/json/JSONArray.java new file mode 100644 index 0000000..906d7b3 --- /dev/null +++ b/core/src/com/hiveworkshop/json/JSONArray.java @@ -0,0 +1,1458 @@ +package com.hiveworkshop.json; + +/* + Copyright (c) 2002 JSON.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + The Software shall be used for Good, not Evil. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + + +/** + * A JSONArray is an ordered sequence of values. Its external text form is a + * string wrapped in square brackets with commas separating the values. The + * internal form is an object having get and opt + * methods for accessing the values by index, and put methods for + * adding or replacing values. The values can be any of these types: + * Boolean, JSONArray, JSONObject, + * Number, String, or the + * JSONObject.NULL object. + *

+ * The constructor can convert a JSON text into a Java object. The + * toString method converts to JSON text. + *

+ * A get method returns a value if one can be found, and throws an + * exception if one cannot be found. An opt method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + *

+ * The generic get() and opt() methods return an + * object which you can cast or query for type. There are also typed + * get and opt methods that do type checking and type + * coercion for you. + *

+ * The texts produced by the toString methods strictly conform to + * JSON syntax rules. The constructors are more forgiving in the texts they will + * accept: + *

    + *
  • An extra , (comma) may appear just + * before the closing bracket.
  • + *
  • The null value will be inserted when there is , + *  (comma) elision.
  • + *
  • Strings may be quoted with ' (single + * quote).
  • + *
  • Strings do not need to be quoted at all if they do not begin with a quote + * or single quote, and if they do not contain leading or trailing spaces, and + * if they do not contain any of these characters: + * { } [ ] / \ : , # and if they do not look like numbers and + * if they are not the reserved words true, false, or + * null.
  • + *
+ * + * @author JSON.org + * @version 2016-08/15 + */ +public class JSONArray implements Iterable { + + /** + * The arrayList where the JSONArray's properties are kept. + */ + private final ArrayList myArrayList; + + /** + * Construct an empty JSONArray. + */ + public JSONArray() { + this.myArrayList = new ArrayList(); + } + + /** + * Construct a JSONArray from a JSONTokener. + * + * @param x + * A JSONTokener + * @throws JSONException + * If there is a syntax error. + */ + public JSONArray(JSONTokener x) throws JSONException { + this(); + if (x.nextClean() != '[') { + throw x.syntaxError("A JSONArray text must start with '['"); + } + + char nextChar = x.nextClean(); + if (nextChar == 0) { + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + } + if (nextChar != ']') { + x.back(); + for (;;) { + if (x.nextClean() == ',') { + x.back(); + this.myArrayList.add(JSONObject.NULL); + } else { + x.back(); + this.myArrayList.add(x.nextValue()); + } + switch (x.nextClean()) { + case 0: + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + case ',': + nextChar = x.nextClean(); + if (nextChar == 0) { + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + } + if (nextChar == ']') { + return; + } + x.back(); + break; + case ']': + return; + default: + throw x.syntaxError("Expected a ',' or ']'"); + } + } + } + } + + /** + * Construct a JSONArray from a source JSON text. + * + * @param source + * A string that begins with [ (left + * bracket) and ends with ] + *  (right bracket). + * @throws JSONException + * If there is a syntax error. + */ + public JSONArray(String source) throws JSONException { + this(new JSONTokener(source)); + } + + /** + * Construct a JSONArray from a Collection. + * + * @param collection + * A Collection. + */ + public JSONArray(Collection collection) { + if (collection == null) { + this.myArrayList = new ArrayList(); + } else { + this.myArrayList = new ArrayList(collection.size()); + for (Object o: collection){ + this.myArrayList.add(JSONObject.wrap(o)); + } + } + } + + /** + * Construct a JSONArray from an array. + * + * @param array + * Array. If the parameter passed is null, or not an array, an + * exception will be thrown. + * + * @throws JSONException + * If not an array or if an array value is non-finite number. + * @throws NullPointerException + * Thrown if the array parameter is null. + */ + public JSONArray(Object array) throws JSONException { + this(); + if (array.getClass().isArray()) { + int length = Array.getLength(array); + this.myArrayList.ensureCapacity(length); + for (int i = 0; i < length; i += 1) { + this.put(JSONObject.wrap(Array.get(array, i))); + } + } else { + throw new JSONException( + "JSONArray initial value should be a string or collection or array."); + } + } + + @Override + public Iterator iterator() { + return this.myArrayList.iterator(); + } + + /** + * Get the object value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return An object value. + * @throws JSONException + * If there is no value for the index. + */ + public Object get(int index) throws JSONException { + Object object = this.opt(index); + if (object == null) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + return object; + } + + /** + * Get the boolean value associated with an index. The string values "true" + * and "false" are converted to boolean. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The truth. + * @throws JSONException + * If there is no value for the index or if the value is not + * convertible to boolean. + */ + public boolean getBoolean(int index) throws JSONException { + Object object = this.get(index); + if (object.equals(Boolean.FALSE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("true"))) { + return true; + } + throw new JSONException("JSONArray[" + index + "] is not a boolean."); + } + + /** + * Get the double value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value cannot be converted + * to a number. + */ + public double getDouble(int index) throws JSONException { + return this.getNumber(index).doubleValue(); + } + + /** + * Get the float value associated with a key. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public float getFloat(int index) throws JSONException { + return this.getNumber(index).floatValue(); + } + + /** + * Get the Number value associated with a key. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public Number getNumber(int index) throws JSONException { + Object object = this.get(index); + try { + if (object instanceof Number) { + return (Number)object; + } + return JSONObject.stringToNumber(object.toString()); + } catch (Exception e) { + throw new JSONException("JSONArray[" + index + "] is not a number.", e); + } + } + + /** + * Get the enum value associated with an index. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param index + * The index must be between 0 and length() - 1. + * @return The enum value at the index location + * @throws JSONException + * if the key is not found or if the value cannot be converted + * to an enum. + */ + public > E getEnum(Class clazz, int index) throws JSONException { + E val = optEnum(clazz, index); + if(val==null) { + // JSONException should really take a throwable argument. + // If it did, I would re-implement this with the Enum.valueOf + // method and place any thrown exception in the JSONException + throw new JSONException("JSONArray[" + index + "] is not an enum of type " + + JSONObject.quote(clazz.getSimpleName()) + "."); + } + return val; + } + + /** + * Get the BigDecimal value associated with an index. If the value is float + * or double, the the {@link BigDecimal#BigDecimal(double)} constructor + * will be used. See notes on the constructor for conversion issues that + * may arise. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value cannot be converted + * to a BigDecimal. + */ + public BigDecimal getBigDecimal (int index) throws JSONException { + Object object = this.get(index); + BigDecimal val = JSONObject.objectToBigDecimal(object, null); + if(val == null) { + throw new JSONException("JSONArray[" + index + + "] could not convert to BigDecimal ("+ object + ")."); + } + return val; + } + + /** + * Get the BigInteger value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value cannot be converted + * to a BigInteger. + */ + public BigInteger getBigInteger (int index) throws JSONException { + Object object = this.get(index); + BigInteger val = JSONObject.objectToBigInteger(object, null); + if(val == null) { + throw new JSONException("JSONArray[" + index + + "] could not convert to BigDecimal ("+ object + ")."); + } + return val; + } + + /** + * Get the int value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value is not a number. + */ + public int getInt(int index) throws JSONException { + return this.getNumber(index).intValue(); + } + + /** + * Get the JSONArray associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return A JSONArray value. + * @throws JSONException + * If there is no value for the index. or if the value is not a + * JSONArray + */ + public JSONArray getJSONArray(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + throw new JSONException("JSONArray[" + index + "] is not a JSONArray."); + } + + /** + * Get the JSONObject associated with an index. + * + * @param index + * subscript + * @return A JSONObject value. + * @throws JSONException + * If there is no value for the index or if the value is not a + * JSONObject + */ + public JSONObject getJSONObject(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + throw new JSONException("JSONArray[" + index + "] is not a JSONObject."); + } + + /** + * Get the long value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException + * If the key is not found or if the value cannot be converted + * to a number. + */ + public long getLong(int index) throws JSONException { + return this.getNumber(index).longValue(); + } + + /** + * Get the string associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @return A string value. + * @throws JSONException + * If there is no string value for the index. + */ + public String getString(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof String) { + return (String) object; + } + throw new JSONException("JSONArray[" + index + "] not a string."); + } + + /** + * Determine if the value is null. + * + * @param index + * The index must be between 0 and length() - 1. + * @return true if the value at the index is null, or if there is no value. + */ + public boolean isNull(int index) { + return JSONObject.NULL.equals(this.opt(index)); + } + + /** + * Make a string from the contents of this JSONArray. The + * separator string is inserted between each element. Warning: + * This method assumes that the data structure is acyclical. + * + * @param separator + * A string that will be inserted between the elements. + * @return a string. + * @throws JSONException + * If the array contains an invalid number. + */ + public String join(String separator) throws JSONException { + int len = this.length(); + if (len == 0) { + return ""; + } + + StringBuilder sb = new StringBuilder( + JSONObject.valueToString(this.myArrayList.get(0))); + + for (int i = 1; i < len; i++) { + sb.append(separator) + .append(JSONObject.valueToString(this.myArrayList.get(i))); + } + return sb.toString(); + } + + /** + * Get the number of elements in the JSONArray, included nulls. + * + * @return The length (or size). + */ + public int length() { + return this.myArrayList.size(); + } + + /** + * Get the optional object value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. If not, null is returned. + * @return An object value, or null if there is no object at that index. + */ + public Object opt(int index) { + return (index < 0 || index >= this.length()) ? null : this.myArrayList + .get(index); + } + + /** + * Get the optional boolean value associated with an index. It returns false + * if there is no value at that index, or if the value is not Boolean.TRUE + * or the String "true". + * + * @param index + * The index must be between 0 and length() - 1. + * @return The truth. + */ + public boolean optBoolean(int index) { + return this.optBoolean(index, false); + } + + /** + * Get the optional boolean value associated with an index. It returns the + * defaultValue if there is no value at that index or if it is not a Boolean + * or the String "true" or "false" (case insensitive). + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * A boolean default. + * @return The truth. + */ + public boolean optBoolean(int index, boolean defaultValue) { + try { + return this.getBoolean(index); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get the optional double value associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + */ + public double optDouble(int index) { + return this.optDouble(index, Double.NaN); + } + + /** + * Get the optional double value associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * subscript + * @param defaultValue + * The default value. + * @return The value. + */ + public double optDouble(int index, double defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + final double doubleValue = val.doubleValue(); + // if (Double.isNaN(doubleValue) || Double.isInfinite(doubleValue)) { + // return defaultValue; + // } + return doubleValue; + } + + /** + * Get the optional float value associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + */ + public float optFloat(int index) { + return this.optFloat(index, Float.NaN); + } + + /** + * Get the optional float value associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * subscript + * @param defaultValue + * The default value. + * @return The value. + */ + public float optFloat(int index, float defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + final float floatValue = val.floatValue(); + // if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) { + // return floatValue; + // } + return floatValue; + } + + /** + * Get the optional int value associated with an index. Zero is returned if + * there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + */ + public int optInt(int index) { + return this.optInt(index, 0); + } + + /** + * Get the optional int value associated with an index. The defaultValue is + * returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return The value. + */ + public int optInt(int index, int defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + return val.intValue(); + } + + /** + * Get the enum value associated with a key. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param index + * The index must be between 0 and length() - 1. + * @return The enum value at the index location or null if not found + */ + public > E optEnum(Class clazz, int index) { + return this.optEnum(clazz, index, null); + } + + /** + * Get the enum value associated with a key. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default in case the value is not found + * @return The enum value at the index location or defaultValue if + * the value is not found or cannot be assigned to clazz + */ + public > E optEnum(Class clazz, int index, E defaultValue) { + try { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { + return defaultValue; + } + if (clazz.isAssignableFrom(val.getClass())) { + // we just checked it! + @SuppressWarnings("unchecked") + E myE = (E) val; + return myE; + } + return Enum.valueOf(clazz, val.toString()); + } catch (IllegalArgumentException e) { + return defaultValue; + } catch (NullPointerException e) { + return defaultValue; + } + } + + /** + * Get the optional BigInteger value associated with an index. The + * defaultValue is returned if there is no value for the index, or if the + * value is not a number and cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return The value. + */ + public BigInteger optBigInteger(int index, BigInteger defaultValue) { + Object val = this.opt(index); + return JSONObject.objectToBigInteger(val, defaultValue); + } + + /** + * Get the optional BigDecimal value associated with an index. The + * defaultValue is returned if there is no value for the index, or if the + * value is not a number and cannot be converted to a number. If the value + * is float or double, the the {@link BigDecimal#BigDecimal(double)} + * constructor will be used. See notes on the constructor for conversion + * issues that may arise. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return The value. + */ + public BigDecimal optBigDecimal(int index, BigDecimal defaultValue) { + Object val = this.opt(index); + return JSONObject.objectToBigDecimal(val, defaultValue); + } + + /** + * Get the optional JSONArray associated with an index. + * + * @param index + * subscript + * @return A JSONArray value, or null if the index has no value, or if the + * value is not a JSONArray. + */ + public JSONArray optJSONArray(int index) { + Object o = this.opt(index); + return o instanceof JSONArray ? (JSONArray) o : null; + } + + /** + * Get the optional JSONObject associated with an index. Null is returned if + * the key is not found, or null if the index has no value, or if the value + * is not a JSONObject. + * + * @param index + * The index must be between 0 and length() - 1. + * @return A JSONObject value. + */ + public JSONObject optJSONObject(int index) { + Object o = this.opt(index); + return o instanceof JSONObject ? (JSONObject) o : null; + } + + /** + * Get the optional long value associated with an index. Zero is returned if + * there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + */ + public long optLong(int index) { + return this.optLong(index, 0); + } + + /** + * Get the optional long value associated with an index. The defaultValue is + * returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return The value. + */ + public long optLong(int index, long defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + return val.longValue(); + } + + /** + * Get an optional {@link Number} value associated with a key, or null + * if there is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number ({@link BigDecimal}). This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param index + * The index must be between 0 and length() - 1. + * @return An object which is the value. + */ + public Number optNumber(int index) { + return this.optNumber(index, null); + } + + /** + * Get an optional {@link Number} value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number ({@link BigDecimal}). This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public Number optNumber(int index, Number defaultValue) { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number){ + return (Number) val; + } + + if (val instanceof String) { + try { + return JSONObject.stringToNumber((String) val); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } + + /** + * Get the optional string value associated with an index. It returns an + * empty string if there is no value at that index. If the value is not a + * string and is not null, then it is converted to a string. + * + * @param index + * The index must be between 0 and length() - 1. + * @return A String value. + */ + public String optString(int index) { + return this.optString(index, ""); + } + + /** + * Get the optional string associated with an index. The defaultValue is + * returned if the key is not found. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @return A String value. + */ + public String optString(int index, String defaultValue) { + Object object = this.opt(index); + return JSONObject.NULL.equals(object) ? defaultValue : object + .toString(); + } + + /** + * Append a boolean value. This increases the array's length by one. + * + * @param value + * A boolean value. + * @return this. + */ + public JSONArray put(boolean value) { + return this.put(value ? Boolean.TRUE : Boolean.FALSE); + } + + /** + * Put a value in the JSONArray, where the value will be a JSONArray which + * is produced from a Collection. + * + * @param value + * A Collection value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + */ + public JSONArray put(Collection value) { + return this.put(new JSONArray(value)); + } + + /** + * Append a double value. This increases the array's length by one. + * + * @param value + * A double value. + * @return this. + * @throws JSONException + * if the value is not finite. + */ + public JSONArray put(double value) throws JSONException { + return this.put(Double.valueOf(value)); + } + + /** + * Append a float value. This increases the array's length by one. + * + * @param value + * A float value. + * @return this. + * @throws JSONException + * if the value is not finite. + */ + public JSONArray put(float value) throws JSONException { + return this.put(Float.valueOf(value)); + } + + /** + * Append an int value. This increases the array's length by one. + * + * @param value + * An int value. + * @return this. + */ + public JSONArray put(int value) { + return this.put(Integer.valueOf(value)); + } + + /** + * Append an long value. This increases the array's length by one. + * + * @param value + * A long value. + * @return this. + */ + public JSONArray put(long value) { + return this.put(Long.valueOf(value)); + } + + /** + * Put a value in the JSONArray, where the value will be a JSONObject which + * is produced from a Map. + * + * @param value + * A Map value. + * @return this. + * @throws JSONException + * If a value in the map is non-finite number. + * @throws NullPointerException + * If a key in the map is null + */ + public JSONArray put(Map value) { + return this.put(new JSONObject(value)); + } + + /** + * Append an object value. This increases the array's length by one. + * + * @param value + * An object value. The value should be a Boolean, Double, + * Integer, JSONArray, JSONObject, Long, or String, or the + * JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the value is non-finite number. + */ + public JSONArray put(Object value) { + JSONObject.testValidity(value); + this.myArrayList.add(value); + return this; + } + + /** + * Put or replace a boolean value in the JSONArray. If the index is greater + * than the length of the JSONArray, then null elements will be added as + * necessary to pad it out. + * + * @param index + * The subscript. + * @param value + * A boolean value. + * @return this. + * @throws JSONException + * If the index is negative. + */ + public JSONArray put(int index, boolean value) throws JSONException { + return this.put(index, value ? Boolean.TRUE : Boolean.FALSE); + } + + /** + * Put a value in the JSONArray, where the value will be a JSONArray which + * is produced from a Collection. + * + * @param index + * The subscript. + * @param value + * A Collection value. + * @return this. + * @throws JSONException + * If the index is negative or if the value is non-finite. + */ + public JSONArray put(int index, Collection value) throws JSONException { + return this.put(index, new JSONArray(value)); + } + + /** + * Put or replace a double value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index + * The subscript. + * @param value + * A double value. + * @return this. + * @throws JSONException + * If the index is negative or if the value is non-finite. + */ + public JSONArray put(int index, double value) throws JSONException { + return this.put(index, Double.valueOf(value)); + } + + /** + * Put or replace a float value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index + * The subscript. + * @param value + * A float value. + * @return this. + * @throws JSONException + * If the index is negative or if the value is non-finite. + */ + public JSONArray put(int index, float value) throws JSONException { + return this.put(index, Float.valueOf(value)); + } + + /** + * Put or replace an int value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index + * The subscript. + * @param value + * An int value. + * @return this. + * @throws JSONException + * If the index is negative. + */ + public JSONArray put(int index, int value) throws JSONException { + return this.put(index, Integer.valueOf(value)); + } + + /** + * Put or replace a long value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index + * The subscript. + * @param value + * A long value. + * @return this. + * @throws JSONException + * If the index is negative. + */ + public JSONArray put(int index, long value) throws JSONException { + return this.put(index, Long.valueOf(value)); + } + + /** + * Put a value in the JSONArray, where the value will be a JSONObject that + * is produced from a Map. + * + * @param index + * The subscript. + * @param value + * The Map value. + * @return this. + * @throws JSONException + * If the index is negative or if the the value is an invalid + * number. + * @throws NullPointerException + * If a key in the map is null + */ + public JSONArray put(int index, Map value) throws JSONException { + this.put(index, new JSONObject(value)); + return this; + } + + /** + * Put or replace an object value in the JSONArray. If the index is greater + * than the length of the JSONArray, then null elements will be added as + * necessary to pad it out. + * + * @param index + * The subscript. + * @param value + * The value to put into the array. The value should be a + * Boolean, Double, Integer, JSONArray, JSONObject, Long, or + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the index is negative or if the the value is an invalid + * number. + */ + public JSONArray put(int index, Object value) throws JSONException { + if (index < 0) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + if (index < this.length()) { + JSONObject.testValidity(value); + this.myArrayList.set(index, value); + return this; + } + if(index == this.length()){ + // simple append + return this.put(value); + } + // if we are inserting past the length, we want to grow the array all at once + // instead of incrementally. + this.myArrayList.ensureCapacity(index + 1); + while (index != this.length()) { + // we don't need to test validity of NULL objects + this.myArrayList.add(JSONObject.NULL); + } + return this.put(value); + } + + /** + * Creates a JSONPointer using an initialization string and tries to + * match it to an item within this JSONArray. For example, given a + * JSONArray initialized with this document: + *
+     * [
+     *     {"b":"c"}
+     * ]
+     * 
+ * and this JSONPointer string: + *
+     * "/0/b"
+     * 
+ * Then this method will return the String "c" + * A JSONPointerException may be thrown from code called by this method. + * + * @param jsonPointer string that can be used to create a JSONPointer + * @return the item matched by the JSONPointer, otherwise null + */ + public Object query(String jsonPointer) { + return query(new JSONPointer(jsonPointer)); + } + + /** + * Uses a user initialized JSONPointer and tries to + * match it to an item within this JSONArray. For example, given a + * JSONArray initialized with this document: + *
+     * [
+     *     {"b":"c"}
+     * ]
+     * 
+ * and this JSONPointer: + *
+     * "/0/b"
+     * 
+ * Then this method will return the String "c" + * A JSONPointerException may be thrown from code called by this method. + * + * @param jsonPointer string that can be used to create a JSONPointer + * @return the item matched by the JSONPointer, otherwise null + */ + public Object query(JSONPointer jsonPointer) { + return jsonPointer.queryFrom(this); + } + + /** + * Queries and returns a value from this object using {@code jsonPointer}, or + * returns null if the query fails due to a missing key. + * + * @param jsonPointer the string representation of the JSON pointer + * @return the queried value or {@code null} + * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax + */ + public Object optQuery(String jsonPointer) { + return optQuery(new JSONPointer(jsonPointer)); + } + + /** + * Queries and returns a value from this object using {@code jsonPointer}, or + * returns null if the query fails due to a missing key. + * + * @param jsonPointer The JSON pointer + * @return the queried value or {@code null} + * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax + */ + public Object optQuery(JSONPointer jsonPointer) { + try { + return jsonPointer.queryFrom(this); + } catch (JSONPointerException e) { + return null; + } + } + + /** + * Remove an index and close the hole. + * + * @param index + * The index of the element to be removed. + * @return The value that was associated with the index, or null if there + * was no value. + */ + public Object remove(int index) { + return index >= 0 && index < this.length() + ? this.myArrayList.remove(index) + : null; + } + + /** + * Determine if two JSONArrays are similar. + * They must contain similar sequences. + * + * @param other The other JSONArray + * @return true if they are equal + */ + public boolean similar(Object other) { + if (!(other instanceof JSONArray)) { + return false; + } + int len = this.length(); + if (len != ((JSONArray)other).length()) { + return false; + } + for (int i = 0; i < len; i += 1) { + Object valueThis = this.myArrayList.get(i); + Object valueOther = ((JSONArray)other).myArrayList.get(i); + if(valueThis == valueOther) { + continue; + } + if(valueThis == null) { + return false; + } + if (valueThis instanceof JSONObject) { + if (!((JSONObject)valueThis).similar(valueOther)) { + return false; + } + } else if (valueThis instanceof JSONArray) { + if (!((JSONArray)valueThis).similar(valueOther)) { + return false; + } + } else if (!valueThis.equals(valueOther)) { + return false; + } + } + return true; + } + + /** + * Produce a JSONObject by combining a JSONArray of names with the values of + * this JSONArray. + * + * @param names + * A JSONArray containing a list of key strings. These will be + * paired with the values. + * @return A JSONObject, or null if there are no names or if this JSONArray + * has no values. + * @throws JSONException + * If any of the names are null. + */ + public JSONObject toJSONObject(JSONArray names) throws JSONException { + if (names == null || names.isEmpty() || this.isEmpty()) { + return null; + } + JSONObject jo = new JSONObject(names.length()); + for (int i = 0; i < names.length(); i += 1) { + jo.put(names.getString(i), this.opt(i)); + } + return jo; + } + + /** + * Make a JSON text of this JSONArray. For compactness, no unnecessary + * whitespace is added. If it is not possible to produce a syntactically + * correct JSON text then null will be returned instead. This could occur if + * the array contains an invalid number. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @return a printable, displayable, transmittable representation of the + * array. + */ + @Override + public String toString() { + try { + return this.toString(0); + } catch (Exception e) { + return null; + } + } + + /** + * Make a pretty-printed JSON text of this JSONArray. + * + *

If indentFactor > 0 and the {@link JSONArray} has only + * one element, then the array will be output on a single line: + *

{@code [1]}
+ * + *

If an array has 2 or more elements, then it will be output across + * multiple lines:

{@code
+     * [
+     * 1,
+     * "value 2",
+     * 3
+     * ]
+     * }
+ *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @return a printable, displayable, transmittable representation of the + * object, beginning with [ (left + * bracket) and ending with ] + *  (right bracket). + * @throws JSONException + */ + public String toString(int indentFactor) throws JSONException { + StringWriter sw = new StringWriter(); + synchronized (sw.getBuffer()) { + return this.write(sw, indentFactor, 0).toString(); + } + } + + /** + * Write the contents of the JSONArray as JSON text to a writer. For + * compactness, no whitespace is added. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer) throws JSONException { + return this.write(writer, 0, 0); + } + + /** + * Write the contents of the JSONArray as JSON text to a writer. + * + *

If indentFactor > 0 and the {@link JSONArray} has only + * one element, then the array will be output on a single line: + *

{@code [1]}
+ * + *

If an array has 2 or more elements, then it will be output across + * multiple lines:

{@code
+     * [
+     * 1,
+     * "value 2",
+     * 3
+     * ]
+     * }
+ *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @param writer + * Writes the serialized JSON + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @param indent + * The indentation of the top level. + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer, int indentFactor, int indent) + throws JSONException { + try { + boolean commanate = false; + int length = this.length(); + writer.write('['); + + if (length == 1) { + try { + JSONObject.writeValue(writer, this.myArrayList.get(0), + indentFactor, indent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONArray value at index: 0", e); + } + } else if (length != 0) { + final int newindent = indent + indentFactor; + + for (int i = 0; i < length; i += 1) { + if (commanate) { + writer.write(','); + } + if (indentFactor > 0) { + writer.write('\n'); + } + JSONObject.indent(writer, newindent); + try { + JSONObject.writeValue(writer, this.myArrayList.get(i), + indentFactor, newindent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONArray value at index: " + i, e); + } + commanate = true; + } + if (indentFactor > 0) { + writer.write('\n'); + } + JSONObject.indent(writer, indent); + } + writer.write(']'); + return writer; + } catch (IOException e) { + throw new JSONException(e); + } + } + + /** + * Returns a java.util.List containing all of the elements in this array. + * If an element in the array is a JSONArray or JSONObject it will also + * be converted. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return a java.util.List containing the elements of this array + */ + public List toList() { + List results = new ArrayList(this.myArrayList.size()); + for (Object element : this.myArrayList) { + if (element == null || JSONObject.NULL.equals(element)) { + results.add(null); + } else if (element instanceof JSONArray) { + results.add(((JSONArray) element).toList()); + } else if (element instanceof JSONObject) { + results.add(((JSONObject) element).toMap()); + } else { + results.add(element); + } + } + return results; + } + + /** + * Check if JSONArray is empty. + * + * @return true if JSONArray is empty, otherwise false. + */ + public boolean isEmpty() { + return this.myArrayList.isEmpty(); + } + +} diff --git a/core/src/com/hiveworkshop/json/JSONException.java b/core/src/com/hiveworkshop/json/JSONException.java new file mode 100644 index 0000000..f924fb5 --- /dev/null +++ b/core/src/com/hiveworkshop/json/JSONException.java @@ -0,0 +1,45 @@ +package com.hiveworkshop.json; + +/** + * The JSONException is thrown by the JSON.org classes when things are amiss. + * + * @author JSON.org + * @version 2015-12-09 + */ +public class JSONException extends RuntimeException { + /** Serialization ID */ + private static final long serialVersionUID = 0; + + /** + * Constructs a JSONException with an explanatory message. + * + * @param message + * Detail about the reason for the exception. + */ + public JSONException(final String message) { + super(message); + } + + /** + * Constructs a JSONException with an explanatory message and cause. + * + * @param message + * Detail about the reason for the exception. + * @param cause + * The cause. + */ + public JSONException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new JSONException with the specified cause. + * + * @param cause + * The cause. + */ + public JSONException(final Throwable cause) { + super(cause.getMessage(), cause); + } + +} diff --git a/core/src/com/hiveworkshop/json/JSONObject.java b/core/src/com/hiveworkshop/json/JSONObject.java new file mode 100644 index 0000000..3bfd26c --- /dev/null +++ b/core/src/com/hiveworkshop/json/JSONObject.java @@ -0,0 +1,2551 @@ +package com.hiveworkshop.json; + +import java.io.Closeable; + +/* + Copyright (c) 2002 JSON.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + The Software shall be used for Good, not Evil. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * A JSONObject is an unordered collection of name/value pairs. Its external + * form is a string wrapped in curly braces with colons between the names and + * values, and commas between the values and names. The internal form is an + * object having get and opt methods for accessing + * the values by name, and put methods for adding or replacing + * values by name. The values can be any of these types: Boolean, + * JSONArray, JSONObject, Number, + * String, or the JSONObject.NULL object. A + * JSONObject constructor can be used to convert an external form JSON text + * into an internal form whose values can be retrieved with the + * get and opt methods, or to convert values into a + * JSON text using the put and toString methods. A + * get method returns a value if one can be found, and throws an + * exception if one cannot be found. An opt method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + *

+ * The generic get() and opt() methods return an + * object, which you can cast or query for type. There are also typed + * get and opt methods that do type checking and type + * coercion for you. The opt methods differ from the get methods in that they + * do not throw. Instead, they return a specified value, such as null. + *

+ * The put methods add or replace values in an object. For + * example, + * + *

+ * myString = new JSONObject()
+ *         .put("JSON", "Hello, World!").toString();
+ * 
+ * + * produces the string {"JSON": "Hello, World"}. + *

+ * The texts produced by the toString methods strictly conform to + * the JSON syntax rules. The constructors are more forgiving in the texts they + * will accept: + *

    + *
  • An extra , (comma) may appear just + * before the closing brace.
  • + *
  • Strings may be quoted with ' (single + * quote).
  • + *
  • Strings do not need to be quoted at all if they do not begin with a + * quote or single quote, and if they do not contain leading or trailing + * spaces, and if they do not contain any of these characters: + * { } [ ] / \ : , # and if they do not look like numbers and + * if they are not the reserved words true, false, + * or null.
  • + *
+ * + * @author JSON.org + * @version 2016-08-15 + */ +public class JSONObject { + /** + * JSONObject.NULL is equivalent to the value that JavaScript calls null, + * whilst Java's null is equivalent to the value that JavaScript calls + * undefined. + */ + private static final class Null { + + /** + * There is only intended to be a single instance of the NULL object, + * so the clone method returns itself. + * + * @return NULL. + */ + @Override + protected final Object clone() { + return this; + } + + /** + * A Null object is equal to the null value and to itself. + * + * @param object + * An object to test for nullness. + * @return true if the object parameter is the JSONObject.NULL object or + * null. + */ + @Override + public boolean equals(Object object) { + return object == null || object == this; + } + /** + * A Null object is equal to the null value and to itself. + * + * @return always returns 0. + */ + @Override + public int hashCode() { + return 0; + } + + /** + * Get the "null" string value. + * + * @return The string "null". + */ + @Override + public String toString() { + return "null"; + } + } + + /** + * Regular Expression Pattern that matches JSON Numbers. This is primarily used for + * output to guarantee that we are always writing valid JSON. + */ + static final Pattern NUMBER_PATTERN = Pattern.compile("-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?"); + + /** + * The map where the JSONObject's properties are kept. + */ + private final Map map; + + /** + * It is sometimes more convenient and less ambiguous to have a + * NULL object than to use Java's null value. + * JSONObject.NULL.equals(null) returns true. + * JSONObject.NULL.toString() returns "null". + */ + public static final Object NULL = new Null(); + + /** + * Construct an empty JSONObject. + */ + public JSONObject() { + // HashMap is used on purpose to ensure that elements are unordered by + // the specification. + // JSON tends to be a portable transfer format to allows the container + // implementations to rearrange their items for a faster element + // retrieval based on associative access. + // Therefore, an implementation mustn't rely on the order of the item. + this.map = new HashMap(); + } + + /** + * Construct a JSONObject from a subset of another JSONObject. An array of + * strings is used to identify the keys that should be copied. Missing keys + * are ignored. + * + * @param jo + * A JSONObject. + * @param names + * An array of strings. + */ + public JSONObject(JSONObject jo, String[] names) { + this(names.length); + for (int i = 0; i < names.length; i += 1) { + try { + this.putOnce(names[i], jo.opt(names[i])); + } catch (Exception ignore) { + } + } + } + + /** + * Construct a JSONObject from a JSONTokener. + * + * @param x + * A JSONTokener object containing the source string. + * @throws JSONException + * If there is a syntax error in the source string or a + * duplicated key. + */ + public JSONObject(JSONTokener x) throws JSONException { + this(); + char c; + String key; + + if (x.nextClean() != '{') { + throw x.syntaxError("A JSONObject text must begin with '{'"); + } + for (;;) { + c = x.nextClean(); + switch (c) { + case 0: + throw x.syntaxError("A JSONObject text must end with '}'"); + case '}': + return; + default: + x.back(); + key = x.nextValue().toString(); + } + + // The key is followed by ':'. + + c = x.nextClean(); + if (c != ':') { + throw x.syntaxError("Expected a ':' after a key"); + } + + // Use syntaxError(..) to include error location + + if (key != null) { + // Check if key exists + if (this.opt(key) != null) { + // key already exists + throw x.syntaxError("Duplicate key \"" + key + "\""); + } + // Only add value if non-null + Object value = x.nextValue(); + if (value!=null) { + this.put(key, value); + } + } + + // Pairs are separated by ','. + + switch (x.nextClean()) { + case ';': + case ',': + if (x.nextClean() == '}') { + return; + } + x.back(); + break; + case '}': + return; + default: + throw x.syntaxError("Expected a ',' or '}'"); + } + } + } + + /** + * Construct a JSONObject from a Map. + * + * @param m + * A map object that can be used to initialize the contents of + * the JSONObject. + * @throws JSONException + * If a value in the map is non-finite number. + * @throws NullPointerException + * If a key in the map is null + */ + public JSONObject(Map m) { + if (m == null) { + this.map = new HashMap(); + } else { + this.map = new HashMap(m.size()); + for (final Entry e : m.entrySet()) { + if(e.getKey() == null) { + throw new NullPointerException("Null key."); + } + final Object value = e.getValue(); + if (value != null) { + this.map.put(String.valueOf(e.getKey()), wrap(value)); + } + } + } + } + + /** + * Construct a JSONObject from an Object using bean getters. It reflects on + * all of the public methods of the object. For each of the methods with no + * parameters and a name starting with "get" or + * "is" followed by an uppercase letter, the method is invoked, + * and a key and the value returned from the getter method are put into the + * new JSONObject. + *

+ * The key is formed by removing the "get" or "is" + * prefix. If the second remaining character is not upper case, then the + * first character is converted to lower case. + *

+ * Methods that are static, return void, + * have parameters, or are "bridge" methods, are ignored. + *

+ * For example, if an object has a method named "getName", and + * if the result of calling object.getName() is + * "Larry Fine", then the JSONObject will contain + * "name": "Larry Fine". + *

+ * The {@link JSONPropertyName} annotation can be used on a bean getter to + * override key name used in the JSONObject. For example, using the object + * above with the getName method, if we annotated it with: + *

+     * @JSONPropertyName("FullName")
+     * public String getName() { return this.name; }
+     * 
+ * The resulting JSON object would contain "FullName": "Larry Fine" + *

+ * Similarly, the {@link JSONPropertyName} annotation can be used on non- + * get and is methods. We can also override key + * name used in the JSONObject as seen below even though the field would normally + * be ignored: + *

+     * @JSONPropertyName("FullName")
+     * public String fullName() { return this.name; }
+     * 
+ * The resulting JSON object would contain "FullName": "Larry Fine" + *

+ * The {@link JSONPropertyIgnore} annotation can be used to force the bean property + * to not be serialized into JSON. If both {@link JSONPropertyIgnore} and + * {@link JSONPropertyName} are defined on the same method, a depth comparison is + * performed and the one closest to the concrete class being serialized is used. + * If both annotations are at the same level, then the {@link JSONPropertyIgnore} + * annotation takes precedent and the field is not serialized. + * For example, the following declaration would prevent the getName + * method from being serialized: + *

+     * @JSONPropertyName("FullName")
+     * @JSONPropertyIgnore 
+     * public String getName() { return this.name; }
+     * 
+ *

+ * + * @param bean + * An object that has getter methods that should be used to make + * a JSONObject. + */ + public JSONObject(Object bean) { + this(); + this.populateMap(bean); + } + + /** + * Construct a JSONObject from an Object, using reflection to find the + * public members. The resulting JSONObject's keys will be the strings from + * the names array, and the values will be the field values associated with + * those keys in the object. If a key is not found or not visible, then it + * will not be copied into the new JSONObject. + * + * @param object + * An object that has fields that should be used to make a + * JSONObject. + * @param names + * An array of strings, the names of the fields to be obtained + * from the object. + */ + public JSONObject(Object object, String names[]) { + this(names.length); + Class c = object.getClass(); + for (int i = 0; i < names.length; i += 1) { + String name = names[i]; + try { + this.putOpt(name, c.getField(name).get(object)); + } catch (Exception ignore) { + } + } + } + + /** + * Construct a JSONObject from a source JSON text string. This is the most + * commonly used JSONObject constructor. + * + * @param source + * A string beginning with { (left + * brace) and ending with } + *  (right brace). + * @exception JSONException + * If there is a syntax error in the source string or a + * duplicated key. + */ + public JSONObject(String source) throws JSONException { + this(new JSONTokener(source)); + } + + /** + * Construct a JSONObject from a ResourceBundle. + * + * @param baseName + * The ResourceBundle base name. + * @param locale + * The Locale to load the ResourceBundle for. + * @throws JSONException + * If any JSONExceptions are detected. + */ + public JSONObject(String baseName, Locale locale) throws JSONException { + this(); + ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale, + Thread.currentThread().getContextClassLoader()); + +// Iterate through the keys in the bundle. + + Enumeration keys = bundle.getKeys(); + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + if (key != null) { + +// Go through the path, ensuring that there is a nested JSONObject for each +// segment except the last. Add the value using the last segment's name into +// the deepest nested JSONObject. + + String[] path = ((String) key).split("\\."); + int last = path.length - 1; + JSONObject target = this; + for (int i = 0; i < last; i += 1) { + String segment = path[i]; + JSONObject nextTarget = target.optJSONObject(segment); + if (nextTarget == null) { + nextTarget = new JSONObject(); + target.put(segment, nextTarget); + } + target = nextTarget; + } + target.put(path[last], bundle.getString((String) key)); + } + } + } + + /** + * Constructor to specify an initial capacity of the internal map. Useful for library + * internal calls where we know, or at least can best guess, how big this JSONObject + * will be. + * + * @param initialCapacity initial capacity of the internal map. + */ + protected JSONObject(int initialCapacity){ + this.map = new HashMap(initialCapacity); + } + + /** + * Accumulate values under a key. It is similar to the put method except + * that if there is already an object stored under the key then a JSONArray + * is stored under the key to hold all of the accumulated values. If there + * is already a JSONArray, then the new value is appended to it. In + * contrast, the put method replaces the previous value. + * + * If only one value is accumulated that is not a JSONArray, then the result + * will be the same as using put. But if multiple values are accumulated, + * then the result will be like append. + * + * @param key + * A key string. + * @param value + * An object to be accumulated under the key. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject accumulate(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, + value instanceof JSONArray ? new JSONArray().put(value) + : value); + } else if (object instanceof JSONArray) { + ((JSONArray) object).put(value); + } else { + this.put(key, new JSONArray().put(object).put(value)); + } + return this; + } + + /** + * Append values to the array under a key. If the key does not exist in the + * JSONObject, then the key is put in the JSONObject with its value being a + * JSONArray containing the value parameter. If the key was already + * associated with a JSONArray, then the value parameter is appended to it. + * + * @param key + * A key string. + * @param value + * An object to be accumulated under the key. + * @return this. + * @throws JSONException + * If the value is non-finite number or if the current value associated with + * the key is not a JSONArray. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject append(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, new JSONArray().put(value)); + } else if (object instanceof JSONArray) { + this.put(key, ((JSONArray) object).put(value)); + } else { + throw new JSONException("JSONObject[" + key + + "] is not a JSONArray."); + } + return this; + } + + /** + * Produce a string from a double. The string "null" will be returned if the + * number is not finite. + * + * @param d + * A double. + * @return A String. + */ + public static String doubleToString(double d) { + if (Double.isInfinite(d) || Double.isNaN(d)) { + return "null"; + } + +// Shave off trailing zeros and decimal point, if possible. + + String string = Double.toString(d); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + /** + * Get the value object associated with a key. + * + * @param key + * A key string. + * @return The object associated with the key. + * @throws JSONException + * if the key is not found. + */ + public Object get(String key) throws JSONException { + if (key == null) { + throw new JSONException("Null key."); + } + Object object = this.opt(key); + if (object == null) { + throw new JSONException("JSONObject[" + quote(key) + "] not found."); + } + return object; + } + + /** + * Get the enum value associated with a key. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param key + * A key string. + * @return The enum value associated with the key + * @throws JSONException + * if the key is not found or if the value cannot be converted + * to an enum. + */ + public > E getEnum(Class clazz, String key) throws JSONException { + E val = optEnum(clazz, key); + if(val==null) { + // JSONException should really take a throwable argument. + // If it did, I would re-implement this with the Enum.valueOf + // method and place any thrown exception in the JSONException + throw new JSONException("JSONObject[" + quote(key) + + "] is not an enum of type " + quote(clazz.getSimpleName()) + + "."); + } + return val; + } + + /** + * Get the boolean value associated with a key. + * + * @param key + * A key string. + * @return The truth. + * @throws JSONException + * if the value is not a Boolean or the String "true" or + * "false". + */ + public boolean getBoolean(String key) throws JSONException { + Object object = this.get(key); + if (object.equals(Boolean.FALSE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("true"))) { + return true; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a Boolean."); + } + + /** + * Get the BigInteger value associated with a key. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value cannot + * be converted to BigInteger. + */ + public BigInteger getBigInteger(String key) throws JSONException { + Object object = this.get(key); + BigInteger ret = objectToBigInteger(object, null); + if (ret != null) { + return ret; + } + throw new JSONException("JSONObject[" + quote(key) + + "] could not be converted to BigInteger (" + object + ")."); + } + + /** + * Get the BigDecimal value associated with a key. If the value is float or + * double, the the {@link BigDecimal#BigDecimal(double)} constructor will + * be used. See notes on the constructor for conversion issues that may + * arise. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value + * cannot be converted to BigDecimal. + */ + public BigDecimal getBigDecimal(String key) throws JSONException { + Object object = this.get(key); + BigDecimal ret = objectToBigDecimal(object, null); + if (ret != null) { + return ret; + } + throw new JSONException("JSONObject[" + quote(key) + + "] could not be converted to BigDecimal (" + object + ")."); + } + + /** + * Get the double value associated with a key. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public double getDouble(String key) throws JSONException { + return this.getNumber(key).doubleValue(); + } + + /** + * Get the float value associated with a key. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public float getFloat(String key) throws JSONException { + return this.getNumber(key).floatValue(); + } + + /** + * Get the Number value associated with a key. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public Number getNumber(String key) throws JSONException { + Object object = this.get(key); + try { + if (object instanceof Number) { + return (Number)object; + } + return stringToNumber(object.toString()); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a number.", e); + } + } + + /** + * Get the int value associated with a key. + * + * @param key + * A key string. + * @return The integer value. + * @throws JSONException + * if the key is not found or if the value cannot be converted + * to an integer. + */ + public int getInt(String key) throws JSONException { + return this.getNumber(key).intValue(); + } + + /** + * Get the JSONArray value associated with a key. + * + * @param key + * A key string. + * @return A JSONArray which is the value. + * @throws JSONException + * if the key is not found or if the value is not a JSONArray. + */ + public JSONArray getJSONArray(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a JSONArray."); + } + + /** + * Get the JSONObject value associated with a key. + * + * @param key + * A key string. + * @return A JSONObject which is the value. + * @throws JSONException + * if the key is not found or if the value is not a JSONObject. + */ + public JSONObject getJSONObject(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a JSONObject."); + } + + /** + * Get the long value associated with a key. + * + * @param key + * A key string. + * @return The long value. + * @throws JSONException + * if the key is not found or if the value cannot be converted + * to a long. + */ + public long getLong(String key) throws JSONException { + return this.getNumber(key).longValue(); + } + + /** + * Get an array of field names from a JSONObject. + * + * @param jo + * JSON object + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(JSONObject jo) { + if (jo.isEmpty()) { + return null; + } + return jo.keySet().toArray(new String[jo.length()]); + } + + /** + * Get an array of public field names from an Object. + * + * @param object + * object to read + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(Object object) { + if (object == null) { + return null; + } + Class klass = object.getClass(); + Field[] fields = klass.getFields(); + int length = fields.length; + if (length == 0) { + return null; + } + String[] names = new String[length]; + for (int i = 0; i < length; i += 1) { + names[i] = fields[i].getName(); + } + return names; + } + + /** + * Get the string associated with a key. + * + * @param key + * A key string. + * @return A string which is the value. + * @throws JSONException + * if there is no string value for the key. + */ + public String getString(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof String) { + return (String) object; + } + throw new JSONException("JSONObject[" + quote(key) + "] not a string."); + } + + /** + * Determine if the JSONObject contains a specific key. + * + * @param key + * A key string. + * @return true if the key exists in the JSONObject. + */ + public boolean has(String key) { + return this.map.containsKey(key); + } + + /** + * Increment a property of a JSONObject. If there is no such property, + * create one with a value of 1. If there is such a property, and if it is + * an Integer, Long, Double, or Float, then add one to it. + * + * @param key + * A key string. + * @return this. + * @throws JSONException + * If there is already a property with this name that is not an + * Integer, Long, Double, or Float. + */ + public JSONObject increment(String key) throws JSONException { + Object value = this.opt(key); + if (value == null) { + this.put(key, 1); + } else if (value instanceof BigInteger) { + this.put(key, ((BigInteger)value).add(BigInteger.ONE)); + } else if (value instanceof BigDecimal) { + this.put(key, ((BigDecimal)value).add(BigDecimal.ONE)); + } else if (value instanceof Integer) { + this.put(key, ((Integer) value).intValue() + 1); + } else if (value instanceof Long) { + this.put(key, ((Long) value).longValue() + 1L); + } else if (value instanceof Double) { + this.put(key, ((Double) value).doubleValue() + 1.0d); + } else if (value instanceof Float) { + this.put(key, ((Float) value).floatValue() + 1.0f); + } else { + throw new JSONException("Unable to increment [" + quote(key) + "]."); + } + return this; + } + + /** + * Determine if the value associated with the key is null or if there is no + * value. + * + * @param key + * A key string. + * @return true if there is no value associated with the key or if the value + * is the JSONObject.NULL object. + */ + public boolean isNull(String key) { + return JSONObject.NULL.equals(this.opt(key)); + } + + /** + * Get an enumeration of the keys of the JSONObject. Modifying this key Set will also + * modify the JSONObject. Use with caution. + * + * @see Set#iterator() + * + * @return An iterator of the keys. + */ + public Iterator keys() { + return this.keySet().iterator(); + } + + /** + * Get a set of keys of the JSONObject. Modifying this key Set will also modify the + * JSONObject. Use with caution. + * + * @see Map#keySet() + * + * @return A keySet. + */ + public Set keySet() { + return this.map.keySet(); + } + + /** + * Get a set of entries of the JSONObject. These are raw values and may not + * match what is returned by the JSONObject get* and opt* functions. Modifying + * the returned EntrySet or the Entry objects contained therein will modify the + * backing JSONObject. This does not return a clone or a read-only view. + * + * Use with caution. + * + * @see Map#entrySet() + * + * @return An Entry Set + */ + protected Set> entrySet() { + return this.map.entrySet(); + } + + /** + * Get the number of keys stored in the JSONObject. + * + * @return The number of keys in the JSONObject. + */ + public int length() { + return this.map.size(); + } + + /** + * Check if JSONObject is empty. + * + * @return true if JSONObject is empty, otherwise false. + */ + public boolean isEmpty() { + return this.map.isEmpty(); + } + + /** + * Produce a JSONArray containing the names of the elements of this + * JSONObject. + * + * @return A JSONArray containing the key strings, or null if the JSONObject + * is empty. + */ + public JSONArray names() { + if(this.map.isEmpty()) { + return null; + } + return new JSONArray(this.map.keySet()); + } + + /** + * Produce a string from a Number. + * + * @param number + * A Number + * @return A String. + * @throws JSONException + * If n is a non-finite number. + */ + public static String numberToString(Number number) throws JSONException { + if (number == null) { + throw new JSONException("Null pointer"); + } + testValidity(number); + + // Shave off trailing zeros and decimal point, if possible. + + String string = number.toString(); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + /** + * Get an optional value associated with a key. + * + * @param key + * A key string. + * @return An object which is the value, or null if there is no value. + */ + public Object opt(String key) { + return key == null ? null : this.map.get(key); + } + + /** + * Get the enum value associated with a key. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param key + * A key string. + * @return The enum value associated with the key or null if not found + */ + public > E optEnum(Class clazz, String key) { + return this.optEnum(clazz, key, null); + } + + /** + * Get the enum value associated with a key. + * + * @param + * Enum Type + * @param clazz + * The type of enum to retrieve. + * @param key + * A key string. + * @param defaultValue + * The default in case the value is not found + * @return The enum value associated with the key or defaultValue + * if the value is not found or cannot be assigned to clazz + */ + public > E optEnum(Class clazz, String key, E defaultValue) { + try { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (clazz.isAssignableFrom(val.getClass())) { + // we just checked it! + @SuppressWarnings("unchecked") + E myE = (E) val; + return myE; + } + return Enum.valueOf(clazz, val.toString()); + } catch (IllegalArgumentException e) { + return defaultValue; + } catch (NullPointerException e) { + return defaultValue; + } + } + + /** + * Get an optional boolean associated with a key. It returns false if there + * is no such key, or if the value is not Boolean.TRUE or the String "true". + * + * @param key + * A key string. + * @return The truth. + */ + public boolean optBoolean(String key) { + return this.optBoolean(key, false); + } + + /** + * Get an optional boolean associated with a key. It returns the + * defaultValue if there is no such key, or if it is not a Boolean or the + * String "true" or "false" (case insensitive). + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return The truth. + */ + public boolean optBoolean(String key, boolean defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Boolean){ + return ((Boolean) val).booleanValue(); + } + try { + // we'll use the get anyway because it does string conversion. + return this.getBoolean(key); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional BigDecimal associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. If the value + * is float or double, then the {@link BigDecimal#BigDecimal(double)} + * constructor will be used. See notes on the constructor for conversion + * issues that may arise. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) { + Object val = this.opt(key); + return objectToBigDecimal(val, defaultValue); + } + + /** + * @param val value to convert + * @param defaultValue default value to return is the conversion doesn't work or is null. + * @return BigDecimal conversion of the original value, or the defaultValue if unable + * to convert. + */ + static BigDecimal objectToBigDecimal(Object val, BigDecimal defaultValue) { + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof BigDecimal){ + return (BigDecimal) val; + } + if (val instanceof BigInteger){ + return new BigDecimal((BigInteger) val); + } + if (val instanceof Double || val instanceof Float){ + final double d = ((Number) val).doubleValue(); + if(Double.isNaN(d)) { + return defaultValue; + } + return new BigDecimal(((Number) val).doubleValue()); + } + if (val instanceof Long || val instanceof Integer + || val instanceof Short || val instanceof Byte){ + return new BigDecimal(((Number) val).longValue()); + } + // don't check if it's a string in case of unchecked Number subclasses + try { + return new BigDecimal(val.toString()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional BigInteger associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public BigInteger optBigInteger(String key, BigInteger defaultValue) { + Object val = this.opt(key); + return objectToBigInteger(val, defaultValue); + } + + /** + * @param val value to convert + * @param defaultValue default value to return is the conversion doesn't work or is null. + * @return BigInteger conversion of the original value, or the defaultValue if unable + * to convert. + */ + static BigInteger objectToBigInteger(Object val, BigInteger defaultValue) { + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof BigInteger){ + return (BigInteger) val; + } + if (val instanceof BigDecimal){ + return ((BigDecimal) val).toBigInteger(); + } + if (val instanceof Double || val instanceof Float){ + final double d = ((Number) val).doubleValue(); + if(Double.isNaN(d)) { + return defaultValue; + } + return new BigDecimal(d).toBigInteger(); + } + if (val instanceof Long || val instanceof Integer + || val instanceof Short || val instanceof Byte){ + return BigInteger.valueOf(((Number) val).longValue()); + } + // don't check if it's a string in case of unchecked Number subclasses + try { + // the other opt functions handle implicit conversions, i.e. + // jo.put("double",1.1d); + // jo.optInt("double"); -- will return 1, not an error + // this conversion to BigDecimal then to BigInteger is to maintain + // that type cast support that may truncate the decimal. + final String valStr = val.toString(); + if(isDecimalNotation(valStr)) { + return new BigDecimal(valStr).toBigInteger(); + } + return new BigInteger(valStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional double associated with a key, or NaN if there is no such + * key or if its value is not a number. If the value is a string, an attempt + * will be made to evaluate it as a number. + * + * @param key + * A string which is the key. + * @return An object which is the value. + */ + public double optDouble(String key) { + return this.optDouble(key, Double.NaN); + } + + /** + * Get an optional double associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public double optDouble(String key, double defaultValue) { + Number val = this.optNumber(key); + if (val == null) { + return defaultValue; + } + final double doubleValue = val.doubleValue(); + // if (Double.isNaN(doubleValue) || Double.isInfinite(doubleValue)) { + // return defaultValue; + // } + return doubleValue; + } + + /** + * Get the optional double value associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param key + * A key string. + * @return The value. + */ + public float optFloat(String key) { + return this.optFloat(key, Float.NaN); + } + + /** + * Get the optional double value associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param key + * A key string. + * @param defaultValue + * The default value. + * @return The value. + */ + public float optFloat(String key, float defaultValue) { + Number val = this.optNumber(key); + if (val == null) { + return defaultValue; + } + final float floatValue = val.floatValue(); + // if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) { + // return defaultValue; + // } + return floatValue; + } + + /** + * Get an optional int value associated with a key, or zero if there is no + * such key or if the value is not a number. If the value is a string, an + * attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @return An object which is the value. + */ + public int optInt(String key) { + return this.optInt(key, 0); + } + + /** + * Get an optional int value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public int optInt(String key, int defaultValue) { + final Number val = this.optNumber(key, null); + if (val == null) { + return defaultValue; + } + return val.intValue(); + } + + /** + * Get an optional JSONArray associated with a key. It returns null if there + * is no such key, or if its value is not a JSONArray. + * + * @param key + * A key string. + * @return A JSONArray which is the value. + */ + public JSONArray optJSONArray(String key) { + Object o = this.opt(key); + return o instanceof JSONArray ? (JSONArray) o : null; + } + + /** + * Get an optional JSONObject associated with a key. It returns null if + * there is no such key, or if its value is not a JSONObject. + * + * @param key + * A key string. + * @return A JSONObject which is the value. + */ + public JSONObject optJSONObject(String key) { + Object object = this.opt(key); + return object instanceof JSONObject ? (JSONObject) object : null; + } + + /** + * Get an optional long value associated with a key, or zero if there is no + * such key or if the value is not a number. If the value is a string, an + * attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @return An object which is the value. + */ + public long optLong(String key) { + return this.optLong(key, 0); + } + + /** + * Get an optional long value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public long optLong(String key, long defaultValue) { + final Number val = this.optNumber(key, null); + if (val == null) { + return defaultValue; + } + + return val.longValue(); + } + + /** + * Get an optional {@link Number} value associated with a key, or null + * if there is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number ({@link BigDecimal}). This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param key + * A key string. + * @return An object which is the value. + */ + public Number optNumber(String key) { + return this.optNumber(key, null); + } + + /** + * Get an optional {@link Number} value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public Number optNumber(String key, Number defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number){ + return (Number) val; + } + + try { + return stringToNumber(val.toString()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional string associated with a key. It returns an empty string + * if there is no such key. If the value is not a string and is not null, + * then it is converted to a string. + * + * @param key + * A key string. + * @return A string which is the value. + */ + public String optString(String key) { + return this.optString(key, ""); + } + + /** + * Get an optional string associated with a key. It returns the defaultValue + * if there is no such key. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return A string which is the value. + */ + public String optString(String key, String defaultValue) { + Object object = this.opt(key); + return NULL.equals(object) ? defaultValue : object.toString(); + } + + /** + * Populates the internal map of the JSONObject with the bean properties. The + * bean can not be recursive. + * + * @see JSONObject#JSONObject(Object) + * + * @param bean + * the bean + */ + private void populateMap(Object bean) { + Class klass = bean.getClass(); + + // If klass is a System class then set includeSuperClass to false. + + boolean includeSuperClass = klass.getClassLoader() != null; + + Method[] methods = includeSuperClass ? klass.getMethods() : klass.getDeclaredMethods(); + for (final Method method : methods) { + final int modifiers = method.getModifiers(); + if (Modifier.isPublic(modifiers) + && !Modifier.isStatic(modifiers) + && method.getParameterTypes().length == 0 + && !method.isBridge() + && method.getReturnType() != Void.TYPE + && isValidMethodName(method.getName())) { + final String key = getKeyNameFromMethod(method); + if (key != null && !key.isEmpty()) { + try { + final Object result = method.invoke(bean); + if (result != null) { + this.map.put(key, wrap(result)); + // we don't use the result anywhere outside of wrap + // if it's a resource we should be sure to close it + // after calling toString + if (result instanceof Closeable) { + try { + ((Closeable) result).close(); + } catch (IOException ignore) { + } + } + } + } catch (IllegalAccessException ignore) { + } catch (IllegalArgumentException ignore) { + } catch (InvocationTargetException ignore) { + } + } + } + } + } + + private boolean isValidMethodName(String name) { + return !"getClass".equals(name) && !"getDeclaringClass".equals(name); + } + + private String getKeyNameFromMethod(Method method) { + final int ignoreDepth = getAnnotationDepth(method, JSONPropertyIgnore.class); + if (ignoreDepth > 0) { + final int forcedNameDepth = getAnnotationDepth(method, JSONPropertyName.class); + if (forcedNameDepth < 0 || ignoreDepth <= forcedNameDepth) { + // the hierarchy asked to ignore, and the nearest name override + // was higher or non-existent + return null; + } + } + JSONPropertyName annotation = getAnnotation(method, JSONPropertyName.class); + if (annotation != null && annotation.value() != null && !annotation.value().isEmpty()) { + return annotation.value(); + } + String key; + final String name = method.getName(); + if (name.startsWith("get") && name.length() > 3) { + key = name.substring(3); + } else if (name.startsWith("is") && name.length() > 2) { + key = name.substring(2); + } else { + return null; + } + // if the first letter in the key is not uppercase, then skip. + // This is to maintain backwards compatibility before PR406 + // (https://github.com/stleary/JSON-java/pull/406/) + if (Character.isLowerCase(key.charAt(0))) { + return null; + } + if (key.length() == 1) { + key = key.toLowerCase(Locale.ROOT); + } else if (!Character.isUpperCase(key.charAt(1))) { + key = key.substring(0, 1).toLowerCase(Locale.ROOT) + key.substring(1); + } + return key; + } + + /** + * Searches the class hierarchy to see if the method or it's super + * implementations and interfaces has the annotation. + * + * @param + * type of the annotation + * + * @param m + * method to check + * @param annotationClass + * annotation to look for + * @return the {@link Annotation} if the annotation exists on the current method + * or one of it's super class definitions + */ + private static A getAnnotation(final Method m, final Class annotationClass) { + // if we have invalid data the result is null + if (m == null || annotationClass == null) { + return null; + } + + if (m.isAnnotationPresent(annotationClass)) { + return m.getAnnotation(annotationClass); + } + + // if we've already reached the Object class, return null; + Class c = m.getDeclaringClass(); + if (c.getSuperclass() == null) { + return null; + } + + // check directly implemented interfaces for the method being checked + for (Class i : c.getInterfaces()) { + try { + Method im = i.getMethod(m.getName(), m.getParameterTypes()); + return getAnnotation(im, annotationClass); + } catch (final SecurityException ex) { + continue; + } catch (final NoSuchMethodException ex) { + continue; + } + } + + try { + return getAnnotation( + c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()), + annotationClass); + } catch (final SecurityException ex) { + return null; + } catch (final NoSuchMethodException ex) { + return null; + } + } + + /** + * Searches the class hierarchy to see if the method or it's super + * implementations and interfaces has the annotation. Returns the depth of the + * annotation in the hierarchy. + * + * @param + * type of the annotation + * + * @param m + * method to check + * @param annotationClass + * annotation to look for + * @return Depth of the annotation or -1 if the annotation is not on the method. + */ + private static int getAnnotationDepth(final Method m, final Class annotationClass) { + // if we have invalid data the result is -1 + if (m == null || annotationClass == null) { + return -1; + } + + if (m.isAnnotationPresent(annotationClass)) { + return 1; + } + + // if we've already reached the Object class, return -1; + Class c = m.getDeclaringClass(); + if (c.getSuperclass() == null) { + return -1; + } + + // check directly implemented interfaces for the method being checked + for (Class i : c.getInterfaces()) { + try { + Method im = i.getMethod(m.getName(), m.getParameterTypes()); + int d = getAnnotationDepth(im, annotationClass); + if (d > 0) { + // since the annotation was on the interface, add 1 + return d + 1; + } + } catch (final SecurityException ex) { + continue; + } catch (final NoSuchMethodException ex) { + continue; + } + } + + try { + int d = getAnnotationDepth( + c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()), + annotationClass); + if (d > 0) { + // since the annotation was on the superclass, add 1 + return d + 1; + } + return -1; + } catch (final SecurityException ex) { + return -1; + } catch (final NoSuchMethodException ex) { + return -1; + } + } + + /** + * Put a key/boolean pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A boolean which is the value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, boolean value) throws JSONException { + return this.put(key, value ? Boolean.TRUE : Boolean.FALSE); + } + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONArray which is produced from a Collection. + * + * @param key + * A key string. + * @param value + * A Collection value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, Collection value) throws JSONException { + return this.put(key, new JSONArray(value)); + } + + /** + * Put a key/double pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A double which is the value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, double value) throws JSONException { + return this.put(key, Double.valueOf(value)); + } + + /** + * Put a key/float pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A float which is the value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, float value) throws JSONException { + return this.put(key, Float.valueOf(value)); + } + + /** + * Put a key/int pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * An int which is the value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, int value) throws JSONException { + return this.put(key, Integer.valueOf(value)); + } + + /** + * Put a key/long pair in the JSONObject. + * + * @param key + * A key string. + * @param value + * A long which is the value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, long value) throws JSONException { + return this.put(key, Long.valueOf(value)); + } + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONObject which is produced from a Map. + * + * @param key + * A key string. + * @param value + * A Map value. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, Map value) throws JSONException { + return this.put(key, new JSONObject(value)); + } + + /** + * Put a key/value pair in the JSONObject. If the value is null, then the + * key will be removed from the JSONObject if it is present. + * + * @param key + * A key string. + * @param value + * An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the value is non-finite number. + * @throws NullPointerException + * If the key is null. + */ + public JSONObject put(String key, Object value) throws JSONException { + if (key == null) { + throw new NullPointerException("Null key."); + } + if (value != null) { + testValidity(value); + this.map.put(key, value); + } else { + this.remove(key); + } + return this; + } + + /** + * Put a key/value pair in the JSONObject, but only if the key and the value + * are both non-null, and only if there is not already a member with that + * name. + * + * @param key + * key to insert into + * @param value + * value to insert + * @return this. + * @throws JSONException + * if the key is a duplicate + */ + public JSONObject putOnce(String key, Object value) throws JSONException { + if (key != null && value != null) { + if (this.opt(key) != null) { + throw new JSONException("Duplicate key \"" + key + "\""); + } + return this.put(key, value); + } + return this; + } + + /** + * Put a key/value pair in the JSONObject, but only if the key and the value + * are both non-null. + * + * @param key + * A key string. + * @param value + * An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException + * If the value is a non-finite number. + */ + public JSONObject putOpt(String key, Object value) throws JSONException { + if (key != null && value != null) { + return this.put(key, value); + } + return this; + } + + /** + * Creates a JSONPointer using an initialization string and tries to + * match it to an item within this JSONObject. For example, given a + * JSONObject initialized with this document: + *

+     * {
+     *     "a":{"b":"c"}
+     * }
+     * 
+ * and this JSONPointer string: + *
+     * "/a/b"
+     * 
+ * Then this method will return the String "c". + * A JSONPointerException may be thrown from code called by this method. + * + * @param jsonPointer string that can be used to create a JSONPointer + * @return the item matched by the JSONPointer, otherwise null + */ + public Object query(String jsonPointer) { + return query(new JSONPointer(jsonPointer)); + } + /** + * Uses a user initialized JSONPointer and tries to + * match it to an item within this JSONObject. For example, given a + * JSONObject initialized with this document: + *
+     * {
+     *     "a":{"b":"c"}
+     * }
+     * 
+ * and this JSONPointer: + *
+     * "/a/b"
+     * 
+ * Then this method will return the String "c". + * A JSONPointerException may be thrown from code called by this method. + * + * @param jsonPointer string that can be used to create a JSONPointer + * @return the item matched by the JSONPointer, otherwise null + */ + public Object query(JSONPointer jsonPointer) { + return jsonPointer.queryFrom(this); + } + + /** + * Queries and returns a value from this object using {@code jsonPointer}, or + * returns null if the query fails due to a missing key. + * + * @param jsonPointer the string representation of the JSON pointer + * @return the queried value or {@code null} + * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax + */ + public Object optQuery(String jsonPointer) { + return optQuery(new JSONPointer(jsonPointer)); + } + + /** + * Queries and returns a value from this object using {@code jsonPointer}, or + * returns null if the query fails due to a missing key. + * + * @param jsonPointer The JSON pointer + * @return the queried value or {@code null} + * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax + */ + public Object optQuery(JSONPointer jsonPointer) { + try { + return jsonPointer.queryFrom(this); + } catch (JSONPointerException e) { + return null; + } + } + + /** + * Produce a string in double quotes with backslash sequences in all the + * right places. A backslash will be inserted within </, producing + * <\/, allowing JSON text to be delivered in HTML. In JSON text, a + * string cannot contain a control character or an unescaped quote or + * backslash. + * + * @param string + * A String + * @return A String correctly formatted for insertion in a JSON text. + */ + public static String quote(String string) { + StringWriter sw = new StringWriter(); + synchronized (sw.getBuffer()) { + try { + return quote(string, sw).toString(); + } catch (IOException ignored) { + // will never happen - we are writing to a string writer + return ""; + } + } + } + + public static Writer quote(String string, Writer w) throws IOException { + if (string == null || string.isEmpty()) { + w.write("\"\""); + return w; + } + + char b; + char c = 0; + String hhhh; + int i; + int len = string.length(); + + w.write('"'); + for (i = 0; i < len; i += 1) { + b = c; + c = string.charAt(i); + switch (c) { + case '\\': + case '"': + w.write('\\'); + w.write(c); + break; + case '/': + if (b == '<') { + w.write('\\'); + } + w.write(c); + break; + case '\b': + w.write("\\b"); + break; + case '\t': + w.write("\\t"); + break; + case '\n': + w.write("\\n"); + break; + case '\f': + w.write("\\f"); + break; + case '\r': + w.write("\\r"); + break; + default: + if (c < ' ' || (c >= '\u0080' && c < '\u00a0') + || (c >= '\u2000' && c < '\u2100')) { + w.write("\\u"); + hhhh = Integer.toHexString(c); + w.write("0000", 0, 4 - hhhh.length()); + w.write(hhhh); + } else { + w.write(c); + } + } + } + w.write('"'); + return w; + } + + /** + * Remove a name and its value, if present. + * + * @param key + * The name to be removed. + * @return The value that was associated with the name, or null if there was + * no value. + */ + public Object remove(String key) { + return this.map.remove(key); + } + + /** + * Determine if two JSONObjects are similar. + * They must contain the same set of names which must be associated with + * similar values. + * + * @param other The other JSONObject + * @return true if they are equal + */ + public boolean similar(Object other) { + try { + if (!(other instanceof JSONObject)) { + return false; + } + if (!this.keySet().equals(((JSONObject)other).keySet())) { + return false; + } + for (final Entry entry : this.entrySet()) { + String name = entry.getKey(); + Object valueThis = entry.getValue(); + Object valueOther = ((JSONObject)other).get(name); + if(valueThis == valueOther) { + continue; + } + if(valueThis == null) { + return false; + } + if (valueThis instanceof JSONObject) { + if (!((JSONObject)valueThis).similar(valueOther)) { + return false; + } + } else if (valueThis instanceof JSONArray) { + if (!((JSONArray)valueThis).similar(valueOther)) { + return false; + } + } else if (!valueThis.equals(valueOther)) { + return false; + } + } + return true; + } catch (Throwable exception) { + return false; + } + } + + /** + * Tests if the value should be tried as a decimal. It makes no test if there are actual digits. + * + * @param val value to test + * @return true if the string is "-0" or if it contains '.', 'e', or 'E', false otherwise. + */ + protected static boolean isDecimalNotation(final String val) { + return val.indexOf('.') > -1 || val.indexOf('e') > -1 + || val.indexOf('E') > -1 || "-0".equals(val); + } + + /** + * Converts a string to a number using the narrowest possible type. Possible + * returns for this function are BigDecimal, Double, BigInteger, Long, and Integer. + * When a Double is returned, it should always be a valid Double and not NaN or +-infinity. + * + * @param val value to convert + * @return Number representation of the value. + * @throws NumberFormatException thrown if the value is not a valid number. A public + * caller should catch this and wrap it in a {@link JSONException} if applicable. + */ + protected static Number stringToNumber(final String val) throws NumberFormatException { + char initial = val.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + // decimal representation + if (isDecimalNotation(val)) { + // quick dirty way to see if we need a BigDecimal instead of a Double + // this only handles some cases of overflow or underflow + if (val.length()>14) { + return new BigDecimal(val); + } + final Double d = Double.valueOf(val); + if (d.isInfinite() || d.isNaN()) { + // if we can't parse it as a double, go up to BigDecimal + // this is probably due to underflow like 4.32e-678 + // or overflow like 4.65e5324. The size of the string is small + // but can't be held in a Double. + return new BigDecimal(val); + } + return d; + } + // integer representation. + // This will narrow any values to the smallest reasonable Object representation + // (Integer, Long, or BigInteger) + + // string version + // The compare string length method reduces GC, + // but leads to smaller integers being placed in larger wrappers even though not + // needed. i.e. 1,000,000,000 -> Long even though it's an Integer + // 1,000,000,000,000,000,000 -> BigInteger even though it's a Long + //if(val.length()<=9){ + // return Integer.valueOf(val); + //} + //if(val.length()<=18){ + // return Long.valueOf(val); + //} + //return new BigInteger(val); + + // BigInteger version: We use a similar bitLenth compare as + // BigInteger#intValueExact uses. Increases GC, but objects hold + // only what they need. i.e. Less runtime overhead if the value is + // long lived. Which is the better tradeoff? This is closer to what's + // in stringToValue. + BigInteger bi = new BigInteger(val); + if(bi.bitLength()<=31){ + return Integer.valueOf(bi.intValue()); + } + if(bi.bitLength()<=63){ + return Long.valueOf(bi.longValue()); + } + return bi; + } + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + + /** + * Try to convert a string into a number, boolean, or null. If the string + * can't be converted, return the string. + * + * @param string + * A String. can not be null. + * @return A simple JSON value. + * @throws NullPointerException + * Thrown if the string is null. + */ + // Changes to this method must be copied to the corresponding method in + // the XML class to keep full support for Android + public static Object stringToValue(String string) { + if ("".equals(string)) { + return string; + } + + // check JSON key words true/false/null + if ("true".equalsIgnoreCase(string)) { + return Boolean.TRUE; + } + if ("false".equalsIgnoreCase(string)) { + return Boolean.FALSE; + } + if ("null".equalsIgnoreCase(string)) { + return JSONObject.NULL; + } + + /* + * If it might be a number, try converting it. If a number cannot be + * produced, then the value will just be a string. + */ + + char initial = string.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + try { + // if we want full Big Number support the contents of this + // `try` block can be replaced with: + // return stringToNumber(string); + if (isDecimalNotation(string)) { + Double d = Double.valueOf(string); + if (!d.isInfinite() && !d.isNaN()) { + return d; + } + } else { + Long myLong = Long.valueOf(string); + if (string.equals(myLong.toString())) { + if (myLong.longValue() == myLong.intValue()) { + return Integer.valueOf(myLong.intValue()); + } + return myLong; + } + } + } catch (Exception ignore) { + } + } + return string; + } + + /** + * Throw an exception if the object is a NaN or infinite number. + * + * @param o + * The object to test. + * @throws JSONException + * If o is a non-finite number. + */ + public static void testValidity(Object o) throws JSONException { + if (o != null) { + if (o instanceof Double) { + if (((Double) o).isInfinite() || ((Double) o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } else if (o instanceof Float) { + if (((Float) o).isInfinite() || ((Float) o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } + } + } + + /** + * Produce a JSONArray containing the values of the members of this + * JSONObject. + * + * @param names + * A JSONArray containing a list of key strings. This determines + * the sequence of the values in the result. + * @return A JSONArray of values. + * @throws JSONException + * If any of the values are non-finite numbers. + */ + public JSONArray toJSONArray(JSONArray names) throws JSONException { + if (names == null || names.isEmpty()) { + return null; + } + JSONArray ja = new JSONArray(); + for (int i = 0; i < names.length(); i += 1) { + ja.put(this.opt(names.getString(i))); + } + return ja; + } + + /** + * Make a JSON text of this JSONObject. For compactness, no whitespace is + * added. If this would not result in a syntactically correct JSON text, + * then null will be returned instead. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @return a printable, displayable, portable, transmittable representation + * of the object, beginning with { (left + * brace) and ending with } (right + * brace). + */ + @Override + public String toString() { + try { + return this.toString(0); + } catch (Exception e) { + return null; + } + } + + /** + * Make a pretty-printed JSON text of this JSONObject. + * + *

If indentFactor > 0 and the {@link JSONObject} + * has only one key, then the object will be output on a single line: + *

{@code {"key": 1}}
+ * + *

If an object has 2 or more keys, then it will be output across + * multiple lines:

{
+     *  "key1": 1,
+     *  "key2": "value 2",
+     *  "key3": 3
+     * }
+ *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @return a printable, displayable, portable, transmittable representation + * of the object, beginning with { (left + * brace) and ending with } (right + * brace). + * @throws JSONException + * If the object contains an invalid number. + */ + public String toString(int indentFactor) throws JSONException { + StringWriter w = new StringWriter(); + synchronized (w.getBuffer()) { + return this.write(w, indentFactor, 0).toString(); + } + } + + /** + * Make a JSON text of an Object value. If the object has an + * value.toJSONString() method, then that method will be used to produce the + * JSON text. The method is required to produce a strictly conforming text. + * If the object does not contain a toJSONString method (which is the most + * common case), then a text will be produced by other means. If the value + * is an array or Collection, then a JSONArray will be made from it and its + * toJSONString method will be called. If the value is a MAP, then a + * JSONObject will be made from it and its toJSONString method will be + * called. Otherwise, the value's toString method will be called, and the + * result will be quoted. + * + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @param value + * The value to be serialized. + * @return a printable, displayable, transmittable representation of the + * object, beginning with { (left + * brace) and ending with } (right + * brace). + * @throws JSONException + * If the value is or contains an invalid number. + */ + public static String valueToString(Object value) throws JSONException { + // moves the implementation to JSONWriter as: + // 1. It makes more sense to be part of the writer class + // 2. For Android support this method is not available. By implementing it in the Writer + // Android users can use the writer with the built in Android JSONObject implementation. + return JSONWriter.valueToString(value); + } + + /** + * Wrap an object, if necessary. If the object is null, return the NULL + * object. If it is an array or collection, wrap it in a JSONArray. If it is + * a map, wrap it in a JSONObject. If it is a standard property (Double, + * String, et al) then it is already wrapped. Otherwise, if it comes from + * one of the java packages, turn it into a string. And if it doesn't, try + * to wrap it in a JSONObject. If the wrapping fails, then null is returned. + * + * @param object + * The object to wrap + * @return The wrapped value + */ + public static Object wrap(Object object) { + try { + if (object == null) { + return NULL; + } + if (object instanceof JSONObject || object instanceof JSONArray + || NULL.equals(object) || object instanceof JSONString + || object instanceof Byte || object instanceof Character + || object instanceof Short || object instanceof Integer + || object instanceof Long || object instanceof Boolean + || object instanceof Float || object instanceof Double + || object instanceof String || object instanceof BigInteger + || object instanceof BigDecimal || object instanceof Enum) { + return object; + } + + if (object instanceof Collection) { + Collection coll = (Collection) object; + return new JSONArray(coll); + } + if (object.getClass().isArray()) { + return new JSONArray(object); + } + if (object instanceof Map) { + Map map = (Map) object; + return new JSONObject(map); + } + Package objectPackage = object.getClass().getPackage(); + String objectPackageName = objectPackage != null ? objectPackage + .getName() : ""; + if (objectPackageName.startsWith("java.") + || objectPackageName.startsWith("javax.") + || object.getClass().getClassLoader() == null) { + return object.toString(); + } + return new JSONObject(object); + } catch (Exception exception) { + return null; + } + } + + /** + * Write the contents of the JSONObject as JSON text to a writer. For + * compactness, no whitespace is added. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer) throws JSONException { + return this.write(writer, 0, 0); + } + + static final Writer writeValue(Writer writer, Object value, + int indentFactor, int indent) throws JSONException, IOException { + if (value == null || value.equals(null)) { + writer.write("null"); + } else if (value instanceof JSONString) { + Object o; + try { + o = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + writer.write(o != null ? o.toString() : quote(value.toString())); + } else if (value instanceof Number) { + // not all Numbers may match actual JSON Numbers. i.e. fractions or Imaginary + final String numberAsString = numberToString((Number) value); + if(NUMBER_PATTERN.matcher(numberAsString).matches()) { + writer.write(numberAsString); + } else { + // The Number value is not a valid JSON number. + // Instead we will quote it as a string + quote(numberAsString, writer); + } + } else if (value instanceof Boolean) { + writer.write(value.toString()); + } else if (value instanceof Enum) { + writer.write(quote(((Enum)value).name())); + } else if (value instanceof JSONObject) { + ((JSONObject) value).write(writer, indentFactor, indent); + } else if (value instanceof JSONArray) { + ((JSONArray) value).write(writer, indentFactor, indent); + } else if (value instanceof Map) { + Map map = (Map) value; + new JSONObject(map).write(writer, indentFactor, indent); + } else if (value instanceof Collection) { + Collection coll = (Collection) value; + new JSONArray(coll).write(writer, indentFactor, indent); + } else if (value.getClass().isArray()) { + new JSONArray(value).write(writer, indentFactor, indent); + } else { + quote(value.toString(), writer); + } + return writer; + } + + static final void indent(Writer writer, int indent) throws IOException { + for (int i = 0; i < indent; i += 1) { + writer.write(' '); + } + } + + /** + * Write the contents of the JSONObject as JSON text to a writer. + * + *

If indentFactor > 0 and the {@link JSONObject} + * has only one key, then the object will be output on a single line: + *

{@code {"key": 1}}
+ * + *

If an object has 2 or more keys, then it will be output across + * multiple lines:

{
+     *  "key1": 1,
+     *  "key2": "value 2",
+     *  "key3": 3
+     * }
+ *

+ * Warning: This method assumes that the data structure is acyclical. + * + * + * @param writer + * Writes the serialized JSON + * @param indentFactor + * The number of spaces to add to each level of indentation. + * @param indent + * The indentation of the top level. + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer, int indentFactor, int indent) + throws JSONException { + try { + boolean commanate = false; + final int length = this.length(); + writer.write('{'); + + if (length == 1) { + final Entry entry = this.entrySet().iterator().next(); + final String key = entry.getKey(); + writer.write(quote(key)); + writer.write(':'); + if (indentFactor > 0) { + writer.write(' '); + } + try{ + writeValue(writer, entry.getValue(), indentFactor, indent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONObject value for key: " + key, e); + } + } else if (length != 0) { + final int newindent = indent + indentFactor; + for (final Entry entry : this.entrySet()) { + if (commanate) { + writer.write(','); + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, newindent); + final String key = entry.getKey(); + writer.write(quote(key)); + writer.write(':'); + if (indentFactor > 0) { + writer.write(' '); + } + try { + writeValue(writer, entry.getValue(), indentFactor, newindent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONObject value for key: " + key, e); + } + commanate = true; + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, indent); + } + writer.write('}'); + return writer; + } catch (IOException exception) { + throw new JSONException(exception); + } + } + + /** + * Returns a java.util.Map containing all of the entries in this object. + * If an entry in the object is a JSONArray or JSONObject it will also + * be converted. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return a java.util.Map containing the entries of this object + */ + public Map toMap() { + Map results = new HashMap(); + for (Entry entry : this.entrySet()) { + Object value; + if (entry.getValue() == null || NULL.equals(entry.getValue())) { + value = null; + } else if (entry.getValue() instanceof JSONObject) { + value = ((JSONObject) entry.getValue()).toMap(); + } else if (entry.getValue() instanceof JSONArray) { + value = ((JSONArray) entry.getValue()).toList(); + } else { + value = entry.getValue(); + } + results.put(entry.getKey(), value); + } + return results; + } +} diff --git a/core/src/com/hiveworkshop/json/JSONPointer.java b/core/src/com/hiveworkshop/json/JSONPointer.java new file mode 100644 index 0000000..fb13323 --- /dev/null +++ b/core/src/com/hiveworkshop/json/JSONPointer.java @@ -0,0 +1,293 @@ +package com.hiveworkshop.json; + +import static java.lang.String.format; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * A JSON Pointer is a simple query language defined for JSON documents by + * RFC 6901. + * + * In a nutshell, JSONPointer allows the user to navigate into a JSON document + * using strings, and retrieve targeted objects, like a simple form of XPATH. + * Path segments are separated by the '/' char, which signifies the root of + * the document when it appears as the first char of the string. Array + * elements are navigated using ordinals, counting from 0. JSONPointer strings + * may be extended to any arbitrary number of segments. If the navigation + * is successful, the matched item is returned. A matched item may be a + * JSONObject, a JSONArray, or a JSON value. If the JSONPointer string building + * fails, an appropriate exception is thrown. If the navigation fails to find + * a match, a JSONPointerException is thrown. + * + * @author JSON.org + * @version 2016-05-14 + */ +public class JSONPointer { + + // used for URL encoding and decoding + private static final String ENCODING = "utf-8"; + + /** + * This class allows the user to build a JSONPointer in steps, using + * exactly one segment in each step. + */ + public static class Builder { + + // Segments for the eventual JSONPointer string + private final List refTokens = new ArrayList(); + + /** + * Creates a {@code JSONPointer} instance using the tokens previously set using the + * {@link #append(String)} method calls. + */ + public JSONPointer build() { + return new JSONPointer(this.refTokens); + } + + /** + * Adds an arbitrary token to the list of reference tokens. It can be any non-null value. + * + * Unlike in the case of JSON string or URI fragment representation of JSON pointers, the + * argument of this method MUST NOT be escaped. If you want to query the property called + * {@code "a~b"} then you should simply pass the {@code "a~b"} string as-is, there is no + * need to escape it as {@code "a~0b"}. + * + * @param token the new token to be appended to the list + * @return {@code this} + * @throws NullPointerException if {@code token} is null + */ + public Builder append(String token) { + if (token == null) { + throw new NullPointerException("token cannot be null"); + } + this.refTokens.add(token); + return this; + } + + /** + * Adds an integer to the reference token list. Although not necessarily, mostly this token will + * denote an array index. + * + * @param arrayIndex the array index to be added to the token list + * @return {@code this} + */ + public Builder append(int arrayIndex) { + this.refTokens.add(String.valueOf(arrayIndex)); + return this; + } + } + + /** + * Static factory method for {@link Builder}. Example usage: + * + *


+     * JSONPointer pointer = JSONPointer.builder()
+     *       .append("obj")
+     *       .append("other~key").append("another/key")
+     *       .append("\"")
+     *       .append(0)
+     *       .build();
+     * 
+ * + * @return a builder instance which can be used to construct a {@code JSONPointer} instance by chained + * {@link Builder#append(String)} calls. + */ + public static Builder builder() { + return new Builder(); + } + + // Segments for the JSONPointer string + private final List refTokens; + + /** + * Pre-parses and initializes a new {@code JSONPointer} instance. If you want to + * evaluate the same JSON Pointer on different JSON documents then it is recommended + * to keep the {@code JSONPointer} instances due to performance considerations. + * + * @param pointer the JSON String or URI Fragment representation of the JSON pointer. + * @throws IllegalArgumentException if {@code pointer} is not a valid JSON pointer + */ + public JSONPointer(final String pointer) { + if (pointer == null) { + throw new NullPointerException("pointer cannot be null"); + } + if (pointer.isEmpty() || pointer.equals("#")) { + this.refTokens = Collections.emptyList(); + return; + } + String refs; + if (pointer.startsWith("#/")) { + refs = pointer.substring(2); + try { + refs = URLDecoder.decode(refs, ENCODING); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } else if (pointer.startsWith("/")) { + refs = pointer.substring(1); + } else { + throw new IllegalArgumentException("a JSON pointer should start with '/' or '#/'"); + } + this.refTokens = new ArrayList(); + int slashIdx = -1; + int prevSlashIdx = 0; + do { + prevSlashIdx = slashIdx + 1; + slashIdx = refs.indexOf('/', prevSlashIdx); + if(prevSlashIdx == slashIdx || prevSlashIdx == refs.length()) { + // found 2 slashes in a row ( obj//next ) + // or single slash at the end of a string ( obj/test/ ) + this.refTokens.add(""); + } else if (slashIdx >= 0) { + final String token = refs.substring(prevSlashIdx, slashIdx); + this.refTokens.add(unescape(token)); + } else { + // last item after separator, or no separator at all. + final String token = refs.substring(prevSlashIdx); + this.refTokens.add(unescape(token)); + } + } while (slashIdx >= 0); + // using split does not take into account consecutive separators or "ending nulls" + //for (String token : refs.split("/")) { + // this.refTokens.add(unescape(token)); + //} + } + + public JSONPointer(List refTokens) { + this.refTokens = new ArrayList(refTokens); + } + + private String unescape(String token) { + return token.replace("~1", "/").replace("~0", "~") + .replace("\\\"", "\"") + .replace("\\\\", "\\"); + } + + /** + * Evaluates this JSON Pointer on the given {@code document}. The {@code document} + * is usually a {@link JSONObject} or a {@link JSONArray} instance, but the empty + * JSON Pointer ({@code ""}) can be evaluated on any JSON values and in such case the + * returned value will be {@code document} itself. + * + * @param document the JSON document which should be the subject of querying. + * @return the result of the evaluation + * @throws JSONPointerException if an error occurs during evaluation + */ + public Object queryFrom(Object document) throws JSONPointerException { + if (this.refTokens.isEmpty()) { + return document; + } + Object current = document; + for (String token : this.refTokens) { + if (current instanceof JSONObject) { + current = ((JSONObject) current).opt(unescape(token)); + } else if (current instanceof JSONArray) { + current = readByIndexToken(current, token); + } else { + throw new JSONPointerException(format( + "value [%s] is not an array or object therefore its key %s cannot be resolved", current, + token)); + } + } + return current; + } + + /** + * Matches a JSONArray element by ordinal position + * @param current the JSONArray to be evaluated + * @param indexToken the array index in string form + * @return the matched object. If no matching item is found a + * @throws JSONPointerException is thrown if the index is out of bounds + */ + private Object readByIndexToken(Object current, String indexToken) throws JSONPointerException { + try { + int index = Integer.parseInt(indexToken); + JSONArray currentArr = (JSONArray) current; + if (index >= currentArr.length()) { + throw new JSONPointerException(format("index %s is out of bounds - the array has %d elements", indexToken, + Integer.valueOf(currentArr.length()))); + } + try { + return currentArr.get(index); + } catch (JSONException e) { + throw new JSONPointerException("Error reading value at index position " + index, e); + } + } catch (NumberFormatException e) { + throw new JSONPointerException(format("%s is not an array index", indexToken), e); + } + } + + /** + * Returns a string representing the JSONPointer path value using string + * representation + */ + @Override + public String toString() { + StringBuilder rval = new StringBuilder(""); + for (String token: this.refTokens) { + rval.append('/').append(escape(token)); + } + return rval.toString(); + } + + /** + * Escapes path segment values to an unambiguous form. + * The escape char to be inserted is '~'. The chars to be escaped + * are ~, which maps to ~0, and /, which maps to ~1. Backslashes + * and double quote chars are also escaped. + * @param token the JSONPointer segment value to be escaped + * @return the escaped value for the token + */ + private String escape(String token) { + return token.replace("~", "~0") + .replace("/", "~1") + .replace("\\", "\\\\") + .replace("\"", "\\\""); + } + + /** + * Returns a string representing the JSONPointer path value using URI + * fragment identifier representation + */ + public String toURIFragment() { + try { + StringBuilder rval = new StringBuilder("#"); + for (String token : this.refTokens) { + rval.append('/').append(URLEncoder.encode(token, ENCODING)); + } + return rval.toString(); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/core/src/com/hiveworkshop/json/JSONPointerException.java b/core/src/com/hiveworkshop/json/JSONPointerException.java new file mode 100644 index 0000000..10d746f --- /dev/null +++ b/core/src/com/hiveworkshop/json/JSONPointerException.java @@ -0,0 +1,45 @@ +package com.hiveworkshop.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * The JSONPointerException is thrown by {@link JSONPointer} if an error occurs + * during evaluating a pointer. + * + * @author JSON.org + * @version 2016-05-13 + */ +public class JSONPointerException extends JSONException { + private static final long serialVersionUID = 8872944667561856751L; + + public JSONPointerException(String message) { + super(message); + } + + public JSONPointerException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/com/hiveworkshop/json/JSONPropertyIgnore.java b/core/src/com/hiveworkshop/json/JSONPropertyIgnore.java new file mode 100644 index 0000000..cf0b428 --- /dev/null +++ b/core/src/com/hiveworkshop/json/JSONPropertyIgnore.java @@ -0,0 +1,43 @@ +package com.hiveworkshop.json; + +/* +Copyright (c) 2018 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(RUNTIME) +@Target({METHOD}) +/** + * Use this annotation on a getter method to override the Bean name + * parser for Bean -> JSONObject mapping. If this annotation is + * present at any level in the class hierarchy, then the method will + * not be serialized from the bean into the JSONObject. + */ +public @interface JSONPropertyIgnore { } diff --git a/core/src/com/hiveworkshop/json/JSONPropertyName.java b/core/src/com/hiveworkshop/json/JSONPropertyName.java new file mode 100644 index 0000000..22d10ad --- /dev/null +++ b/core/src/com/hiveworkshop/json/JSONPropertyName.java @@ -0,0 +1,47 @@ +package com.hiveworkshop.json; + +/* +Copyright (c) 2018 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(RUNTIME) +@Target({METHOD}) +/** + * Use this annotation on a getter method to override the Bean name + * parser for Bean -> JSONObject mapping. A value set to empty string "" + * will have the Bean parser fall back to the default field name processing. + */ +public @interface JSONPropertyName { + /** + * @return The name of the property as to be used in the JSON Object. + */ + String value(); +} diff --git a/core/src/com/hiveworkshop/json/JSONString.java b/core/src/com/hiveworkshop/json/JSONString.java new file mode 100644 index 0000000..444bc8a --- /dev/null +++ b/core/src/com/hiveworkshop/json/JSONString.java @@ -0,0 +1,18 @@ +package com.hiveworkshop.json; +/** + * The JSONString interface allows a toJSONString() + * method so that a class can change the behavior of + * JSONObject.toString(), JSONArray.toString(), + * and JSONWriter.value(Object). The + * toJSONString method will be used instead of the default behavior + * of using the Object's toString() method and quoting the result. + */ +public interface JSONString { + /** + * The toJSONString method allows a class to produce its own JSON + * serialization. + * + * @return A strictly syntactically correct JSON text. + */ + public String toJSONString(); +} diff --git a/core/src/com/hiveworkshop/json/JSONStringer.java b/core/src/com/hiveworkshop/json/JSONStringer.java new file mode 100644 index 0000000..7330d40 --- /dev/null +++ b/core/src/com/hiveworkshop/json/JSONStringer.java @@ -0,0 +1,79 @@ +package com.hiveworkshop.json; + +/* +Copyright (c) 2006 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.io.StringWriter; + +/** + * JSONStringer provides a quick and convenient way of producing JSON text. + * The texts produced strictly conform to JSON syntax rules. No whitespace is + * added, so the results are ready for transmission or storage. Each instance of + * JSONStringer can produce one JSON text. + *

+ * A JSONStringer instance provides a value method for appending + * values to the + * text, and a key + * method for adding keys before values in objects. There are array + * and endArray methods that make and bound array values, and + * object and endObject methods which make and bound + * object values. All of these methods return the JSONWriter instance, + * permitting cascade style. For example,

+ * myString = new JSONStringer()
+ *     .object()
+ *         .key("JSON")
+ *         .value("Hello, World!")
+ *     .endObject()
+ *     .toString();
which produces the string
+ * {"JSON":"Hello, World!"}
+ *

+ * The first method called must be array or object. + * There are no methods for adding commas or colons. JSONStringer adds them for + * you. Objects and arrays can be nested up to 20 levels deep. + *

+ * This can sometimes be easier than using a JSONObject to build a string. + * @author JSON.org + * @version 2015-12-09 + */ +public class JSONStringer extends JSONWriter { + /** + * Make a fresh JSONStringer. It can be used to build one JSON text. + */ + public JSONStringer() { + super(new StringWriter()); + } + + /** + * Return the JSON text. This method is used to obtain the product of the + * JSONStringer instance. It will return null if there was a + * problem in the construction of the JSON text (such as the calls to + * array were not properly balanced with calls to + * endArray). + * @return The JSON text. + */ + @Override + public String toString() { + return this.mode == 'd' ? this.writer.toString() : null; + } +} diff --git a/core/src/com/hiveworkshop/json/JSONTokener.java b/core/src/com/hiveworkshop/json/JSONTokener.java new file mode 100644 index 0000000..5d78054 --- /dev/null +++ b/core/src/com/hiveworkshop/json/JSONTokener.java @@ -0,0 +1,531 @@ +package com.hiveworkshop.json; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +/** + * A JSONTokener takes a source string and extracts characters and tokens from + * it. It is used by the JSONObject and JSONArray constructors to parse + * JSON source strings. + * @author JSON.org + * @version 2014-05-03 + */ +public class JSONTokener { + /** current read character position on the current line. */ + private long character; + /** flag to indicate if the end of the input has been found. */ + private boolean eof; + /** current read index of the input. */ + private long index; + /** current line of the input. */ + private long line; + /** previous character read from the input. */ + private char previous; + /** Reader for the input. */ + private final Reader reader; + /** flag to indicate that a previous character was requested. */ + private boolean usePrevious; + /** the number of characters read in the previous line. */ + private long characterPreviousLine; + + + /** + * Construct a JSONTokener from a Reader. The caller must close the Reader. + * + * @param reader A reader. + */ + public JSONTokener(Reader reader) { + this.reader = reader.markSupported() + ? reader + : new BufferedReader(reader); + this.eof = false; + this.usePrevious = false; + this.previous = 0; + this.index = 0; + this.character = 1; + this.characterPreviousLine = 0; + this.line = 1; + } + + + /** + * Construct a JSONTokener from an InputStream. The caller must close the input stream. + * @param inputStream The source. + */ + public JSONTokener(InputStream inputStream) { + this(new InputStreamReader(inputStream)); + } + + + /** + * Construct a JSONTokener from a string. + * + * @param s A source string. + */ + public JSONTokener(String s) { + this(new StringReader(s)); + } + + + /** + * Back up one character. This provides a sort of lookahead capability, + * so that you can test for a digit or letter before attempting to parse + * the next number or identifier. + * @throws JSONException Thrown if trying to step back more than 1 step + * or if already at the start of the string + */ + public void back() throws JSONException { + if (this.usePrevious || this.index <= 0) { + throw new JSONException("Stepping back two steps is not supported"); + } + this.decrementIndexes(); + this.usePrevious = true; + this.eof = false; + } + + /** + * Decrements the indexes for the {@link #back()} method based on the previous character read. + */ + private void decrementIndexes() { + this.index--; + if(this.previous=='\r' || this.previous == '\n') { + this.line--; + this.character=this.characterPreviousLine ; + } else if(this.character > 0){ + this.character--; + } + } + + /** + * Get the hex value of a character (base16). + * @param c A character between '0' and '9' or between 'A' and 'F' or + * between 'a' and 'f'. + * @return An int between 0 and 15, or -1 if c was not a hex digit. + */ + public static int dehexchar(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'F') { + return c - ('A' - 10); + } + if (c >= 'a' && c <= 'f') { + return c - ('a' - 10); + } + return -1; + } + + /** + * Checks if the end of the input has been reached. + * + * @return true if at the end of the file and we didn't step back + */ + public boolean end() { + return this.eof && !this.usePrevious; + } + + + /** + * Determine if the source string still contains characters that next() + * can consume. + * @return true if not yet at the end of the source. + * @throws JSONException thrown if there is an error stepping forward + * or backward while checking for more data. + */ + public boolean more() throws JSONException { + if(this.usePrevious) { + return true; + } + try { + this.reader.mark(1); + } catch (IOException e) { + throw new JSONException("Unable to preserve stream position", e); + } + try { + // -1 is EOF, but next() can not consume the null character '\0' + if(this.reader.read() <= 0) { + this.eof = true; + return false; + } + this.reader.reset(); + } catch (IOException e) { + throw new JSONException("Unable to read the next character from the stream", e); + } + return true; + } + + + /** + * Get the next character in the source string. + * + * @return The next character, or 0 if past the end of the source string. + * @throws JSONException Thrown if there is an error reading the source string. + */ + public char next() throws JSONException { + int c; + if (this.usePrevious) { + this.usePrevious = false; + c = this.previous; + } else { + try { + c = this.reader.read(); + } catch (IOException exception) { + throw new JSONException(exception); + } + } + if (c <= 0) { // End of stream + this.eof = true; + return 0; + } + this.incrementIndexes(c); + this.previous = (char) c; + return this.previous; + } + + /** + * Increments the internal indexes according to the previous character + * read and the character passed as the current character. + * @param c the current character read. + */ + private void incrementIndexes(int c) { + if(c > 0) { + this.index++; + if(c=='\r') { + this.line++; + this.characterPreviousLine = this.character; + this.character=0; + }else if (c=='\n') { + if(this.previous != '\r') { + this.line++; + this.characterPreviousLine = this.character; + } + this.character=0; + } else { + this.character++; + } + } + } + + /** + * Consume the next character, and check that it matches a specified + * character. + * @param c The character to match. + * @return The character. + * @throws JSONException if the character does not match. + */ + public char next(char c) throws JSONException { + char n = this.next(); + if (n != c) { + if(n > 0) { + throw this.syntaxError("Expected '" + c + "' and instead saw '" + + n + "'"); + } + throw this.syntaxError("Expected '" + c + "' and instead saw ''"); + } + return n; + } + + + /** + * Get the next n characters. + * + * @param n The number of characters to take. + * @return A string of n characters. + * @throws JSONException + * Substring bounds error if there are not + * n characters remaining in the source string. + */ + public String next(int n) throws JSONException { + if (n == 0) { + return ""; + } + + char[] chars = new char[n]; + int pos = 0; + + while (pos < n) { + chars[pos] = this.next(); + if (this.end()) { + throw this.syntaxError("Substring bounds error"); + } + pos += 1; + } + return new String(chars); + } + + + /** + * Get the next char in the string, skipping whitespace. + * @throws JSONException Thrown if there is an error reading the source string. + * @return A character, or 0 if there are no more characters. + */ + public char nextClean() throws JSONException { + for (;;) { + char c = this.next(); + if (c == 0 || c > ' ') { + return c; + } + } + } + + + /** + * Return the characters up to the next close quote character. + * Backslash processing is done. The formal JSON format does not + * allow strings in single quotes, but an implementation is allowed to + * accept them. + * @param quote The quoting character, either + * " (double quote) or + * ' (single quote). + * @return A String. + * @throws JSONException Unterminated string. + */ + public String nextString(char quote) throws JSONException { + char c; + StringBuilder sb = new StringBuilder(); + for (;;) { + c = this.next(); + switch (c) { + case 0: + case '\n': + case '\r': + throw this.syntaxError("Unterminated string"); + case '\\': + c = this.next(); + switch (c) { + case 'b': + sb.append('\b'); + break; + case 't': + sb.append('\t'); + break; + case 'n': + sb.append('\n'); + break; + case 'f': + sb.append('\f'); + break; + case 'r': + sb.append('\r'); + break; + case 'u': + try { + sb.append((char)Integer.parseInt(this.next(4), 16)); + } catch (NumberFormatException e) { + throw this.syntaxError("Illegal escape.", e); + } + break; + case '"': + case '\'': + case '\\': + case '/': + sb.append(c); + break; + default: + throw this.syntaxError("Illegal escape."); + } + break; + default: + if (c == quote) { + return sb.toString(); + } + sb.append(c); + } + } + } + + + /** + * Get the text up but not including the specified character or the + * end of line, whichever comes first. + * @param delimiter A delimiter character. + * @return A string. + * @throws JSONException Thrown if there is an error while searching + * for the delimiter + */ + public String nextTo(char delimiter) throws JSONException { + StringBuilder sb = new StringBuilder(); + for (;;) { + char c = this.next(); + if (c == delimiter || c == 0 || c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + + /** + * Get the text up but not including one of the specified delimiter + * characters or the end of line, whichever comes first. + * @param delimiters A set of delimiter characters. + * @return A string, trimmed. + * @throws JSONException Thrown if there is an error while searching + * for the delimiter + */ + public String nextTo(String delimiters) throws JSONException { + char c; + StringBuilder sb = new StringBuilder(); + for (;;) { + c = this.next(); + if (delimiters.indexOf(c) >= 0 || c == 0 || + c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + + /** + * Get the next value. The value can be a Boolean, Double, Integer, + * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object. + * @throws JSONException If syntax error. + * + * @return An object. + */ + public Object nextValue() throws JSONException { + char c = this.nextClean(); + String string; + + switch (c) { + case '"': + case '\'': + return this.nextString(c); + case '{': + this.back(); + return new JSONObject(this); + case '[': + this.back(); + return new JSONArray(this); + } + + /* + * Handle unquoted text. This could be the values true, false, or + * null, or it can be a number. An implementation (such as this one) + * is allowed to also accept non-standard forms. + * + * Accumulate characters until we reach the end of the text or a + * formatting character. + */ + + StringBuilder sb = new StringBuilder(); + while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { + sb.append(c); + c = this.next(); + } + if (!this.eof) { + this.back(); + } + + string = sb.toString().trim(); + if ("".equals(string)) { + throw this.syntaxError("Missing value"); + } + return JSONObject.stringToValue(string); + } + + + /** + * Skip characters until the next character is the requested character. + * If the requested character is not found, no characters are skipped. + * @param to A character to skip to. + * @return The requested character, or zero if the requested character + * is not found. + * @throws JSONException Thrown if there is an error while searching + * for the to character + */ + public char skipTo(char to) throws JSONException { + char c; + try { + long startIndex = this.index; + long startCharacter = this.character; + long startLine = this.line; + this.reader.mark(1000000); + do { + c = this.next(); + if (c == 0) { + // in some readers, reset() may throw an exception if + // the remaining portion of the input is greater than + // the mark size (1,000,000 above). + this.reader.reset(); + this.index = startIndex; + this.character = startCharacter; + this.line = startLine; + return 0; + } + } while (c != to); + this.reader.mark(1); + } catch (IOException exception) { + throw new JSONException(exception); + } + this.back(); + return c; + } + + /** + * Make a JSONException to signal a syntax error. + * + * @param message The error message. + * @return A JSONException object, suitable for throwing + */ + public JSONException syntaxError(String message) { + return new JSONException(message + this.toString()); + } + + /** + * Make a JSONException to signal a syntax error. + * + * @param message The error message. + * @param causedBy The throwable that caused the error. + * @return A JSONException object, suitable for throwing + */ + public JSONException syntaxError(String message, Throwable causedBy) { + return new JSONException(message + this.toString(), causedBy); + } + + /** + * Make a printable string of this JSONTokener. + * + * @return " at {index} [character {character} line {line}]" + */ + @Override + public String toString() { + return " at " + this.index + " [character " + this.character + " line " + + this.line + "]"; + } +} diff --git a/core/src/com/hiveworkshop/json/JSONWriter.java b/core/src/com/hiveworkshop/json/JSONWriter.java new file mode 100644 index 0000000..44bfdeb --- /dev/null +++ b/core/src/com/hiveworkshop/json/JSONWriter.java @@ -0,0 +1,413 @@ +package com.hiveworkshop.json; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; + +/* +Copyright (c) 2006 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * JSONWriter provides a quick and convenient way of producing JSON text. + * The texts produced strictly conform to JSON syntax rules. No whitespace is + * added, so the results are ready for transmission or storage. Each instance of + * JSONWriter can produce one JSON text. + *

+ * A JSONWriter instance provides a value method for appending + * values to the + * text, and a key + * method for adding keys before values in objects. There are array + * and endArray methods that make and bound array values, and + * object and endObject methods which make and bound + * object values. All of these methods return the JSONWriter instance, + * permitting a cascade style. For example,

+ * new JSONWriter(myWriter)
+ *     .object()
+ *         .key("JSON")
+ *         .value("Hello, World!")
+ *     .endObject();
which writes
+ * {"JSON":"Hello, World!"}
+ *

+ * The first method called must be array or object. + * There are no methods for adding commas or colons. JSONWriter adds them for + * you. Objects and arrays can be nested up to 200 levels deep. + *

+ * This can sometimes be easier than using a JSONObject to build a string. + * @author JSON.org + * @version 2016-08-08 + */ +public class JSONWriter { + private static final int maxdepth = 200; + + /** + * The comma flag determines if a comma should be output before the next + * value. + */ + private boolean comma; + + /** + * The current mode. Values: + * 'a' (array), + * 'd' (done), + * 'i' (initial), + * 'k' (key), + * 'o' (object). + */ + protected char mode; + + /** + * The object/array stack. + */ + private final JSONObject stack[]; + + /** + * The stack top index. A value of 0 indicates that the stack is empty. + */ + private int top; + + /** + * The writer that will receive the output. + */ + protected Appendable writer; + + /** + * Make a fresh JSONWriter. It can be used to build one JSON text. + */ + public JSONWriter(Appendable w) { + this.comma = false; + this.mode = 'i'; + this.stack = new JSONObject[maxdepth]; + this.top = 0; + this.writer = w; + } + + /** + * Append a value. + * @param string A string value. + * @return this + * @throws JSONException If the value is out of sequence. + */ + private JSONWriter append(String string) throws JSONException { + if (string == null) { + throw new JSONException("Null pointer"); + } + if (this.mode == 'o' || this.mode == 'a') { + try { + if (this.comma && this.mode == 'a') { + this.writer.append(','); + } + this.writer.append(string); + } catch (IOException e) { + // Android as of API 25 does not support this exception constructor + // however we won't worry about it. If an exception is happening here + // it will just throw a "Method not found" exception instead. + throw new JSONException(e); + } + if (this.mode == 'o') { + this.mode = 'k'; + } + this.comma = true; + return this; + } + throw new JSONException("Value out of sequence."); + } + + /** + * Begin appending a new array. All values until the balancing + * endArray will be appended to this array. The + * endArray method must be called to mark the array's end. + * @return this + * @throws JSONException If the nesting is too deep, or if the object is + * started in the wrong place (for example as a key or after the end of the + * outermost array or object). + */ + public JSONWriter array() throws JSONException { + if (this.mode == 'i' || this.mode == 'o' || this.mode == 'a') { + this.push(null); + this.append("["); + this.comma = false; + return this; + } + throw new JSONException("Misplaced array."); + } + + /** + * End something. + * @param m Mode + * @param c Closing character + * @return this + * @throws JSONException If unbalanced. + */ + private JSONWriter end(char m, char c) throws JSONException { + if (this.mode != m) { + throw new JSONException(m == 'a' + ? "Misplaced endArray." + : "Misplaced endObject."); + } + this.pop(m); + try { + this.writer.append(c); + } catch (IOException e) { + // Android as of API 25 does not support this exception constructor + // however we won't worry about it. If an exception is happening here + // it will just throw a "Method not found" exception instead. + throw new JSONException(e); + } + this.comma = true; + return this; + } + + /** + * End an array. This method most be called to balance calls to + * array. + * @return this + * @throws JSONException If incorrectly nested. + */ + public JSONWriter endArray() throws JSONException { + return this.end('a', ']'); + } + + /** + * End an object. This method most be called to balance calls to + * object. + * @return this + * @throws JSONException If incorrectly nested. + */ + public JSONWriter endObject() throws JSONException { + return this.end('k', '}'); + } + + /** + * Append a key. The key will be associated with the next value. In an + * object, every value must be preceded by a key. + * @param string A key string. + * @return this + * @throws JSONException If the key is out of place. For example, keys + * do not belong in arrays or if the key is null. + */ + public JSONWriter key(String string) throws JSONException { + if (string == null) { + throw new JSONException("Null key."); + } + if (this.mode == 'k') { + try { + JSONObject topObject = this.stack[this.top - 1]; + // don't use the built in putOnce method to maintain Android support + if(topObject.has(string)) { + throw new JSONException("Duplicate key \"" + string + "\""); + } + topObject.put(string, true); + if (this.comma) { + this.writer.append(','); + } + this.writer.append(JSONObject.quote(string)); + this.writer.append(':'); + this.comma = false; + this.mode = 'o'; + return this; + } catch (IOException e) { + // Android as of API 25 does not support this exception constructor + // however we won't worry about it. If an exception is happening here + // it will just throw a "Method not found" exception instead. + throw new JSONException(e); + } + } + throw new JSONException("Misplaced key."); + } + + + /** + * Begin appending a new object. All keys and values until the balancing + * endObject will be appended to this object. The + * endObject method must be called to mark the object's end. + * @return this + * @throws JSONException If the nesting is too deep, or if the object is + * started in the wrong place (for example as a key or after the end of the + * outermost array or object). + */ + public JSONWriter object() throws JSONException { + if (this.mode == 'i') { + this.mode = 'o'; + } + if (this.mode == 'o' || this.mode == 'a') { + this.append("{"); + this.push(new JSONObject()); + this.comma = false; + return this; + } + throw new JSONException("Misplaced object."); + + } + + + /** + * Pop an array or object scope. + * @param c The scope to close. + * @throws JSONException If nesting is wrong. + */ + private void pop(char c) throws JSONException { + if (this.top <= 0) { + throw new JSONException("Nesting error."); + } + char m = this.stack[this.top - 1] == null ? 'a' : 'k'; + if (m != c) { + throw new JSONException("Nesting error."); + } + this.top -= 1; + this.mode = this.top == 0 + ? 'd' + : this.stack[this.top - 1] == null + ? 'a' + : 'k'; + } + + /** + * Push an array or object scope. + * @param jo The scope to open. + * @throws JSONException If nesting is too deep. + */ + private void push(JSONObject jo) throws JSONException { + if (this.top >= maxdepth) { + throw new JSONException("Nesting too deep."); + } + this.stack[this.top] = jo; + this.mode = jo == null ? 'a' : 'k'; + this.top += 1; + } + + /** + * Make a JSON text of an Object value. If the object has an + * value.toJSONString() method, then that method will be used to produce the + * JSON text. The method is required to produce a strictly conforming text. + * If the object does not contain a toJSONString method (which is the most + * common case), then a text will be produced by other means. If the value + * is an array or Collection, then a JSONArray will be made from it and its + * toJSONString method will be called. If the value is a MAP, then a + * JSONObject will be made from it and its toJSONString method will be + * called. Otherwise, the value's toString method will be called, and the + * result will be quoted. + * + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @param value + * The value to be serialized. + * @return a printable, displayable, transmittable representation of the + * object, beginning with { (left + * brace) and ending with } (right + * brace). + * @throws JSONException + * If the value is or contains an invalid number. + */ + public static String valueToString(Object value) throws JSONException { + if (value == null || value.equals(null)) { + return "null"; + } + if (value instanceof JSONString) { + String object; + try { + object = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + if (object != null) { + return object; + } + throw new JSONException("Bad value from toJSONString: " + object); + } + if (value instanceof Number) { + // not all Numbers may match actual JSON Numbers. i.e. Fractions or Complex + final String numberAsString = JSONObject.numberToString((Number) value); + if(JSONObject.NUMBER_PATTERN.matcher(numberAsString).matches()) { + // Close enough to a JSON number that we will return it unquoted + return numberAsString; + } + // The Number value is not a valid JSON number. + // Instead we will quote it as a string + return JSONObject.quote(numberAsString); + } + if (value instanceof Boolean || value instanceof JSONObject + || value instanceof JSONArray) { + return value.toString(); + } + if (value instanceof Map) { + Map map = (Map) value; + return new JSONObject(map).toString(); + } + if (value instanceof Collection) { + Collection coll = (Collection) value; + return new JSONArray(coll).toString(); + } + if (value.getClass().isArray()) { + return new JSONArray(value).toString(); + } + if(value instanceof Enum){ + return JSONObject.quote(((Enum)value).name()); + } + return JSONObject.quote(value.toString()); + } + + /** + * Append either the value true or the value + * false. + * @param b A boolean. + * @return this + * @throws JSONException + */ + public JSONWriter value(boolean b) throws JSONException { + return this.append(b ? "true" : "false"); + } + + /** + * Append a double value. + * @param d A double. + * @return this + * @throws JSONException If the number is not finite. + */ + public JSONWriter value(double d) throws JSONException { + return this.value(Double.valueOf(d)); + } + + /** + * Append a long value. + * @param l A long. + * @return this + * @throws JSONException + */ + public JSONWriter value(long l) throws JSONException { + return this.append(Long.toString(l)); + } + + + /** + * Append an object value. + * @param object The object to append. It can be null, or a Boolean, Number, + * String, JSONObject, or JSONArray, or an object that implements JSONString. + * @return this + * @throws JSONException If the value is out of sequence. + */ + public JSONWriter value(Object object) throws JSONException { + return this.append(valueToString(object)); + } +} diff --git a/core/src/com/hiveworkshop/lang/Hex.java b/core/src/com/hiveworkshop/lang/Hex.java new file mode 100644 index 0000000..8dfd024 --- /dev/null +++ b/core/src/com/hiveworkshop/lang/Hex.java @@ -0,0 +1,82 @@ +package com.hiveworkshop.lang; + +import java.util.Arrays; + +public abstract class Hex { + private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', + 'E', 'F' }; + + public static final int RADIX_SIZE = 4; + + public static final int RADIX = HEX_DIGITS.length; + + private static final byte NO_VALUE = -1; + + private static final int DECIMAL_CHARACTERS = 10; + + private static final byte[] CHAR_VALUES = new byte[1 << Byte.SIZE]; + static { + Arrays.fill(CHAR_VALUES, NO_VALUE); + int value = 0; + for (; value < DECIMAL_CHARACTERS; value += 1) { + CHAR_VALUES['0' + value] = (byte) value; + } + for (; value < RADIX; value += 1) { + CHAR_VALUES[('a' - DECIMAL_CHARACTERS) + value] = (byte) value; + CHAR_VALUES[('A' - DECIMAL_CHARACTERS) + value] = (byte) value; + } + } + + /** + * Hexadecimal prefix string. + */ + public static final String HEX_PREFIX = "0x"; + + private static int NIBBLE_MASK = 0b1111; + + public static byte decodeNibble(final int codePoint) { + if (codePoint > CHAR_VALUES.length) { + return NO_VALUE; + } else { + return CHAR_VALUES[codePoint]; + } + } + + public static byte[] decodeHex(final CharSequence hex) { + final int nibbleCount = hex.length(); + int valueNibbleShift = ((nibbleCount - 1) % (Byte.SIZE / RADIX_SIZE)) * RADIX_SIZE; + final byte[] values = new byte[(nibbleCount + 1) >> 1]; + int valueIndex = 0; + int value = 0; + + for (int nibbleIndex = 0; nibbleIndex < nibbleCount; nibbleIndex += 1) { + final byte nibble = decodeNibble(hex.charAt(nibbleIndex)); + if (nibble == NO_VALUE) { + throw new NumberFormatException("non-hex character"); + } + + value |= nibble << valueNibbleShift; + + if (valueNibbleShift == 0) { + valueNibbleShift = Byte.SIZE; + values[valueIndex++] = (byte) value; + value = 0; + } + + valueNibbleShift -= RADIX_SIZE; + } + + return values; + } + + public static void stringBufferAppendHex(final StringBuilder builder, final byte hex) { + builder.append(HEX_DIGITS[(hex >> 4) & NIBBLE_MASK]); + builder.append(HEX_DIGITS[hex & NIBBLE_MASK]); + } + + public static void stringBufferAppendHex(final StringBuilder builder, final byte[] hex) { + for (int i = 0; i < hex.length; i += 1) { + stringBufferAppendHex(builder, hex[i]); + } + } +} diff --git a/core/src/com/hiveworkshop/nio/ByteBufferInputStream.java b/core/src/com/hiveworkshop/nio/ByteBufferInputStream.java new file mode 100644 index 0000000..f7eff2a --- /dev/null +++ b/core/src/com/hiveworkshop/nio/ByteBufferInputStream.java @@ -0,0 +1,39 @@ +package com.hiveworkshop.nio; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * Simple InputStream wrapper for ByteBuffer. + *

+ * This class is not thread safe. + *

+ * https://stackoverflow.com/questions/4332264/wrapping-a-bytebuffer-with-an-inputstream + */ +public class ByteBufferInputStream extends InputStream { + + ByteBuffer buf; + + public ByteBufferInputStream(ByteBuffer buf) { + this.buf = buf; + } + + public int read() throws IOException { + if (!buf.hasRemaining()) { + return -1; + } + return buf.get() & 0xFF; + } + + public int read(byte[] bytes, int off, int len) + throws IOException { + if (!buf.hasRemaining()) { + return -1; + } + + len = Math.min(len, buf.remaining()); + buf.get(bytes, off, len); + return len; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/AnimationMap.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/AnimationMap.java new file mode 100644 index 0000000..9e88301 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/AnimationMap.java @@ -0,0 +1,266 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import java.util.HashMap; +import java.util.Map; + +import com.etheller.warsmash.util.War3ID; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; + +/** + * A map from MDX animation tags to their equivalent MDL tokens, and the + * implementation objects. + * + *

+ * Based on the works of Chananya Freiman. + */ +public enum AnimationMap { + // Layer + /** + * Layer texture ID + */ + KMTF(MdlUtils.TOKEN_TEXTURE_ID, MdlxTimelineDescriptor.UINT32_TIMELINE), + /** + * Layer alpha + */ + KMTA(MdlUtils.TOKEN_ALPHA, MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Layer emissive gain + */ + KMTE("EmissiveGain", MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Layer fresnel color + */ + KFC3("FresnelColor", MdlxTimelineDescriptor.VECTOR3_TIMELINE), + /** + * Layer fresnel opacity + */ + KFCA("FresnelOpacity", MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Layer fresnel team color + */ + KFTC("FresnelTeamColor", MdlxTimelineDescriptor.UINT32_TIMELINE), + // TextureAnimation + /** + * Texture animation translation + */ + KTAT(MdlUtils.TOKEN_TRANSLATION, MdlxTimelineDescriptor.VECTOR3_TIMELINE), + /** + * Texture animation rotation + */ + KTAR(MdlUtils.TOKEN_ROTATION, MdlxTimelineDescriptor.VECTOR4_TIMELINE), + /** + * Texture animation scaling + */ + KTAS(MdlUtils.TOKEN_SCALING, MdlxTimelineDescriptor.VECTOR3_TIMELINE), + // GeosetAnimation + /** + * Geoset animation alpha + */ + KGAO(MdlUtils.TOKEN_ALPHA, MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Geoset animation color + */ + KGAC(MdlUtils.TOKEN_COLOR, MdlxTimelineDescriptor.VECTOR3_TIMELINE), + // Light + /** + * Light attenuation start + */ + KLAS(MdlUtils.TOKEN_ATTENUATION_START, MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Light attenuation end + */ + KLAE(MdlUtils.TOKEN_ATTENUATION_END, MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Light color + */ + KLAC(MdlUtils.TOKEN_COLOR, MdlxTimelineDescriptor.VECTOR3_TIMELINE), + /** + * Light intensity + */ + KLAI(MdlUtils.TOKEN_INTENSITY, MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Light ambient intensity + */ + KLBI(MdlUtils.TOKEN_AMB_INTENSITY, MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Light ambient color + */ + KLBC(MdlUtils.TOKEN_AMB_COLOR, MdlxTimelineDescriptor.VECTOR3_TIMELINE), + /** + * Light visibility + */ + KLAV(MdlUtils.TOKEN_VISIBILITY, MdlxTimelineDescriptor.FLOAT_TIMELINE), + // Attachment + /** + * Attachment visibility + */ + KATV(MdlUtils.TOKEN_VISIBILITY, MdlxTimelineDescriptor.FLOAT_TIMELINE), + // ParticleEmitter + /** + * Particle emitter emission rate + */ + KPEE(MdlUtils.TOKEN_EMISSION_RATE, MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Particle emitter gravity + */ + KPEG(MdlUtils.TOKEN_GRAVITY, MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Particle emitter longitude + */ + KPLN(MdlUtils.TOKEN_LONGITUDE, MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Particle emitter latitude + */ + KPLT(MdlUtils.TOKEN_LATITUDE, MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Particle emitter lifespan + */ + KPEL(MdlUtils.TOKEN_LIFE_SPAN, MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Particle emitter initial velocity + */ + KPES(MdlUtils.TOKEN_INIT_VELOCITY, MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Particle emitter visibility + */ + KPEV(MdlUtils.TOKEN_VISIBILITY, MdlxTimelineDescriptor.FLOAT_TIMELINE), + // ParticleEmitter2 + /** + * Particle emitter 2 speed + */ + KP2S("Speed", MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Particle emitter 2 variation + */ + KP2R("Variation", MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Particle emitter 2 latitude + */ + KP2L("Latitude", MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Particle emitter 2 gravity + */ + KP2G("Gravity", MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Particle emitter 2 emission rate + */ + KP2E("EmissionRate", MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Particle emitter 2 length + */ + KP2N("Length", MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Particle emitter 2 width + */ + KP2W("Width", MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Particle emitter 2 visibility + */ + KP2V("Visibility", MdlxTimelineDescriptor.FLOAT_TIMELINE), + // ParticleEmitterCorn + /** + * Popcorn emitter alpha + */ + KPPA("Alpha", MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Popcorn emitter color + */ + KPPC("Color", MdlxTimelineDescriptor.VECTOR3_TIMELINE), + /** + * Popcorn emitter emission rate + */ + KPPE("EmissionRate", MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Popcorn emitter lifespan + */ + KPPL("LifeSpan", MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Popcorn emitter speed + */ + KPPS("Speed", MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Popcorn emitter visibility + */ + KPPV("Visibility", MdlxTimelineDescriptor.FLOAT_TIMELINE), + // RibbonEmitter + /** + * Ribbon emitter height above + */ + KRHA("HeightAbove", MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Ribbon emitter height below + */ + KRHB("HeightBelow", MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Ribbon emitter alpha + */ + KRAL("Alpha", MdlxTimelineDescriptor.FLOAT_TIMELINE), + /** + * Ribbon emitter color + */ + KRCO("Color", MdlxTimelineDescriptor.VECTOR3_TIMELINE), + /** + * Ribbon emitter texture slot + */ + KRTX("TextureSlot", MdlxTimelineDescriptor.UINT32_TIMELINE), + /** + * Ribbon emitter visibility + */ + KRVS("Visibility", MdlxTimelineDescriptor.FLOAT_TIMELINE), + // Camera + /** + * Camera source translation + */ + KCTR(MdlUtils.TOKEN_TRANSLATION, MdlxTimelineDescriptor.VECTOR3_TIMELINE), + /** + * Camera target translation + */ + KTTR(MdlUtils.TOKEN_TRANSLATION, MdlxTimelineDescriptor.VECTOR3_TIMELINE), + /** + * Camera source rotation + */ + KCRL(MdlUtils.TOKEN_ROTATION, MdlxTimelineDescriptor.FLOAT_TIMELINE), + // GenericObject + /** + * Generic object translation + */ + KGTR(MdlUtils.TOKEN_TRANSLATION, MdlxTimelineDescriptor.VECTOR3_TIMELINE), + /** + * Generic object rotation + */ + KGRT(MdlUtils.TOKEN_ROTATION, MdlxTimelineDescriptor.VECTOR4_TIMELINE), + /** + * Generic object scaling + */ + KGSC(MdlUtils.TOKEN_SCALING, MdlxTimelineDescriptor.VECTOR3_TIMELINE); + + private final String mdlToken; + private final MdlxTimelineDescriptor implementation; + private final War3ID war3id; + + AnimationMap(final String mdlToken, final MdlxTimelineDescriptor implementation) { + this.mdlToken = mdlToken; + this.implementation = implementation; + this.war3id = War3ID.fromString(name()); + } + + public String getMdlToken() { + return this.mdlToken; + } + + public MdlxTimelineDescriptor getImplementation() { + return this.implementation; + } + + public War3ID getWar3id() { + return this.war3id; + } + + public static final Map ID_TO_TAG = new HashMap<>(); + + static { + for (final AnimationMap tag : AnimationMap.values()) { + ID_TO_TAG.put(tag.getWar3id(), tag); + } + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/InterpolationType.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/InterpolationType.java new file mode 100644 index 0000000..3856a18 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/InterpolationType.java @@ -0,0 +1,26 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +public enum InterpolationType { + DONT_INTERP("DontInterp"), LINEAR("Linear"), HERMITE("Hermite"), BEZIER("Bezier"); + + public static final InterpolationType[] VALUES = values(); + + private final String token; + + InterpolationType(final String token) { + this.token = token; + } + + public static InterpolationType getType(final int whichValue) { + return VALUES[whichValue]; + } + + public boolean tangential() { + return ordinal() > 1; + } + + @Override + public String toString() { + return token; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxAnimatedObject.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxAnimatedObject.java new file mode 100644 index 0000000..16e9460 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxAnimatedObject.java @@ -0,0 +1,102 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.parsers.mdlx.timeline.MdlxTimeline; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +/** + * Based on the works of Chananya Freiman. + */ +public abstract class MdlxAnimatedObject implements MdlxChunk, MdlxBlock { + public final List> timelines = new ArrayList<>(); + + public void readTimelines(final BinaryReader reader, long size) { + while (size > 0) { + final War3ID name = new War3ID(reader.readTag()); + final MdlxTimeline timeline = AnimationMap.ID_TO_TAG.get(name).getImplementation().createTimeline(); + + timeline.readMdx(reader, name); + + size -= timeline.getByteLength(); + + this.timelines.add(timeline); + } + } + + public void writeTimelines(final BinaryWriter writer) { + for (final MdlxTimeline timeline : this.timelines) { + timeline.writeMdx(writer); + } + } + + public Iterator readAnimatedBlock(final MdlTokenInputStream stream) { + return new TransformedAnimatedBlockIterator(stream.readBlock().iterator()); + } + + public void readTimeline(final MdlTokenInputStream stream, final AnimationMap name) { + final MdlxTimeline timeline = name.getImplementation().createTimeline(); + + timeline.readMdl(stream, name.getWar3id()); + + this.timelines.add(timeline); + } + + public boolean writeTimeline(final MdlTokenOutputStream stream, final AnimationMap name) { + for (final MdlxTimeline timeline : this.timelines) { + if (timeline.name.equals(name.getWar3id())) { + timeline.writeMdl(stream); + return true; + } + } + return false; + } + + @Override + public long getByteLength(final int version) { + long size = 0; + for (final MdlxTimeline timeline : this.timelines) { + size += timeline.getByteLength(); + } + return size; + } + + public List> getTimelines() { + return this.timelines; + } + + /** + * TODO: This code uses StringBuilder implicitly during string concat. This + * should be upgraded for performance. + * + * @author micro + */ + private static final class TransformedAnimatedBlockIterator implements Iterator { + private final Iterator delegate; + + public TransformedAnimatedBlockIterator(final Iterator delegate) { + this.delegate = delegate; + } + + @Override + public boolean hasNext() { + return this.delegate.hasNext(); + } + + @Override + public String next() { + final String token = this.delegate.next(); + if (token.equals(MdlUtils.TOKEN_STATIC) && hasNext()) { + return MdlUtils.TOKEN_STATIC + " " + this.delegate.next(); + } + return token; + } + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxAttachment.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxAttachment.java new file mode 100644 index 0000000..28a23fe --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxAttachment.java @@ -0,0 +1,105 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxAttachment extends MdlxGenericObject { + public String path = ""; + public int attachmentId = 0; + + public MdlxAttachment() { + super(0x800); + } + + @Override + public void readMdx(final BinaryReader reader, final int version) { + final int position = reader.position(); + final long size = reader.readUInt32(); + + super.readMdx(reader, version); + + this.path = reader.read(260); + this.attachmentId = reader.readInt32(); + + readTimelines(reader, size - (reader.position() - position)); + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeUInt32(getByteLength(version)); + + super.writeMdx(writer, version); + + writer.writeWithNulls(this.path, 260); + writer.writeInt32(this.attachmentId); + + writeNonGenericAnimationChunks(writer); + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + for (final String token : super.readMdlGeneric(stream)) { + if (MdlUtils.TOKEN_ATTACHMENT_ID.equals(token)) { + this.attachmentId = stream.readInt(); + } + else if (MdlUtils.TOKEN_PATH.equals(token)) { + this.path = stream.read(); + } + else if (MdlUtils.TOKEN_VISIBILITY.equals(token)) { + readTimeline(stream, AnimationMap.KATV); + } + else { + throw new RuntimeException("Unknown token in Attachment " + this.name + ": " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startObjectBlock(MdlUtils.TOKEN_ATTACHMENT, this.name); + writeGenericHeader(stream); + + // flowtsohg asks in his JS: + // Is this needed? MDX supplies it, but MdlxConv does not use it. + // Retera: + // I tried to preserve it when it was shown, but omit it when it was omitted + // for MDL in Matrix Eater. Matrix Eater's MDL -> MDX is generating them + // and discarding what was read from the MDL. The Matrix Eater is notably + // buggy from a cursory read through, and would always omit AttachmentID 0 + // in MDL output. + stream.writeAttrib(MdlUtils.TOKEN_ATTACHMENT_ID, this.attachmentId); + + if ((this.path != null) && (this.path.length() > 0)) { + stream.writeStringAttrib(MdlUtils.TOKEN_PATH, this.path); + } + + writeTimeline(stream, AnimationMap.KATV); + + writeGenericTimelines(stream); + stream.endBlock(); + } + + @Override + public long getByteLength(final int version) { + return 268 + super.getByteLength(version); + } + + public String getPath() { + return this.path; + } + + public int getAttachmentId() { + return this.attachmentId; + } + + public void setPath(final String path) { + this.path = path; + } + + public void setAttachmentId(final int attachmentId) { + this.attachmentId = attachmentId; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxBlock.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxBlock.java new file mode 100644 index 0000000..08d92a7 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxBlock.java @@ -0,0 +1,16 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public interface MdlxBlock { + void readMdx(final BinaryReader reader, final int version); + + void writeMdx(final BinaryWriter writer, final int version); + + void readMdl(final MdlTokenInputStream stream, final int version); + + void writeMdl(final MdlTokenOutputStream stream, final int version); +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxBlockDescriptor.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxBlockDescriptor.java new file mode 100644 index 0000000..a45ae6d --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxBlockDescriptor.java @@ -0,0 +1,44 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.util.Descriptor; + +public interface MdlxBlockDescriptor extends Descriptor { + + MdlxBlockDescriptor ATTACHMENT = MdlxAttachment::new; + + MdlxBlockDescriptor BONE = MdlxBone::new; + + MdlxBlockDescriptor CAMERA = MdlxCamera::new; + + MdlxBlockDescriptor COLLISION_SHAPE = MdlxCollisionShape::new; + + MdlxBlockDescriptor EVENT_OBJECT = MdlxEventObject::new; + + MdlxBlockDescriptor GEOSET = MdlxGeoset::new; + + MdlxBlockDescriptor GEOSET_ANIMATION = MdlxGeosetAnimation::new; + + MdlxBlockDescriptor HELPER = MdlxHelper::new; + + MdlxBlockDescriptor LIGHT = MdlxLight::new; + + MdlxBlockDescriptor LAYER = MdlxLayer::new; + + MdlxBlockDescriptor MATERIAL = MdlxMaterial::new; + + MdlxBlockDescriptor PARTICLE_EMITTER = MdlxParticleEmitter::new; + + MdlxBlockDescriptor PARTICLE_EMITTER2 = MdlxParticleEmitter2::new; + + MdlxBlockDescriptor PARTICLE_EMITTER_POPCORN = MdlxParticleEmitterPopcorn::new; + + MdlxBlockDescriptor RIBBON_EMITTER = MdlxRibbonEmitter::new; + + MdlxBlockDescriptor SEQUENCE = MdlxSequence::new; + + MdlxBlockDescriptor TEXTURE = MdlxTexture::new; + + MdlxBlockDescriptor TEXTURE_ANIMATION = MdlxTextureAnimation::new; + + MdlxBlockDescriptor FACE_EFFECT = MdlxFaceEffect::new; +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxBone.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxBone.java new file mode 100644 index 0000000..3bc443a --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxBone.java @@ -0,0 +1,104 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxBone extends MdlxGenericObject { + public int geosetId = -1; + public int geosetAnimationId = -1; + + public MdlxBone() { + super(0x100); + } + + @Override + public void readMdx(final BinaryReader reader, final int version) { + super.readMdx(reader, version); + + this.geosetId = reader.readInt32(); + this.geosetAnimationId = reader.readInt32(); + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + super.writeMdx(writer, version); + + writer.writeInt32(this.geosetId); + writer.writeInt32(this.geosetAnimationId); + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + for (String token : super.readMdlGeneric(stream)) { + if (MdlUtils.TOKEN_GEOSETID.equals(token)) { + token = stream.read(); + + if (MdlUtils.TOKEN_MULTIPLE.equals(token)) { + this.geosetId = -1; + } + else { + this.geosetId = Integer.parseInt(token); + } + } + else if (MdlUtils.TOKEN_GEOSETANIMID.equals(token)) { + token = stream.read(); + + if (MdlUtils.TOKEN_NONE.equals(token)) { + this.geosetAnimationId = -1; + } + else { + this.geosetAnimationId = Integer.parseInt(token); + } + } + else { + throw new RuntimeException("Unknown token in Bone " + this.name + ": " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startObjectBlock(MdlUtils.TOKEN_BONE, this.name); + writeGenericHeader(stream); + + if (this.geosetId == -1) { + stream.writeAttrib(MdlUtils.TOKEN_GEOSETID, MdlUtils.TOKEN_MULTIPLE); + } + else { + stream.writeAttrib(MdlUtils.TOKEN_GEOSETID, this.geosetId); + } + if (this.geosetAnimationId == -1) { + stream.writeAttrib(MdlUtils.TOKEN_GEOSETANIMID, MdlUtils.TOKEN_NONE); + } + else { + stream.writeAttrib(MdlUtils.TOKEN_GEOSETANIMID, this.geosetAnimationId); + } + + writeGenericTimelines(stream); + stream.endBlock(); + } + + @Override + public long getByteLength(final int version) { + return 8 + super.getByteLength(version); + } + + public int getGeosetId() { + return this.geosetId; + } + + public int getGeosetAnimationId() { + return this.geosetAnimationId; + } + + public void setGeosetId(final int geosetId) { + this.geosetId = geosetId; + } + + public void setGeosetAnimationId(final int geosetAnimationId) { + this.geosetAnimationId = geosetAnimationId; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxCamera.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxCamera.java new file mode 100644 index 0000000..3d257a0 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxCamera.java @@ -0,0 +1,160 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxCamera extends MdlxAnimatedObject { + public String name = ""; + public float[] position = new float[3]; + public float fieldOfView = 0; + public float farClippingPlane = 0; + public float nearClippingPlane = 0; + public float[] targetPosition = new float[3]; + + @Override + public void readMdx(final BinaryReader reader, final int version) { + final long size = reader.readUInt32(); + + this.name = reader.read(80); + reader.readFloat32Array(this.position); + this.fieldOfView = reader.readFloat32(); + this.farClippingPlane = reader.readFloat32(); + this.nearClippingPlane = reader.readFloat32(); + reader.readFloat32Array(this.targetPosition); + + readTimelines(reader, size - 120); + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeUInt32(getByteLength(version)); + writer.writeWithNulls(this.name, 80); + writer.writeFloat32Array(this.position); + writer.writeFloat32(this.fieldOfView); + writer.writeFloat32(this.farClippingPlane); + writer.writeFloat32(this.nearClippingPlane); + writer.writeFloat32Array(this.targetPosition); + + writeTimelines(writer); + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + this.name = stream.read(); + + for (final String token : stream.readBlock()) { + switch (token) { + case MdlUtils.TOKEN_POSITION: + stream.readFloatArray(this.position); + break; + case MdlUtils.TOKEN_TRANSLATION: + readTimeline(stream, AnimationMap.KCTR); + break; + case MdlUtils.TOKEN_ROTATION: + readTimeline(stream, AnimationMap.KCRL); + break; + case MdlUtils.TOKEN_FIELDOFVIEW: + this.fieldOfView = stream.readFloat(); + break; + case MdlUtils.TOKEN_FARCLIP: + this.farClippingPlane = stream.readFloat(); + break; + case MdlUtils.TOKEN_NEARCLIP: + this.nearClippingPlane = stream.readFloat(); + break; + case MdlUtils.TOKEN_TARGET: + for (final String subToken : stream.readBlock()) { + switch (subToken) { + case MdlUtils.TOKEN_POSITION: + stream.readFloatArray(this.targetPosition); + break; + case MdlUtils.TOKEN_TRANSLATION: + readTimeline(stream, AnimationMap.KTTR); + break; + default: + throw new IllegalStateException( + "Unknown token in Camera " + this.name + "'s Target: " + subToken); + } + } + break; + default: + throw new RuntimeException("Unknown token in Camera " + this.name + ": " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startObjectBlock(MdlUtils.TOKEN_CAMERA, this.name); + + stream.writeFloatArrayAttrib(MdlUtils.TOKEN_POSITION, this.position); + writeTimeline(stream, AnimationMap.KCTR); + writeTimeline(stream, AnimationMap.KCRL); + stream.writeFloatAttrib(MdlUtils.TOKEN_FIELDOFVIEW, this.fieldOfView); + stream.writeFloatAttrib(MdlUtils.TOKEN_FARCLIP, this.farClippingPlane); + stream.writeFloatAttrib(MdlUtils.TOKEN_NEARCLIP, this.nearClippingPlane); + + stream.startBlock(MdlUtils.TOKEN_TARGET); + stream.writeFloatArrayAttrib(MdlUtils.TOKEN_POSITION, this.targetPosition); + writeTimeline(stream, AnimationMap.KTTR); + stream.endBlock(); + + stream.endBlock(); + } + + @Override + public long getByteLength(final int version) { + return 120 + super.getByteLength(version); + } + + public String getName() { + return this.name; + } + + public float[] getPosition() { + return this.position; + } + + public float getFieldOfView() { + return this.fieldOfView; + } + + public float getFarClippingPlane() { + return this.farClippingPlane; + } + + public float getNearClippingPlane() { + return this.nearClippingPlane; + } + + public float[] getTargetPosition() { + return this.targetPosition; + } + + public void setName(final String name) { + this.name = name; + } + + public void setPosition(final float[] position) { + this.position = position; + } + + public void setFieldOfView(final float fieldOfView) { + this.fieldOfView = fieldOfView; + } + + public void setFarClippingPlane(final float farClippingPlane) { + this.farClippingPlane = farClippingPlane; + } + + public void setNearClippingPlane(final float nearClippingPlane) { + this.nearClippingPlane = nearClippingPlane; + } + + public void setTargetPosition(final float[] targetPosition) { + this.targetPosition = targetPosition; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxChunk.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxChunk.java new file mode 100644 index 0000000..3a2b79b --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxChunk.java @@ -0,0 +1,5 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +public interface MdlxChunk { + long getByteLength(int version); +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxCollisionShape.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxCollisionShape.java new file mode 100644 index 0000000..840301f --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxCollisionShape.java @@ -0,0 +1,187 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxCollisionShape extends MdlxGenericObject { + public enum Type { + BOX, + PLANE, + SPHERE, + CYLINDER; + + private static final Type[] VALUES = values(); + + private final boolean boundsRadius; + + Type() { + this.boundsRadius = false; + } + + Type(final boolean boundsRadius) { + this.boundsRadius = boundsRadius; + } + + public boolean isBoundsRadius() { + return this.boundsRadius; + } + + public static Type from(final int index) { + return VALUES[index]; + } + } + + public MdlxCollisionShape.Type type = Type.SPHERE; + public final float[][] vertices = { new float[3], new float[3] }; + public float boundsRadius = 0; + + public MdlxCollisionShape() { + super(0x2000); + } + + @Override + public void readMdx(final BinaryReader reader, final int version) { + super.readMdx(reader, version); + + this.type = MdlxCollisionShape.Type.from(reader.readInt32()); + reader.readFloat32Array(this.vertices[0]); + + if (this.type != Type.SPHERE) { + reader.readFloat32Array(this.vertices[1]); + } + + if ((this.type == Type.SPHERE) || (this.type == Type.CYLINDER)) { + this.boundsRadius = reader.readFloat32(); + } + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + super.writeMdx(writer, version); + + writer.writeInt32(this.type.ordinal()); + writer.writeFloat32Array(this.vertices[0]); + + if (this.type != MdlxCollisionShape.Type.SPHERE) { + writer.writeFloat32Array(this.vertices[1]); + } + + if ((this.type == Type.SPHERE) || (this.type == Type.CYLINDER)) { + writer.writeFloat32(this.boundsRadius); + } + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + for (final String token : super.readMdlGeneric(stream)) { + switch (token) { + case MdlUtils.TOKEN_BOX: + this.type = Type.BOX; + break; + case MdlUtils.TOKEN_PLANE: + this.type = Type.PLANE; + break; + case MdlUtils.TOKEN_SPHERE: + this.type = Type.SPHERE; + break; + case MdlUtils.TOKEN_CYLINDER: + this.type = Type.CYLINDER; + break; + case MdlUtils.TOKEN_VERTICES: { + final int count = stream.readInt(); + stream.read(); // { + stream.readFloatArray(this.vertices[0]); + if (count == 2) { + stream.readFloatArray(this.vertices[1]); + } + stream.read(); // } + } + break; + case MdlUtils.TOKEN_BOUNDSRADIUS: + this.boundsRadius = stream.readFloat(); + break; + default: + throw new RuntimeException("Unknown token in CollisionShape " + this.name + ": " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startObjectBlock(MdlUtils.TOKEN_COLLISION_SHAPE, this.name); + writeGenericHeader(stream); + final String type; + int vertices = 2; + switch (this.type) { + case BOX: + type = MdlUtils.TOKEN_BOX; + break; + case PLANE: + type = MdlUtils.TOKEN_PLANE; + break; + case SPHERE: { + type = MdlUtils.TOKEN_SPHERE; + vertices = 1; + } + break; + case CYLINDER: + type = MdlUtils.TOKEN_CYLINDER; + break; + default: + throw new IllegalStateException("Invalid type in CollisionShape " + this.name + ": " + this.type); + } + + stream.writeFlag(type); + stream.startBlock(MdlUtils.TOKEN_VERTICES, vertices); + stream.writeFloatArray(this.vertices[0]); + if (vertices == 2) { + stream.writeFloatArray(this.vertices[1]); + } + stream.endBlock(); + + if (this.type.boundsRadius) { + stream.writeFloatAttrib(MdlUtils.TOKEN_BOUNDSRADIUS, this.boundsRadius); + } + + writeGenericTimelines(stream); + stream.endBlock(); + } + + @Override + public long getByteLength(final int version) { + long size = 16 + super.getByteLength(version); + + if (this.type != Type.SPHERE) { + size += 12; + } + + if ((this.type == Type.SPHERE) || (this.type == Type.CYLINDER)) { + size += 4; + } + + return size; + } + + public MdlxCollisionShape.Type getType() { + return this.type; + } + + public float[][] getVertices() { + return this.vertices; + } + + public float getBoundsRadius() { + return this.boundsRadius; + } + + public void setType(final MdlxCollisionShape.Type type) { + this.type = type; + } + + public void setBoundsRadius(final float boundsRadius) { + this.boundsRadius = boundsRadius; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxEventObject.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxEventObject.java new file mode 100644 index 0000000..fa20996 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxEventObject.java @@ -0,0 +1,99 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.etheller.warsmash.util.War3ID; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxEventObject extends MdlxGenericObject { + private static final War3ID KEVT = War3ID.fromString("KEVT"); + + public int globalSequenceId = -1; + public long[] keyFrames = { 1 }; + + public MdlxEventObject() { + super(0x400); + } + + @Override + public void readMdx(final BinaryReader reader, final int version) { + super.readMdx(reader, version); + + reader.readInt32(); // KEVT skipped + + final long count = reader.readUInt32(); + + this.globalSequenceId = reader.readInt32(); + + this.keyFrames = new long[(int) count]; + + for (int i = 0; i < count; i++) { + this.keyFrames[i] = reader.readInt32(); + } + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + super.writeMdx(writer, version); + + writer.writeTag(KEVT.getValue()); + writer.writeUInt32(this.keyFrames.length); + writer.writeInt32(this.globalSequenceId); + + for (final long keyFrame : this.keyFrames) { + writer.writeUInt32(keyFrame); + } + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + for (final String token : super.readMdlGeneric(stream)) { + if (MdlUtils.TOKEN_EVENT_TRACK.equals(token)) { + this.keyFrames = new long[stream.readInt()]; + stream.readIntArray(this.keyFrames); + } + else { + throw new RuntimeException("Unknown token in EventObject " + this.name + ": " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startObjectBlock(MdlUtils.TOKEN_EVENT_OBJECT, this.name); + writeGenericHeader(stream); + stream.startBlock(MdlUtils.TOKEN_EVENT_TRACK, this.keyFrames.length); + + for (final long keyFrame : this.keyFrames) { + stream.writeFlagUInt32(keyFrame); + } + + stream.endBlock(); + + writeGenericTimelines(stream); + stream.endBlock(); + } + + @Override + public long getByteLength(final int version) { + return 12 + (this.keyFrames.length * 4) + super.getByteLength(version); + } + + public int getGlobalSequenceId() { + return this.globalSequenceId; + } + + public long[] getKeyFrames() { + return this.keyFrames; + } + + public void setGlobalSequenceId(final int globalSequenceId) { + this.globalSequenceId = globalSequenceId; + } + + public void setKeyFrames(final long[] keyFrames) { + this.keyFrames = keyFrames; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxExtent.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxExtent.java new file mode 100644 index 0000000..1dd590d --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxExtent.java @@ -0,0 +1,62 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxExtent { + public float boundsRadius = 0; + public float[] min = new float[3]; + public float[] max = new float[3]; + + public void readMdx(final BinaryReader reader) { + this.boundsRadius = reader.readFloat32(); + reader.readFloat32Array(this.min); + reader.readFloat32Array(this.max); + } + + public void writeMdx(final BinaryWriter writer) { + writer.writeFloat32(this.boundsRadius); + writer.writeFloat32Array(this.min); + writer.writeFloat32Array(this.max); + } + + public void writeMdl(final MdlTokenOutputStream stream) { + if ((this.min[0] != 0) || (this.min[1] != 0) || (this.min[2] != 0)) { + stream.writeFloatArrayAttrib(MdlUtils.TOKEN_MINIMUM_EXTENT, this.min); + } + + if ((this.max[0] != 0) || (this.max[1] != 0) || (this.max[2] != 0)) { + stream.writeFloatArrayAttrib(MdlUtils.TOKEN_MAXIMUM_EXTENT, this.max); + } + + if (this.boundsRadius != 0) { + stream.writeFloatAttrib(MdlUtils.TOKEN_BOUNDSRADIUS, this.boundsRadius); + } + } + + public float getBoundsRadius() { + return this.boundsRadius; + } + + public float[] getMin() { + return this.min; + } + + public float[] getMax() { + return this.max; + } + + public void setBoundsRadius(final float boundsRadius) { + this.boundsRadius = boundsRadius; + } + + public void setMin(final float[] min) { + this.min = min; + } + + public void setMax(final float[] max) { + this.max = max; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxFaceEffect.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxFaceEffect.java new file mode 100644 index 0000000..47cb82b --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxFaceEffect.java @@ -0,0 +1,45 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxFaceEffect implements MdlxBlock { + public String type = ""; + public String path = ""; + + @Override + public void readMdx(final BinaryReader reader, final int version) { + type = reader.read(80); + path = reader.read(260); + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeWithNulls(type, 80); + writer.writeWithNulls(path, 260); + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + type = stream.read(); + + for (final String token : stream.readBlock()) { + if (token.equals("Path")) { + path = stream.read(); + } else { + throw new IllegalStateException("Unknown token in MdlxFaceEffect: " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startObjectBlock("FaceFX", type); + + stream.writeStringAttrib("Path", path); + + stream.endBlock(); + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxGenericObject.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxGenericObject.java new file mode 100644 index 0000000..12e9ee2 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxGenericObject.java @@ -0,0 +1,278 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import java.util.Iterator; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.parsers.mdlx.timeline.MdlxTimeline; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +/** + * A generic object. + *

+ * The parent class for all objects that exist in the world, and may contain + * spatial animations. This includes bones, particle emitters, and many other + * things. + *

+ * Based on the works of Chananya Freiman. + */ +public abstract class MdlxGenericObject extends MdlxAnimatedObject { + public String name = ""; + public int objectId = -1; + public int parentId = -1; + public int flags = 0; + + public MdlxGenericObject(final int flags) { + this.flags = flags; + } + + @Override + public void readMdx(final BinaryReader reader, final int version) { + final long size = reader.readUInt32(); + + this.name = reader.read(80); + this.objectId = reader.readInt32(); + this.parentId = reader.readInt32(); + this.flags = reader.readInt32(); + + readTimelines(reader, size - 96); + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeUInt32(getGenericByteLength(version)); + writer.writeWithNulls(this.name, 80); + writer.writeInt32(this.objectId); + writer.writeInt32(this.parentId); + writer.writeInt32(this.flags); + + for (final MdlxTimeline timeline : this.timelines) { + if (isGeneric(timeline)) { + timeline.writeMdx(writer); + } + } + } + + public void writeNonGenericAnimationChunks(final BinaryWriter writer) { + for (final MdlxTimeline timeline : this.timelines) { + if (!isGeneric(timeline)) { + timeline.writeMdx(writer); + } + } + } + + protected final Iterable readMdlGeneric(final MdlTokenInputStream stream) { + this.name = stream.read(); + return () -> new WrappedMdlTokenIterator(readAnimatedBlock(stream), MdlxGenericObject.this, stream); + } + + public void writeGenericHeader(final MdlTokenOutputStream stream) { + stream.writeAttrib(MdlUtils.TOKEN_OBJECTID, this.objectId); + + if (this.parentId != -1) { + stream.writeAttrib("Parent", this.parentId); + } + + if ((this.flags & 0x40) != 0) { + stream.writeFlag(MdlUtils.TOKEN_BILLBOARDED_LOCK_Z); + } + + if ((this.flags & 0x20) != 0) { + stream.writeFlag(MdlUtils.TOKEN_BILLBOARDED_LOCK_Y); + } + + if ((this.flags & 0x10) != 0) { + stream.writeFlag(MdlUtils.TOKEN_BILLBOARDED_LOCK_X); + } + + if ((this.flags & 0x8) != 0) { + stream.writeFlag(MdlUtils.TOKEN_BILLBOARDED); + } + + if ((this.flags & 0x80) != 0) { + stream.writeFlag(MdlUtils.TOKEN_CAMERA_ANCHORED); + } + + if ((this.flags & 0x2) != 0) { + stream.writeFlag(MdlUtils.TOKEN_DONT_INHERIT + " { " + MdlUtils.TOKEN_ROTATION + " }"); + } + + if ((this.flags & 0x1) != 0) { + stream.writeFlag(MdlUtils.TOKEN_DONT_INHERIT + " { " + MdlUtils.TOKEN_TRANSLATION + " }"); + } + + if ((this.flags & 0x4) != 0) { + stream.writeFlag(MdlUtils.TOKEN_DONT_INHERIT + " { " + MdlUtils.TOKEN_SCALING + " }"); + } + } + + public void writeGenericTimelines(final MdlTokenOutputStream stream) { + writeTimeline(stream, AnimationMap.KGTR); + writeTimeline(stream, AnimationMap.KGRT); + writeTimeline(stream, AnimationMap.KGSC); + } + + public long getGenericByteLength(final int version) { + long size = 96; + + for (final MdlxTimeline timeline : this.timelines) { + if (isGeneric(timeline)) { + size += timeline.getByteLength(); + } + } + + return size; + } + + public boolean isGeneric(final MdlxTimeline timeline) { + final AnimationMap type = AnimationMap.ID_TO_TAG.get(timeline.name); + + return (type == AnimationMap.KGTR) || (type == AnimationMap.KGRT) || (type == AnimationMap.KGSC); + } + + @Override + public long getByteLength(final int version) { + return 96 + super.getByteLength(version); + } + + public String getName() { + return this.name; + } + + public int getObjectId() { + return this.objectId; + } + + public int getParentId() { + return this.parentId; + } + + public int getFlags() { + return this.flags; + } + + public void setName(final String name) { + this.name = name; + } + + public void setObjectId(final int objectId) { + this.objectId = objectId; + } + + public void setParentId(final int parentId) { + this.parentId = parentId; + } + + public void setFlags(final int flags) { + this.flags = flags; + } + + private static final class WrappedMdlTokenIterator implements Iterator { + private final Iterator delegate; + private final MdlxGenericObject updatingObject; + private final MdlTokenInputStream stream; + private String next; + private boolean hasLoaded = false; + + public WrappedMdlTokenIterator(final Iterator delegate, final MdlxGenericObject updatingObject, + final MdlTokenInputStream stream) { + this.delegate = delegate; + this.updatingObject = updatingObject; + this.stream = stream; + } + + @Override + public boolean hasNext() { + if (this.delegate.hasNext()) { + this.next = read(); + this.hasLoaded = true; + return this.next != null; + } + return false; + } + + @Override + public String next() { + if (!this.hasLoaded) { + this.next = read(); + } + this.hasLoaded = false; + return this.next; + } + + private String read() { + String token; + InteriorParsing: do { + token = this.delegate.next(); + if (token == null) { + break; + } + switch (token) { + case MdlUtils.TOKEN_OBJECTID: + this.updatingObject.objectId = Integer.parseInt(this.delegate.next()); + token = null; + break; + case MdlUtils.TOKEN_PARENT: + this.updatingObject.parentId = Integer.parseInt(this.delegate.next()); + token = null; + break; + case MdlUtils.TOKEN_BILLBOARDED_LOCK_Z: + this.updatingObject.flags |= 0x40; + token = null; + break; + case MdlUtils.TOKEN_BILLBOARDED_LOCK_Y: + this.updatingObject.flags |= 0x20; + token = null; + break; + case MdlUtils.TOKEN_BILLBOARDED_LOCK_X: + this.updatingObject.flags |= 0x10; + token = null; + break; + case MdlUtils.TOKEN_BILLBOARDED: + this.updatingObject.flags |= 0x8; + token = null; + break; + case MdlUtils.TOKEN_CAMERA_ANCHORED: + this.updatingObject.flags |= 0x80; + token = null; + break; + case MdlUtils.TOKEN_DONT_INHERIT: + for (final String subToken : this.stream.readBlock()) { + switch (subToken) { + case MdlUtils.TOKEN_ROTATION: + this.updatingObject.flags |= 0x2; + break; + case MdlUtils.TOKEN_TRANSLATION: + this.updatingObject.flags |= 0x1; + break; + case MdlUtils.TOKEN_SCALING: + this.updatingObject.flags |= 0x0; + break; + } + } + token = null; + break; + case MdlUtils.TOKEN_TRANSLATION: + this.updatingObject.readTimeline(this.stream, AnimationMap.KGTR); + token = null; + break; + case MdlUtils.TOKEN_ROTATION: + this.updatingObject.readTimeline(this.stream, AnimationMap.KGRT); + token = null; + break; + case MdlUtils.TOKEN_SCALING: + this.updatingObject.readTimeline(this.stream, AnimationMap.KGSC); + token = null; + break; + default: + break InteriorParsing; + } + } + while (this.delegate.hasNext()); + return token; + } + + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxGeoset.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxGeoset.java new file mode 100644 index 0000000..8a0d198 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxGeoset.java @@ -0,0 +1,592 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxGeoset implements MdlxBlock, MdlxChunk { + private static final War3ID VRTX = War3ID.fromString("VRTX"); + private static final War3ID NRMS = War3ID.fromString("NRMS"); + private static final War3ID PTYP = War3ID.fromString("PTYP"); + private static final War3ID PCNT = War3ID.fromString("PCNT"); + private static final War3ID PVTX = War3ID.fromString("PVTX"); + private static final War3ID GNDX = War3ID.fromString("GNDX"); + private static final War3ID MTGC = War3ID.fromString("MTGC"); + private static final War3ID MATS = War3ID.fromString("MATS"); + private static final War3ID TANG = War3ID.fromString("TANG"); + private static final War3ID SKIN = War3ID.fromString("SKIN"); + private static final War3ID UVAS = War3ID.fromString("UVAS"); + private static final War3ID UVBS = War3ID.fromString("UVBS"); + + public float[] vertices; + public float[] normals; + public long[] faceTypeGroups; // unsigned int[] + public long[] faceGroups; // unsigned int[] + public int[] faces; // unsigned short[] + public short[] vertexGroups; // unsigned byte[] + public long[] matrixGroups; // unsigned int[] + public long[] matrixIndices; // unsigned int[] + public long materialId = 0; + public long selectionGroup = 0; + public long selectionFlags = 0; + /** + * @since 900 + */ + public int lod = 0; + /** + * @since 900 + */ + public String lodName = ""; + public MdlxExtent extent = new MdlxExtent(); + public List sequenceExtents = new ArrayList<>(); + /** + * @since 900 + */ + public float[] tangents; + /** + * @since 900 + */ + public short[] skin; + public float[][] uvSets; + + @Override + public void readMdx(final BinaryReader reader, final int version) { + final long size = reader.readUInt32(); + + reader.readInt32(); // skip VRTX + this.vertices = reader.readFloat32Array(reader.readInt32() * 3); + reader.readInt32(); // skip NRMS + this.normals = reader.readFloat32Array(reader.readInt32() * 3); + reader.readInt32(); // skip PTYP + this.faceTypeGroups = reader.readUInt32Array(reader.readInt32()); + reader.readInt32(); // skip PCNT + this.faceGroups = reader.readUInt32Array(reader.readInt32()); + reader.readInt32(); // skip PVTX + this.faces = reader.readUInt16Array(reader.readInt32()); + reader.readInt32(); // skip GNDX + this.vertexGroups = reader.readUInt8Array(reader.readInt32()); + reader.readInt32(); // skip MTGC + this.matrixGroups = reader.readUInt32Array(reader.readInt32()); + reader.readInt32(); // skip MATS + this.matrixIndices = reader.readUInt32Array(reader.readInt32()); + this.materialId = reader.readUInt32(); + this.selectionGroup = reader.readUInt32(); + this.selectionFlags = reader.readUInt32(); + + if (version > 800) { + this.lod = reader.readInt32(); + this.lodName = reader.read(80); + } + + this.extent.readMdx(reader); + + final long numExtents = reader.readUInt32(); + + for (int i = 0; i < numExtents; i++) { + final MdlxExtent extent = new MdlxExtent(); + extent.readMdx(reader); + this.sequenceExtents.add(extent); + } + + int id = reader.readTag(); // TANG or SKIN or UVAS + + if ((version > 800) && (id != UVAS.getValue())) { + if (id == TANG.getValue()) { + this.tangents = reader.readFloat32Array(reader.readInt32() * 4); + + id = reader.readTag(); // SKIN or UVAS + } + + if (id == SKIN.getValue()) { + this.skin = reader.readUInt8Array(reader.readInt32()); + + id = reader.readInt32(); // UVAS + } + } + + final long numUVLayers = reader.readUInt32(); + this.uvSets = new float[(int) numUVLayers][]; + for (int i = 0; i < numUVLayers; i++) { + reader.readInt32(); // skip UVBS + this.uvSets[i] = reader.readFloat32Array(reader.readInt32() * 2); + } + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeUInt32(getByteLength(version)); + writer.writeTag(VRTX.getValue()); + writer.writeUInt32(this.vertices.length / 3); + writer.writeFloat32Array(this.vertices); + writer.writeTag(NRMS.getValue()); + writer.writeUInt32(this.normals.length / 3); + writer.writeFloat32Array(this.normals); + writer.writeTag(PTYP.getValue()); + writer.writeUInt32(this.faceTypeGroups.length); + writer.writeUInt32Array(this.faceTypeGroups); + writer.writeTag(PCNT.getValue()); + writer.writeUInt32(this.faceGroups.length); + writer.writeUInt32Array(this.faceGroups); + writer.writeTag(PVTX.getValue()); + writer.writeUInt32(this.faces.length); + writer.writeUInt16Array(this.faces); + writer.writeTag(GNDX.getValue()); + writer.writeUInt32(this.vertexGroups.length); + writer.writeUInt8Array(this.vertexGroups); + writer.writeTag(MTGC.getValue()); + writer.writeUInt32(this.matrixGroups.length); + writer.writeUInt32Array(this.matrixGroups); + writer.writeTag(MATS.getValue()); + writer.writeUInt32(this.matrixIndices.length); + writer.writeUInt32Array(this.matrixIndices); + writer.writeUInt32(this.materialId); + writer.writeUInt32(this.selectionGroup); + writer.writeUInt32(this.selectionFlags); + + if (version > 800) { + writer.writeInt32(this.lod); + writer.writeWithNulls(this.lodName, 80); + } + + this.extent.writeMdx(writer); + writer.writeUInt32(this.sequenceExtents.size()); + + for (final MdlxExtent sequenceExtent : this.sequenceExtents) { + sequenceExtent.writeMdx(writer); + } + + if (version > 800) { + if (this.tangents != null) { + writer.writeTag(TANG.getValue()); + writer.writeUInt32(this.tangents.length / 4); + writer.writeFloat32Array(this.tangents); + } + + if (this.skin != null) { + writer.writeTag(SKIN.getValue()); + writer.writeUInt32(this.skin.length); + writer.writeUInt8Array(this.skin); + } + } + + writer.writeTag(UVAS.getValue()); + writer.writeUInt32(this.uvSets.length); + + for (final float[] uvSet : this.uvSets) { + writer.writeTag(UVBS.getValue()); + writer.writeUInt32(uvSet.length / 2); + writer.writeFloat32Array(uvSet); + } + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + this.uvSets = new float[0][]; + + for (final String token : stream.readBlock()) { + // For now hardcoded for triangles, until I see a model with something + // different. + switch (token) { + case MdlUtils.TOKEN_VERTICES: + this.vertices = stream.readVectorArray(new float[stream.readInt() * 3], 3); + break; + case MdlUtils.TOKEN_NORMALS: + this.normals = stream.readVectorArray(new float[stream.readInt() * 3], 3); + break; + case MdlUtils.TOKEN_TVERTICES: { + this.uvSets = Arrays.copyOf(this.uvSets, this.uvSets.length + 1); + this.uvSets[this.uvSets.length - 1] = stream.readVectorArray(new float[stream.readInt() * 2], 2); + } + break; + case MdlUtils.TOKEN_VERTEX_GROUP: { + // Vertex groups are stored in a block with no count, can't allocate the buffer + // yet. + final List vertexGroups = new ArrayList<>(); + for (final String vertexGroup : stream.readBlock()) { + vertexGroups.add(Short.valueOf(vertexGroup)); + } + + this.vertexGroups = new short[vertexGroups.size()]; + int i = 0; + for (final Short vertexGroup : vertexGroups) { + this.vertexGroups[i++] = vertexGroup; + } + } + break; + case "Tangents": { + final int tansCount = (int) stream.readUInt32(); + this.tangents = new float[tansCount * 4]; + stream.readVectorArray(this.tangents, 4); + } + break; + case "SkinWeights": { + final int skinCount = (int) stream.readUInt32(); + this.skin = new short[skinCount * 8]; + stream.readUInt8Array(this.skin); + } + break; + case MdlUtils.TOKEN_FACES: { + this.faceTypeGroups = new long[] { 4L }; + stream.readInt(); // number of groups + final int count = stream.readInt(); + stream.read(); // { + stream.read(); // Triangles + stream.read(); // { + this.faces = stream.readUInt16Array(new int[count]); + this.faceGroups = new long[] { count }; + stream.read(); // } + stream.read(); // } + } + break; + case MdlUtils.TOKEN_GROUPS: { + final List indices = new ArrayList<>(); + final List groups = new ArrayList<>(); + + stream.readInt(); // matrices count + stream.readInt(); // total indices + + // eslint-disable-next-line no-unused-vars + for (final String matrix : stream.readBlock()) { + int size = 0; + + for (final String index : stream.readBlock()) { + indices.add(Integer.valueOf(index)); + size += 1; + } + groups.add(size); + } + + this.matrixIndices = new long[indices.size()]; + int i = 0; + for (final Integer index : indices) { + this.matrixIndices[i++] = index; + } + this.matrixGroups = new long[groups.size()]; + i = 0; + for (final Integer group : groups) { + this.matrixGroups[i++] = group; + } + } + break; + case MdlUtils.TOKEN_MINIMUM_EXTENT: + stream.readFloatArray(this.extent.min); + break; + case MdlUtils.TOKEN_MAXIMUM_EXTENT: + stream.readFloatArray(this.extent.max); + break; + case MdlUtils.TOKEN_BOUNDSRADIUS: + this.extent.boundsRadius = stream.readFloat(); + break; + case MdlUtils.TOKEN_ANIM: { + final MdlxExtent extent = new MdlxExtent(); + for (final String subToken : stream.readBlock()) { + switch (subToken) { + case MdlUtils.TOKEN_MINIMUM_EXTENT: + stream.readFloatArray(extent.min); + break; + case MdlUtils.TOKEN_MAXIMUM_EXTENT: + stream.readFloatArray(extent.max); + break; + case MdlUtils.TOKEN_BOUNDSRADIUS: + extent.boundsRadius = stream.readFloat(); + break; + } + } + this.sequenceExtents.add(extent); + } + break; + case MdlUtils.TOKEN_MATERIAL_ID: + this.materialId = stream.readInt(); + break; + case MdlUtils.TOKEN_SELECTION_GROUP: + this.selectionGroup = stream.readInt(); + break; + case MdlUtils.TOKEN_UNSELECTABLE: + this.selectionFlags = 4; + break; + case "LevelOfDetail": + this.lod = stream.readInt(); + break; + case "Name": + this.lodName = stream.read(); + break; + default: + throw new RuntimeException("Unknown token in Geoset: " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startBlock(MdlUtils.TOKEN_GEOSET); + + stream.writeVectorArray(MdlUtils.TOKEN_VERTICES, this.vertices, 3); + stream.writeVectorArray(MdlUtils.TOKEN_NORMALS, this.normals, 3); + + for (final float[] uvSet : this.uvSets) { + stream.writeVectorArray(MdlUtils.TOKEN_TVERTICES, uvSet, 2); + } + + if (version <= 800) { + stream.startBlock(MdlUtils.TOKEN_VERTEX_GROUP); + for (final short vertexGroup : this.vertexGroups) { + stream.writeLine(vertexGroup + ","); + } + stream.endBlock(); + } + + if (version > 800) { + + stream.startBlock(MdlUtils.TOKEN_VERTEX_GROUP); + if (this.skin == null) { + for (final short vertexGroup : this.vertexGroups) { + stream.writeLine(vertexGroup + ","); + } + } + stream.endBlock(); + + if (this.tangents != null) { + stream.startBlock("Tangents", this.tangents.length / 4); + + for (int i = 0, l = this.tangents.length; i < l; i += 4) { + stream.writeFloatArray(Arrays.copyOfRange(this.tangents, i, i + 4)); + } + + stream.endBlock(); + } + + if (this.skin != null) { + stream.startBlock("SkinWeights", this.skin.length / 8); + + for (int i = 0, l = this.skin.length; i < l; i += 8) { + stream.writeShortArrayRaw(Arrays.copyOfRange(this.skin, i, i + 8)); + } + + stream.endBlock(); + } + } + + // For now hardcoded for triangles, until I see a model with something + // different. + stream.startBlock(MdlUtils.TOKEN_FACES, 1, this.faces.length); + stream.startBlock(MdlUtils.TOKEN_TRIANGLES); + final StringBuilder facesBuffer = new StringBuilder(); + for (final int faceValue : this.faces) { + if (facesBuffer.length() > 0) { + facesBuffer.append(", "); + } + facesBuffer.append(faceValue); + } + stream.writeLine("{ " + facesBuffer.toString() + " },"); + stream.endBlock(); + stream.endBlock(); + + stream.startBlock(MdlUtils.TOKEN_GROUPS, this.matrixGroups.length, this.matrixIndices.length); + int index = 0; + for (final long groupSize : this.matrixGroups) { + stream.writeLongSubArrayAttrib(MdlUtils.TOKEN_MATRICES, this.matrixIndices, index, + (int) (index + groupSize)); + index += groupSize; + } + stream.endBlock(); + + this.extent.writeMdl(stream); + + for (final MdlxExtent sequenceExtent : this.sequenceExtents) { + stream.startBlock(MdlUtils.TOKEN_ANIM); + sequenceExtent.writeMdl(stream); + stream.endBlock(); + } + + stream.writeAttribUInt32("MaterialID", this.materialId); + stream.writeAttribUInt32("SelectionGroup", this.selectionGroup); + if (this.selectionFlags == 4) { + stream.writeFlag("Unselectable"); + } + + if (version > 800) { + stream.writeAttrib("LevelOfDetail", this.lod); + + if (this.lodName.length() > 0) { + stream.writeStringAttrib("Name", this.lodName); + } + } + + stream.endBlock(); + } + + @Override + public long getByteLength(final int version) { + long size = 120 + (this.vertices.length * 4) + (this.normals.length * 4) + (this.faceTypeGroups.length * 4) + + (this.faceGroups.length * 4) + (this.faces.length * 2) + this.vertexGroups.length + + (this.matrixGroups.length * 4) + (this.matrixIndices.length * 4) + (this.sequenceExtents.size() * 28); + for (final float[] uvSet : this.uvSets) { + size += 8 + (uvSet.length * 4); + } + + if (version > 800) { + size += 84; + + if (this.tangents != null) { + size += 8 + (this.tangents.length * 4); + } + + if (this.skin != null) { + size += 8 + this.skin.length; + } + } + + return size; + } + + public float[] getVertices() { + return this.vertices; + } + + public float[] getNormals() { + return this.normals; + } + + public long[] getFaceTypeGroups() { + return this.faceTypeGroups; + } + + public long[] getFaceGroups() { + return this.faceGroups; + } + + public int[] getFaces() { + return this.faces; + } + + public short[] getVertexGroups() { + return this.vertexGroups; + } + + public long[] getMatrixGroups() { + return this.matrixGroups; + } + + public long[] getMatrixIndices() { + return this.matrixIndices; + } + + public long getMaterialId() { + return this.materialId; + } + + public long getSelectionGroup() { + return this.selectionGroup; + } + + public long getSelectionFlags() { + return this.selectionFlags; + } + + public int getLod() { + return this.lod; + } + + public String getLodName() { + return this.lodName; + } + + public MdlxExtent getExtent() { + return this.extent; + } + + public List getSequenceExtents() { + return this.sequenceExtents; + } + + public float[] getTangents() { + return this.tangents; + } + + public short[] getSkin() { + return this.skin; + } + + public float[][] getUvSets() { + return this.uvSets; + } + + public void setVertices(final float[] vertices) { + this.vertices = vertices; + } + + public void setNormals(final float[] normals) { + this.normals = normals; + } + + public void setFaceTypeGroups(final long[] faceTypeGroups) { + this.faceTypeGroups = faceTypeGroups; + } + + public void setFaceGroups(final long[] faceGroups) { + this.faceGroups = faceGroups; + } + + public void setFaces(final int[] faces) { + this.faces = faces; + } + + public void setVertexGroups(final short[] vertexGroups) { + this.vertexGroups = vertexGroups; + } + + public void setMatrixGroups(final long[] matrixGroups) { + this.matrixGroups = matrixGroups; + } + + public void setMatrixIndices(final long[] matrixIndices) { + this.matrixIndices = matrixIndices; + } + + public void setMaterialId(final long materialId) { + this.materialId = materialId; + } + + public void setSelectionGroup(final long selectionGroup) { + this.selectionGroup = selectionGroup; + } + + public void setSelectionFlags(final long selectionFlags) { + this.selectionFlags = selectionFlags; + } + + public void setLod(final int lod) { + this.lod = lod; + } + + public void setLodName(final String lodName) { + this.lodName = lodName; + } + + public void setExtent(final MdlxExtent extent) { + this.extent = extent; + } + + public void setSequenceExtents(final List sequenceExtents) { + this.sequenceExtents = sequenceExtents; + } + + public void setTangents(final float[] tangents) { + this.tangents = tangents; + } + + public void setSkin(final short[] skin) { + this.skin = skin; + } + + public void setUvSets(final float[][] uvSets) { + this.uvSets = uvSets; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxGeosetAnimation.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxGeosetAnimation.java new file mode 100644 index 0000000..cf18f92 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxGeosetAnimation.java @@ -0,0 +1,136 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import java.util.Iterator; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxGeosetAnimation extends MdlxAnimatedObject { + public float alpha = 1; + public int flags = 0; + public float[] color = { 1, 1, 1 }; + public int geosetId = -1; + + @Override + public void readMdx(final BinaryReader reader, final int version) { + final long size = reader.readUInt32(); + + this.alpha = reader.readFloat32(); + this.flags = reader.readInt32(); + reader.readFloat32Array(this.color); + this.geosetId = reader.readInt32(); + + readTimelines(reader, size - 28); + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeUInt32(getByteLength(version)); + writer.writeFloat32(this.alpha); + writer.writeInt32(this.flags); + writer.writeFloat32Array(this.color); + writer.writeInt32(this.geosetId); + + writeTimelines(writer); + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + final Iterator blockIterator = readAnimatedBlock(stream); + while (blockIterator.hasNext()) { + final String token = blockIterator.next(); + switch (token) { + case MdlUtils.TOKEN_DROP_SHADOW: + this.flags |= 0x1; + break; + case MdlUtils.TOKEN_STATIC_ALPHA: + this.alpha = stream.readFloat(); + break; + case MdlUtils.TOKEN_ALPHA: + readTimeline(stream, AnimationMap.KGAO); + break; + case MdlUtils.TOKEN_STATIC_COLOR: { + this.flags |= 0x2; + stream.readColor(this.color); + } + break; + case MdlUtils.TOKEN_COLOR: { + this.flags |= 0x2; + readTimeline(stream, AnimationMap.KGAC); + } + break; + case MdlUtils.TOKEN_GEOSETID: + this.geosetId = stream.readInt(); + break; + default: + throw new RuntimeException("Unknown token in GeosetAnimation: " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startBlock(MdlUtils.TOKEN_GEOSETANIM); + + if ((this.flags & 0x1) != 0) { + stream.writeFlag(MdlUtils.TOKEN_DROP_SHADOW); + } + + if (!writeTimeline(stream, AnimationMap.KGAO)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_ALPHA, this.alpha); + } + + if ((this.flags & 0x2) != 0) { + if (!writeTimeline(stream, AnimationMap.KGAC) + && ((this.color[0] != 0) || (this.color[1] != 0) || (this.color[2] != 0))) { + stream.writeColor(MdlUtils.TOKEN_STATIC_COLOR, this.color); + } + } + + if (this.geosetId != -1) { // TODO Retera added -1 check here, why wasn't it there before in JS??? + stream.writeAttrib(MdlUtils.TOKEN_GEOSETID, this.geosetId); + } + + stream.endBlock(); + } + + @Override + public long getByteLength(final int version) { + return 28 + super.getByteLength(version); + } + + public float getAlpha() { + return this.alpha; + } + + public int getFlags() { + return this.flags; + } + + public float[] getColor() { + return this.color; + } + + public int getGeosetId() { + return this.geosetId; + } + + public void setAlpha(final float alpha) { + this.alpha = alpha; + } + + public void setFlags(final int flags) { + this.flags = flags; + } + + public void setColor(final float[] color) { + this.color = color; + } + + public void setGeosetId(final int geosetId) { + this.geosetId = geosetId; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxHelper.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxHelper.java new file mode 100644 index 0000000..962b020 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxHelper.java @@ -0,0 +1,28 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; + +public class MdlxHelper extends MdlxGenericObject { + public MdlxHelper() { + super(0x0); // NOTE: ghostwolf JS didn't pass the 0x1 flag???? + // ANOTHER NOTE: setting the 0x1 flag causes other fan programs to spam error + // messages + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + for (final String token : readMdlGeneric(stream)) { + throw new RuntimeException("Unknown token in Helper: " + token); + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startObjectBlock(MdlUtils.TOKEN_HELPER, name); + writeGenericHeader(stream); + writeGenericTimelines(stream); + stream.endBlock(); + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxLayer.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxLayer.java new file mode 100644 index 0000000..0f54476 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxLayer.java @@ -0,0 +1,369 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import java.util.Iterator; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxLayer extends MdlxAnimatedObject { + public enum FilterMode { + NONE("None"), + TRANSPARENT("Transparent"), + BLEND("Blend"), + ADDITIVE("Additive"), + ADDALPHA("AddAlpha"), + MODULATE("Modulate"), + MODULATE2X("Modulate2x"); + + String token; + + FilterMode(final String token) { + this.token = token; + } + + public static FilterMode fromId(final int id) { + return values()[id]; + } + + public static int nameToId(final String name) { + for (final FilterMode mode : values()) { + if (mode.token.equals(name)) { + return mode.ordinal(); + } + } + return -1; + } + + public static FilterMode nameToFilter(final String name) { + for (final FilterMode mode : values()) { + if (mode.token.equals(name)) { + return mode; + } + } + return null; + } + + @Override + public String toString() { + return this.token; + } + } + + public FilterMode filterMode = FilterMode.NONE; + public int flags = 0; + public int textureId = -1; + public int textureAnimationId = -1; + public long coordId = 0; + public float alpha = 1; + /** + * @since 900 + */ + public float emissiveGain = 1; + /** + * @since 1000 + */ + public float[] fresnelColor = new float[] { 1, 1, 1 }; + /** + * @since 1000 + */ + public float fresnelOpacity = 0; + /** + * @since 1000 + */ + public float fresnelTeamColor = 0; + + @Override + public void readMdx(final BinaryReader reader, final int version) { + final int position = reader.position(); + final long size = reader.readUInt32(); + + this.filterMode = FilterMode.fromId(reader.readInt32()); + this.flags = reader.readInt32(); // UInt32 in JS + this.textureId = reader.readInt32(); + this.textureAnimationId = reader.readInt32(); + this.coordId = reader.readInt32(); + this.alpha = reader.readFloat32(); + + if (version > 800) { + this.emissiveGain = reader.readFloat32(); + + if (version > 900) { + reader.readFloat32Array(this.fresnelColor); + this.fresnelOpacity = reader.readFloat32(); + this.fresnelTeamColor = reader.readFloat32(); + } + } + + readTimelines(reader, size - (reader.position() - position)); + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeUInt32(getByteLength(version)); + writer.writeUInt32(this.filterMode.ordinal()); + writer.writeUInt32(this.flags); + writer.writeInt32(this.textureId); + writer.writeInt32(this.textureAnimationId); + writer.writeUInt32(this.coordId); + writer.writeFloat32(this.alpha); + + if (version > 800) { + writer.writeFloat32(this.emissiveGain); + + if (version > 900) { + writer.writeFloat32Array(this.fresnelColor); + writer.writeFloat32(this.fresnelOpacity); + writer.writeFloat32(this.fresnelTeamColor); + } + } + + writeTimelines(writer); + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + final Iterator iterator = readAnimatedBlock(stream); + while (iterator.hasNext()) { + final String token = iterator.next(); + switch (token) { + case MdlUtils.TOKEN_FILTER_MODE: + this.filterMode = FilterMode.fromId(FilterMode.nameToId(stream.read())); + break; + case MdlUtils.TOKEN_UNSHADED: + this.flags |= 0x1; + break; + case MdlUtils.TOKEN_SPHERE_ENV_MAP: + this.flags |= 0x2; + break; + case MdlUtils.TOKEN_TWO_SIDED: + this.flags |= 0x10; + break; + case MdlUtils.TOKEN_UNFOGGED: + this.flags |= 0x20; + break; + case MdlUtils.TOKEN_NO_DEPTH_TEST: + this.flags |= 0x40; + break; + case MdlUtils.TOKEN_NO_DEPTH_SET: + this.flags |= 0x80; + break; + case "Unlit": + this.flags |= 0x100; + case MdlUtils.TOKEN_STATIC_TEXTURE_ID: + this.textureId = stream.readInt(); + break; + case MdlUtils.TOKEN_TEXTURE_ID: + readTimeline(stream, AnimationMap.KMTF); + break; + case MdlUtils.TOKEN_TVERTEX_ANIM_ID: + this.textureAnimationId = stream.readInt(); + break; + case MdlUtils.TOKEN_COORD_ID: + this.coordId = stream.readInt(); + break; + case MdlUtils.TOKEN_STATIC_ALPHA: + this.alpha = stream.readFloat(); + break; + case MdlUtils.TOKEN_ALPHA: + readTimeline(stream, AnimationMap.KMTA); + break; + case "static EmissiveGain": + this.emissiveGain = stream.readFloat(); + break; + case "EmissiveGain": + readTimeline(stream, AnimationMap.KMTE); + break; + case "static FresnelColor": + stream.readColor(this.fresnelColor); + break; + case "FresnelColor": + readTimeline(stream, AnimationMap.KFC3); + break; + case "static FresnelOpacity": + this.fresnelOpacity = stream.readFloat(); + break; + case "FresnelOpacity": + readTimeline(stream, AnimationMap.KFCA); + break; + case "static FresnelTeamColor": + this.fresnelTeamColor = stream.readFloat(); + break; + case "FresnelTeamColor": + readTimeline(stream, AnimationMap.KFTC); + break; + default: + throw new RuntimeException("Unknown token in Layer: " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startBlock(MdlUtils.TOKEN_LAYER); + + stream.writeAttrib(MdlUtils.TOKEN_FILTER_MODE, this.filterMode.toString()); + + if ((this.flags & 0x1) != 0) { + stream.writeFlag(MdlUtils.TOKEN_UNSHADED); + } + + if ((this.flags & 0x2) != 0) { + stream.writeFlag(MdlUtils.TOKEN_SPHERE_ENV_MAP); + } + + if ((this.flags & 0x10) != 0) { + stream.writeFlag(MdlUtils.TOKEN_TWO_SIDED); + } + + if ((this.flags & 0x20) != 0) { + stream.writeFlag(MdlUtils.TOKEN_UNFOGGED); + } + + if ((this.flags & 0x40) != 0) { + stream.writeFlag(MdlUtils.TOKEN_NO_DEPTH_TEST); + } + + if ((this.flags & 0x100) != 0) { + stream.writeFlag(MdlUtils.TOKEN_NO_DEPTH_SET); + } + + if ((version > 800) && ((this.flags & 0x100) != 0)) { + stream.writeFlag("Unlit"); + } + + if (!writeTimeline(stream, AnimationMap.KMTF)) { + stream.writeAttrib(MdlUtils.TOKEN_STATIC_TEXTURE_ID, this.textureId); + } + + if (this.textureAnimationId != -1) { + stream.writeAttrib(MdlUtils.TOKEN_TVERTEX_ANIM_ID, this.textureAnimationId); + } + + if (this.coordId != 0) { + stream.writeAttribUInt32(MdlUtils.TOKEN_COORD_ID, this.coordId); + } + + if (!writeTimeline(stream, AnimationMap.KMTA) && (this.alpha != 1)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_ALPHA, this.alpha); + } + + if (version > 800) { + if (!writeTimeline(stream, AnimationMap.KMTE) && (this.emissiveGain != 1)) { + stream.writeFloatAttrib("static EmissiveGain", this.emissiveGain); + } + + if (!writeTimeline(stream, AnimationMap.KFC3) + && ((this.fresnelColor[0] != 1) || (this.fresnelColor[1] != 1) || (this.fresnelColor[2] != 1))) { + stream.writeFloatArrayAttrib("static FresnelColor", this.fresnelColor); + } + + if (!writeTimeline(stream, AnimationMap.KFCA) && (this.fresnelOpacity != 0)) { + stream.writeFloatAttrib("static FresnelOpacity", this.fresnelOpacity); + } + + if (!writeTimeline(stream, AnimationMap.KFTC) && (this.fresnelTeamColor != 0)) { + stream.writeFloatAttrib("static FresnelTeamColor", this.fresnelTeamColor); + } + } + + stream.endBlock(); + } + + @Override + public long getByteLength(final int version) { + long byteLength = 28 + super.getByteLength(version); + + if (version > 800) { + byteLength += 4; + + if (version > 900) { + byteLength += 20; + } + } + + return byteLength; + } + + public FilterMode getFilterMode() { + return this.filterMode; + } + + public int getFlags() { + return this.flags; + } + + public int getTextureId() { + return this.textureId; + } + + public int getTextureAnimationId() { + return this.textureAnimationId; + } + + public long getCoordId() { + return this.coordId; + } + + public float getAlpha() { + return this.alpha; + } + + public float getEmissiveGain() { + return this.emissiveGain; + } + + public float[] getFresnelColor() { + return this.fresnelColor; + } + + public float getFresnelOpacity() { + return this.fresnelOpacity; + } + + public float getFresnelTeamColor() { + return this.fresnelTeamColor; + } + + public void setFilterMode(final FilterMode filterMode) { + this.filterMode = filterMode; + } + + public void setFlags(final int flags) { + this.flags = flags; + } + + public void setTextureId(final int textureId) { + this.textureId = textureId; + } + + public void setTextureAnimationId(final int textureAnimationId) { + this.textureAnimationId = textureAnimationId; + } + + public void setCoordId(final long coordId) { + this.coordId = coordId; + } + + public void setAlpha(final float alpha) { + this.alpha = alpha; + } + + public void setEmissiveGain(final float emissiveGain) { + this.emissiveGain = emissiveGain; + } + + public void setFresnelColor(final float[] fresnelColor) { + this.fresnelColor = fresnelColor; + } + + public void setFresnelOpacity(final float fresnelOpacity) { + this.fresnelOpacity = fresnelOpacity; + } + + public void setFresnelTeamColor(final float fresnelTeamColor) { + this.fresnelTeamColor = fresnelTeamColor; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxLight.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxLight.java new file mode 100644 index 0000000..1a65685 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxLight.java @@ -0,0 +1,222 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxLight extends MdlxGenericObject { + public enum Type { + OMNIDIRECTIONAL("Omnidirectional"), + DIRECTIONAL("Directional"), + AMBIENT("Ambient"); + + String token; + + Type(final String token) { + this.token = token; + } + + public static Type fromId(final int id) { + return values()[id]; + } + + @Override + public String toString() { + return this.token; + } + } + + public Type type = Type.OMNIDIRECTIONAL; + public float[] attenuation = new float[2]; + public float[] color = new float[3]; + public float intensity = 0; + public float[] ambientColor = new float[3]; + public float ambientIntensity = 0; + + public MdlxLight() { + super(0x200); + } + + @Override + public void readMdx(final BinaryReader reader, final int version) { + final int position = reader.position(); + final long size = reader.readUInt32(); + + super.readMdx(reader, version); + + this.type = Type.fromId(reader.readInt32()); + reader.readFloat32Array(this.attenuation); + reader.readFloat32Array(this.color); + this.intensity = reader.readFloat32(); + reader.readFloat32Array(this.ambientColor); + this.ambientIntensity = reader.readFloat32(); + + readTimelines(reader, size - (reader.position() - position)); + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeUInt32(getByteLength(version)); + + super.writeMdx(writer, version); + + writer.writeUInt32(this.type.ordinal()); + writer.writeFloat32Array(this.attenuation); + writer.writeFloat32Array(this.color); + writer.writeFloat32(this.intensity); + writer.writeFloat32Array(this.ambientColor); + writer.writeFloat32(this.ambientIntensity); + + writeNonGenericAnimationChunks(writer); + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + for (final String token : super.readMdlGeneric(stream)) { + switch (token) { + case MdlUtils.TOKEN_OMNIDIRECTIONAL: + this.type = Type.OMNIDIRECTIONAL; + break; + case MdlUtils.TOKEN_DIRECTIONAL: + this.type = Type.DIRECTIONAL; + break; + case MdlUtils.TOKEN_AMBIENT: + this.type = Type.AMBIENT; + break; + case MdlUtils.TOKEN_STATIC_ATTENUATION_START: + this.attenuation[0] = stream.readFloat(); + break; + case MdlUtils.TOKEN_ATTENUATION_START: + readTimeline(stream, AnimationMap.KLAS); + break; + case MdlUtils.TOKEN_STATIC_ATTENUATION_END: + this.attenuation[1] = stream.readFloat(); + break; + case MdlUtils.TOKEN_ATTENUATION_END: + readTimeline(stream, AnimationMap.KLAE); + break; + case MdlUtils.TOKEN_STATIC_INTENSITY: + this.intensity = stream.readFloat(); + break; + case MdlUtils.TOKEN_INTENSITY: + readTimeline(stream, AnimationMap.KLAI); + break; + case MdlUtils.TOKEN_STATIC_COLOR: + stream.readColor(this.color); + break; + case MdlUtils.TOKEN_COLOR: + readTimeline(stream, AnimationMap.KLAC); + break; + case MdlUtils.TOKEN_STATIC_AMB_INTENSITY: + this.ambientIntensity = stream.readFloat(); + break; + case MdlUtils.TOKEN_AMB_INTENSITY: + readTimeline(stream, AnimationMap.KLBI); + break; + case MdlUtils.TOKEN_STATIC_AMB_COLOR: + stream.readColor(this.ambientColor); + break; + case MdlUtils.TOKEN_AMB_COLOR: + readTimeline(stream, AnimationMap.KLBC); + break; + case MdlUtils.TOKEN_VISIBILITY: + readTimeline(stream, AnimationMap.KLAV); + break; + default: + throw new RuntimeException("Unknown token in Light: " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startObjectBlock(MdlUtils.TOKEN_LIGHT, this.name); + writeGenericHeader(stream); + + stream.writeFlag(this.type.toString()); + + if (!writeTimeline(stream, AnimationMap.KLAS)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_ATTENUATION_START, this.attenuation[0]); + } + + if (!writeTimeline(stream, AnimationMap.KLAE)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_ATTENUATION_END, this.attenuation[1]); + } + + if (!writeTimeline(stream, AnimationMap.KLAI)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_INTENSITY, this.intensity); + } + + if (!writeTimeline(stream, AnimationMap.KLAC)) { + stream.writeColor(MdlUtils.TOKEN_STATIC_COLOR, this.color); + } + + if (!writeTimeline(stream, AnimationMap.KLBI)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_AMB_INTENSITY, this.ambientIntensity); + } + + if (!writeTimeline(stream, AnimationMap.KLBC)) { + stream.writeColor(MdlUtils.TOKEN_STATIC_AMB_COLOR, this.ambientColor); + } + + writeTimeline(stream, AnimationMap.KLAV); + + writeGenericTimelines(stream); + stream.endBlock(); + } + + @Override + public long getByteLength(final int version) { + return 48 + super.getByteLength(version); + } + + public Type getType() { + return this.type; + } + + public float[] getAttenuation() { + return this.attenuation; + } + + public float[] getColor() { + return this.color; + } + + public float getIntensity() { + return this.intensity; + } + + public float[] getAmbientColor() { + return this.ambientColor; + } + + public float getAmbientIntensity() { + return this.ambientIntensity; + } + + public void setType(final Type type) { + this.type = type; + } + + public void setAttenuation(final float[] attenuation) { + this.attenuation = attenuation; + } + + public void setColor(final float[] color) { + this.color = color; + } + + public void setIntensity(final float intensity) { + this.intensity = intensity; + } + + public void setAmbientColor(final float[] ambientColor) { + this.ambientColor = ambientColor; + } + + public void setAmbientIntensity(final float ambientIntensity) { + this.ambientIntensity = ambientIntensity; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxMaterial.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxMaterial.java new file mode 100644 index 0000000..e7e013b --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxMaterial.java @@ -0,0 +1,173 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxMaterial implements MdlxBlock, MdlxChunk { + public static final War3ID LAYS = War3ID.fromString("LAYS"); + public int priorityPlane = 0; + public int flags; + /** + * @since 900 + */ + public String shader = ""; + public final List layers = new ArrayList<>(); + + @Override + public void readMdx(final BinaryReader reader, final int version) { + reader.readUInt32(); // Don't care about the size + + this.priorityPlane = reader.readInt32(); + this.flags = reader.readInt32(); + + if (version > 800) { + this.shader = reader.read(80); + } + + reader.readInt32(); // skip LAYS + + final long layerCount = reader.readUInt32(); + for (int i = 0; i < layerCount; i++) { + final MdlxLayer layer = new MdlxLayer(); + layer.readMdx(reader, version); + this.layers.add(layer); + } + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeUInt32(getByteLength(version)); + writer.writeInt32(this.priorityPlane); + writer.writeInt32(this.flags); + + if (version > 800) { + writer.writeWithNulls(this.shader, 80); + } + + writer.writeTag(LAYS.getValue()); + writer.writeUInt32(this.layers.size()); + + for (final MdlxLayer layer : this.layers) { + layer.writeMdx(writer, version); + } + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + for (final String token : stream.readBlock()) { + switch (token) { + case MdlUtils.TOKEN_CONSTANT_COLOR: + this.flags |= 0x1; + break; + case MdlUtils.TOKEN_SORT_PRIMS_NEAR_Z: + this.flags |= 0x8; + break; + case MdlUtils.TOKEN_SORT_PRIMS_FAR_Z: + this.flags |= 0x10; + break; + case MdlUtils.TOKEN_FULL_RESOLUTION: + this.flags |= 0x20; + break; + case MdlUtils.TOKEN_PRIORITY_PLANE: + this.priorityPlane = stream.readInt(); + break; + case "Shader": + this.shader = stream.read(); + break; + case MdlUtils.TOKEN_LAYER: { + final MdlxLayer layer = new MdlxLayer(); + layer.readMdl(stream, version); + this.layers.add(layer); + } + break; + default: + throw new RuntimeException("Unknown token in Material: " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startBlock(MdlUtils.TOKEN_MATERIAL); + + if ((this.flags & 0x1) != 0) { + stream.writeFlag(MdlUtils.TOKEN_CONSTANT_COLOR); + } + + if ((this.flags & 0x8) != 0) { + stream.writeFlag(MdlUtils.TOKEN_SORT_PRIMS_NEAR_Z); + } + + if ((this.flags & 0x10) != 0) { + stream.writeFlag(MdlUtils.TOKEN_SORT_PRIMS_FAR_Z); + } + + if ((this.flags & 0x20) != 0) { + stream.writeFlag(MdlUtils.TOKEN_FULL_RESOLUTION); + } + + if (this.priorityPlane != 0) { + stream.writeAttrib(MdlUtils.TOKEN_PRIORITY_PLANE, this.priorityPlane); + } + + if (version > 800) { + stream.writeStringAttrib("Shader", this.shader); + } + + for (final MdlxLayer layer : this.layers) { + layer.writeMdl(stream, version); + } + + stream.endBlock(); + } + + @Override + public long getByteLength(final int version) { + long size = 20; + + if (version > 800) { + size += 80; + } + + for (final MdlxLayer layer : this.layers) { + size += layer.getByteLength(version); + } + + return size; + } + + public int getPriorityPlane() { + return this.priorityPlane; + } + + public int getFlags() { + return this.flags; + } + + public String getShader() { + return this.shader; + } + + public List getLayers() { + return this.layers; + } + + public void setPriorityPlane(final int priorityPlane) { + this.priorityPlane = priorityPlane; + } + + public void setFlags(final int flags) { + this.flags = flags; + } + + public void setShader(final String shader) { + this.shader = shader; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxModel.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxModel.java new file mode 100644 index 0000000..01511eb --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxModel.java @@ -0,0 +1,967 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.util.War3ID; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +/** + * A Warcraft 3 model. Supports loading from and saving to both the binary MDX + * and text MDL file formats. + */ +public class MdlxModel { + // Below, these can't call a function on a string to make their value + // because + // switch/case statements require the value to be compile-time defined in + // order + // to be legal, and it appears to only allow basic binary operators for + // that. + // I would love a clearer way to just type 'MDLX' in a character constant in + // Java for this + private static final int MDLX = ('M' << 24) | ('D' << 16) | ('L' << 8) | ('X');// War3ID.fromString("MDLX").getValue(); + private static final int VERS = ('V' << 24) | ('E' << 16) | ('R' << 8) | ('S');// War3ID.fromString("VERS").getValue(); + private static final int MODL = ('M' << 24) | ('O' << 16) | ('D' << 8) | ('L');// War3ID.fromString("MODL").getValue(); + private static final int SEQS = ('S' << 24) | ('E' << 16) | ('Q' << 8) | ('S');// War3ID.fromString("SEQS").getValue(); + private static final int GLBS = ('G' << 24) | ('L' << 16) | ('B' << 8) | ('S');// War3ID.fromString("GLBS").getValue(); + private static final int MTLS = ('M' << 24) | ('T' << 16) | ('L' << 8) | ('S');// War3ID.fromString("MTLS").getValue(); + private static final int TEXS = ('T' << 24) | ('E' << 16) | ('X' << 8) | ('S');// War3ID.fromString("TEXS").getValue(); + private static final int TXAN = ('T' << 24) | ('X' << 16) | ('A' << 8) | ('N');// War3ID.fromString("TXAN").getValue(); + private static final int GEOS = ('G' << 24) | ('E' << 16) | ('O' << 8) | ('S');// War3ID.fromString("GEOS").getValue(); + private static final int GEOA = ('G' << 24) | ('E' << 16) | ('O' << 8) | ('A');// War3ID.fromString("GEOA").getValue(); + private static final int BONE = ('B' << 24) | ('O' << 16) | ('N' << 8) | ('E');// War3ID.fromString("BONE").getValue(); + private static final int LITE = ('L' << 24) | ('I' << 16) | ('T' << 8) | ('E');// War3ID.fromString("LITE").getValue(); + private static final int HELP = ('H' << 24) | ('E' << 16) | ('L' << 8) | ('P');// War3ID.fromString("HELP").getValue(); + private static final int ATCH = ('A' << 24) | ('T' << 16) | ('C' << 8) | ('H');// War3ID.fromString("ATCH").getValue(); + private static final int PIVT = ('P' << 24) | ('I' << 16) | ('V' << 8) | ('T');// War3ID.fromString("PIVT").getValue(); + private static final int PREM = ('P' << 24) | ('R' << 16) | ('E' << 8) | ('M');// War3ID.fromString("PREM").getValue(); + private static final int PRE2 = ('P' << 24) | ('R' << 16) | ('E' << 8) | ('2');// War3ID.fromString("PRE2").getValue(); + private static final int CORN = ('C' << 24) | ('O' << 16) | ('R' << 8) | ('N');// War3ID.fromString("CORN").getValue(); + private static final int RIBB = ('R' << 24) | ('I' << 16) | ('B' << 8) | ('B');// War3ID.fromString("RIBB").getValue(); + private static final int CAMS = ('C' << 24) | ('A' << 16) | ('M' << 8) | ('S');// War3ID.fromString("CAMS").getValue(); + private static final int EVTS = ('E' << 24) | ('V' << 16) | ('T' << 8) | ('S');// War3ID.fromString("EVTS").getValue(); + private static final int CLID = ('C' << 24) | ('L' << 16) | ('I' << 8) | ('D');// War3ID.fromString("CLID").getValue(); + private static final int FAFX = ('F' << 24) | ('A' << 16) | ('F' << 8) | ('X');// War3ID.fromString("FAFX").getValue(); + private static final int BPOS = ('B' << 24) | ('P' << 16) | ('O' << 8) | ('S');// War3ID.fromString("BPOS").getValue(); + + public int version = 800; + public String name = ""; + /** + * (Comment copied from Ghostwolf JS) To the best of my knowledge, this should + * always be left empty. This is probably a leftover from the Warcraft 3 beta. + * (WS game note: No, I never saw any animation files in the RoC 2001-2002 Beta. + * So it must be from the Alpha) + * + * @member {string} + */ + public String animationFile = ""; + public MdlxExtent extent = new MdlxExtent(); + public long blendTime = 0; + public List sequences = new ArrayList<>(); + public List globalSequences = new ArrayList<>(); + public List materials = new ArrayList<>(); + public List textures = new ArrayList<>(); + public List textureAnimations = new ArrayList<>(); + public List geosets = new ArrayList<>(); + public List geosetAnimations = new ArrayList<>(); + public List bones = new ArrayList<>(); + public List lights = new ArrayList<>(); + public List helpers = new ArrayList<>(); + public List attachments = new ArrayList<>(); + public List pivotPoints = new ArrayList<>(); + public List particleEmitters = new ArrayList<>(); + public List particleEmitters2 = new ArrayList<>(); + public List particleEmittersPopcorn = new ArrayList<>(); + public List ribbonEmitters = new ArrayList<>(); + public List cameras = new ArrayList<>(); + public List eventObjects = new ArrayList<>(); + public List collisionShapes = new ArrayList<>(); + /** + * @since 900 + */ + public List faceEffects = new ArrayList<>(); + /** + * @since 900 + */ + public List bindPose = new ArrayList<>(); + public List unknownChunks = new ArrayList<>(); + + public MdlxModel() { + + } + + public MdlxModel(final ByteBuffer buffer) { + load(buffer); + } + + public void load(final ByteBuffer buffer) { + // MDX files start with "MDLX". + if ((buffer.get(0) == 77) && (buffer.get(1) == 68) && (buffer.get(2) == 76) && (buffer.get(3) == 88)) { + loadMdx(buffer); + } + else { + loadMdl(buffer); + } + } + + public void loadMdx(final ByteBuffer buffer) { + final BinaryReader reader = new BinaryReader(buffer); + + if (reader.readTag() != MDLX) { + throw new IllegalStateException("WrongMagicNumber"); + } + + while (reader.remaining() > 0) { + final int tag = reader.readTag(); + final int size = reader.readInt32(); + + switch (tag) { + case VERS: + loadVersionChunk(reader); + break; + case MODL: + loadModelChunk(reader); + break; + case SEQS: + loadStaticObjects(this.sequences, MdlxBlockDescriptor.SEQUENCE, reader, size / 132); + break; + case GLBS: + loadGlobalSequenceChunk(reader, size); + break; + case MTLS: + loadDynamicObjects(this.materials, MdlxBlockDescriptor.MATERIAL, reader, size); + break; + case TEXS: + loadStaticObjects(this.textures, MdlxBlockDescriptor.TEXTURE, reader, size / 268); + break; + case TXAN: + loadDynamicObjects(this.textureAnimations, MdlxBlockDescriptor.TEXTURE_ANIMATION, reader, size); + break; + case GEOS: + loadDynamicObjects(this.geosets, MdlxBlockDescriptor.GEOSET, reader, size); + break; + case GEOA: + loadDynamicObjects(this.geosetAnimations, MdlxBlockDescriptor.GEOSET_ANIMATION, reader, size); + break; + case BONE: + loadDynamicObjects(this.bones, MdlxBlockDescriptor.BONE, reader, size); + break; + case LITE: + loadDynamicObjects(this.lights, MdlxBlockDescriptor.LIGHT, reader, size); + break; + case HELP: + loadDynamicObjects(this.helpers, MdlxBlockDescriptor.HELPER, reader, size); + break; + case ATCH: + loadDynamicObjects(this.attachments, MdlxBlockDescriptor.ATTACHMENT, reader, size); + break; + case PIVT: + loadPivotPointChunk(reader, size); + break; + case PREM: + loadDynamicObjects(this.particleEmitters, MdlxBlockDescriptor.PARTICLE_EMITTER, reader, size); + break; + case PRE2: + loadDynamicObjects(this.particleEmitters2, MdlxBlockDescriptor.PARTICLE_EMITTER2, reader, size); + break; + case CORN: + loadDynamicObjects(this.particleEmittersPopcorn, MdlxBlockDescriptor.PARTICLE_EMITTER_POPCORN, reader, + size); + break; + case RIBB: + loadDynamicObjects(this.ribbonEmitters, MdlxBlockDescriptor.RIBBON_EMITTER, reader, size); + break; + case CAMS: + loadDynamicObjects(this.cameras, MdlxBlockDescriptor.CAMERA, reader, size); + break; + case EVTS: + loadDynamicObjects(this.eventObjects, MdlxBlockDescriptor.EVENT_OBJECT, reader, size); + break; + case CLID: + loadDynamicObjects(this.collisionShapes, MdlxBlockDescriptor.COLLISION_SHAPE, reader, size); + break; + case FAFX: + loadStaticObjects(this.faceEffects, MdlxBlockDescriptor.FACE_EFFECT, reader, size / 340); + break; + case BPOS: + loadBindPoseChunk(reader, size); + break; + default: + this.unknownChunks.add(new MdlxUnknownChunk(reader, size, new War3ID(tag))); + break; + } + } + } + + private void loadVersionChunk(final BinaryReader reader) { + this.version = reader.readInt32(); + } + + private void loadModelChunk(final BinaryReader reader) { + this.name = reader.read(80); + this.animationFile = reader.read(260); + this.extent.readMdx(reader); + this.blendTime = reader.readInt32(); + } + + private void loadStaticObjects(final List out, final MdlxBlockDescriptor constructor, + final BinaryReader reader, final long count) { + for (int i = 0; i < count; i++) { + final E object = constructor.create(); + + object.readMdx(reader, this.version); + + out.add(object); + } + } + + private void loadGlobalSequenceChunk(final BinaryReader reader, final long size) { + for (long i = 0, l = size / 4; i < l; i++) { + this.globalSequences.add(reader.readUInt32()); + } + } + + private void loadDynamicObjects(final List out, + final MdlxBlockDescriptor constructor, final BinaryReader reader, final long size) { + long totalSize = 0; + while (totalSize < size) { + final E object = constructor.create(); + + object.readMdx(reader, this.version); + + totalSize += object.getByteLength(this.version); + + out.add(object); + } + } + + private void loadPivotPointChunk(final BinaryReader reader, final long size) { + for (long i = 0, l = size / 12; i < l; i++) { + this.pivotPoints.add(reader.readFloat32Array(3)); + } + } + + private void loadBindPoseChunk(final BinaryReader reader, final long size) { + for (int i = 0, l = reader.readInt32(); i < l; i++) { + this.bindPose.add(reader.readFloat32Array(12)); + } + } + + public ByteBuffer saveMdx() { + final BinaryWriter writer = new BinaryWriter(getByteLength()); + + writer.writeTag(MDLX); + saveVersionChunk(writer); + saveModelChunk(writer); + saveStaticObjectChunk(writer, SEQS, this.sequences, 132); + saveGlobalSequenceChunk(writer); + saveDynamicObjectChunk(writer, MTLS, this.materials); + saveStaticObjectChunk(writer, TEXS, this.textures, 268); + saveDynamicObjectChunk(writer, TXAN, this.textureAnimations); + saveDynamicObjectChunk(writer, GEOS, this.geosets); + saveDynamicObjectChunk(writer, GEOA, this.geosetAnimations); + saveDynamicObjectChunk(writer, BONE, this.bones); + saveDynamicObjectChunk(writer, LITE, this.lights); + saveDynamicObjectChunk(writer, HELP, this.helpers); + saveDynamicObjectChunk(writer, ATCH, this.attachments); + savePivotPointChunk(writer); + saveDynamicObjectChunk(writer, PREM, this.particleEmitters); + saveDynamicObjectChunk(writer, PRE2, this.particleEmitters2); + + if (this.version > 800) { + saveDynamicObjectChunk(writer, CORN, this.particleEmittersPopcorn); + } + + saveDynamicObjectChunk(writer, RIBB, this.ribbonEmitters); + saveDynamicObjectChunk(writer, CAMS, this.cameras); + saveDynamicObjectChunk(writer, EVTS, this.eventObjects); + saveDynamicObjectChunk(writer, CLID, this.collisionShapes); + + if (this.version > 800) { + saveStaticObjectChunk(writer, FAFX, this.faceEffects, 340); + saveBindPoseChunk(writer); + } + + for (final MdlxUnknownChunk chunk : this.unknownChunks) { + chunk.writeMdx(writer, this.version); + } + + return writer.buffer; + } + + private void saveVersionChunk(final BinaryWriter writer) { + writer.writeTag(VERS); + writer.writeUInt32(4); + writer.writeUInt32(this.version); + } + + private void saveModelChunk(final BinaryWriter writer) { + writer.writeTag(MODL); + writer.writeUInt32(372); + writer.writeWithNulls(this.name, 80); + writer.writeWithNulls(this.animationFile, 260); + this.extent.writeMdx(writer); + writer.writeUInt32(this.blendTime); + } + + private void saveStaticObjectChunk(final BinaryWriter writer, final int name, + final List objects, final long size) { + if (!objects.isEmpty()) { + writer.writeTag(name); + writer.writeUInt32(objects.size() * size); + + for (final E object : objects) { + object.writeMdx(writer, this.version); + } + } + } + + private void saveGlobalSequenceChunk(final BinaryWriter writer) { + if (!this.globalSequences.isEmpty()) { + writer.writeTag(GLBS); + writer.writeUInt32(this.globalSequences.size() * 4); + + for (final Long globalSequence : this.globalSequences) { + writer.writeUInt32(globalSequence); + } + } + } + + private void saveDynamicObjectChunk(final BinaryWriter writer, final int name, + final List objects) { + if (!objects.isEmpty()) { + writer.writeTag(name); + writer.writeUInt32(getObjectsByteLength(objects)); + + for (final E object : objects) { + object.writeMdx(writer, this.version); + } + } + } + + private void savePivotPointChunk(final BinaryWriter writer) { + if (this.pivotPoints.size() > 0) { + writer.writeTag(PIVT); + writer.writeUInt32(this.pivotPoints.size() * 12); + + for (final float[] pivotPoint : this.pivotPoints) { + writer.writeFloat32Array(pivotPoint); + } + } + } + + private void saveBindPoseChunk(final BinaryWriter writer) { + if (this.bindPose.size() > 0) { + writer.writeTag(BPOS); + writer.writeUInt32(4 + (this.bindPose.size() * 48)); + writer.writeUInt32(this.bindPose.size()); + + for (final float[] matrix : this.bindPose) { + writer.writeFloat32Array(matrix); + } + } + } + + public void loadMdl(final ByteBuffer buffer) { + String token; + final MdlTokenInputStream stream = new MdlTokenInputStream(buffer); + + while ((token = stream.read()) != null) { + switch (token) { + case MdlUtils.TOKEN_VERSION: + loadVersionBlock(stream); + break; + case MdlUtils.TOKEN_MODEL: + loadModelBlock(stream); + break; + case MdlUtils.TOKEN_SEQUENCES: + loadNumberedObjectBlock(this.sequences, MdlxBlockDescriptor.SEQUENCE, MdlUtils.TOKEN_ANIM, stream); + break; + case MdlUtils.TOKEN_GLOBAL_SEQUENCES: + loadGlobalSequenceBlock(stream); + break; + case MdlUtils.TOKEN_TEXTURES: + loadNumberedObjectBlock(this.textures, MdlxBlockDescriptor.TEXTURE, MdlUtils.TOKEN_BITMAP, stream); + break; + case MdlUtils.TOKEN_MATERIALS: + loadNumberedObjectBlock(this.materials, MdlxBlockDescriptor.MATERIAL, MdlUtils.TOKEN_MATERIAL, stream); + break; + case MdlUtils.TOKEN_TEXTURE_ANIMS: + loadNumberedObjectBlock(this.textureAnimations, MdlxBlockDescriptor.TEXTURE_ANIMATION, + MdlUtils.TOKEN_TVERTEX_ANIM, stream); + break; + case MdlUtils.TOKEN_GEOSET: + loadObject(this.geosets, MdlxBlockDescriptor.GEOSET, stream); + break; + case MdlUtils.TOKEN_GEOSETANIM: + loadObject(this.geosetAnimations, MdlxBlockDescriptor.GEOSET_ANIMATION, stream); + break; + case MdlUtils.TOKEN_BONE: + loadObject(this.bones, MdlxBlockDescriptor.BONE, stream); + break; + case MdlUtils.TOKEN_LIGHT: + loadObject(this.lights, MdlxBlockDescriptor.LIGHT, stream); + break; + case MdlUtils.TOKEN_HELPER: + loadObject(this.helpers, MdlxBlockDescriptor.HELPER, stream); + break; + case MdlUtils.TOKEN_ATTACHMENT: + loadObject(this.attachments, MdlxBlockDescriptor.ATTACHMENT, stream); + break; + case MdlUtils.TOKEN_PIVOT_POINTS: + loadPivotPointBlock(stream); + break; + case MdlUtils.TOKEN_PARTICLE_EMITTER: + loadObject(this.particleEmitters, MdlxBlockDescriptor.PARTICLE_EMITTER, stream); + break; + case MdlUtils.TOKEN_PARTICLE_EMITTER2: + loadObject(this.particleEmitters2, MdlxBlockDescriptor.PARTICLE_EMITTER2, stream); + break; + case "ParticleEmitterPopcorn": + loadObject(this.particleEmittersPopcorn, MdlxBlockDescriptor.PARTICLE_EMITTER_POPCORN, stream); + break; + case MdlUtils.TOKEN_RIBBON_EMITTER: + loadObject(this.ribbonEmitters, MdlxBlockDescriptor.RIBBON_EMITTER, stream); + break; + case MdlUtils.TOKEN_CAMERA: + loadObject(this.cameras, MdlxBlockDescriptor.CAMERA, stream); + break; + case MdlUtils.TOKEN_EVENT_OBJECT: + loadObject(this.eventObjects, MdlxBlockDescriptor.EVENT_OBJECT, stream); + break; + case MdlUtils.TOKEN_COLLISION_SHAPE: + loadObject(this.collisionShapes, MdlxBlockDescriptor.COLLISION_SHAPE, stream); + break; + case "FaceFX": + loadObject(this.faceEffects, MdlxBlockDescriptor.FACE_EFFECT, stream); + break; + case "BindPose": + loadBindPoseBlock(stream); + break; + default: + throw new IllegalStateException("Unsupported block: " + token); + } + } + } + + private void loadVersionBlock(final MdlTokenInputStream stream) { + for (final String token : stream.readBlock()) { + if (MdlUtils.TOKEN_FORMAT_VERSION.equals(token)) { + this.version = stream.readInt(); + } + else { + throw new IllegalStateException("Unknown token in Version: " + token); + } + } + } + + private void loadModelBlock(final MdlTokenInputStream stream) { + this.name = stream.read(); + for (final String token : stream.readBlock()) { + if (token.startsWith("Num")) { + /*- + * Don't care about the number of things, the arrays will grow as they wish. + * This includes: + * NumGeosets + * NumGeosetAnims + * NumHelpers + * NumLights + * NumBones + * NumAttachments + * NumParticleEmitters + * NumParticleEmitters2 + * NumRibbonEmitters + * NumEvents + */ + stream.read(); + } + else { + switch (token) { + case MdlUtils.TOKEN_BLEND_TIME: + this.blendTime = stream.readUInt32(); + break; + case MdlUtils.TOKEN_MINIMUM_EXTENT: + stream.readFloatArray(this.extent.min); + break; + case MdlUtils.TOKEN_MAXIMUM_EXTENT: + stream.readFloatArray(this.extent.max); + break; + case MdlUtils.TOKEN_BOUNDSRADIUS: + this.extent.boundsRadius = stream.readFloat(); + break; + default: + throw new IllegalStateException("Unknown token in Model: " + token); + } + } + } + } + + private void loadNumberedObjectBlock(final List out, + final MdlxBlockDescriptor constructor, final String name, final MdlTokenInputStream stream) { + stream.read(); // Don't care about the number, the array will grow + + for (final String token : stream.readBlock()) { + if (token.equals(name)) { + final E object = constructor.create(); + + object.readMdl(stream, this.version); + + out.add(object); + } + else { + throw new IllegalStateException("Unknown token in " + name + ": " + token); + } + } + } + + private void loadGlobalSequenceBlock(final MdlTokenInputStream stream) { + stream.read(); // Don't care about the number, the array will grow + + for (final String token : stream.readBlock()) { + if (token.equals(MdlUtils.TOKEN_DURATION)) { + this.globalSequences.add(stream.readUInt32()); + } + else { + throw new IllegalStateException("Unknown token in GlobalSequences: " + token); + } + } + } + + private void loadObject(final List out, final MdlxBlockDescriptor descriptor, + final MdlTokenInputStream stream) { + final E object = descriptor.create(); + + object.readMdl(stream, this.version); + + out.add(object); + } + + private void loadPivotPointBlock(final MdlTokenInputStream stream) { + final int count = stream.readInt(); + + stream.read(); // { + + for (int i = 0; i < count; i++) { + this.pivotPoints.add(stream.readFloatArray(new float[3])); + } + + stream.read(); // } + } + + private void loadBindPoseBlock(final MdlTokenInputStream stream) { + for (final String token : stream.readBlock()) { + if (token.equals("Matrices")) { + final int matrices = stream.readInt(); + + stream.read(); // { + + for (int i = 0; i < matrices; i++) { + this.bindPose.add(stream.readFloatArray(new float[12])); + } + + stream.read(); // } + } + else { + throw new IllegalStateException("Unknown token in BindPose: " + token); + } + } + } + + public ByteBuffer saveMdl() { + final MdlTokenOutputStream stream = new MdlTokenOutputStream(); + + saveVersionBlock(stream); + saveModelBlock(stream); + saveStaticObjectsBlock(stream, MdlUtils.TOKEN_SEQUENCES, this.sequences); + saveGlobalSequenceBlock(stream); + saveStaticObjectsBlock(stream, MdlUtils.TOKEN_TEXTURES, this.textures); + saveStaticObjectsBlock(stream, MdlUtils.TOKEN_MATERIALS, this.materials); + saveStaticObjectsBlock(stream, MdlUtils.TOKEN_TEXTURE_ANIMS, this.textureAnimations); + saveObjects(stream, this.geosets); + saveObjects(stream, this.geosetAnimations); + saveObjects(stream, this.bones); + saveObjects(stream, this.lights); + saveObjects(stream, this.helpers); + saveObjects(stream, this.attachments); + savePivotPointBlock(stream); + saveObjects(stream, this.particleEmitters); + saveObjects(stream, this.particleEmitters2); + + if (this.version > 800) { + saveObjects(stream, this.particleEmittersPopcorn); + } + + saveObjects(stream, this.ribbonEmitters); + saveObjects(stream, this.cameras); + saveObjects(stream, this.eventObjects); + saveObjects(stream, this.collisionShapes); + + if (this.version > 800) { + saveObjects(stream, this.faceEffects); + saveBindPoseBlock(stream); + } + + return ByteBuffer.wrap(stream.buffer.toString().getBytes()); + } + + private void saveVersionBlock(final MdlTokenOutputStream stream) { + stream.startBlock(MdlUtils.TOKEN_VERSION); + stream.writeAttrib(MdlUtils.TOKEN_FORMAT_VERSION, this.version); + stream.endBlock(); + } + + private void saveModelBlock(final MdlTokenOutputStream stream) { + stream.startObjectBlock(MdlUtils.TOKEN_MODEL, this.name); + stream.writeAttribUInt32(MdlUtils.TOKEN_BLEND_TIME, this.blendTime); + this.extent.writeMdl(stream); + stream.endBlock(); + } + + private void saveStaticObjectsBlock(final MdlTokenOutputStream stream, final String name, + final List objects) { + if (!objects.isEmpty()) { + stream.startBlock(name, objects.size()); + + for (final MdlxBlock object : objects) { + object.writeMdl(stream, this.version); + } + + stream.endBlock(); + } + } + + private void saveGlobalSequenceBlock(final MdlTokenOutputStream stream) { + if (!this.globalSequences.isEmpty()) { + stream.startBlock(MdlUtils.TOKEN_GLOBAL_SEQUENCES, this.globalSequences.size()); + + for (final Long globalSequence : this.globalSequences) { + stream.writeAttribUInt32(MdlUtils.TOKEN_DURATION, globalSequence); + } + + stream.endBlock(); + } + } + + private void saveObjects(final MdlTokenOutputStream stream, final List objects) { + for (final MdlxBlock object : objects) { + object.writeMdl(stream, this.version); + } + } + + private void savePivotPointBlock(final MdlTokenOutputStream stream) { + if (!this.pivotPoints.isEmpty()) { + stream.startBlock(MdlUtils.TOKEN_PIVOT_POINTS, this.pivotPoints.size()); + + for (final float[] pivotPoint : this.pivotPoints) { + stream.writeFloatArray(pivotPoint); + } + + stream.endBlock(); + } + } + + private void saveBindPoseBlock(final MdlTokenOutputStream stream) { + if (!this.bindPose.isEmpty()) { + stream.startBlock("BindPose"); + + stream.startBlock("Matrices", this.bindPose.size()); + + for (final float[] matrix : this.bindPose) { + stream.writeFloatArray(matrix); + } + + stream.endBlock(); + + stream.endBlock(); + } + } + + public int getByteLength() { + int size = 396; + + size += getStaticObjectsChunkByteLength(this.sequences, 132); + size += getStaticObjectsChunkByteLength(this.globalSequences, 4); + size += getDynamicObjectsChunkByteLength(this.materials); + size += getStaticObjectsChunkByteLength(this.textures, 268); + size += getDynamicObjectsChunkByteLength(this.textureAnimations); + size += getDynamicObjectsChunkByteLength(this.geosets); + size += getDynamicObjectsChunkByteLength(this.geosetAnimations); + size += getDynamicObjectsChunkByteLength(this.bones); + size += getDynamicObjectsChunkByteLength(this.lights); + size += getDynamicObjectsChunkByteLength(this.helpers); + size += getDynamicObjectsChunkByteLength(this.attachments); + size += getStaticObjectsChunkByteLength(this.pivotPoints, 12); + size += getDynamicObjectsChunkByteLength(this.particleEmitters); + size += getDynamicObjectsChunkByteLength(this.particleEmitters2); + + if (this.version > 800) { + size += getDynamicObjectsChunkByteLength(this.particleEmittersPopcorn); + } + + size += getDynamicObjectsChunkByteLength(this.ribbonEmitters); + size += getDynamicObjectsChunkByteLength(this.cameras); + size += getDynamicObjectsChunkByteLength(this.eventObjects); + size += getDynamicObjectsChunkByteLength(this.collisionShapes); + size += getObjectsByteLength(this.unknownChunks); + + if (this.version > 800) { + size += getStaticObjectsChunkByteLength(this.faceEffects, 340); + size += getBindPoseChunkByteLength(); + } + + return size; + } + + private long getObjectsByteLength(final List objects) { + long size = 0; + for (final E object : objects) { + size += object.getByteLength(this.version); + } + return size; + } + + private long getDynamicObjectsChunkByteLength(final List objects) { + if (!objects.isEmpty()) { + return 8 + getObjectsByteLength(objects); + } + + return 0; + } + + private long getStaticObjectsChunkByteLength(final List objects, final long size) { + if (!objects.isEmpty()) { + return 8 + (objects.size() * size); + } + + return 0; + } + + private long getBindPoseChunkByteLength() { + if (this.bindPose.size() > 0) { + return 12 + (this.bindPose.size() * 48); + } + + return 0; + } + + public List getGeosets() { + return this.geosets; + } + + public int getVersion() { + return this.version; + } + + public String getName() { + return this.name; + } + + public String getAnimationFile() { + return this.animationFile; + } + + public MdlxExtent getExtent() { + return this.extent; + } + + public long getBlendTime() { + return this.blendTime; + } + + public List getSequences() { + return this.sequences; + } + + public List getGlobalSequences() { + return this.globalSequences; + } + + public List getMaterials() { + return this.materials; + } + + public List getTextures() { + return this.textures; + } + + public List getTextureAnimations() { + return this.textureAnimations; + } + + public List getGeosetAnimations() { + return this.geosetAnimations; + } + + public List getBones() { + return this.bones; + } + + public List getLights() { + return this.lights; + } + + public List getHelpers() { + return this.helpers; + } + + public List getAttachments() { + return this.attachments; + } + + public List getPivotPoints() { + return this.pivotPoints; + } + + public List getParticleEmitters() { + return this.particleEmitters; + } + + public List getParticleEmitters2() { + return this.particleEmitters2; + } + + public List getParticleEmittersPopcorn() { + return this.particleEmittersPopcorn; + } + + public List getRibbonEmitters() { + return this.ribbonEmitters; + } + + public List getCameras() { + return this.cameras; + } + + public List getEventObjects() { + return this.eventObjects; + } + + public List getCollisionShapes() { + return this.collisionShapes; + } + + public List getFaceEffects() { + return this.faceEffects; + } + + public List getBindPose() { + return this.bindPose; + } + + public List getUnknownChunks() { + return this.unknownChunks; + } + + public void setVersion(final int version) { + this.version = version; + } + + public void setName(final String name) { + this.name = name; + } + + public void setAnimationFile(final String animationFile) { + this.animationFile = animationFile; + } + + public void setExtent(final MdlxExtent extent) { + this.extent = extent; + } + + public void setBlendTime(final long blendTime) { + this.blendTime = blendTime; + } + + public void setSequences(final List sequences) { + this.sequences = sequences; + } + + public void setGlobalSequences(final List globalSequences) { + this.globalSequences = globalSequences; + } + + public void setMaterials(final List materials) { + this.materials = materials; + } + + public void setTextures(final List textures) { + this.textures = textures; + } + + public void setTextureAnimations(final List textureAnimations) { + this.textureAnimations = textureAnimations; + } + + public void setGeosets(final List geosets) { + this.geosets = geosets; + } + + public void setGeosetAnimations(final List geosetAnimations) { + this.geosetAnimations = geosetAnimations; + } + + public void setBones(final List bones) { + this.bones = bones; + } + + public void setLights(final List lights) { + this.lights = lights; + } + + public void setHelpers(final List helpers) { + this.helpers = helpers; + } + + public void setAttachments(final List attachments) { + this.attachments = attachments; + } + + public void setPivotPoints(final List pivotPoints) { + this.pivotPoints = pivotPoints; + } + + public void setParticleEmitters(final List particleEmitters) { + this.particleEmitters = particleEmitters; + } + + public void setParticleEmitters2(final List particleEmitters2) { + this.particleEmitters2 = particleEmitters2; + } + + public void setParticleEmittersPopcorn(final List particleEmittersPopcorn) { + this.particleEmittersPopcorn = particleEmittersPopcorn; + } + + public void setRibbonEmitters(final List ribbonEmitters) { + this.ribbonEmitters = ribbonEmitters; + } + + public void setCameras(final List cameras) { + this.cameras = cameras; + } + + public void setEventObjects(final List eventObjects) { + this.eventObjects = eventObjects; + } + + public void setCollisionShapes(final List collisionShapes) { + this.collisionShapes = collisionShapes; + } + + public void setFaceEffects(final List faceEffects) { + this.faceEffects = faceEffects; + } + + public void setBindPose(final List bindPose) { + this.bindPose = bindPose; + } + + public void setUnknownChunks(final List unknownChunks) { + this.unknownChunks = unknownChunks; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxParticleEmitter.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxParticleEmitter.java new file mode 100644 index 0000000..d4d4ca5 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxParticleEmitter.java @@ -0,0 +1,241 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import java.util.Iterator; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxParticleEmitter extends MdlxGenericObject { + public float emissionRate = 0; + public float gravity = 0; + public float longitude = 0; + public float latitude = 0; + public String path = ""; + public float lifeSpan = 0; + public float speed = 0; + + public MdlxParticleEmitter() { + super(0x1000); + } + + @Override + public void readMdx(final BinaryReader reader, final int version) { + final int position = reader.position(); + final long size = reader.readUInt32(); + + super.readMdx(reader, version); + + this.emissionRate = reader.readFloat32(); + this.gravity = reader.readFloat32(); + this.longitude = reader.readFloat32(); + this.latitude = reader.readFloat32(); + this.path = reader.read(260); + this.lifeSpan = reader.readFloat32(); + this.speed = reader.readFloat32(); + + readTimelines(reader, size - (reader.position() - position)); + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeUInt32(getByteLength(version)); + + super.writeMdx(writer, version); + + writer.writeFloat32(this.emissionRate); + writer.writeFloat32(this.gravity); + writer.writeFloat32(this.longitude); + writer.writeFloat32(this.latitude); + writer.writeWithNulls(this.path, 260); + writer.writeFloat32(this.lifeSpan); + writer.writeFloat32(this.speed); + + writeNonGenericAnimationChunks(writer); + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + for (final String token : super.readMdlGeneric(stream)) { + switch (token) { + case MdlUtils.TOKEN_EMITTER_USES_MDL: + this.flags |= 0x8000; + break; + case MdlUtils.TOKEN_EMITTER_USES_TGA: + this.flags |= 0x10000; + break; + case MdlUtils.TOKEN_STATIC_EMISSION_RATE: + this.emissionRate = stream.readFloat(); + break; + case MdlUtils.TOKEN_EMISSION_RATE: + readTimeline(stream, AnimationMap.KPEE); + break; + case MdlUtils.TOKEN_STATIC_GRAVITY: + this.gravity = stream.readFloat(); + break; + case MdlUtils.TOKEN_GRAVITY: + readTimeline(stream, AnimationMap.KPEG); + break; + case MdlUtils.TOKEN_STATIC_LONGITUDE: + this.longitude = stream.readFloat(); + break; + case MdlUtils.TOKEN_LONGITUDE: + readTimeline(stream, AnimationMap.KPLN); + break; + case MdlUtils.TOKEN_STATIC_LATITUDE: + this.latitude = stream.readFloat(); + break; + case MdlUtils.TOKEN_LATITUDE: + readTimeline(stream, AnimationMap.KPLT); + break; + case MdlUtils.TOKEN_VISIBILITY: + readTimeline(stream, AnimationMap.KPEV); + break; + case MdlUtils.TOKEN_PARTICLE: { + final Iterator iterator = readAnimatedBlock(stream); + while (iterator.hasNext()) { + final String subToken = iterator.next(); + switch (subToken) { + case MdlUtils.TOKEN_STATIC_LIFE_SPAN: + this.lifeSpan = stream.readFloat(); + break; + case MdlUtils.TOKEN_LIFE_SPAN: + readTimeline(stream, AnimationMap.KPEL); + break; + case MdlUtils.TOKEN_STATIC_INIT_VELOCITY: + this.speed = stream.readFloat(); + break; + case MdlUtils.TOKEN_INIT_VELOCITY: + readTimeline(stream, AnimationMap.KPES); + break; + case MdlUtils.TOKEN_PATH: + this.path = stream.read(); + break; + default: + throw new RuntimeException( + "Unknown token in ParticleEmitter " + this.name + "'s Particle: " + subToken); + } + } + } + break; + default: + throw new RuntimeException("Unknown token in ParticleEmitter " + this.name + ": " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startObjectBlock(MdlUtils.TOKEN_PARTICLE_EMITTER, this.name); + writeGenericHeader(stream); + + if ((this.flags & 0x8000) != 0) { + stream.writeFlag(MdlUtils.TOKEN_EMITTER_USES_MDL); + } + + if ((this.flags & 0x10000) != 0) { + stream.writeFlag(MdlUtils.TOKEN_EMITTER_USES_TGA); + } + + if (!writeTimeline(stream, AnimationMap.KPEE)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_EMISSION_RATE, this.emissionRate); + } + + if (!writeTimeline(stream, AnimationMap.KPEG)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_GRAVITY, this.gravity); + } + + if (!writeTimeline(stream, AnimationMap.KPLN)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_LONGITUDE, this.longitude); + } + + if (!writeTimeline(stream, AnimationMap.KPLT)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_LATITUDE, this.latitude); + } + + writeTimeline(stream, AnimationMap.KPEV); + + stream.startBlock(MdlUtils.TOKEN_PARTICLE); + + if (!writeTimeline(stream, AnimationMap.KPEL)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_LIFE_SPAN, this.lifeSpan); + } + + if (!writeTimeline(stream, AnimationMap.KPES)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_INIT_VELOCITY, this.speed); + } + + if (((this.flags & 0x8000) != 0) || ((this.flags & 0x10000) != 0)) { + stream.writeAttrib(MdlUtils.TOKEN_PATH, this.path); + } + + stream.endBlock(); + + writeGenericTimelines(stream); + + stream.endBlock(); + } + + @Override + public long getByteLength(final int version) { + return 288 + super.getByteLength(version); + } + + public float getEmissionRate() { + return this.emissionRate; + } + + public float getGravity() { + return this.gravity; + } + + public float getLongitude() { + return this.longitude; + } + + public float getLatitude() { + return this.latitude; + } + + public String getPath() { + return this.path; + } + + public float getLifeSpan() { + return this.lifeSpan; + } + + public float getSpeed() { + return this.speed; + } + + public void setEmissionRate(final float emissionRate) { + this.emissionRate = emissionRate; + } + + public void setGravity(final float gravity) { + this.gravity = gravity; + } + + public void setLongitude(final float longitude) { + this.longitude = longitude; + } + + public void setLatitude(final float latitude) { + this.latitude = latitude; + } + + public void setPath(final String path) { + this.path = path; + } + + public void setLifeSpan(final float lifeSpan) { + this.lifeSpan = lifeSpan; + } + + public void setSpeed(final float speed) { + this.speed = speed; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxParticleEmitter2.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxParticleEmitter2.java new file mode 100644 index 0000000..aceb85e --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxParticleEmitter2.java @@ -0,0 +1,622 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxParticleEmitter2 extends MdlxGenericObject { + public enum FilterMode { + BLEND("Blend"), + ADDITIVE("Additive"), + MODULATE("Modulate"), + MODULATE2X("Modulate2x"), + ALPHAKEY("AlphaKey"); + + String token; + + FilterMode(final String token) { + this.token = token; + } + + public static FilterMode fromId(final int id) { + return values()[id]; + } + + public static int nameToId(final String name) { + for (final FilterMode mode : values()) { + if (mode.token.equals(name)) { + return mode.ordinal(); + } + } + return -1; + } + + @Override + public String toString() { + return this.token; + } + } + + public enum HeadOrTail { + HEAD("Head", true, false), + TAIL("Tail", false, true), + BOTH("Both", true, true); + + String token; + boolean includesHead; + boolean includesTail; + + private HeadOrTail(final String token, final boolean includesHead, final boolean includesTail) { + this.token = token; + this.includesHead = includesHead; + this.includesTail = includesTail; + } + + public static HeadOrTail fromId(final int id) { + return values()[id]; + } + + public static int nameToId(final String name) { + for (final HeadOrTail mode : values()) { + if (mode.token.equals(name)) { + return mode.ordinal(); + } + } + + return -1; + } + + @Override + public String toString() { + return this.token; + } + + public boolean isIncludesHead() { + return this.includesHead; + } + + public boolean isIncludesTail() { + return this.includesTail; + } + } + + public float speed = 0; + public float variation = 0; + public float latitude = 0; + public float gravity = 0; + public float lifeSpan = 0; + public float emissionRate = 0; + public float length = 0; + public float width = 0; + public FilterMode filterMode = FilterMode.BLEND; + public long rows = 0; + public long columns = 0; + public HeadOrTail headOrTail = HeadOrTail.HEAD; + public float tailLength = 0; + public float timeMiddle = 0; + public final float[][] segmentColors = new float[3][3]; + public short[] segmentAlphas = new short[3]; // unsigned byte[] + public float[] segmentScaling = new float[3]; + public long[][] headIntervals = new long[2][3]; + public long[][] tailIntervals = new long[2][3]; + public int textureId = -1; + public long squirt = 0; + public int priorityPlane = 0; + public long replaceableId = 0; + + public MdlxParticleEmitter2() { + super(0x1000); + } + + @Override + public void readMdx(final BinaryReader reader, final int version) { + final int position = reader.position(); + final long size = reader.readUInt32(); + + super.readMdx(reader, version); + + this.speed = reader.readFloat32(); + this.variation = reader.readFloat32(); + this.latitude = reader.readFloat32(); + this.gravity = reader.readFloat32(); + this.lifeSpan = reader.readFloat32(); + this.emissionRate = reader.readFloat32(); + this.length = reader.readFloat32(); + this.width = reader.readFloat32(); + this.filterMode = FilterMode.fromId(reader.readInt32()); + this.rows = reader.readUInt32(); + this.columns = reader.readUInt32(); + this.headOrTail = HeadOrTail.fromId(reader.readInt32()); + this.tailLength = reader.readFloat32(); + this.timeMiddle = reader.readFloat32(); + reader.readFloat32Array(this.segmentColors[0]); + reader.readFloat32Array(this.segmentColors[1]); + reader.readFloat32Array(this.segmentColors[2]); + reader.readUInt8Array(this.segmentAlphas); + reader.readFloat32Array(this.segmentScaling); + reader.readUInt32Array(this.headIntervals[0]); + reader.readUInt32Array(this.headIntervals[1]); + reader.readUInt32Array(this.tailIntervals[0]); + reader.readUInt32Array(this.tailIntervals[1]); + this.textureId = reader.readInt32(); + this.squirt = reader.readUInt32(); + this.priorityPlane = reader.readInt32(); + this.replaceableId = reader.readUInt32(); + + readTimelines(reader, size - (reader.position() - position)); + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeUInt32(getByteLength(version)); + + super.writeMdx(writer, version); + + writer.writeFloat32(this.speed); + writer.writeFloat32(this.variation); + writer.writeFloat32(this.latitude); + writer.writeFloat32(this.gravity); + writer.writeFloat32(this.lifeSpan); + writer.writeFloat32(this.emissionRate); + writer.writeFloat32(this.length); + writer.writeFloat32(this.width); + writer.writeInt32(this.filterMode.ordinal()); + writer.writeUInt32(this.rows); + writer.writeUInt32(this.columns); + writer.writeInt32(this.headOrTail.ordinal()); + writer.writeFloat32(this.tailLength); + writer.writeFloat32(this.timeMiddle); + writer.writeFloat32Array(this.segmentColors[0]); + writer.writeFloat32Array(this.segmentColors[1]); + writer.writeFloat32Array(this.segmentColors[2]); + writer.writeUInt8Array(this.segmentAlphas); + writer.writeFloat32Array(this.segmentScaling); + writer.writeUInt32Array(this.headIntervals[0]); + writer.writeUInt32Array(this.headIntervals[1]); + writer.writeUInt32Array(this.tailIntervals[0]); + writer.writeUInt32Array(this.tailIntervals[1]); + writer.writeInt32(this.textureId); + writer.writeUInt32(this.squirt); + writer.writeInt32(this.priorityPlane); + writer.writeUInt32(this.replaceableId); + + writeNonGenericAnimationChunks(writer); + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + for (final String token : super.readMdlGeneric(stream)) { + switch (token) { + case MdlUtils.TOKEN_SORT_PRIMS_FAR_Z: + this.flags |= 0x10000; + break; + case MdlUtils.TOKEN_UNSHADED: + this.flags |= 0x8000; + break; + case MdlUtils.TOKEN_LINE_EMITTER: + this.flags |= 0x20000; + break; + case MdlUtils.TOKEN_UNFOGGED: + this.flags |= 0x40000; + break; + case MdlUtils.TOKEN_MODEL_SPACE: + this.flags |= 0x80000; + break; + case MdlUtils.TOKEN_XY_QUAD: + this.flags |= 0x100000; + break; + case MdlUtils.TOKEN_STATIC_SPEED: + this.speed = stream.readFloat(); + break; + case MdlUtils.TOKEN_SPEED: + readTimeline(stream, AnimationMap.KP2S); + break; + case MdlUtils.TOKEN_STATIC_VARIATION: + this.variation = stream.readFloat(); + break; + case MdlUtils.TOKEN_VARIATION: + readTimeline(stream, AnimationMap.KP2R); + break; + case MdlUtils.TOKEN_STATIC_LATITUDE: + this.latitude = stream.readFloat(); + break; + case MdlUtils.TOKEN_LATITUDE: + readTimeline(stream, AnimationMap.KP2L); + break; + case MdlUtils.TOKEN_STATIC_GRAVITY: + this.gravity = stream.readFloat(); + break; + case MdlUtils.TOKEN_GRAVITY: + readTimeline(stream, AnimationMap.KP2G); + break; + case MdlUtils.TOKEN_VISIBILITY: + readTimeline(stream, AnimationMap.KP2V); + break; + case MdlUtils.TOKEN_SQUIRT: + this.squirt = 1; + break; + case MdlUtils.TOKEN_LIFE_SPAN: + this.lifeSpan = stream.readFloat(); + break; + case MdlUtils.TOKEN_STATIC_EMISSION_RATE: + this.emissionRate = stream.readFloat(); + break; + case MdlUtils.TOKEN_EMISSION_RATE: + readTimeline(stream, AnimationMap.KP2E); + break; + case MdlUtils.TOKEN_STATIC_WIDTH: + this.width = stream.readFloat(); + break; + case MdlUtils.TOKEN_WIDTH: + readTimeline(stream, AnimationMap.KP2W); + break; + case MdlUtils.TOKEN_STATIC_LENGTH: + this.length = stream.readFloat(); + break; + case MdlUtils.TOKEN_LENGTH: + readTimeline(stream, AnimationMap.KP2N); + break; + case MdlUtils.TOKEN_BLEND: + this.filterMode = FilterMode.BLEND; + break; + case MdlUtils.TOKEN_ADDITIVE: + this.filterMode = FilterMode.ADDITIVE; + break; + case MdlUtils.TOKEN_MODULATE: + this.filterMode = FilterMode.MODULATE; + break; + case MdlUtils.TOKEN_MODULATE2X: + this.filterMode = FilterMode.MODULATE2X; + break; + case MdlUtils.TOKEN_ALPHAKEY: + this.filterMode = FilterMode.ALPHAKEY; + break; + case MdlUtils.TOKEN_ROWS: + this.rows = stream.readUInt32(); + break; + case MdlUtils.TOKEN_COLUMNS: + this.columns = stream.readUInt32(); + break; + case MdlUtils.TOKEN_HEAD: + this.headOrTail = HeadOrTail.HEAD; + break; + case MdlUtils.TOKEN_TAIL: + this.headOrTail = HeadOrTail.TAIL; + break; + case MdlUtils.TOKEN_BOTH: + this.headOrTail = HeadOrTail.BOTH; + break; + case MdlUtils.TOKEN_TAIL_LENGTH: + this.tailLength = stream.readFloat(); + break; + case MdlUtils.TOKEN_TIME: + this.timeMiddle = stream.readFloat(); + break; + case MdlUtils.TOKEN_SEGMENT_COLOR: { + stream.read(); // { + for (int i = 0; i < 3; i++) { + stream.read(); // Color + stream.readColor(this.segmentColors[i]); + } + stream.read(); // } + } + break; + case MdlUtils.TOKEN_ALPHA: + stream.readUInt8Array(this.segmentAlphas); + break; + case MdlUtils.TOKEN_PARTICLE_SCALING: + stream.readFloatArray(this.segmentScaling); + break; + case MdlUtils.TOKEN_LIFE_SPAN_UV_ANIM: + stream.readIntArray(this.headIntervals[0]); + break; + case MdlUtils.TOKEN_DECAY_UV_ANIM: + stream.readIntArray(this.headIntervals[1]); + break; + case MdlUtils.TOKEN_TAIL_UV_ANIM: + stream.readIntArray(this.tailIntervals[0]); + break; + case MdlUtils.TOKEN_TAIL_DECAY_UV_ANIM: + stream.readIntArray(this.tailIntervals[1]); + break; + case MdlUtils.TOKEN_TEXTURE_ID: + this.textureId = stream.readInt(); + break; + case MdlUtils.TOKEN_REPLACEABLE_ID: + this.replaceableId = stream.readUInt32(); + break; + case MdlUtils.TOKEN_PRIORITY_PLANE: + this.priorityPlane = stream.readInt(); + break; + default: + throw new RuntimeException("Unknown token in ParticleEmitter2 " + this.name + ": " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startObjectBlock(MdlUtils.TOKEN_PARTICLE_EMITTER2, this.name); + writeGenericHeader(stream); + + if ((this.flags & 0x10000) != 0) { + stream.writeFlag(MdlUtils.TOKEN_SORT_PRIMS_FAR_Z); + } + + if ((this.flags & 0x8000) != 0) { + stream.writeFlag(MdlUtils.TOKEN_UNSHADED); + } + + if ((this.flags & 0x20000) != 0) { + stream.writeFlag(MdlUtils.TOKEN_LINE_EMITTER); + } + + if ((this.flags & 0x40000) != 0) { + stream.writeFlag(MdlUtils.TOKEN_UNFOGGED); + } + + if ((this.flags & 0x80000) != 0) { + stream.writeFlag(MdlUtils.TOKEN_MODEL_SPACE); + } + + if ((this.flags & 0x100000) != 0) { + stream.writeFlag(MdlUtils.TOKEN_XY_QUAD); + } + + if (!writeTimeline(stream, AnimationMap.KP2S)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_SPEED, this.speed); + } + + if (!writeTimeline(stream, AnimationMap.KP2R)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_VARIATION, this.variation); + } + + if (!writeTimeline(stream, AnimationMap.KP2L)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_LATITUDE, this.latitude); + } + + if (!writeTimeline(stream, AnimationMap.KP2G)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_GRAVITY, this.gravity); + } + + writeTimeline(stream, AnimationMap.KP2V); + + if (this.squirt != 0) { + stream.writeFlag(MdlUtils.TOKEN_SQUIRT); + } + + stream.writeFloatAttrib(MdlUtils.TOKEN_LIFE_SPAN, this.lifeSpan); + + if (!writeTimeline(stream, AnimationMap.KP2E)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_EMISSION_RATE, this.emissionRate); + } + + if (!writeTimeline(stream, AnimationMap.KP2W)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_WIDTH, this.width); + } + + if (!writeTimeline(stream, AnimationMap.KP2N)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_LENGTH, this.length); + } + + stream.writeFlag(this.filterMode.toString()); + stream.writeAttribUInt32(MdlUtils.TOKEN_ROWS, this.rows); + stream.writeAttribUInt32(MdlUtils.TOKEN_COLUMNS, this.columns); + stream.writeFlag(this.headOrTail.toString()); + stream.writeFloatAttrib(MdlUtils.TOKEN_TAIL_LENGTH, this.tailLength); + stream.writeFloatAttrib(MdlUtils.TOKEN_TIME, this.timeMiddle); + + stream.startBlock(MdlUtils.TOKEN_SEGMENT_COLOR); + stream.writeColor(MdlUtils.TOKEN_COLOR, this.segmentColors[0]); + stream.writeColor(MdlUtils.TOKEN_COLOR, this.segmentColors[1]); + stream.writeColor(MdlUtils.TOKEN_COLOR, this.segmentColors[2]); + stream.endBlockComma(); + + stream.writeArrayAttrib(MdlUtils.TOKEN_ALPHA, this.segmentAlphas); + stream.writeFloatArrayAttrib(MdlUtils.TOKEN_PARTICLE_SCALING, this.segmentScaling); + stream.writeArrayAttrib(MdlUtils.TOKEN_LIFE_SPAN_UV_ANIM, this.headIntervals[0]); + stream.writeArrayAttrib(MdlUtils.TOKEN_DECAY_UV_ANIM, this.headIntervals[1]); + stream.writeArrayAttrib(MdlUtils.TOKEN_TAIL_UV_ANIM, this.tailIntervals[0]); + stream.writeArrayAttrib(MdlUtils.TOKEN_TAIL_DECAY_UV_ANIM, this.tailIntervals[1]); + stream.writeAttrib(MdlUtils.TOKEN_TEXTURE_ID, this.textureId); + + if (this.replaceableId != 0) { + stream.writeAttribUInt32(MdlUtils.TOKEN_REPLACEABLE_ID, this.replaceableId); + } + + if (this.priorityPlane != 0) { + stream.writeAttrib(MdlUtils.TOKEN_PRIORITY_PLANE, this.priorityPlane); + } + + writeGenericTimelines(stream); + + stream.endBlock(); + } + + @Override + public long getByteLength(final int version) { + return 175 + super.getByteLength(version); + } + + public float getSpeed() { + return this.speed; + } + + public float getVariation() { + return this.variation; + } + + public float getLatitude() { + return this.latitude; + } + + public float getGravity() { + return this.gravity; + } + + public float getLifeSpan() { + return this.lifeSpan; + } + + public float getEmissionRate() { + return this.emissionRate; + } + + public float getLength() { + return this.length; + } + + public float getWidth() { + return this.width; + } + + public FilterMode getFilterMode() { + return this.filterMode; + } + + public long getRows() { + return this.rows; + } + + public long getColumns() { + return this.columns; + } + + public HeadOrTail getHeadOrTail() { + return this.headOrTail; + } + + public float getTailLength() { + return this.tailLength; + } + + public float getTimeMiddle() { + return this.timeMiddle; + } + + public float[][] getSegmentColors() { + return this.segmentColors; + } + + public short[] getSegmentAlphas() { + return this.segmentAlphas; + } + + public float[] getSegmentScaling() { + return this.segmentScaling; + } + + public long[][] getHeadIntervals() { + return this.headIntervals; + } + + public long[][] getTailIntervals() { + return this.tailIntervals; + } + + public int getTextureId() { + return this.textureId; + } + + public long getSquirt() { + return this.squirt; + } + + public int getPriorityPlane() { + return this.priorityPlane; + } + + public long getReplaceableId() { + return this.replaceableId; + } + + public void setSpeed(final float speed) { + this.speed = speed; + } + + public void setVariation(final float variation) { + this.variation = variation; + } + + public void setLatitude(final float latitude) { + this.latitude = latitude; + } + + public void setGravity(final float gravity) { + this.gravity = gravity; + } + + public void setLifeSpan(final float lifeSpan) { + this.lifeSpan = lifeSpan; + } + + public void setEmissionRate(final float emissionRate) { + this.emissionRate = emissionRate; + } + + public void setLength(final float length) { + this.length = length; + } + + public void setWidth(final float width) { + this.width = width; + } + + public void setFilterMode(final FilterMode filterMode) { + this.filterMode = filterMode; + } + + public void setRows(final long rows) { + this.rows = rows; + } + + public void setColumns(final long columns) { + this.columns = columns; + } + + public void setHeadOrTail(final HeadOrTail headOrTail) { + this.headOrTail = headOrTail; + } + + public void setTailLength(final float tailLength) { + this.tailLength = tailLength; + } + + public void setTimeMiddle(final float timeMiddle) { + this.timeMiddle = timeMiddle; + } + + public void setSegmentAlphas(final short[] segmentAlphas) { + this.segmentAlphas = segmentAlphas; + } + + public void setSegmentScaling(final float[] segmentScaling) { + this.segmentScaling = segmentScaling; + } + + public void setHeadIntervals(final long[][] headIntervals) { + this.headIntervals = headIntervals; + } + + public void setTailIntervals(final long[][] tailIntervals) { + this.tailIntervals = tailIntervals; + } + + public void setTextureId(final int textureId) { + this.textureId = textureId; + } + + public void setSquirt(final long squirt) { + this.squirt = squirt; + } + + public void setPriorityPlane(final int priorityPlane) { + this.priorityPlane = priorityPlane; + } + + public void setReplaceableId(final long replaceableId) { + this.replaceableId = replaceableId; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxParticleEmitterPopcorn.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxParticleEmitterPopcorn.java new file mode 100644 index 0000000..f48f4ff --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxParticleEmitterPopcorn.java @@ -0,0 +1,180 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxParticleEmitterPopcorn extends MdlxGenericObject { + public float lifeSpan = 0; + public float emissionRate = 0; + public float speed = 0; + public float[] color = new float[] { 1, 1, 1 }; + public float alpha = 0; + public int replaceableId = 0; + public String path = ""; + public String animationVisiblityGuide = ""; + + public MdlxParticleEmitterPopcorn() { + super(0); + } + + @Override + public void readMdx(final BinaryReader reader, final int version) { + final int position = reader.position(); + final long size = reader.readUInt32(); + + super.readMdx(reader, version); + + this.lifeSpan = reader.readFloat32(); + this.emissionRate = reader.readFloat32(); + this.speed = reader.readFloat32(); + reader.readFloat32Array(this.color); + this.alpha = reader.readFloat32(); + this.replaceableId = reader.readInt32(); + this.path = reader.read(260); + this.animationVisiblityGuide = reader.read(260); + + readTimelines(reader, size - (reader.position() - position)); + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeUInt32(getByteLength(version)); + + super.writeMdx(writer, version); + + writer.writeFloat32(this.lifeSpan); + writer.writeFloat32(this.emissionRate); + writer.writeFloat32(this.speed); + writer.writeFloat32Array(this.color); + writer.writeFloat32(this.alpha); + writer.writeInt32(this.replaceableId); + writer.writeWithNulls(this.path, 260); + writer.writeWithNulls(this.animationVisiblityGuide, 260); + + writeNonGenericAnimationChunks(writer); + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + for (final String token : super.readMdlGeneric(stream)) { + switch (token) { + case "SortPrimsFarZ": + this.flags |= 0x10000; + break; + case "Unshaded": + this.flags |= 0x8000; + break; + case "Unfogged": + this.flags |= 0x40000; + break; + case "static LifeSpan": + this.lifeSpan = stream.readFloat(); + break; + case "LifeSpan": + readTimeline(stream, AnimationMap.KPPL); + break; + case "static EmissionRate": + this.emissionRate = stream.readFloat(); + break; + case "EmissionRate": + readTimeline(stream, AnimationMap.KPPE); + break; + case "static Speed": + this.speed = stream.readFloat(); + break; + case "Speed": + readTimeline(stream, AnimationMap.KPPS); + break; + case "static Color": + stream.readColor(this.color); + break; + case "Color": + readTimeline(stream, AnimationMap.KPPC); + break; + case "static Alpha": + this.alpha = stream.readFloat(); + break; + case "Alpha": + readTimeline(stream, AnimationMap.KPPA); + break; + case "Visibility": + readTimeline(stream, AnimationMap.KPPV); + break; + case "ReplaceableId": + this.replaceableId = stream.readInt(); + break; + case "Path": + this.path = stream.read(); + break; + case "AnimVisibilityGuide": + this.animationVisiblityGuide = stream.read(); + break; + default: + throw new RuntimeException("Unknown token in MdlxParticleEmitterPopcorn " + this.name + ": " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startObjectBlock(MdlUtils.TOKEN_PARTICLE_EMITTER2, this.name); + writeGenericHeader(stream); + + if ((this.flags & 0x10000) != 0) { + stream.writeFlag("SortPrimsFarZ"); + } + + if ((this.flags & 0x8000) != 0) { + stream.writeFlag("Unshaded"); + } + + if ((this.flags & 0x40000) != 0) { + stream.writeFlag("Unfogged"); + } + + if (!writeTimeline(stream, AnimationMap.KPPL)) { + stream.writeFloatAttrib("static LifeSpan", this.lifeSpan); + } + + if (!writeTimeline(stream, AnimationMap.KPPE)) { + stream.writeFloatAttrib("static EmissionRate", this.emissionRate); + } + + if (!writeTimeline(stream, AnimationMap.KPPS)) { + stream.writeFloatAttrib("static Speed", this.speed); + } + + if (!writeTimeline(stream, AnimationMap.KPPC)) { + stream.writeFloatArrayAttrib("static Color", this.color); + } + + if (!writeTimeline(stream, AnimationMap.KPPA)) { + stream.writeFloatAttrib("static Alpha", this.alpha); + } + + writeTimeline(stream, AnimationMap.KPPV); + + if (this.replaceableId != 0) { + stream.writeAttrib("ReplaceableId", this.replaceableId); + } + + if (this.path.length() != 0) { + stream.writeStringAttrib("Path", this.path); + } + + if (this.animationVisiblityGuide.length() != 0) { + stream.writeStringAttrib("AnimVisibilityGuide", this.animationVisiblityGuide); + } + + writeGenericTimelines(stream); + stream.endBlock(); + } + + @Override + public long getByteLength(final int version) { + return 556 + super.getByteLength(version); + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxRibbonEmitter.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxRibbonEmitter.java new file mode 100644 index 0000000..c504621 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxRibbonEmitter.java @@ -0,0 +1,264 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxRibbonEmitter extends MdlxGenericObject { + public float heightAbove = 0; + public float heightBelow = 0; + public float alpha = 0; + public float[] color = new float[3]; + public float lifeSpan = 0; + public long textureSlot = 0; + public long emissionRate = 0; + public long rows = 0; + public long columns = 0; + public int materialId = 0; + public float gravity = 0; + + public MdlxRibbonEmitter() { + super(0x4000); + } + + @Override + public void readMdx(final BinaryReader reader, final int version) { + final int position = reader.position(); + final long size = reader.readUInt32(); + + super.readMdx(reader, version); + + this.heightAbove = reader.readFloat32(); + this.heightBelow = reader.readFloat32(); + this.alpha = reader.readFloat32(); + reader.readFloat32Array(this.color); + this.lifeSpan = reader.readFloat32(); + this.textureSlot = reader.readUInt32(); + this.emissionRate = reader.readUInt32(); + this.rows = reader.readUInt32(); + this.columns = reader.readUInt32(); + this.materialId = reader.readInt32(); + this.gravity = reader.readFloat32(); + + readTimelines(reader, size - (reader.position() - position)); + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeUInt32(getByteLength(version)); + + super.writeMdx(writer, version); + + writer.writeFloat32(this.heightAbove); + writer.writeFloat32(this.heightBelow); + writer.writeFloat32(this.alpha); + writer.writeFloat32Array(this.color); + writer.writeFloat32(this.lifeSpan); + writer.writeUInt32(this.textureSlot); + writer.writeUInt32(this.emissionRate); + writer.writeUInt32(this.rows); + writer.writeUInt32(this.columns); + writer.writeInt32(this.materialId); + writer.writeFloat32(this.gravity); + + writeNonGenericAnimationChunks(writer); + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + for (final String token : super.readMdlGeneric(stream)) { + switch (token) { + case MdlUtils.TOKEN_STATIC_HEIGHT_ABOVE: + this.heightAbove = stream.readFloat(); + break; + case MdlUtils.TOKEN_HEIGHT_ABOVE: + readTimeline(stream, AnimationMap.KRHA); + break; + case MdlUtils.TOKEN_STATIC_HEIGHT_BELOW: + this.heightBelow = stream.readFloat(); + break; + case MdlUtils.TOKEN_HEIGHT_BELOW: + readTimeline(stream, AnimationMap.KRHB); + break; + case MdlUtils.TOKEN_STATIC_ALPHA: + this.alpha = stream.readFloat(); + break; + case MdlUtils.TOKEN_ALPHA: + readTimeline(stream, AnimationMap.KRAL); + break; + case MdlUtils.TOKEN_STATIC_COLOR: + stream.readColor(this.color); + break; + case MdlUtils.TOKEN_COLOR: + readTimeline(stream, AnimationMap.KRCO); + break; + case MdlUtils.TOKEN_STATIC_TEXTURE_SLOT: + this.textureSlot = stream.readUInt32(); + break; + case MdlUtils.TOKEN_TEXTURE_SLOT: + readTimeline(stream, AnimationMap.KRTX); + break; + case MdlUtils.TOKEN_VISIBILITY: + readTimeline(stream, AnimationMap.KRVS); + break; + case MdlUtils.TOKEN_EMISSION_RATE: + this.emissionRate = stream.readUInt32(); + break; + case MdlUtils.TOKEN_LIFE_SPAN: + this.lifeSpan = stream.readFloat(); + break; + case MdlUtils.TOKEN_GRAVITY: + this.gravity = stream.readFloat(); + break; + case MdlUtils.TOKEN_ROWS: + this.rows = stream.readUInt32(); + break; + case MdlUtils.TOKEN_COLUMNS: + this.columns = stream.readUInt32(); + break; + case MdlUtils.TOKEN_MATERIAL_ID: + this.materialId = stream.readInt(); + break; + default: + throw new RuntimeException("Unknown token in RibbonEmitter " + this.name + ": " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startObjectBlock(MdlUtils.TOKEN_RIBBON_EMITTER, this.name); + writeGenericHeader(stream); + + if (!writeTimeline(stream, AnimationMap.KRHA)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_HEIGHT_ABOVE, this.heightAbove); + } + + if (!writeTimeline(stream, AnimationMap.KRHB)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_HEIGHT_BELOW, this.heightBelow); + } + + if (!writeTimeline(stream, AnimationMap.KRAL)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_ALPHA, this.alpha); + } + + if (!writeTimeline(stream, AnimationMap.KRCO)) { + stream.writeColor(MdlUtils.TOKEN_STATIC_COLOR, this.color); + } + + if (!writeTimeline(stream, AnimationMap.KRTX)) { + stream.writeAttribUInt32(MdlUtils.TOKEN_STATIC_TEXTURE_SLOT, this.textureSlot); + } + + writeTimeline(stream, AnimationMap.KRVS); + + stream.writeAttribUInt32(MdlUtils.TOKEN_EMISSION_RATE, this.emissionRate); + stream.writeFloatAttrib(MdlUtils.TOKEN_LIFE_SPAN, this.lifeSpan); + + if (this.gravity != 0) { + stream.writeFloatAttrib(MdlUtils.TOKEN_GRAVITY, this.gravity); + } + + stream.writeAttribUInt32(MdlUtils.TOKEN_ROWS, this.rows); + stream.writeAttribUInt32(MdlUtils.TOKEN_COLUMNS, this.columns); + stream.writeAttrib(MdlUtils.TOKEN_MATERIAL_ID, this.materialId); + + writeGenericTimelines(stream); + stream.endBlock(); + } + + @Override + public long getByteLength(final int version) { + return 56 + super.getByteLength(version); + } + + public float getHeightAbove() { + return this.heightAbove; + } + + public float getHeightBelow() { + return this.heightBelow; + } + + public float getAlpha() { + return this.alpha; + } + + public float[] getColor() { + return this.color; + } + + public float getLifeSpan() { + return this.lifeSpan; + } + + public long getTextureSlot() { + return this.textureSlot; + } + + public long getEmissionRate() { + return this.emissionRate; + } + + public long getRows() { + return this.rows; + } + + public long getColumns() { + return this.columns; + } + + public int getMaterialId() { + return this.materialId; + } + + public float getGravity() { + return this.gravity; + } + + public void setHeightAbove(final float heightAbove) { + this.heightAbove = heightAbove; + } + + public void setHeightBelow(final float heightBelow) { + this.heightBelow = heightBelow; + } + + public void setAlpha(final float alpha) { + this.alpha = alpha; + } + + public void setColor(final float[] color) { + this.color = color; + } + + public void setLifeSpan(final float lifeSpan) { + this.lifeSpan = lifeSpan; + } + + public void setTextureSlot(final long textureSlot) { + this.textureSlot = textureSlot; + } + + public void setEmissionRate(final long emissionRate) { + this.emissionRate = emissionRate; + } + + public void setRows(final long rows) { + this.rows = rows; + } + + public void setColumns(final long columns) { + this.columns = columns; + } + + public void setMaterialId(final int materialId) { + this.materialId = materialId; + } + + public void setGravity(final float gravity) { + this.gravity = gravity; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxSequence.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxSequence.java new file mode 100644 index 0000000..9f0b3c8 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxSequence.java @@ -0,0 +1,121 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxSequence implements MdlxBlock { + public String name = ""; + public long[] interval = new long[2]; + public float moveSpeed = 0; + public int flags = 0; + public float rarity = 0; + public long syncPoint = 0; + public MdlxExtent extent = new MdlxExtent(); + + @Override + public void readMdx(final BinaryReader reader, final int version) { + this.name = reader.read(80); + reader.readUInt32Array(this.interval); + this.moveSpeed = reader.readFloat32(); + this.flags = reader.readInt32(); + this.rarity = reader.readFloat32(); + this.syncPoint = reader.readUInt32(); + this.extent.readMdx(reader); + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeWithNulls(this.name, 80); + writer.writeUInt32Array(this.interval); + writer.writeFloat32(this.moveSpeed); + writer.writeUInt32(this.flags); + writer.writeFloat32(this.rarity); + writer.writeUInt32(this.syncPoint); + this.extent.writeMdx(writer); + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + this.name = stream.read(); + + for (final String token : stream.readBlock()) { + switch (token) { + case MdlUtils.TOKEN_INTERVAL: + stream.readIntArray(this.interval); + break; + case MdlUtils.TOKEN_NONLOOPING: + this.flags = 1; + break; + case MdlUtils.TOKEN_MOVESPEED: + this.moveSpeed = stream.readFloat(); + break; + case MdlUtils.TOKEN_RARITY: + this.rarity = stream.readFloat(); + break; + case MdlUtils.TOKEN_MINIMUM_EXTENT: + stream.readFloatArray(this.extent.min); + break; + case MdlUtils.TOKEN_MAXIMUM_EXTENT: + stream.readFloatArray(this.extent.max); + break; + case MdlUtils.TOKEN_BOUNDSRADIUS: + this.extent.boundsRadius = stream.readFloat(); + break; + default: + throw new IllegalStateException("Unknown token in Sequence \"" + this.name + "\": " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startObjectBlock(MdlUtils.TOKEN_ANIM, this.name); + stream.writeArrayAttrib(MdlUtils.TOKEN_INTERVAL, this.interval); + + if (this.flags == 1) { + stream.writeFlag(MdlUtils.TOKEN_NONLOOPING); + } + + if (this.moveSpeed != 0) { + stream.writeFloatAttrib(MdlUtils.TOKEN_MOVESPEED, this.moveSpeed); + } + + if (this.rarity != 0) { + stream.writeFloatAttrib(MdlUtils.TOKEN_RARITY, this.rarity); + } + + this.extent.writeMdl(stream); + stream.endBlock(); + } + + public String getName() { + return this.name; + } + + public long[] getInterval() { + return this.interval; + } + + public float getMoveSpeed() { + return this.moveSpeed; + } + + public int getFlags() { + return this.flags; + } + + public float getRarity() { + return this.rarity; + } + + public long getSyncPoint() { + return this.syncPoint; + } + + public MdlxExtent getExtent() { + return this.extent; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxTexture.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxTexture.java new file mode 100644 index 0000000..657eb5f --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxTexture.java @@ -0,0 +1,120 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxTexture implements MdlxBlock { + public enum WrapMode { + REPEAT_BOTH(false, false), + WRAP_WIDTH(true, false), + WRAP_HEIGHT(false, true), + WRAP_BOTH(true, true); + + private final boolean wrapWidth; + private final boolean wrapHeight; + + public static WrapMode fromId(final int id) { + return values()[id]; + } + + private WrapMode(final boolean wrapWidth, final boolean wrapHeight) { + this.wrapWidth = wrapWidth; + this.wrapHeight = wrapHeight; + } + + public boolean isWrapWidth() { + return this.wrapWidth; + } + + public boolean isWrapHeight() { + return this.wrapHeight; + } + } + + public int replaceableId = 0; + public String path = ""; + public WrapMode wrapMode = WrapMode.REPEAT_BOTH; + + @Override + public void readMdx(final BinaryReader reader, final int version) { + this.replaceableId = reader.readInt32(); + this.path = reader.read(260); + this.wrapMode = WrapMode.fromId(reader.readInt32()); + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeInt32(this.replaceableId); + writer.writeWithNulls(this.path, 260); + writer.writeInt32(this.wrapMode.ordinal()); + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + for (final String token : stream.readBlock()) { + switch (token) { + case MdlUtils.TOKEN_IMAGE: + this.path = stream.read(); + break; + case MdlUtils.TOKEN_REPLACEABLE_ID: + this.replaceableId = stream.readInt(); + break; + case MdlUtils.TOKEN_WRAP_WIDTH: + this.wrapMode = WrapMode.fromId(this.wrapMode.ordinal() + 0x1); + break; + case MdlUtils.TOKEN_WRAP_HEIGHT: + this.wrapMode = WrapMode.fromId(this.wrapMode.ordinal() + 0x2); + break; + default: + throw new RuntimeException("Unknown token in Texture: " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startBlock(MdlUtils.TOKEN_BITMAP); + stream.writeStringAttrib(MdlUtils.TOKEN_IMAGE, this.path); + + if (this.replaceableId != 0) { + stream.writeAttrib(MdlUtils.TOKEN_REPLACEABLE_ID, this.replaceableId); + } + + if ((this.wrapMode == WrapMode.WRAP_WIDTH) || (this.wrapMode == WrapMode.WRAP_BOTH)) { + stream.writeFlag(MdlUtils.TOKEN_WRAP_WIDTH); + } + + if ((this.wrapMode == WrapMode.WRAP_HEIGHT) || (this.wrapMode == WrapMode.WRAP_BOTH)) { + stream.writeFlag(MdlUtils.TOKEN_WRAP_HEIGHT); + } + + stream.endBlock(); + } + + public int getReplaceableId() { + return this.replaceableId; + } + + public String getPath() { + return this.path; + } + + public WrapMode getWrapMode() { + return this.wrapMode; + } + + public void setReplaceableId(final int replaceableId) { + this.replaceableId = replaceableId; + } + + public void setPath(final String path) { + this.path = path; + } + + public void setWrapMode(final WrapMode wrapMode) { + this.wrapMode = wrapMode; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxTextureAnimation.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxTextureAnimation.java new file mode 100644 index 0000000..acbc889 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxTextureAnimation.java @@ -0,0 +1,56 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxTextureAnimation extends MdlxAnimatedObject { + @Override + public void readMdx(final BinaryReader reader, final int version) { + final long size = reader.readUInt32(); + + readTimelines(reader, size - 4); + } + + @Override + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeUInt32(getByteLength(version)); + + writeTimelines(writer); + } + + @Override + public void readMdl(final MdlTokenInputStream stream, final int version) { + for (final String token : stream.readBlock()) { + switch (token) { + case MdlUtils.TOKEN_TRANSLATION: + readTimeline(stream, AnimationMap.KTAT); + break; + case MdlUtils.TOKEN_ROTATION: + readTimeline(stream, AnimationMap.KTAR); + break; + case MdlUtils.TOKEN_SCALING: + readTimeline(stream, AnimationMap.KTAS); + break; + default: + throw new RuntimeException("Unknown token in TextureAnimation: " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream, final int version) { + stream.startBlock(MdlUtils.TOKEN_TVERTEX_ANIM); + writeTimeline(stream, AnimationMap.KTAT); + writeTimeline(stream, AnimationMap.KTAR); + writeTimeline(stream, AnimationMap.KTAS); + stream.endBlock(); + } + + @Override + public long getByteLength(final int version) { + return 4 + super.getByteLength(version); + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxTimelineDescriptor.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxTimelineDescriptor.java new file mode 100644 index 0000000..b413cfc --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxTimelineDescriptor.java @@ -0,0 +1,18 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.hiveworkshop.rms.parsers.mdlx.timeline.MdlxFloatArrayTimeline; +import com.hiveworkshop.rms.parsers.mdlx.timeline.MdlxFloatTimeline; +import com.hiveworkshop.rms.parsers.mdlx.timeline.MdlxTimeline; +import com.hiveworkshop.rms.parsers.mdlx.timeline.MdlxUInt32Timeline; + +public interface MdlxTimelineDescriptor { + MdlxTimeline createTimeline(); + + MdlxTimelineDescriptor UINT32_TIMELINE = MdlxUInt32Timeline::new; + + MdlxTimelineDescriptor FLOAT_TIMELINE = MdlxFloatTimeline::new; + + MdlxTimelineDescriptor VECTOR3_TIMELINE = () -> new MdlxFloatArrayTimeline(3); + + MdlxTimelineDescriptor VECTOR4_TIMELINE = () -> new MdlxFloatArrayTimeline(4); +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxUnknownChunk.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxUnknownChunk.java new file mode 100644 index 0000000..e478787 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/MdlxUnknownChunk.java @@ -0,0 +1,31 @@ +package com.hiveworkshop.rms.parsers.mdlx; + +import com.etheller.warsmash.util.War3ID; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public class MdlxUnknownChunk implements MdlxChunk { + public final short[] chunk; + public final War3ID tag; + + public MdlxUnknownChunk(final BinaryReader reader, final long size, final War3ID tag) { + System.err.println("Loading unknown chunk: " + tag); + this.chunk = reader.readUInt8Array((int) size); + this.tag = tag; + } + + public void writeMdx(final BinaryWriter writer, final int version) { + writer.writeTag(this.tag.getValue()); + // Below: Byte.BYTES used because it's mean as a UInt8 array. This is + // not using Short.BYTES, deliberately, despite using a short[] as the + // type for the array. This is a Java problem that did not exist in the original + // JavaScript implementation by Ghostwolf + writer.writeUInt32(this.chunk.length * Byte.BYTES); + writer.writeUInt8Array(this.chunk); + } + + @Override + public long getByteLength(final int version) { + return 8 + (this.chunk.length * Byte.BYTES); + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/mdl/MdlTokenInputStream.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/mdl/MdlTokenInputStream.java new file mode 100644 index 0000000..bdba276 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/mdl/MdlTokenInputStream.java @@ -0,0 +1,208 @@ +package com.hiveworkshop.rms.parsers.mdlx.mdl; + +import java.nio.ByteBuffer; +import java.util.Iterator; + +public class MdlTokenInputStream { + private final ByteBuffer buffer; + private int index; + + public MdlTokenInputStream(final ByteBuffer buffer) { + this.buffer = buffer; + this.index = 0; + } + + public String read() { + boolean inComment = false; + boolean inString = false; + final StringBuilder token = new StringBuilder(); + final int length = this.buffer.remaining(); + + while (this.index < length) { + // Note: cast from 'byte' to 'char' will cause Java incompatibility with Chinese + // and Russian/Cyrillic and others + final char c = (char) this.buffer.get(this.buffer.position() + this.index++); + + if (inComment) { + if (c == '\n') { + inComment = false; + } + } + else if (inString) { + if (c == '"') { + return token.toString(); + } + else { + token.append(c); + } + } + else if ((c == ' ') || (c == ',') || (c == '\t') || (c == '\n') || (c == ':') || (c == '\r')) { + if (token.length() > 0) { + return token.toString(); + } + } + else if ((c == '{') || (c == '}')) { + if (token.length() > 0) { + this.index--; + return token.toString(); + } + else { + return Character.toString(c); + } + } + else if ((c == '/') && (this.buffer.get(this.buffer.position() + this.index) == '/')) { + if (token.length() > 0) { + this.index--; + return token.toString(); + } + else { + inComment = true; + } + } + else if (c == '"') { + if (token.length() > 0) { + this.index--; + return token.toString(); + } + else { + inString = true; + } + } + else { + token.append(c); + } + } + return null; + } + + public String peek() { + final int index = this.index; + final String value = read(); + + this.index = index; + return value; + } + + public long readUInt32() { + return Long.parseLong(read()); + } + + public int readInt() { + return Integer.parseInt(read()); + } + + public float readFloat() { + return Float.parseFloat(read()); + } + + public void readIntArray(final long[] values) { + read(); // { + + for (int i = 0, l = values.length; i < l; i++) { + values[i] = readInt(); + } + + read(); // } + } + + public float[] readFloatArray(final float[] values) { + read(); // { + + for (int i = 0, l = values.length; i < l; i++) { + values[i] = readFloat(); + } + + read(); // } + return values; + } + + /** + * Read an MDL keyframe value. If the value is a scalar, it is just the number. + * If the value is a vector, it is enclosed with curly braces. + * + * @param values {Float32Array|Uint32Array} + */ + public void readKeyframe(final float[] values) { + if (values.length == 1) { + values[0] = readFloat(); + } + else { + readFloatArray(values); + } + } + + public float[] readVectorArray(final float[] array, final int vectorLength) { + read(); // { + + for (int i = 0, l = array.length / vectorLength; i < l; i++) { + read(); // { + + for (int j = 0; j < vectorLength; j++) { + array[(i * vectorLength) + j] = readFloat(); + } + + read(); // } + } + + read(); // } + return array; + } + + public Iterable readBlock() { + read(); // { + return () -> new Iterator() { + String current; + private boolean hasLoaded = false; + + @Override + public String next() { + if (!this.hasLoaded) { + hasNext(); + } + this.hasLoaded = false; + return this.current; + } + + @Override + public boolean hasNext() { + this.current = read(); + this.hasLoaded = true; + return (this.current != null) && !this.current.equals("}"); + } + }; + } + + public int[] readUInt16Array(final int[] values) { + read(); // { + + for (int i = 0, l = values.length; i < l; i++) { + values[i] = readInt(); + } + + read(); // } + + return values; + } + + public short[] readUInt8Array(final short[] values) { + read(); // { + + for (int i = 0, l = values.length; i < l; i++) { + values[i] = Short.parseShort(read()); + } + + read(); // } + + return values; + } + + public void readColor(final float[] color) { + read(); // { + + color[2] = readFloat(); + color[1] = readFloat(); + color[0] = readFloat(); + + read(); // } + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/mdl/MdlTokenOutputStream.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/mdl/MdlTokenOutputStream.java new file mode 100644 index 0000000..8d35bfc --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/mdl/MdlTokenOutputStream.java @@ -0,0 +1,220 @@ +package com.hiveworkshop.rms.parsers.mdlx.mdl; + +public class MdlTokenOutputStream { + public final StringBuilder buffer = new StringBuilder(); + public int ident = 0; + public int fractionDigits = 6; + + public void writeKeyframe(final String prefix, final long uInt32Value) { + writeAttribUInt32(prefix, uInt32Value); + } + + public void writeKeyframe(final String prefix, final float floatValue) { + writeFloatAttrib(prefix, floatValue); + } + + public void writeKeyframe(final String prefix, final float[] floatArrayValues) { + writeFloatArrayAttrib(prefix, floatArrayValues); + } + + public void indent() { + this.ident += 1; + } + + public void unindent() { + this.ident -= 1; + } + + public void startObjectBlock(final String name, final String objectName) { + writeLine(name + " \"" + objectName + "\" {"); + this.ident += 1; + } + + public void startBlock(final String name, final int blockSize) { + writeLine(name + " " + blockSize + " {" + ""); + this.ident += 1; + } + + public void startBlock(final String name) { + writeLine(name + " {" + ""); + this.ident += 1; + } + + public void writeFlag(final String token) { + writeLine(token + ","); + } + + public void writeFlagUInt32(final long flag) { + writeLine(flag + ","); + } + + public void writeAttrib(final String string, final int globalSequenceId) { + writeLine(string + " " + globalSequenceId + ","); + } + + public void writeAttribUInt32(final String attribName, final long uInt) { + writeLine(attribName + " " + uInt + ","); + } + + public void writeAttrib(final String string, final String value) { + writeLine(string + " " + value + ","); + } + + public void writeFloatAttrib(final String attribName, final float value) { + writeLine(attribName + " " + value + ","); + } + + public void writeStringAttrib(final String attribName, final String value) { + writeLine(attribName + " \"" + value + "\","); + + } + + public void writeFloatArrayAttrib(final String attribName, final float[] floatArray) { + writeLine(attribName + " { " + formatFloatArray(floatArray) + " },"); + } + + public void writeLongSubArrayAttrib(final String attribName, final long[] array, final int startIndexInclusive, + final int endIndexExclusive) { + writeLine(attribName + " { " + formatLongSubArray(array, startIndexInclusive, endIndexExclusive) + " },"); + } + + public void writeFloatArray(final float[] floatArray) { + writeLine("{ " + formatFloatArray(floatArray) + " },"); + } + + public void writeShortArrayRaw(final short[] shortArray) { + writeLine(formatShortArray(shortArray) + ","); + } + + public void writeFloatSubArray(final float[] floatArray, final int startIndexInclusive, + final int endIndexExclusive) { + writeLine("{ " + formatFloatSubArray(floatArray, startIndexInclusive, endIndexExclusive) + " },"); + } + + public void writeVectorArray(final String token, final float[] vectors, final int vectorLength) { + startBlock(token, vectors.length / vectorLength); + + for (int i = 0, l = vectors.length; i < l; i += vectorLength) { + writeFloatSubArray(vectors, i, i + vectorLength); + } + + endBlock(); + } + + public void endBlock() { + this.ident -= 1; + writeLine("}"); + } + + public void endBlockComma() { + this.ident -= 1; + writeLine("},"); + } + + public void writeLine(final String string) { + for (int i = 0; i < this.ident; i++) { + this.buffer.append("\t"); + } + this.buffer.append(string); + this.buffer.append('\n'); + } + + public void startBlock(final String tokenFaces, final int sizeNumberProbably, final int length) { + writeLine(tokenFaces + " " + sizeNumberProbably + " " + length + " {" + ""); + this.ident += 1; + } + + public void writeColor(final String tokenStaticColor, final float[] color) { + writeLine(tokenStaticColor + " { " + color[2] + ", " + color[1] + ", " + color[0] + " },"); + } + + public void writeArrayAttrib(final String tokenAlpha, final short[] uint8Array) { + writeLine(tokenAlpha + " { " + formatShortArray(uint8Array) + " },"); + } + + public void writeArrayAttrib(final String tokenAlpha, final int[] uint16Array) { + writeLine(tokenAlpha + " { " + formatIntArray(uint16Array) + " },"); + } + + public void writeArrayAttrib(final String tokenAlpha, final long[] uint32Array) { + writeLine(tokenAlpha + " { " + formatLongArray(uint32Array) + " },"); + } + + private String formatFloat(final float value) { + final String s = Float.toString(value); + final String f = String.format("%." + this.fractionDigits + "f", value); + if (s.length() > f.length()) { + return f; + } + else { + return s; + } + } + + private String formatFloatArray(final float[] value) { + final StringBuilder stringBuilder = new StringBuilder(); + for (final float v : value) { + if (stringBuilder.length() > 0) { + stringBuilder.append(", "); + } + stringBuilder.append(formatFloat(v)); + } + return stringBuilder.toString(); + } + + private String formatLongArray(final long[] value) { + final StringBuilder stringBuilder = new StringBuilder(); + for (final long item : value) { + if (stringBuilder.length() > 0) { + stringBuilder.append(", "); + } + stringBuilder.append(item); + } + return stringBuilder.toString(); + } + + private String formatShortArray(final short[] value) { + final StringBuilder stringBuilder = new StringBuilder(); + for (final short item : value) { + if (stringBuilder.length() > 0) { + stringBuilder.append(", "); + } + stringBuilder.append(item); + } + return stringBuilder.toString(); + } + + private String formatIntArray(final int[] value) { + final StringBuilder stringBuilder = new StringBuilder(); + for (final int j : value) { + if (stringBuilder.length() > 0) { + stringBuilder.append(", "); + } + stringBuilder.append(j); + } + return stringBuilder.toString(); + } + + private String formatLongSubArray(final long[] value, final int startIndexInclusive, final int endIndexExclusive) { + final StringBuilder stringBuilder = new StringBuilder(); + for (int i = startIndexInclusive; i < endIndexExclusive; i++) { + if (stringBuilder.length() > 0) { + stringBuilder.append(", "); + } + stringBuilder.append(value[i]); + } + return stringBuilder.toString(); + } + + private String formatFloatSubArray(final float[] value, final int startIndexInclusive, + final int endIndexExclusive) { + final StringBuilder stringBuilder = new StringBuilder(); + for (int i = startIndexInclusive; i < endIndexExclusive; i++) { + if (stringBuilder.length() > 0) { + stringBuilder.append(", "); + } + stringBuilder.append(formatFloat(value[i])); + } + return stringBuilder.toString(); + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/mdl/MdlUtils.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/mdl/MdlUtils.java new file mode 100644 index 0000000..5f3f4fa --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/mdl/MdlUtils.java @@ -0,0 +1,203 @@ +package com.hiveworkshop.rms.parsers.mdlx.mdl; + +/** + * Constants for the tokens were used to prevent typos in token literals. It + * would be very easy for me to type "Interval" in one place and "Intreval" in + * another by mistake. With this paradigm, that mistake causes a compile error, + * since TOKEN_INTREVAL does not exist. + */ +public class MdlUtils { + public static final String TOKEN_VERSION = "Version"; + + public static final String TOKEN_MODEL = "Model"; + + public static final String TOKEN_SEQUENCES = "Sequences"; + + public static final String TOKEN_GLOBAL_SEQUENCES = "GlobalSequences"; + + public static final String TOKEN_INTERVAL = "Interval"; + public static final String TOKEN_NONLOOPING = "NonLooping"; + public static final String TOKEN_MOVESPEED = "MoveSpeed"; + public static final String TOKEN_RARITY = "Rarity"; + + public static final String TOKEN_FORMAT_VERSION = "FormatVersion"; + public static final String TOKEN_BLEND_TIME = "BlendTime"; + public static final String TOKEN_DURATION = "Duration"; + + public static final String TOKEN_IMAGE = "Image"; + public static final String TOKEN_WRAP_WIDTH = "WrapWidth"; + public static final String TOKEN_WRAP_HEIGHT = "WrapHeight"; + public static final String TOKEN_BITMAP = "Bitmap"; + + public static final String TOKEN_TVERTEX_ANIM = "TVertexAnim"; + + public static final String TOKEN_DONT_INTERP = "DontInterp"; + public static final String TOKEN_LINEAR = "Linear"; + public static final String TOKEN_HERMITE = "Hermite"; + public static final String TOKEN_BEZIER = "Bezier"; + public static final String TOKEN_GLOBAL_SEQ_ID = "GlobalSeqId"; + + public static final String TOKEN_PLANE = "Plane"; + public static final String TOKEN_BOX = "Box"; + public static final String TOKEN_SPHERE = "Sphere"; + public static final String TOKEN_CYLINDER = "Cylinder"; + + public static final String TOKEN_GEOSETID = "GeosetId"; + public static final String TOKEN_MULTIPLE = "Multiple"; + public static final String TOKEN_GEOSETANIMID = "GeosetAnimId"; + public static final String TOKEN_NONE = "None"; + public static final String TOKEN_OBJECTID = "ObjectId"; + public static final String TOKEN_PARENT = "Parent"; + public static final String TOKEN_BILLBOARDED_LOCK_Z = "BillboardedLockZ"; + public static final String TOKEN_BILLBOARDED_LOCK_Y = "BillboardedLockY"; + public static final String TOKEN_BILLBOARDED_LOCK_X = "BillboardedLockX"; + public static final String TOKEN_BILLBOARDED = "Billboarded"; + public static final String TOKEN_CAMERA_ANCHORED = "CameraAnchored"; + public static final String TOKEN_DONT_INHERIT = "DontInherit"; + public static final String TOKEN_ROTATION = "Rotation"; + public static final String TOKEN_TRANSLATION = "Translation"; + public static final String TOKEN_SCALING = "Scaling"; + public static final String TOKEN_STATIC = "static"; + public static final String TOKEN_ATTACHMENT_ID = "AttachmentID"; + public static final String TOKEN_PATH = "Path"; + public static final String TOKEN_VISIBILITY = "Visibility"; + public static final String TOKEN_POSITION = "Position"; + public static final String TOKEN_FIELDOFVIEW = "FieldOfView"; + public static final String TOKEN_FARCLIP = "FarClip"; + public static final String TOKEN_NEARCLIP = "NearClip"; + public static final String TOKEN_TARGET = "Target"; + public static final String TOKEN_VERTICES = "Vertices"; + public static final String TOKEN_BOUNDSRADIUS = "BoundsRadius"; + public static final String TOKEN_EVENT_TRACK = "EventTrack"; + public static final String TOKEN_MAXIMUM_EXTENT = "MaximumExtent"; + public static final String TOKEN_MINIMUM_EXTENT = "MinimumExtent"; + public static final String TOKEN_NORMALS = "Normals"; + public static final String TOKEN_TVERTICES = "TVertices"; + public static final String TOKEN_VERTEX_GROUP = "VertexGroup"; + public static final String TOKEN_FACES = "Faces"; + public static final String TOKEN_GROUPS = "Groups"; + public static final String TOKEN_ANIM = "Anim"; + public static final String TOKEN_MATERIAL_ID = "MaterialID"; + public static final String TOKEN_SELECTION_GROUP = "SelectionGroup"; + public static final String TOKEN_UNSELECTABLE = "Unselectable"; + public static final String TOKEN_TRIANGLES = "Triangles"; + public static final String TOKEN_MATRICES = "Matrices"; + public static final String TOKEN_DROP_SHADOW = "DropShadow"; + public static final String TOKEN_ALPHA = "Alpha"; + public static final String TOKEN_COLOR = "Color"; + public static final String TOKEN_STATIC_ALPHA = TOKEN_STATIC + " " + TOKEN_ALPHA; + public static final String TOKEN_STATIC_COLOR = TOKEN_STATIC + " " + TOKEN_COLOR; + public static final String TOKEN_FILTER_MODE = "FilterMode"; + public static final String TOKEN_UNSHADED = "Unshaded"; + public static final String TOKEN_SPHERE_ENV_MAP = "SphereEnvMap"; + public static final String TOKEN_TWO_SIDED = "TwoSided"; + public static final String TOKEN_UNFOGGED = "Unfogged"; + public static final String TOKEN_NO_DEPTH_TEST = "NoDepthTest"; + public static final String TOKEN_NO_DEPTH_SET = "NoDepthSet"; + public static final String TOKEN_TEXTURE_ID = "TextureID"; + public static final String TOKEN_STATIC_TEXTURE_ID = TOKEN_STATIC + " " + TOKEN_TEXTURE_ID; + public static final String TOKEN_TVERTEX_ANIM_ID = "TVertexAnimId"; + public static final String TOKEN_COORD_ID = "CoordId"; + + public static final String TOKEN_OMNIDIRECTIONAL = "Omnidirectional"; + public static final String TOKEN_DIRECTIONAL = "Directional"; + public static final String TOKEN_AMBIENT = "Ambient"; + public static final String TOKEN_ATTENUATION_START = "AttenuationStart"; + public static final String TOKEN_STATIC_ATTENUATION_START = TOKEN_STATIC + " " + TOKEN_ATTENUATION_START; + public static final String TOKEN_ATTENUATION_END = "AttenuationEnd"; + public static final String TOKEN_STATIC_ATTENUATION_END = TOKEN_STATIC + " " + TOKEN_ATTENUATION_END; + public static final String TOKEN_INTENSITY = "Intensity"; + public static final String TOKEN_STATIC_INTENSITY = TOKEN_STATIC + " " + TOKEN_INTENSITY; + public static final String TOKEN_AMB_INTENSITY = "AmbIntensity"; + public static final String TOKEN_STATIC_AMB_INTENSITY = TOKEN_STATIC + " " + TOKEN_AMB_INTENSITY; + public static final String TOKEN_AMB_COLOR = "AmbColor"; + public static final String TOKEN_STATIC_AMB_COLOR = TOKEN_STATIC + " " + TOKEN_AMB_COLOR; + + public static final String TOKEN_CONSTANT_COLOR = "ConstantColor"; + public static final String TOKEN_SORT_PRIMS_NEAR_Z = "SortPrimsNearZ"; + public static final String TOKEN_SORT_PRIMS_FAR_Z = "SortPrimsFarZ"; + public static final String TOKEN_FULL_RESOLUTION = "FullResolution"; + public static final String TOKEN_PRIORITY_PLANE = "PriorityPlane"; + + public static final String TOKEN_EMITTER_USES_MDL = "EmitterUsesMDL"; + public static final String TOKEN_EMITTER_USES_TGA = "EmitterUsesTGA"; + public static final String TOKEN_EMISSION_RATE = "EmissionRate"; + public static final String TOKEN_STATIC_EMISSION_RATE = TOKEN_STATIC + " " + TOKEN_EMISSION_RATE; + public static final String TOKEN_GRAVITY = "Gravity"; + public static final String TOKEN_STATIC_GRAVITY = TOKEN_STATIC + " " + TOKEN_GRAVITY; + public static final String TOKEN_LONGITUDE = "Longitude"; + public static final String TOKEN_STATIC_LONGITUDE = TOKEN_STATIC + " " + TOKEN_LONGITUDE; + public static final String TOKEN_LATITUDE = "Latitude"; + public static final String TOKEN_STATIC_LATITUDE = TOKEN_STATIC + " " + TOKEN_LATITUDE; + public static final String TOKEN_PARTICLE = "Particle"; + public static final String TOKEN_LIFE_SPAN = "LifeSpan"; + public static final String TOKEN_STATIC_LIFE_SPAN = TOKEN_STATIC + " " + TOKEN_LIFE_SPAN; + public static final String TOKEN_INIT_VELOCITY = "InitVelocity"; + public static final String TOKEN_STATIC_INIT_VELOCITY = TOKEN_STATIC + " " + TOKEN_INIT_VELOCITY; + + public static final String TOKEN_LINE_EMITTER = "LineEmitter"; + public static final String TOKEN_MODEL_SPACE = "ModelSpace"; + public static final String TOKEN_XY_QUAD = "XYQuad"; + public static final String TOKEN_SPEED = "Speed"; + public static final String TOKEN_STATIC_SPEED = TOKEN_STATIC + " " + TOKEN_SPEED; + public static final String TOKEN_VARIATION = "Variation"; + public static final String TOKEN_STATIC_VARIATION = TOKEN_STATIC + " " + TOKEN_VARIATION; + public static final String TOKEN_SQUIRT = "Squirt"; + public static final String TOKEN_WIDTH = "Width"; + public static final String TOKEN_STATIC_WIDTH = TOKEN_STATIC + " " + TOKEN_WIDTH; + public static final String TOKEN_LENGTH = "Length"; + public static final String TOKEN_STATIC_LENGTH = TOKEN_STATIC + " " + TOKEN_LENGTH; + public static final String TOKEN_ROWS = "Rows"; + public static final String TOKEN_COLUMNS = "Columns"; + public static final String TOKEN_HEAD = "Head"; + public static final String TOKEN_TAIL = "Tail"; + public static final String TOKEN_BOTH = "Both"; + public static final String TOKEN_TAIL_LENGTH = "TailLength"; + public static final String TOKEN_TIME = "Time"; + public static final String TOKEN_SEGMENT_COLOR = "SegmentColor"; + public static final String TOKEN_PARTICLE_SCALING = "ParticleScaling"; + public static final String TOKEN_LIFE_SPAN_UV_ANIM = "LifeSpanUVAnim"; + public static final String TOKEN_DECAY_UV_ANIM = "DecayUVAnim"; + public static final String TOKEN_TAIL_UV_ANIM = "TailUVAnim"; + public static final String TOKEN_TAIL_DECAY_UV_ANIM = "TailDecayUVAnim"; + public static final String TOKEN_REPLACEABLE_ID = "ReplaceableId"; + public static final String TOKEN_BLEND = "Blend";// ParticleEmitter2.FilterMode.BLEND.getMdlText(); + public static final String TOKEN_ADDITIVE = "Additive";// ParticleEmitter2.FilterMode.ADDITIVE.getMdlText(); + public static final String TOKEN_MODULATE = "Modulate";// ParticleEmitter2.FilterMode.MODULATE.getMdlText(); + public static final String TOKEN_MODULATE2X = "Modulate2x";// ParticleEmitter2.FilterMode.MODULATE2X.getMdlText(); + public static final String TOKEN_ALPHAKEY = "AlphaKey";// ParticleEmitter2.FilterMode.ALPHAKEY.getMdlText(); + + public static final String TOKEN_HEIGHT_ABOVE = "HeightAbove"; + public static final String TOKEN_STATIC_HEIGHT_ABOVE = TOKEN_STATIC + " " + TOKEN_HEIGHT_ABOVE; + public static final String TOKEN_HEIGHT_BELOW = "HeightBelow"; + public static final String TOKEN_STATIC_HEIGHT_BELOW = TOKEN_STATIC + " " + TOKEN_HEIGHT_BELOW; + public static final String TOKEN_TEXTURE_SLOT = "TextureSlot"; + public static final String TOKEN_STATIC_TEXTURE_SLOT = TOKEN_STATIC + " " + TOKEN_TEXTURE_SLOT; + + public static final String TOKEN_TEXTURES = "Textures"; + public static final String TOKEN_MATERIALS = "Materials"; + public static final String TOKEN_TEXTURE_ANIMS = "TextureAnims"; + public static final String TOKEN_PIVOT_POINTS = "PivotPoints"; + + public static final String TOKEN_ATTACHMENT = "Attachment"; + public static final String TOKEN_BONE = "Bone"; + public static final String TOKEN_CAMERA = "Camera"; + public static final String TOKEN_COLLISION_SHAPE = "CollisionShape"; + public static final String TOKEN_EVENT_OBJECT = "EventObject"; + public static final String TOKEN_GEOSET = "Geoset"; + public static final String TOKEN_GEOSETANIM = "GeosetAnim"; + public static final String TOKEN_HELPER = "Helper"; + public static final String TOKEN_LAYER = "Layer"; + public static final String TOKEN_LIGHT = "Light"; + public static final String TOKEN_MATERIAL = "Material"; + public static final String TOKEN_PARTICLE_EMITTER = "ParticleEmitter"; + public static final String TOKEN_PARTICLE_EMITTER2 = "ParticleEmitter2"; + public static final String TOKEN_RIBBON_EMITTER = "RibbonEmitter"; + + // > 800 + + public static final String TOKEN_EMISSIVE_GAIN = "EmissiveGain"; + public static final String TOKEN_FRESNEL_COLOR = "FresnelColor"; + public static final String TOKEN_FRESNEL_OPACITY = "FresnelOpacity"; + public static final String TOKEN_FRESNEL_TEAM_COLOR = "FresnelTeamColor"; +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/timeline/MdlxFloatArrayTimeline.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/timeline/MdlxFloatArrayTimeline.java new file mode 100644 index 0000000..d877dd4 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/timeline/MdlxFloatArrayTimeline.java @@ -0,0 +1,45 @@ +package com.hiveworkshop.rms.parsers.mdlx.timeline; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public final class MdlxFloatArrayTimeline extends MdlxTimeline { + private final int arraySize; + + public MdlxFloatArrayTimeline(final int arraySize) { + this.arraySize = arraySize; + } + + @Override + protected int size() { + return arraySize; + } + + @Override + protected float[] readMdxValue(final BinaryReader reader) { + return reader.readFloat32Array(arraySize); + } + + @Override + protected float[] readMdlValue(final MdlTokenInputStream stream) { + final float[] output = new float[arraySize]; + stream.readKeyframe(output); + return output; + } + + @Override + protected void writeMdxValue(final BinaryWriter writer, final float[] value) { + writer.writeFloat32Array(value); + } + + @Override + protected void writeMdlValue(final MdlTokenOutputStream stream, final String prefix, final float[] value) { + stream.writeKeyframe(prefix, value); + } + + public int getArraySize() { + return arraySize; + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/timeline/MdlxFloatTimeline.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/timeline/MdlxFloatTimeline.java new file mode 100644 index 0000000..ad5ac52 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/timeline/MdlxFloatTimeline.java @@ -0,0 +1,33 @@ +package com.hiveworkshop.rms.parsers.mdlx.timeline; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public final class MdlxFloatTimeline extends MdlxTimeline { + @Override + protected int size() { + return 1; + } + + @Override + protected float[] readMdxValue(final BinaryReader reader) { + return new float[] { reader.readFloat32() }; + } + + @Override + protected float[] readMdlValue(final MdlTokenInputStream stream) { + return new float[] { stream.readFloat() }; + } + + @Override + protected void writeMdxValue(final BinaryWriter writer, final float[] value) { + writer.writeFloat32(value[0]); + } + + @Override + protected void writeMdlValue(final MdlTokenOutputStream stream, final String prefix, final float[] value) { + stream.writeKeyframe(prefix, value[0]); + } +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/timeline/MdlxTimeline.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/timeline/MdlxTimeline.java new file mode 100644 index 0000000..a677791 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/timeline/MdlxTimeline.java @@ -0,0 +1,216 @@ +package com.hiveworkshop.rms.parsers.mdlx.timeline; + +import com.etheller.warsmash.util.War3ID; +import com.hiveworkshop.rms.parsers.mdlx.AnimationMap; +import com.hiveworkshop.rms.parsers.mdlx.InterpolationType; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlUtils; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public abstract class MdlxTimeline { + public War3ID name; + public InterpolationType interpolationType; + public int globalSequenceId = -1; + + public long[] frames; + public TYPE[] values; + public TYPE[] inTans; + public TYPE[] outTans; + + /** + * Restricts us to only be able to parse models on one thread at a time, in + * return for high performance. + */ + private static final StringBuffer STRING_BUFFER_HEAP = new StringBuffer(); + + public MdlxTimeline() { + + } + + public void readMdx(final BinaryReader reader, final War3ID name) { + this.name = name; + + final long keyFrameCount = reader.readUInt32(); + + this.interpolationType = InterpolationType.getType(reader.readInt32()); + this.globalSequenceId = reader.readInt32(); + + this.frames = new long[(int) keyFrameCount]; + this.values = (TYPE[]) new Object[(int) keyFrameCount]; + if (this.interpolationType.tangential()) { + this.inTans = (TYPE[]) new Object[(int) keyFrameCount]; + this.outTans = (TYPE[]) new Object[(int) keyFrameCount]; + } + + for (int i = 0; i < keyFrameCount; i++) { + this.frames[i] = reader.readInt32(); + this.values[i] = (readMdxValue(reader)); + + if (this.interpolationType.tangential()) { + this.inTans[i] = (readMdxValue(reader)); + this.outTans[i] = (readMdxValue(reader)); + } + } + } + + public void writeMdx(final BinaryWriter writer) { + writer.writeTag(this.name.getValue()); + + final int keyframeCount = this.frames.length; + + writer.writeInt32(keyframeCount); + writer.writeInt32(this.interpolationType.ordinal()); + writer.writeInt32(this.globalSequenceId); + + for (int i = 0; i < keyframeCount; i++) { + writer.writeInt32((int) this.frames[i]); + writeMdxValue(writer, this.values[i]); + + if (this.interpolationType.tangential()) { + writeMdxValue(writer, this.inTans[i]); + writeMdxValue(writer, this.outTans[i]); + } + } + } + + public void readMdl(final MdlTokenInputStream stream, final War3ID name) { + this.name = name; + + final int keyFrameCount = stream.readInt(); + + stream.read(); // { + + final String token = stream.read(); + final InterpolationType interpolationType; + switch (token) { + case MdlUtils.TOKEN_DONT_INTERP: + interpolationType = InterpolationType.DONT_INTERP; + break; + case MdlUtils.TOKEN_LINEAR: + interpolationType = InterpolationType.LINEAR; + break; + case MdlUtils.TOKEN_HERMITE: + interpolationType = InterpolationType.HERMITE; + break; + case MdlUtils.TOKEN_BEZIER: + interpolationType = InterpolationType.BEZIER; + break; + default: + interpolationType = InterpolationType.DONT_INTERP; + break; + } + ; + + this.interpolationType = interpolationType; + + if (stream.peek().equals(MdlUtils.TOKEN_GLOBAL_SEQ_ID)) { + stream.read(); + this.globalSequenceId = stream.readInt(); + } + else { + this.globalSequenceId = -1; + } + + this.frames = new long[keyFrameCount]; + this.values = (TYPE[]) new Object[keyFrameCount]; + if (this.interpolationType.tangential()) { + this.inTans = (TYPE[]) new Object[keyFrameCount]; + this.outTans = (TYPE[]) new Object[keyFrameCount]; + } + for (int i = 0; i < keyFrameCount; i++) { + this.frames[i] = (stream.readInt()); + this.values[i] = (readMdlValue(stream)); + if (interpolationType.tangential()) { + stream.read(); // InTan + this.inTans[i] = (readMdlValue(stream)); + stream.read(); // OutTan + this.outTans[i] = (readMdlValue(stream)); + } + } + + stream.read(); // } + } + + public void writeMdl(final MdlTokenOutputStream stream) { + final int tracksCount = this.frames.length; + stream.startBlock(AnimationMap.ID_TO_TAG.get(this.name).getMdlToken(), tracksCount); + + stream.writeFlag(this.interpolationType.toString()); + + if (this.globalSequenceId != -1) { + stream.writeAttrib(MdlUtils.TOKEN_GLOBAL_SEQ_ID, this.globalSequenceId); + } + + for (int i = 0; i < tracksCount; i++) { + STRING_BUFFER_HEAP.setLength(0); + STRING_BUFFER_HEAP.append(this.frames[i]); + STRING_BUFFER_HEAP.append(':'); + writeMdlValue(stream, STRING_BUFFER_HEAP.toString(), this.values[i]); + if (this.interpolationType.tangential()) { + stream.indent(); + writeMdlValue(stream, "InTan", this.inTans[i]); + writeMdlValue(stream, "OutTan", this.outTans[i]); + stream.unindent(); + } + } + + stream.endBlock(); + } + + public long getByteLength() { + final int tracksCount = this.frames.length; + int size = 16; + + if (tracksCount > 0) { + final int bytesPerValue = size() * 4; + int valuesPerTrack = 1; + if (this.interpolationType.tangential()) { + valuesPerTrack = 3; + } + + size += (4 + (valuesPerTrack * bytesPerValue)) * tracksCount; + } + return size; + } + + protected abstract int size(); + + protected abstract TYPE readMdxValue(BinaryReader reader); + + protected abstract TYPE readMdlValue(MdlTokenInputStream stream); + + protected abstract void writeMdxValue(BinaryWriter writer, TYPE value); + + protected abstract void writeMdlValue(MdlTokenOutputStream stream, String prefix, TYPE value); + + public War3ID getName() { + return this.name; + } + + public InterpolationType getInterpolationType() { + return this.interpolationType; + } + + public int getGlobalSequenceId() { + return this.globalSequenceId; + } + + public long[] getFrames() { + return this.frames; + } + + public TYPE[] getValues() { + return this.values; + } + + public TYPE[] getInTans() { + return this.inTans; + } + + public TYPE[] getOutTans() { + return this.outTans; + } + +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/timeline/MdlxUInt32Timeline.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/timeline/MdlxUInt32Timeline.java new file mode 100644 index 0000000..a80c197 --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/timeline/MdlxUInt32Timeline.java @@ -0,0 +1,34 @@ +package com.hiveworkshop.rms.parsers.mdlx.timeline; + +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenInputStream; +import com.hiveworkshop.rms.parsers.mdlx.mdl.MdlTokenOutputStream; +import com.hiveworkshop.rms.util.BinaryReader; +import com.hiveworkshop.rms.util.BinaryWriter; + +public final class MdlxUInt32Timeline extends MdlxTimeline { + @Override + protected int size() { + return 1; + } + + @Override + protected long[] readMdxValue(final BinaryReader reader) { + return new long[]{reader.readUInt32()}; + } + + @Override + protected long[] readMdlValue(final MdlTokenInputStream stream) { + return new long[]{stream.readUInt32()}; + } + + @Override + protected void writeMdxValue(final BinaryWriter writer, final long[] uint32) { + writer.writeUInt32(uint32[0]); + } + + @Override + protected void writeMdlValue(final MdlTokenOutputStream stream, final String prefix, final long[] uint32) { + stream.writeKeyframe(prefix, uint32[0]); + } + +} diff --git a/core/src/com/hiveworkshop/rms/parsers/mdlx/util/MdxUtils.java b/core/src/com/hiveworkshop/rms/parsers/mdlx/util/MdxUtils.java new file mode 100644 index 0000000..514d43e --- /dev/null +++ b/core/src/com/hiveworkshop/rms/parsers/mdlx/util/MdxUtils.java @@ -0,0 +1,32 @@ +package com.hiveworkshop.rms.parsers.mdlx.util; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +import org.apache.commons.compress.utils.IOUtils; + +import com.hiveworkshop.rms.parsers.mdlx.MdlxModel; + +public class MdxUtils { + public static MdlxModel loadMdlx(final InputStream inputStream) throws IOException { + + return new MdlxModel(ByteBuffer.wrap(IOUtils.toByteArray(inputStream))); + } + + public static void saveMdx(final MdlxModel model, final OutputStream outputStream) throws IOException { + outputStream.write(model.saveMdx().array()); + } + + public static void saveMdl(final MdlxModel model, final OutputStream outputStream) throws IOException { + outputStream.write(model.saveMdl().array()); + } + + public static void saveMdl(final MdlxModel model, final File file) throws IOException { + saveMdl(model, new FileOutputStream(file)); + } + +} diff --git a/core/src/com/hiveworkshop/rms/util/BinaryReader.java b/core/src/com/hiveworkshop/rms/util/BinaryReader.java new file mode 100644 index 0000000..eb6affd --- /dev/null +++ b/core/src/com/hiveworkshop/rms/util/BinaryReader.java @@ -0,0 +1,216 @@ +package com.hiveworkshop.rms.util; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class BinaryReader { + ByteBuffer buffer; + + public BinaryReader(final ByteBuffer buffer) { + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.position(0); + + this.buffer = buffer; + } + + public int remaining() { + return this.buffer.remaining(); + } + + public int position() { + return this.buffer.position(); + } + + public void position(final int newPosition) { + this.buffer.position(newPosition); + } + + public void move(final int offset) { + this.buffer.position(this.buffer.position() + offset); + } + + public String read(final int count) { + final StringBuilder value = new StringBuilder(); + + for (int i = 0; i < count; i++) { + final byte b = this.buffer.get(); + + if (b != 0) { + value.append((char) (b & 0xFF)); + } + } + + return value.toString(); + } + + public String readBytes(final int count) { + final StringBuilder value = new StringBuilder(); + + for (int i = 0; i < count; i++) { + value.append((char) (this.buffer.get() & 0xFF)); + } + + return value.toString(); + } + + public String readUntilNull() { + final StringBuilder value = new StringBuilder(); + byte b = this.buffer.get(); + + while (b != 0) { + value.append((char) (b & 0xFF)); + + b = this.buffer.get(); + } + + return value.toString(); + } + + public byte readInt8() { + return this.buffer.get(); + } + + public short readInt16() { + return this.buffer.getShort(); + } + + public int readInt32() { + return this.buffer.getInt(); + } + + public long readInt64() { + return this.buffer.getLong(); + } + + public short readUInt8() { + return (short) Byte.toUnsignedInt(this.buffer.get()); + } + + public int readUInt16() { + return Short.toUnsignedInt(this.buffer.getShort()); + } + + public long readUInt32() { + return Integer.toUnsignedLong(this.buffer.getInt()); + } + + public float readFloat32() { + return this.buffer.getFloat(); + } + + public double readFloat64() { + return this.buffer.getDouble(); + } + + public byte[] readInt8Array(final byte[] out) { + for (int i = 0, l = out.length; i < l; i++) { + out[i] = readInt8(); + } + + return out; + } + + public byte[] readInt8Array(final int count) { + return readInt8Array(new byte[count]); + } + + public short[] readInt16Array(final short[] out) { + for (int i = 0, l = out.length; i < l; i++) { + out[i] = readInt16(); + } + + return out; + } + + public short[] readInt16Array(final int count) { + return readInt16Array(new short[count]); + } + + public int[] readInt32Array(final int[] out) { + for (int i = 0, l = out.length; i < l; i++) { + out[i] = readInt32(); + } + + return out; + } + + public int[] readInt32Array(final int count) { + return readInt32Array(new int[count]); + } + + public long[] readInt64Array(final long[] out) { + for (int i = 0, l = out.length; i < l; i++) { + out[i] = readInt64(); + } + + return out; + } + + public long[] readInt64Array(final int count) { + return readInt64Array(new long[count]); + } + + public short[] readUInt8Array(final short[] out) { + for (int i = 0, l = out.length; i < l; i++) { + out[i] = readUInt8(); + } + + return out; + } + + public short[] readUInt8Array(final int count) { + return readUInt8Array(new short[count]); + } + + public int[] readUInt16Array(final int[] out) { + for (int i = 0, l = out.length; i < l; i++) { + out[i] = readUInt16(); + } + + return out; + } + + public int[] readUInt16Array(final int count) { + return readUInt16Array(new int[count]); + } + + public long[] readUInt32Array(final long[] out) { + for (int i = 0, l = out.length; i < l; i++) { + out[i] = readUInt32(); + } + + return out; + } + + public long[] readUInt32Array(final int count) { + return readUInt32Array(new long[count]); + } + + public float[] readFloat32Array(final float[] out) { + for (int i = 0, l = out.length; i < l; i++) { + out[i] = readFloat32(); + } + + return out; + } + + public float[] readFloat32Array(final int count) { + return readFloat32Array(new float[count]); + } + + public double[] readFloat64Array(final double[] out) { + for (int i = 0, l = out.length; i < l; i++) { + out[i] = readFloat64(); + } + + return out; + } + + public double[] readFloat64Array(final int count) { + return readFloat64Array(new double[count]); + } + + public int readTag() { + return Integer.reverseBytes(readInt32()); + } +} diff --git a/core/src/com/hiveworkshop/rms/util/BinaryWriter.java b/core/src/com/hiveworkshop/rms/util/BinaryWriter.java new file mode 100644 index 0000000..a56277c --- /dev/null +++ b/core/src/com/hiveworkshop/rms/util/BinaryWriter.java @@ -0,0 +1,140 @@ +package com.hiveworkshop.rms.util; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class BinaryWriter { + public ByteBuffer buffer; + + public BinaryWriter(final int capacity) { + this.buffer = ByteBuffer.allocate(capacity); + this.buffer.order(ByteOrder.LITTLE_ENDIAN); + } + + public int remaining() { + return this.buffer.remaining(); + } + + public int position() { + return this.buffer.position(); + } + + public void position(final int newPosition) { + this.buffer.position(newPosition); + } + + public void move(final int offset) { + this.buffer.position(this.buffer.position() + offset); + } + + public void write(final String value) { + writeInt8Array(value.getBytes()); + } + + public void writeWithNulls(final String value, final int length) { + final byte[] bytes = value.getBytes(); + final int nulls = length - bytes.length; + + writeInt8Array(bytes); + + if (nulls > 0) { + for (int i = 0; i < nulls; i++) { + writeInt8((byte) 0); + } + } + } + + public void writeInt8(final byte value) { + this.buffer.put(value); + } + + public void writeInt16(final short value) { + this.buffer.putShort(value); + } + + public void writeInt32(final int value) { + this.buffer.putInt(value); + } + + public void writeInt64(final long value) { + this.buffer.putLong(value); + } + + public void writeUInt8(final short value) { + this.buffer.put((byte) value); + } + + public void writeUInt16(final int value) { + this.buffer.putShort((short) value); + } + + public void writeUInt32(final long value) { + this.buffer.putInt((int) value); + } + + public void writeFloat32(final float value) { + this.buffer.putFloat(value); + } + + public void writeFloat64(final double value) { + this.buffer.putDouble(value); + } + + public void writeInt8Array(final byte[] values) { + for (final byte value : values) { + writeInt8(value); + } + } + + public void writeInt16Array(final short[] values) { + for (final short value : values) { + writeInt16(value); + } + } + + public void writeInt32Array(final int[] values) { + for (final int value : values) { + writeInt32(value); + } + } + + public void writeInt64Array(final long[] values) { + for (final long value : values) { + writeInt64(value); + } + } + + public void writeUInt8Array(final short[] values) { + for (final short value : values) { + writeUInt8(value); + } + } + + public void writeUInt16Array(final int[] values) { + for (final int value : values) { + writeUInt16(value); + } + } + + public void writeUInt32Array(final long[] values) { + for (final long value : values) { + writeUInt32(value); + } + } + + public void writeFloat32Array(final float[] values) { + for (final float value : values) { + writeFloat32(value); + } + } + + public void writeFloat64Array(final double[] values) { + for (final double value : values) { + writeFloat64(value); + } + } + + public void writeTag(final int tag) { + writeInt32(Integer.reverseBytes(tag)); + } +} diff --git a/core/src/com/hiveworkshop/rms/util/Descriptor.java b/core/src/com/hiveworkshop/rms/util/Descriptor.java new file mode 100644 index 0000000..654533c --- /dev/null +++ b/core/src/com/hiveworkshop/rms/util/Descriptor.java @@ -0,0 +1,6 @@ +package com.hiveworkshop.rms.util; + +public interface Descriptor { + E create(); + +} diff --git a/core/src/mpq/ArchivedFile.java b/core/src/mpq/ArchivedFile.java new file mode 100644 index 0000000..39fa3fd --- /dev/null +++ b/core/src/mpq/ArchivedFile.java @@ -0,0 +1,132 @@ +package mpq; + +import java.io.IOException; +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; + +import mpq.data.RawArrays; +import mpq.util.Cryption; + +public class ArchivedFile implements Serializable { + private static final long serialVersionUID = 5033693351138253083L; + + // CRC is Adler? + // CRC requires version safety check. + // Specification is unclear when [CRC block size > archive block size]. Assuming this never happens. May need to handle as special case. + // Specification is unclear when [(file is single unit) equals TRUE AND (file uses CRC) equals TRUE]. Assuming flag is ignored. + // Single Unit requires version safety check. + + public boolean ready; + public final int blockShift; + public final int compressedSize; + public final int fileSize; + public final int flags; + public final long fileOffset; + public final int[] blockOffsets; + public int[] blockChecksums = null; + public final int key; + public final byte compression; + + public ArchivedFile( MPQArchive archive, HashLookup search, BlockTable.Entry file ) throws MPQException{ + // *** load simple values + compressedSize = file.compressedSize; + fileSize = file.fileSize; + flags = file.flags; + fileOffset = file.filePosition + archive.getArchiveOffset(); + + // *** load complex values + if( hasFlag(BlockTable.FLAG_SINGLE_UNIT) ) blockShift = -fileSize; + else blockShift = archive.getBlockShift(); + + if( hasFlag(BlockTable.FLAG_ENCRYPTED) ){ + int key = Cryption.HashString(search.lookup, Cryption.MPQ_HASH_FILE_KEY); + if( hasFlag(BlockTable.FLAG_FIX_KEY) ){ + key = Cryption.adjustFileDecryptKey(key, file.getFilePosition(), file.getFileSize()); + } + this.key = key; + }else + key = 0; + + if( hasFlag(BlockTable.FLAG_COMPRESS | BlockTable.FLAG_IMPLODE) ){ + // blocks cannot be both compressed and imploded + if( hasFlag(BlockTable.FLAG_COMPRESS) && hasFlag(BlockTable.FLAG_IMPLODE) ) throw new MPQException("invalid block: a block is both compressed and imploded"); + + // determine the type of sector compression to use + if( hasFlag(BlockTable.FLAG_COMPRESS) ){ + if( archive.getVersion() > 1 ) compression = 3; + else compression = 2; + }else{ + compression = 1; + } + + // all compressed files use sector tables for standardization + // single unit files have a known table + if( hasFlag(BlockTable.FLAG_SINGLE_UNIT) ){ + + blockOffsets = new int[2]; + blockOffsets[1] = compressedSize; + blockChecksums = null; + ready = true; + // table will need to be looked up + }else{ + int blockn = (fileSize + (512 << blockShift) - 1) / (512 << blockShift); + + // if CRC is used, there is an additional checksum sector with checksums for all other sectors. + if( hasFlag(BlockTable.FLAG_SECTOR_CRC) ) blockn+= 1; + + blockOffsets = new int[blockn+1]; + ready = false; + } + }else{ + compression = 0; + blockOffsets = null; + ready = true; + } + } + + public void loadOffsets( SeekableByteChannel in ) throws IOException, MPQException{ + // read sector table from file + ByteBuffer temp = ByteBuffer.allocate(blockOffsets.length * 4); + in.position( fileOffset ); + while( temp.hasRemaining() ) + if( in.read(temp) == -1 ) + break; + temp.rewind(); + + // decrypt if required + if( hasFlag(BlockTable.FLAG_ENCRYPTED) ){ + Cryption.decryptData(temp, temp, key - 1); + } + + // interpret sector table + RawArrays.getArray(temp, blockOffsets); + + // validate offsets in case of corruption + if( blockOffsets[0] >= 0 && blockOffsets[0] < blockOffsets.length * 4 || + blockOffsets[0] < 0 && blockOffsets[blockOffsets.length - 1] > 0 ) throw new MPQException("block sector intersects sector offset table"); + else if( blockOffsets[0] < 0 || blockOffsets[0] > blockOffsets.length * 4 ) + System.err.printf("block at %X has detached sectors starting at %X (%d bytes from end of sector table)%n", + fileOffset, fileOffset + blockOffsets[0], blockOffsets[0] - blockOffsets.length * 4); + if( fileOffset + blockOffsets[0] < 0 || fileOffset + blockOffsets[blockOffsets.length - 1] > in.size() ) + throw new MPQException("block sector located outside channel"); + for( int i = 1, prevoff = blockOffsets[0] ; i < blockOffsets.length ; i+= 1){ + int curroff = blockOffsets[i]; + if( curroff < prevoff ) throw new MPQException("block sector with negative size"); + prevoff = curroff; + } + + // load CRC sector if present + if( hasFlag(BlockTable.FLAG_SECTOR_CRC) && blockOffsets[blockOffsets.length - 1] != blockOffsets[blockOffsets.length - 2] ){ + System.err.println("block sector CRC reading currently not supported"); + } + + ready = true; + } + + public boolean hasFlag(int flag){ + return (flags & flag) != 0; + } +} + + diff --git a/core/src/mpq/ArchivedFileExtractor.java b/core/src/mpq/ArchivedFileExtractor.java new file mode 100644 index 0000000..7703f61 --- /dev/null +++ b/core/src/mpq/ArchivedFileExtractor.java @@ -0,0 +1,66 @@ +package mpq; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; + +import mpq.compression.Compression; +import mpq.util.Cryption; + +public class ArchivedFileExtractor { + private Compression decompress = new Compression(); + + public ByteBuffer readBlock(ByteBuffer bufferold, SeekableByteChannel in, ArchivedFile file, int block) throws IOException, MPQException{ + // *** calculate the current block size + int currentSize; + if( file.fileSize < (block + 1) * bufferold.capacity() ) + currentSize = file.fileSize % bufferold.capacity(); + else + currentSize = bufferold.capacity(); + + // *** read block + if( file.blockOffsets != null ){ + // use block offset table + if( !file.ready ){ + file.loadOffsets(in); + } + bufferold.limit(file.blockOffsets[block+1] - file.blockOffsets[block]); + in.position(file.fileOffset + file.blockOffsets[block]); + }else{ + // compute offset + bufferold.limit(currentSize); + in.position(file.fileOffset + bufferold.capacity() * block); + } + while( bufferold.hasRemaining() ) + if( in.read(bufferold) == -1 ) + break; + bufferold.rewind(); + + // *** decrypt if required + if( file.key != 0 ){ + Cryption.decryptData(bufferold, bufferold, file.key + block); + } + + // *** CRC check goes here + if( file.blockChecksums != null ){ + // TODO add support for CRC + System.err.println("block sector CRC validation currently not supported"); + } + + // *** decompress if required + if( file.compression > 0 ){ + // only decompress if block is compressed + if( bufferold.limit() < currentSize ){ + // decompress block + if( file.compression >= 3 ){ + bufferold = decompress.blockDecompress3(bufferold, file.blockShift); + }else if( file.compression == 2 ){ + bufferold = decompress.blockDecompress2(bufferold, file.blockShift); + }else{ + bufferold = decompress.blockDecompress1(bufferold, file.blockShift); + } + } + } + return bufferold; + } +} diff --git a/core/src/mpq/ArchivedFileStream.java b/core/src/mpq/ArchivedFileStream.java new file mode 100644 index 0000000..3776105 --- /dev/null +++ b/core/src/mpq/ArchivedFileStream.java @@ -0,0 +1,131 @@ +package mpq; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.NonWritableChannelException; +import java.nio.channels.SeekableByteChannel; + +public class ArchivedFileStream implements SeekableByteChannel{ + private boolean open; + private SeekableByteChannel from; + private ByteBuffer buffer; + private ArchivedFile file; + private ArchivedFileExtractor extractor; + private long position; + private int currentBlock; + + public ArchivedFileStream(SeekableByteChannel in, ArchivedFileExtractor extractor, ArchivedFile file){ + from = in; + this.extractor = extractor; + this.file = file; + if( file.hasFlag(BlockTable.FLAG_SINGLE_UNIT) ){ + buffer = ByteBuffer.allocate(file.fileSize); + }else{ + buffer = ByteBuffer.allocate(512 << file.blockShift); + } + position = 0; + currentBlock = -1; + open = true; + } + + @Override + public void close() throws IOException { + from = null; + buffer = null; + open = false; + } + + @Override + public boolean isOpen() { + return open; + } + + @Override + public long position() throws IOException { + return position; + } + + @Override + public SeekableByteChannel position(long newPosition) + throws IOException { + // *** argument validation as described by SeekableByteChannel interface + if( newPosition < 0 ) throw new IllegalArgumentException("files cannot have a negative positon"); + + // update stream position + position = newPosition; + // try and update the buffer position of loaded sectors + if( currentBlock != -1 ){ + if( currentBlock != newPosition / buffer.capacity() ) + currentBlock = -1; + else + buffer.position((int) (newPosition % buffer.capacity())); + } + + // *** return value as described by SeekableByteChannel interface + return this; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + // closed + if( !open ) throw new ClosedChannelException(); + // end of stream + if( position >= file.fileSize ) return -1; + + // load current block if no block is currently loaded + if( currentBlock == -1 ){ + currentBlock = (int) (position / buffer.capacity()); + buffer.clear(); + try { + buffer = extractor.readBlock(buffer, from, file, currentBlock); + } catch (MPQException e) { + throw new IOException(e); + } + buffer.position((int) (position % buffer.capacity())); + + } + + long positionstart = position; + while( dst.hasRemaining() ){ + if( buffer.remaining() > dst.remaining() ){ + int limit = buffer.limit(); + buffer.limit(buffer.position() + dst.remaining()); + position+= buffer.remaining(); + dst.put(buffer); + buffer.limit(limit); + }else{ + position+= buffer.remaining(); + dst.put(buffer); + if(position < file.fileSize){ + currentBlock = (int) (position / buffer.capacity()); + buffer.clear(); + try { + buffer = extractor.readBlock(buffer, from, file, currentBlock); + } catch (MPQException e) { + throw new IOException(e); + } + }else{ + break; + } + } + } + + return (int) (position - positionstart); + } + + @Override + public long size() throws IOException { + return file.fileSize; + } + + @Override + public SeekableByteChannel truncate(long size) throws IOException { + throw new NonWritableChannelException(); + } + + @Override + public int write(ByteBuffer src) throws IOException { + throw new NonWritableChannelException(); + } +} diff --git a/core/src/mpq/BlockTable.java b/core/src/mpq/BlockTable.java new file mode 100644 index 0000000..6d8f64f --- /dev/null +++ b/core/src/mpq/BlockTable.java @@ -0,0 +1,78 @@ +package mpq; + +import mpq.data.BlockTableEntry; + +public class BlockTable { + public static final int FLAG_IMPLODE = 0x00000100; + public static final int FLAG_COMPRESS = 0x00000200; + public static final int FLAG_ENCRYPTED = 0x00010000; + public static final int FLAG_FIX_KEY = 0x00020000; + public static final int FLAG_PATCH_FILE = 0x00100000; + public static final int FLAG_SINGLE_UNIT = 0x01000000; + public static final int FLAG_DELETE_MARKER = 0x02000000; + public static final int FLAG_SECTOR_CRC = 0x04000000; + public static final int FLAG_EXISTS = 0x80000000; + + private Entry[] tableArray; + + // raw constructor, expects all entries to be non-null. + public BlockTable(Entry[] entries){ + tableArray = entries; + } + + public Entry lookupEntry(int entry){ + return tableArray[entry]; + } + + public static String flagsToString(int source){ + return ( (source&FLAG_IMPLODE) != 0 ? "IMPLODE " : "" )+ + ( (source&FLAG_COMPRESS) != 0 ? "COMPRESS " : "" )+ + ( (source&FLAG_ENCRYPTED) != 0 ? "ENCRYPTED " : "" )+ + ( (source&FLAG_FIX_KEY) != 0 ? "FIX_KEY " : "" )+ + ( (source&FLAG_PATCH_FILE) != 0 ? "PATCH_FILE " : "" )+ + ( (source&FLAG_SINGLE_UNIT) != 0 ? "SINGLE_UNIT " : "" )+ + ( (source&FLAG_DELETE_MARKER) != 0 ? "DELETE_MARKER " : "" )+ + ( (source&FLAG_SECTOR_CRC) != 0 ? "SECTOR_CRC " : "" )+ + ( (source&FLAG_EXISTS) != 0 ? "EXISTS " : "" ); + } + + + public static class Entry{ + public long filePosition; + public int compressedSize; + public int fileSize; + public int flags; + + public Entry(BlockTableEntry source){ + filePosition = source.getFilePosition(); + compressedSize = source.getCompressedSize(); + fileSize = source.getFileSize(); + flags = source.getFlags(); + } + + // raw constructor for data field + public Entry(){ + } + + public int getFilePosition() { + return (int) filePosition; + } + + public int getCompressedSize() { + return compressedSize; + } + + public int getFileSize() { + return fileSize; + } + + public int getFlags() { + return flags; + } + + public boolean hasFlag(int flag){ + return (flags & flag) != 0; + } + + } +} diff --git a/core/src/mpq/HashLookup.java b/core/src/mpq/HashLookup.java new file mode 100644 index 0000000..33a384c --- /dev/null +++ b/core/src/mpq/HashLookup.java @@ -0,0 +1,35 @@ +package mpq; + +import java.io.Serializable; + +import mpq.util.Cryption; + +public class HashLookup implements Serializable { + private static final long serialVersionUID = -731458056988218435L; + + public final byte[] lookup; + public final long hash; + public final int index; + + public HashLookup(String path){ + // *** convert string to 8 bit ascii + byte[] raw = Cryption.stringToHashable(path); + + // *** generate hashtable lookup arguments + hash = Cryption.HashString(raw, Cryption.MPQ_HASH_NAME_A) & 0xFFFFFFFFL | (long)Cryption.HashString(raw, Cryption.MPQ_HASH_NAME_B)<<32; + index = Cryption.HashString(raw, Cryption.MPQ_HASH_TABLE_OFFSET); + + // *** find file name + int index = 0; + for( int i = raw.length ; --i >= 0 ; ){ + if( raw[i] == (byte) '\\' || raw[i] == (byte) '/' ){ + index = i + 1; + break; + } + } + + // *** save raw ascii file name in-case file is encrypted + lookup = new byte[raw.length - index]; + System.arraycopy(raw, index, lookup, 0, lookup.length); + } +} diff --git a/core/src/mpq/HashTable.java b/core/src/mpq/HashTable.java new file mode 100644 index 0000000..aa0174f --- /dev/null +++ b/core/src/mpq/HashTable.java @@ -0,0 +1,82 @@ +package mpq; + +import mpq.data.HashTableEntry; + +public class HashTable { + public static final int BLOCK_EMPTY_ALWAYS = 0xFFFFFFFF; + public static final int BLOCK_EMPTY_NOW = 0xFFFFFFFE; + private Entry[] bucketArray; + + // raw constructor, assumes every entry is not null and the array is a power of 2 + public HashTable(Entry[] entries){ + bucketArray = entries; + } + + public int lookupBlock(HashLookup what) throws MPQException{ + int mask = bucketArray.length-1; + int index = what.index & mask; + for(int pos = index ; ; ){ + Entry temp = bucketArray[pos]; + if(temp.blockIndex == BLOCK_EMPTY_ALWAYS) break; + if(temp.getHash() == what.hash) return temp.blockIndex; + pos = ( pos + 1 ) & mask; + if(pos == index) break; + } + throw new MPQException("lookup not found"); + } + + /*public static int lookupBlock(Entry[] hashtable, byte[] file) throws FileNotFoundException{ + int mask = hashtable.length-1; + int index = Cryption.HashString(file, Cryption.MPQ_HASH_TABLE_OFFSET) & mask; + long hash = Cryption.HashString(file, Cryption.MPQ_HASH_NAME_A) & 0xFFFFFFFFL | (long)Cryption.HashString(file, Cryption.MPQ_HASH_NAME_B)<<32; + for(int pos = index ; ; ){ + Entry temp = hashtable[pos]; + if(temp.getBlockIndex() == BLOCK_EMPTY_ALWAYS) break; + if(temp.getHash() == hash) return temp.getBlockIndex(); + pos = ( pos + 1 ) & mask; + if(pos == index) break; + } + throw new FileNotFoundException("hash not in hashtable"); + }*/ + + /*public static int lookupBlock(Entry[] hashtable, String file) throws FileNotFoundException{ + return lookupBlock(hashtable, Cryption.stringToHashable(file)); + }*/ + + // entry is an internal data type and as such performs no safety checks + public static class Entry{ + public long hash; + public short locale; + public short platform; + public int blockIndex; + + // raw constructor + public Entry(){ + } + + public Entry(HashTableEntry source){ + hash = source.getHash(); + locale = source.getLocale(); + platform = source.getPlatform(); + blockIndex = source.getBlockIndex(); + } + + public long getHash() { + return hash; + } + + public short getLocale() { + return locale; + } + + public short getPlatform() { + return platform; + } + + public int getBlockIndex() { + return blockIndex; + } + + + } +} diff --git a/core/src/mpq/MPQArchive.java b/core/src/mpq/MPQArchive.java new file mode 100644 index 0000000..db05042 --- /dev/null +++ b/core/src/mpq/MPQArchive.java @@ -0,0 +1,291 @@ +package mpq; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; + +import mpq.compression.Compression; +import mpq.data.BlockTableEntry; +import mpq.data.FileHeader; +import mpq.data.HashTableEntry; +import mpq.data.ArchiveHeader; +import mpq.data.RawArrays; +import mpq.data.UserDataHeader; +import mpq.util.Cryption; + +public class MPQArchive { + private long archiveOffset; + private short blockShift; + private HashTable hashTable; + private BlockTable blockTable; + private short version; + private long archiveSize; + + // locates the archive header from within a SeekableByteChannel + private ArchiveHeader locateArchive(SeekableByteChannel in) throws IOException, MPQException{ + // *** find MPQ archive header + // allocate a buffer and header interpreter + ByteBuffer buffer = ByteBuffer.allocate(FileHeader.STRUCT_SIZE); + FileHeader header = new FileHeader(buffer); + + // look for the header positioned at 512 bytes + archiveOffset = in.position() & ~(512 - 1); + for(;;){ + // read in possible header data + in.position(archiveOffset); + while( buffer.hasRemaining() ) + if( in.read(buffer) == -1 ) + throw new MPQException("channel does not contain a MPQ archive"); + buffer.clear(); + + // check header validity + if( header.getIdentifierInt() == FileHeader.ARCHIVE_IDENTIFIER_INT ) break; + // if user data header is found, extract archive header offset and continue + else if( header.getIdentifierInt() == FileHeader.USERDATA_IDENTIFIER_INT ){ + ByteBuffer temp = ByteBuffer.allocate(header.getHeaderSize()); + UserDataHeader udheader = new UserDataHeader(temp); + + while( temp.hasRemaining() ) + if( in.read(temp) == -1 ) + break; + + archiveOffset+= udheader.getArchiveOffset(); + continue; + } + + // prepare buffer for next operation and skip to next 512 bytes + archiveOffset+= 512; + } + + // *** load MPQ archive header + // allocate a buffer and archive header interpreter + int size = header.getHeaderSize() - FileHeader.STRUCT_SIZE; + // place a reasonable 8KB limit to archive header size + if( size > 1 << 13 || size < 0 ) size = 1 << 13; + buffer = ByteBuffer.allocate(size); + ArchiveHeader archiveheader = new ArchiveHeader(buffer); + + // read in archive header bytes + while( buffer.hasRemaining() ) + if( in.read(buffer) == -1 ) + break; + + return archiveheader; + } + + // + private void deserializeHashTable(SeekableByteChannel in, ArchiveHeader archiveheader) throws IOException, MPQException{ + // *** deserialize header + // version 1 + long htoffset = (long) archiveheader.getHashTablePosition() & 0xFFFFFFFFL; + int htsize = archiveheader.getHashTableSize(); + int rsize = htsize * HashTableEntry.STRUCT_SIZE; + // version 2 + if( version >= 1 ){ + htoffset|= ((long) archiveheader.getHashTablePositionHigh() & 0xFFFFL) << 32; + } + // version 3 + int csize; + if( version >= 3 ){ + csize = (int) archiveheader.getHashTableSizeCompressed(); + }else{ + csize = rsize; + } + + // *** validate hashtable + // no hashtable to load + if( htoffset == 0 ){ + hashTable = null; + return; + // hashtable size not power of 2 + }else if( (htsize & htsize - 1) != 0 ) + throw new MPQException("hashtable was not power of two ( was " + htsize + " )"); + + // *** read MPQ archive hashtable + ByteBuffer buffer = ByteBuffer.allocate(rsize); + buffer.limit(csize); + in.position(htoffset + archiveOffset); + while( buffer.hasRemaining() ) + if( in.read(buffer) == -1 ) + break; + buffer.rewind(); + + // *** decrypt hashtable + Cryption.decryptData(buffer, buffer, Cryption.KEY_HASH_TABLE); + + // *** decompress hashtable + if( csize < rsize ){ + buffer = new Compression().blockDecompressAny(buffer, ByteBuffer.allocate(rsize)); + if( buffer.limit() != buffer.capacity() ) System.err.println("hashtable decompressed size did not match expected size"); + } + + // *** deserialize hashtable + HashTable.Entry[] entries = new HashTable.Entry[htsize]; + HashTableEntry htentry = new HashTableEntry(); + for( int i = 0 ; i < htsize ; i+= 1 ){ + htentry.move(buffer); + HashTable.Entry tempentry = new HashTable.Entry(); + tempentry.hash = htentry.getHash(); + tempentry.locale = htentry.getLocale(); + tempentry.platform = htentry.getPlatform(); + tempentry.blockIndex = htentry.getBlockIndex(); + entries[i] = tempentry; + buffer.position(buffer.position() + HashTableEntry.STRUCT_SIZE); + } + hashTable = new HashTable(entries); + } + + private void deserializeBlockTable(SeekableByteChannel in, ArchiveHeader archiveheader) throws IOException, MPQException{ + // *** deserialize header + // version 1 + long btoffset = (long) archiveheader.getBlockTablePosition() & 0xFFFFFFFFL; + int btsize = archiveheader.getBlockTableSize(); + int rsize = btsize * BlockTableEntry.STRUCT_SIZE; + // version 2 + long hbtoffset; + int rhsize; + if( version >= 1 ){ + hbtoffset = archiveheader.getHighBlockTablePosition(); + rhsize = btsize * 2; + btoffset|= ((long) archiveheader.getBlockTablePositionHigh() & 0xFFFFL) << 32; + }else{ + hbtoffset = 0; + rhsize = 0; + } + // version 4 + int csize; + int chsize; + if( version >= 3 ){ + csize = (int) archiveheader.getBlockTableSizeCompressed(); + chsize = (int) archiveheader.getHighBlockTableSizeCompressed(); + }else{ + csize = rsize; + chsize = rhsize; + } + + // *** validate blocktable + // no blocktable to load + if( btoffset == 0 ){ + blockTable = null; + return; + // blocktable size clamp + }else if( btsize > 1 << 20 || btsize < 0 ){ + System.err.println("blocktable is stupidly large ( " + btsize + " ) so was clamped to " + (1 << 20)); + btsize = 1 << 20; + } + + // *** read MPQ archive blocktable + ByteBuffer buffer = ByteBuffer.allocate(rsize); + buffer.limit(csize); + in.position(btoffset + archiveOffset); + while( buffer.hasRemaining() ) + if( in.read(buffer) == -1 ) + break; + buffer.rewind(); + + // *** decrypt blocktable + Cryption.decryptData(buffer, buffer, Cryption.KEY_BLOCK_TABLE); + + // *** decompress blocktable + if( csize < rsize ){ + buffer = new Compression().blockDecompressAny(buffer, ByteBuffer.allocate(rsize)); + if( buffer.limit() != buffer.capacity() ) System.err.println("blocktable decompressed size did not match expected size"); + } + + // *** deserialize blocktable + BlockTable.Entry[] entries = new BlockTable.Entry[btsize]; + BlockTableEntry btentry = new BlockTableEntry(); + for( int i = 0 ; i < btsize ; i+= 1 ){ + btentry.move(buffer); + BlockTable.Entry tempentry = new BlockTable.Entry(); + tempentry.filePosition = (long) btentry.getFilePosition() & 0xFFFFFFFFL; + tempentry.compressedSize = btentry.getCompressedSize(); + tempentry.fileSize = btentry.getFileSize(); + tempentry.flags = btentry.getFlags(); + entries[i] = tempentry; + buffer.position(buffer.position() + BlockTableEntry.STRUCT_SIZE); + } + + // *** add high blocktable + if( hbtoffset > 0 ){ + // read MPQ archive high blocktable + buffer = ByteBuffer.allocate(rhsize); + buffer.limit(chsize); + in.position(hbtoffset + archiveOffset); + while( buffer.hasRemaining() ) + if( in.read(buffer) == -1 ) + break; + buffer.rewind(); + + // decompress high blocktable + if( chsize < rhsize ){ + buffer = new Compression().blockDecompressAny(buffer, ByteBuffer.allocate(rhsize)); + if( buffer.limit() != buffer.capacity() ) System.err.println("high blocktable decompressed size did not match expected size"); + } + + // deserialize high blocktable + short[] highposarray = RawArrays.getShortArray(buffer); + for( int i = 0 ; i < btsize ; i+= 1 ){ + entries[i].filePosition|= ((long) highposarray[i] & 0xFFFFL) << 32; + } + } + blockTable = new BlockTable(entries); + } + + public void loadArchive(SeekableByteChannel in, boolean fold) throws IOException, MPQException{ + // *** find archive header + ArchiveHeader archiveheader = locateArchive(in); + + // *** deserialize archive globals + archiveSize = (long) archiveheader.getArchiveSize() & 0xFFFFFFFFL; + // force old allows support for Warcraft III archives with corrupted version field (not 0) + if( fold ) version = 0; + else version = archiveheader.getFormatVersion(); + blockShift = archiveheader.getBlockSize(); + if( version >= 2 ) archiveSize = archiveheader.getArchiveSizeLong(); + + // *** deserialize archive components + deserializeHashTable(in, archiveheader); + deserializeBlockTable(in, archiveheader); + if( version >= 2 ) System.err.println("het and bet tables not supported"); + } + + public MPQArchive(SeekableByteChannel in) throws MPQException, IOException{ + loadArchive(in, false); + } + + public MPQArchive(){ + } + + public long getArchiveOffset() { + return archiveOffset; + } + + public short getBlockShift() { + return blockShift; + } + + public short getVersion() { + return version; + } + + public boolean isOffsetInArchive(long offset){ + return offset >= 0 && offset <= archiveSize; + } + + public boolean isPositionInArchive(long position){ + return isOffsetInArchive(position - archiveOffset); + } + + public int lookupPath(String path) throws MPQException{ + return hashTable.lookupBlock(new HashLookup(path)); + } + + public BlockTable.Entry lookupHash(HashLookup hash) throws MPQException{ + return blockTable.lookupEntry(hashTable.lookupBlock(hash)); + } + + public ArchivedFile lookupHash2(HashLookup hash) throws MPQException{ + return new ArchivedFile(this, hash, blockTable.lookupEntry(hashTable.lookupBlock(hash))); + } +} diff --git a/core/src/mpq/MPQException.java b/core/src/mpq/MPQException.java new file mode 100644 index 0000000..c90140a --- /dev/null +++ b/core/src/mpq/MPQException.java @@ -0,0 +1,29 @@ +package mpq; + +public class MPQException extends Exception { + + /** + * + */ + private static final long serialVersionUID = 1106829951432295418L; + + public MPQException() { + } + + public MPQException(String arg0) { + super(arg0); + } + + public MPQException(Throwable arg0) { + super(arg0); + } + + public MPQException(String arg0, Throwable arg1) { + super(arg0, arg1); + } + + public MPQException(String arg0, Throwable arg1, boolean arg2, boolean arg3) { + super(arg0, arg1, arg2, arg3); + } + +} diff --git a/core/src/mpq/compression/Compression.java b/core/src/mpq/compression/Compression.java new file mode 100644 index 0000000..2caa5e3 --- /dev/null +++ b/core/src/mpq/compression/Compression.java @@ -0,0 +1,397 @@ +package mpq.compression; + +import java.nio.ByteBuffer; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +import mpq.compression.adpcm.ADPCM; +import mpq.compression.huffman.Huffman; +import mpq.compression.pkware.PKException; +import mpq.compression.pkware.PKExploder; + +public class Compression { + /*static TDecompressTable dcmp_table[] = + { + {MPQ_COMPRESSION_BZIP2, Decompress_BZIP2}, // Decompression with Bzip2 library + {MPQ_COMPRESSION_PKWARE, Decompress_PKLIB}, // Decompression with Pkware Data Compression Library + {MPQ_COMPRESSION_ZLIB, Decompress_ZLIB}, // Decompression with the "zlib" library + {MPQ_COMPRESSION_HUFFMANN, Decompress_huff}, // Huffmann decompression + {MPQ_COMPRESSION_ADPCM_STEREO, Decompress_ADPCM_stereo}, // IMA ADPCM stereo decompression + {MPQ_COMPRESSION_ADPCM_MONO, Decompress_ADPCM_mono}, // IMA ADPCM mono decompression + {MPQ_COMPRESSION_SPARSE, Decompress_SPARSE} // Sparse decompression + };*/ + + //private WritableByteBuffer adapter = new WritableByteBuffer(); + /*private OutputStream adapterFront = Channels.newOutputStream(adapter); + + private InflaterOutputStream zlibInflater = new InflaterOutputStream(adapterFront); + private WritableByteChannel zlibDecompressWriter = Channels.newChannel(zlibInflater);*/ + private PKExploder pkexploderDecompress = new PKExploder(); + private Huffman huffmanDecompress = new Huffman(); + private ADPCM adpcmDecompress = new ADPCM(2); + + // an array used to cache buffers of various regular sizes to reduce allocation overhead + private final ByteBuffer[] bufferCache = new ByteBuffer[22]; + + /* + * Compression Masks + */ + /* Masks for Compression Type 2 */ + private static final byte FLAG_HUFFMAN = 0x01; + private static final byte FLAG_DEFLATE = 0x02; + // 0x04 is unknown + private static final byte FLAG_IMPLODE = 0x08; + private static final byte FLAG_BZIP2 = 0x10; // introduced in version 1 + private static final byte FLAG_SPARSE = 0x20; // introduced in version 2 + private static final byte FLAG_ADPCM1C = 0x40; + private static final byte FLAG_ADPCM2C =-0x80; + /* Masks for Compresion Type 3 */ + private static final byte FLAG_LZMA = 0x12; + private static final byte FLAG_SPARSE_DEFLATE = FLAG_SPARSE | FLAG_DEFLATE; + private static final byte FLAG_SPARSE_BZIP2 = FLAG_SPARSE | FLAG_BZIP2 ; + + private ByteBuffer fetchBuffer(int size){ + ByteBuffer out; + if( size >= 0 ){ + out = bufferCache[size]; + if( out == null ){ + out = ByteBuffer.allocate(512 << size); + bufferCache[size] = out; + } + }else out = ByteBuffer.allocate(-size); + return out; + } + + /** + * Decompresses a sector following compression specification 3 used in version 2 and later MPQs. + * + * A lookup table is used to resolve compression. + * + * @param in buffer with compressed sector + * @param size sector size as bit shift or negative for sectors of irregular size + * @return buffer with decompressed sector + * @throws DecompressionException when decompression fails + */ + public ByteBuffer blockDecompress3(ByteBuffer in, int size) throws DecompressionException{ + byte mask = in.get(); + + ByteBuffer out = fetchBuffer(size); + boolean flip = true; + + // lookup table for valid compression types + switch( mask ) { + case FLAG_DEFLATE: + sectorInflate(in, out); + break; + case FLAG_IMPLODE: + sectorExplode(in, out); + break; + case FLAG_BZIP2: + // TODO add support + throw new DecompressionException(in, "unsupported compression type: BZIP2"); + case FLAG_SPARSE: + // TODO add support + throw new DecompressionException(in, "unsupported compression type: SPARSE"); + case FLAG_LZMA: + // TODO add support + throw new DecompressionException(in, "unsupported compression type: LZMA"); + case FLAG_SPARSE_DEFLATE: + // TODO add support + throw new DecompressionException(in, "unsupported compression type: SPARSE"); + case FLAG_SPARSE_BZIP2: + // TODO add support + throw new DecompressionException(in, "unsupported compression type: SPARSE"); + default: + throw new DecompressionException(in, "sector has unknown compression"); + } + + if( size >= 0 ) + if( flip ){ + in.clear(); + bufferCache[size] = in; + }else{ + out.clear(); + out = in; + } + else + if( !flip ) out = in; + + return out; + } + + /** + * Decompresses a sector following compression specification 2 used in version 0 and 1 MPQs. + * + * Masks are evaluated in order to undo compression. + * + * @param in buffer with compressed sector + * @param size sector size as bit shift or negative for sectors of irregular size + * @return buffer with decompressed sector + * @throws DecompressionException when decompression fails + */ + public ByteBuffer blockDecompress2(ByteBuffer in, int size) throws DecompressionException{ + byte mask = in.get(); + + ByteBuffer out = fetchBuffer(size); + boolean flip = false; + + // apply decompression flag at a time + if( (mask & FLAG_BZIP2) != 0 ){ + // TODO add support + throw new DecompressionException(in, "unsupported compression type: BZIP2"); + } + if( (mask & FLAG_IMPLODE) != 0 ){ + sectorExplode(flip ? out : in, flip ? in : out); + (flip ? out : in).clear(); + flip = !flip; + } + if( (mask & FLAG_DEFLATE) != 0 ){ + sectorInflate(flip ? out : in, flip ? in : out); + (flip ? out : in).clear(); + flip = !flip; + } + if( (mask & FLAG_HUFFMAN) != 0 ){ + sectorHuffmanExpand(flip ? out : in, flip ? in : out); + (flip ? out : in).clear(); + flip = !flip; + } + if( (mask & FLAG_ADPCM2C) != 0 ){ + sectorADPCMReconstruct(flip ? out : in, flip ? in : out, 2); + (flip ? out : in).clear(); + flip = !flip; + } + if( (mask & FLAG_ADPCM1C) != 0 ){ + sectorADPCMReconstruct(flip ? out : in, flip ? in : out, 1); + (flip ? out : in).clear(); + flip = !flip; + } + if( (mask & FLAG_SPARSE) != 0 ) System.err.println("sparse compression flag present in mpq version that lacked support"); + + if( flip ){ + bufferCache[size] = in; + return out; + }else return in; + } + + /** + * Decompresses a sector following compression specification 1 used by imploded blocks. + * + * pkware explode is always used on the input. A buffer flip always occurs. + * + * @param in buffer with compressed sector + * @param size sector size as bit shift or negative for sectors of irregular size + * @return buffer with decompressed sector + * @throws DecompressionException when decompression fails + */ + public ByteBuffer blockDecompress1(ByteBuffer in, int size) throws DecompressionException{ + ByteBuffer out = fetchBuffer(size); + sectorExplode(in, out); + in.clear(); + bufferCache[size] = in; + return out; + } + + private void sectorExplode(ByteBuffer in, ByteBuffer out) throws DecompressionException{ + try { + pkexploderDecompress.explode(in, out); + } catch (PKException e) { + throw new DecompressionException(in, "sector explode exception", e); + } + + out.flip(); + } + + private void sectorInflate(ByteBuffer in, ByteBuffer out) throws DecompressionException{ + try { + // a new inflater is needed for each sector as they cannot be recycled + Inflater zlibInflater = new Inflater(); + zlibInflater.setInput(in.array(), in.position(), in.remaining()); + out.position(zlibInflater.inflate(out.array())); + } catch ( DataFormatException e ) { + throw new DecompressionException(in, "sector deflae exception", e); + } + + out.flip(); + } + + private void sectorHuffmanExpand(ByteBuffer in, ByteBuffer out) throws DecompressionException{ + try { + huffmanDecompress.Decompress(in, out); + } catch ( Exception e ) { + throw new DecompressionException(in, "sector huffman expand exception", e); + } + + out.flip(); + } + + private void sectorADPCMReconstruct(ByteBuffer in, ByteBuffer out, int channeln) throws DecompressionException{ + try { + adpcmDecompress.decompress(in, out, channeln); + } catch ( Exception e ) { + throw new DecompressionException(in, "sector adpcm reconstruction exception", e); + } + + out.flip(); + } + + /*private static class WritableByteBuffer implements WritableByteChannel{ + public ByteBuffer dst; + + @Override + public void close() throws IOException { + // nothing to close + } + + @Override + public boolean isOpen() { + // always open + return true; + } + + @Override + public int write(ByteBuffer src) throws IOException { + int size = src.remaining(); + dst.put(src); + return size; + } + + }*/ + + public ByteBuffer blockDecompressAny(ByteBuffer block, ByteBuffer extra) throws DecompressionException{ + if( blockDecompress(block, extra) ) return extra; + else return block; + } + + public void blockExplode(ByteBuffer block, ByteBuffer extra){ + try { + pkexploderDecompress.explode(block, extra); + } catch (PKException e) { + System.err.println("pkware decompression exception: "+e.getLocalizedMessage()); + } + + if( extra.position() != extra.limit() ){ + System.err.println("a block failed exploding"); + } + + block.clear(); + extra.rewind();; + } + + public boolean blockDecompress(ByteBuffer block, ByteBuffer extra) throws DecompressionException{ + return blockDecompress(block, extra, false); + } + + public boolean blockDecompress(ByteBuffer block, ByteBuffer extra, boolean strict) throws DecompressionException{ + byte mask = block.get(); + + if( strict ){ + System.err.println("strict compression flag mode not supported"); + } + + boolean swap = false; + + // BZIP2 + if( (mask & 0x10) > 0 ){ + // TODO add support + throw new DecompressionException(block, "unsupported compression type: BZIP2"); + } + + // PKWARE + if( (mask & 0x08) > 0 ){ + try { + pkexploderDecompress.explode(block, extra); + } catch (PKException e) { + throw new DecompressionException(block, "failed PKWARE decompression", e); + } + block.rewind(); + block.limit(extra.limit()); + extra.flip(); + + ByteBuffer temp = extra; + extra = block; + block = temp; + + swap = !swap; + } + + // ZLIB + if( (mask & 0x02) > 0 ){ + try { + Inflater zlibInflater = new Inflater(); + zlibInflater.setInput(block.array(), block.position(), block.remaining()); + extra.position(zlibInflater.inflate(extra.array())); + } catch ( DataFormatException e ) { + throw new DecompressionException(block, "failed ZLIB decompression", e); + } + block.rewind(); + block.limit(extra.limit()); + extra.flip(); + + ByteBuffer temp = extra; + extra = block; + block = temp; + + swap = !swap; + } + + // HUFFMANN + if( (mask & 0x01) > 0 ){ + huffmanDecompress.Decompress(block, extra); + + block.rewind(); + block.limit(extra.limit()); + extra.flip(); + + ByteBuffer temp = extra; + extra = block; + block = temp; + + swap = !swap; + } + + // ADPCM_STEREO + if( (mask & 0x80) > 0 ){ + adpcmDecompress.decompress(block, extra, 2); + + block.rewind(); + block.limit(extra.limit()); + extra.flip(); + + ByteBuffer temp = extra; + extra = block; + block = temp; + + swap = !swap; + } + + // ADPCM_MONO + if( (mask & 0x40) > 0 ){ + adpcmDecompress.decompress(block, extra, 1); + + block.rewind(); + block.limit(extra.limit()); + extra.flip(); + + ByteBuffer temp = extra; + extra = block; + block = temp; + + swap = !swap; + } + + // SPARSE + if( (mask & 0x20) > 0 ){ + // TODO add support + throw new DecompressionException(block, "unsupported compression type: SPARSE"); + } + + if( block.limit() != extra.limit() ){ + throw new DecompressionException(block, "decompression result was smaller than expected"); + //System.err.println("a sector passed decompression but failed to meet the expected size"); + //block.limit(extra.limit()); + } + + extra.clear(); + return swap; + } +} \ No newline at end of file diff --git a/core/src/mpq/compression/DecompressionException.java b/core/src/mpq/compression/DecompressionException.java new file mode 100644 index 0000000..7a46595 --- /dev/null +++ b/core/src/mpq/compression/DecompressionException.java @@ -0,0 +1,25 @@ +package mpq.compression; + +import java.nio.ByteBuffer; + +import mpq.MPQException; + +public class DecompressionException extends MPQException { + + private static final long serialVersionUID = 5481075695238468958L; + private final ByteBuffer decompressedBuffer; + + public DecompressionException(ByteBuffer buff, String arg0) { + super(arg0); + decompressedBuffer = buff.asReadOnlyBuffer(); + } + + public DecompressionException(ByteBuffer buff, String arg0, Throwable arg1) { + super(arg0, arg1); + decompressedBuffer = buff.asReadOnlyBuffer(); + } + + public ByteBuffer getDecompressedBuffer() { + return decompressedBuffer; + } +} diff --git a/core/src/mpq/compression/adpcm/ADPCM.java b/core/src/mpq/compression/adpcm/ADPCM.java new file mode 100644 index 0000000..c39ee3e --- /dev/null +++ b/core/src/mpq/compression/adpcm/ADPCM.java @@ -0,0 +1,118 @@ +package mpq.compression.adpcm; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class ADPCM { + + private static class Channel{ + public short value; + public byte rate; + } + + private final Channel[] state; + + public ADPCM(int channelmax){ + state = new Channel[channelmax]; + for( int i = 0 ; i < state.length ; i+= 1 ) state[i] = new Channel(); + } + + public void decompress(ByteBuffer in, ByteBuffer out, int channeln){ + // prepare buffers + in.order(ByteOrder.LITTLE_ENDIAN); + out.order(ByteOrder.LITTLE_ENDIAN); + + byte stepshift = (byte) (in.getShort() >>> 8); + + // initialize channels + for( int i = 0 ; i < channeln ; i+= 1 ){ + Channel chan = state[i]; + chan.rate = 0x2C; + chan.value = in.getShort(); + out.putShort(chan.value); + } + + int current = 0; + Channel chan = state[current]; + boolean multichannel = channeln > 1; + + // decompress + while( in.hasRemaining() ){ + byte op = in.get(); + + if( (op & 0x80) > 0 ){ + switch( op & 0x7F ){ + // write current value + case 0 : + if( chan.rate != 0 ) chan.rate-= 1; + out.putShort(chan.value); + if( multichannel ) chan = state[++current % channeln]; + break; + // increment period + case 1 : + chan.rate+= 8; + if( chan.rate > 0x58 ) chan.rate = 0x58; + break; + // skip channel + case 2 : + if( multichannel ) chan = state[++current % channeln]; + break; + // all other values + default : + chan.rate-= 8; + if( chan.rate < 0 ) chan.rate = 0; + } + }else{ + // adjust value + short stepunit = STEP_TABLE[chan.rate]; + short stepsize = (short) (stepunit >>> stepshift); + int value = chan.value; + + for( int i = 0 ; i < 6 ; i+= 1 ){ + if( (op & 1 << i) > 0 ) stepsize+= stepunit >> i; + } + + if( (op & 0x40) > 0 ){ + value-= stepsize; + if( value < Short.MIN_VALUE ) value = Short.MIN_VALUE; + }else{ + value+= stepsize; + if( value > Short.MAX_VALUE ) value = Short.MAX_VALUE; + } + chan.value = (short) value; + + out.putShort(chan.value); + + chan.rate+= CHANGE_TABLE[op & 0x1F]; + if( chan.rate < 0 ) chan.rate = 0; + else if( chan.rate > 0x58 ) chan.rate = 0x58; + + if( multichannel ) chan = state[++current % channeln]; + } + } + } + + private static final byte CHANGE_TABLE[] = + { + 0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000004, 0xFFFFFFFF, 0x00000002, 0xFFFFFFFF, 0x00000006, + 0xFFFFFFFF, 0x00000001, 0xFFFFFFFF, 0x00000005, 0xFFFFFFFF, 0x00000003, 0xFFFFFFFF, 0x00000007, + 0xFFFFFFFF, 0x00000001, 0xFFFFFFFF, 0x00000005, 0xFFFFFFFF, 0x00000003, 0xFFFFFFFF, 0x00000007, + 0xFFFFFFFF, 0x00000002, 0xFFFFFFFF, 0x00000004, 0xFFFFFFFF, 0x00000006, 0xFFFFFFFF, 0x00000008 + }; + + private static final short STEP_TABLE[] = + { + 0x00000007, 0x00000008, 0x00000009, 0x0000000A, 0x0000000B, 0x0000000C, 0x0000000D, 0x0000000E, + 0x00000010, 0x00000011, 0x00000013, 0x00000015, 0x00000017, 0x00000019, 0x0000001C, 0x0000001F, + 0x00000022, 0x00000025, 0x00000029, 0x0000002D, 0x00000032, 0x00000037, 0x0000003C, 0x00000042, + 0x00000049, 0x00000050, 0x00000058, 0x00000061, 0x0000006B, 0x00000076, 0x00000082, 0x0000008F, + 0x0000009D, 0x000000AD, 0x000000BE, 0x000000D1, 0x000000E6, 0x000000FD, 0x00000117, 0x00000133, + 0x00000151, 0x00000173, 0x00000198, 0x000001C1, 0x000001EE, 0x00000220, 0x00000256, 0x00000292, + 0x000002D4, 0x0000031C, 0x0000036C, 0x000003C3, 0x00000424, 0x0000048E, 0x00000502, 0x00000583, + 0x00000610, 0x000006AB, 0x00000756, 0x00000812, 0x000008E0, 0x000009C3, 0x00000ABD, 0x00000BD0, + 0x00000CFF, 0x00000E4C, 0x00000FBA, 0x0000114C, 0x00001307, 0x000014EE, 0x00001706, 0x00001954, + 0x00001BDC, 0x00001EA5, 0x000021B6, 0x00002515, 0x000028CA, 0x00002CDF, 0x0000315B, 0x0000364B, + 0x00003BB9, 0x000041B2, 0x00004844, 0x00004F7E, 0x00005771, 0x0000602F, 0x000069CE, 0x00007462, + 0x00007FFF + }; +} diff --git a/core/src/mpq/compression/huffman/Huffman.java b/core/src/mpq/compression/huffman/Huffman.java new file mode 100644 index 0000000..1d11739 --- /dev/null +++ b/core/src/mpq/compression/huffman/Huffman.java @@ -0,0 +1,481 @@ +package mpq.compression.huffman; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Map.Entry; +import java.util.TreeMap; + +public class Huffman { + private static class Node { + public Node parent; + public final Node[] child = new Node[2]; + public Node next; + public Node prev; + public int value; + public int probability; + + public void treeSwap(Node with){ + Node temp; + + if( parent == with.parent ){ + temp = parent.child[0]; + parent.child[0] = parent.child[1]; + parent.child[1] = temp; + }else{ + if( with.parent.child[0] == with ) with.parent.child[0] = this; + else with.parent.child[1] = this; + if( this.parent.child[0] == this ) this.parent.child[0] = with; + else this.parent.child[1] = with; + } + + temp = parent; + parent = with.parent; + with.parent = temp; + } + + public void insertAfter(Node where){ + prev = where; + next = where.next; + where.next = this; + next.prev = this; + } + + public void listSwap(Node with){ + if( next == with ){ + next = with.next; + with.next = this; + with.prev = prev; + prev = with; + + with.prev.next = with; + next.prev = this; + }else if( prev == with ){ + prev = with.prev; + with.prev = this; + with.next = next; + next = with; + + with.next.prev = with; + prev.next = this; + }else{ + Node temp = prev; + prev = with.prev; + with.prev = temp; + + temp = next; + next = with.next; + with.next = temp; + + prev.next = this; + next.prev = this; + + with.prev.next = with; + with.next.prev = with; + } + } + + public void newList(){ + prev = next = this; + } + + public Node removeFromList(){ + if( this == next ) return null; + + prev.next = next; + next.prev = prev; + + return next; + } + + public void joinList(Node list){ + Node tail = prev; + + prev = list.prev; + prev.next = this; + + list.prev = tail; + tail.next = list; + } + } + + private boolean adjustProbability; + + private Node nodes = null; + private TreeMap sorted2 = new TreeMap(); + + private Node root = null; + private Node[] valueToNode = new Node[0x102]; + + private int bitBuffer; + private byte bitNumber; + private ByteBuffer source; + + private void setSource(ByteBuffer source){ + this.source = source; + bitBuffer = 0; + bitNumber = 0; + } + + private int getBits(int bits){ + while( bitNumber < bits ){ + bitBuffer|= ((int) source.get() & 0xFF) << bitNumber; + bitNumber+= 8; + } + + int result = bitBuffer & ((1 << bits) - 1); + bitBuffer>>>= bits; + bitNumber-= bits; + + return result; + } + + private Node getNode(){ + Node node; + if( nodes == null ) node = new Node(); + else{ + node = nodes; + nodes = nodes.removeFromList(); + } + return node; + } + + private void destroyTree(Node root){ + if( nodes == null ) nodes = root; + else nodes.joinList(root); + this.root = null; + sorted2.clear(); + Arrays.fill(valueToNode, null); + } + + private void insertNode( Node node ){ + Entry test2 = sorted2.ceilingEntry(node.probability); + Node current; + + + if( test2 != null ){ + current = test2.getValue(); + node.insertAfter(current); + }else{ + current = root; + if( root != null ){ + node.insertAfter(root.prev); + }else{ + node.newList(); + } + root = node; + } + + sorted2.put(node.probability, node); + } + + private Node addValueToTree( int value ){ + // create leaf node + Node node = getNode(); + node.value = value; + node.probability = 0; + node.child[0] = null; + node.child[1] = null; + + valueToNode[value] = node; + insertNode( node ); + + // create branch node + Node node2 = getNode(); + Node child1 = root.prev; + Node child2 = child1.prev; + + node2.value = -1; + node2.probability = child1.probability + child2.probability; + node2.child[0] = child1; + node2.child[1] = child2; + node2.parent = child2.parent; + + node2.insertAfter(child2.prev); + + // insert into tree + if( node2.parent.child[0] == child2 ) node2.parent.child[0] = node2; + else node2.parent.child[1] = node2; + + child1.parent = node2; + child2.parent = node2; + + return node; + } + + private void incrementProbability( Node node ){ + while( node != null ){ + // possible optimization here. Is all this really nescescary to enforce order? + if( sorted2.get(node.probability) == node ){ + if( node.probability == node.prev.probability ) + sorted2.put(node.probability, node.prev); + else + sorted2.remove(node.probability); + } + node.probability+= 1; + + Entry test2 = sorted2.ceilingEntry(node.probability); + Node where; + if( test2 != null ) where = test2.getValue().next; + else where = root; + + if( where != node ){ + node.listSwap(where); + node.treeSwap(where); + + if( where.probability != where.next.probability ){ + sorted2.put(where.probability, where); + } + } + sorted2.put(node.probability, node); + + node = node.parent; + } + } + + private void buildTree( byte tree ){ + byte[] probabilities = PROBABILITY_TABLES[tree]; + + // destroy any existing tree + if( root != null ) destroyTree(root); + + // generate leaves + for( int i = 0 ; i < 0x102 ; i++ ){ + int prob = (int) probabilities[i] & 0xFF; + + if( prob == 0 ) continue; + + Node node = getNode(); + node.value = i; + node.probability = prob; + node.child[0] = null; + node.child[1] = null; + + insertNode( node ); + valueToNode[i] = node; + } + + // generate tree + Node current = root.prev; + while( current != root ){ + Node node = getNode(); + Node child1 = current; + Node child2 = current = current.prev; + + child1.parent = node; + child2.parent = node; + + node.value = -1; + node.probability = child1.probability + child2.probability; + node.child[0] = child1; + node.child[1] = child2; + insertNode( node ); + + current = current.prev; + } + + root.parent = null; + } + + public void Decompress( ByteBuffer in, ByteBuffer out ){ + setSource(in); + byte type = (byte) getBits(8); + buildTree(type); + + adjustProbability = type == 0; + + for(;;){ + Node current = root; + while( current.value == -1 ) + current = current.child[getBits(1)]; + + if( current.value == 0x101 ){ + int value = getBits(8); + current = addValueToTree(value); + incrementProbability(current); + if( !adjustProbability ) incrementProbability(current); + }else if( current.value == 0x100 ){ + break; + } + + out.put((byte) current.value); + + if( adjustProbability ){ + incrementProbability(current); + } + } + + } + + private static final byte[][] PROBABILITY_TABLES = { + // Data for compression type 0x00 + {0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + 0x01, 0x01}, + + // Data for compression type 0x01 + {0x54, 0x16, 0x16, 0x0D, 0x0C, 0x08, 0x06, 0x05, 0x06, 0x05, 0x06, 0x03, 0x04, 0x04, 0x03, 0x05, + 0x0E, 0x0B, 0x14, 0x13, 0x13, 0x09, 0x0B, 0x06, 0x05, 0x04, 0x03, 0x02, 0x03, 0x02, 0x02, 0x02, + 0x0D, 0x07, 0x09, 0x06, 0x06, 0x04, 0x03, 0x02, 0x04, 0x03, 0x03, 0x03, 0x03, 0x03, 0x02, 0x02, + 0x09, 0x06, 0x04, 0x04, 0x04, 0x04, 0x03, 0x02, 0x03, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x04, + 0x08, 0x03, 0x04, 0x07, 0x09, 0x05, 0x03, 0x03, 0x03, 0x03, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, + 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, + 0x06, 0x0A, 0x08, 0x08, 0x06, 0x07, 0x04, 0x03, 0x04, 0x04, 0x02, 0x02, 0x04, 0x02, 0x03, 0x03, + 0x04, 0x03, 0x07, 0x07, 0x09, 0x06, 0x04, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x0A, 0x02, 0x02, 0x03, 0x02, 0x02, 0x01, 0x01, 0x02, 0x02, 0x02, 0x06, 0x03, 0x05, 0x02, 0x03, + 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x03, 0x01, 0x01, 0x01, + 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x04, 0x04, 0x04, 0x07, 0x09, 0x08, 0x0C, 0x02, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01, 0x03, + 0x04, 0x01, 0x02, 0x04, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01, + 0x04, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x01, 0x01, 0x02, 0x02, 0x02, 0x06, 0x4B, + 0x01, 0x01}, + + // Data for compression type 0x02 + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x27, 0x00, 0x00, 0x23, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + (byte) 0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x01, 0x01, 0x06, 0x0E, 0x10, 0x04, + 0x06, 0x08, 0x05, 0x04, 0x04, 0x03, 0x03, 0x02, 0x02, 0x03, 0x03, 0x01, 0x01, 0x02, 0x01, 0x01, + 0x01, 0x04, 0x02, 0x04, 0x02, 0x02, 0x02, 0x01, 0x01, 0x04, 0x01, 0x01, 0x02, 0x03, 0x03, 0x02, + 0x03, 0x01, 0x03, 0x06, 0x04, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x01, 0x01, + 0x01, 0x29, 0x07, 0x16, 0x12, 0x40, 0x0A, 0x0A, 0x11, 0x25, 0x01, 0x03, 0x17, 0x10, 0x26, 0x2A, + 0x10, 0x01, 0x23, 0x23, 0x2F, 0x10, 0x06, 0x07, 0x02, 0x09, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x01}, + + // Data for compression type 0x03 + {(byte) 0xFF, 0x0B, 0x07, 0x05, 0x0B, 0x02, 0x02, 0x02, 0x06, 0x02, 0x02, 0x01, 0x04, 0x02, 0x01, 0x03, + 0x09, 0x01, 0x01, 0x01, 0x03, 0x04, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01, + 0x05, 0x01, 0x01, 0x01, 0x0D, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x02, 0x01, 0x01, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01, 0x01, + 0x0A, 0x04, 0x02, 0x01, 0x06, 0x03, 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0x03, 0x01, 0x01, 0x01, + 0x05, 0x02, 0x03, 0x04, 0x03, 0x03, 0x03, 0x02, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x03, 0x03, + 0x01, 0x03, 0x01, 0x01, 0x02, 0x05, 0x01, 0x01, 0x04, 0x03, 0x05, 0x01, 0x03, 0x01, 0x03, 0x03, + 0x02, 0x01, 0x04, 0x03, 0x0A, 0x06, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x02, 0x02, 0x01, 0x0A, 0x02, 0x05, 0x01, 0x01, 0x02, 0x07, 0x02, 0x17, 0x01, 0x05, 0x01, 0x01, + 0x0E, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x06, 0x02, 0x01, 0x04, 0x05, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x07, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01, 0x01, + 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x11, + 0x01, 0x01}, + + // Data for compression type 0x04 + {(byte) 0xFF, (byte) 0xFB, (byte) 0x98, (byte) 0x9A, (byte) 0x84, (byte) 0x85, 0x63, 0x64, 0x3E, 0x3E, 0x22, 0x22, 0x13, 0x13, 0x18, 0x17, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x01}, + + // Data for compression type 0x05 + {(byte) 0xFF, (byte) 0xF1, (byte) 0x9D, (byte) 0x9E, (byte) 0x9A, (byte) 0x9B, (byte) 0x9A, (byte) 0x97, (byte) 0x93, (byte) 0x93, (byte) 0x8C, (byte) 0x8E, (byte) 0x86, (byte) 0x88, (byte) 0x80, (byte) 0x82, + 0x7C, 0x7C, 0x72, 0x73, 0x69, 0x6B, 0x5F, 0x60, 0x55, 0x56, 0x4A, 0x4B, 0x40, 0x41, 0x37, 0x37, + 0x2F, 0x2F, 0x27, 0x27, 0x21, 0x21, 0x1B, 0x1C, 0x17, 0x17, 0x13, 0x13, 0x10, 0x10, 0x0D, 0x0D, + 0x0B, 0x0B, 0x09, 0x09, 0x08, 0x08, 0x07, 0x07, 0x06, 0x05, 0x05, 0x04, 0x04, 0x04, 0x19, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x01}, + + // Data for compression type 0x06 + {(byte) 0xC3, (byte) 0xCB, (byte) 0xF5, 0x41, (byte) 0xFF, 0x7B, (byte) 0xF7, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + (byte) 0xBF, (byte) 0xCC, (byte) 0xF2, 0x40, (byte) 0xFD, 0x7C, (byte) 0xF7, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7A, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x01}, + + // Data for compression type 0x07 + {(byte) 0xC3, (byte) 0xD9, (byte) 0xEF, 0x3D, (byte) 0xF9, 0x7C, (byte) 0xE9, 0x1E, (byte) 0xFD, (byte) 0xAB, (byte) 0xF1, 0x2C, (byte) 0xFC, 0x5B, (byte) 0xFE, 0x17, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + (byte) 0xBD, (byte) 0xD9, (byte) 0xEC, 0x3D, (byte) 0xF5, 0x7D, (byte) 0xE8, 0x1D, (byte) 0xFB, (byte) 0xAE, (byte) 0xF0, 0x2C, (byte) 0xFB, 0x5C, (byte) 0xFF, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x01}, + + // Data for compression type 0x08 + {(byte) 0xBA, (byte) 0xC5, (byte) 0xDA, 0x33, (byte) 0xE3, 0x6D, (byte) 0xD8, 0x18, (byte) 0xE5, (byte) 0x94, (byte) 0xDA, 0x23, (byte) 0xDF, 0x4A, (byte) 0xD1, 0x10, + (byte) 0xEE, (byte) 0xAF, (byte) 0xE4, 0x2C, (byte) 0xEA, 0x5A, (byte) 0xDE, 0x15, (byte) 0xF4, (byte) 0x87, (byte) 0xE9, 0x21, (byte) 0xF6, 0x43, (byte) 0xFC, 0x12, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + (byte) 0xB0, (byte) 0xC7, (byte) 0xD8, 0x33, (byte) 0xE3, 0x6B, (byte) 0xD6, 0x18, (byte) 0xE7, (byte) 0x95, (byte) 0xD8, 0x23, (byte) 0xDB, 0x49, (byte) 0xD0, 0x11, + (byte) 0xE9, (byte) 0xB2, (byte) 0xE2, 0x2B, (byte) 0xE8, 0x5C, (byte) 0xDD, 0x15, (byte) 0xF1, (byte) 0x87, (byte) 0xE7, 0x20, (byte) 0xF7, 0x44, (byte) 0xFF, 0x13, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x5F, (byte) 0x9E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x01} + }; +} diff --git a/core/src/mpq/compression/pkware/PKException.java b/core/src/mpq/compression/pkware/PKException.java new file mode 100644 index 0000000..858ca19 --- /dev/null +++ b/core/src/mpq/compression/pkware/PKException.java @@ -0,0 +1,14 @@ +package mpq.compression.pkware; + +public class PKException extends Exception { + + /** + * + */ + private static final long serialVersionUID = 6514086311357764773L; + + public PKException(String arg0) { + super(arg0); + } + +} diff --git a/core/src/mpq/compression/pkware/PKExploder.java b/core/src/mpq/compression/pkware/PKExploder.java new file mode 100644 index 0000000..8a6db4c --- /dev/null +++ b/core/src/mpq/compression/pkware/PKExploder.java @@ -0,0 +1,200 @@ +package mpq.compression.pkware; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +public class PKExploder { + private static final byte CMP_BINARY = 0; + private static final byte CMP_ASCII = 1; + + private static final byte LEN_SIZE = 16; + private static final byte[] LEN_BITS = { + 0x03, 0x02, 0x03, 0x03, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x07, 0x07 + }; + private static final short[] LEN_CODES = { + 0x05, 0x03, 0x01, 0x06, 0x0A, 0x02, 0x0C, 0x14, 0x04, 0x18, 0x08, 0x30, 0x10, 0x20, 0x40, 0x00 + }; + private static final byte[] LENGTH_CODES = new byte[0x100]; + static { + denDecodeTabs(LENGTH_CODES, LEN_CODES, LEN_BITS, LEN_SIZE); + } + + private static final byte[] EX_LEN_BITS = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 + }; + + private static final short[] LEN_BASE ={ + 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, + 0x0008, 0x000A, 0x000E, 0x0016, 0x0026, 0x0046, 0x0086, 0x0106 + }; + + private static final byte DIST_SIZE = 64; + private static final byte[] DIST_BITS = { + 0x02, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08 + }; + private static final short[] DIST_CODES = + { + 0x03, 0x0D, 0x05, 0x19, 0x09, 0x11, 0x01, 0x3E, 0x1E, 0x2E, 0x0E, 0x36, 0x16, 0x26, 0x06, 0x3A, + 0x1A, 0x2A, 0x0A, 0x32, 0x12, 0x22, 0x42, 0x02, 0x7C, 0x3C, 0x5C, 0x1C, 0x6C, 0x2C, 0x4C, 0x0C, + 0x74, 0x34, 0x54, 0x14, 0x64, 0x24, 0x44, 0x04, 0x78, 0x38, 0x58, 0x18, 0x68, 0x28, 0x48, 0x08, + 0xF0, 0x70, 0xB0, 0x30, 0xD0, 0x50, 0x90, 0x10, 0xE0, 0x60, 0xA0, 0x20, 0xC0, 0x40, 0x80, 0x00 + }; + private static final byte[] DIST_POS_CODES = new byte[0x100]; + static { + denDecodeTabs(DIST_POS_CODES, DIST_CODES, DIST_BITS, DIST_SIZE); + } + + private static void denDecodeTabs(byte[] pos, short[] sindex, byte[] len, int size){ + for( byte i = 0 ; i < size ; i++ ){ + short length = (short) (1 << len[i]); + + for( short index = sindex[i] ; index < pos.length ; index+= length ) + { + pos[index] = i; + } + } + } + + private ByteBuffer in; + private ByteBuffer out; + private byte ctype; + private byte dsize_bits; + private short bit_buff; + private byte extra_bits; + private byte dsize_mask; + + private void WasteBits(byte nbits){ + bit_buff = (short) ((bit_buff & 0xFFFF) >>> nbits); + if( nbits <= extra_bits ){ + extra_bits-= nbits; + }else{ + bit_buff = (short) ((bit_buff | ((in.get() & 0xFF ) << (8 - nbits + extra_bits))) & 0xFFFF); + extra_bits-= nbits - 8; + } + } + + private void expand() throws PKException{ + for( ; ; ){ + if( (bit_buff & 0x01) != 0 ){ + WasteBits((byte) 1); + + // --- repeat bytes + + // get length + short length_code = (short) LENGTH_CODES[bit_buff & 0xFF]; + WasteBits((byte) LEN_BITS[length_code]); + + byte extra_length_bits; + if((extra_length_bits = EX_LEN_BITS[length_code]) != 0) + { + byte extra_length = (byte) (bit_buff & ((1 << extra_length_bits) - 1)); + + try{ + WasteBits(extra_length_bits); + }catch( BufferUnderflowException e ){ + if( (length_code + (extra_length & 0xFF)) == 0x10E ) return; + else throw e; + } + length_code = (short) (LEN_BASE[length_code] + (extra_length & 0xFF)); + + } + + length_code+= 2; + + // get distance + byte dist_pos_code = DIST_POS_CODES[bit_buff & 0xFF]; + byte dist_pos_bits = DIST_BITS[dist_pos_code]; + WasteBits(dist_pos_bits); + + short distance; + + if(length_code == 2){ + // If the repetition is only 2 bytes length, + // then take 2 bits from the stream in order to get the distance + distance = (short) ((dist_pos_code << 2) | (bit_buff & 0x03)); + WasteBits((byte) 2); + }else{ + // If the repetition is more than 2 bytes length, + // then take "dsize_bits" bits in order to get the distance + distance = (short) ((dist_pos_code << dsize_bits) | (bit_buff & dsize_mask)); + WasteBits(dsize_bits); + } + distance+= 1; + + // do the copying + int target = out.position(); + int source = target - distance; + + while( length_code > 0 ){ + if(source >= 0) + out.put(target++, out.get(source++)); + else{ + throw new PKException("distance pointing before output"); + } + length_code-= 1; + } + + out.position(target); + + }else{ + WasteBits((byte) 1); + + // --- raw byte + + switch( ctype ){ + case CMP_BINARY: + // read raw byte + byte uncompressed_byte = (byte) (bit_buff & 0xFF); + WasteBits((byte) 8); + + // write raw byte + out.put(uncompressed_byte); + break; + case CMP_ASCII: + // TODO add ASCII decompression stuff here + System.err.println("pkware ascii compression not supported"); + return; + default: throw new PKException("invalid compression mode"); + } + } + } + } + + public void explode(ByteBuffer in, ByteBuffer out) throws PKException{ + // set in and out buffers + this.in = in; + this.out = out; + if( in.remaining() <= 4 ) throw new PKException("received bad data"); + + // initialize state with compression header + ctype = in.get(); + dsize_bits = in.get(); + bit_buff = (short) (in.get() & 0xFF); + extra_bits = 0; + + // dictionary size mask + if( dsize_bits < 4 || 6 < dsize_bits ) throw new PKException("invalid dictionary size"); + dsize_mask = (byte) (0xFFFF >> (0x10 - dsize_bits)); + + // setup compression type dependent data + switch( ctype ){ + case CMP_BINARY: break; + case CMP_ASCII: + // TODO add ASCII decompression stuff here + System.err.println("pkware ascii compression not supported"); + return; + default: throw new PKException("invalid compression mode"); + } + + // perform explode + try{ + expand(); + }catch( BufferUnderflowException e ){ + throw new PKException("unexpected end of data"); + } + + } +} diff --git a/core/src/mpq/data/ArchiveHeader.java b/core/src/mpq/data/ArchiveHeader.java new file mode 100644 index 0000000..2da8caf --- /dev/null +++ b/core/src/mpq/data/ArchiveHeader.java @@ -0,0 +1,98 @@ +package mpq.data; + +import java.nio.ByteBuffer; + +public class ArchiveHeader extends Raw{ + public static final int STRUCT_SIZE_V1 = 32 - 8; + public static final int STRUCT_SIZE_V2 = STRUCT_SIZE_V1 + 12; + public static final int STRUCT_SIZE_V3 = STRUCT_SIZE_V2 + 24; + public static final int STRUCT_SIZE_V4 = STRUCT_SIZE_V3 + 44; + + public ArchiveHeader(ByteBuffer source) { + super(source); + } + + // VERSION 1 + + public int getArchiveSize() { + return data.getInt(0); + } + + public short getFormatVersion() { + return data.getShort(4); + } + + public short getBlockSize() { + return data.getShort(6); + } + + public int getHashTablePosition() { + return data.getInt(8); + } + + public int getBlockTablePosition() { + return data.getInt(12); + } + + public int getHashTableSize() { + return data.getInt(16); + } + + public int getBlockTableSize() { + return data.getInt(20); + } + + // VERSION 2 + + public long getHighBlockTablePosition() { + return data.getLong(24); + } + + public short getHashTablePositionHigh() { + return data.getShort(32); + } + + public short getBlockTablePositionHigh() { + return data.getShort(34); + } + + // VERSION 3 + + public long getArchiveSizeLong() { + return data.getLong(36); + } + + public long getBetTablePosition() { + return data.getLong(44); + } + + public long getHetTablePosition() { + return data.getLong(52); + } + + // VERSION 4 + + public long getHashTableSizeCompressed() { + return data.getLong(60); + } + + public long getBlockTableSizeCompressed() { + return data.getLong(68); + } + + public long getHighBlockTableSizeCompressed() { + return data.getLong(76); + } + + public long getHetTableSizeCompressed() { + return data.getLong(84); + } + + public long getBetTableSizeCompressed() { + return data.getLong(92); + } + + public int getRawChunkSize() { + return data.getInt(100); + } +} diff --git a/core/src/mpq/data/BlockTableEntry.java b/core/src/mpq/data/BlockTableEntry.java new file mode 100644 index 0000000..bd262d5 --- /dev/null +++ b/core/src/mpq/data/BlockTableEntry.java @@ -0,0 +1,31 @@ +package mpq.data; + +import java.nio.ByteBuffer; + +public class BlockTableEntry extends Raw { + public static final int STRUCT_SIZE = 16; + + public BlockTableEntry(ByteBuffer source) { + super(source); + } + + public BlockTableEntry() { + super(); + } + + public int getFilePosition() { + return data.getInt(0); + } + + public int getCompressedSize() { + return data.getInt(4); + } + + public int getFileSize() { + return data.getInt(8); + } + + public int getFlags() { + return data.getInt(12); + } +} diff --git a/core/src/mpq/data/FileHeader.java b/core/src/mpq/data/FileHeader.java new file mode 100644 index 0000000..ca1b4c1 --- /dev/null +++ b/core/src/mpq/data/FileHeader.java @@ -0,0 +1,31 @@ +package mpq.data; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class FileHeader extends Raw{ + public static final byte[] ARCHIVE_IDENTIFIER_BYTES = {'M','P','Q',0x1a}; + public static final int ARCHIVE_IDENTIFIER_INT = ByteBuffer.wrap(ARCHIVE_IDENTIFIER_BYTES).order(ByteOrder.LITTLE_ENDIAN).getInt(0); + public static final byte[] USERDATA_IDENTIFIER_BYTES = {'M','P','Q',0x1b}; + public static final int USERDATA_IDENTIFIER_INT = ByteBuffer.wrap(USERDATA_IDENTIFIER_BYTES).order(ByteOrder.LITTLE_ENDIAN).getInt(0); + public static final int STRUCT_SIZE = 8; + + public FileHeader(ByteBuffer source) { + super(source); + } + + public byte[] getIdentifierBytes() { + byte[] bytes = new byte[4]; + data.position(0); + data.get(bytes); + return bytes; + } + + public int getIdentifierInt() { + return data.getInt(0); + } + + public int getHeaderSize() { + return data.getInt(4); + } +} diff --git a/core/src/mpq/data/HashTableEntry.java b/core/src/mpq/data/HashTableEntry.java new file mode 100644 index 0000000..1e970f8 --- /dev/null +++ b/core/src/mpq/data/HashTableEntry.java @@ -0,0 +1,31 @@ +package mpq.data; + +import java.nio.ByteBuffer; + +public class HashTableEntry extends Raw { + public static final int STRUCT_SIZE = 16; + + public HashTableEntry(ByteBuffer source) { + super(source); + } + + public HashTableEntry() { + super(); + } + + public long getHash() { + return data.getLong(0); + } + + public short getLocale() { + return data.getShort(8); + } + + public short getPlatform() { + return data.getShort(10); + } + + public int getBlockIndex() { + return data.getInt(12); + } +} diff --git a/core/src/mpq/data/Raw.java b/core/src/mpq/data/Raw.java new file mode 100644 index 0000000..80fd27f --- /dev/null +++ b/core/src/mpq/data/Raw.java @@ -0,0 +1,20 @@ +package mpq.data; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public abstract class Raw { + protected ByteBuffer data; + + public Raw(ByteBuffer source){ + move(source); + } + + public Raw(){ + data = null; + } + + public void move(ByteBuffer source){ + data = source.slice().order(ByteOrder.LITTLE_ENDIAN); + } +} diff --git a/core/src/mpq/data/RawArrays.java b/core/src/mpq/data/RawArrays.java new file mode 100644 index 0000000..4efe1e1 --- /dev/null +++ b/core/src/mpq/data/RawArrays.java @@ -0,0 +1,36 @@ +package mpq.data; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; + +public class RawArrays { + public static int[] getArray(ByteBuffer source){ + source.order(ByteOrder.LITTLE_ENDIAN); + IntBuffer in = source.asIntBuffer(); + + IntBuffer out = IntBuffer.allocate(in.capacity()); + out.put(in); + + return out.array(); + } + + public static void getArray(ByteBuffer source, int[] destination){ + source.order(ByteOrder.LITTLE_ENDIAN); + IntBuffer in = source.asIntBuffer(); + + IntBuffer out = IntBuffer.wrap(destination); + out.put(in); + } + + public static short[] getShortArray(ByteBuffer source){ + source.order(ByteOrder.LITTLE_ENDIAN); + ShortBuffer in = source.asShortBuffer(); + + ShortBuffer out = ShortBuffer.allocate(in.capacity()); + out.put(in); + + return out.array(); + } +} diff --git a/core/src/mpq/data/UserDataHeader.java b/core/src/mpq/data/UserDataHeader.java new file mode 100644 index 0000000..978186a --- /dev/null +++ b/core/src/mpq/data/UserDataHeader.java @@ -0,0 +1,21 @@ +package mpq.data; + +import java.nio.ByteBuffer; + +public class UserDataHeader extends Raw{ + public static final int STRUCT_SIZE = 16 - 8; + + public UserDataHeader(ByteBuffer source) { + super(source); + } + + + public int getArchiveOffset() { + return data.getInt(0); + } + + public int getUserDataSize() { + return data.getInt(4); + } + +} diff --git a/core/src/mpq/util/Cryption.java b/core/src/mpq/util/Cryption.java new file mode 100644 index 0000000..073c226 --- /dev/null +++ b/core/src/mpq/util/Cryption.java @@ -0,0 +1,116 @@ +package mpq.util; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +public class Cryption { + private static final int[] CRYPT_TABLE = new int[0x500]; + static { + int seed = 0x00100001; + + for (int index1 = 0; index1 < 0x100; index1++) { + for (int index2 = index1, i = 0; i < 5; i++, index2 += 0x100) { + seed = ((seed * 125) + 3) % 0x2AAAAB; + final int temp1 = (seed & 0xFFFF) << 0x10; + + seed = ((seed * 125) + 3) % 0x2AAAAB; + final int temp2 = (seed & 0xFFFF); + + CRYPT_TABLE[index2] = (temp1 | temp2); + } + } + } + + // different types of hashes to make with HashString + public static final int MPQ_HASH_TABLE_OFFSET = 0; + public static final int MPQ_HASH_NAME_A = 1; + public static final int MPQ_HASH_NAME_B = 2; + public static final int MPQ_HASH_FILE_KEY = 3; + + // cached hashes + public static final int KEY_HASH_TABLE = HashString("(hash table)", MPQ_HASH_FILE_KEY); + public static final int KEY_BLOCK_TABLE = HashString("(block table)", MPQ_HASH_FILE_KEY); + + public static void cryptData(ByteBuffer in, ByteBuffer out, int length, int key, final boolean de) { + // prepare platform independent views + in = in.asReadOnlyBuffer().order(ByteOrder.LITTLE_ENDIAN); + out = out.duplicate().order(ByteOrder.LITTLE_ENDIAN); + // cryption + int seed = 0xEEEEEEEE; + length /= 4; + while (length-- > 0) { + seed += CRYPT_TABLE[0x400 + (key & 0xFF)]; + // basic algorithm + final int read = in.getInt(); + final int ch = read ^ (key + seed); + out.putInt(ch); + // generation for next iteration + seed += (de ? ch : read) + (seed << 5) + 3; + key = ((~key << 21) + 0x11111111) | (key >>> 11); + } + out.rewind(); + } + + public static void encryptData(final ByteBuffer in, final ByteBuffer out, final int length, final int key) { + cryptData(in, out, length, key, false); + } + + public static void decryptData(final ByteBuffer in, final ByteBuffer out, final int length, final int key) { + cryptData(in, out, length, key, true); + } + + public static void cryptData(ByteBuffer in, ByteBuffer out, int key, final boolean de) { + // prepare platform independent views + in = in.asReadOnlyBuffer().order(ByteOrder.LITTLE_ENDIAN); + out = out.duplicate().order(ByteOrder.LITTLE_ENDIAN); + + // round to last dword to prevent buffer underflow + in.limit(in.limit() & ~0x03); + + // cryption + int seed = 0xEEEEEEEE; + while (in.hasRemaining()) { + seed += CRYPT_TABLE[0x400 + (key & 0xFF)]; + // basic algorithm + final int read = in.getInt(); + final int ch = read ^ (key + seed); + out.putInt(ch); + // generation for next iteration + seed += (de ? ch : read) + (seed << 5) + 3; + key = ((~key << 21) + 0x11111111) | (key >>> 11); + } + } + + public static void encryptData(final ByteBuffer in, final ByteBuffer out, final int key) { + cryptData(in, out, key, false); + } + + public static void decryptData(final ByteBuffer in, final ByteBuffer out, final int key) { + cryptData(in, out, key, true); + } + + public static byte[] stringToHashable(final String in) { + return in.toUpperCase(Locale.US).getBytes(StandardCharsets.UTF_8); // UTF_8 defined for platform independence + } + + public static int HashString(final String in, final int HashType) { + return HashString(stringToHashable(in), HashType); + } + + // Based on code from StormLib. + public static int HashString(final byte[] in, final int HashType) { + int seed1 = 0x7FED7FED; + int seed2 = 0xEEEEEEEE; + for (final byte ch : in) { + seed1 = CRYPT_TABLE[(HashType * 0x100) + ch] ^ (seed1 + seed2); + seed2 = ch + seed1 + seed2 + (seed2 << 5) + 3; + } + return seed1; + } + + public static int adjustFileDecryptKey(final int in, final int pos, final int size) { + return (in + pos) ^ size; + } +} diff --git a/desktop/build.gradle b/desktop/build.gradle new file mode 100644 index 0000000..509f1ad --- /dev/null +++ b/desktop/build.gradle @@ -0,0 +1,55 @@ +apply plugin: "java" + +sourceCompatibility = 1.7 +sourceSets.main.java.srcDirs = [ "src/" ] + +project.ext.mainClassName = "com.etheller.warsmash.desktop.DesktopLauncher" +project.ext.assetsDir = new File("../core/assets"); + +task run(dependsOn: classes, type: JavaExec) { + main = project.mainClassName + classpath = sourceSets.main.runtimeClasspath + standardInput = System.in + workingDir = project.assetsDir + ignoreExitValue = true +} + +task debug(dependsOn: classes, type: JavaExec) { + main = project.mainClassName + classpath = sourceSets.main.runtimeClasspath + standardInput = System.in + workingDir = project.assetsDir + ignoreExitValue = true + debug = true +} + +task dist(type: Jar) { + from files(sourceSets.main.output.classesDir) + from files(sourceSets.main.output.resourcesDir) + from {configurations.compile.collect {zipTree(it)}} + from files(project.assetsDir); + + manifest { + attributes 'Main-Class': project.mainClassName + } +} + +dist.dependsOn classes + +eclipse { + project { + name = appName + "-desktop" + linkedResource name: 'assets', type: '2', location: 'PARENT-1-PROJECT_LOC/core/assets' + } +} + +task afterEclipseImport(description: "Post processing after project generation", group: "IDE") { + doLast { + def classpath = new XmlParser().parse(file(".classpath")) + new Node(classpath, "classpathentry", [ kind: 'src', path: 'assets' ]); + def writer = new FileWriter(file(".classpath")) + def printer = new XmlNodePrinter(new PrintWriter(writer)) + printer.setPreserveWhitespace(true) + printer.print(classpath) + } +} diff --git a/desktop/src/com/badlogic/gdx/backends/lwjgl/LwjglApplication.java b/desktop/src/com/badlogic/gdx/backends/lwjgl/LwjglApplication.java new file mode 100644 index 0000000..2123195 --- /dev/null +++ b/desktop/src/com/badlogic/gdx/backends/lwjgl/LwjglApplication.java @@ -0,0 +1,487 @@ +/******************************************************************************* + * Copyright 2011 See AUTHORS file. + * + * 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 + * + * http://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. + ******************************************************************************/ + +package com.badlogic.gdx.backends.lwjgl; + +import java.awt.Canvas; +import java.io.File; + +import org.lwjgl.LWJGLException; +import org.lwjgl.opengl.Display; + +import com.badlogic.gdx.Application; +import com.badlogic.gdx.ApplicationListener; +import com.badlogic.gdx.ApplicationLogger; +import com.badlogic.gdx.Audio; +import com.badlogic.gdx.Files; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Input; +import com.badlogic.gdx.LifecycleListener; +import com.badlogic.gdx.Net; +import com.badlogic.gdx.Preferences; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.Clipboard; +import com.badlogic.gdx.utils.GdxRuntimeException; +import com.badlogic.gdx.utils.ObjectMap; +import com.badlogic.gdx.utils.SnapshotArray; +import com.etheller.warsmash.audio.OpenALAudio; + +/** + * An OpenGL surface fullscreen or in a lightweight window. This was modified by + * Retera in accordance with the permission to do so from the Apache 2.0 license + * listed at the top of the file. The ONLY reason for this modified file + * override currently is to use a replacement for the OpenALAudio class that + * will now support the 3D sound! + */ +public class LwjglApplication implements Application { + protected final LwjglGraphics graphics; + protected OpenALAudio audio; + protected final LwjglFiles files; + protected final LwjglInput input; + protected final LwjglNet net; + protected final ApplicationListener listener; + protected Thread mainLoopThread; + protected boolean running = true; + protected final Array runnables = new Array(); + protected final Array executedRunnables = new Array(); + protected final SnapshotArray lifecycleListeners = new SnapshotArray( + LifecycleListener.class); + protected int logLevel = LOG_INFO; + protected ApplicationLogger applicationLogger; + protected String preferencesdir; + protected Files.FileType preferencesFileType; + + public LwjglApplication(final ApplicationListener listener, final String title, final int width, final int height) { + this(listener, createConfig(title, width, height)); + } + + public LwjglApplication(final ApplicationListener listener) { + this(listener, null, 640, 480); + } + + public LwjglApplication(final ApplicationListener listener, final LwjglApplicationConfiguration config) { + this(listener, config, new LwjglGraphics(config)); + } + + public LwjglApplication(final ApplicationListener listener, final Canvas canvas) { + this(listener, new LwjglApplicationConfiguration(), new LwjglGraphics(canvas)); + } + + public LwjglApplication(final ApplicationListener listener, final LwjglApplicationConfiguration config, + final Canvas canvas) { + this(listener, config, new LwjglGraphics(canvas, config)); + } + + public LwjglApplication(final ApplicationListener listener, final LwjglApplicationConfiguration config, + final LwjglGraphics graphics) { + LwjglNativesLoader.load(); + setApplicationLogger(new LwjglApplicationLogger()); + + if (config.title == null) { + config.title = listener.getClass().getSimpleName(); + } + this.graphics = graphics; + if (!LwjglApplicationConfiguration.disableAudio) { + try { + this.audio = new OpenALAudio(config.audioDeviceSimultaneousSources, config.audioDeviceBufferCount, + config.audioDeviceBufferSize); + } + catch (final Throwable t) { + log("LwjglApplication", "Couldn't initialize audio, disabling audio", t); + LwjglApplicationConfiguration.disableAudio = true; + } + } + this.files = new LwjglFiles(); + this.input = new LwjglInput(); + this.net = new LwjglNet(); + this.listener = listener; + this.preferencesdir = config.preferencesDirectory; + this.preferencesFileType = config.preferencesFileType; + + Gdx.app = this; + Gdx.graphics = graphics; + Gdx.audio = this.audio; + Gdx.files = this.files; + Gdx.input = this.input; + Gdx.net = this.net; + initialize(); + } + + private static LwjglApplicationConfiguration createConfig(final String title, final int width, final int height) { + final LwjglApplicationConfiguration config = new LwjglApplicationConfiguration(); + config.title = title; + config.width = width; + config.height = height; + config.vSyncEnabled = true; + return config; + } + + private void initialize() { + this.mainLoopThread = new Thread("LWJGL Application") { + @Override + public void run() { + LwjglApplication.this.graphics.setVSync(LwjglApplication.this.graphics.config.vSyncEnabled); + try { + LwjglApplication.this.mainLoop(); + } + catch (final Throwable t) { + if (LwjglApplication.this.audio != null) { + LwjglApplication.this.audio.dispose(); + } + Gdx.input.setCursorCatched(false); + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } + else { + throw new GdxRuntimeException(t); + } + } + } + }; + this.mainLoopThread.start(); + } + + void mainLoop() { + final SnapshotArray lifecycleListeners = this.lifecycleListeners; + + try { + this.graphics.setupDisplay(); + } + catch (final LWJGLException e) { + throw new GdxRuntimeException(e); + } + + this.listener.create(); + this.graphics.resize = true; + + int lastWidth = this.graphics.getWidth(); + int lastHeight = this.graphics.getHeight(); + + this.graphics.lastTime = System.nanoTime(); + boolean wasActive = true; + while (this.running) { + Display.processMessages(); + if (Display.isCloseRequested()) { + exit(); + } + + final boolean isActive = Display.isActive(); + if (wasActive && !isActive) { // if it's just recently minimized from active state + wasActive = false; + synchronized (lifecycleListeners) { + final LifecycleListener[] listeners = lifecycleListeners.begin(); + for (int i = 0, n = lifecycleListeners.size; i < n; ++i) { + listeners[i].pause(); + } + lifecycleListeners.end(); + } + this.listener.pause(); + } + if (!wasActive && isActive) { // if it's just recently focused from minimized state + wasActive = true; + synchronized (lifecycleListeners) { + final LifecycleListener[] listeners = lifecycleListeners.begin(); + for (int i = 0, n = lifecycleListeners.size; i < n; ++i) { + listeners[i].resume(); + } + lifecycleListeners.end(); + } + this.listener.resume(); + } + + boolean shouldRender = false; + + if (this.graphics.canvas != null) { + final int width = this.graphics.canvas.getWidth(); + final int height = this.graphics.canvas.getHeight(); + if ((lastWidth != width) || (lastHeight != height)) { + lastWidth = width; + lastHeight = height; + Gdx.gl.glViewport(0, 0, lastWidth, lastHeight); + this.listener.resize(lastWidth, lastHeight); + shouldRender = true; + } + } + else { + this.graphics.config.x = Display.getX(); + this.graphics.config.y = Display.getY(); + if (this.graphics.resize || Display.wasResized() + || ((int) (Display.getWidth() * Display.getPixelScaleFactor()) != this.graphics.config.width) + || ((int) (Display.getHeight() + * Display.getPixelScaleFactor()) != this.graphics.config.height)) { + this.graphics.resize = false; + this.graphics.config.width = (int) (Display.getWidth() * Display.getPixelScaleFactor()); + this.graphics.config.height = (int) (Display.getHeight() * Display.getPixelScaleFactor()); + Gdx.gl.glViewport(0, 0, this.graphics.config.width, this.graphics.config.height); + if (this.listener != null) { + this.listener.resize(this.graphics.config.width, this.graphics.config.height); + } + this.graphics.requestRendering(); + } + } + + if (executeRunnables()) { + shouldRender = true; + } + + // If one of the runnables set running to false, for example after an exit(). + if (!this.running) { + break; + } + + this.input.update(); + shouldRender |= this.graphics.shouldRender(); + this.input.processEvents(); + if (this.audio != null) { + this.audio.update(); + } + + if (!isActive && (this.graphics.config.backgroundFPS == -1)) { + shouldRender = false; + } + int frameRate = isActive ? this.graphics.config.foregroundFPS : this.graphics.config.backgroundFPS; + if (shouldRender) { + this.graphics.updateTime(); + this.graphics.frameId++; + this.listener.render(); + Display.update(false); + } + else { + // Sleeps to avoid wasting CPU in an empty loop. + if (frameRate == -1) { + frameRate = 10; + } + if (frameRate == 0) { + frameRate = this.graphics.config.backgroundFPS; + } + if (frameRate == 0) { + frameRate = 30; + } + } + if (frameRate > 0) { + Display.sync(frameRate); + } + } + + synchronized (lifecycleListeners) { + final LifecycleListener[] listeners = lifecycleListeners.begin(); + for (int i = 0, n = lifecycleListeners.size; i < n; ++i) { + listeners[i].pause(); + listeners[i].dispose(); + } + lifecycleListeners.end(); + } + this.listener.pause(); + this.listener.dispose(); + Display.destroy(); + if (this.audio != null) { + this.audio.dispose(); + } + if (this.graphics.config.forceExit) { + System.exit(-1); + } + } + + public boolean executeRunnables() { + synchronized (this.runnables) { + for (int i = this.runnables.size - 1; i >= 0; i--) { + this.executedRunnables.add(this.runnables.get(i)); + } + this.runnables.clear(); + } + if (this.executedRunnables.size == 0) { + return false; + } + do { + this.executedRunnables.pop().run(); + } + while (this.executedRunnables.size > 0); + return true; + } + + @Override + public ApplicationListener getApplicationListener() { + return this.listener; + } + + @Override + public Audio getAudio() { + return this.audio; + } + + @Override + public Files getFiles() { + return this.files; + } + + @Override + public LwjglGraphics getGraphics() { + return this.graphics; + } + + @Override + public Input getInput() { + return this.input; + } + + @Override + public Net getNet() { + return this.net; + } + + @Override + public ApplicationType getType() { + return ApplicationType.Desktop; + } + + @Override + public int getVersion() { + return 0; + } + + public void stop() { + this.running = false; + try { + this.mainLoopThread.join(); + } + catch (final Exception ex) { + } + } + + @Override + public long getJavaHeap() { + return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + } + + @Override + public long getNativeHeap() { + return getJavaHeap(); + } + + ObjectMap preferences = new ObjectMap(); + + @Override + public Preferences getPreferences(final String name) { + if (this.preferences.containsKey(name)) { + return this.preferences.get(name); + } + else { + final Preferences prefs = new LwjglPreferences( + new LwjglFileHandle(new File(this.preferencesdir, name), this.preferencesFileType)); + this.preferences.put(name, prefs); + return prefs; + } + } + + @Override + public Clipboard getClipboard() { + return new LwjglClipboard(); + } + + @Override + public void postRunnable(final Runnable runnable) { + synchronized (this.runnables) { + this.runnables.add(runnable); + Gdx.graphics.requestRendering(); + } + } + + @Override + public void debug(final String tag, final String message) { + if (this.logLevel >= LOG_DEBUG) { + getApplicationLogger().debug(tag, message); + } + } + + @Override + public void debug(final String tag, final String message, final Throwable exception) { + if (this.logLevel >= LOG_DEBUG) { + getApplicationLogger().debug(tag, message, exception); + } + } + + @Override + public void log(final String tag, final String message) { + if (this.logLevel >= LOG_INFO) { + getApplicationLogger().log(tag, message); + } + } + + @Override + public void log(final String tag, final String message, final Throwable exception) { + if (this.logLevel >= LOG_INFO) { + getApplicationLogger().log(tag, message, exception); + } + } + + @Override + public void error(final String tag, final String message) { + if (this.logLevel >= LOG_ERROR) { + getApplicationLogger().error(tag, message); + } + } + + @Override + public void error(final String tag, final String message, final Throwable exception) { + if (this.logLevel >= LOG_ERROR) { + getApplicationLogger().error(tag, message, exception); + } + } + + @Override + public void setLogLevel(final int logLevel) { + this.logLevel = logLevel; + } + + @Override + public int getLogLevel() { + return this.logLevel; + } + + @Override + public void setApplicationLogger(final ApplicationLogger applicationLogger) { + this.applicationLogger = applicationLogger; + } + + @Override + public ApplicationLogger getApplicationLogger() { + return this.applicationLogger; + } + + @Override + public void exit() { + postRunnable(new Runnable() { + @Override + public void run() { + LwjglApplication.this.running = false; + } + }); + } + + @Override + public void addLifecycleListener(final LifecycleListener listener) { + synchronized (this.lifecycleListeners) { + this.lifecycleListeners.add(listener); + } + } + + @Override + public void removeLifecycleListener(final LifecycleListener listener) { + synchronized (this.lifecycleListeners) { + this.lifecycleListeners.removeValue(listener, true); + } + } +} diff --git a/desktop/src/com/badlogic/gdx/backends/lwjgl/LwjglCanvas.java b/desktop/src/com/badlogic/gdx/backends/lwjgl/LwjglCanvas.java new file mode 100644 index 0000000..a1c915e --- /dev/null +++ b/desktop/src/com/badlogic/gdx/backends/lwjgl/LwjglCanvas.java @@ -0,0 +1,501 @@ +/******************************************************************************* + * Copyright 2011 See AUTHORS file. + * + * 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 + * + * http://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. + ******************************************************************************/ + +package com.badlogic.gdx.backends.lwjgl; + +import java.awt.Canvas; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.util.HashMap; +import java.util.Map; + +import org.lwjgl.opengl.AWTGLCanvas; +import org.lwjgl.opengl.Display; + +import com.badlogic.gdx.Application; +import com.badlogic.gdx.ApplicationListener; +import com.badlogic.gdx.ApplicationLogger; +import com.badlogic.gdx.Audio; +import com.badlogic.gdx.Files; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Graphics; +import com.badlogic.gdx.Input; +import com.badlogic.gdx.LifecycleListener; +import com.badlogic.gdx.Net; +import com.badlogic.gdx.Preferences; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.Clipboard; +import com.badlogic.gdx.utils.SharedLibraryLoader; +import com.etheller.warsmash.audio.OpenALAudio; + +/** + * An OpenGL surface on an AWT Canvas, allowing OpenGL to be embedded in a Swing + * application. This uses {@link Display#setParent(Canvas)}, which is preferred + * over {@link AWTGLCanvas} but is limited to a single LwjglCanvas in an + * application. All OpenGL calls are done on the EDT. Note that you may need to + * call {@link #stop()} or a Swing application may deadlock on System.exit due + * to how LWJGL and/or Swing deal with shutdown hooks. + * + * @author Nathan Sweet + */ +public class LwjglCanvas implements Application { + static boolean isWindows = System.getProperty("os.name").contains("Windows"); + + LwjglGraphics graphics; + OpenALAudio audio; + LwjglFiles files; + LwjglInput input; + LwjglNet net; + ApplicationListener listener; + Canvas canvas; + final Array runnables = new Array(); + final Array executedRunnables = new Array(); + final Array lifecycleListeners = new Array(); + boolean running = true; + int logLevel = LOG_INFO; + ApplicationLogger applicationLogger; + Cursor cursor; + + public LwjglCanvas(final ApplicationListener listener) { + final LwjglApplicationConfiguration config = new LwjglApplicationConfiguration(); + initialize(listener, config); + } + + public LwjglCanvas(final ApplicationListener listener, final LwjglApplicationConfiguration config) { + initialize(listener, config); + } + + private void initialize(final ApplicationListener listener, final LwjglApplicationConfiguration config) { + LwjglNativesLoader.load(); + setApplicationLogger(new LwjglApplicationLogger()); + this.canvas = new Canvas() { + private final Dimension minSize = new Dimension(1, 1); + + @Override + public final void addNotify() { + super.addNotify(); + if (SharedLibraryLoader.isMac) { + EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + create(); + } + }); + } + else { + create(); + } + } + + @Override + public final void removeNotify() { + stop(); + super.removeNotify(); + } + + @Override + public Dimension getMinimumSize() { + return this.minSize; + } + }; + this.canvas.setSize(1, 1); + this.canvas.setIgnoreRepaint(true); + + this.graphics = new LwjglGraphics(this.canvas, config) { + @Override + public void setTitle(final String title) { + super.setTitle(title); + LwjglCanvas.this.setTitle(title); + } + + public boolean setWindowedMode(final int width, final int height, final boolean fullscreen) { + if (!super.setWindowedMode(width, height)) { + return false; + } + if (!fullscreen) { + LwjglCanvas.this.setDisplayMode(width, height); + } + return true; + } + + @Override + public boolean setFullscreenMode(final DisplayMode displayMode) { + if (!super.setFullscreenMode(displayMode)) { + return false; + } + LwjglCanvas.this.setDisplayMode(displayMode.width, displayMode.height); + return true; + } + }; + this.graphics.setVSync(config.vSyncEnabled); + if (!LwjglApplicationConfiguration.disableAudio) { + this.audio = new OpenALAudio(); + } + this.files = new LwjglFiles(); + this.input = new LwjglInput(); + this.net = new LwjglNet(); + this.listener = listener; + + Gdx.app = this; + Gdx.graphics = this.graphics; + Gdx.audio = this.audio; + Gdx.files = this.files; + Gdx.input = this.input; + Gdx.net = this.net; + } + + protected void setDisplayMode(final int width, final int height) { + } + + protected void setTitle(final String title) { + } + + @Override + public ApplicationListener getApplicationListener() { + return this.listener; + } + + public Canvas getCanvas() { + return this.canvas; + } + + @Override + public Audio getAudio() { + return this.audio; + } + + @Override + public Files getFiles() { + return this.files; + } + + @Override + public Graphics getGraphics() { + return this.graphics; + } + + @Override + public Input getInput() { + return this.input; + } + + @Override + public Net getNet() { + return this.net; + } + + @Override + public ApplicationType getType() { + return ApplicationType.Desktop; + } + + @Override + public int getVersion() { + return 0; + } + + void create() { + try { + this.graphics.setupDisplay(); + + this.listener.create(); + this.listener.resize(Math.max(1, this.graphics.getWidth()), Math.max(1, this.graphics.getHeight())); + + start(); + } + catch (final Exception ex) { + stopped(); + exception(ex); + return; + } + + EventQueue.invokeLater(new Runnable() { + int lastWidth = Math.max(1, LwjglCanvas.this.graphics.getWidth()); + int lastHeight = Math.max(1, LwjglCanvas.this.graphics.getHeight()); + + @Override + public void run() { + if (!LwjglCanvas.this.running || Display.isCloseRequested()) { + LwjglCanvas.this.running = false; + stopped(); + return; + } + try { + Display.processMessages(); + if ((LwjglCanvas.this.cursor != null) || !isWindows) { + LwjglCanvas.this.canvas.setCursor(LwjglCanvas.this.cursor); + } + + boolean shouldRender = false; + + final int width = Math.max(1, LwjglCanvas.this.graphics.getWidth()); + final int height = Math.max(1, LwjglCanvas.this.graphics.getHeight()); + if ((this.lastWidth != width) || (this.lastHeight != height)) { + this.lastWidth = width; + this.lastHeight = height; + Gdx.gl.glViewport(0, 0, this.lastWidth, this.lastHeight); + resize(width, height); + LwjglCanvas.this.listener.resize(width, height); + shouldRender = true; + } + + if (executeRunnables()) { + shouldRender = true; + } + + // If one of the runnables set running to false, for example after an exit(). + if (!LwjglCanvas.this.running) { + return; + } + + LwjglCanvas.this.input.update(); + shouldRender |= LwjglCanvas.this.graphics.shouldRender(); + LwjglCanvas.this.input.processEvents(); + if (LwjglCanvas.this.audio != null) { + LwjglCanvas.this.audio.update(); + } + + if (shouldRender) { + LwjglCanvas.this.graphics.updateTime(); + LwjglCanvas.this.graphics.frameId++; + LwjglCanvas.this.listener.render(); + Display.update(false); + } + + Display.sync(getFrameRate()); + } + catch (final Throwable ex) { + exception(ex); + } + EventQueue.invokeLater(this); + } + }); + } + + public boolean executeRunnables() { + synchronized (this.runnables) { + for (int i = this.runnables.size - 1; i >= 0; i--) { + this.executedRunnables.addAll(this.runnables.get(i)); + } + this.runnables.clear(); + } + if (this.executedRunnables.size == 0) { + return false; + } + do { + this.executedRunnables.pop().run(); + } + while (this.executedRunnables.size > 0); + return true; + } + + protected int getFrameRate() { + int frameRate = Display.isActive() ? this.graphics.config.foregroundFPS : this.graphics.config.backgroundFPS; + if (frameRate == -1) { + frameRate = 10; + } + if (frameRate == 0) { + frameRate = this.graphics.config.backgroundFPS; + } + if (frameRate == 0) { + frameRate = 30; + } + return frameRate; + } + + protected void exception(final Throwable ex) { + ex.printStackTrace(); + stop(); + } + + /** + * Called after {@link ApplicationListener} create and resize, but before the + * game loop iteration. + */ + protected void start() { + } + + /** Called when the canvas size changes. */ + protected void resize(final int width, final int height) { + } + + /** Called when the game loop has stopped. */ + protected void stopped() { + } + + public void stop() { + EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + if (!LwjglCanvas.this.running) { + return; + } + LwjglCanvas.this.running = false; + final Array listeners = LwjglCanvas.this.lifecycleListeners; + synchronized (listeners) { + for (final LifecycleListener listener : listeners) { + listener.pause(); + listener.dispose(); + } + } + LwjglCanvas.this.listener.pause(); + LwjglCanvas.this.listener.dispose(); + try { + Display.destroy(); + if (LwjglCanvas.this.audio != null) { + LwjglCanvas.this.audio.dispose(); + } + } + catch (final Throwable ignored) { + } + } + }); + } + + @Override + public long getJavaHeap() { + return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + } + + @Override + public long getNativeHeap() { + return getJavaHeap(); + } + + Map preferences = new HashMap(); + + @Override + public Preferences getPreferences(final String name) { + if (this.preferences.containsKey(name)) { + return this.preferences.get(name); + } + else { + final Preferences prefs = new LwjglPreferences(name, ".prefs/"); + this.preferences.put(name, prefs); + return prefs; + } + } + + @Override + public Clipboard getClipboard() { + return new LwjglClipboard(); + } + + @Override + public void postRunnable(final Runnable runnable) { + synchronized (this.runnables) { + this.runnables.add(runnable); + Gdx.graphics.requestRendering(); + } + } + + @Override + public void debug(final String tag, final String message) { + if (this.logLevel >= LOG_DEBUG) { + getApplicationLogger().debug(tag, message); + } + } + + @Override + public void debug(final String tag, final String message, final Throwable exception) { + if (this.logLevel >= LOG_DEBUG) { + getApplicationLogger().debug(tag, message, exception); + } + } + + @Override + public void log(final String tag, final String message) { + if (this.logLevel >= LOG_INFO) { + getApplicationLogger().log(tag, message); + } + } + + @Override + public void log(final String tag, final String message, final Throwable exception) { + if (this.logLevel >= LOG_INFO) { + getApplicationLogger().log(tag, message, exception); + } + } + + @Override + public void error(final String tag, final String message) { + if (this.logLevel >= LOG_ERROR) { + getApplicationLogger().error(tag, message); + } + } + + @Override + public void error(final String tag, final String message, final Throwable exception) { + if (this.logLevel >= LOG_ERROR) { + getApplicationLogger().error(tag, message, exception); + } + } + + @Override + public void setLogLevel(final int logLevel) { + this.logLevel = logLevel; + } + + @Override + public int getLogLevel() { + return this.logLevel; + } + + @Override + public void setApplicationLogger(final ApplicationLogger applicationLogger) { + this.applicationLogger = applicationLogger; + } + + @Override + public ApplicationLogger getApplicationLogger() { + return this.applicationLogger; + } + + @Override + public void exit() { + postRunnable(new Runnable() { + @Override + public void run() { + LwjglCanvas.this.listener.pause(); + LwjglCanvas.this.listener.dispose(); + if (LwjglCanvas.this.audio != null) { + LwjglCanvas.this.audio.dispose(); + } + System.exit(-1); + } + }); + } + + /** @param cursor May be null. */ + public void setCursor(final Cursor cursor) { + this.cursor = cursor; + } + + @Override + public void addLifecycleListener(final LifecycleListener listener) { + synchronized (this.lifecycleListeners) { + this.lifecycleListeners.add(listener); + } + } + + @Override + public void removeLifecycleListener(final LifecycleListener listener) { + synchronized (this.lifecycleListeners) { + this.lifecycleListeners.removeValue(listener, true); + } + } +} diff --git a/desktop/src/com/etheller/warsmash/audio/Flac.java b/desktop/src/com/etheller/warsmash/audio/Flac.java new file mode 100644 index 0000000..3a99bf9 --- /dev/null +++ b/desktop/src/com/etheller/warsmash/audio/Flac.java @@ -0,0 +1,195 @@ +/******************************************************************************* + * Copyright 2011 See AUTHORS file. + * + * 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 + * + * http://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. + ******************************************************************************/ + +package com.etheller.warsmash.audio; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; + +import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.utils.GdxRuntimeException; +import com.badlogic.gdx.utils.StreamUtils; +import com.etheller.warsmash.audio.Wav.WavInputStream; + +import io.nayuki.flac.common.StreamInfo; +import io.nayuki.flac.decode.DataFormatException; +import io.nayuki.flac.decode.FlacDecoder; + +public class Flac { + static public class Music extends OpenALMusic { + private WavInputStream input; + + public Music(final OpenALAudio audio, final FileHandle file) { + super(audio, file); + try { + this.input = new WavInputStream(new ByteArrayInputStream(makeItWav(file)), file); + } + catch (final IOException ex) { + throw new GdxRuntimeException("Error reading FLAC file: " + this.file, ex); + } + if (audio.noDevice) { + return; + } + setup(this.input.channels, this.input.sampleRate); + } + + @Override + public int read(final byte[] buffer) { + if (this.input == null) { + try { + this.input = new WavInputStream(new ByteArrayInputStream(makeItWav(this.file)), this.file); + } + catch (final IOException ex) { + throw new GdxRuntimeException("Error reading FLAC file: " + this.file, ex); + } + setup(this.input.channels, this.input.sampleRate); + } + try { + return this.input.read(buffer); + } + catch (final IOException ex) { + throw new GdxRuntimeException("Error reading FLAC file: " + this.file, ex); + } + } + + @Override + public void reset() { + StreamUtils.closeQuietly(this.input); + this.input = null; + } + } + + static public class Sound extends OpenALSound { + public Sound(final OpenALAudio audio, final FileHandle file) { + super(audio); + if (audio.noDevice) { + return; + } + + WavInputStream input = null; + try { + input = new WavInputStream(new ByteArrayInputStream(makeItWav(file)), file); + setup(StreamUtils.copyStreamToByteArray(input, input.dataRemaining), input.channels, input.sampleRate); + } + catch (final IOException ex) { + throw new GdxRuntimeException("Error reading FLAC file: " + file, ex); + } + finally { + StreamUtils.closeQuietly(input); + } + } + } + + private static byte[] makeItWav(final FileHandle file) throws IOException { + // Decode input FLAC file + StreamInfo streamInfo; + int[][] samples; + try (FlacDecoder dec = new FlacDecoder(file.readBytes())) { + + // Handle metadata header blocks + while (dec.readAndHandleMetadataBlock() != null) { + ; + } + streamInfo = dec.streamInfo; + if ((streamInfo.sampleDepth % 8) != 0) { + throw new UnsupportedOperationException("Only whole-byte sample depth supported"); + } + + // Decode every block + samples = new int[streamInfo.numChannels][(int) streamInfo.numSamples]; + for (int off = 0;;) { + final int len = dec.readAudioBlock(samples, off); + if (len == 0) { + break; + } + off += len; + } + } + + // Check audio MD5 hash + final byte[] expectHash = streamInfo.md5Hash; + if (Arrays.equals(expectHash, new byte[16])) { + System.err.println("Warning: MD5 hash field was blank"); + } + else if (!Arrays.equals(StreamInfo.getMd5Hash(samples, streamInfo.sampleDepth), expectHash)) { + throw new DataFormatException("MD5 hash check failed"); + // Else the hash check passed + } + + // Start writing WAV output file + int bytesPerSample = streamInfo.sampleDepth / 8; + final boolean needsDownscaleForLibgdx = bytesPerSample >= 3; + int downsampleBytes = 0; + if (needsDownscaleForLibgdx) { + downsampleBytes = bytesPerSample - 2; + bytesPerSample = 2; + } + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(baos))) { + // Header chunk + final int sampleDataLen = samples[0].length * streamInfo.numChannels * bytesPerSample; + out.writeInt(0x52494646); // "RIFF" + writeLittleInt32(out, sampleDataLen + 36); + out.writeInt(0x57415645); // "WAVE" + + // Metadata chunk + out.writeInt(0x666D7420); // "fmt " + writeLittleInt32(out, 16); + writeLittleInt16(out, 0x0001); + writeLittleInt16(out, streamInfo.numChannels); + writeLittleInt32(out, streamInfo.sampleRate); + writeLittleInt32(out, streamInfo.sampleRate * streamInfo.numChannels * bytesPerSample); + writeLittleInt16(out, streamInfo.numChannels * bytesPerSample); + writeLittleInt16(out, needsDownscaleForLibgdx ? 16 : streamInfo.sampleDepth); + + // Audio data chunk ("data") + out.writeInt(0x64617461); // "data" + writeLittleInt32(out, sampleDataLen); + for (int i = 0; i < samples[0].length; i++) { + for (int j = 0; j < samples.length; j++) { + int val = samples[j][i]; + for (int k = 0; k < downsampleBytes; k++) { + val = val >>> 8; + } + if (bytesPerSample == 1) { + out.write(val + 128); // Convert to unsigned, as per WAV PCM conventions + } + else { // 2 <= bytesPerSample <= 4 + for (int k = 0; k < bytesPerSample; k++) { + out.write(val >>> (k * 8)); // Little endian + } + } + } + } + return baos.toByteArray(); + } + } + + // Helper members for writing WAV files + + private static void writeLittleInt16(final DataOutputStream out, final int x) throws IOException { + out.writeShort(Integer.reverseBytes(x) >>> 16); + } + + private static void writeLittleInt32(final DataOutputStream out, final int x) throws IOException { + out.writeInt(Integer.reverseBytes(x)); + } + +} diff --git a/desktop/src/com/etheller/warsmash/audio/JavaSoundAudioRecorder.java b/desktop/src/com/etheller/warsmash/audio/JavaSoundAudioRecorder.java new file mode 100644 index 0000000..a9f663f --- /dev/null +++ b/desktop/src/com/etheller/warsmash/audio/JavaSoundAudioRecorder.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright 2011 See AUTHORS file. + * + * 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 + * + * http://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. + ******************************************************************************/ + +package com.etheller.warsmash.audio; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioFormat.Encoding; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.TargetDataLine; + +import com.badlogic.gdx.audio.AudioRecorder; +import com.badlogic.gdx.utils.GdxRuntimeException; + +/** @author mzechner */ +public class JavaSoundAudioRecorder implements AudioRecorder { + private TargetDataLine line; + private byte[] buffer = new byte[1024 * 4]; + + public JavaSoundAudioRecorder(final int samplingRate, final boolean isMono) { + try { + final AudioFormat format = new AudioFormat(Encoding.PCM_SIGNED, samplingRate, 16, isMono ? 1 : 2, + isMono ? 2 : 4, samplingRate, false); + this.line = AudioSystem.getTargetDataLine(format); + this.line.open(format, this.buffer.length); + this.line.start(); + } + catch (final Exception ex) { + throw new GdxRuntimeException("Error creating JavaSoundAudioRecorder.", ex); + } + } + + @Override + public void read(final short[] samples, final int offset, final int numSamples) { + if (this.buffer.length < (numSamples * 2)) { + this.buffer = new byte[numSamples * 2]; + } + + final int toRead = numSamples * 2; + int read = 0; + while (read != toRead) { + read += this.line.read(this.buffer, read, toRead - read); + } + + for (int i = 0, j = 0; i < (numSamples * 2); i += 2, j++) { + samples[offset + j] = (short) ((this.buffer[i + 1] << 8) | (this.buffer[i] & 0xff)); + } + } + + @Override + public void dispose() { + this.line.close(); + } +} diff --git a/desktop/src/com/etheller/warsmash/audio/Mp3.java b/desktop/src/com/etheller/warsmash/audio/Mp3.java new file mode 100644 index 0000000..02bc10c --- /dev/null +++ b/desktop/src/com/etheller/warsmash/audio/Mp3.java @@ -0,0 +1,163 @@ +/******************************************************************************* + * Copyright 2011 See AUTHORS file. + * + * 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 + * + * http://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. + ******************************************************************************/ + +package com.etheller.warsmash.audio; + +import java.io.ByteArrayOutputStream; + +import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.utils.GdxRuntimeException; + +import javazoom.jl.decoder.Bitstream; +import javazoom.jl.decoder.BitstreamException; +import javazoom.jl.decoder.Header; +import javazoom.jl.decoder.MP3Decoder; +import javazoom.jl.decoder.OutputBuffer; + +/** @author Nathan Sweet */ +public class Mp3 { + static public class Music extends OpenALMusic { + // Note: This uses a slightly modified version of JLayer. + + private Bitstream bitstream; + private OutputBuffer outputBuffer; + private MP3Decoder decoder; + + public Music(final OpenALAudio audio, final FileHandle file) { + super(audio, file); + if (audio.noDevice) { + return; + } + this.bitstream = new Bitstream(file.read()); + this.decoder = new MP3Decoder(); + this.bufferOverhead = 4096; + try { + final Header header = this.bitstream.readFrame(); + if (header == null) { + throw new GdxRuntimeException("Empty MP3"); + } + final int channels = header.mode() == Header.SINGLE_CHANNEL ? 1 : 2; + this.outputBuffer = new OutputBuffer(channels, false); + this.decoder.setOutputBuffer(this.outputBuffer); + setup(channels, header.getSampleRate()); + } + catch (final BitstreamException e) { + throw new GdxRuntimeException("error while preloading mp3", e); + } + } + + @Override + public int read(final byte[] buffer) { + try { + boolean setup = this.bitstream == null; + if (setup) { + this.bitstream = new Bitstream(this.file.read()); + this.decoder = new MP3Decoder(); + } + + int totalLength = 0; + final int minRequiredLength = buffer.length - (OutputBuffer.BUFFERSIZE * 2); + while (totalLength <= minRequiredLength) { + final Header header = this.bitstream.readFrame(); + if (header == null) { + break; + } + if (setup) { + final int channels = header.mode() == Header.SINGLE_CHANNEL ? 1 : 2; + this.outputBuffer = new OutputBuffer(channels, false); + this.decoder.setOutputBuffer(this.outputBuffer); + setup(channels, header.getSampleRate()); + setup = false; + } + try { + this.decoder.decodeFrame(header, this.bitstream); + } + catch (final Exception ignored) { + // JLayer's decoder throws ArrayIndexOutOfBoundsException sometimes!? + } + this.bitstream.closeFrame(); + + final int length = this.outputBuffer.reset(); + System.arraycopy(this.outputBuffer.getBuffer(), 0, buffer, totalLength, length); + totalLength += length; + } + return totalLength; + } + catch (final Throwable ex) { + reset(); + throw new GdxRuntimeException("Error reading audio data.", ex); + } + } + + @Override + public void reset() { + if (this.bitstream == null) { + return; + } + try { + this.bitstream.close(); + } + catch (final BitstreamException ignored) { + } + this.bitstream = null; + } + } + + static public class Sound extends OpenALSound { + // Note: This uses a slightly modified version of JLayer. + + public Sound(final OpenALAudio audio, final FileHandle file) { + super(audio); + if (audio.noDevice) { + return; + } + final ByteArrayOutputStream output = new ByteArrayOutputStream(4096); + + final Bitstream bitstream = new Bitstream(file.read()); + final MP3Decoder decoder = new MP3Decoder(); + + try { + OutputBuffer outputBuffer = null; + int sampleRate = -1, channels = -1; + while (true) { + final Header header = bitstream.readFrame(); + if (header == null) { + break; + } + if (outputBuffer == null) { + channels = header.mode() == Header.SINGLE_CHANNEL ? 1 : 2; + outputBuffer = new OutputBuffer(channels, false); + decoder.setOutputBuffer(outputBuffer); + sampleRate = header.getSampleRate(); + } + try { + decoder.decodeFrame(header, bitstream); + } + catch (final Exception ignored) { + // JLayer's decoder throws ArrayIndexOutOfBoundsException sometimes!? + } + bitstream.closeFrame(); + output.write(outputBuffer.getBuffer(), 0, outputBuffer.reset()); + } + bitstream.close(); + setup(output.toByteArray(), channels, sampleRate); + } + catch (final Throwable ex) { + throw new GdxRuntimeException("Error reading audio data.", ex); + } + } + } +} diff --git a/desktop/src/com/etheller/warsmash/audio/Ogg.java b/desktop/src/com/etheller/warsmash/audio/Ogg.java new file mode 100644 index 0000000..f61fb19 --- /dev/null +++ b/desktop/src/com/etheller/warsmash/audio/Ogg.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright 2011 See AUTHORS file. + * + * 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 + * + * http://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. + ******************************************************************************/ + +package com.etheller.warsmash.audio; + +import java.io.ByteArrayOutputStream; + +import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.utils.StreamUtils; + +/** @author Nathan Sweet */ +public class Ogg { + static public class Music extends OpenALMusic { + private OggInputStream input; + private OggInputStream previousInput; + + public Music(final OpenALAudio audio, final FileHandle file) { + super(audio, file); + if (audio.noDevice) { + return; + } + this.input = new OggInputStream(file.read()); + setup(this.input.getChannels(), this.input.getSampleRate()); + } + + @Override + public int read(final byte[] buffer) { + if (this.input == null) { + this.input = new OggInputStream(this.file.read(), this.previousInput); + setup(this.input.getChannels(), this.input.getSampleRate()); + this.previousInput = null; // release this reference + } + return this.input.read(buffer); + } + + @Override + public void reset() { + StreamUtils.closeQuietly(this.input); + this.previousInput = null; + this.input = null; + } + + @Override + protected void loop() { + StreamUtils.closeQuietly(this.input); + this.previousInput = this.input; + this.input = null; + } + } + + static public class Sound extends OpenALSound { + public Sound(final OpenALAudio audio, final FileHandle file) { + super(audio); + if (audio.noDevice) { + return; + } + OggInputStream input = null; + try { + input = new OggInputStream(file.read()); + final ByteArrayOutputStream output = new ByteArrayOutputStream(4096); + final byte[] buffer = new byte[2048]; + while (!input.atEnd()) { + final int length = input.read(buffer); + if (length == -1) { + break; + } + output.write(buffer, 0, length); + } + setup(output.toByteArray(), input.getChannels(), input.getSampleRate()); + } + finally { + StreamUtils.closeQuietly(input); + } + } + } +} diff --git a/desktop/src/com/etheller/warsmash/audio/OggInputStream.java b/desktop/src/com/etheller/warsmash/audio/OggInputStream.java new file mode 100644 index 0000000..9fed1cf --- /dev/null +++ b/desktop/src/com/etheller/warsmash/audio/OggInputStream.java @@ -0,0 +1,520 @@ +/** + * Copyright (c) 2007, Slick 2D + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following + * conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the distribution. Neither the name of the Slick 2D nor the names of + * its contributors may be used to endorse or promote products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.etheller.warsmash.audio; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.utils.GdxRuntimeException; +import com.badlogic.gdx.utils.StreamUtils; +import com.jcraft.jogg.Packet; +import com.jcraft.jogg.Page; +import com.jcraft.jogg.StreamState; +import com.jcraft.jogg.SyncState; +import com.jcraft.jorbis.Block; +import com.jcraft.jorbis.Comment; +import com.jcraft.jorbis.DspState; +import com.jcraft.jorbis.Info; + +/** + * An input stream to read Ogg Vorbis. + * + * @author kevin + */ +public class OggInputStream extends InputStream { + private final static int BUFFER_SIZE = 512; + + /** The conversion buffer size */ + private int convsize = BUFFER_SIZE * 4; + /** The buffer used to read OGG file */ + private byte[] convbuffer; + /** The stream we're reading the OGG file from */ + private final InputStream input; + /** The audio information from the OGG header */ + private final Info oggInfo = new Info(); // struct that stores all the static vorbis bitstream settings + /** True if we're at the end of the available data */ + private boolean endOfStream; + + /** The Vorbis SyncState used to decode the OGG */ + private final SyncState syncState = new SyncState(); // sync and verify incoming physical bitstream + /** The Vorbis Stream State used to decode the OGG */ + private final StreamState streamState = new StreamState(); // take physical pages, weld into a logical stream of + // packets + /** The current OGG page */ + private final Page page = new Page(); // one Ogg bitstream page. Vorbis packets are inside + /** The current packet page */ + private final Packet packet = new Packet(); // one raw packet of data for decode + + /** The comment read from the OGG file */ + private final Comment comment = new Comment(); // struct that stores all the bitstream user comments + /** The Vorbis DSP stat eused to decode the OGG */ + private final DspState dspState = new DspState(); // central working state for the packet->PCM decoder + /** The OGG block we're currently working with to convert PCM */ + private final Block vorbisBlock = new Block(this.dspState); // local working space for packet->PCM decode + + /** Temporary scratch buffer */ + byte[] buffer; + /** The number of bytes read */ + int bytes = 0; + /** The true if we should be reading big endian */ + boolean bigEndian = ByteOrder.nativeOrder().equals(ByteOrder.BIG_ENDIAN); + /** True if we're reached the end of the current bit stream */ + boolean endOfBitStream = true; + /** True if we're initialise the OGG info block */ + boolean inited = false; + + /** The index into the byte array we currently read from */ + private int readIndex; + /** The byte array store used to hold the data read from the ogg */ + private byte[] outBuffer; + private int outIndex; + /** The total number of bytes */ + private int total; + + /** + * Create a new stream to decode OGG data + * + * @param input The input stream from which to read the OGG file + */ + public OggInputStream(final InputStream input) { + this(input, null); + } + + /** + * Create a new stream to decode OGG data, reusing buffers from another stream. + * + * It's not a good idea to use the old stream instance afterwards. + * + * @param input The input stream from which to read the OGG file + * @param previousStream The stream instance to reuse buffers from, may be null + */ + public OggInputStream(final InputStream input, final OggInputStream previousStream) { + if (previousStream == null) { + this.convbuffer = new byte[this.convsize]; + this.outBuffer = new byte[4096 * 500]; + } + else { + this.convbuffer = previousStream.convbuffer; + this.outBuffer = previousStream.outBuffer; + } + + this.input = input; + try { + this.total = input.available(); + } + catch (final IOException ex) { + throw new GdxRuntimeException(ex); + } + + init(); + } + + /** + * Get the number of bytes on the stream + * + * @return The number of the bytes on the stream + */ + public int getLength() { + return this.total; + } + + public int getChannels() { + return this.oggInfo.channels; + } + + public int getSampleRate() { + return this.oggInfo.rate; + } + + /** Initialise the streams and thread involved in the streaming of OGG data */ + private void init() { + initVorbis(); + readPCM(); + } + + /** @see java.io.InputStream#available() */ + @Override + public int available() { + return this.endOfStream ? 0 : 1; + } + + /** Initialise the vorbis decoding */ + private void initVorbis() { + this.syncState.init(); + } + + /** + * Get a page and packet from that page + * + * @return True if there was a page available + */ + private boolean getPageAndPacket() { + // grab some data at the head of the stream. We want the first page + // (which is guaranteed to be small and only contain the Vorbis + // stream initial header) We need the first page to get the stream + // serialno. + + // submit a 4k block to libvorbis' Ogg layer + int index = this.syncState.buffer(BUFFER_SIZE); + if (index == -1) { + return false; + } + + this.buffer = this.syncState.data; + if (this.buffer == null) { + this.endOfStream = true; + return false; + } + + try { + this.bytes = this.input.read(this.buffer, index, BUFFER_SIZE); + } + catch (final Exception e) { + throw new GdxRuntimeException("Failure reading Vorbis.", e); + } + this.syncState.wrote(this.bytes); + + // Get the first page. + if (this.syncState.pageout(this.page) != 1) { + // have we simply run out of data? If so, we're done. + if (this.bytes < BUFFER_SIZE) { + return false; + } + + // error case. Must not be Vorbis data + throw new GdxRuntimeException("Input does not appear to be an Ogg bitstream."); + } + + // Get the serial number and set up the rest of decode. + // serialno first; use it to set up a logical stream + this.streamState.init(this.page.serialno()); + + // extract the initial header from the first page and verify that the + // Ogg bitstream is in fact Vorbis data + + // I handle the initial header first instead of just having the code + // read all three Vorbis headers at once because reading the initial + // header is an easy way to identify a Vorbis bitstream and it's + // useful to see that functionality seperated out. + + this.oggInfo.init(); + this.comment.init(); + if (this.streamState.pagein(this.page) < 0) { + // error; stream version mismatch perhaps + throw new GdxRuntimeException("Error reading first page of Ogg bitstream."); + } + + if (this.streamState.packetout(this.packet) != 1) { + // no page? must not be vorbis + throw new GdxRuntimeException("Error reading initial header packet."); + } + + if (this.oggInfo.synthesis_headerin(this.comment, this.packet) < 0) { + // error case; not a vorbis header + throw new GdxRuntimeException("Ogg bitstream does not contain Vorbis audio data."); + } + + // At this point, we're sure we're Vorbis. We've set up the logical + // (Ogg) bitstream decoder. Get the comment and codebook headers and + // set up the Vorbis decoder + + // The next two packets in order are the comment and codebook headers. + // They're likely large and may span multiple pages. Thus we reead + // and submit data until we get our two pacakets, watching that no + // pages are missing. If a page is missing, error out; losing a + // header page is the only place where missing data is fatal. */ + + int i = 0; + while (i < 2) { + while (i < 2) { + int result = this.syncState.pageout(this.page); + if (result == 0) { + break; // Need more data + // Don't complain about missing or corrupt data yet. We'll + // catch it at the packet output phase + } + + if (result == 1) { + this.streamState.pagein(this.page); // we can ignore any errors here + // as they'll also become apparent + // at packetout + while (i < 2) { + result = this.streamState.packetout(this.packet); + if (result == 0) { + break; + } + if (result == -1) { + // Uh oh; data at some point was corrupted or missing! + // We can't tolerate that in a header. Die. + throw new GdxRuntimeException("Corrupt secondary header."); + } + + this.oggInfo.synthesis_headerin(this.comment, this.packet); + i++; + } + } + } + // no harm in not checking before adding more + index = this.syncState.buffer(BUFFER_SIZE); + if (index == -1) { + return false; + } + this.buffer = this.syncState.data; + try { + this.bytes = this.input.read(this.buffer, index, BUFFER_SIZE); + } + catch (final Exception e) { + throw new GdxRuntimeException("Failed to read Vorbis.", e); + } + if ((this.bytes == 0) && (i < 2)) { + throw new GdxRuntimeException("End of file before finding all Vorbis headers."); + } + this.syncState.wrote(this.bytes); + } + + this.convsize = BUFFER_SIZE / this.oggInfo.channels; + + // OK, got and parsed all three headers. Initialize the Vorbis + // packet->PCM decoder. + this.dspState.synthesis_init(this.oggInfo); // central decode state + this.vorbisBlock.init(this.dspState); // local state for most of the decode + // so multiple block decodes can + // proceed in parallel. We could init + // multiple vorbis_block structures + // for vd here + + return true; + } + + /** Decode the OGG file as shown in the jogg/jorbis examples */ + private void readPCM() { + boolean wrote = false; + + while (true) { // we repeat if the bitstream is chained + if (this.endOfBitStream) { + if (!getPageAndPacket()) { + break; + } + this.endOfBitStream = false; + } + + if (!this.inited) { + this.inited = true; + return; + } + + final float[][][] _pcm = new float[1][][]; + final int[] _index = new int[this.oggInfo.channels]; + // The rest is just a straight decode loop until end of stream + while (!this.endOfBitStream) { + while (!this.endOfBitStream) { + int result = this.syncState.pageout(this.page); + + if (result == 0) { + break; // need more data + } + + if (result == -1) { // missing or corrupt data at this page position + // throw new GdxRuntimeException("Corrupt or missing data in bitstream."); + Gdx.app.log("gdx-audio", "Error reading OGG: Corrupt or missing data in bitstream."); + } + else { + this.streamState.pagein(this.page); // can safely ignore errors at + // this point + while (true) { + result = this.streamState.packetout(this.packet); + + if (result == 0) { + break; // need more data + } + if (result == -1) { // missing or corrupt data at this page position + // no reason to complain; already complained above + } + else { + // we have a packet. Decode it + int samples; + if (this.vorbisBlock.synthesis(this.packet) == 0) { // test for success! + this.dspState.synthesis_blockin(this.vorbisBlock); + } + + // **pcm is a multichannel float vector. In stereo, for + // example, pcm[0] is left, and pcm[1] is right. samples is + // the size of each channel. Convert the float values + // (-1.<=range<=1.) to whatever PCM format and write it out + + while ((samples = this.dspState.synthesis_pcmout(_pcm, _index)) > 0) { + final float[][] pcm = _pcm[0]; + // boolean clipflag = false; + final int bout = (samples < this.convsize ? samples : this.convsize); + + // convert floats to 16 bit signed ints (host order) and + // interleave + for (int i = 0; i < this.oggInfo.channels; i++) { + int ptr = i * 2; + // int ptr=i; + final int mono = _index[i]; + for (int j = 0; j < bout; j++) { + int val = (int) (pcm[i][mono + j] * 32767.); + // might as well guard against clipping + if (val > 32767) { + val = 32767; + } + if (val < -32768) { + val = -32768; + } + if (val < 0) { + val = val | 0x8000; + } + + if (this.bigEndian) { + this.convbuffer[ptr] = (byte) (val >>> 8); + this.convbuffer[ptr + 1] = (byte) (val); + } + else { + this.convbuffer[ptr] = (byte) (val); + this.convbuffer[ptr + 1] = (byte) (val >>> 8); + } + ptr += 2 * (this.oggInfo.channels); + } + } + + final int bytesToWrite = 2 * this.oggInfo.channels * bout; + if ((this.outIndex + bytesToWrite) > this.outBuffer.length) { + throw new GdxRuntimeException("Ogg block too big to be buffered: " + + bytesToWrite + ", " + (this.outBuffer.length - this.outIndex)); + } + else { + System.arraycopy(this.convbuffer, 0, this.outBuffer, this.outIndex, + bytesToWrite); + this.outIndex += bytesToWrite; + } + + wrote = true; + this.dspState.synthesis_read(bout); // tell libvorbis how + // many samples we + // actually consumed + } + } + } + if (this.page.eos() != 0) { + this.endOfBitStream = true; + } + + if ((!this.endOfBitStream) && (wrote)) { + return; + } + } + } + + if (!this.endOfBitStream) { + this.bytes = 0; + final int index = this.syncState.buffer(BUFFER_SIZE); + if (index >= 0) { + this.buffer = this.syncState.data; + try { + this.bytes = this.input.read(this.buffer, index, BUFFER_SIZE); + } + catch (final Exception e) { + throw new GdxRuntimeException("Error during Vorbis decoding.", e); + } + } + else { + this.bytes = 0; + } + this.syncState.wrote(this.bytes); + if (this.bytes == 0) { + this.endOfBitStream = true; + } + } + } + + // clean up this logical bitstream; before exit we see if we're + // followed by another [chained] + this.streamState.clear(); + + // ogg_page and ogg_packet structs always point to storage in + // libvorbis. They're never freed or manipulated directly + + this.vorbisBlock.clear(); + this.dspState.clear(); + this.oggInfo.clear(); // must be called last + } + + // OK, clean up the framer + this.syncState.clear(); + this.endOfStream = true; + } + + @Override + public int read() { + if (this.readIndex >= this.outIndex) { + this.outIndex = 0; + readPCM(); + this.readIndex = 0; + if (this.outIndex == 0) { + return -1; + } + } + + int value = this.outBuffer[this.readIndex]; + if (value < 0) { + value = 256 + value; + } + this.readIndex++; + + return value; + } + + public boolean atEnd() { + return this.endOfStream && (this.readIndex >= this.outIndex); + } + + @Override + public int read(final byte[] b, final int off, final int len) { + for (int i = 0; i < len; i++) { + final int value = read(); + if (value >= 0) { + b[i] = (byte) value; + } + else { + if (i == 0) { + return -1; + } + return i; + } + } + return len; + } + + @Override + public int read(final byte[] b) { + return read(b, 0, b.length); + } + + @Override + public void close() { + StreamUtils.closeQuietly(this.input); + } +} diff --git a/desktop/src/com/etheller/warsmash/audio/OpenALAudio.java b/desktop/src/com/etheller/warsmash/audio/OpenALAudio.java new file mode 100644 index 0000000..ac16260 --- /dev/null +++ b/desktop/src/com/etheller/warsmash/audio/OpenALAudio.java @@ -0,0 +1,479 @@ +/******************************************************************************* + * Copyright 2011 See AUTHORS file. + * + * 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 + * + * http://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. + ******************************************************************************/ + +package com.etheller.warsmash.audio; + +import static org.lwjgl.openal.AL10.AL_BUFFER; +import static org.lwjgl.openal.AL10.AL_NO_ERROR; +import static org.lwjgl.openal.AL10.AL_ORIENTATION; +import static org.lwjgl.openal.AL10.AL_PAUSED; +import static org.lwjgl.openal.AL10.AL_PLAYING; +import static org.lwjgl.openal.AL10.AL_POSITION; +import static org.lwjgl.openal.AL10.AL_SOURCE_STATE; +import static org.lwjgl.openal.AL10.AL_STOPPED; +import static org.lwjgl.openal.AL10.AL_VELOCITY; +import static org.lwjgl.openal.AL10.alDeleteSources; +import static org.lwjgl.openal.AL10.alGenSources; +import static org.lwjgl.openal.AL10.alGetError; +import static org.lwjgl.openal.AL10.alGetSourcei; +import static org.lwjgl.openal.AL10.alListener; +import static org.lwjgl.openal.AL10.alSourcePause; +import static org.lwjgl.openal.AL10.alSourcePlay; +import static org.lwjgl.openal.AL10.alSourceStop; +import static org.lwjgl.openal.AL10.alSourcei; + +import java.nio.FloatBuffer; + +import org.lwjgl.BufferUtils; +import org.lwjgl.LWJGLException; +import org.lwjgl.openal.AL; +import org.lwjgl.openal.AL10; + +import com.badlogic.gdx.Audio; +import com.badlogic.gdx.audio.AudioDevice; +import com.badlogic.gdx.audio.AudioRecorder; +import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.GdxRuntimeException; +import com.badlogic.gdx.utils.IntArray; +import com.badlogic.gdx.utils.IntMap; +import com.badlogic.gdx.utils.LongMap; +import com.badlogic.gdx.utils.ObjectMap; + +/** @author Nathan Sweet */ +public class OpenALAudio implements Audio { + private final int deviceBufferSize; + private final int deviceBufferCount; + private IntArray idleSources, allSources; + private LongMap soundIdToSource; + private IntMap sourceToSoundId; + private long nextSoundId = 0; + private final ObjectMap> extensionToSoundClass = new ObjectMap(); + private final ObjectMap> extensionToMusicClass = new ObjectMap(); + private OpenALSound[] recentSounds; + private int mostRecetSound = -1; + + Array music = new Array(false, 1, OpenALMusic.class); + boolean noDevice = false; + + public OpenALAudio() { + this(16, 9, 512); + } + + public OpenALAudio(final int simultaneousSources, final int deviceBufferCount, final int deviceBufferSize) { + this.deviceBufferSize = deviceBufferSize; + this.deviceBufferCount = deviceBufferCount; + + registerSound("ogg", Ogg.Sound.class); + registerMusic("ogg", Ogg.Music.class); + registerSound("wav", Wav.Sound.class); + registerMusic("wav", Wav.Music.class); + registerSound("mp3", Mp3.Sound.class); + registerMusic("mp3", Mp3.Music.class); + registerSound("flac", Flac.Sound.class); + registerMusic("flac", Flac.Music.class); + + try { + AL.create(); + } + catch (final LWJGLException ex) { + this.noDevice = true; + ex.printStackTrace(); + return; + } + + this.allSources = new IntArray(false, simultaneousSources); + for (int i = 0; i < simultaneousSources; i++) { + final int sourceID = alGenSources(); + if (alGetError() != AL_NO_ERROR) { + break; + } + this.allSources.add(sourceID); + } + this.idleSources = new IntArray(this.allSources); + this.soundIdToSource = new LongMap(); + this.sourceToSoundId = new IntMap(); + + final FloatBuffer orientation = (FloatBuffer) BufferUtils.createFloatBuffer(6) + .put(new float[] { 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f }).flip(); + alListener(AL_ORIENTATION, orientation); + final FloatBuffer velocity = (FloatBuffer) BufferUtils.createFloatBuffer(3) + .put(new float[] { 0.0f, 0.0f, 0.0f }).flip(); + alListener(AL_VELOCITY, velocity); + final FloatBuffer position = (FloatBuffer) BufferUtils.createFloatBuffer(3) + .put(new float[] { 0.0f, 0.0f, 0.0f }).flip(); + alListener(AL_POSITION, position); + + this.recentSounds = new OpenALSound[simultaneousSources]; + } + + public void registerSound(final String extension, final Class soundClass) { + if (extension == null) { + throw new IllegalArgumentException("extension cannot be null."); + } + if (soundClass == null) { + throw new IllegalArgumentException("soundClass cannot be null."); + } + this.extensionToSoundClass.put(extension, soundClass); + } + + public void registerMusic(final String extension, final Class musicClass) { + if (extension == null) { + throw new IllegalArgumentException("extension cannot be null."); + } + if (musicClass == null) { + throw new IllegalArgumentException("musicClass cannot be null."); + } + this.extensionToMusicClass.put(extension, musicClass); + } + + @Override + public OpenALSound newSound(final FileHandle file) { + if (file == null) { + throw new IllegalArgumentException("file cannot be null."); + } + final Class soundClass = this.extensionToSoundClass.get(file.extension().toLowerCase()); + if (soundClass == null) { + throw new GdxRuntimeException("Unknown file extension for sound: " + file); + } + try { + return soundClass.getConstructor(new Class[] { OpenALAudio.class, FileHandle.class }).newInstance(this, + file); + } + catch (final Exception ex) { + throw new GdxRuntimeException("Error creating sound " + soundClass.getName() + " for file: " + file, ex); + } + } + + @Override + public OpenALMusic newMusic(final FileHandle file) { + if (file == null) { + throw new IllegalArgumentException("file cannot be null."); + } + final Class musicClass = this.extensionToMusicClass.get(file.extension().toLowerCase()); + if (musicClass == null) { + throw new GdxRuntimeException("Unknown file extension for music: " + file); + } + try { + return musicClass.getConstructor(new Class[] { OpenALAudio.class, FileHandle.class }).newInstance(this, + file); + } + catch (final Exception ex) { + throw new GdxRuntimeException("Error creating music " + musicClass.getName() + " for file: " + file, ex); + } + } + + int obtainSource(final boolean isMusic) { + if (this.noDevice) { + return 0; + } + for (int i = 0, n = this.idleSources.size; i < n; i++) { + final int sourceId = this.idleSources.get(i); + final int state = alGetSourcei(sourceId, AL_SOURCE_STATE); + if ((state != AL_PLAYING) && (state != AL_PAUSED)) { + if (isMusic) { + this.idleSources.removeIndex(i); + } + else { + if (this.sourceToSoundId.containsKey(sourceId)) { + final long soundId = this.sourceToSoundId.get(sourceId); + this.sourceToSoundId.remove(sourceId); + this.soundIdToSource.remove(soundId); + } + + final long soundId = this.nextSoundId++; + this.sourceToSoundId.put(sourceId, soundId); + this.soundIdToSource.put(soundId, sourceId); + } + alSourceStop(sourceId); + alSourcei(sourceId, AL_BUFFER, 0); + AL10.alSourcef(sourceId, AL10.AL_GAIN, 1); + AL10.alSourcef(sourceId, AL10.AL_PITCH, 1); + AL10.alSource3f(sourceId, AL10.AL_POSITION, 0, 0, 1f); + return sourceId; + } + } + return -1; + } + + void freeSource(final int sourceID) { + if (this.noDevice) { + return; + } + alSourceStop(sourceID); + alSourcei(sourceID, AL_BUFFER, 0); + if (this.sourceToSoundId.containsKey(sourceID)) { + final long soundId = this.sourceToSoundId.remove(sourceID); + this.soundIdToSource.remove(soundId); + } + this.idleSources.add(sourceID); + } + + void freeBuffer(final int bufferID) { + if (this.noDevice) { + return; + } + for (int i = 0, n = this.idleSources.size; i < n; i++) { + final int sourceID = this.idleSources.get(i); + if (alGetSourcei(sourceID, AL_BUFFER) == bufferID) { + if (this.sourceToSoundId.containsKey(sourceID)) { + final long soundId = this.sourceToSoundId.remove(sourceID); + this.soundIdToSource.remove(soundId); + } + alSourceStop(sourceID); + alSourcei(sourceID, AL_BUFFER, 0); + } + } + } + + void stopSourcesWithBuffer(final int bufferID) { + if (this.noDevice) { + return; + } + for (int i = 0, n = this.idleSources.size; i < n; i++) { + final int sourceID = this.idleSources.get(i); + if (alGetSourcei(sourceID, AL_BUFFER) == bufferID) { + if (this.sourceToSoundId.containsKey(sourceID)) { + final long soundId = this.sourceToSoundId.remove(sourceID); + this.soundIdToSource.remove(soundId); + } + alSourceStop(sourceID); + } + } + } + + void pauseSourcesWithBuffer(final int bufferID) { + if (this.noDevice) { + return; + } + for (int i = 0, n = this.idleSources.size; i < n; i++) { + final int sourceID = this.idleSources.get(i); + if (alGetSourcei(sourceID, AL_BUFFER) == bufferID) { + alSourcePause(sourceID); + } + } + } + + void resumeSourcesWithBuffer(final int bufferID) { + if (this.noDevice) { + return; + } + for (int i = 0, n = this.idleSources.size; i < n; i++) { + final int sourceID = this.idleSources.get(i); + if (alGetSourcei(sourceID, AL_BUFFER) == bufferID) { + if (alGetSourcei(sourceID, AL_SOURCE_STATE) == AL_PAUSED) { + alSourcePlay(sourceID); + } + } + } + } + + public void update() { + if (this.noDevice) { + return; + } + for (int i = 0; i < this.music.size; i++) { + this.music.items[i].update(); + } + } + + public long getSoundId(final int sourceId) { + if (!this.sourceToSoundId.containsKey(sourceId)) { + return -1; + } + return this.sourceToSoundId.get(sourceId); + } + + public void stopSound(final long soundId) { + if (!this.soundIdToSource.containsKey(soundId)) { + return; + } + final int sourceId = this.soundIdToSource.get(soundId); + alSourceStop(sourceId); + } + + public void pauseSound(final long soundId) { + if (!this.soundIdToSource.containsKey(soundId)) { + return; + } + final int sourceId = this.soundIdToSource.get(soundId); + alSourcePause(sourceId); + } + + public void resumeSound(final long soundId) { + if (!this.soundIdToSource.containsKey(soundId)) { + return; + } + final int sourceId = this.soundIdToSource.get(soundId); + if (alGetSourcei(sourceId, AL_SOURCE_STATE) == AL_PAUSED) { + alSourcePlay(sourceId); + } + } + + public void setSoundGain(final long soundId, final float volume) { + if (!this.soundIdToSource.containsKey(soundId)) { + return; + } + final int sourceId = this.soundIdToSource.get(soundId); + AL10.alSourcef(sourceId, AL10.AL_GAIN, volume); + } + + public void setSoundLooping(final long soundId, final boolean looping) { + if (!this.soundIdToSource.containsKey(soundId)) { + return; + } + final int sourceId = this.soundIdToSource.get(soundId); + alSourcei(sourceId, AL10.AL_LOOPING, looping ? AL10.AL_TRUE : AL10.AL_FALSE); + } + + public void setSoundPitch(final long soundId, final float pitch) { + if (!this.soundIdToSource.containsKey(soundId)) { + return; + } + final int sourceId = this.soundIdToSource.get(soundId); + AL10.alSourcef(sourceId, AL10.AL_PITCH, pitch); + } + + public void setSoundPan(final long soundId, final float pan, final float volume) { + if (!this.soundIdToSource.containsKey(soundId)) { + return; + } + final int sourceId = this.soundIdToSource.get(soundId); + + AL10.alSource3f(sourceId, AL10.AL_POSITION, MathUtils.cos(((pan - 1) * MathUtils.PI) / 2), 0, + MathUtils.sin(((pan + 1) * MathUtils.PI) / 2)); + AL10.alSourcef(sourceId, AL10.AL_GAIN, volume); + } + + public void setSoundPosition(final long soundId, final float x, final float y, final float z, + final boolean is3DSound, final float maxDistance, final float refDistance) { + if (!this.soundIdToSource.containsKey(soundId)) { + return; + } + final int sourceId = this.soundIdToSource.get(soundId); + + AL10.alSource3f(sourceId, AL10.AL_POSITION, x, y, z); + AL10.alSourcef(sourceId, AL10.AL_MAX_DISTANCE, maxDistance); + AL10.alSourcef(sourceId, AL10.AL_REFERENCE_DISTANCE, refDistance); + AL10.alSourcef(sourceId, AL10.AL_ROLLOFF_FACTOR, 1.0f); + AL10.alSourcei(sourceId, AL10.AL_SOURCE_RELATIVE, is3DSound ? AL10.AL_FALSE : AL10.AL_TRUE); + } + + public void dispose() { + if (this.noDevice) { + return; + } + for (int i = 0, n = this.allSources.size; i < n; i++) { + final int sourceID = this.allSources.get(i); + final int state = alGetSourcei(sourceID, AL_SOURCE_STATE); + if (state != AL_STOPPED) { + alSourceStop(sourceID); + } + alDeleteSources(sourceID); + } + + this.sourceToSoundId.clear(); + this.soundIdToSource.clear(); + + AL.destroy(); + while (AL.isCreated()) { + try { + Thread.sleep(10); + } + catch (final InterruptedException e) { + } + } + } + + @Override + public AudioDevice newAudioDevice(final int sampleRate, final boolean isMono) { + if (this.noDevice) { + return new AudioDevice() { + @Override + public void writeSamples(final float[] samples, final int offset, final int numSamples) { + } + + @Override + public void writeSamples(final short[] samples, final int offset, final int numSamples) { + } + + @Override + public void setVolume(final float volume) { + } + + @Override + public boolean isMono() { + return isMono; + } + + @Override + public int getLatency() { + return 0; + } + + @Override + public void dispose() { + } + }; + } + return new OpenALAudioDevice(this, sampleRate, isMono, this.deviceBufferSize, this.deviceBufferCount); + } + + @Override + public AudioRecorder newAudioRecorder(final int samplingRate, final boolean isMono) { + if (this.noDevice) { + return new AudioRecorder() { + @Override + public void read(final short[] samples, final int offset, final int numSamples) { + } + + @Override + public void dispose() { + } + }; + } + return new JavaSoundAudioRecorder(samplingRate, isMono); + } + + /** + * Retains a list of the most recently played sounds and stops the sound played + * least recently if necessary for a new sound to play + */ + protected void retain(final OpenALSound sound, final boolean stop) { + // Move the pointer ahead and wrap + this.mostRecetSound++; + this.mostRecetSound %= this.recentSounds.length; + + if (stop) { + // Stop the least recent sound (the one we are about to bump off the buffer) + if (this.recentSounds[this.mostRecetSound] != null) { + this.recentSounds[this.mostRecetSound].stop(); + } + } + + this.recentSounds[this.mostRecetSound] = sound; + } + + /** Removes the disposed sound from the least recently played list */ + public void forget(final OpenALSound sound) { + for (int i = 0; i < this.recentSounds.length; i++) { + if (this.recentSounds[i] == sound) { + this.recentSounds[i] = null; + } + } + } +} diff --git a/desktop/src/com/etheller/warsmash/audio/OpenALAudioDevice.java b/desktop/src/com/etheller/warsmash/audio/OpenALAudioDevice.java new file mode 100644 index 0000000..a251466 --- /dev/null +++ b/desktop/src/com/etheller/warsmash/audio/OpenALAudioDevice.java @@ -0,0 +1,265 @@ +/******************************************************************************* + * Copyright 2011 See AUTHORS file. + * + * 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 + * + * http://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. + ******************************************************************************/ + +package com.etheller.warsmash.audio; + +import static org.lwjgl.openal.AL10.AL_BUFFERS_PROCESSED; +import static org.lwjgl.openal.AL10.AL_FALSE; +import static org.lwjgl.openal.AL10.AL_FORMAT_MONO16; +import static org.lwjgl.openal.AL10.AL_FORMAT_STEREO16; +import static org.lwjgl.openal.AL10.AL_GAIN; +import static org.lwjgl.openal.AL10.AL_INVALID_VALUE; +import static org.lwjgl.openal.AL10.AL_LOOPING; +import static org.lwjgl.openal.AL10.AL_NO_ERROR; +import static org.lwjgl.openal.AL10.AL_PLAYING; +import static org.lwjgl.openal.AL10.AL_SOURCE_STATE; +import static org.lwjgl.openal.AL10.alBufferData; +import static org.lwjgl.openal.AL10.alDeleteBuffers; +import static org.lwjgl.openal.AL10.alGenBuffers; +import static org.lwjgl.openal.AL10.alGetError; +import static org.lwjgl.openal.AL10.alGetSourcef; +import static org.lwjgl.openal.AL10.alGetSourcei; +import static org.lwjgl.openal.AL10.alSourcePlay; +import static org.lwjgl.openal.AL10.alSourceQueueBuffers; +import static org.lwjgl.openal.AL10.alSourceUnqueueBuffers; +import static org.lwjgl.openal.AL10.alSourcef; +import static org.lwjgl.openal.AL10.alSourcei; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +import org.lwjgl.BufferUtils; +import org.lwjgl.openal.AL11; + +import com.badlogic.gdx.audio.AudioDevice; +import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.utils.GdxRuntimeException; + +/** @author Nathan Sweet */ +public class OpenALAudioDevice implements AudioDevice { + static private final int bytesPerSample = 2; + + private final OpenALAudio audio; + private final int channels; + private IntBuffer buffers; + private int sourceID = -1; + private final int format, sampleRate; + private boolean isPlaying; + private float volume = 1; + private float renderedSeconds; + + private final float secondsPerBuffer; + private byte[] bytes; + private final int bufferSize; + private final int bufferCount; + private final ByteBuffer tempBuffer; + + public OpenALAudioDevice(final OpenALAudio audio, final int sampleRate, final boolean isMono, final int bufferSize, + final int bufferCount) { + this.audio = audio; + this.channels = isMono ? 1 : 2; + this.bufferSize = bufferSize; + this.bufferCount = bufferCount; + this.format = this.channels > 1 ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16; + this.sampleRate = sampleRate; + this.secondsPerBuffer = (float) bufferSize / bytesPerSample / this.channels / sampleRate; + this.tempBuffer = BufferUtils.createByteBuffer(bufferSize); + } + + @Override + public void writeSamples(final short[] samples, final int offset, final int numSamples) { + if ((this.bytes == null) || (this.bytes.length < (numSamples * 2))) { + this.bytes = new byte[numSamples * 2]; + } + final int end = Math.min(offset + numSamples, samples.length); + for (int i = offset, ii = 0; i < end; i++) { + final short sample = samples[i]; + this.bytes[ii++] = (byte) (sample & 0xFF); + this.bytes[ii++] = (byte) ((sample >> 8) & 0xFF); + } + writeSamples(this.bytes, 0, numSamples * 2); + } + + @Override + public void writeSamples(final float[] samples, final int offset, final int numSamples) { + if ((this.bytes == null) || (this.bytes.length < (numSamples * 2))) { + this.bytes = new byte[numSamples * 2]; + } + final int end = Math.min(offset + numSamples, samples.length); + for (int i = offset, ii = 0; i < end; i++) { + float floatSample = samples[i]; + floatSample = MathUtils.clamp(floatSample, -1f, 1f); + final int intSample = (int) (floatSample * 32767); + this.bytes[ii++] = (byte) (intSample & 0xFF); + this.bytes[ii++] = (byte) ((intSample >> 8) & 0xFF); + } + writeSamples(this.bytes, 0, numSamples * 2); + } + + public void writeSamples(final byte[] data, int offset, int length) { + if (length < 0) { + throw new IllegalArgumentException("length cannot be < 0."); + } + + if (this.sourceID == -1) { + this.sourceID = this.audio.obtainSource(true); + if (this.sourceID == -1) { + return; + } + if (this.buffers == null) { + this.buffers = BufferUtils.createIntBuffer(this.bufferCount); + alGenBuffers(this.buffers); + if (alGetError() != AL_NO_ERROR) { + throw new GdxRuntimeException("Unabe to allocate audio buffers."); + } + } + alSourcei(this.sourceID, AL_LOOPING, AL_FALSE); + alSourcef(this.sourceID, AL_GAIN, this.volume); + // Fill initial buffers. + int queuedBuffers = 0; + for (int i = 0; i < this.bufferCount; i++) { + final int bufferID = this.buffers.get(i); + final int written = Math.min(this.bufferSize, length); + this.tempBuffer.clear(); + this.tempBuffer.put(data, offset, written).flip(); + alBufferData(bufferID, this.format, this.tempBuffer, this.sampleRate); + alSourceQueueBuffers(this.sourceID, bufferID); + length -= written; + offset += written; + queuedBuffers++; + } + // Queue rest of buffers, empty. + this.tempBuffer.clear().flip(); + for (int i = queuedBuffers; i < this.bufferCount; i++) { + final int bufferID = this.buffers.get(i); + alBufferData(bufferID, this.format, this.tempBuffer, this.sampleRate); + alSourceQueueBuffers(this.sourceID, bufferID); + } + alSourcePlay(this.sourceID); + this.isPlaying = true; + } + + while (length > 0) { + final int written = fillBuffer(data, offset, length); + length -= written; + offset += written; + } + } + + /** Blocks until some of the data could be buffered. */ + private int fillBuffer(final byte[] data, final int offset, final int length) { + final int written = Math.min(this.bufferSize, length); + + outer: while (true) { + int buffers = alGetSourcei(this.sourceID, AL_BUFFERS_PROCESSED); + while (buffers-- > 0) { + final int bufferID = alSourceUnqueueBuffers(this.sourceID); + if (bufferID == AL_INVALID_VALUE) { + break; + } + this.renderedSeconds += this.secondsPerBuffer; + + this.tempBuffer.clear(); + this.tempBuffer.put(data, offset, written).flip(); + alBufferData(bufferID, this.format, this.tempBuffer, this.sampleRate); + + alSourceQueueBuffers(this.sourceID, bufferID); + break outer; + } + // Wait for buffer to be free. + try { + Thread.sleep((long) (1000 * this.secondsPerBuffer)); + } + catch (final InterruptedException ignored) { + } + } + + // A buffer underflow will cause the source to stop. + if (!this.isPlaying || (alGetSourcei(this.sourceID, AL_SOURCE_STATE) != AL_PLAYING)) { + alSourcePlay(this.sourceID); + this.isPlaying = true; + } + + return written; + } + + public void stop() { + if (this.sourceID == -1) { + return; + } + this.audio.freeSource(this.sourceID); + this.sourceID = -1; + this.renderedSeconds = 0; + this.isPlaying = false; + } + + public boolean isPlaying() { + if (this.sourceID == -1) { + return false; + } + return this.isPlaying; + } + + @Override + public void setVolume(final float volume) { + this.volume = volume; + if (this.sourceID != -1) { + alSourcef(this.sourceID, AL_GAIN, volume); + } + } + + public float getPosition() { + if (this.sourceID == -1) { + return 0; + } + return this.renderedSeconds + alGetSourcef(this.sourceID, AL11.AL_SEC_OFFSET); + } + + public void setPosition(final float position) { + this.renderedSeconds = position; + } + + public int getChannels() { + return this.format == AL_FORMAT_STEREO16 ? 2 : 1; + } + + public int getRate() { + return this.sampleRate; + } + + @Override + public void dispose() { + if (this.buffers == null) { + return; + } + if (this.sourceID != -1) { + this.audio.freeSource(this.sourceID); + this.sourceID = -1; + } + alDeleteBuffers(this.buffers); + this.buffers = null; + } + + @Override + public boolean isMono() { + return this.channels == 1; + } + + @Override + public int getLatency() { + return (int) (this.secondsPerBuffer * this.bufferCount * 1000); + } +} diff --git a/desktop/src/com/etheller/warsmash/audio/OpenALMusic.java b/desktop/src/com/etheller/warsmash/audio/OpenALMusic.java new file mode 100644 index 0000000..279fea3 --- /dev/null +++ b/desktop/src/com/etheller/warsmash/audio/OpenALMusic.java @@ -0,0 +1,396 @@ +/******************************************************************************* + * Copyright 2011 See AUTHORS file. + * + * 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 + * + * http://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. + ******************************************************************************/ + +package com.etheller.warsmash.audio; + +import static org.lwjgl.openal.AL10.AL_BUFFERS_PROCESSED; +import static org.lwjgl.openal.AL10.AL_BUFFERS_QUEUED; +import static org.lwjgl.openal.AL10.AL_FALSE; +import static org.lwjgl.openal.AL10.AL_FORMAT_MONO16; +import static org.lwjgl.openal.AL10.AL_FORMAT_STEREO16; +import static org.lwjgl.openal.AL10.AL_GAIN; +import static org.lwjgl.openal.AL10.AL_INVALID_VALUE; +import static org.lwjgl.openal.AL10.AL_LOOPING; +import static org.lwjgl.openal.AL10.AL_NO_ERROR; +import static org.lwjgl.openal.AL10.AL_PLAYING; +import static org.lwjgl.openal.AL10.AL_POSITION; +import static org.lwjgl.openal.AL10.AL_SOURCE_STATE; +import static org.lwjgl.openal.AL10.alBufferData; +import static org.lwjgl.openal.AL10.alDeleteBuffers; +import static org.lwjgl.openal.AL10.alGenBuffers; +import static org.lwjgl.openal.AL10.alGetError; +import static org.lwjgl.openal.AL10.alGetSourcef; +import static org.lwjgl.openal.AL10.alGetSourcei; +import static org.lwjgl.openal.AL10.alSource3f; +import static org.lwjgl.openal.AL10.alSourcePause; +import static org.lwjgl.openal.AL10.alSourcePlay; +import static org.lwjgl.openal.AL10.alSourceQueueBuffers; +import static org.lwjgl.openal.AL10.alSourceStop; +import static org.lwjgl.openal.AL10.alSourceUnqueueBuffers; +import static org.lwjgl.openal.AL10.alSourcef; +import static org.lwjgl.openal.AL10.alSourcei; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +import org.lwjgl.BufferUtils; +import org.lwjgl.openal.AL11; + +import com.badlogic.gdx.audio.Music; +import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.utils.FloatArray; +import com.badlogic.gdx.utils.GdxRuntimeException; + +/** @author Nathan Sweet */ +public abstract class OpenALMusic implements Music { + static private final int bufferSize = 4096 * 10; + static private final int bufferCount = 3; + static private final int bytesPerSample = 2; + static private final byte[] tempBytes = new byte[bufferSize]; + static private final ByteBuffer tempBuffer = BufferUtils.createByteBuffer(bufferSize); + + private final FloatArray renderedSecondsQueue = new FloatArray(bufferCount); + + private final OpenALAudio audio; + private IntBuffer buffers; + private int sourceID = -1; + private int format, sampleRate; + private boolean isLooping, isPlaying; + private float volume = 1; + private float pan = 0; + private float renderedSeconds, maxSecondsPerBuffer; + + protected final FileHandle file; + protected int bufferOverhead = 0; + + private OnCompletionListener onCompletionListener; + + public OpenALMusic(final OpenALAudio audio, final FileHandle file) { + this.audio = audio; + this.file = file; + this.onCompletionListener = null; + } + + protected void setup(final int channels, final int sampleRate) { + this.format = channels > 1 ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16; + this.sampleRate = sampleRate; + this.maxSecondsPerBuffer = (float) (bufferSize - this.bufferOverhead) + / (bytesPerSample * channels * sampleRate); + } + + @Override + public void play() { + if (this.audio.noDevice) { + return; + } + if (this.sourceID == -1) { + this.sourceID = this.audio.obtainSource(true); + if (this.sourceID == -1) { + return; + } + + this.audio.music.add(this); + + if (this.buffers == null) { + this.buffers = BufferUtils.createIntBuffer(bufferCount); + alGenBuffers(this.buffers); + final int errorCode = alGetError(); + if (errorCode != AL_NO_ERROR) { + throw new GdxRuntimeException("Unable to allocate audio buffers. AL Error: " + errorCode); + } + } + alSourcei(this.sourceID, AL_LOOPING, AL_FALSE); + setPan(this.pan, this.volume); + + boolean filled = false; // Check if there's anything to actually play. + for (int i = 0; i < bufferCount; i++) { + final int bufferID = this.buffers.get(i); + if (!fill(bufferID)) { + break; + } + filled = true; + alSourceQueueBuffers(this.sourceID, bufferID); + } + if (!filled && (this.onCompletionListener != null)) { + this.onCompletionListener.onCompletion(this); + } + + if (alGetError() != AL_NO_ERROR) { + stop(); + return; + } + } + if (!this.isPlaying) { + alSourcePlay(this.sourceID); + this.isPlaying = true; + } + } + + @Override + public void stop() { + if (this.audio.noDevice) { + return; + } + if (this.sourceID == -1) { + return; + } + this.audio.music.removeValue(this, true); + reset(); + this.audio.freeSource(this.sourceID); + this.sourceID = -1; + this.renderedSeconds = 0; + this.renderedSecondsQueue.clear(); + this.isPlaying = false; + } + + @Override + public void pause() { + if (this.audio.noDevice) { + return; + } + if (this.sourceID != -1) { + alSourcePause(this.sourceID); + } + this.isPlaying = false; + } + + @Override + public boolean isPlaying() { + if (this.audio.noDevice) { + return false; + } + if (this.sourceID == -1) { + return false; + } + return this.isPlaying; + } + + @Override + public void setLooping(final boolean isLooping) { + this.isLooping = isLooping; + } + + @Override + public boolean isLooping() { + return this.isLooping; + } + + @Override + public void setVolume(final float volume) { + this.volume = volume; + if (this.audio.noDevice) { + return; + } + if (this.sourceID != -1) { + alSourcef(this.sourceID, AL_GAIN, volume); + } + } + + @Override + public float getVolume() { + return this.volume; + } + + @Override + public void setPan(final float pan, final float volume) { + this.volume = volume; + this.pan = pan; + if (this.audio.noDevice) { + return; + } + if (this.sourceID == -1) { + return; + } + alSource3f(this.sourceID, AL_POSITION, MathUtils.cos(((pan - 1) * MathUtils.PI) / 2), 0, + MathUtils.sin(((pan + 1) * MathUtils.PI) / 2)); + alSourcef(this.sourceID, AL_GAIN, volume); + } + + @Override + public void setPosition(final float position) { + if (this.audio.noDevice) { + return; + } + if (this.sourceID == -1) { + return; + } + final boolean wasPlaying = this.isPlaying; + this.isPlaying = false; + alSourceStop(this.sourceID); + alSourceUnqueueBuffers(this.sourceID, this.buffers); + while (this.renderedSecondsQueue.size > 0) { + this.renderedSeconds = this.renderedSecondsQueue.pop(); + } + if (position <= this.renderedSeconds) { + reset(); + this.renderedSeconds = 0; + } + while (this.renderedSeconds < (position - this.maxSecondsPerBuffer)) { + if (read(tempBytes) <= 0) { + break; + } + this.renderedSeconds += this.maxSecondsPerBuffer; + } + this.renderedSecondsQueue.add(this.renderedSeconds); + boolean filled = false; + for (int i = 0; i < bufferCount; i++) { + final int bufferID = this.buffers.get(i); + if (!fill(bufferID)) { + break; + } + filled = true; + alSourceQueueBuffers(this.sourceID, bufferID); + } + this.renderedSecondsQueue.pop(); + if (!filled) { + stop(); + if (this.onCompletionListener != null) { + this.onCompletionListener.onCompletion(this); + } + } + alSourcef(this.sourceID, AL11.AL_SEC_OFFSET, position - this.renderedSeconds); + if (wasPlaying) { + alSourcePlay(this.sourceID); + this.isPlaying = true; + } + } + + @Override + public float getPosition() { + if (this.audio.noDevice) { + return 0; + } + if (this.sourceID == -1) { + return 0; + } + return this.renderedSeconds + alGetSourcef(this.sourceID, AL11.AL_SEC_OFFSET); + } + + /** + * Fills as much of the buffer as possible and returns the number of bytes + * filled. Returns <= 0 to indicate the end of the stream. + */ + abstract public int read(byte[] buffer); + + /** Resets the stream to the beginning. */ + abstract public void reset(); + + /** + * By default, does just the same as reset(). Used to add special behaviour in + * Ogg.Music. + */ + protected void loop() { + reset(); + } + + public int getChannels() { + return this.format == AL_FORMAT_STEREO16 ? 2 : 1; + } + + public int getRate() { + return this.sampleRate; + } + + public void update() { + if (this.audio.noDevice) { + return; + } + if (this.sourceID == -1) { + return; + } + + boolean end = false; + int buffers = alGetSourcei(this.sourceID, AL_BUFFERS_PROCESSED); + while (buffers-- > 0) { + final int bufferID = alSourceUnqueueBuffers(this.sourceID); + if (bufferID == AL_INVALID_VALUE) { + break; + } + this.renderedSeconds = this.renderedSecondsQueue.pop(); + if (end) { + continue; + } + if (fill(bufferID)) { + alSourceQueueBuffers(this.sourceID, bufferID); + } + else { + end = true; + } + } + if (end && (alGetSourcei(this.sourceID, AL_BUFFERS_QUEUED) == 0)) { + stop(); + if (this.onCompletionListener != null) { + this.onCompletionListener.onCompletion(this); + } + } + + // A buffer underflow will cause the source to stop. + if (this.isPlaying && (alGetSourcei(this.sourceID, AL_SOURCE_STATE) != AL_PLAYING)) { + alSourcePlay(this.sourceID); + } + } + + private boolean fill(final int bufferID) { + tempBuffer.clear(); + int length = read(tempBytes); + if (length <= 0) { + if (this.isLooping) { + loop(); + length = read(tempBytes); + if (length <= 0) { + return false; + } + if (this.renderedSecondsQueue.size > 0) { + this.renderedSecondsQueue.set(0, 0); + } + } + else { + return false; + } + } + final float previousLoadedSeconds = this.renderedSecondsQueue.size > 0 ? this.renderedSecondsQueue.first() : 0; + final float currentBufferSeconds = (this.maxSecondsPerBuffer * length) / bufferSize; + this.renderedSecondsQueue.insert(0, previousLoadedSeconds + currentBufferSeconds); + + tempBuffer.put(tempBytes, 0, length).flip(); + alBufferData(bufferID, this.format, tempBuffer, this.sampleRate); + return true; + } + + @Override + public void dispose() { + stop(); + if (this.audio.noDevice) { + return; + } + if (this.buffers == null) { + return; + } + alDeleteBuffers(this.buffers); + this.buffers = null; + this.onCompletionListener = null; + } + + @Override + public void setOnCompletionListener(final OnCompletionListener listener) { + this.onCompletionListener = listener; + } + + public int getSourceId() { + return this.sourceID; + } +} diff --git a/desktop/src/com/etheller/warsmash/audio/OpenALSound.java b/desktop/src/com/etheller/warsmash/audio/OpenALSound.java new file mode 100644 index 0000000..0a27681 --- /dev/null +++ b/desktop/src/com/etheller/warsmash/audio/OpenALSound.java @@ -0,0 +1,249 @@ +/******************************************************************************* + * Copyright 2011 See AUTHORS file. + * + * 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 + * + * http://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. + ******************************************************************************/ + +package com.etheller.warsmash.audio; + +import static org.lwjgl.openal.AL10.AL_BUFFER; +import static org.lwjgl.openal.AL10.AL_FALSE; +import static org.lwjgl.openal.AL10.AL_FORMAT_MONO16; +import static org.lwjgl.openal.AL10.AL_FORMAT_STEREO16; +import static org.lwjgl.openal.AL10.AL_GAIN; +import static org.lwjgl.openal.AL10.AL_LOOPING; +import static org.lwjgl.openal.AL10.AL_TRUE; +import static org.lwjgl.openal.AL10.alBufferData; +import static org.lwjgl.openal.AL10.alDeleteBuffers; +import static org.lwjgl.openal.AL10.alGenBuffers; +import static org.lwjgl.openal.AL10.alSourcePlay; +import static org.lwjgl.openal.AL10.alSourcef; +import static org.lwjgl.openal.AL10.alSourcei; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import com.badlogic.gdx.audio.Sound; + +/** @author Nathan Sweet */ +public class OpenALSound implements Sound { + private int bufferID = -1; + private final OpenALAudio audio; + private float duration; + + public OpenALSound(final OpenALAudio audio) { + this.audio = audio; + } + + void setup(final byte[] pcm, final int channels, final int sampleRate) { + final int bytes = pcm.length - (pcm.length % (channels > 1 ? 4 : 2)); + final int samples = bytes / (2 * channels); + this.duration = samples / (float) sampleRate; + + final ByteBuffer buffer = ByteBuffer.allocateDirect(bytes); + buffer.order(ByteOrder.nativeOrder()); + buffer.put(pcm, 0, bytes); + buffer.flip(); + + if (this.bufferID == -1) { + this.bufferID = alGenBuffers(); + alBufferData(this.bufferID, channels > 1 ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16, buffer.asShortBuffer(), + sampleRate); + } + } + + @Override + public long play() { + return play(1); + } + + @Override + public long play(final float volume) { + if (this.audio.noDevice) { + return 0; + } + int sourceID = this.audio.obtainSource(false); + if (sourceID == -1) { + // Attempt to recover by stopping the least recently played sound + this.audio.retain(this, true); + sourceID = this.audio.obtainSource(false); + } + else { + this.audio.retain(this, false); + } + // In case it still didn't work + if (sourceID == -1) { + return -1; + } + final long soundId = this.audio.getSoundId(sourceID); + alSourcei(sourceID, AL_BUFFER, this.bufferID); + alSourcei(sourceID, AL_LOOPING, AL_FALSE); + alSourcef(sourceID, AL_GAIN, volume); + alSourcePlay(sourceID); + return soundId; + } + + @Override + public long loop() { + return loop(1); + } + + @Override + public long loop(final float volume) { + if (this.audio.noDevice) { + return 0; + } + final int sourceID = this.audio.obtainSource(false); + if (sourceID == -1) { + return -1; + } + final long soundId = this.audio.getSoundId(sourceID); + alSourcei(sourceID, AL_BUFFER, this.bufferID); + alSourcei(sourceID, AL_LOOPING, AL_TRUE); + alSourcef(sourceID, AL_GAIN, volume); + alSourcePlay(sourceID); + return soundId; + } + + @Override + public void stop() { + if (this.audio.noDevice) { + return; + } + this.audio.stopSourcesWithBuffer(this.bufferID); + } + + @Override + public void dispose() { + if (this.audio.noDevice) { + return; + } + if (this.bufferID == -1) { + return; + } + this.audio.freeBuffer(this.bufferID); + alDeleteBuffers(this.bufferID); + this.bufferID = -1; + this.audio.forget(this); + } + + @Override + public void stop(final long soundId) { + if (this.audio.noDevice) { + return; + } + this.audio.stopSound(soundId); + } + + @Override + public void pause() { + if (this.audio.noDevice) { + return; + } + this.audio.pauseSourcesWithBuffer(this.bufferID); + } + + @Override + public void pause(final long soundId) { + if (this.audio.noDevice) { + return; + } + this.audio.pauseSound(soundId); + } + + @Override + public void resume() { + if (this.audio.noDevice) { + return; + } + this.audio.resumeSourcesWithBuffer(this.bufferID); + } + + @Override + public void resume(final long soundId) { + if (this.audio.noDevice) { + return; + } + this.audio.resumeSound(soundId); + } + + @Override + public void setPitch(final long soundId, final float pitch) { + if (this.audio.noDevice) { + return; + } + this.audio.setSoundPitch(soundId, pitch); + } + + @Override + public void setVolume(final long soundId, final float volume) { + if (this.audio.noDevice) { + return; + } + this.audio.setSoundGain(soundId, volume); + } + + @Override + public void setLooping(final long soundId, final boolean looping) { + if (this.audio.noDevice) { + return; + } + this.audio.setSoundLooping(soundId, looping); + } + + @Override + public void setPan(final long soundId, final float pan, final float volume) { + if (this.audio.noDevice) { + return; + } + this.audio.setSoundPan(soundId, pan, volume); + } + + public void setPosition(final long soundId, final float x, final float y, final float z, final boolean is3DSound, + final float maxDistance, final float refDistance) { + if (this.audio.noDevice) { + return; + } + this.audio.setSoundPosition(soundId, x, y, z, is3DSound, maxDistance, refDistance); + } + + @Override + public long play(final float volume, final float pitch, final float pan) { + final long id = play(); + setPitch(id, pitch); + setPan(id, pan, volume); + return id; + } + + public long play(final float volume, final float pitch, final float x, final float y, final float z, + final boolean is3DSound, final float maxDistance, final float refDistance, final boolean looping) { + final long id = looping ? loop() : play(); + setPitch(id, pitch); + setVolume(id, volume); + setPosition(id, x, y, z, is3DSound, maxDistance, refDistance); + return id; + } + + @Override + public long loop(final float volume, final float pitch, final float pan) { + final long id = loop(); + setPitch(id, pitch); + setPan(id, pan, volume); + return id; + } + + /** Returns the length of the sound in seconds. */ + public float duration() { + return this.duration; + } +} diff --git a/desktop/src/com/etheller/warsmash/audio/Wav.java b/desktop/src/com/etheller/warsmash/audio/Wav.java new file mode 100644 index 0000000..7c8b6e3 --- /dev/null +++ b/desktop/src/com/etheller/warsmash/audio/Wav.java @@ -0,0 +1,201 @@ +/******************************************************************************* + * Copyright 2011 See AUTHORS file. + * + * 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 + * + * http://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. + ******************************************************************************/ + +package com.etheller.warsmash.audio; + +import java.io.EOFException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.utils.GdxRuntimeException; +import com.badlogic.gdx.utils.StreamUtils; + +public class Wav { + static public class Music extends OpenALMusic { + private WavInputStream input; + + public Music(final OpenALAudio audio, final FileHandle file) { + super(audio, file); + this.input = new WavInputStream(file); + if (audio.noDevice) { + return; + } + setup(this.input.channels, this.input.sampleRate); + } + + @Override + public int read(final byte[] buffer) { + if (this.input == null) { + this.input = new WavInputStream(this.file); + setup(this.input.channels, this.input.sampleRate); + } + try { + return this.input.read(buffer); + } + catch (final IOException ex) { + throw new GdxRuntimeException("Error reading WAV file: " + this.file, ex); + } + } + + @Override + public void reset() { + StreamUtils.closeQuietly(this.input); + this.input = null; + } + } + + static public class Sound extends OpenALSound { + public Sound(final OpenALAudio audio, final FileHandle file) { + super(audio); + if (audio.noDevice) { + return; + } + + WavInputStream input = null; + try { + input = new WavInputStream(file); + setup(StreamUtils.copyStreamToByteArray(input, input.dataRemaining), input.channels, input.sampleRate); + } + catch (final IOException ex) { + throw new GdxRuntimeException("Error reading WAV file: " + file, ex); + } + finally { + StreamUtils.closeQuietly(input); + } + } + } + + /** @author Nathan Sweet */ + static public class WavInputStream extends FilterInputStream { + + public int channels, sampleRate, dataRemaining; + + public WavInputStream(final FileHandle file) { + this(file.read(), file); + } + + public WavInputStream(final InputStream stream, final Object loggableFile) { + super(stream); + try { + if ((read() != 'R') || (read() != 'I') || (read() != 'F') || (read() != 'F')) { + throw new GdxRuntimeException("RIFF header not found: " + loggableFile); + } + + skipFully(4); + + if ((read() != 'W') || (read() != 'A') || (read() != 'V') || (read() != 'E')) { + throw new GdxRuntimeException("Invalid wave file header: " + loggableFile); + } + + final int fmtChunkLength = seekToChunk('f', 'm', 't', ' '); + + // http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html + // http://soundfile.sapp.org/doc/WaveFormat/ + final int type = (read() & 0xff) | ((read() & 0xff) << 8); + if (type != 1) { + String name; + switch (type) { + case 0x0002: + name = "ADPCM"; + break; + case 0x0003: + name = "IEEE float"; + break; + case 0x0006: + name = "8-bit ITU-T G.711 A-law"; + break; + case 0x0007: + name = "8-bit ITU-T G.711 u-law"; + break; + case 0xFFFE: + name = "Extensible"; + break; + default: + name = "Unknown"; + } + throw new GdxRuntimeException( + "WAV files must be PCM, unsupported format: " + name + " (" + type + ")"); + } + + this.channels = (read() & 0xff) | ((read() & 0xff) << 8); + if ((this.channels != 1) && (this.channels != 2)) { + throw new GdxRuntimeException("WAV files must have 1 or 2 channels: " + this.channels); + } + + this.sampleRate = (read() & 0xff) | ((read() & 0xff) << 8) | ((read() & 0xff) << 16) + | ((read() & 0xff) << 24); + + skipFully(6); + + final int bitsPerSample = (read() & 0xff) | ((read() & 0xff) << 8); + if (bitsPerSample != 16) { + throw new GdxRuntimeException("WAV files must have 16 bits per sample: " + bitsPerSample); + } + + skipFully(fmtChunkLength - 16); + + this.dataRemaining = seekToChunk('d', 'a', 't', 'a'); + } + catch (final Throwable ex) { + StreamUtils.closeQuietly(this); + throw new GdxRuntimeException("Error reading WAV file: " + loggableFile, ex); + } + } + + private int seekToChunk(final char c1, final char c2, final char c3, final char c4) throws IOException { + while (true) { + boolean found = read() == c1; + found &= read() == c2; + found &= read() == c3; + found &= read() == c4; + final int chunkLength = (read() & 0xff) | ((read() & 0xff) << 8) | ((read() & 0xff) << 16) + | ((read() & 0xff) << 24); + if (chunkLength == -1) { + throw new IOException("Chunk not found: " + c1 + c2 + c3 + c4); + } + if (found) { + return chunkLength; + } + skipFully(chunkLength); + } + } + + private void skipFully(int count) throws IOException { + while (count > 0) { + final long skipped = this.in.skip(count); + if (skipped <= 0) { + throw new EOFException("Unable to skip."); + } + count -= skipped; + } + } + + @Override + public int read(final byte[] buffer) throws IOException { + if (this.dataRemaining == 0) { + return -1; + } + final int length = Math.min(super.read(buffer), this.dataRemaining); + if (length == -1) { + return -1; + } + this.dataRemaining -= length; + return length; + } + } +} diff --git a/desktop/src/com/etheller/warsmash/desktop/DesktopLauncher.java b/desktop/src/com/etheller/warsmash/desktop/DesktopLauncher.java new file mode 100644 index 0000000..01b5234 --- /dev/null +++ b/desktop/src/com/etheller/warsmash/desktop/DesktopLauncher.java @@ -0,0 +1,215 @@ +package com.etheller.warsmash.desktop; + +import static org.lwjgl.openal.AL10.AL_ORIENTATION; +import static org.lwjgl.openal.AL10.AL_POSITION; +import static org.lwjgl.openal.AL10.alListener; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.FloatBuffer; + +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL31; +import org.lwjgl.opengl.GL32; +import org.lwjgl.opengl.GL33; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Graphics.DisplayMode; +import com.badlogic.gdx.audio.Sound; +import com.badlogic.gdx.backends.lwjgl.LwjglApplication; +import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration; +import com.badlogic.gdx.backends.lwjgl.LwjglNativesLoader; +import com.etheller.warsmash.WarsmashGdxMenuScreen; +import com.etheller.warsmash.WarsmashGdxMultiScreenGame; +import com.etheller.warsmash.audio.OpenALSound; +import com.etheller.warsmash.units.DataTable; +import com.etheller.warsmash.util.StringBundle; +import com.etheller.warsmash.viewer5.AudioContext; +import com.etheller.warsmash.viewer5.AudioContext.Listener; +import com.etheller.warsmash.viewer5.AudioDestination; +import com.etheller.warsmash.viewer5.gl.ANGLEInstancedArrays; +import com.etheller.warsmash.viewer5.gl.AudioExtension; +import com.etheller.warsmash.viewer5.gl.DynamicShadowExtension; +import com.etheller.warsmash.viewer5.gl.Extensions; +import com.etheller.warsmash.viewer5.gl.WireframeExtension; + +public class DesktopLauncher { + public static void main(final String[] arg) { + final LwjglApplicationConfiguration config = new LwjglApplicationConfiguration(); + config.useGL30 = true; + config.gles30ContextMajorVersion = 3; + config.gles30ContextMinorVersion = 3; + // config.samples = 16; + config.vSyncEnabled = false; + config.foregroundFPS = 0; + config.backgroundFPS = 0; + final DisplayMode desktopDisplayMode = LwjglApplicationConfiguration.getDesktopDisplayMode(); + config.width = desktopDisplayMode.width; + config.height = desktopDisplayMode.height; + String fileToLoad = null; + config.fullscreen = true; + for (int argIndex = 0; argIndex < arg.length; argIndex++) { + if ("-windowed".equals(arg[argIndex])) { + config.fullscreen = false; + } + else if ((arg.length > (argIndex + 1)) && "-loadfile".equals(arg[argIndex])) { + argIndex++; + fileToLoad = arg[argIndex]; + } + } + loadExtensions(); + final DataTable warsmashIni = loadWarsmashIni(); + final WarsmashGdxMultiScreenGame warsmashGdxMultiScreenGame = new WarsmashGdxMultiScreenGame(); + new LwjglApplication(warsmashGdxMultiScreenGame, config); + final String finalFileToLoad = fileToLoad; + Gdx.app.postRunnable(new Runnable() { + @Override + public void run() { + final WarsmashGdxMenuScreen menuScreen = new WarsmashGdxMenuScreen(warsmashIni, + warsmashGdxMultiScreenGame); + warsmashGdxMultiScreenGame.setScreen(menuScreen); + if (finalFileToLoad != null) { + menuScreen.startMap(finalFileToLoad); + } + } + }); + } + + public static DataTable loadWarsmashIni() { + final DataTable warsmashIni = new DataTable(StringBundle.EMPTY); + try (FileInputStream warsmashIniInputStream = new FileInputStream("warsmash.ini")) { + warsmashIni.readTXT(warsmashIniInputStream, true); + } + catch (final FileNotFoundException e) { + throw new RuntimeException(e); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + return warsmashIni; + } + + public static void loadExtensions() { + LwjglNativesLoader.load(); + Extensions.angleInstancedArrays = new ANGLEInstancedArrays() { + @Override + public void glVertexAttribDivisorANGLE(final int index, final int divisor) { + GL33.glVertexAttribDivisor(index, divisor); + } + + @Override + public void glDrawElementsInstancedANGLE(final int mode, final int count, final int type, + final int indicesOffset, final int instanceCount) { + GL31.glDrawElementsInstanced(mode, count, type, indicesOffset, instanceCount); + } + + @Override + public void glDrawArraysInstancedANGLE(final int mode, final int first, final int count, + final int instanceCount) { + GL31.glDrawArraysInstanced(mode, first, count, instanceCount); + } + }; + Extensions.dynamicShadowExtension = new DynamicShadowExtension() { + @Override + public void glFramebufferTexture(final int target, final int attachment, final int texture, + final int level) { + GL32.glFramebufferTexture(target, attachment, texture, level); + } + + @Override + public void glDrawBuffer(final int mode) { + GL11.glDrawBuffer(mode); + } + }; + Extensions.wireframeExtension = new WireframeExtension() { + @Override + public void glPolygonMode(final int face, final int mode) { + GL11.glPolygonMode(face, mode); + } + }; + Extensions.audio = new AudioExtension() { + final FloatBuffer orientation = (FloatBuffer) BufferUtils.createFloatBuffer(6).clear(); + final FloatBuffer position = (FloatBuffer) BufferUtils.createFloatBuffer(3).clear(); + + @Override + public float getDuration(final Sound sound) { + if (sound == null) { + return 1; + } + return ((OpenALSound) sound).duration(); + } + + @Override + public void play(final Sound buffer, final float volume, final float pitch, final float x, final float y, + final float z, final boolean is3dSound, final float maxDistance, final float refDistance, + final boolean looping) { + ((OpenALSound) buffer).play(volume, pitch, x, y, z, is3dSound, maxDistance, refDistance, looping); + } + + @Override + public AudioContext createContext(final boolean world) { + Listener listener; + if (world) { + listener = new Listener() { + private float x; + private float y; + private float z; + + @Override + public void setPosition(final float x, final float y, final float z) { + this.x = x; + this.y = y; + this.z = z; + position.put(0, x); + position.put(1, y); + position.put(2, z); + alListener(AL_POSITION, position); + } + + @Override + public float getX() { + return this.x; + } + + @Override + public float getY() { + return this.y; + } + + @Override + public float getZ() { + return this.z; + } + + @Override + public void setOrientation(final float forwardX, final float forwardY, final float forwardZ, + final float upX, final float upY, final float upZ) { + orientation.put(0, forwardX); + orientation.put(1, forwardY); + orientation.put(2, forwardZ); + orientation.put(3, upX); + orientation.put(4, upY); + orientation.put(5, upZ); + alListener(AL_ORIENTATION, orientation); + } + + @Override + public boolean is3DSupported() { + return true; + } + }; + } + else { + listener = Listener.DO_NOTHING; + } + + return new AudioContext(listener, new AudioDestination() { + }); + } + }; + Extensions.GL_LINE = GL11.GL_LINE; + Extensions.GL_FILL = GL11.GL_FILL; + } +} diff --git a/desktop/src/com/etheller/warsmash/desktop/editor/mdx/MdxEditorMain.java b/desktop/src/com/etheller/warsmash/desktop/editor/mdx/MdxEditorMain.java new file mode 100644 index 0000000..ae7430b --- /dev/null +++ b/desktop/src/com/etheller/warsmash/desktop/editor/mdx/MdxEditorMain.java @@ -0,0 +1,33 @@ +package com.etheller.warsmash.desktop.editor.mdx; + +import javax.swing.SwingUtilities; +import javax.swing.UIManager; + +import com.etheller.warsmash.desktop.DesktopLauncher; +import com.etheller.warsmash.desktop.editor.mdx.ui.YseraFrame; +import com.etheller.warsmash.units.DataTable; + +public class MdxEditorMain { + + public static void main(final String[] args) { + DesktopLauncher.loadExtensions(); + + try { + // UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel"); + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + catch (final Exception exc) { + } + + final DataTable warsmashIni = DesktopLauncher.loadWarsmashIni(); + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + final YseraFrame frame = new YseraFrame(warsmashIni); + frame.setVisible(true); + } + }); + } + +} diff --git a/desktop/src/com/etheller/warsmash/desktop/editor/mdx/listeners/YseraGUIListener.java b/desktop/src/com/etheller/warsmash/desktop/editor/mdx/listeners/YseraGUIListener.java new file mode 100644 index 0000000..7255d28 --- /dev/null +++ b/desktop/src/com/etheller/warsmash/desktop/editor/mdx/listeners/YseraGUIListener.java @@ -0,0 +1,29 @@ +package com.etheller.warsmash.desktop.editor.mdx.listeners; + +import com.etheller.warsmash.util.SubscriberSetNotifier; +import com.hiveworkshop.rms.parsers.mdlx.MdlxModel; + +public interface YseraGUIListener { + void openModel(MdlxModel model); + + // probably repaint + void stateChanged(); + + class YseraGUINotifier extends SubscriberSetNotifier implements YseraGUIListener { + + @Override + public void openModel(final MdlxModel model) { + for (final YseraGUIListener listener : set) { + listener.openModel(model); + } + } + + @Override + public void stateChanged() { + for (final YseraGUIListener listener : set) { + listener.stateChanged(); + } + } + + } +} diff --git a/desktop/src/com/etheller/warsmash/desktop/editor/mdx/ui/AnimationControllerFrame.java b/desktop/src/com/etheller/warsmash/desktop/editor/mdx/ui/AnimationControllerFrame.java new file mode 100644 index 0000000..87dba1f --- /dev/null +++ b/desktop/src/com/etheller/warsmash/desktop/editor/mdx/ui/AnimationControllerFrame.java @@ -0,0 +1,30 @@ +package com.etheller.warsmash.desktop.editor.mdx.ui; + +import javax.swing.JFrame; +import javax.swing.WindowConstants; + +import com.etheller.warsmash.WarsmashPreviewApplication; +import com.etheller.warsmash.desktop.editor.mdx.listeners.YseraGUIListener; +import com.hiveworkshop.rms.parsers.mdlx.MdlxModel; + +public class AnimationControllerFrame extends JFrame implements YseraGUIListener { + private final AnimationControllerPanel animationControllerPanel; + + public AnimationControllerFrame(final WarsmashPreviewApplication warsmashPreviewApplication) { + super("Animation Controller"); + this.animationControllerPanel = new AnimationControllerPanel(warsmashPreviewApplication); + setContentPane(this.animationControllerPanel); + setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); + pack(); + } + + @Override + public void openModel(final MdlxModel model) { + this.animationControllerPanel.openModel(model); + } + + @Override + public void stateChanged() { + this.animationControllerPanel.stateChanged(); + } +} diff --git a/desktop/src/com/etheller/warsmash/desktop/editor/mdx/ui/AnimationControllerPanel.java b/desktop/src/com/etheller/warsmash/desktop/editor/mdx/ui/AnimationControllerPanel.java new file mode 100644 index 0000000..abdd053 --- /dev/null +++ b/desktop/src/com/etheller/warsmash/desktop/editor/mdx/ui/AnimationControllerPanel.java @@ -0,0 +1,210 @@ +package com.etheller.warsmash.desktop.editor.mdx.ui; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; + +import javax.swing.ButtonGroup; +import javax.swing.DefaultComboBoxModel; +import javax.swing.GroupLayout; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JSlider; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.plaf.basic.BasicComboBoxRenderer; + +import com.etheller.warsmash.WarsmashPreviewApplication; +import com.etheller.warsmash.desktop.editor.mdx.listeners.YseraGUIListener; +import com.etheller.warsmash.viewer5.handlers.mdx.MdxComplexInstance; +import com.etheller.warsmash.viewer5.handlers.mdx.SequenceLoopMode; +import com.hiveworkshop.rms.parsers.mdlx.MdlxModel; +import com.hiveworkshop.rms.parsers.mdlx.MdlxSequence; + +public class AnimationControllerPanel extends JPanel implements YseraGUIListener { + private final WarsmashPreviewApplication previewApplication; + private final DefaultComboBoxModel animations; + private final JComboBox animationBox; + private MdlxModel model; + private JRadioButton defaultLoopButton; + private JRadioButton alwaysLoopButton; + private JRadioButton neverLoopButton; + private JSlider speedSlider; + private JLabel speedSliderLabel; + + public AnimationControllerPanel(final WarsmashPreviewApplication previewApplication) { + this.previewApplication = previewApplication; + + this.animations = new DefaultComboBoxModel<>(); + repopulateSequenceList(); + this.animationBox = new JComboBox<>(this.animations); + this.animationBox.setRenderer(new BasicComboBoxRenderer() { + @Override + public Component getListCellRendererComponent(final JList list, final Object value, final int index, + final boolean isSelected, final boolean cellHasFocus) { + Object display = value == null ? "(Unanimated)" : ((MdlxSequence) value).getName(); + if ((value != null) && (AnimationControllerPanel.this.model != null)) { + display = "(" + AnimationControllerPanel.this.model.getSequences().indexOf(value) + ") " + display; + } + return super.getListCellRendererComponent(list, display, index, isSelected, cellHasFocus); + } + }); + this.animationBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(final ActionEvent e) { + update(true); + } + }); + this.animationBox.setMaximumSize(new Dimension(99999999, 35)); + this.animationBox.setFocusable(true); + this.animationBox.addMouseWheelListener(new MouseWheelListener() { + @Override + public void mouseWheelMoved(final MouseWheelEvent e) { + final int wheelRotation = e.getWheelRotation(); + int previousSelectedIndex = AnimationControllerPanel.this.animationBox.getSelectedIndex(); + if (previousSelectedIndex < 0) { + previousSelectedIndex = 0; + } + int newIndex = previousSelectedIndex + wheelRotation; + if (newIndex > (AnimationControllerPanel.this.animations.getSize() - 1)) { + newIndex = AnimationControllerPanel.this.animations.getSize() - 1; + } + else if (newIndex < 0) { + newIndex = 0; + } + if (newIndex != previousSelectedIndex) { + AnimationControllerPanel.this.animationBox.setSelectedIndex(newIndex); + // animationBox.setSelectedIndex( + // ((newIndex % animations.getSize()) + animations.getSize()) % + // animations.getSize()); + } + } + }); + this.speedSlider = new JSlider(0, 100, 50); + this.speedSliderLabel = new JLabel("Speed: 100%"); + this.speedSlider.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(final ChangeEvent e) { + update(false); + } + }); + + final JButton playAnimationButton = new JButton("Play Animation"); + final ActionListener playAnimationActionListener = new ActionListener() { + @Override + public void actionPerformed(final ActionEvent e) { + update(true); + } + }; + playAnimationButton.addActionListener(playAnimationActionListener); + + this.defaultLoopButton = new JRadioButton("Default Loop"); + this.alwaysLoopButton = new JRadioButton("Always Loop"); + this.neverLoopButton = new JRadioButton("Never Loop"); + + final ButtonGroup buttonGroup = new ButtonGroup(); + buttonGroup.add(this.defaultLoopButton); + buttonGroup.add(this.alwaysLoopButton); + buttonGroup.add(this.neverLoopButton); + final ActionListener setLoopTypeActionListener = new ActionListener() { + @Override + public void actionPerformed(final ActionEvent e) { + update(true); + } + }; + this.defaultLoopButton.addActionListener(setLoopTypeActionListener); + this.alwaysLoopButton.addActionListener(setLoopTypeActionListener); + this.neverLoopButton.addActionListener(setLoopTypeActionListener); + + final JLabel levelOfDetailLabel = new JLabel("Level of Detail"); + final JSpinner levelOfDetailSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 5, 1)); + levelOfDetailSpinner.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(final ChangeEvent e) { +// listener.setLevelOfDetail(((Number) levelOfDetailSpinner.getValue()).intValue()); + } + }); + levelOfDetailSpinner.setMaximumSize(new Dimension(99999, 25)); + levelOfDetailLabel.setVisible(false); + levelOfDetailSpinner.setVisible(false); + + final GroupLayout groupLayout = new GroupLayout(this); + + groupLayout.setHorizontalGroup(groupLayout.createParallelGroup().addComponent(this.animationBox) + .addGroup(groupLayout.createSequentialGroup().addGap(8) + .addGroup(groupLayout.createParallelGroup().addComponent(playAnimationButton) + .addComponent(this.defaultLoopButton).addComponent(this.alwaysLoopButton) + .addComponent(this.neverLoopButton).addComponent(this.speedSliderLabel) + .addComponent(this.speedSlider).addComponent(levelOfDetailLabel) + .addComponent(levelOfDetailSpinner)) + .addGap(8) + + )); + groupLayout.setVerticalGroup(groupLayout.createSequentialGroup().addComponent(this.animationBox).addGap(32) + .addComponent(playAnimationButton).addGap(16).addComponent(this.defaultLoopButton) + .addComponent(this.alwaysLoopButton).addComponent(this.neverLoopButton).addGap(16) + .addComponent(this.speedSliderLabel).addComponent(this.speedSlider).addGap(16) + .addComponent(levelOfDetailLabel).addComponent(levelOfDetailSpinner) + + ); + setLayout(groupLayout); + + this.defaultLoopButton.doClick(); + } + + private void update(final boolean playSequence) { + SequenceLoopMode loopType; + if (this.defaultLoopButton.isSelected()) { + loopType = SequenceLoopMode.MODEL_LOOP; + } + else if (this.alwaysLoopButton.isSelected()) { + loopType = SequenceLoopMode.ALWAYS_LOOP; + } + else if (this.neverLoopButton.isSelected()) { + loopType = SequenceLoopMode.NEVER_LOOP; + } + else { + throw new IllegalStateException(); + } + this.speedSliderLabel.setText("Speed: " + (this.speedSlider.getValue() * 2) + "%"); + final MdxComplexInstance mainInstance = this.previewApplication.getMainInstance(); + if (mainInstance != null) { + mainInstance.setAnimationSpeed(this.speedSlider.getValue() / 50f); + mainInstance.setSequenceLoopMode(loopType); + if (playSequence) { + mainInstance.setSequence(this.animationBox.getSelectedIndex() - 1); + } + } + } + + @Override + public void openModel(final MdlxModel model) { + this.model = model; + + repopulateSequenceList(); + } + + private void repopulateSequenceList() { + this.animations.removeAllElements(); + this.animations.addElement(null); + if (this.model != null) { + for (final MdlxSequence animation : this.model.getSequences()) { + this.animations.addElement(animation); + } + } + } + + @Override + public void stateChanged() { + + } +} diff --git a/desktop/src/com/etheller/warsmash/desktop/editor/mdx/ui/YseraFrame.java b/desktop/src/com/etheller/warsmash/desktop/editor/mdx/ui/YseraFrame.java new file mode 100644 index 0000000..e75c53f --- /dev/null +++ b/desktop/src/com/etheller/warsmash/desktop/editor/mdx/ui/YseraFrame.java @@ -0,0 +1,64 @@ +package com.etheller.warsmash.desktop.editor.mdx.ui; + +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; + +import javax.swing.JFrame; + +import com.badlogic.gdx.Gdx; +import com.etheller.warsmash.WarsmashPreviewApplication; +import com.etheller.warsmash.units.DataTable; + +public class YseraFrame extends JFrame { + public YseraFrame(final DataTable warsmashIni) { + super("Warsmash Model Editor"); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + final WarsmashPreviewApplication warsmashPreviewApplication = new WarsmashPreviewApplication(warsmashIni); + final YseraPanel contentPane = new YseraPanel(warsmashPreviewApplication); + setContentPane(contentPane); +// setIconImage(ImageUtils.getBLPImage(warsmashGdxGame.getCodebase(), +// "ReplaceableTextures\\CommandButtons\\BTNGreenDragon.blp")); + setJMenuBar(contentPane.createJMenuBar(this)); + pack(); + setLocationRelativeTo(null); + + addWindowListener(new WindowListener() { + + @Override + public void windowOpened(final WindowEvent e) { + + } + + @Override + public void windowIconified(final WindowEvent e) { + + } + + @Override + public void windowDeiconified(final WindowEvent e) { + + } + + @Override + public void windowDeactivated(final WindowEvent e) { + + } + + @Override + public void windowClosing(final WindowEvent e) { + Gdx.app.exit(); + } + + @Override + public void windowClosed(final WindowEvent e) { + + } + + @Override + public void windowActivated(final WindowEvent e) { + + } + }); + } + +} diff --git a/desktop/src/com/etheller/warsmash/desktop/editor/mdx/ui/YseraPanel.java b/desktop/src/com/etheller/warsmash/desktop/editor/mdx/ui/YseraPanel.java new file mode 100644 index 0000000..340bf02 --- /dev/null +++ b/desktop/src/com/etheller/warsmash/desktop/editor/mdx/ui/YseraPanel.java @@ -0,0 +1,193 @@ +package com.etheller.warsmash.desktop.editor.mdx.ui; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; + +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.filechooser.FileNameExtensionFilter; + +import org.lwjgl.util.vector.Quaternion; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Input; +import com.badlogic.gdx.InputProcessor; +import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration; +import com.badlogic.gdx.backends.lwjgl.LwjglCanvas; +import com.badlogic.gdx.math.Vector3; +import com.etheller.warsmash.WarsmashPreviewApplication; +import com.etheller.warsmash.desktop.editor.mdx.listeners.YseraGUIListener; +import com.etheller.warsmash.desktop.editor.util.ExceptionPopup; +import com.etheller.warsmash.viewer5.handlers.w3x.camera.PortraitCameraManager; +import com.hiveworkshop.rms.parsers.mdlx.MdlxModel; + +public class YseraPanel extends JPanel { + private static final Quaternion IDENTITY = new Quaternion(); + private final WarsmashPreviewApplication warsmashPreviewApplication; + private final JFileChooser userFileChooser = new JFileChooser(); + private final YseraGUIListener.YseraGUINotifier notifier = new YseraGUIListener.YseraGUINotifier(); + + private MdlxModel model; + + private AnimationControllerFrame animationControllerFrame; + + public YseraPanel(final WarsmashPreviewApplication warsmashPreviewApplication) { + this.warsmashPreviewApplication = warsmashPreviewApplication; + setLayout(new BorderLayout()); + final LwjglApplicationConfiguration config = new LwjglApplicationConfiguration(); + config.useGL30 = true; + config.gles30ContextMajorVersion = 3; + config.gles30ContextMinorVersion = 3; + final LwjglCanvas lwjglCanvas = new LwjglCanvas(warsmashPreviewApplication, config); + add(BorderLayout.CENTER, lwjglCanvas.getCanvas()); + setPreferredSize(new Dimension(640, 480)); + this.userFileChooser + .setFileFilter(new FileNameExtensionFilter("Warcraft III Model or Texture", "mdx", "mdl", "blp")); + + final CameraMouseHandler cameraMouseHandler = new CameraMouseHandler(warsmashPreviewApplication); + Gdx.input.setInputProcessor(cameraMouseHandler); + + } + + public JMenuBar createJMenuBar(final JFrame frame) { + final JMenuBar jMenuBar = new JMenuBar(); + + final JMenu fileMenu = new JMenu("File"); + final JMenuItem openItem = new JMenuItem("Open"); + openItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(final ActionEvent e) { + try { + final int userResult = YseraPanel.this.userFileChooser.showOpenDialog(frame); + if (userResult == JFileChooser.APPROVE_OPTION) { + final File selectedFile = YseraPanel.this.userFileChooser.getSelectedFile(); + if (selectedFile != null) { + YseraPanel.this.model = YseraPanel.this.warsmashPreviewApplication + .loadCustomModel(selectedFile.getPath()); + YseraPanel.this.notifier.openModel(YseraPanel.this.model); + } + } + } + catch (final Exception exc) { + ExceptionPopup.display(exc); + } + } + }); + fileMenu.add(openItem); + jMenuBar.add(fileMenu); + jMenuBar.add(new JMenu("Recent Files")); + jMenuBar.add(new JMenu("Edit")); + jMenuBar.add(new JMenu("View")); + jMenuBar.add(new JMenu("Team Color")); + final JMenu windowMenu = new JMenu("Windows"); + final JMenuItem modelEditorItem = new JMenuItem("Model Editor"); + windowMenu.add(modelEditorItem); + final JMenuItem animationControllerItem = new JMenuItem("Animation Controller"); + animationControllerItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(final ActionEvent e) { + if (YseraPanel.this.animationControllerFrame == null) { + YseraPanel.this.animationControllerFrame = new AnimationControllerFrame( + YseraPanel.this.warsmashPreviewApplication); + YseraPanel.this.notifier.subscribe(YseraPanel.this.animationControllerFrame); + YseraPanel.this.animationControllerFrame.setLocationRelativeTo(frame); + } + YseraPanel.this.animationControllerFrame.setVisible(true); + YseraPanel.this.animationControllerFrame.toFront(); + } + }); + windowMenu.add(animationControllerItem); + jMenuBar.add(windowMenu); + jMenuBar.add(new JMenu("Extras")); + jMenuBar.add(new JMenu("Help")); + + return jMenuBar; + } + + private static final class CameraMouseHandler implements InputProcessor { + private int lastX, lastY; + private final Vector3 screenDimension = new Vector3(); + private int button; + private final WarsmashPreviewApplication warsmashPreviewApplication; + + public CameraMouseHandler(final WarsmashPreviewApplication warsmashPreviewApplication) { + this.warsmashPreviewApplication = warsmashPreviewApplication; + } + + @Override + public boolean keyDown(final int keycode) { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean keyUp(final int keycode) { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean keyTyped(final char character) { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean touchDown(final int screenX, final int screenY, final int pointer, final int button) { + this.lastX = screenX; + this.lastY = screenY; + this.button = button; + return false; + } + + @Override + public boolean touchUp(final int screenX, final int screenY, final int pointer, final int button) { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean touchDragged(final int screenX, final int screenY, final int pointer) { + final int newX = screenX; + final int newY = screenY; + final int dx = newX - this.lastX; + final int dy = newY - this.lastY; + final PortraitCameraManager cameraManager = this.warsmashPreviewApplication.getCameraManager(); + if (this.button == Input.Buttons.RIGHT) { + this.screenDimension.set(-1, 0, 0); + this.screenDimension.unrotate(cameraManager.camera.viewProjectionMatrix); + cameraManager.target.add(this.screenDimension.nor().scl(dx * 5)); + this.screenDimension.set(0, 1, 0); + this.screenDimension.unrotate(cameraManager.camera.viewProjectionMatrix); + cameraManager.target.add(this.screenDimension.nor().scl(dy * 5)); + } + else if (this.button == Input.Buttons.LEFT) { + cameraManager.horizontalAngle -= Math.toRadians(dx); + cameraManager.verticalAngle -= Math.toRadians(dy); + } + this.lastX = newX; + this.lastY = newY; + return false; + } + + @Override + public boolean mouseMoved(final int screenX, final int screenY) { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean scrolled(final int amount) { + final PortraitCameraManager cameraManager = this.warsmashPreviewApplication.getCameraManager(); + cameraManager.distance += amount * 100; + return false; + } + } +} diff --git a/desktop/src/com/etheller/warsmash/desktop/editor/util/ExceptionPopup.java b/desktop/src/com/etheller/warsmash/desktop/editor/util/ExceptionPopup.java new file mode 100644 index 0000000..588414a --- /dev/null +++ b/desktop/src/com/etheller/warsmash/desktop/editor/util/ExceptionPopup.java @@ -0,0 +1,84 @@ +package com.etheller.warsmash.desktop.editor.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; + +import javax.swing.JOptionPane; +import javax.swing.JTextPane; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; + +public class ExceptionPopup { + public static void display(final Throwable e) { + + final JTextPane pane = new JTextPane(); + final OutputStream stream = new OutputStream() { + public void updateStreamWith(final String s) { + final Document doc = pane.getDocument(); + try { + doc.insertString(doc.getLength(), s, null); + } + catch (final BadLocationException e) { + JOptionPane.showMessageDialog(null, "MDL open error popup failed to create info popup."); + e.printStackTrace(); + } + } + + @Override + public void write(final int b) throws IOException { + updateStreamWith(String.valueOf((char) b)); + } + + @Override + public void write(final byte[] b, final int off, final int len) throws IOException { + updateStreamWith(new String(b, off, len)); + } + + @Override + public void write(final byte[] b) throws IOException { + write(b, 0, b.length); + } + }; + final PrintStream ps = new PrintStream(stream); + ps.println("Unknown error occurred:"); + e.printStackTrace(ps); + JOptionPane.showMessageDialog(null, pane); + } + + public static void display(final String s, final Exception e) { + + final JTextPane pane = new JTextPane(); + final OutputStream stream = new OutputStream() { + public void updateStreamWith(final String s) { + final Document doc = pane.getDocument(); + try { + doc.insertString(doc.getLength(), s, null); + } + catch (final BadLocationException e) { + JOptionPane.showMessageDialog(null, "MDL open error popup failed to create info popup."); + e.printStackTrace(); + } + } + + @Override + public void write(final int b) throws IOException { + updateStreamWith(String.valueOf((char) b)); + } + + @Override + public void write(final byte[] b, final int off, final int len) throws IOException { + updateStreamWith(new String(b, off, len)); + } + + @Override + public void write(final byte[] b) throws IOException { + write(b, 0, b.length); + } + }; + final PrintStream ps = new PrintStream(stream); + ps.println(s + ":"); + e.printStackTrace(ps); + JOptionPane.showMessageDialog(null, pane); + } +} diff --git a/desktop/src/com/etheller/warsmash/desktop/util/TerrainView.java b/desktop/src/com/etheller/warsmash/desktop/util/TerrainView.java new file mode 100644 index 0000000..08f50e8 --- /dev/null +++ b/desktop/src/com/etheller/warsmash/desktop/util/TerrainView.java @@ -0,0 +1,36 @@ +package com.etheller.warsmash.desktop.util; + +import java.io.IOException; + +import javax.swing.JFrame; +import javax.swing.JScrollPane; +import javax.swing.WindowConstants; + +import com.etheller.warsmash.WarsmashGdxMapScreen; +import com.etheller.warsmash.datasources.DataSource; +import com.etheller.warsmash.desktop.DesktopLauncher; +import com.etheller.warsmash.parsers.w3x.War3Map; +import com.etheller.warsmash.parsers.w3x.w3e.War3MapW3e; +import com.etheller.warsmash.units.DataTable; + +public class TerrainView { + public static void main(final String[] args) { + final DataTable warsmashIni = DesktopLauncher.loadWarsmashIni(); + final DataSource dataSources = WarsmashGdxMapScreen.parseDataSources(warsmashIni); + final War3Map war3Map = new War3Map(dataSources, warsmashIni.get("Map").getField("FilePath")); + try { + final War3MapW3e environmentFile = war3Map.readEnvironment(); + final TerrainViewPanel terrainViewPanel = new TerrainViewPanel(environmentFile); + + final JFrame frame = new JFrame("TerrainView"); + frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + frame.setContentPane(new JScrollPane(terrainViewPanel)); + frame.setBounds(0, 0, 800, 600); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + catch (final IOException e) { + e.printStackTrace(); + } + } +} diff --git a/desktop/src/com/etheller/warsmash/desktop/util/TerrainViewPanel.java b/desktop/src/com/etheller/warsmash/desktop/util/TerrainViewPanel.java new file mode 100644 index 0000000..d1055c7 --- /dev/null +++ b/desktop/src/com/etheller/warsmash/desktop/util/TerrainViewPanel.java @@ -0,0 +1,93 @@ +package com.etheller.warsmash.desktop.util; + +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +import javax.swing.Box; +import javax.swing.JPanel; + +import com.etheller.warsmash.parsers.w3x.w3e.Corner; +import com.etheller.warsmash.parsers.w3x.w3e.War3MapW3e; + +public class TerrainViewPanel extends JPanel { + private final War3MapW3e environmentFile; + private final Font baseFont; + private final Font biggerFont; + private int cliffMode = 0; + + public TerrainViewPanel(final War3MapW3e environmentFile) { + this.environmentFile = environmentFile; + add(Box.createRigidArea(new Dimension(this.environmentFile.getCorners()[0].length * 32, + this.environmentFile.getCorners().length * 32))); + this.baseFont = getFont(); + this.biggerFont = this.baseFont.deriveFont(24f); + addMouseListener(new MouseListener() { + @Override + public void mouseReleased(final MouseEvent e) { + + } + + @Override + public void mousePressed(final MouseEvent e) { + TerrainViewPanel.this.cliffMode = (TerrainViewPanel.this.cliffMode + 1) % 4; + repaint(); + } + + @Override + public void mouseExited(final MouseEvent e) { + + } + + @Override + public void mouseEntered(final MouseEvent e) { + + } + + @Override + public void mouseClicked(final MouseEvent e) { + + } + }); + } + + @Override + protected void paintComponent(final Graphics g) { + super.paintComponent(g); + final Corner[][] corners = this.environmentFile.getCorners(); + for (int i = 0; i < corners.length; i++) { + final int length = corners[i].length; + for (int j = 0; j < length; j++) { + int base = 0; + final int value; + switch (this.cliffMode) { + case 0: + value = corners[i][j].getRamp(); + break; + case 1: + value = corners[i][j].getLayerHeight(); + base = 2; + break; + case 2: + value = corners[i][j].getGroundTexture(); + break; + case 3: + value = corners[i][j].getCliffTexture(); + break; + default: + value = 0; + break; + } + if (value != base) { + g.setFont(this.biggerFont); + } + else { + g.setFont(this.baseFont); + } + g.drawString(value + "", j * 32, (length - i - 1) * 32); + } + } + } +} diff --git a/desktop/src/io/nayuki/flac/app/DecodeFlacToWav.java b/desktop/src/io/nayuki/flac/app/DecodeFlacToWav.java new file mode 100644 index 0000000..bf47e3f --- /dev/null +++ b/desktop/src/io/nayuki/flac/app/DecodeFlacToWav.java @@ -0,0 +1,141 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.app; + +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Arrays; +import io.nayuki.flac.common.StreamInfo; +import io.nayuki.flac.decode.DataFormatException; +import io.nayuki.flac.decode.FlacDecoder; + + +/** + * Decodes a FLAC file to an uncompressed PCM WAV file. Overwrites output file if already exists. + * Runs silently if successful, otherwise prints error messages to standard error. + *

Usage: java DecodeFlacToWav InFile.flac OutFile.wav

+ *

Requirements on the FLAC file:

+ *
    + *
  • Sample depth is 8, 16, 24, or 32 bits (not 4, 17, 23, etc.)
  • + *
  • Contains no ID3v1 or ID3v2 tags, or other data unrecognized by the FLAC format
  • + *
  • Correct total number of samples (not zero) is stored in stream info block
  • + *
  • Every frame has a correct header, subframes do not overflow the sample depth, + * and other strict checks enforced by this decoder library
  • + *
+ */ +public final class DecodeFlacToWav { + + public static void main(String[] args) throws IOException { + // Handle command line arguments + if (args.length != 2) { + System.err.println("Usage: java DecodeFlacToWav InFile.flac OutFile.wav"); + System.exit(1); + return; + } + File inFile = new File(args[0]); + File outFile = new File(args[1]); + + // Decode input FLAC file + StreamInfo streamInfo; + int[][] samples; + try (FlacDecoder dec = new FlacDecoder(inFile)) { + + // Handle metadata header blocks + while (dec.readAndHandleMetadataBlock() != null); + streamInfo = dec.streamInfo; + if (streamInfo.sampleDepth % 8 != 0) + throw new UnsupportedOperationException("Only whole-byte sample depth supported"); + + // Decode every block + samples = new int[streamInfo.numChannels][(int)streamInfo.numSamples]; + for (int off = 0; ;) { + int len = dec.readAudioBlock(samples, off); + if (len == 0) + break; + off += len; + } + } + + // Check audio MD5 hash + byte[] expectHash = streamInfo.md5Hash; + if (Arrays.equals(expectHash, new byte[16])) + System.err.println("Warning: MD5 hash field was blank"); + else if (!Arrays.equals(StreamInfo.getMd5Hash(samples, streamInfo.sampleDepth), expectHash)) + throw new DataFormatException("MD5 hash check failed"); + // Else the hash check passed + + // Start writing WAV output file + int bytesPerSample = streamInfo.sampleDepth / 8; + try (DataOutputStream out = new DataOutputStream( + new BufferedOutputStream(new FileOutputStream(outFile)))) { + DecodeFlacToWav.out = out; + + // Header chunk + int sampleDataLen = samples[0].length * streamInfo.numChannels * bytesPerSample; + out.writeInt(0x52494646); // "RIFF" + writeLittleInt32(sampleDataLen + 36); + out.writeInt(0x57415645); // "WAVE" + + // Metadata chunk + out.writeInt(0x666D7420); // "fmt " + writeLittleInt32(16); + writeLittleInt16(0x0001); + writeLittleInt16(streamInfo.numChannels); + writeLittleInt32(streamInfo.sampleRate); + writeLittleInt32(streamInfo.sampleRate * streamInfo.numChannels * bytesPerSample); + writeLittleInt16(streamInfo.numChannels * bytesPerSample); + writeLittleInt16(streamInfo.sampleDepth); + + // Audio data chunk ("data") + out.writeInt(0x64617461); // "data" + writeLittleInt32(sampleDataLen); + for (int i = 0; i < samples[0].length; i++) { + for (int j = 0; j < samples.length; j++) { + int val = samples[j][i]; + if (bytesPerSample == 1) + out.write(val + 128); // Convert to unsigned, as per WAV PCM conventions + else { // 2 <= bytesPerSample <= 4 + for (int k = 0; k < bytesPerSample; k++) + out.write(val >>> (k * 8)); // Little endian + } + } + } + } + } + + + // Helper members for writing WAV files + + private static DataOutputStream out; + + private static void writeLittleInt16(int x) throws IOException { + out.writeShort(Integer.reverseBytes(x) >>> 16); + } + + private static void writeLittleInt32(int x) throws IOException { + out.writeInt(Integer.reverseBytes(x)); + } + +} diff --git a/desktop/src/io/nayuki/flac/app/EncodeWavToFlac.java b/desktop/src/io/nayuki/flac/app/EncodeWavToFlac.java new file mode 100644 index 0000000..64fbd95 --- /dev/null +++ b/desktop/src/io/nayuki/flac/app/EncodeWavToFlac.java @@ -0,0 +1,176 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.app; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.nio.charset.StandardCharsets; +import io.nayuki.flac.common.StreamInfo; +import io.nayuki.flac.decode.DataFormatException; +import io.nayuki.flac.encode.BitOutputStream; +import io.nayuki.flac.encode.FlacEncoder; +import io.nayuki.flac.encode.RandomAccessFileOutputStream; +import io.nayuki.flac.encode.SubframeEncoder; + + +/** + * Encodes an uncompressed PCM WAV file to a FLAC file. + * Overwrites the output file if it already exists. + *

Usage: java EncodeWavToFlac InFile.wav OutFile.flac

+ *

Requirements on the WAV file:

+ *
    + *
  • Sample depth is 8, 16, 24, or 32 bits (not 4, 17, 23, etc.)
  • + *
  • Number of channels is between 1 to 8 inclusive
  • + *
  • Sample rate is less than 220 hertz
  • + *
+ */ +public final class EncodeWavToFlac { + + public static void main(String[] args) throws IOException { + // Handle command line arguments + if (args.length != 2) { + System.err.println("Usage: java EncodeWavToFlac InFile.wav OutFile.flac"); + System.exit(1); + return; + } + File inFile = new File(args[0]); + File outFile = new File(args[1]); + + // Read WAV file headers and audio sample data + int[][] samples; + int sampleRate; + int sampleDepth; + try (InputStream in = new BufferedInputStream(new FileInputStream(inFile))) { + // Parse and check WAV header + if (!readString(in, 4).equals("RIFF")) + throw new DataFormatException("Invalid RIFF file header"); + readLittleUint(in, 4); // Remaining data length + if (!readString(in, 4).equals("WAVE")) + throw new DataFormatException("Invalid WAV file header"); + + // Handle the format chunk + if (!readString(in, 4).equals("fmt ")) + throw new DataFormatException("Unrecognized WAV file chunk"); + if (readLittleUint(in, 4) != 16) + throw new DataFormatException("Unsupported WAV file type"); + if (readLittleUint(in, 2) != 0x0001) + throw new DataFormatException("Unsupported WAV file codec"); + int numChannels = readLittleUint(in, 2); + if (numChannels < 0 || numChannels > 8) + throw new RuntimeException("Too many (or few) audio channels"); + sampleRate = readLittleUint(in, 4); + if (sampleRate <= 0 || sampleRate >= (1 << 20)) + throw new RuntimeException("Sample rate too large or invalid"); + int byteRate = readLittleUint(in, 4); + int blockAlign = readLittleUint(in, 2); + sampleDepth = readLittleUint(in, 2); + if (sampleDepth == 0 || sampleDepth > 32 || sampleDepth % 8 != 0) + throw new RuntimeException("Unsupported sample depth"); + int bytesPerSample = sampleDepth / 8; + if (bytesPerSample * numChannels != blockAlign) + throw new RuntimeException("Invalid block align value"); + if (bytesPerSample * numChannels * sampleRate != byteRate) + throw new RuntimeException("Invalid byte rate value"); + + // Handle the data chunk + if (!readString(in, 4).equals("data")) + throw new DataFormatException("Unrecognized WAV file chunk"); + int sampleDataLen = readLittleUint(in, 4); + if (sampleDataLen <= 0 || sampleDataLen % (numChannels * bytesPerSample) != 0) + throw new DataFormatException("Invalid length of audio sample data"); + int numSamples = sampleDataLen / (numChannels * bytesPerSample); + samples = new int[numChannels][numSamples]; + for (int i = 0; i < numSamples; i++) { + for (int ch = 0; ch < numChannels; ch++) { + int val = readLittleUint(in, bytesPerSample); + if (sampleDepth == 8) + val -= 128; + else + val = (val << (32 - sampleDepth)) >> (32 - sampleDepth); + samples[ch][i] = val; + } + } + // Note: There might be chunks after "data", but they can be ignored + } + + // Open output file and encode samples to FLAC + try (RandomAccessFile raf = new RandomAccessFile(outFile, "rw")) { + raf.setLength(0); // Truncate an existing file + BitOutputStream out = new BitOutputStream( + new BufferedOutputStream(new RandomAccessFileOutputStream(raf))); + out.writeInt(32, 0x664C6143); + + // Populate and write the stream info structure + StreamInfo info = new StreamInfo(); + info.sampleRate = sampleRate; + info.numChannels = samples.length; + info.sampleDepth = sampleDepth; + info.numSamples = samples[0].length; + info.md5Hash = StreamInfo.getMd5Hash(samples, sampleDepth); + info.write(true, out); + + // Encode all frames + new FlacEncoder(info, samples, 4096, SubframeEncoder.SearchOptions.SUBSET_BEST, out); + out.flush(); + + // Rewrite the stream info metadata block, which is + // located at a fixed offset in the file by definition + raf.seek(4); + info.write(true, out); + out.flush(); + } + } + + + // Reads len bytes from the given stream and interprets them as a UTF-8 string. + private static String readString(InputStream in, int len) throws IOException { + byte[] temp = new byte[len]; + for (int i = 0; i < temp.length; i++) { + int b = in.read(); + if (b == -1) + throw new EOFException(); + temp[i] = (byte)b; + } + return new String(temp, StandardCharsets.UTF_8); + } + + + // Reads n bytes (0 <= n <= 4) from the given stream, interpreting + // them as an unsigned integer encoded in little endian. + private static int readLittleUint(InputStream in, int n) throws IOException { + int result = 0; + for (int i = 0; i < n; i++) { + int b = in.read(); + if (b == -1) + throw new EOFException(); + result |= b << (i * 8); + } + return result; + } + +} diff --git a/desktop/src/io/nayuki/flac/app/SeekableFlacPlayerGui.java b/desktop/src/io/nayuki/flac/app/SeekableFlacPlayerGui.java new file mode 100644 index 0000000..418b1f4 --- /dev/null +++ b/desktop/src/io/nayuki/flac/app/SeekableFlacPlayerGui.java @@ -0,0 +1,236 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.app; + +import java.awt.Dimension; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.io.IOException; +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.SourceDataLine; +import javax.swing.JFrame; +import javax.swing.JSlider; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.plaf.basic.BasicSliderUI; +import javax.swing.plaf.metal.MetalSliderUI; +import io.nayuki.flac.common.StreamInfo; +import io.nayuki.flac.decode.FlacDecoder; + + +/** + * Plays a single FLAC file to the system audio output, showing a GUI window with a seek bar. + * The file to play is specified as a command line argument. The seek bar is responsible for both + * displaying the current playback position, and allowing the user to click to seek to new positions. + *

Usage: java SeekableFlacPlayerGui InFile.flac

+ */ +public final class SeekableFlacPlayerGui { + + public static void main(String[] args) throws + LineUnavailableException, IOException, InterruptedException { + + /*-- Initialization code --*/ + + // Handle command line arguments + if (args.length != 1) { + System.err.println("Usage: java SeekableFlacPlayerGui InFile.flac"); + System.exit(1); + return; + } + File inFile = new File(args[0]); + + // Process header metadata blocks + FlacDecoder decoder = new FlacDecoder(inFile); + while (decoder.readAndHandleMetadataBlock() != null); + StreamInfo streamInfo = decoder.streamInfo; + if (streamInfo.numSamples == 0) + throw new IllegalArgumentException("Unknown audio length"); + + // Start Java sound output API + AudioFormat format = new AudioFormat(streamInfo.sampleRate, + streamInfo.sampleDepth, streamInfo.numChannels, true, false); + DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); + SourceDataLine line = (SourceDataLine)AudioSystem.getLine(info); + line.open(format); + line.start(); + + // Create GUI object, event handler, communication object + final double[] seekRequest = {-1}; + AudioPlayerGui gui = new AudioPlayerGui("FLAC Player"); + gui.listener = new AudioPlayerGui.Listener() { + public void seekRequested(double t) { + synchronized(seekRequest) { + seekRequest[0] = t; + seekRequest.notify(); + } + } + public void windowClosing() { + System.exit(0); + } + }; + + /*-- Audio player loop --*/ + + // Decode and write audio data, handle seek requests, wait for seek when end of stream reached + int bytesPerSample = streamInfo.sampleDepth / 8; + long startTime = line.getMicrosecondPosition(); + + // Buffers for data created and discarded within each loop iteration, but allocated outside the loop + int[][] samples = new int[streamInfo.numChannels][65536]; + byte[] sampleBytes = new byte[65536 * streamInfo.numChannels * bytesPerSample]; + while (true) { + + // Get and clear seek request, if any + double seekReq; + synchronized(seekRequest) { + seekReq = seekRequest[0]; + seekRequest[0] = -1; + } + + // Decode next audio block, or seek and decode + int blockSamples; + if (seekReq == -1) + blockSamples = decoder.readAudioBlock(samples, 0); + else { + long samplePos = Math.round(seekReq * streamInfo.numSamples); + seekReq = -1; + blockSamples = decoder.seekAndReadAudioBlock(samplePos, samples, 0); + line.flush(); + startTime = line.getMicrosecondPosition() - Math.round(samplePos * 1e6 / streamInfo.sampleRate); + } + + // Set display position + double timePos = (line.getMicrosecondPosition() - startTime) / 1e6; + gui.setPosition(timePos * streamInfo.sampleRate / streamInfo.numSamples); + + // Wait when end of stream reached + if (blockSamples == 0) { + synchronized(seekRequest) { + while (seekRequest[0] == -1) + seekRequest.wait(); + } + continue; + } + + // Convert samples to channel-interleaved bytes in little endian + int sampleBytesLen = 0; + for (int i = 0; i < blockSamples; i++) { + for (int ch = 0; ch < streamInfo.numChannels; ch++) { + int val = samples[ch][i]; + for (int j = 0; j < bytesPerSample; j++, sampleBytesLen++) + sampleBytes[sampleBytesLen] = (byte)(val >>> (j << 3)); + } + } + line.write(sampleBytes, 0, sampleBytesLen); + } + } + + + + /*---- User interface classes ----*/ + + private static final class AudioPlayerGui { + + /*-- Fields --*/ + + public Listener listener; + private JSlider slider; + private BasicSliderUI sliderUi; + + + /*-- Constructor --*/ + + public AudioPlayerGui(String windowTitle) { + // Create and configure slider + slider = new JSlider(SwingConstants.HORIZONTAL, 0, 10000, 0); + sliderUi = new MetalSliderUI(); + slider.setUI(sliderUi); + slider.setPreferredSize(new Dimension(800, 50)); + slider.addMouseListener(new MouseAdapter() { + public void mousePressed(MouseEvent ev) { + moveSlider(ev); + } + public void mouseReleased(MouseEvent ev) { + moveSlider(ev); + listener.seekRequested((double)slider.getValue() / slider.getMaximum()); + } + }); + slider.addMouseMotionListener(new MouseMotionAdapter() { + public void mouseDragged(MouseEvent ev) { + moveSlider(ev); + } + }); + + // Create and configure frame (window) + JFrame frame = new JFrame(windowTitle); + frame.add(slider); + frame.pack(); + frame.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent ev) { + listener.windowClosing(); + } + }); + frame.setResizable(false); + frame.setVisible(true); + } + + + /*-- Methods --*/ + + public void setPosition(double t) { + if (Double.isNaN(t)) + return; + final double val = Math.max(Math.min(t, 1), 0); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + if (!slider.getValueIsAdjusting()) + slider.setValue((int)Math.round(val * slider.getMaximum())); + } + }); + } + + + private void moveSlider(MouseEvent ev) { + slider.setValue(sliderUi.valueForXPosition(ev.getX())); + } + + + /*-- Helper interface --*/ + + public interface Listener { + + public void seekRequested(double t); // 0.0 <= t <= 1.0 + + public void windowClosing(); + + } + + } + +} diff --git a/desktop/src/io/nayuki/flac/app/ShowFlacFileStats.java b/desktop/src/io/nayuki/flac/app/ShowFlacFileStats.java new file mode 100644 index 0000000..21f4216 --- /dev/null +++ b/desktop/src/io/nayuki/flac/app/ShowFlacFileStats.java @@ -0,0 +1,277 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.app; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.SortedMap; +import java.util.TreeMap; +import io.nayuki.flac.common.FrameInfo; +import io.nayuki.flac.common.StreamInfo; +import io.nayuki.flac.decode.DataFormatException; +import io.nayuki.flac.decode.FlacLowLevelInput; +import io.nayuki.flac.decode.FrameDecoder; +import io.nayuki.flac.decode.SeekableFileFlacInput; + + +/** + * Reads a FLAC file, collects various statistics, and + * prints human-formatted information to standard output. + *

Usage: java ShowFlacFileStats InFile.flac

+ *

Example output from this program (abbreviated):

+ *
===== Block sizes (samples) =====
+ * 4096: * (11)
+ * 5120: ***** (56)
+ * 6144: *********** (116)
+ * 7168: ************* (134)
+ * 8192: ***************** (177)
+ * 9216: ***************** (182)
+ *10240: ***************** (179)
+ *11264: ****************************** (318)
+ *12288: ****************** (194)
+ *
+ *===== Frame sizes (bytes) =====
+ *12000: ****** (20)
+ *13000: ******* (24)
+ *14000: ********** (34)
+ *15000: **************** (51)
+ *16000: ********************* (68)
+ *17000: ******************* (63)
+ *18000: ******************* (63)
+ *19000: ************************ (77)
+ *20000: ********************* (70)
+ *21000: ****************** (60)
+ *22000: ************************* (82)
+ *23000: ********************* (69)
+ *24000: *************************** (87)
+ *25000: *************************** (88)
+ *26000: ********************** (73)
+ *27000: ************************** (84)
+ *28000: ****************************** (98)
+ *29000: ********************** (73)
+ *30000: *********************** (75)
+ *31000: ************ (39)
+ *
+ *===== Average compression ratio at block sizes =====
+ * 4096: ********************** (0.7470)
+ * 5120: ******************** (0.6815)
+ * 6144: ******************** (0.6695)
+ * 7168: ******************* (0.6438)
+ * 8192: ******************* (0.6379)
+ * 9216: ****************** (0.6107)
+ *10240: ****************** (0.6022)
+ *11264: ***************** (0.5628)
+ *12288: ***************** (0.5724)
+ *
+ *===== Stereo coding modes =====
+ *Independent: **** (83)
+ *Left-side  :  (3)
+ *Right-side : ************************ (574)
+ *Mid-side   : ****************************** (708)
+ */ +public final class ShowFlacFileStats { + + /*---- Main application function ----*/ + + public static void main(String[] args) throws IOException { + // Handle command line arguments + if (args.length != 1) { + System.err.println("Usage: java ShowFlacFileStats InFile.flac"); + System.exit(1); + return; + } + File inFile = new File(args[0]); + + // Data structures to hold statistics + List blockSizes = new ArrayList<>(); + List frameSizes = new ArrayList<>(); + List channelAssignments = new ArrayList<>(); + + // Read input file + StreamInfo streamInfo = null; + try (FlacLowLevelInput input = new SeekableFileFlacInput(inFile)) { + // Magic string "fLaC" + if (input.readUint(32) != 0x664C6143) + throw new DataFormatException("Invalid magic string"); + + // Handle metadata blocks + for (boolean last = false; !last; ) { + last = input.readUint(1) != 0; + int type = input.readUint(7); + int length = input.readUint(24); + byte[] data = new byte[length]; + input.readFully(data); + if (type == 0) + streamInfo = new StreamInfo(data); + } + + // Decode every frame + FrameDecoder dec = new FrameDecoder(input, streamInfo.sampleDepth); + int[][] blockSamples = new int[8][65536]; + while (true) { + FrameInfo meta = dec.readFrame(blockSamples, 0); + if (meta == null) + break; + blockSizes.add(meta.blockSize); + frameSizes.add(meta.frameSize); + channelAssignments.add(meta.channelAssignment); + } + } + + // Build and print graphs + printBlockSizeHistogram(blockSizes); + printFrameSizeHistogram(frameSizes); + printCompressionRatioGraph(streamInfo, blockSizes, frameSizes); + if (streamInfo.numChannels == 2) + printStereoModeGraph(channelAssignments); + } + + + + /*---- Statistics-processing functions ----*/ + + private static void printBlockSizeHistogram(List blockSizes) { + Map blockSizeCounts = new TreeMap<>(); + for (int bs : blockSizes) { + if (!blockSizeCounts.containsKey(bs)) + blockSizeCounts.put(bs, 0); + int count = blockSizeCounts.get(bs) + 1; + blockSizeCounts.put(bs, count); + } + List blockSizeLabels = new ArrayList<>(); + List blockSizeValues = new ArrayList<>(); + for (Map.Entry entry : blockSizeCounts.entrySet()) { + blockSizeLabels.add(String.format("%5d", entry.getKey())); + blockSizeValues.add((double)entry.getValue()); + } + printNormalizedBarGraph("Block sizes (samples)", blockSizeLabels, blockSizeValues); + } + + + private static void printFrameSizeHistogram(List frameSizes) { + final int step = 1000; + SortedMap frameSizeCounts = new TreeMap<>(); + int maxKeyLen = 0; + for (int fs : frameSizes) { + int key = (int)Math.round((double)fs / step); + maxKeyLen = Math.max(Integer.toString(key * step).length(), maxKeyLen); + if (!frameSizeCounts.containsKey(key)) + frameSizeCounts.put(key, 0); + frameSizeCounts.put(key, frameSizeCounts.get(key) + 1); + } + for (int i = frameSizeCounts.firstKey(); i < frameSizeCounts.lastKey(); i++) { + if (!frameSizeCounts.containsKey(i)) + frameSizeCounts.put(i, 0); + } + List frameSizeLabels = new ArrayList<>(); + List frameSizeValues = new ArrayList<>(); + for (Map.Entry entry : frameSizeCounts.entrySet()) { + frameSizeLabels.add(String.format("%" + maxKeyLen + "d", entry.getKey() * step)); + frameSizeValues.add((double)entry.getValue()); + } + printNormalizedBarGraph("Frame sizes (bytes)", frameSizeLabels, frameSizeValues); + } + + + private static void printCompressionRatioGraph(StreamInfo streamInfo, List blockSizes, List frameSizes) { + Map blockSizeCounts = new TreeMap<>(); + Map blockSizeBytes = new TreeMap<>(); + for (int i = 0; i < blockSizes.size(); i++) { + int bs = blockSizes.get(i); + if (!blockSizeCounts.containsKey(bs)) { + blockSizeCounts.put(bs, 0); + blockSizeBytes.put(bs, 0L); + } + blockSizeCounts.put(bs, blockSizeCounts.get(bs) + 1); + blockSizeBytes.put(bs, blockSizeBytes.get(bs) + frameSizes.get(i)); + } + List blockRatioLabels = new ArrayList<>(); + List blockRatioValues = new ArrayList<>(); + for (Map.Entry entry : blockSizeCounts.entrySet()) { + blockRatioLabels.add(String.format("%5d", entry.getKey())); + blockRatioValues.add(blockSizeBytes.get(entry.getKey()) / ((double)entry.getValue() * entry.getKey() * streamInfo.numChannels * streamInfo.sampleDepth / 8)); + } + printNormalizedBarGraph("Average compression ratio at block sizes", blockRatioLabels, blockRatioValues); + } + + + private static void printStereoModeGraph(List channelAssignments) { + List stereoModeLabels = Arrays.asList("Independent", "Left-side", "Right-side", "Mid-side"); + List stereoModeValues = new ArrayList<>(); + for (int i = 0; i < 4; i++) + stereoModeValues.add(0.0); + for (int mode : channelAssignments) { + int index; + switch (mode) { + case 1: index = 0; break; + case 8: index = 1; break; + case 9: index = 2; break; + case 10: index = 3; break; + default: throw new DataFormatException("Invalid mode in stereo stream"); + } + stereoModeValues.set(index, stereoModeValues.get(index) + 1); + } + printNormalizedBarGraph("Stereo coding modes", stereoModeLabels, stereoModeValues); + } + + + + /*---- Utility functions ----*/ + + private static void printNormalizedBarGraph(String heading, List labels, List values) { + Objects.requireNonNull(heading); + Objects.requireNonNull(labels); + Objects.requireNonNull(values); + if (labels.size() != values.size()) + throw new IllegalArgumentException(); + + final int maxBarWidth = 100; + System.out.printf("==================== %s ====================%n", heading); + System.out.println(); + + int maxLabelLen = 0; + for (String s : labels) + maxLabelLen = Math.max(s.length(), maxLabelLen); + String spaces = new String(new char[maxLabelLen]).replace((char)0, ' '); + + double maxValue = 1; // This avoids division by zero + for (double val : values) + maxValue = Math.max(val, maxValue); + + for (int i = 0; i < labels.size(); i++) { + String label = labels.get(i); + double value = values.get(i); + int barWidth = (int)Math.round(value / maxValue * maxBarWidth); + String bar = new String(new char[barWidth]).replace((char)0, '*'); + System.out.printf("%s%s: %s (%s)%n", label, spaces.substring(label.length()), + bar, (long)value == value ? Long.toString((long)value) : Double.toString(value)); + } + System.out.println(); + System.out.println(); + } + +} diff --git a/desktop/src/io/nayuki/flac/common/FrameInfo.java b/desktop/src/io/nayuki/flac/common/FrameInfo.java new file mode 100644 index 0000000..a4c1562 --- /dev/null +++ b/desktop/src/io/nayuki/flac/common/FrameInfo.java @@ -0,0 +1,456 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.common; + +import java.io.IOException; +import java.util.Objects; +import io.nayuki.flac.decode.DataFormatException; +import io.nayuki.flac.decode.FlacLowLevelInput; +import io.nayuki.flac.decode.FrameDecoder; +import io.nayuki.flac.encode.BitOutputStream; + + +/** + * Represents most fields in a frame header, in decoded (not raw) form. Mutable structure, + * not thread safe. Also has methods for parsing and serializing this structure to/from bytes. + * All fields can be modified freely when no method call is active. + * @see FrameDecoder + * @see StreamInfo#checkFrame(FrameInfo) + */ +public final class FrameInfo { + + /*---- Fields ----*/ + + // Exactly one of these following two fields equals -1. + + /** + * The index of this frame, where the foremost frame has index 0 and each subsequent frame + * increments it. This is either a uint31 value or −1 if unused. Exactly one of the fields + * frameIndex and sampleOffse is equal to −1 (not both nor neither). This value can only + * be used if the stream info's minBlockSize = maxBlockSize (constant block size encoding style). + */ + public int frameIndex; + + /** + * The offset of the first sample in this frame with respect to the beginning of the + * audio stream. This is either a uint36 value or −1 if unused. Exactly one of + * the fields frameIndex and sampleOffse is equal to −1 (not both nor neither). + */ + public long sampleOffset; + + + /** + * The number of audio channels in this frame, in the range 1 to 8 inclusive. + * This value is fully determined by the channelAssignment field. + */ + public int numChannels; + + /** + * The raw channel assignment value of this frame, which is a uint4 value. + * This indicates the number of channels, but also tells the stereo coding mode. + */ + public int channelAssignment; + + /** + * The number of samples per channel in this frame, in the range 1 to 65536 inclusive. + */ + public int blockSize; + + /** + * The sample rate of this frame in hertz (Hz), in the range 1 to 655360 inclusive, + * or −1 if unavailable (i.e. the stream info should be consulted). + */ + public int sampleRate; + + /** + * The sample depth of this frame in bits, in the range 8 to 24 inclusive, + * or −1 if unavailable (i.e. the stream info should be consulted). + */ + public int sampleDepth; + + /** + * The size of this frame in bytes, from the start of the sync sequence to the end + * of the trailing CRC-16 checksum. A valid value is at least 10, or −1 + * if unavailable (e.g. the frame header was parsed but not the entire frame). + */ + public int frameSize; + + + + /*---- Constructors ----*/ + + /** + * Constructs a blank frame metadata structure, setting all fields to unknown or invalid values. + */ + public FrameInfo() { + frameIndex = -1; + sampleOffset = -1; + numChannels = -1; + channelAssignment = -1; + blockSize = -1; + sampleRate = -1; + sampleDepth = -1; + frameSize = -1; + } + + + + /*---- Functions to read FrameInfo from stream ----*/ + + /** + * Reads the next FLAC frame header from the specified input stream, either returning + * a new frame info object or {@code null}. The stream must be aligned to a byte + * boundary and start at a sync sequence. If EOF is immediately encountered before + * any bytes were read, then this returns {@code null}. + *

Otherwise this reads between 6 to 16 bytes from the stream – starting + * from the sync code, and ending after the CRC-8 value is read (but before reading + * any subframes). It tries to parse the frame header data. After the values are + * successfully decoded, a new frame info object is created, almost all fields are + * set to the parsed values, and it is returned. (This doesn't read to the end + * of the frame, so the frameSize field is set to -1.)

+ * @param in the input stream to read from (not {@code null}) + * @return a new frame info object or {@code null} + * @throws NullPointerException if the input stream is {@code null} + * @throws DataFormatException if the input data contains invalid values + * @throws IOException if an I/O exception occurred + */ + public static FrameInfo readFrame(FlacLowLevelInput in) throws IOException { + // Preliminaries + in.resetCrcs(); + int temp = in.readByte(); + if (temp == -1) + return null; + FrameInfo result = new FrameInfo(); + result.frameSize = -1; + + // Read sync bits + int sync = temp << 6 | in.readUint(6); // Uint14 + if (sync != 0x3FFE) + throw new DataFormatException("Sync code expected"); + + // Read various simple fields + if (in.readUint(1) != 0) + throw new DataFormatException("Reserved bit"); + int blockStrategy = in.readUint(1); + int blockSizeCode = in.readUint(4); + int sampleRateCode = in.readUint(4); + int chanAsgn = in.readUint(4); + result.channelAssignment = chanAsgn; + if (chanAsgn < 8) + result.numChannels = chanAsgn + 1; + else if (8 <= chanAsgn && chanAsgn <= 10) + result.numChannels = 2; + else + throw new DataFormatException("Reserved channel assignment"); + result.sampleDepth = decodeSampleDepth(in.readUint(3)); + if (in.readUint(1) != 0) + throw new DataFormatException("Reserved bit"); + + // Read and check the frame/sample position field + long position = readUtf8Integer(in); // Reads 1 to 7 bytes + if (blockStrategy == 0) { + if ((position >>> 31) != 0) + throw new DataFormatException("Frame index too large"); + result.frameIndex = (int)position; + result.sampleOffset = -1; + } else if (blockStrategy == 1) { + result.sampleOffset = position; + result.frameIndex = -1; + } else + throw new AssertionError(); + + // Read variable-length data for some fields + result.blockSize = decodeBlockSize(blockSizeCode, in); // Reads 0 to 2 bytes + result.sampleRate = decodeSampleRate(sampleRateCode, in); // Reads 0 to 2 bytes + int computedCrc8 = in.getCrc8(); + if (in.readUint(8) != computedCrc8) + throw new DataFormatException("CRC-8 mismatch"); + return result; + } + + + // Reads 1 to 7 whole bytes from the input stream. Return value is a uint36. + // See: https://hydrogenaud.io/index.php/topic,112831.msg929128.html#msg929128 + private static long readUtf8Integer(FlacLowLevelInput in) throws IOException { + int head = in.readUint(8); + int n = Integer.numberOfLeadingZeros(~(head << 24)); // Number of leading 1s in the byte + assert 0 <= n && n <= 8; + if (n == 0) + return head; + else if (n == 1 || n == 8) + throw new DataFormatException("Invalid UTF-8 coded number"); + else { + long result = head & (0x7F >>> n); + for (int i = 0; i < n - 1; i++) { + int temp = in.readUint(8); + if ((temp & 0xC0) != 0x80) + throw new DataFormatException("Invalid UTF-8 coded number"); + result = (result << 6) | (temp & 0x3F); + } + if ((result >>> 36) != 0) + throw new AssertionError(); + return result; + } + } + + + // Argument is a uint4 value. Reads 0 to 2 bytes from the input stream. + // Return value is in the range [1, 65536]. + private static int decodeBlockSize(int code, FlacLowLevelInput in) throws IOException { + if ((code >>> 4) != 0) + throw new IllegalArgumentException(); + switch (code) { + case 0: throw new DataFormatException("Reserved block size"); + case 6: return in.readUint(8) + 1; + case 7: return in.readUint(16) + 1; + default: + int result = searchSecond(BLOCK_SIZE_CODES, code); + if (result < 1 || result > 65536) + throw new AssertionError(); + return result; + } + } + + + // Argument is a uint4 value. Reads 0 to 2 bytes from the input stream. + // Return value is in the range [-1, 655350]. + private static int decodeSampleRate(int code, FlacLowLevelInput in) throws IOException { + if ((code >>> 4) != 0) + throw new IllegalArgumentException(); + switch (code) { + case 0: return -1; // Caller should obtain value from stream info metadata block + case 12: return in.readUint(8); + case 13: return in.readUint(16); + case 14: return in.readUint(16) * 10; + case 15: throw new DataFormatException("Invalid sample rate"); + default: + int result = searchSecond(SAMPLE_RATE_CODES, code); + if (result < 1 || result > 655350) + throw new AssertionError(); + return result; + } + } + + + // Argument is a uint3 value. Pure function and performs no I/O. Return value is in the range [-1, 24]. + private static int decodeSampleDepth(int code) { + if ((code >>> 3) != 0) + throw new IllegalArgumentException(); + else if (code == 0) + return -1; // Caller should obtain value from stream info metadata block + else { + int result = searchSecond(SAMPLE_DEPTH_CODES, code); + if (result == -1) + throw new DataFormatException("Reserved bit depth"); + if (result < 1 || result > 32) + throw new AssertionError(); + return result; + } + } + + + + /*---- Functions to write FrameInfo to stream ----*/ + + /** + * Writes the current state of this object as a frame header to the specified + * output stream, from the sync field through to the CRC-8 field (inclusive). + * This does not write the data of subframes, the bit padding, nor the CRC-16 field.

+ *

The stream must be byte-aligned before this method is called, and will be aligned + * upon returning (i.e. it writes a whole number of bytes). This method initially resets + * the stream's CRC computations, which is useful behavior for the caller because + * it will need to write the CRC-16 at the end of the frame.

+ * @param out the output stream to write to (not {@code null}) + * @throws NullPointerException if the output stream is {@code null} + * @throws IOException if an I/O exception occurred + */ + public void writeHeader(BitOutputStream out) throws IOException { + Objects.requireNonNull(out); + out.resetCrcs(); + out.writeInt(14, 0x3FFE); // Sync + out.writeInt(1, 0); // Reserved + out.writeInt(1, 1); // Blocking strategy + + int blockSizeCode = getBlockSizeCode(blockSize); + out.writeInt(4, blockSizeCode); + int sampleRateCode = getSampleRateCode(sampleRate); + out.writeInt(4, sampleRateCode); + + out.writeInt(4, channelAssignment); + out.writeInt(3, getSampleDepthCode(sampleDepth)); + out.writeInt(1, 0); // Reserved + + // Variable-length: 1 to 7 bytes + if (frameIndex != -1 && sampleOffset == -1) + writeUtf8Integer(sampleOffset, out); + else if (sampleOffset != -1 && frameIndex == -1) + writeUtf8Integer(sampleOffset, out); + else + throw new IllegalStateException(); + + // Variable-length: 0 to 2 bytes + if (blockSizeCode == 6) + out.writeInt(8, blockSize - 1); + else if (blockSizeCode == 7) + out.writeInt(16, blockSize - 1); + + // Variable-length: 0 to 2 bytes + if (sampleRateCode == 12) + out.writeInt(8, sampleRate); + else if (sampleRateCode == 13) + out.writeInt(16, sampleRate); + else if (sampleRateCode == 14) + out.writeInt(16, sampleRate / 10); + + out.writeInt(8, out.getCrc8()); + } + + + // Given a uint36 value, this writes 1 to 7 whole bytes to the given output stream. + private static void writeUtf8Integer(long val, BitOutputStream out) throws IOException { + if ((val >>> 36) != 0) + throw new IllegalArgumentException(); + int bitLen = 64 - Long.numberOfLeadingZeros(val); + if (bitLen <= 7) + out.writeInt(8, (int)val); + else { + int n = (bitLen - 2) / 5; + out.writeInt(8, (0xFF80 >>> n) | (int)(val >>> (n * 6))); + for (int i = n - 1; i >= 0; i--) + out.writeInt(8, 0x80 | ((int)(val >>> (i * 6)) & 0x3F)); + } + } + + + // Returns a uint4 value representing the given block size. Pure function. + private static int getBlockSizeCode(int blockSize) { + int result = searchFirst(BLOCK_SIZE_CODES, blockSize); + if (result != -1); // Already done + else if (1 <= blockSize && blockSize <= 256) + result = 6; + else if (1 <= blockSize && blockSize <= 65536) + result = 7; + else // blockSize < 1 || blockSize > 65536 + throw new IllegalArgumentException(); + + if ((result >>> 4) != 0) + throw new AssertionError(); + return result; + } + + + // Returns a uint4 value representing the given sample rate. Pure function. + private static int getSampleRateCode(int sampleRate) { + if (sampleRate == 0 || sampleRate < -1) + throw new IllegalArgumentException(); + int result = searchFirst(SAMPLE_RATE_CODES, sampleRate); + if (result != -1); // Already done + else if (0 <= sampleRate && sampleRate < 256) + result = 12; + else if (0 <= sampleRate && sampleRate < 65536) + result = 13; + else if (0 <= sampleRate && sampleRate < 655360 && sampleRate % 10 == 0) + result = 14; + else + result = 0; + + if ((result >>> 4) != 0) + throw new AssertionError(); + return result; + } + + + // Returns a uint3 value representing the given sample depth. Pure function. + private static int getSampleDepthCode(int sampleDepth) { + if (sampleDepth != -1 && (sampleDepth < 1 || sampleDepth > 32)) + throw new IllegalArgumentException(); + int result = searchFirst(SAMPLE_DEPTH_CODES, sampleDepth); + if (result == -1) + result = 0; + if ((result >>> 3) != 0) + throw new AssertionError(); + return result; + } + + + + /*---- Tables of constants and search functions ----*/ + + private static final int searchFirst(int[][] table, int key) { + for (int[] pair : table) { + if (pair[0] == key) + return pair[1]; + } + return -1; + } + + + private static final int searchSecond(int[][] table, int key) { + for (int[] pair : table) { + if (pair[1] == key) + return pair[0]; + } + return -1; + } + + + private static final int[][] BLOCK_SIZE_CODES = { + { 192, 1}, + { 576, 2}, + { 1152, 3}, + { 2304, 4}, + { 4608, 5}, + { 256, 8}, + { 512, 9}, + { 1024, 10}, + { 2048, 11}, + { 4096, 12}, + { 8192, 13}, + {16384, 14}, + {32768, 15}, + }; + + + private static final int[][] SAMPLE_DEPTH_CODES = { + { 8, 1}, + {12, 2}, + {16, 4}, + {20, 5}, + {24, 6}, + }; + + + private static final int[][] SAMPLE_RATE_CODES = { + { 88200, 1}, + {176400, 2}, + {192000, 3}, + { 8000, 4}, + { 16000, 5}, + { 22050, 6}, + { 24000, 7}, + { 32000, 8}, + { 44100, 9}, + { 48000, 10}, + { 96000, 11}, + }; + +} diff --git a/desktop/src/io/nayuki/flac/common/SeekTable.java b/desktop/src/io/nayuki/flac/common/SeekTable.java new file mode 100644 index 0000000..04b433b --- /dev/null +++ b/desktop/src/io/nayuki/flac/common/SeekTable.java @@ -0,0 +1,204 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.common; + +import java.io.ByteArrayInputStream; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import io.nayuki.flac.decode.FlacDecoder; +import io.nayuki.flac.encode.BitOutputStream; + + +/** + * Represents precisely all the fields of a seek table metadata block. Mutable structure, + * not thread-safe. Also has methods for parsing and serializing this structure to/from bytes. + * All fields and objects can be modified freely when no method call is active. + * @see FlacDecoder + */ +public final class SeekTable { + + /*---- Fields ----*/ + + /** + * The list of seek points in this seek table. It is okay to replace this + * list as needed (the initially constructed list object is not special). + */ + public List points; + + + + /*---- Constructors ----*/ + + /** + * Constructs a blank seek table with an initially empty + * list of seek points. (Note that the empty state is legal.) + */ + public SeekTable() { + points = new ArrayList<>(); + } + + + /** + * Constructs a seek table by parsing the given byte array representing the metadata block. + * (The array must contain only the metadata payload, without the type or length fields.) + *

This constructor does not check the validity of the seek points, namely the ordering + * of seek point offsets, so calling {@link#checkValues()} on the freshly constructed object + * can fail. However, this does guarantee that every point's frameSamples field is a uint16.

+ * @param b the metadata block's payload data to parse (not {@code null}) + * @throws NullPointerException if the array is {@code null} + * @throws IllegalArgumentException if the array length + * is not a multiple of 18 (size of each seek point) + */ + public SeekTable(byte[] b) { + this(); + Objects.requireNonNull(b); + if (b.length % 18 != 0) + throw new IllegalArgumentException("Data contains a partial seek point"); + try { + DataInput in = new DataInputStream(new ByteArrayInputStream(b)); + for (int i = 0; i < b.length; i += 18) { + SeekPoint p = new SeekPoint(); + p.sampleOffset = in.readLong(); + p.fileOffset = in.readLong(); + p.frameSamples = in.readUnsignedShort(); + points.add(p); + } + // Skip closing the in-memory streams + } catch (IOException e) { + throw new AssertionError(e); + } + } + + + + /*---- Methods ----*/ + + /** + * Checks the state of this object and returns silently if all these criteria pass: + *
    + *
  • No object is {@code null}
  • + *
  • The frameSamples field of each point is a uint16 value
  • + *
  • All points with sampleOffset = −1 (i.e. 0xFFF...FFF) are at the end of the list
  • + *
  • All points with sampleOffset ≠ −1 have strictly increasing + * values of sampleOffset and non-decreasing values of fileOffset
  • + *
+ * @throws NullPointerException if the list or an element is {@code null} + * @throws IllegalStateException if the current list of seek points is contains invalid data + */ + public void checkValues() { + // Check list and each point + Objects.requireNonNull(points); + for (SeekPoint p : points) { + Objects.requireNonNull(p); + if ((p.frameSamples & 0xFFFF) != p.frameSamples) + throw new IllegalStateException("Frame samples outside uint16 range"); + } + + // Check ordering of points + for (int i = 1; i < points.size(); i++) { + SeekPoint p = points.get(i); + if (p.sampleOffset != -1) { + SeekPoint q = points.get(i - 1); + if (p.sampleOffset <= q.sampleOffset) + throw new IllegalStateException("Sample offsets out of order"); + if (p.fileOffset < q.fileOffset) + throw new IllegalStateException("File offsets out of order"); + } + } + } + + + /** + * Writes all the points of this seek table as a metadata block to the specified output stream, + * also indicating whether it is the last metadata block. (This does write the type and length + * fields for the metadata block, unlike the constructor which takes an array without those fields.) + * @param last whether the metadata block is the final one in the FLAC file + * @param out the output stream to write to (not {@code null}) + * @throws NullPointerException if the output stream is {@code null} + * @throws IllegalStateException if there are too many + * @throws IOException if an I/O exception occurred + * seek points (> 932067) or {@link#checkValues()} fails + */ + public void write(boolean last, BitOutputStream out) throws IOException { + // Check arguments and state + Objects.requireNonNull(out); + Objects.requireNonNull(points); + if (points.size() > ((1 << 24) - 1) / 18) + throw new IllegalStateException("Too many seek points"); + checkValues(); + + // Write metadata block header + out.writeInt(1, last ? 1 : 0); + out.writeInt(7, 3); + out.writeInt(24, points.size() * 18); + + // Write each seek point + for (SeekPoint p : points) { + out.writeInt(32, (int)(p.sampleOffset >>> 32)); + out.writeInt(32, (int)(p.sampleOffset >>> 0)); + out.writeInt(32, (int)(p.fileOffset >>> 32)); + out.writeInt(32, (int)(p.fileOffset >>> 0)); + out.writeInt(16, p.frameSamples); + } + } + + + + /*---- Helper structure ----*/ + + /** + * Represents a seek point entry in a seek table. Mutable structure, not thread-safe. + * This class itself does not check the correctness of data, but other classes might. + *

A seek point with data (sampleOffset = x, fileOffset = y, frameSamples = z) means + * that at byte position (y + (byte offset of foremost audio frame)) in the file, + * a FLAC frame begins (with the sync sequence), that frame has sample offset x + * (where sample 0 is defined as the start of the audio stream), + * and the frame contains z samples per channel. + * @see SeekTable + */ + public static final class SeekPoint { + + /** + * The sample offset in the audio stream, a uint64 value. + * A value of -1 (i.e. 0xFFF...FFF) means this is a placeholder point. + */ + public long sampleOffset; + + /** + * The byte offset relative to the start of the foremost frame, a uint64 value. + * If sampleOffset is -1, then this value is ignored. + */ + public long fileOffset; + + /** + * The number of audio samples in the target block/frame, a uint16 value. + * If sampleOffset is -1, then this value is ignored. + */ + public int frameSamples; + + } + +} diff --git a/desktop/src/io/nayuki/flac/common/StreamInfo.java b/desktop/src/io/nayuki/flac/common/StreamInfo.java new file mode 100644 index 0000000..d8ab3e1 --- /dev/null +++ b/desktop/src/io/nayuki/flac/common/StreamInfo.java @@ -0,0 +1,310 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.common; + +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Objects; +import io.nayuki.flac.decode.ByteArrayFlacInput; +import io.nayuki.flac.decode.DataFormatException; +import io.nayuki.flac.decode.FlacDecoder; +import io.nayuki.flac.decode.FlacLowLevelInput; +import io.nayuki.flac.encode.BitOutputStream; + + +/** + * Represents precisely all the fields of a stream info metadata block. Mutable structure, + * not thread-safe. Also has methods for parsing and serializing this structure to/from bytes. + * All fields can be modified freely when no method call is active. + * @see FrameInfo + * @see FlacDecoder + */ +public final class StreamInfo { + + /*---- Fields about block and frame sizes ----*/ + + /** + * Minimum block size (in samples per channel) among the whole stream, a uint16 value. + * However when minBlockSize = maxBlockSize (constant block size encoding style), + * the final block is allowed to be smaller than minBlockSize. + */ + public int minBlockSize; + + /** + * Maximum block size (in samples per channel) among the whole stream, a uint16 value. + */ + public int maxBlockSize; + + /** + * Minimum frame size (in bytes) among the whole stream, a uint24 value. + * However, a value of 0 signifies that the value is unknown. + */ + public int minFrameSize; + + /** + * Maximum frame size (in bytes) among the whole stream, a uint24 value. + * However, a value of 0 signifies that the value is unknown. + */ + public int maxFrameSize; + + + /*---- Fields about stream properties ----*/ + + /** + * The sample rate of the audio stream (in hertz (Hz)), a positive uint20 value. + * Note that 0 is an invalid value. + */ + public int sampleRate; + + /** + * The number of channels in the audio stream, between 1 and 8 inclusive. + * 1 means mono, 2 means stereo, et cetera. + */ + public int numChannels; + + /** + * The bits per sample in the audio stream, in the range 4 to 32 inclusive. + */ + public int sampleDepth; + + /** + * The total number of samples per channel in the whole stream, a uint36 value. + * The special value of 0 signifies that the value is unknown (not empty zero-length stream). + */ + public long numSamples; + + /** + * The 16-byte MD5 hash of the raw uncompressed audio data serialized in little endian with + * channel interleaving (not planar). It can be all zeros to signify that the hash was not computed. + * It is okay to replace this array as needed (the initially constructed array object is not special). + */ + public byte[] md5Hash; + + + + /*---- Constructors ----*/ + + /** + * Constructs a blank stream info structure with certain default values. + */ + public StreamInfo() { + // Set these fields to legal unknown values + minFrameSize = 0; + maxFrameSize = 0; + numSamples = 0; + md5Hash = new byte[16]; + + // Set these fields to invalid (not reserved) values + minBlockSize = 0; + maxBlockSize = 0; + sampleRate = 0; + } + + + /** + * Constructs a stream info structure by parsing the specified 34-byte metadata block. + * (The array must contain only the metadata payload, without the type or length fields.) + * @param b the metadata block's payload data to parse (not {@code null}) + * @throws NullPointerException if the array is {@code null} + * @throws IllegalArgumentException if the array length is not 34 + * @throws DataFormatException if the data contains invalid values + */ + public StreamInfo(byte[] b) { + Objects.requireNonNull(b); + if (b.length != 34) + throw new IllegalArgumentException("Invalid data length"); + try { + FlacLowLevelInput in = new ByteArrayFlacInput(b); + minBlockSize = in.readUint(16); + maxBlockSize = in.readUint(16); + minFrameSize = in.readUint(24); + maxFrameSize = in.readUint(24); + if (minBlockSize < 16) + throw new DataFormatException("Minimum block size less than 16"); + if (maxBlockSize > 65535) + throw new DataFormatException("Maximum block size greater than 65535"); + if (maxBlockSize < minBlockSize) + throw new DataFormatException("Maximum block size less than minimum block size"); + if (minFrameSize != 0 && maxFrameSize != 0 && maxFrameSize < minFrameSize) + throw new DataFormatException("Maximum frame size less than minimum frame size"); + sampleRate = in.readUint(20); + if (sampleRate == 0 || sampleRate > 655350) + throw new DataFormatException("Invalid sample rate"); + numChannels = in.readUint(3) + 1; + sampleDepth = in.readUint(5) + 1; + numSamples = (long)in.readUint(18) << 18 | in.readUint(18); // uint36 + md5Hash = new byte[16]; + in.readFully(md5Hash); + // Skip closing the in-memory stream + } catch (IOException e) { + throw new AssertionError(e); + } + } + + + + /*---- Methods ----*/ + + /** + * Checks the state of this object, and either returns silently or throws an exception. + * @throws NullPointerException if the MD5 hash array is {@code null} + * @throws IllegalStateException if any field has an invalid value + */ + public void checkValues() { + if ((minBlockSize >>> 16) != 0) + throw new IllegalStateException("Invalid minimum block size"); + if ((maxBlockSize >>> 16) != 0) + throw new IllegalStateException("Invalid maximum block size"); + if ((minFrameSize >>> 24) != 0) + throw new IllegalStateException("Invalid minimum frame size"); + if ((maxFrameSize >>> 24) != 0) + throw new IllegalStateException("Invalid maximum frame size"); + if (sampleRate == 0 || (sampleRate >>> 20) != 0) + throw new IllegalStateException("Invalid sample rate"); + if (numChannels < 1 || numChannels > 8) + throw new IllegalStateException("Invalid number of channels"); + if (sampleDepth < 4 || sampleDepth > 32) + throw new IllegalStateException("Invalid sample depth"); + if ((numSamples >>> 36) != 0) + throw new IllegalStateException("Invalid number of samples"); + Objects.requireNonNull(md5Hash); + if (md5Hash.length != 16) + throw new IllegalStateException("Invalid MD5 hash length"); + } + + + /** + * Checks whether the specified frame information is consistent with values in + * this stream info object, either returning silently or throwing an exception. + * @param meta the frame info object to check (not {@code null}) + * @throws NullPointerException if the frame info is {@code null} + * @throws DataFormatException if the frame info contains bad values + */ + public void checkFrame(FrameInfo meta) { + if (meta.numChannels != numChannels) + throw new DataFormatException("Channel count mismatch"); + if (meta.sampleRate != -1 && meta.sampleRate != sampleRate) + throw new DataFormatException("Sample rate mismatch"); + if (meta.sampleDepth != -1 && meta.sampleDepth != sampleDepth) + throw new DataFormatException("Sample depth mismatch"); + if (numSamples != 0 && meta.blockSize > numSamples) + throw new DataFormatException("Block size exceeds total number of samples"); + + if (meta.blockSize > maxBlockSize) + throw new DataFormatException("Block size exceeds maximum"); + // Note: If minBlockSize == maxBlockSize, then the final block + // in the stream is allowed to be smaller than minBlockSize + + if (minFrameSize != 0 && meta.frameSize < minFrameSize) + throw new DataFormatException("Frame size less than minimum"); + if (maxFrameSize != 0 && meta.frameSize > maxFrameSize) + throw new DataFormatException("Frame size exceeds maximum"); + } + + + /** + * Writes this stream info metadata block to the specified output stream, including the + * metadata block header, writing exactly 38 bytes. (This is unlike the constructor, + * which takes an array without the type and length fields.) The output stream must + * initially be aligned to a byte boundary, and will finish at a byte boundary. + * @param last whether the metadata block is the final one in the FLAC file + * @param out the output stream to write to (not {@code null}) + * @throws NullPointerException if the output stream is {@code null} + * @throws IOException if an I/O exception occurred + */ + public void write(boolean last, BitOutputStream out) throws IOException { + // Check arguments and state + Objects.requireNonNull(out); + checkValues(); + + // Write metadata block header + out.writeInt(1, last ? 1 : 0); + out.writeInt(7, 0); // Type + out.writeInt(24, 34); // Length + + // Write stream info block fields + out.writeInt(16, minBlockSize); + out.writeInt(16, maxBlockSize); + out.writeInt(24, minFrameSize); + out.writeInt(24, maxFrameSize); + out.writeInt(20, sampleRate); + out.writeInt(3, numChannels - 1); + out.writeInt(5, sampleDepth - 1); + out.writeInt(18, (int)(numSamples >>> 18)); + out.writeInt(18, (int)(numSamples >>> 0)); + for (byte b : md5Hash) + out.writeInt(8, b); + } + + + + /*---- Static functions ----*/ + + /** + * Computes and returns the MD5 hash of the specified raw audio sample data at the specified + * bit depth. Currently, the bit depth must be a multiple of 8, between 8 and 32 inclusive. + * The returned array is a new object of length 16. + * @param samples the audio samples to hash, where + * each subarray is a channel (all not {@code null}) + * @param depth the bit depth of the audio samples + * (i.e. each sample value is a signed 'depth'-bit integer) + * @return a new 16-byte array representing the MD5 hash of the audio data + * @throws NullPointerException if the array or any subarray is {@code null} + * @throws IllegalArgumentException if the bit depth is unsupported + */ + public static byte[] getMd5Hash(int[][] samples, int depth) { + // Check arguments + Objects.requireNonNull(samples); + for (int[] chanSamples : samples) + Objects.requireNonNull(chanSamples); + if (depth < 0 || depth > 32 || depth % 8 != 0) + throw new IllegalArgumentException("Unsupported bit depth"); + + // Create hasher + MessageDigest hasher; + try { // Guaranteed available by the Java Cryptography Architecture + hasher = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + + // Convert samples to a stream of bytes, compute hash + int numChannels = samples.length; + int numSamples = samples[0].length; + int numBytes = depth / 8; + byte[] buf = new byte[numChannels * numBytes * Math.min(numSamples, 2048)]; + for (int i = 0, l = 0; i < numSamples; i++) { + for (int j = 0; j < numChannels; j++) { + int val = samples[j][i]; + for (int k = 0; k < numBytes; k++, l++) + buf[l] = (byte)(val >>> (k << 3)); + } + if (l == buf.length || i == numSamples - 1) { + hasher.update(buf, 0, l); + l = 0; + } + } + return hasher.digest(); + } + +} diff --git a/desktop/src/io/nayuki/flac/decode/AbstractFlacLowLevelInput.java b/desktop/src/io/nayuki/flac/decode/AbstractFlacLowLevelInput.java new file mode 100644 index 0000000..bca3b88 --- /dev/null +++ b/desktop/src/io/nayuki/flac/decode/AbstractFlacLowLevelInput.java @@ -0,0 +1,354 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.decode; + +import java.io.EOFException; +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + + +/** + * A basic implementation of most functionality required by FlacLowLevelInpuut. + */ +public abstract class AbstractFlacLowLevelInput implements FlacLowLevelInput { + + /*---- Fields ----*/ + + // Data from the underlying stream is first stored into this byte buffer before further processing. + private long byteBufferStartPos; + private byte[] byteBuffer; + private int byteBufferLen; + private int byteBufferIndex; + + // The buffer of next bits to return to a reader. Note that byteBufferIndex is incremented when byte + // values are put into the bit buffer, but they might not have been consumed by the ultimate reader yet. + private long bitBuffer; // Only the bottom bitBufferLen bits are valid; the top bits are garbage. + private int bitBufferLen; // Always in the range [0, 64]. + + // Current state of the CRC calculations. + private int crc8; // Always a uint8 value. + private int crc16; // Always a uint16 value. + private int crcStartIndex; // In the range [0, byteBufferLen], unless byteBufferLen = -1. + + + + /*---- Constructors ----*/ + + public AbstractFlacLowLevelInput() { + byteBuffer = new byte[4096]; + positionChanged(0); + } + + + + /*---- Methods ----*/ + + /*-- Stream position --*/ + + public long getPosition() { + return byteBufferStartPos + byteBufferIndex - (bitBufferLen + 7) / 8; + } + + + public int getBitPosition() { + return (-bitBufferLen) & 7; + } + + + // When a subclass handles seekTo() and didn't throw UnsupportedOperationException, + // it must call this method to flush the buffers of upcoming data. + protected void positionChanged(long pos) { + byteBufferStartPos = pos; + Arrays.fill(byteBuffer, (byte)0); // Defensive clearing, should have no visible effect outside of debugging + byteBufferLen = 0; + byteBufferIndex = 0; + bitBuffer = 0; // Defensive clearing, should have no visible effect outside of debugging + bitBufferLen = 0; + resetCrcs(); + } + + + // Either returns silently or throws an exception. + private void checkByteAligned() { + if (bitBufferLen % 8 != 0) + throw new IllegalStateException("Not at a byte boundary"); + } + + + /*-- Reading bitwise integers --*/ + + public int readUint(int n) throws IOException { + if (n < 0 || n > 32) + throw new IllegalArgumentException(); + while (bitBufferLen < n) { + int b = readUnderlying(); + if (b == -1) + throw new EOFException(); + bitBuffer = (bitBuffer << 8) | b; + bitBufferLen += 8; + assert 0 <= bitBufferLen && bitBufferLen <= 64; + } + int result = (int)(bitBuffer >>> (bitBufferLen - n)); + if (n != 32) { + result &= (1 << n) - 1; + assert (result >>> n) == 0; + } + bitBufferLen -= n; + assert 0 <= bitBufferLen && bitBufferLen <= 64; + return result; + } + + + public int readSignedInt(int n) throws IOException { + int shift = 32 - n; + return (readUint(n) << shift) >> shift; + } + + + public void readRiceSignedInts(int param, long[] result, int start, int end) throws IOException { + if (param < 0 || param > 31) + throw new IllegalArgumentException(); + long unaryLimit = 1L << (53 - param); + + byte[] consumeTable = RICE_DECODING_CONSUMED_TABLES[param]; + int[] valueTable = RICE_DECODING_VALUE_TABLES[param]; + while (true) { + middle: + while (start <= end - RICE_DECODING_CHUNK) { + if (bitBufferLen < RICE_DECODING_CHUNK * RICE_DECODING_TABLE_BITS) { + if (byteBufferIndex <= byteBufferLen - 8) { + fillBitBuffer(); + } else + break; + } + for (int i = 0; i < RICE_DECODING_CHUNK; i++, start++) { + // Fast decoder + int extractedBits = (int)(bitBuffer >>> (bitBufferLen - RICE_DECODING_TABLE_BITS)) & RICE_DECODING_TABLE_MASK; + int consumed = consumeTable[extractedBits]; + if (consumed == 0) + break middle; + bitBufferLen -= consumed; + result[start] = valueTable[extractedBits]; + } + } + + // Slow decoder + if (start >= end) + break; + long val = 0; + while (readUint(1) == 0) { + if (val >= unaryLimit) { + // At this point, the final decoded value would be so large that the result of the + // downstream restoreLpc() calculation would not fit in the output sample's bit depth - + // hence why we stop early and throw an exception. However, this check is conservative + // and doesn't catch all the cases where the post-LPC result wouldn't fit. + throw new DataFormatException("Residual value too large"); + } + val++; + } + val = (val << param) | readUint(param); // Note: Long masking unnecessary because param <= 31 + assert (val >>> 53) == 0; // Must fit a uint53 by design due to unaryLimit + val = (val >>> 1) ^ -(val & 1); // Transform uint53 to int53 according to Rice coding of signed numbers + assert (val >> 52) == 0 || (val >> 52) == -1; // Must fit a signed int53 by design + result[start] = val; + start++; + } + } + + + // Appends at least 8 bits to the bit buffer, or throws EOFException. + private void fillBitBuffer() throws IOException { + int i = byteBufferIndex; + int n = Math.min((64 - bitBufferLen) >>> 3, byteBufferLen - i); + byte[] b = byteBuffer; + if (n > 0) { + for (int j = 0; j < n; j++, i++) + bitBuffer = (bitBuffer << 8) | (b[i] & 0xFF); + bitBufferLen += n << 3; + } else if (bitBufferLen <= 56) { + int temp = readUnderlying(); + if (temp == -1) + throw new EOFException(); + bitBuffer = (bitBuffer << 8) | temp; + bitBufferLen += 8; + } + assert 8 <= bitBufferLen && bitBufferLen <= 64; + byteBufferIndex += n; + } + + + /*-- Reading bytes --*/ + + public int readByte() throws IOException { + checkByteAligned(); + if (bitBufferLen >= 8) + return readUint(8); + else { + assert bitBufferLen == 0; + return readUnderlying(); + } + } + + + public void readFully(byte[] b) throws IOException { + Objects.requireNonNull(b); + checkByteAligned(); + for (int i = 0; i < b.length; i++) + b[i] = (byte)readUint(8); + } + + + // Reads a byte from the byte buffer (if available) or from the underlying stream, returning either a uint8 or -1. + private int readUnderlying() throws IOException { + if (byteBufferIndex >= byteBufferLen) { + if (byteBufferLen == -1) + return -1; + byteBufferStartPos += byteBufferLen; + updateCrcs(0); + byteBufferLen = readUnderlying(byteBuffer, 0, byteBuffer.length); + crcStartIndex = 0; + if (byteBufferLen <= 0) + return -1; + byteBufferIndex = 0; + } + assert byteBufferIndex < byteBufferLen; + int temp = byteBuffer[byteBufferIndex] & 0xFF; + byteBufferIndex++; + return temp; + } + + + // Reads up to 'len' bytes from the underlying byte-based input stream into the given array subrange. + // Returns a value in the range [0, len] for a successful read, or -1 if the end of stream was reached. + protected abstract int readUnderlying(byte[] buf, int off, int len) throws IOException; + + + /*-- CRC calculations --*/ + + public void resetCrcs() { + checkByteAligned(); + crcStartIndex = byteBufferIndex - bitBufferLen / 8; + crc8 = 0; + crc16 = 0; + } + + + public int getCrc8() { + checkByteAligned(); + updateCrcs(bitBufferLen / 8); + if ((crc8 >>> 8) != 0) + throw new AssertionError(); + return crc8; + } + + + public int getCrc16() { + checkByteAligned(); + updateCrcs(bitBufferLen / 8); + if ((crc16 >>> 16) != 0) + throw new AssertionError(); + return crc16; + } + + + // Updates the two CRC values with data in byteBuffer[crcStartIndex : byteBufferIndex - unusedTrailingBytes]. + private void updateCrcs(int unusedTrailingBytes) { + int end = byteBufferIndex - unusedTrailingBytes; + for (int i = crcStartIndex; i < end; i++) { + int b = byteBuffer[i] & 0xFF; + crc8 = CRC8_TABLE[crc8 ^ b] & 0xFF; + crc16 = CRC16_TABLE[(crc16 >>> 8) ^ b] ^ ((crc16 & 0xFF) << 8); + assert (crc8 >>> 8) == 0; + assert (crc16 >>> 16) == 0; + } + crcStartIndex = end; + } + + + /*-- Miscellaneous --*/ + + // Note: This class only uses memory and has no native resources. It's not strictly necessary to + // call the implementation of AbstractFlacLowLevelInput.close() here, but it's a good habit anyway. + public void close() throws IOException { + byteBuffer = null; + byteBufferLen = -1; + byteBufferIndex = -1; + bitBuffer = 0; + bitBufferLen = -1; + crc8 = -1; + crc16 = -1; + crcStartIndex = -1; + } + + + + /*---- Tables of constants ----*/ + + // For Rice decoding + + private static final int RICE_DECODING_TABLE_BITS = 13; // Configurable, must be positive + private static final int RICE_DECODING_TABLE_MASK = (1 << RICE_DECODING_TABLE_BITS) - 1; + private static final byte[][] RICE_DECODING_CONSUMED_TABLES = new byte[31][1 << RICE_DECODING_TABLE_BITS]; + private static final int[][] RICE_DECODING_VALUE_TABLES = new int[31][1 << RICE_DECODING_TABLE_BITS]; + private static final int RICE_DECODING_CHUNK = 4; // Configurable, must be positive, and RICE_DECODING_CHUNK * RICE_DECODING_TABLE_BITS <= 64 + + static { + for (int param = 0; param < RICE_DECODING_CONSUMED_TABLES.length; param++) { + byte[] consumed = RICE_DECODING_CONSUMED_TABLES[param]; + int[] values = RICE_DECODING_VALUE_TABLES[param]; + for (int i = 0; ; i++) { + int numBits = (i >>> param) + 1 + param; + if (numBits > RICE_DECODING_TABLE_BITS) + break; + int bits = ((1 << param) | (i & ((1 << param) - 1))); + int shift = RICE_DECODING_TABLE_BITS - numBits; + for (int j = 0; j < (1 << shift); j++) { + consumed[(bits << shift) | j] = (byte)numBits; + values[(bits << shift) | j] = (i >>> 1) ^ -(i & 1); + } + } + if (consumed[0] != 0) + throw new AssertionError(); + } + } + + + // For CRC calculations + + private static byte[] CRC8_TABLE = new byte[256]; + private static char[] CRC16_TABLE = new char[256]; + + static { + for (int i = 0; i < CRC8_TABLE.length; i++) { + int temp8 = i; + int temp16 = i << 8; + for (int j = 0; j < 8; j++) { + temp8 = (temp8 << 1) ^ ((temp8 >>> 7) * 0x107); + temp16 = (temp16 << 1) ^ ((temp16 >>> 15) * 0x18005); + } + CRC8_TABLE[i] = (byte)temp8; + CRC16_TABLE[i] = (char)temp16; + } + } + +} diff --git a/desktop/src/io/nayuki/flac/decode/ByteArrayFlacInput.java b/desktop/src/io/nayuki/flac/decode/ByteArrayFlacInput.java new file mode 100644 index 0000000..2e99809 --- /dev/null +++ b/desktop/src/io/nayuki/flac/decode/ByteArrayFlacInput.java @@ -0,0 +1,86 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.decode; + +import java.io.IOException; +import java.util.Objects; + + +/** + * A FLAC input stream based on a fixed byte array. + */ +public final class ByteArrayFlacInput extends AbstractFlacLowLevelInput { + + /*---- Fields ----*/ + + // The underlying byte array to read from. + private byte[] data; + private int offset; + + + + /*---- Constructors ----*/ + + public ByteArrayFlacInput(byte[] b) { + super(); + data = Objects.requireNonNull(b); + offset = 0; + } + + + + /*---- Methods ----*/ + + public long getLength() { + return data.length; + } + + + public void seekTo(long pos) { + offset = (int)pos; + positionChanged(pos); + } + + + protected int readUnderlying(byte[] buf, int off, int len) { + if (off < 0 || off > buf.length || len < 0 || len > buf.length - off) + throw new ArrayIndexOutOfBoundsException(); + int n = Math.min(data.length - offset, len); + if (n == 0) + return -1; + System.arraycopy(data, offset, buf, off, n); + offset += n; + return n; + } + + + // Discards data buffers and invalidates this stream. Because this class and its superclass + // only use memory and have no native resources, it's okay to simply let a ByteArrayFlacInput + // be garbage-collected without calling close(). + public void close() throws IOException { + if (data != null) { + data = null; + super.close(); + } + } + +} diff --git a/desktop/src/io/nayuki/flac/decode/DataFormatException.java b/desktop/src/io/nayuki/flac/decode/DataFormatException.java new file mode 100644 index 0000000..80e1f51 --- /dev/null +++ b/desktop/src/io/nayuki/flac/decode/DataFormatException.java @@ -0,0 +1,47 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.decode; + + +/** + * Thrown when data being read violates the FLAC file format. + */ +@SuppressWarnings("serial") +public class DataFormatException extends RuntimeException { + + /*---- Constructors ----*/ + + public DataFormatException() { + super(); + } + + + public DataFormatException(String msg) { + super(msg); + } + + + public DataFormatException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/desktop/src/io/nayuki/flac/decode/FlacDecoder.java b/desktop/src/io/nayuki/flac/decode/FlacDecoder.java new file mode 100644 index 0000000..9a2d7cf --- /dev/null +++ b/desktop/src/io/nayuki/flac/decode/FlacDecoder.java @@ -0,0 +1,337 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.decode; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; + +import io.nayuki.flac.common.FrameInfo; +import io.nayuki.flac.common.SeekTable; +import io.nayuki.flac.common.StreamInfo; + +/** + * Handles high-level decoding and seeking in FLAC files. Also returns metadata + * blocks. Every object is stateful, not thread-safe, and needs to be closed. + * Sample usage: + * + *

+ * // Create a decoder
+ *FlacDecoder dec = new FlacDecoder(...);
+ *
+ *// Make the decoder process all metadata blocks internally.
+ *// We could capture the returned data for extra processing.
+ *// We must read all metadata before reading audio data.
+ *while (dec.readAndHandleMetadataBlock() != null);
+ *
+ *// Read audio samples starting from beginning
+ *int[][] samples = (...);
+ *dec.readAudioBlock(samples, ...);
+ *dec.readAudioBlock(samples, ...);
+ *dec.readAudioBlock(samples, ...);
+ *
+ *// Seek to some position and continue reading
+ *dec.seekAndReadAudioBlock(..., samples, ...);
+ *dec.readAudioBlock(samples, ...);
+ *dec.readAudioBlock(samples, ...);
+ *
+ *// Close underlying file stream
+ *dec.close();
+ * 
+ * + * @see FrameDecoder + * @see FlacLowLevelInput + */ +public final class FlacDecoder implements AutoCloseable { + + /*---- Fields ----*/ + + public StreamInfo streamInfo; + public SeekTable seekTable; + + private FlacLowLevelInput input; + + private long metadataEndPos; + + private FrameDecoder frameDec; + + /*---- Constructors ----*/ + + // Constructs a new FLAC decoder to read the given file. + // This immediately reads the basic header but not metadata blocks. + public FlacDecoder(final File file) throws IOException { + // Initialize streams + Objects.requireNonNull(file); + this.input = new SeekableFileFlacInput(file); + + // Read basic header + if (this.input.readUint(32) != 0x664C6143) { + throw new DataFormatException("Invalid magic string"); + } + this.metadataEndPos = -1; + } + + // Constructs a new FLAC decoder to read the given file. + // This immediately reads the basic header but not metadata blocks. + public FlacDecoder(final byte[] file) throws IOException { + // Initialize streams + Objects.requireNonNull(file); + this.input = new ByteArrayFlacInput(file); + + // Read basic header + if (this.input.readUint(32) != 0x664C6143) { + throw new DataFormatException("Invalid magic string"); + } + this.metadataEndPos = -1; + } + + /*---- Methods ----*/ + + // Reads, handles, and returns the next metadata block. Returns a pair (Integer + // type, byte[] data) if the + // next metadata block exists, otherwise returns null if the final metadata + // block was previously read. + // In addition to reading and returning data, this method also updates the + // internal state + // of this object to reflect the new data seen, and throws exceptions for + // situations such as + // not starting with a stream info metadata block or encountering duplicates of + // certain blocks. + public Object[] readAndHandleMetadataBlock() throws IOException { + if (this.metadataEndPos != -1) { + return null; // All metadata already consumed + } + + // Read entire block + final boolean last = this.input.readUint(1) != 0; + final int type = this.input.readUint(7); + final int length = this.input.readUint(24); + final byte[] data = new byte[length]; + this.input.readFully(data); + + // Handle recognized block + if (type == 0) { + if (this.streamInfo != null) { + throw new DataFormatException("Duplicate stream info metadata block"); + } + this.streamInfo = new StreamInfo(data); + } + else { + if (this.streamInfo == null) { + throw new DataFormatException("Expected stream info metadata block"); + } + if (type == 3) { + if (this.seekTable != null) { + throw new DataFormatException("Duplicate seek table metadata block"); + } + this.seekTable = new SeekTable(data); + } + } + + if (last) { + this.metadataEndPos = this.input.getPosition(); + this.frameDec = new FrameDecoder(this.input, this.streamInfo.sampleDepth); + } + return new Object[] { type, data }; + } + + // Reads and decodes the next block of audio samples into the given buffer, + // returning the number of samples in the block. The return value is 0 if the + // read + // started at the end of stream, or a number in the range [1, 65536] for a valid + // block. + // All metadata blocks must be read before starting to read audio blocks. + public int readAudioBlock(final int[][] samples, final int off) throws IOException { + if (this.frameDec == null) { + throw new IllegalStateException("Metadata blocks not fully consumed yet"); + } + final FrameInfo frame = this.frameDec.readFrame(samples, off); + if (frame == null) { + return 0; + } + else { + return frame.blockSize; // In the range [1, 65536] + } + } + + // Seeks to the given sample position and reads audio samples into the given + // buffer, + // returning the number of samples filled. If audio data is available then the + // return value + // is at least 1; otherwise 0 is returned to indicate the end of stream. Note + // that the + // sample position can land in the middle of a FLAC block and will still behave + // correctly. + // In theory this method subsumes the functionality of readAudioBlock(), but + // seeking can be + // an expensive operation so readAudioBlock() should be used for ordinary + // contiguous streaming. + public int seekAndReadAudioBlock(final long pos, final int[][] samples, final int off) throws IOException { + if (this.frameDec == null) { + throw new IllegalStateException("Metadata blocks not fully consumed yet"); + } + + long[] sampleAndFilePos = getBestSeekPoint(pos); + if ((pos - sampleAndFilePos[0]) > 300000) { + sampleAndFilePos = seekBySyncAndDecode(pos); + sampleAndFilePos[1] -= this.metadataEndPos; + } + this.input.seekTo(sampleAndFilePos[1] + this.metadataEndPos); + + long curPos = sampleAndFilePos[0]; + final int[][] smpl = new int[this.streamInfo.numChannels][65536]; + while (true) { + final FrameInfo frame = this.frameDec.readFrame(smpl, 0); + if (frame == null) { + return 0; + } + final long nextPos = curPos + frame.blockSize; + if (nextPos > pos) { + for (int ch = 0; ch < smpl.length; ch++) { + System.arraycopy(smpl[ch], (int) (pos - curPos), samples[ch], off, (int) (nextPos - pos)); + } + return (int) (nextPos - pos); + } + curPos = nextPos; + } + } + + private long[] getBestSeekPoint(final long pos) { + long samplePos = 0; + long filePos = 0; + if (this.seekTable != null) { + for (final SeekTable.SeekPoint p : this.seekTable.points) { + if (p.sampleOffset <= pos) { + samplePos = p.sampleOffset; + filePos = p.fileOffset; + } + else { + break; + } + } + } + return new long[] { samplePos, filePos }; + } + + // Returns a pair (sample offset, file position) such sampleOffset <= pos and + // abs(sampleOffset - pos) + // is a relatively small number compared to the total number of samples in the + // audio file. + // This method works by skipping to arbitrary places in the file, finding a sync + // sequence, + // decoding the frame header, examining the audio position stored in the frame, + // and possibly deciding + // to skip to other places and retrying. This changes the state of the input + // streams as a side effect. + // There is a small chance of finding a valid-looking frame header but causing + // erroneous decoding later. + private long[] seekBySyncAndDecode(final long pos) throws IOException { + long start = this.metadataEndPos; + long end = this.input.getLength(); + while ((end - start) > 100000) { // Binary search + final long mid = (start + end) >>> 1; + final long[] offsets = getNextFrameOffsets(mid); + if ((offsets == null) || (offsets[0] > pos)) { + end = mid; + } + else { + start = offsets[1]; + } + } + return getNextFrameOffsets(start); + } + + // Returns a pair (sample offset, file position) describing the next frame found + // starting + // at the given file offset, or null if no frame is found before the end of + // stream. + // This changes the state of the input streams as a side effect. + private long[] getNextFrameOffsets(long filePos) throws IOException { + if ((filePos < this.metadataEndPos) || (filePos > this.input.getLength())) { + throw new IllegalArgumentException("File position out of bounds"); + } + + // Repeatedly search for a sync + while (true) { + this.input.seekTo(filePos); + + // Finite state machine to match the 2-byte sync sequence + int state = 0; + while (true) { + final int b = this.input.readByte(); + if (b == -1) { + return null; + } + else if (b == 0xFF) { + state = 1; + } + else if ((state == 1) && ((b & 0xFE) == 0xF8)) { + break; + } + else { + state = 0; + } + } + + // Sync found, rewind 2 bytes, try to decode frame header + filePos = this.input.getPosition() - 2; + this.input.seekTo(filePos); + try { + final FrameInfo frame = FrameInfo.readFrame(this.input); + return new long[] { getSampleOffset(frame), filePos }; + } + catch (final DataFormatException e) { + // Advance past the sync and search again + filePos += 2; + } + } + } + + // Calculates the sample offset of the given frame, automatically handling the + // constant-block-size case. + private long getSampleOffset(final FrameInfo frame) { + Objects.requireNonNull(frame); + if (frame.sampleOffset != -1) { + return frame.sampleOffset; + } + else if (frame.frameIndex != -1) { + return frame.frameIndex * this.streamInfo.maxBlockSize; + } + else { + throw new AssertionError(); + } + } + + // Closes the underlying input streams and discards object data. + // This decoder object becomes invalid for any method calls or field usages. + @Override + public void close() throws IOException { + if (this.input != null) { + this.streamInfo = null; + this.seekTable = null; + this.frameDec = null; + this.input.close(); + this.input = null; + } + } + +} diff --git a/desktop/src/io/nayuki/flac/decode/FlacLowLevelInput.java b/desktop/src/io/nayuki/flac/decode/FlacLowLevelInput.java new file mode 100644 index 0000000..24c2a80 --- /dev/null +++ b/desktop/src/io/nayuki/flac/decode/FlacLowLevelInput.java @@ -0,0 +1,129 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.decode; + +import java.io.IOException; + + +/** + * A low-level input stream tailored to the needs of FLAC decoding. An overview of methods includes + * bit reading, CRC calculation, Rice decoding, and positioning and seeking (partly optional). + * @see SeekableFileFlacInput + * @see FrameDecoder + */ +public interface FlacLowLevelInput extends AutoCloseable { + + /*---- Stream position ----*/ + + // Returns the total number of bytes in the FLAC file represented by this input stream. + // This number should not change for the lifetime of this object. Implementing this is optional; + // it's intended to support blind seeking without the use of seek tables, such as binary searching + // the whole file. A class may choose to throw UnsupportedOperationException instead, + // such as for a non-seekable network input stream of unknown length. + public long getLength(); + + + // Returns the current byte position in the stream, a non-negative value. + // This increments after every 8 bits read, and a partially read byte is treated as unread. + // This value is 0 initially, is set directly by seekTo(), and potentially increases + // after every call to a read*() method. Other methods do not affect this value. + public long getPosition(); + + + // Returns the current number of consumed bits in the current byte. This starts at 0, + // increments for each bit consumed, maxes out at 7, then resets to 0 and repeats. + public int getBitPosition(); + + + // Changes the position of the next read to the given byte offset from the start of the stream. + // This also resets CRCs and sets the bit position to 0. + // Implementing this is optional; it is intended to support playback seeking. + // A class may choose to throw UnsupportedOperationException instead. + public void seekTo(long pos) throws IOException; + + + + /*---- Reading bitwise integers ----*/ + + // Reads the next given number of bits (0 <= n <= 32) as an unsigned integer (i.e. zero-extended to int32). + // However in the case of n = 32, the result will be a signed integer that represents a uint32. + public int readUint(int n) throws IOException; + + + // Reads the next given number of bits (0 <= n <= 32) as an signed integer (i.e. sign-extended to int32). + public int readSignedInt(int n) throws IOException; + + + // Reads and decodes the next batch of Rice-coded signed integers. Note that any Rice-coded integer might read a large + // number of bits from the underlying stream (but not in practice because it would be a very inefficient encoding). + // Every new value stored into the array is guaranteed to fit into a signed int53 - see FrameDecoder.restoreLpc() + // for an explanation of why this is a necessary (but not sufficient) bound on the range of decoded values. + public void readRiceSignedInts(int param, long[] result, int start, int end) throws IOException; + + + + /*---- Reading bytes ----*/ + + // Returns the next unsigned byte value (in the range [0, 255]) or -1 for EOF. + // Must be called at a byte boundary (i.e. getBitPosition() == 0), otherwise IllegalStateException is thrown. + public int readByte() throws IOException; + + + // Discards any partial bits, then reads the given array fully or throws EOFException. + // Must be called at a byte boundary (i.e. getBitPosition() == 0), otherwise IllegalStateException is thrown. + public void readFully(byte[] b) throws IOException; + + + + /*---- CRC calculations ----*/ + + // Marks the current byte position as the start of both CRC calculations. + // The effect of resetCrcs() is implied at the beginning of stream and when seekTo() is called. + // Must be called at a byte boundary (i.e. getBitPosition() == 0), otherwise IllegalStateException is thrown. + public void resetCrcs(); + + + // Returns the CRC-8 hash of all the bytes read since the most recent time one of these + // events occurred: a call to resetCrcs(), a call to seekTo(), the beginning of stream. + // Must be called at a byte boundary (i.e. getBitPosition() == 0), otherwise IllegalStateException is thrown. + public int getCrc8(); + + + // Returns the CRC-16 hash of all the bytes read since the most recent time one of these + // events occurred: a call to resetCrcs(), a call to seekTo(), the beginning of stream. + // Must be called at a byte boundary (i.e. getBitPosition() == 0), otherwise IllegalStateException is thrown. + public int getCrc16(); + + + + /*---- Miscellaneous ----*/ + + // Closes underlying objects / native resources, and possibly discards memory buffers. + // Generally speaking, this operation invalidates this input stream, so calling methods + // (other than close()) or accessing fields thereafter should be forbidden. + // The close() method must be idempotent and safe when called more than once. + // If an implementation does not have native or time-sensitive resources, it is okay for the class user + // to skip calling close() and simply let the object be garbage-collected. But out of good habit, it is + // recommended to always close a FlacLowLevelInput stream so that the logic works correctly on all types. + public void close() throws IOException; + +} diff --git a/desktop/src/io/nayuki/flac/decode/FrameDecoder.java b/desktop/src/io/nayuki/flac/decode/FrameDecoder.java new file mode 100644 index 0000000..7d1d12d --- /dev/null +++ b/desktop/src/io/nayuki/flac/decode/FrameDecoder.java @@ -0,0 +1,387 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.decode; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; +import io.nayuki.flac.common.FrameInfo; + + +/** + * Decodes a FLAC frame from an input stream into raw audio samples. Note that these objects are + * stateful and not thread-safe, due to the bit input stream field, private temporary arrays, etc. + *

This class only uses memory and has no native resources; however, the + * code that uses this class is responsible for cleaning up the input stream.

+ * @see FlacDecoder + * @see FlacLowLevelInput + */ +public final class FrameDecoder { + + /*---- Fields ----*/ + + // Can be changed when there is no active call of readFrame(). + // Must be not null when readFrame() is called. + public FlacLowLevelInput in; + + // Can be changed when there is no active call of readFrame(). + // Must be in the range [4, 32]. + public int expectedSampleDepth; + + // Temporary arrays to hold two decoded audio channels (a.k.a. subframes). They have int64 range + // because the worst case of 32-bit audio encoded in stereo side mode uses signed 33 bits. + // The maximum possible block size is either 65536 samples per channel from the + // frame header logic, or 65535 from a strict reading of the FLAC specification. + // Two buffers are needed for stereo coding modes, but not more than two because + // all other multi-channel audio is processed independently per channel. + private long[] temp0; + private long[] temp1; + + // The number of samples (per channel) in the current block/frame being processed. + // This value is only valid while the method readFrame() is on the call stack. + // When readFrame() is active, this value is in the range [1, 65536]. + private int currentBlockSize; + + + + /*---- Constructors ----*/ + + // Constructs a frame decoder that initially uses the given stream. + // The caller is responsible for cleaning up the input stream. + public FrameDecoder(FlacLowLevelInput in, int expectDepth) { + this.in = in; + expectedSampleDepth = expectDepth; + temp0 = new long[65536]; + temp1 = new long[65536]; + currentBlockSize = -1; + } + + + + /*---- Methods ----*/ + + // Reads the next frame of FLAC data from the current bit input stream, decodes it, + // and stores output samples into the given array, and returns a new metadata object. + // The bit input stream must be initially aligned at a byte boundary. If EOF is encountered before + // any actual bytes were read, then this returns null. Otherwise this function either successfully + // decodes a frame and returns a new metadata object, or throws an appropriate exception. A frame + // may have up to 8 channels and 65536 samples, so the output arrays need to be sized appropriately. + public FrameInfo readFrame(int[][] outSamples, int outOffset) throws IOException { + // Check field states + Objects.requireNonNull(in); + if (currentBlockSize != -1) + throw new IllegalStateException("Concurrent call"); + + // Parse the frame header to see if one is available + long startByte = in.getPosition(); + FrameInfo meta = FrameInfo.readFrame(in); + if (meta == null) // EOF occurred cleanly + return null; + if (meta.sampleDepth != -1 && meta.sampleDepth != expectedSampleDepth) + throw new DataFormatException("Sample depth mismatch"); + + // Check arguments and read frame header + currentBlockSize = meta.blockSize; + Objects.requireNonNull(outSamples); + if (outOffset < 0 || outOffset > outSamples[0].length) + throw new IndexOutOfBoundsException(); + if (outSamples.length < meta.numChannels) + throw new IllegalArgumentException("Output array too small for number of channels"); + if (outOffset > outSamples[0].length - currentBlockSize) + throw new IndexOutOfBoundsException(); + + // Do the hard work + decodeSubframes(expectedSampleDepth, meta.channelAssignment, outSamples, outOffset); + + // Read padding and footer + if (in.readUint((8 - in.getBitPosition()) % 8) != 0) + throw new DataFormatException("Invalid padding bits"); + int computedCrc16 = in.getCrc16(); + if (in.readUint(16) != computedCrc16) + throw new DataFormatException("CRC-16 mismatch"); + + // Handle frame size and miscellaneous + long frameSize = in.getPosition() - startByte; + if (frameSize < 10) + throw new AssertionError(); + if ((int)frameSize != frameSize) + throw new DataFormatException("Frame size too large"); + meta.frameSize = (int)frameSize; + currentBlockSize = -1; + return meta; + } + + + // Based on the current bit input stream and the two given arguments, this method reads and decodes + // each subframe, performs stereo decoding if applicable, and writes the final uncompressed audio data + // to the array range outSamples[0 : numChannels][outOffset : outOffset + currentBlockSize]. + // Note that this method uses the private temporary arrays and passes them into sub-method calls. + private void decodeSubframes(int sampleDepth, int chanAsgn, int[][] outSamples, int outOffset) throws IOException { + // Check arguments + if (sampleDepth < 1 || sampleDepth > 32) + throw new IllegalArgumentException(); + if ((chanAsgn >>> 4) != 0) + throw new IllegalArgumentException(); + + if (0 <= chanAsgn && chanAsgn <= 7) { + // Handle 1 to 8 independently coded channels + int numChannels = chanAsgn + 1; + for (int ch = 0; ch < numChannels; ch++) { + decodeSubframe(sampleDepth, temp0); + int[] outChan = outSamples[ch]; + for (int i = 0; i < currentBlockSize; i++) + outChan[outOffset + i] = checkBitDepth(temp0[i], sampleDepth); + } + + } else if (8 <= chanAsgn && chanAsgn <= 10) { + // Handle one of the side-coded stereo methods + decodeSubframe(sampleDepth + (chanAsgn == 9 ? 1 : 0), temp0); + decodeSubframe(sampleDepth + (chanAsgn == 9 ? 0 : 1), temp1); + + if (chanAsgn == 8) { // Left-side stereo + for (int i = 0; i < currentBlockSize; i++) + temp1[i] = temp0[i] - temp1[i]; + } else if (chanAsgn == 9) { // Side-right stereo + for (int i = 0; i < currentBlockSize; i++) + temp0[i] += temp1[i]; + } else if (chanAsgn == 10) { // Mid-side stereo + for (int i = 0; i < currentBlockSize; i++) { + long side = temp1[i]; + long right = temp0[i] - (side >> 1); + temp1[i] = right; + temp0[i] = right + side; + } + } else + throw new AssertionError(); + + // Copy data from temporary to output arrays, and convert from long to int + int[] outLeft = outSamples[0]; + int[] outRight = outSamples[1]; + for (int i = 0; i < currentBlockSize; i++) { + outLeft [outOffset + i] = checkBitDepth(temp0[i], sampleDepth); + outRight[outOffset + i] = checkBitDepth(temp1[i], sampleDepth); + } + } else // 11 <= channelAssignment <= 15 + throw new DataFormatException("Reserved channel assignment"); + } + + + // Checks that 'val' is a signed 'depth'-bit integer, and either returns the + // value downcasted to an int or throws an exception if it's out of range. + // Note that depth must be in the range [1, 32] because the return value is an int. + // For example when depth = 16, the range of valid values is [-32768, 32767]. + private static int checkBitDepth(long val, int depth) { + assert 1 <= depth && depth <= 32; + // Equivalent check: (val >> (depth - 1)) == 0 || (val >> (depth - 1)) == -1 + if (val >> (depth - 1) == val >> depth) + return (int)val; + else + throw new IllegalArgumentException(val + " is not a signed " + depth + "-bit value"); + } + + + // Reads one subframe from the bit input stream, decodes it, and writes to result[0 : currentBlockSize]. + private void decodeSubframe(int sampleDepth, long[] result) throws IOException { + // Check arguments + Objects.requireNonNull(result); + if (sampleDepth < 1 || sampleDepth > 33) + throw new IllegalArgumentException(); + if (result.length < currentBlockSize) + throw new IllegalArgumentException(); + + // Read header fields + if (in.readUint(1) != 0) + throw new DataFormatException("Invalid padding bit"); + int type = in.readUint(6); + int shift = in.readUint(1); // Also known as "wasted bits-per-sample" + if (shift == 1) { + while (in.readUint(1) == 0) { // Unary coding + if (shift >= sampleDepth) + throw new DataFormatException("Waste-bits-per-sample exceeds sample depth"); + shift++; + } + } + if (!(0 <= shift && shift <= sampleDepth)) + throw new AssertionError(); + sampleDepth -= shift; + + // Read sample data based on type + if (type == 0) // Constant coding + Arrays.fill(result, 0, currentBlockSize, in.readSignedInt(sampleDepth)); + else if (type == 1) { // Verbatim coding + for (int i = 0; i < currentBlockSize; i++) + result[i] = in.readSignedInt(sampleDepth); + } else if (8 <= type && type <= 12) + decodeFixedPredictionSubframe(type - 8, sampleDepth, result); + else if (32 <= type && type <= 63) + decodeLinearPredictiveCodingSubframe(type - 31, sampleDepth, result); + else + throw new DataFormatException("Reserved subframe type"); + + // Add trailing zeros to each sample + if (shift > 0) { + for (int i = 0; i < currentBlockSize; i++) + result[i] <<= shift; + } + } + + + // Reads from the input stream, performs computation, and writes to result[0 : currentBlockSize]. + private void decodeFixedPredictionSubframe(int predOrder, int sampleDepth, long[] result) throws IOException { + // Check arguments + Objects.requireNonNull(result); + if (sampleDepth < 1 || sampleDepth > 33) + throw new IllegalArgumentException(); + if (predOrder < 0 || predOrder >= FIXED_PREDICTION_COEFFICIENTS.length) + throw new IllegalArgumentException(); + if (predOrder > currentBlockSize) + throw new DataFormatException("Fixed prediction order exceeds block size"); + if (result.length < currentBlockSize) + throw new IllegalArgumentException(); + + // Read and compute various values + for (int i = 0; i < predOrder; i++) // Non-Rice-coded warm-up samples + result[i] = in.readSignedInt(sampleDepth); + readResiduals(predOrder, result); + restoreLpc(result, FIXED_PREDICTION_COEFFICIENTS[predOrder], sampleDepth, 0); + } + + private static final int[][] FIXED_PREDICTION_COEFFICIENTS = { + {}, + {1}, + {2, -1}, + {3, -3, 1}, + {4, -6, 4, -1}, + }; + + + // Reads from the input stream, performs computation, and writes to result[0 : currentBlockSize]. + private void decodeLinearPredictiveCodingSubframe(int lpcOrder, int sampleDepth, long[] result) throws IOException { + // Check arguments + Objects.requireNonNull(result); + if (sampleDepth < 1 || sampleDepth > 33) + throw new IllegalArgumentException(); + if (lpcOrder < 1 || lpcOrder > 32) + throw new IllegalArgumentException(); + if (lpcOrder > currentBlockSize) + throw new DataFormatException("LPC order exceeds block size"); + if (result.length < currentBlockSize) + throw new IllegalArgumentException(); + + // Read non-Rice-coded warm-up samples + for (int i = 0; i < lpcOrder; i++) + result[i] = in.readSignedInt(sampleDepth); + + // Read parameters for the LPC coefficients + int precision = in.readUint(4) + 1; + if (precision == 16) + throw new DataFormatException("Invalid LPC precision"); + int shift = in.readSignedInt(5); + if (shift < 0) + throw new DataFormatException("Invalid LPC shift"); + + // Read the coefficients themselves + int[] coefs = new int[lpcOrder]; + for (int i = 0; i < coefs.length; i++) + coefs[i] = in.readSignedInt(precision); + + // Perform the main LPC decoding + readResiduals(lpcOrder, result); + restoreLpc(result, coefs, sampleDepth, shift); + } + + + // Updates the values of result[coefs.length : currentBlockSize] according to linear predictive coding. + // This method reads all the arguments and the field currentBlockSize, only writes to result, and has no other side effects. + // After this method returns, every value in result must fit in a signed sampleDepth-bit integer. + // The largest allowed sample depth is 33, hence the largest absolute value allowed in the result is 2^32. + // During the LPC restoration process, the prefix of result before index i consists of entirely int33 values. + // Because coefs.length <= 32 and each coefficient fits in a signed int15 (both according to the FLAC specification), + // the maximum (worst-case) absolute value of 'sum' is 2^32 * 2^14 * 32 = 2^51, which fits in a signed int53. + // And because of this, the maximum possible absolute value of a residual before LPC restoration is applied, + // such that the post-LPC result fits in a signed int33, is 2^51 + 2^32 which also fits in a signed int53. + // Therefore a residue that is larger than a signed int53 will necessarily not fit in the int33 result and is wrong. + private void restoreLpc(long[] result, int[] coefs, int sampleDepth, int shift) { + // Check and handle arguments + Objects.requireNonNull(result); + Objects.requireNonNull(coefs); + if (result.length < currentBlockSize) + throw new IllegalArgumentException(); + if (sampleDepth < 1 || sampleDepth > 33) + throw new IllegalArgumentException(); + if (shift < 0 || shift > 63) + throw new IllegalArgumentException(); + long lowerBound = (-1) << (sampleDepth - 1); + long upperBound = -(lowerBound + 1); + + for (int i = coefs.length; i < currentBlockSize; i++) { + long sum = 0; + for (int j = 0; j < coefs.length; j++) + sum += result[i - 1 - j] * coefs[j]; + assert (sum >> 53) == 0 || (sum >> 53) == -1; // Fits in signed int54 + sum = result[i] + (sum >> shift); + // Check that sum fits in a sampleDepth-bit signed integer, + // i.e. -(2^(sampleDepth-1)) <= sum < 2^(sampleDepth-1) + if (sum < lowerBound || sum > upperBound) + throw new DataFormatException("Post-LPC result exceeds bit depth"); + result[i] = sum; + } + } + + + // Reads metadata and Rice-coded numbers from the input stream, storing them in result[warmup : currentBlockSize]. + // The stored numbers are guaranteed to fit in a signed int53 - see the explanation in restoreLpc(). + private void readResiduals(int warmup, long[] result) throws IOException { + // Check and handle arguments + Objects.requireNonNull(result); + if (warmup < 0 || warmup > currentBlockSize) + throw new IllegalArgumentException(); + if (result.length < currentBlockSize) + throw new IllegalArgumentException(); + + int method = in.readUint(2); + if (method >= 2) + throw new DataFormatException("Reserved residual coding method"); + assert method == 0 || method == 1; + int paramBits = method == 0 ? 4 : 5; + int escapeParam = method == 0 ? 0xF : 0x1F; + + int partitionOrder = in.readUint(4); + int numPartitions = 1 << partitionOrder; + if (currentBlockSize % numPartitions != 0) + throw new DataFormatException("Block size not divisible by number of Rice partitions"); + for (int inc = currentBlockSize >>> partitionOrder, partEnd = inc, resultIndex = warmup; + partEnd <= currentBlockSize; partEnd += inc) { + + int param = in.readUint(paramBits); + if (param == escapeParam) { + int numBits = in.readUint(5); + for (; resultIndex < partEnd; resultIndex++) + result[resultIndex] = in.readSignedInt(numBits); + } else { + in.readRiceSignedInts(param, result, resultIndex, partEnd); + resultIndex = partEnd; + } + } + } + +} diff --git a/desktop/src/io/nayuki/flac/decode/SeekableFileFlacInput.java b/desktop/src/io/nayuki/flac/decode/SeekableFileFlacInput.java new file mode 100644 index 0000000..f446ff0 --- /dev/null +++ b/desktop/src/io/nayuki/flac/decode/SeekableFileFlacInput.java @@ -0,0 +1,83 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.decode; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Objects; + + +/** + * A FLAC input stream based on a {@link RandomAccessFile}. + */ +public final class SeekableFileFlacInput extends AbstractFlacLowLevelInput { + + /*---- Fields ----*/ + + // The underlying byte-based input stream to read from. + private RandomAccessFile raf; + + + + /*---- Constructors ----*/ + + public SeekableFileFlacInput(File file) throws IOException { + super(); + Objects.requireNonNull(file); + this.raf = new RandomAccessFile(file, "r"); + } + + + + /*---- Methods ----*/ + + public long getLength() { + try { + return raf.length(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + public void seekTo(long pos) throws IOException { + raf.seek(pos); + positionChanged(pos); + } + + + protected int readUnderlying(byte[] buf, int off, int len) throws IOException { + return raf.read(buf, off, len); + } + + + // Closes the underlying RandomAccessFile stream (very important). + public void close() throws IOException { + if (raf != null) { + raf.close(); + raf = null; + super.close(); + } + } + +} diff --git a/desktop/src/io/nayuki/flac/encode/AdvancedFlacEncoder.java b/desktop/src/io/nayuki/flac/encode/AdvancedFlacEncoder.java new file mode 100644 index 0000000..17004ec --- /dev/null +++ b/desktop/src/io/nayuki/flac/encode/AdvancedFlacEncoder.java @@ -0,0 +1,115 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.encode; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import io.nayuki.flac.common.StreamInfo; + + +public final class AdvancedFlacEncoder { + + public AdvancedFlacEncoder(StreamInfo info, int[][] samples, int baseSize, int[] sizeMultiples, SubframeEncoder.SearchOptions opts, BitOutputStream out) throws IOException { + int numSamples = samples[0].length; + + // Calculate compressed sizes for many block positions and sizes + @SuppressWarnings("unchecked") + SizeEstimate[][] encoderInfo = new SizeEstimate[sizeMultiples.length][(numSamples + baseSize - 1) / baseSize]; + long startTime = System.currentTimeMillis(); + for (int i = 0; i < encoderInfo[0].length; i++) { + double progress = (double)i / encoderInfo[0].length; + double timeRemain = (System.currentTimeMillis() - startTime) / 1000.0 / progress * (1 - progress); + System.err.printf("\rprogress=%.2f%% timeRemain=%ds", progress * 100, Math.round(timeRemain)); + + int pos = i * baseSize; + for (int j = 0; j < encoderInfo.length; j++) { + int n = Math.min(sizeMultiples[j] * baseSize, numSamples - pos); + long[][] subsamples = getRange(samples, pos, n); + encoderInfo[j][i] = FrameEncoder.computeBest(pos, subsamples, info.sampleDepth, info.sampleRate, opts); + } + } + System.err.println(); + + // Initialize arrays to prepare for dynamic programming + FrameEncoder[] bestEncoders = new FrameEncoder[encoderInfo[0].length]; + long[] bestSizes = new long[bestEncoders.length]; + Arrays.fill(bestSizes, Long.MAX_VALUE); + + // Use dynamic programming to calculate optimum block size switching + for (int i = 0; i < encoderInfo.length; i++) { + for (int j = bestSizes.length - 1; j >= 0; j--) { + long size = encoderInfo[i][j].sizeEstimate; + if (j + sizeMultiples[i] < bestSizes.length) + size += bestSizes[j + sizeMultiples[i]]; + if (size < bestSizes[j]) { + bestSizes[j] = size; + bestEncoders[j] = encoderInfo[i][j].encoder; + } + } + } + + // Do the actual encoding and writing + info.minBlockSize = 0; + info.maxBlockSize = 0; + info.minFrameSize = 0; + info.maxFrameSize = 0; + List blockSizes = new ArrayList<>(); + for (int i = 0; i < bestEncoders.length; ) { + FrameEncoder enc = bestEncoders[i]; + int pos = i * baseSize; + int n = Math.min(enc.metadata.blockSize, numSamples - pos); + blockSizes.add(n); + if (info.minBlockSize == 0 || n < info.minBlockSize) + info.minBlockSize = Math.max(n, 16); + info.maxBlockSize = Math.max(n, info.maxBlockSize); + + long[][] subsamples = getRange(samples, pos, n); + long startByte = out.getByteCount(); + bestEncoders[i].encode(subsamples, out); + i += (n + baseSize - 1) / baseSize; + + long frameSize = out.getByteCount() - startByte; + if (frameSize < 0 || (int)frameSize != frameSize) + throw new AssertionError(); + if (info.minFrameSize == 0 || frameSize < info.minFrameSize) + info.minFrameSize = (int)frameSize; + if (frameSize > info.maxFrameSize) + info.maxFrameSize = (int)frameSize; + } + } + + + // Returns the subrange array[ : ][off : off + len] upcasted to long. + private static long[][] getRange(int[][] array, int off, int len) { + long[][] result = new long[array.length][len]; + for (int i = 0; i < array.length; i++) { + int[] src = array[i]; + long[] dest = result[i]; + for (int j = 0; j < len; j++) + dest[j] = src[off + j]; + } + return result; + } + +} diff --git a/desktop/src/io/nayuki/flac/encode/BitOutputStream.java b/desktop/src/io/nayuki/flac/encode/BitOutputStream.java new file mode 100644 index 0000000..b7a2dc5 --- /dev/null +++ b/desktop/src/io/nayuki/flac/encode/BitOutputStream.java @@ -0,0 +1,174 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.encode; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Objects; + + +/* + * A bit-oriented output stream, with other methods tailored for FLAC usage (such as CRC calculation). + */ +public final class BitOutputStream implements AutoCloseable { + + /*---- Fields ----*/ + + private OutputStream out; // The underlying byte-based output stream to write to. + private long bitBuffer; // Only the bottom bitBufferLen bits are valid; the top bits are garbage. + private int bitBufferLen; // Always in the range [0, 64]. + private long byteCount; // Number of bytes written since the start of stream. + + // Current state of the CRC calculations. + private int crc8; // Always a uint8 value. + private int crc16; // Always a uint16 value. + + + + /*---- Constructors ----*/ + + // Constructs a FLAC-oriented bit output stream from the given byte-based output stream. + public BitOutputStream(OutputStream out) throws IOException { + this.out = Objects.requireNonNull(out); + bitBuffer = 0; + bitBufferLen = 0; + byteCount = 0; + resetCrcs(); + } + + + + /*---- Methods ----*/ + + /*-- Bit position --*/ + + // Writes between 0 to 7 zero bits, to align the current bit position to a byte boundary. + public void alignToByte() throws IOException { + writeInt((64 - bitBufferLen) % 8, 0); + } + + + // Either returns silently or throws an exception. + private void checkByteAligned() { + if (bitBufferLen % 8 != 0) + throw new IllegalStateException("Not at a byte boundary"); + } + + + /*-- Writing bitwise integers --*/ + + // Writes the lowest n bits of the given value to this bit output stream. + // This doesn't care whether val represents a signed or unsigned integer. + public void writeInt(int n, int val) throws IOException { + if (n < 0 || n > 32) + throw new IllegalArgumentException(); + + if (bitBufferLen + n > 64) { + flush(); + assert bitBufferLen + n <= 64; + } + bitBuffer <<= n; + bitBuffer |= val & ((1L << n) - 1); + bitBufferLen += n; + assert 0 <= bitBufferLen && bitBufferLen <= 64; + } + + + // Writes out whole bytes from the bit buffer to the underlying stream. After this is done, + // only 0 to 7 bits remain in the bit buffer. Also updates the CRCs on each byte written. + public void flush() throws IOException { + while (bitBufferLen >= 8) { + bitBufferLen -= 8; + int b = (int)(bitBuffer >>> bitBufferLen) & 0xFF; + out.write(b); + byteCount++; + crc8 ^= b; + crc16 ^= b << 8; + for (int i = 0; i < 8; i++) { + crc8 <<= 1; + crc16 <<= 1; + crc8 ^= (crc8 >>> 8) * 0x107; + crc16 ^= (crc16 >>> 16) * 0x18005; + assert (crc8 >>> 8) == 0; + assert (crc16 >>> 16) == 0; + } + } + assert 0 <= bitBufferLen && bitBufferLen <= 64; + out.flush(); + } + + + /*-- CRC calculations --*/ + + // Marks the current position (which must be byte-aligned) as the start of both CRC calculations. + public void resetCrcs() throws IOException { + flush(); + crc8 = 0; + crc16 = 0; + } + + + // Returns the CRC-8 hash of all the bytes written since the last call to resetCrcs() + // (or from the beginning of stream if reset was never called). + public int getCrc8() throws IOException { + checkByteAligned(); + flush(); + if ((crc8 >>> 8) != 0) + throw new AssertionError(); + return crc8; + } + + + // Returns the CRC-16 hash of all the bytes written since the last call to resetCrcs() + // (or from the beginning of stream if reset was never called). + public int getCrc16() throws IOException { + checkByteAligned(); + flush(); + if ((crc16 >>> 16) != 0) + throw new AssertionError(); + return crc16; + } + + + /*-- Miscellaneous --*/ + + // Returns the number of bytes written since the start of the stream. + public long getByteCount() { + return byteCount + bitBufferLen / 8; + } + + + // Writes out any internally buffered bit data, closes the underlying output stream, and invalidates this + // bit output stream object for any future operation. Note that a BitOutputStream only uses memory but + // does not have native resources. It is okay to flush() the pending data and simply let a BitOutputStream + // be garbage collected without calling close(), but the parent is still responsible for calling close() + // on the underlying output stream if it uses native resources (such as FileOutputStream or SocketOutputStream). + public void close() throws IOException { + if (out != null) { + checkByteAligned(); + flush(); + out.close(); + out = null; + } + } + +} diff --git a/desktop/src/io/nayuki/flac/encode/ConstantEncoder.java b/desktop/src/io/nayuki/flac/encode/ConstantEncoder.java new file mode 100644 index 0000000..ddaa707 --- /dev/null +++ b/desktop/src/io/nayuki/flac/encode/ConstantEncoder.java @@ -0,0 +1,79 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.encode; + +import java.io.IOException; + + +/* + * Under the constant coding mode, this provides size calculations on and bitstream encoding of audio sample data. + * Note that unlike the other subframe encoders which are fully general, not all data can be encoded using constant mode. + * The encoded size depends on the shift and bit depth, but not on the data length or contents. + */ +final class ConstantEncoder extends SubframeEncoder { + + // Computes the best way to encode the given values under the constant coding mode, + // returning an exact size plus a new encoder object associated with the input arguments. + // However if the sample data is non-constant then null is returned instead, + // to indicate that the data is impossible to represent in this mode. + public static SizeEstimate computeBest(long[] samples, int shift, int depth) { + if (!isConstant(samples)) + return null; + ConstantEncoder enc = new ConstantEncoder(samples, shift, depth); + long size = 1 + 6 + 1 + shift + depth; + return new SizeEstimate(size, enc); + } + + + // Constructs a constant encoder for the given data, right shift, and sample depth. + public ConstantEncoder(long[] samples, int shift, int depth) { + super(shift, depth); + } + + + // Encodes the given vector of audio sample data to the given bit output stream using + // the this encoding method (and the superclass fields sampleShift and sampleDepth). + // This requires the data array to have the same values (but not necessarily + // the same object reference) as the array that was passed to the constructor. + public void encode(long[] samples, BitOutputStream out) throws IOException { + if (!isConstant(samples)) + throw new IllegalArgumentException("Data is not constant-valued"); + if ((samples[0] >> sampleShift) << sampleShift != samples[0]) + throw new IllegalArgumentException("Invalid shift value for data"); + writeTypeAndShift(0, out); + writeRawSample(samples[0] >> sampleShift, out); + } + + + // Returns true iff the set of unique values in the array has size exactly 1. Pure function. + private static boolean isConstant(long[] data) { + if (data.length == 0) + return false; + long val = data[0]; + for (long x : data) { + if (x != val) + return false; + } + return true; + } + +} diff --git a/desktop/src/io/nayuki/flac/encode/FastDotProduct.java b/desktop/src/io/nayuki/flac/encode/FastDotProduct.java new file mode 100644 index 0000000..aaa4305 --- /dev/null +++ b/desktop/src/io/nayuki/flac/encode/FastDotProduct.java @@ -0,0 +1,97 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.encode; + +import java.util.Objects; + + +/* + * Speeds up computations of a signal vector's autocorrelation by avoiding redundant + * arithmetic operations. Acts as a helper class for LinearPredictiveEncoder. + * Objects of this class are intended to be immutable, but can't enforce it because + * they store a reference to a caller-controlled array without making a private copy. + */ +final class FastDotProduct { + + /*---- Fields ----*/ + + // Not null, and precomputed.length <= data.length. + private long[] data; + + // precomputed[i] = dotProduct(0, i, data.length - i). In other words, it is the sum + // the of products of all unordered pairs of elements whose indices differ by i. + private double[] precomputed; + + + + /*---- Constructors ----*/ + + // Constructs a fast dot product calculator over the given array, with the given maximum difference in indexes. + // This pre-calculates some dot products and caches them so that future queries can be answered faster. + // To avoid the cost of copying the entire vector, a reference to the array is saved into this object. + // The values from data array are still needed when dotProduct() is called, thus no other code is allowed to modify the values. + public FastDotProduct(long[] data, int maxDelta) { + // Check arguments + this.data = Objects.requireNonNull(data); + if (maxDelta < 0 || maxDelta >= data.length) + throw new IllegalArgumentException(); + + // Precompute some dot products + precomputed = new double[maxDelta + 1]; + for (int i = 0; i < precomputed.length; i++) { + double sum = 0; + for (int j = 0; i + j < data.length; j++) + sum += (double)data[j] * data[i + j]; + precomputed[i] = sum; + } + } + + + + /*---- Methods ----*/ + + // Returns the dot product of data[off0 : off0 + len] with data[off1 : off1 + len], + // i.e. data[off0]*data[off1] + data[off0+1]*data[off1+1] + ... + data[off0+len-1]*data[off1+len-1], + // with potential rounding error. Note that all the endpoints must lie within the bounds + // of the data array. Also, this method requires abs(off0 - off1) <= maxDelta. + public double dotProduct(int off0, int off1, int len) { + if (off0 > off1) // Symmetric case + return dotProduct(off1, off0, len); + + // Check arguments + if (off0 < 0 || off1 < 0 || len < 0 || data.length - len < off1) + throw new IndexOutOfBoundsException(); + assert off0 <= off1; + int delta = off1 - off0; + if (delta > precomputed.length) + throw new IllegalArgumentException(); + + // Add up a small number of products to remove from the precomputed sum + double removal = 0; + for (int i = 0; i < off0; i++) + removal += (double)data[i] * data[i + delta]; + for (int i = off1 + len; i < data.length; i++) + removal += (double)data[i] * data[i - delta]; + return precomputed[delta] - removal; + } + +} diff --git a/desktop/src/io/nayuki/flac/encode/FixedPredictionEncoder.java b/desktop/src/io/nayuki/flac/encode/FixedPredictionEncoder.java new file mode 100644 index 0000000..498ad39 --- /dev/null +++ b/desktop/src/io/nayuki/flac/encode/FixedPredictionEncoder.java @@ -0,0 +1,85 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.encode; + +import java.io.IOException; +import java.util.Objects; + + +/* + * Under the fixed prediction coding mode of some order, this provides size calculations on and bitstream encoding of audio sample data. + */ +final class FixedPredictionEncoder extends SubframeEncoder { + + // Computes the best way to encode the given values under the fixed prediction coding mode of the given order, + // returning a size plus a new encoder object associated with the input arguments. The maxRiceOrder argument + // is used by the Rice encoder to estimate the size of coding the residual signal. + public static SizeEstimate computeBest(long[] samples, int shift, int depth, int order, int maxRiceOrder) { + FixedPredictionEncoder enc = new FixedPredictionEncoder(samples, shift, depth, order); + samples = LinearPredictiveEncoder.shiftRight(samples, shift); + LinearPredictiveEncoder.applyLpc(samples, COEFFICIENTS[order], 0); + long temp = RiceEncoder.computeBestSizeAndOrder(samples, order, maxRiceOrder); + enc.riceOrder = (int)(temp & 0xF); + long size = 1 + 6 + 1 + shift + order * depth + (temp >>> 4); + return new SizeEstimate(size, enc); + } + + + + private final int order; + public int riceOrder; + + + public FixedPredictionEncoder(long[] samples, int shift, int depth, int order) { + super(shift, depth); + if (order < 0 || order >= COEFFICIENTS.length || samples.length < order) + throw new IllegalArgumentException(); + this.order = order; + } + + + public void encode(long[] samples, BitOutputStream out) throws IOException { + Objects.requireNonNull(samples); + Objects.requireNonNull(out); + if (samples.length < order) + throw new IllegalArgumentException(); + + writeTypeAndShift(8 + order, out); + samples = LinearPredictiveEncoder.shiftRight(samples, sampleShift); + + for (int i = 0; i < order; i++) // Warmup + writeRawSample(samples[i], out); + LinearPredictiveEncoder.applyLpc(samples, COEFFICIENTS[order], 0); + RiceEncoder.encode(samples, order, riceOrder, out); + } + + + // The linear predictive coding (LPC) coefficients for fixed prediction of orders 0 to 4 (inclusive). + private static final int[][] COEFFICIENTS = { + {}, + {1}, + {2, -1}, + {3, -3, 1}, + {4, -6, 4, -1}, + }; + +} diff --git a/desktop/src/io/nayuki/flac/encode/FlacEncoder.java b/desktop/src/io/nayuki/flac/encode/FlacEncoder.java new file mode 100644 index 0000000..d9e9c66 --- /dev/null +++ b/desktop/src/io/nayuki/flac/encode/FlacEncoder.java @@ -0,0 +1,67 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.encode; + +import java.io.IOException; +import io.nayuki.flac.common.StreamInfo; + + +public final class FlacEncoder { + + public FlacEncoder(StreamInfo info, int[][] samples, int blockSize, SubframeEncoder.SearchOptions opt, BitOutputStream out) throws IOException { + info.minBlockSize = blockSize; + info.maxBlockSize = blockSize; + info.minFrameSize = 0; + info.maxFrameSize = 0; + + for (int i = 0, pos = 0; pos < samples[0].length; i++) { + System.err.printf("frame=%d position=%d %.2f%%%n", i, pos, 100.0 * pos / samples[0].length); + int n = Math.min(samples[0].length - pos, blockSize); + long[][] subsamples = getRange(samples, pos, n); + FrameEncoder enc = FrameEncoder.computeBest(pos, subsamples, info.sampleDepth, info.sampleRate, opt).encoder; + long startByte = out.getByteCount(); + enc.encode(subsamples, out); + long frameSize = out.getByteCount() - startByte; + if (frameSize < 0 || (int)frameSize != frameSize) + throw new AssertionError(); + if (info.minFrameSize == 0 || frameSize < info.minFrameSize) + info.minFrameSize = (int)frameSize; + if (frameSize > info.maxFrameSize) + info.maxFrameSize = (int)frameSize; + pos += n; + } + } + + + // Returns the subrange array[ : ][off : off + len] upcasted to long. + private static long[][] getRange(int[][] array, int off, int len) { + long[][] result = new long[array.length][len]; + for (int i = 0; i < array.length; i++) { + int[] src = array[i]; + long[] dest = result[i]; + for (int j = 0; j < len; j++) + dest[j] = src[off + j]; + } + return result; + } + +} diff --git a/desktop/src/io/nayuki/flac/encode/FrameEncoder.java b/desktop/src/io/nayuki/flac/encode/FrameEncoder.java new file mode 100644 index 0000000..6d1cf18 --- /dev/null +++ b/desktop/src/io/nayuki/flac/encode/FrameEncoder.java @@ -0,0 +1,174 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.encode; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Objects; +import io.nayuki.flac.common.FrameInfo; + + +/* + * Calculates/estimates the encoded size of a frame of audio sample data + * (including the frame header), and also performs the encoding to an output stream. + */ +final class FrameEncoder { + + /*---- Static functions ----*/ + + public static SizeEstimate computeBest(int sampleOffset, long[][] samples, int sampleDepth, int sampleRate, SubframeEncoder.SearchOptions opt) { + FrameEncoder enc = new FrameEncoder(sampleOffset, samples, sampleDepth, sampleRate); + int numChannels = samples.length; + @SuppressWarnings("unchecked") + SizeEstimate[] encoderInfo = new SizeEstimate[numChannels]; + if (numChannels != 2) { + enc.metadata.channelAssignment = numChannels - 1; + for (int i = 0; i < encoderInfo.length; i++) + encoderInfo[i] = SubframeEncoder.computeBest(samples[i], sampleDepth, opt); + } else { // Explore the 4 stereo encoding modes + long[] left = samples[0]; + long[] right = samples[1]; + long[] mid = new long[samples[0].length]; + long[] side = new long[mid.length]; + for (int i = 0; i < mid.length; i++) { + mid[i] = (left[i] + right[i]) >> 1; + side[i] = left[i] - right[i]; + } + SizeEstimate leftInfo = SubframeEncoder.computeBest(left , sampleDepth, opt); + SizeEstimate rightInfo = SubframeEncoder.computeBest(right, sampleDepth, opt); + SizeEstimate midInfo = SubframeEncoder.computeBest(mid , sampleDepth, opt); + SizeEstimate sideInfo = SubframeEncoder.computeBest(side , sampleDepth + 1, opt); + long mode1Size = leftInfo.sizeEstimate + rightInfo.sizeEstimate; + long mode8Size = leftInfo.sizeEstimate + sideInfo.sizeEstimate; + long mode9Size = rightInfo.sizeEstimate + sideInfo.sizeEstimate; + long mode10Size = midInfo.sizeEstimate + sideInfo.sizeEstimate; + long minimum = Math.min(Math.min(mode1Size, mode8Size), Math.min(mode9Size, mode10Size)); + if (mode1Size == minimum) { + enc.metadata.channelAssignment = 1; + encoderInfo[0] = leftInfo; + encoderInfo[1] = rightInfo; + } else if (mode8Size == minimum) { + enc.metadata.channelAssignment = 8; + encoderInfo[0] = leftInfo; + encoderInfo[1] = sideInfo; + } else if (mode9Size == minimum) { + enc.metadata.channelAssignment = 9; + encoderInfo[0] = sideInfo; + encoderInfo[1] = rightInfo; + } else if (mode10Size == minimum) { + enc.metadata.channelAssignment = 10; + encoderInfo[0] = midInfo; + encoderInfo[1] = sideInfo; + } else + throw new AssertionError(); + } + + // Add up subframe sizes + long size = 0; + enc.subEncoders = new SubframeEncoder[encoderInfo.length]; + for (int i = 0; i < enc.subEncoders.length; i++) { + size += encoderInfo[i].sizeEstimate; + enc.subEncoders[i] = encoderInfo[i].encoder; + } + + // Count length of header (always in whole bytes) + try { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + try (BitOutputStream bitout = new BitOutputStream(bout)) { + enc.metadata.writeHeader(bitout); + } + bout.close(); + size += bout.toByteArray().length * 8; + } catch (IOException e) { + throw new AssertionError(e); + } + + // Count padding and footer + size = (size + 7) / 8; // Round up to nearest byte + size += 2; // CRC-16 + return new SizeEstimate<>(size, enc); + } + + + + /*---- Fields ----*/ + + public FrameInfo metadata; + private SubframeEncoder[] subEncoders; + + + + /*---- Constructors ----*/ + + public FrameEncoder(int sampleOffset, long[][] samples, int sampleDepth, int sampleRate) { + metadata = new FrameInfo(); + metadata.sampleOffset = sampleOffset; + metadata.sampleDepth = sampleDepth; + metadata.sampleRate = sampleRate; + metadata.blockSize = samples[0].length; + metadata.channelAssignment = samples.length - 1; + } + + + + /*---- Public methods ----*/ + + public void encode(long[][] samples, BitOutputStream out) throws IOException { + // Check arguments + Objects.requireNonNull(samples); + Objects.requireNonNull(out); + if (samples[0].length != metadata.blockSize) + throw new IllegalArgumentException(); + + metadata.writeHeader(out); + + int chanAsgn = metadata.channelAssignment; + if (0 <= chanAsgn && chanAsgn <= 7) { + for (int i = 0; i < samples.length; i++) + subEncoders[i].encode(samples[i], out); + } else if (8 <= chanAsgn || chanAsgn <= 10) { + long[] left = samples[0]; + long[] right = samples[1]; + long[] mid = new long[metadata.blockSize]; + long[] side = new long[metadata.blockSize]; + for (int i = 0; i < metadata.blockSize; i++) { + mid[i] = (left[i] + right[i]) >> 1; + side[i] = left[i] - right[i]; + } + if (chanAsgn == 8) { + subEncoders[0].encode(left, out); + subEncoders[1].encode(side, out); + } else if (chanAsgn == 9) { + subEncoders[0].encode(side, out); + subEncoders[1].encode(right, out); + } else if (chanAsgn == 10) { + subEncoders[0].encode(mid, out); + subEncoders[1].encode(side, out); + } else + throw new AssertionError(); + } else + throw new AssertionError(); + out.alignToByte(); + out.writeInt(16, out.getCrc16()); + } + +} diff --git a/desktop/src/io/nayuki/flac/encode/LinearPredictiveEncoder.java b/desktop/src/io/nayuki/flac/encode/LinearPredictiveEncoder.java new file mode 100644 index 0000000..9ac1130 --- /dev/null +++ b/desktop/src/io/nayuki/flac/encode/LinearPredictiveEncoder.java @@ -0,0 +1,277 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.encode; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Objects; + + +/* + * Under the linear predictive coding (LPC) mode of some order, this provides size estimates on and bitstream encoding of audio sample data. + */ +final class LinearPredictiveEncoder extends SubframeEncoder { + + // Computes a good way to encode the given values under the linear predictive coding (LPC) mode of the given order, + // returning a size plus a new encoder object associated with the input arguments. This process of minimizing the size + // has an enormous search space, and it is impossible to guarantee the absolute optimal solution. The maxRiceOrder argument + // is used by the Rice encoder to estimate the size of coding the residual signal. The roundVars argument controls + // how many different coefficients are tested rounding both up and down, resulting in exponential time behavior. + public static SizeEstimate computeBest(long[] samples, int shift, int depth, int order, int roundVars, FastDotProduct fdp, int maxRiceOrder) { + // Check arguments + if (order < 1 || order > 32) + throw new IllegalArgumentException(); + if (roundVars < 0 || roundVars > order || roundVars > 30) + throw new IllegalArgumentException(); + + LinearPredictiveEncoder enc = new LinearPredictiveEncoder(samples, shift, depth, order, fdp); + samples = shiftRight(samples, shift); + + final double[] residues; + Integer[] indices = null; + int scaler = 1 << enc.coefShift; + if (roundVars > 0) { + residues = new double[order]; + indices = new Integer[order]; + for (int i = 0; i < order; i++) { + residues[i] = Math.abs(Math.round(enc.realCoefs[i] * scaler) - enc.realCoefs[i] * scaler); + indices[i] = i; + } + Arrays.sort(indices, new Comparator() { + public int compare(Integer x, Integer y) { + return Double.compare(residues[y], residues[x]); + } + }); + } else + residues = null; + + long bestSize = Long.MAX_VALUE; + int[] bestCoefs = enc.coefficients.clone(); + for (int i = 0; i < (1 << roundVars); i++) { + for (int j = 0; j < roundVars; j++) { + int k = indices[j]; + double coef = enc.realCoefs[k]; + int val; + if (((i >>> j) & 1) == 0) + val = (int)Math.floor(coef * scaler); + else + val = (int)Math.ceil(coef * scaler); + enc.coefficients[order - 1 - k] = Math.max(Math.min(val, (1 << (enc.coefDepth - 1)) - 1), -(1 << (enc.coefDepth - 1))); + } + + long[] newData = roundVars > 0 ? samples.clone() : samples; + applyLpc(newData, enc.coefficients, enc.coefShift); + long temp = RiceEncoder.computeBestSizeAndOrder(newData, order, maxRiceOrder); + long size = 1 + 6 + 1 + shift + order * depth + (temp >>> 4); + if (size < bestSize) { + bestSize = size; + bestCoefs = enc.coefficients.clone(); + enc.riceOrder = (int)(temp & 0xF); + } + } + enc.coefficients = bestCoefs; + return new SizeEstimate(bestSize, enc); + } + + + + private final int order; + private final double[] realCoefs; + private int[] coefficients; + private final int coefDepth; + private final int coefShift; + public int riceOrder; + + + public LinearPredictiveEncoder(long[] samples, int shift, int depth, int order, FastDotProduct fdp) { + super(shift, depth); + int numSamples = samples.length; + if (order < 1 || order > 32 || numSamples < order) + throw new IllegalArgumentException(); + this.order = order; + + // Set up matrix to solve linear least squares problem + double[][] matrix = new double[order][order + 1]; + for (int r = 0; r < matrix.length; r++) { + for (int c = 0; c < matrix[r].length; c++) { + double val; + if (c >= r) + val = fdp.dotProduct(r, c, samples.length - order); + else + val = matrix[c][r]; + matrix[r][c] = val; + } + } + + // Solve matrix, then examine range of coefficients + realCoefs = solveMatrix(matrix); + double maxCoef = 0; + for (double x : realCoefs) + maxCoef = Math.max(Math.abs(x), maxCoef); + int wholeBits = maxCoef >= 1 ? (int)(Math.log(maxCoef) / Math.log(2)) + 1 : 0; + + // Quantize and store the coefficients + coefficients = new int[order]; + coefDepth = 15; // The maximum possible + coefShift = coefDepth - 1 - wholeBits; + for (int i = 0; i < realCoefs.length; i++) { + double coef = realCoefs[realCoefs.length - 1 - i]; + int val = (int)Math.round(coef * (1 << coefShift)); + coefficients[i] = Math.max(Math.min(val, (1 << (coefDepth - 1)) - 1), -(1 << (coefDepth - 1))); + } + } + + + // Solves an n * (n+1) augmented matrix (which modifies its values as a side effect), + // returning a new solution vector of length n. + private static double[] solveMatrix(double[][] mat) { + // Gauss-Jordan elimination algorithm + int rows = mat.length; + int cols = mat[0].length; + if (rows + 1 != cols) + throw new IllegalArgumentException(); + + // Forward elimination + int numPivots = 0; + for (int j = 0; j < rows && numPivots < rows; j++) { + int pivotRow = rows; + double pivotMag = 0; + for (int i = numPivots; i < rows; i++) { + if (Math.abs(mat[i][j]) > pivotMag) { + pivotMag = Math.abs(mat[i][j]); + pivotRow = i; + } + } + if (pivotRow == rows) + continue; + + double[] temp = mat[numPivots]; + mat[numPivots] = mat[pivotRow]; + mat[pivotRow] = temp; + pivotRow = numPivots; + numPivots++; + + double factor = mat[pivotRow][j]; + for (int k = 0; k < cols; k++) + mat[pivotRow][k] /= factor; + mat[pivotRow][j] = 1; + + for (int i = pivotRow + 1; i < rows; i++) { + factor = mat[i][j]; + for (int k = 0; k < cols; k++) + mat[i][k] -= mat[pivotRow][k] * factor; + mat[i][j] = 0; + } + } + + // Back substitution + double[] result = new double[rows]; + for (int i = numPivots - 1; i >= 0; i--) { + int pivotCol = 0; + while (pivotCol < cols && mat[i][pivotCol] == 0) + pivotCol++; + if (pivotCol == cols) + continue; + result[pivotCol] = mat[i][cols - 1]; + + for (int j = i - 1; j >= 0; j--) { + double factor = mat[j][pivotCol]; + for (int k = 0; k < cols; k++) + mat[j][k] -= mat[i][k] * factor; + mat[j][pivotCol] = 0; + } + } + return result; + } + + + public void encode(long[] samples, BitOutputStream out) throws IOException { + Objects.requireNonNull(samples); + Objects.requireNonNull(out); + if (samples.length < order) + throw new IllegalArgumentException(); + + writeTypeAndShift(32 + order - 1, out); + samples = shiftRight(samples, sampleShift); + + for (int i = 0; i < order; i++) // Warmup + writeRawSample(samples[i], out); + out.writeInt(4, coefDepth - 1); + out.writeInt(5, coefShift); + for (int x : coefficients) + out.writeInt(coefDepth, x); + applyLpc(samples, coefficients, coefShift); + RiceEncoder.encode(samples, order, riceOrder, out); + } + + + + /*---- Static helper functions ----*/ + + // Applies linear prediction to data[coefs.length : data.length] so that newdata[i] = + // data[i] - ((data[i-1]*coefs[0] + data[i-2]*coefs[1] + ... + data[i-coefs.length]*coefs[coefs.length]) >> shift). + // By FLAC parameters, each data[i] must fit in a signed 33-bit integer, each coef must fit in signed int15, and coefs.length <= 32. + // When these preconditions are met, they guarantee the lack of arithmetic overflow in the computation and results, + // and each value written back to the data array fits in a signed int53. + static void applyLpc(long[] data, int[] coefs, int shift) { + // Check arguments and arrays strictly + Objects.requireNonNull(data); + Objects.requireNonNull(coefs); + if (coefs.length > 32 || shift < 0 || shift > 63) + throw new IllegalArgumentException(); + for (long x : data) { + x >>= 32; + if (x != 0 && x != -1) // Check if it fits in signed int33 + throw new IllegalArgumentException(); + } + for (int x : coefs) { + x >>= 14; + if (x != 0 && x != -1) // Check if it fits in signed int15 + throw new IllegalArgumentException(); + } + + // Perform the LPC convolution/FIR + for (int i = data.length - 1; i >= coefs.length; i--) { + long sum = 0; + for (int j = 0; j < coefs.length; j++) + sum += data[i - 1 - j] * coefs[j]; + long val = data[i] - (sum >> shift); + if ((val >> 52) != 0 && (val >> 52) != -1) // Check if it fits in signed int53 + throw new AssertionError(); + data[i] = val; + } + } + + + // Returns a new array where each result[i] = data[i] >> shift. + static long[] shiftRight(long[] data, int shift) { + Objects.requireNonNull(data); + if (shift < 0 || shift > 63) + throw new IllegalArgumentException(); + long[] result = new long[data.length]; + for (int i = 0; i < data.length; i++) + result[i] = data[i] >> shift; + return result; + } + +} diff --git a/desktop/src/io/nayuki/flac/encode/RandomAccessFileOutputStream.java b/desktop/src/io/nayuki/flac/encode/RandomAccessFileOutputStream.java new file mode 100644 index 0000000..ac54c64 --- /dev/null +++ b/desktop/src/io/nayuki/flac/encode/RandomAccessFileOutputStream.java @@ -0,0 +1,81 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.encode; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.util.Objects; + + +/* + * An adapter from RandomAccessFile to OutputStream. These objects have no buffer, so seek() + * and write() can be safely interleaved. Also, objects of this class have no direct + * native resources - so it is safe to discard a RandomAccessFileOutputStream object without + * closing it, as long as other code will close() the underlying RandomAccessFile object. + */ +public final class RandomAccessFileOutputStream extends OutputStream { + + /*---- Fields ----*/ + + private RandomAccessFile out; + + + + /*---- Constructors ----*/ + + public RandomAccessFileOutputStream(RandomAccessFile raf) { + this.out = Objects.requireNonNull(raf); + } + + + + /*---- Methods ----*/ + + public long getPosition() throws IOException { + return out.getFilePointer(); + } + + + public void seek(long pos) throws IOException { + out.seek(pos); + } + + + public void write(int b) throws IOException { + out.write(b); + } + + + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + } + + + public void close() throws IOException { + if (out != null) { + out.close(); + out = null; + } + } + +} diff --git a/desktop/src/io/nayuki/flac/encode/RiceEncoder.java b/desktop/src/io/nayuki/flac/encode/RiceEncoder.java new file mode 100644 index 0000000..be6a291 --- /dev/null +++ b/desktop/src/io/nayuki/flac/encode/RiceEncoder.java @@ -0,0 +1,216 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.encode; + +import java.io.IOException; +import java.util.Objects; + + +/* + * Calculates/estimates the encoded size of a vector of residuals, and also performs the encoding to an output stream. + */ +final class RiceEncoder { + + /*---- Functions for size calculation ---*/ + + // Calculates the best number of bits and partition order needed to encode the values data[warmup : data.length]. + // Each value in that subrange of data must fit in a signed 53-bit integer. The result is packed in the form + // ((bestSize << 4) | bestOrder), where bestSize is an unsigned integer and bestOrder is a uint4. + // Note that the partition orders searched, and hence the resulting bestOrder, are in the range [0, maxPartOrder]. + public static long computeBestSizeAndOrder(long[] data, int warmup, int maxPartOrder) { + // Check arguments strictly + Objects.requireNonNull(data); + if (warmup < 0 || warmup > data.length) + throw new IllegalArgumentException(); + if (maxPartOrder < 0 || maxPartOrder > 15) + throw new IllegalArgumentException(); + for (long x : data) { + x >>= 52; + if (x != 0 && x != -1) // Check that it fits in a signed int53 + throw new IllegalArgumentException(); + } + + long bestSize = Integer.MAX_VALUE; + int bestOrder = -1; + + int[] escapeBits = null; + int[] bitsAtParam = null; + for (int order = maxPartOrder; order >= 0; order--) { + int partSize = data.length >>> order; + if ((partSize << order) != data.length || partSize < warmup) + continue; + int numPartitions = 1 << order; + + if (escapeBits == null) { // And bitsAtParam == null + escapeBits = new int[numPartitions]; + bitsAtParam = new int[numPartitions * 16]; + for (int i = warmup; i < data.length; i++) { + int j = i / partSize; + long val = data[i]; + escapeBits[j] = Math.max(65 - Long.numberOfLeadingZeros(val ^ (val >> 63)), escapeBits[j]); + val = (val >= 0) ? (val << 1) : (((-val) << 1) - 1); + for (int param = 0; param < 15; param++, val >>>= 1) + bitsAtParam[param + j * 16] += val + 1 + param; + } + } else { // Both arrays are non-null + // Logically halve the size of both arrays (but without reallocating to the true new size) + for (int i = 0; i < numPartitions; i++) { + int j = i << 1; + escapeBits[i] = Math.max(escapeBits[j], escapeBits[j + 1]); + for (int param = 0; param < 15; param++) + bitsAtParam[param + i * 16] = bitsAtParam[param + j * 16] + bitsAtParam[param + (j + 1) * 16]; + } + } + + long size = 4 + (4 << order); + for (int i = 0; i < numPartitions; i++) { + int min = Integer.MAX_VALUE; + if (escapeBits[i] <= 31) + min = 5 + escapeBits[i] * (partSize - (i == 0 ? warmup : 0)); + for (int param = 0; param < 15; param++) + min = Math.min(bitsAtParam[param + i * 16], min); + size += min; + } + if (size < bestSize) { + bestSize = size; + bestOrder = order; + } + } + + if (bestSize == Integer.MAX_VALUE || (bestOrder >>> 4) != 0) + throw new AssertionError(); + return bestSize << 4 | bestOrder; + } + + + // Calculates the number of bits needed to encode the sequence of values + // data[start : end] with an optimally chosen Rice parameter. + private static long computeBestSizeAndParam(long[] data, int start, int end) { + assert data != null && 0 <= start && start <= end && end <= data.length; + + // Use escape code + int bestParam; + long bestSize; + { + long accumulator = 0; + for (int i = start; i < end; i++) { + long val = data[i]; + accumulator |= val ^ (val >> 63); + } + int numBits = 65 - Long.numberOfLeadingZeros(accumulator); + assert 1 <= numBits && numBits <= 65; + if (numBits <= 31) { + bestSize = 4 + 5 + (end - start) * numBits; + bestParam = 16 + numBits; + if ((bestParam >>> 6) != 0) + throw new AssertionError(); + } else { + bestSize = Long.MAX_VALUE; + bestParam = 0; + } + } + + // Use Rice coding + for (int param = 0; param <= 14; param++) { + long size = 4; + for (int i = start; i < end; i++) { + long val = data[i]; + if (val >= 0) + val <<= 1; + else + val = ((-val) << 1) - 1; + size += (val >>> param) + 1 + param; + } + if (size < bestSize) { + bestSize = size; + bestParam = param; + } + } + return bestSize << 6 | bestParam; + } + + + + /*---- Functions for encoding data ---*/ + + // Encodes the sequence of values data[warmup : data.length] with an appropriately chosen order and Rice parameters. + // Each value in data must fit in a signed 53-bit integer. + public static void encode(long[] data, int warmup, int order, BitOutputStream out) throws IOException { + // Check arguments strictly + Objects.requireNonNull(data); + Objects.requireNonNull(out); + if (warmup < 0 || warmup > data.length) + throw new IllegalArgumentException(); + if (order < 0 || order > 15) + throw new IllegalArgumentException(); + for (long x : data) { + x >>= 52; + if (x != 0 && x != -1) // Check that it fits in a signed int53 + throw new IllegalArgumentException(); + } + + out.writeInt(2, 0); + out.writeInt(4, order); + int numPartitions = 1 << order; + int start = warmup; + int end = data.length >>> order; + for (int i = 0; i < numPartitions; i++) { + int param = (int)computeBestSizeAndParam(data, start, end) & 0x3F; + encode(data, start, end, param, out); + start = end; + end += data.length >>> order; + } + } + + + // Encodes the sequence of values data[start : end] with the given Rice parameter. + private static void encode(long[] data, int start, int end, int param, BitOutputStream out) throws IOException { + assert 0 <= param && param <= 31 && data != null && out != null; + assert 0 <= start && start <= end && end <= data.length; + + if (param < 15) { + out.writeInt(4, param); + for (int j = start; j < end; j++) + writeRiceSignedInt(data[j], param, out); + } else { + out.writeInt(4, 15); + int numBits = param - 16; + out.writeInt(5, numBits); + for (int j = start; j < end; j++) + out.writeInt(numBits, (int)data[j]); + } + } + + + private static void writeRiceSignedInt(long val, int param, BitOutputStream out) throws IOException { + assert 0 <= param && param <= 31 && out != null; + assert (val >> 52) == 0 || (val >> 52) == -1; // Fits in a signed int53 + + long unsigned = val >= 0 ? val << 1 : ((-val) << 1) - 1; + int unary = (int)(unsigned >>> param); + for (int i = 0; i < unary; i++) + out.writeInt(1, 0); + out.writeInt(1, 1); + out.writeInt(param, (int)unsigned); + } + +} diff --git a/desktop/src/io/nayuki/flac/encode/SizeEstimate.java b/desktop/src/io/nayuki/flac/encode/SizeEstimate.java new file mode 100644 index 0000000..80258d2 --- /dev/null +++ b/desktop/src/io/nayuki/flac/encode/SizeEstimate.java @@ -0,0 +1,61 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.encode; + +import java.util.Objects; + + +/* + * Pairs an integer with an arbitrary object. Immutable structure. + */ +final class SizeEstimate { + + /*---- Fields ----*/ + + public final long sizeEstimate; // Non-negative + public final E encoder; // Not null + + + + /*---- Constructors ----*/ + + public SizeEstimate(long size, E enc) { + if (size < 0) + throw new IllegalArgumentException(); + sizeEstimate = size; + encoder = Objects.requireNonNull(enc); + } + + + + /*---- Methods ----*/ + + // Returns this object if the size is less than or equal to the other object, otherwise returns other. + public SizeEstimate minimum(SizeEstimate other) { + Objects.requireNonNull(other); + if (sizeEstimate <= other.sizeEstimate) + return this; + else + return other; + } + +} diff --git a/desktop/src/io/nayuki/flac/encode/SubframeEncoder.java b/desktop/src/io/nayuki/flac/encode/SubframeEncoder.java new file mode 100644 index 0000000..09ae8f0 --- /dev/null +++ b/desktop/src/io/nayuki/flac/encode/SubframeEncoder.java @@ -0,0 +1,247 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.encode; + +import java.io.IOException; +import java.util.Objects; + + +/* + * Calculates/estimates the encoded size of a subframe of audio sample data, and also performs the encoding to an output stream. + */ +public abstract class SubframeEncoder { + + /*---- Static functions ----*/ + + // Computes/estimates the best way to encode the given vector of audio sample data at the given sample depth under + // the given search criteria, returning a size estimate plus a new encoder object associated with that size. + public static SizeEstimate computeBest(long[] samples, int sampleDepth, SearchOptions opt) { + // Check arguments + Objects.requireNonNull(samples); + if (sampleDepth < 1 || sampleDepth > 33) + throw new IllegalArgumentException(); + Objects.requireNonNull(opt); + for (long x : samples) { + x >>= sampleDepth - 1; + if (x != 0 && x != -1) // Check that the input actually fits the indicated sample depth + throw new IllegalArgumentException(); + } + + // Encode with constant if possible + SizeEstimate result = ConstantEncoder.computeBest(samples, 0, sampleDepth); + if (result != null) + return result; + + // Detect number of trailing zero bits + int shift = computeWastedBits(samples); + + // Start with verbatim as fallback + result = VerbatimEncoder.computeBest(samples, shift, sampleDepth); + + // Try fixed prediction encoding + for (int order = opt.minFixedOrder; 0 <= order && order <= opt.maxFixedOrder; order++) { + SizeEstimate temp = FixedPredictionEncoder.computeBest( + samples, shift, sampleDepth, order, opt.maxRiceOrder); + result = result.minimum(temp); + } + + // Try linear predictive coding + FastDotProduct fdp = new FastDotProduct(samples, Math.max(opt.maxLpcOrder, 0)); + for (int order = opt.minLpcOrder; 0 <= order && order <= opt.maxLpcOrder; order++) { + SizeEstimate temp = LinearPredictiveEncoder.computeBest( + samples, shift, sampleDepth, order, Math.min(opt.lpcRoundVariables, order), fdp, opt.maxRiceOrder); + result = result.minimum(temp); + } + + // Return the encoder found with the lowest bit length + return result; + } + + + // Looks at each value in the array and computes the minimum number of trailing binary zeros + // among all the elements. For example, computedwastedBits({0b10, 0b10010, 0b1100}) = 1. + // If there are no elements or every value is zero (the former actually implies the latter), then + // the return value is 0. This is because every zero value has an infinite number of trailing zeros. + private static int computeWastedBits(long[] data) { + Objects.requireNonNull(data); + long accumulator = 0; + for (long x : data) + accumulator |= x; + if (accumulator == 0) + return 0; + else { + int result = Long.numberOfTrailingZeros(accumulator); + assert 0 <= result && result <= 63; + return result; + } + } + + + + /*---- Instance members ----*/ + + protected final int sampleShift; // Number of bits to shift each sample right by. In the range [0, sampleDepth]. + protected final int sampleDepth; // Stipulate that each audio sample fits in a signed integer of this width. In the range [1, 33]. + + + // Constructs a subframe encoder on some data array with the given right shift (wasted bits) and sample depth. + // Note that every element of the array must fit in a signed depth-bit integer and have at least 'shift' trailing binary zeros. + // After the encoder object is created and when encode() is called, it must receive the same array length and values (but the object reference can be different). + // Subframe encoders should not retain a reference to the sample data array because the higher-level encoder may request and + // keep many size estimates coupled with encoder objects, but only utilize a small number of encoder objects in the end. + protected SubframeEncoder(int shift, int depth) { + if (depth < 1 || depth > 33 || shift < 0 || shift > depth) + throw new IllegalArgumentException(); + sampleShift = shift; + sampleDepth = depth; + } + + + // Encodes the given vector of audio sample data to the given bit output stream + // using the current encoding method (dictated by subclasses and field values). + // This requires the data array to have the same values (but not necessarily be the same object reference) + // as the array that was passed to the constructor when this encoder object was created. + public abstract void encode(long[] samples, BitOutputStream out) throws IOException; + + + // Writes the subframe header to the given output stream, based on the given + // type code (uint6) and this object's sampleShift field (a.k.a. wasted bits per sample). + protected final void writeTypeAndShift(int type, BitOutputStream out) throws IOException { + // Check arguments + if ((type >>> 6) != 0) + throw new IllegalArgumentException(); + Objects.requireNonNull(out); + + // Write some fields + out.writeInt(1, 0); + out.writeInt(6, type); + + // Write shift value in quasi-unary + if (sampleShift == 0) + out.writeInt(1, 0); + else { + out.writeInt(1, 1); + for (int i = 0; i < sampleShift - 1; i++) + out.writeInt(1, 0); + out.writeInt(1, 1); + } + } + + + // Writes the given value to the output stream as a signed (sampleDepth-sampleShift) bit integer. + // Note that the value to being written is equal to the raw sample value shifted right by sampleShift. + protected final void writeRawSample(long val, BitOutputStream out) throws IOException { + int width = sampleDepth - sampleShift; + if (width < 1 || width > 33) + throw new IllegalStateException(); + long temp = val >> (width - 1); + if (temp != 0 && temp != -1) + throw new IllegalArgumentException(); + if (width <= 32) + out.writeInt(width, (int)val); + else { // width == 33 + out.writeInt(1, (int)(val >>> 32)); + out.writeInt(32, (int)val); + } + } + + + + /*---- Helper structure ----*/ + + // Represents options for how to search the encoding parameters for a subframe. It is used directly by + // SubframeEncoder.computeBest() and indirectly by its sub-calls. Objects of this class are immutable. + public static final class SearchOptions { + + /*-- Fields --*/ + + // The range of orders to test for fixed prediction mode, possibly none. + // The values satisfy (minFixedOrder = maxFixedOrder = -1) || (0 <= minFixedOrder <= maxFixedOrder <= 4). + public final int minFixedOrder; + public final int maxFixedOrder; + + // The range of orders to test for linear predictive coding (LPC) mode, possibly none. + // The values satisfy (minLpcOrder = maxLpcOrder = -1) || (1 <= minLpcOrder <= maxLpcOrder <= 32). + // Note that the FLAC subset format requires maxLpcOrder <= 12 when sampleRate <= 48000. + public final int minLpcOrder; + public final int maxLpcOrder; + + // How many LPC coefficient variables to try rounding both up and down. + // In the range [0, 30]. Note that each increase by one will double the search time! + public final int lpcRoundVariables; + + // The maximum partition order used in Rice coding. The minimum is not configurable and always 0. + // In the range [0, 15]. Note that the FLAC subset format requires maxRiceOrder <= 8. + public final int maxRiceOrder; + + + /*-- Constructors --*/ + + // Constructs a search options object based on the given values, + // throwing an IllegalArgumentException if and only if they are nonsensical. + public SearchOptions(int minFixedOrder, int maxFixedOrder, int minLpcOrder, int maxLpcOrder, int lpcRoundVars, int maxRiceOrder) { + // Check argument ranges + if ((minFixedOrder != -1 || maxFixedOrder != -1) && + !(0 <= minFixedOrder && minFixedOrder <= maxFixedOrder && maxFixedOrder <= 4)) + throw new IllegalArgumentException(); + if ((minLpcOrder != -1 || maxLpcOrder != -1) && + !(1 <= minLpcOrder && minLpcOrder <= maxLpcOrder && maxLpcOrder <= 32)) + throw new IllegalArgumentException(); + if (lpcRoundVars < 0 || lpcRoundVars > 30) + throw new IllegalArgumentException(); + if (maxRiceOrder < 0 || maxRiceOrder > 15) + throw new IllegalArgumentException(); + + // Copy arguments to fields + this.minFixedOrder = minFixedOrder; + this.maxFixedOrder = maxFixedOrder; + this.minLpcOrder = minLpcOrder; + this.maxLpcOrder = maxLpcOrder; + this.lpcRoundVariables = lpcRoundVars; + this.maxRiceOrder = maxRiceOrder; + } + + + /*-- Constants for recommended defaults --*/ + + // Note that these constants are for convenience only, and offer little promises in terms of API stability. + // For example, there is no expectation that the set of search option names as a whole, + // or the values of each search option will remain the same from version to version. + // Even if a search option retains the same value across code versions, the underlying encoder implementation + // can change in such a way that the encoded output is not bit-identical or size-identical across versions. + // Therefore, treat these search options as suggestions that strongly influence the encoded FLAC output, + // but *not* as firm guarantees that the same audio data with the same options will forever produce the same result. + + // These search ranges conform to the FLAC subset format. + public static final SearchOptions SUBSET_ONLY_FIXED = new SearchOptions(0, 4, -1, -1, 0, 8); + public static final SearchOptions SUBSET_MEDIUM = new SearchOptions(0, 1, 2, 8, 0, 5); + public static final SearchOptions SUBSET_BEST = new SearchOptions(0, 1, 2, 12, 0, 8); + public static final SearchOptions SUBSET_INSANE = new SearchOptions(0, 4, 1, 12, 4, 8); + + // These cannot guarantee that an encoded file conforms to the FLAC subset (i.e. they are lax). + public static final SearchOptions LAX_MEDIUM = new SearchOptions(0, 1, 2, 22, 0, 15); + public static final SearchOptions LAX_BEST = new SearchOptions(0, 1, 2, 32, 0, 15); + public static final SearchOptions LAX_INSANE = new SearchOptions(0, 1, 2, 32, 4, 15); + + } + +} diff --git a/desktop/src/io/nayuki/flac/encode/VerbatimEncoder.java b/desktop/src/io/nayuki/flac/encode/VerbatimEncoder.java new file mode 100644 index 0000000..0257743 --- /dev/null +++ b/desktop/src/io/nayuki/flac/encode/VerbatimEncoder.java @@ -0,0 +1,58 @@ +/* + * FLAC library (Java) + * + * Copyright (c) Project Nayuki + * https://www.nayuki.io/page/flac-library-java + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +package io.nayuki.flac.encode; + +import java.io.IOException; + + +/* + * Under the verbatim coding mode, this provides size calculations on and bitstream encoding of audio sample data. + * Note that the size depends on the data length, shift, and bit depth, but not on the data contents. + */ +final class VerbatimEncoder extends SubframeEncoder { + + // Computes the best way to encode the given values under the verbatim coding mode, + // returning an exact size plus a new encoder object associated with the input arguments. + public static SizeEstimate computeBest(long[] samples, int shift, int depth) { + VerbatimEncoder enc = new VerbatimEncoder(samples, shift, depth); + long size = 1 + 6 + 1 + shift + samples.length * depth; + return new SizeEstimate(size, enc); + } + + + // Constructs a constant encoder for the given data, right shift, and sample depth. + public VerbatimEncoder(long[] samples, int shift, int depth) { + super(shift, depth); + } + + + // Encodes the given vector of audio sample data to the given bit output stream using + // the this encoding method (and the superclass fields sampleShift and sampleDepth). + // This requires the data array to have the same values (but not necessarily + // the same object reference) as the array that was passed to the constructor. + public void encode(long[] samples, BitOutputStream out) throws IOException { + writeTypeAndShift(1, out); + for (long val : samples) + writeRawSample(val >> sampleShift, out); + } + +} diff --git a/fdfparser/.gitignore b/fdfparser/.gitignore new file mode 100644 index 0000000..84c048a --- /dev/null +++ b/fdfparser/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/fdfparser/antlr-src/FDF.g4 b/fdfparser/antlr-src/FDF.g4 new file mode 100644 index 0000000..7728f25 --- /dev/null +++ b/fdfparser/antlr-src/FDF.g4 @@ -0,0 +1,151 @@ +/** + * Define a grammar called FDF + */ +grammar FDF; + +@header { + package com.etheller.warsmash.fdfparser; +} + +program : + (statement)* + ; + +statement: + STRING_LIST OPEN_CURLY (ID STRING_LITERAL COMMA)*? CLOSE_CURLY # StringListStatement + | + INCLUDE_FILE STRING_LITERAL COMMA # IncludeStatement + | + frame # FrameStatement + ; + +frame: + frame_type_qualifier OPEN_CURLY frame_element* CLOSE_CURLY # AnonymousCompDefinition + | + frame_type_qualifier INHERITS STRING_LITERAL OPEN_CURLY frame_element* CLOSE_CURLY # AnonymousCompSubTypeDefinition + | + frame_type_qualifier INHERITS WITHCHILDREN STRING_LITERAL OPEN_CURLY frame_element* CLOSE_CURLY # AnonymousCompSubTypeDefinitionWithChildren + | + frame_type_qualifier STRING_LITERAL OPEN_CURLY frame_element* CLOSE_CURLY # CompDefinition + | + frame_type_qualifier STRING_LITERAL INHERITS STRING_LITERAL OPEN_CURLY frame_element* CLOSE_CURLY # CompSubTypeDefinition + | + frame_type_qualifier STRING_LITERAL INHERITS WITHCHILDREN STRING_LITERAL OPEN_CURLY frame_element* CLOSE_CURLY # CompSubTypeDefinitionWithChildren + | + FRAME STRING_LITERAL STRING_LITERAL OPEN_CURLY frame_element* CLOSE_CURLY # FrameDefinition + | + FRAME STRING_LITERAL STRING_LITERAL INHERITS STRING_LITERAL OPEN_CURLY frame_element* CLOSE_CURLY # FrameSubTypeDefinition + | + FRAME STRING_LITERAL STRING_LITERAL INHERITS WITHCHILDREN STRING_LITERAL OPEN_CURLY frame_element* CLOSE_CURLY # FrameSubTypeDefinitionWithChildren + ; + +frame_element: + frame # FrameFrameElement + | + ID FLOAT COMMA # FloatElement + | + ID STRING_LITERAL COMMA # StringElement + | + ID STRING_LITERAL STRING_LITERAL COMMA # StringPairElement + | + ID FLOAT FLOAT COMMA # Vector2Element + | + ID COMMA # FlagElement + | + ID FLOAT FLOAT FLOAT FLOAT COMMA # Vector4Element + | + ID FLOAT COMMA FLOAT COMMA FLOAT COMMA FLOAT COMMA # Vector4CommaElement + | + SETPOINT frame_point COMMA STRING_LITERAL COMMA frame_point COMMA FLOAT COMMA FLOAT COMMA # SetPointElement + | + ANCHOR frame_point COMMA FLOAT COMMA FLOAT COMMA # AnchorElement + | + ID STRING_LITERAL COMMA FLOAT COMMA STRING_LITERAL COMMA # FontElement + | + ID FLOAT FLOAT FLOAT COMMA # Vector3Element + | + ID text_justify COMMA # TextJustifyElement + | + ID STRING_LITERAL COMMA FLOAT COMMA # SimpleFontElement + ; + +text_justify: + JUSTIFYTOP | JUSTIFYMIDDLE | JUSTIFYBOTTOM | JUSTIFYLEFT | JUSTIFYCENTER | JUSTIFYRIGHT; + +frame_point: + FRAMEPOINT_TOPLEFT + | FRAMEPOINT_TOP + | FRAMEPOINT_TOPRIGHT + | FRAMEPOINT_LEFT + | FRAMEPOINT_CENTER + | FRAMEPOINT_RIGHT + | FRAMEPOINT_BOTTOMLEFT + | FRAMEPOINT_BOTTOM + | FRAMEPOINT_BOTTOMRIGHT; + +color: + FLOAT FLOAT FLOAT + | + FLOAT FLOAT FLOAT FLOAT + ; + +frame_type_qualifier: + STRING + | + TEXTURE + | + LAYER + ; + +OPEN_CURLY : '{'; + +CLOSE_CURLY : '}'; + +STRING_LIST : 'StringList' ; + +INCLUDE_FILE : 'IncludeFile' ; + +FRAME : 'Frame' ; + +STRING : 'String' ; + +TEXTURE : 'Texture' ; + +LAYER : 'Layer' ; + +INHERITS : 'INHERITS' ; + +WITHCHILDREN : 'WITHCHILDREN' ; + +SETPOINT : 'SetPoint'; +ANCHOR : 'Anchor'; + +JUSTIFYTOP : 'JUSTIFYTOP'; +JUSTIFYMIDDLE : 'JUSTIFYMIDDLE'; +JUSTIFYBOTTOM : 'JUSTIFYBOTTOM'; +JUSTIFYLEFT : 'JUSTIFYLEFT'; +JUSTIFYCENTER : 'JUSTIFYCENTER'; +JUSTIFYRIGHT : 'JUSTIFYRIGHT'; + +FRAMEPOINT_TOPLEFT : 'TOPLEFT'; +FRAMEPOINT_TOP : 'TOP'; +FRAMEPOINT_TOPRIGHT : 'TOPRIGHT'; +FRAMEPOINT_LEFT : 'LEFT'; +FRAMEPOINT_CENTER : 'CENTER'; +FRAMEPOINT_RIGHT : 'RIGHT'; +FRAMEPOINT_BOTTOMLEFT : 'BOTTOMLEFT'; +FRAMEPOINT_BOTTOM : 'BOTTOM'; +FRAMEPOINT_BOTTOMRIGHT : 'BOTTOMRIGHT'; + +ID : ([a-zA-Z_][a-zA-Z_0-9]*) ; + +COMMA : ','; + +STRING_LITERAL : ('"'.*?'"'); + +WS : [ \t\r\n]+ -> skip ; + +FLOAT : '-'?([0]|([1-9][0-9]*))('.'([0-9]*)?)?'f'? ; + +MULTI_LINE_COMMENT : '/*'.*?'*/' -> skip ; +COMMENT : '//'.*?'\n' -> skip ; diff --git a/fdfparser/build.gradle b/fdfparser/build.gradle new file mode 100644 index 0000000..679dff9 --- /dev/null +++ b/fdfparser/build.gradle @@ -0,0 +1,49 @@ +apply plugin: "antlr" + +sourceCompatibility = 1.8 +[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' + +sourceSets.main.java.srcDirs = [ "src/", "build/generated-src" ] +sourceSets.main.antlr.srcDirs = [ "antlr-src/" ] + +project.ext.mainClassName = "com.etheller.warsmash.fdfparser.Main" + +task run(dependsOn: classes, type: JavaExec) { + main = project.mainClassName + classpath = sourceSets.main.runtimeClasspath + standardInput = System.in + ignoreExitValue = true +} + +task dist(type: Jar) { + from files(sourceSets.main.output.classesDir) + from files(sourceSets.main.output.resourcesDir) + from {configurations.compile.collect {zipTree(it)}} + + manifest { + attributes 'Main-Class': project.mainClassName + } +} + +dist.dependsOn classes + +eclipse.project { + name = appName + "-fdfparser" +} + +task afterEclipseImport(description: "Post processing after project generation", group: "IDE") { + doLast { + def classpath = new XmlParser().parse(file(".classpath")) + def writer = new FileWriter(file(".classpath")) + def printer = new XmlNodePrinter(new PrintWriter(writer)) + printer.setPreserveWhitespace(true) + printer.print(classpath) + } +} + + +generateGrammarSource { + maxHeapSize = "64m" + arguments += ["-visitor", "-no-listener"] + outputDirectory = file("build/generated-src/com/etheller/warsmash/fdfparser") +} \ No newline at end of file diff --git a/fdfparser/src/com/etheller/warsmash/fdfparser/FDFParserBuilder.java b/fdfparser/src/com/etheller/warsmash/fdfparser/FDFParserBuilder.java new file mode 100644 index 0000000..66c2df4 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/fdfparser/FDFParserBuilder.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.fdfparser; + +public interface FDFParserBuilder { + FDFParser build(String path); +} diff --git a/fdfparser/src/com/etheller/warsmash/fdfparser/FrameDefinitionFieldVisitor.java b/fdfparser/src/com/etheller/warsmash/fdfparser/FrameDefinitionFieldVisitor.java new file mode 100644 index 0000000..00a8d42 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/fdfparser/FrameDefinitionFieldVisitor.java @@ -0,0 +1,159 @@ +package com.etheller.warsmash.fdfparser; + +import com.etheller.warsmash.fdfparser.FDFParser.AnchorElementContext; +import com.etheller.warsmash.fdfparser.FDFParser.FlagElementContext; +import com.etheller.warsmash.fdfparser.FDFParser.FloatElementContext; +import com.etheller.warsmash.fdfparser.FDFParser.FontElementContext; +import com.etheller.warsmash.fdfparser.FDFParser.FrameFrameElementContext; +import com.etheller.warsmash.fdfparser.FDFParser.SetPointElementContext; +import com.etheller.warsmash.fdfparser.FDFParser.SimpleFontElementContext; +import com.etheller.warsmash.fdfparser.FDFParser.StringElementContext; +import com.etheller.warsmash.fdfparser.FDFParser.StringPairElementContext; +import com.etheller.warsmash.fdfparser.FDFParser.TextJustifyElementContext; +import com.etheller.warsmash.fdfparser.FDFParser.Vector2ElementContext; +import com.etheller.warsmash.fdfparser.FDFParser.Vector3ElementContext; +import com.etheller.warsmash.fdfparser.FDFParser.Vector4CommaElementContext; +import com.etheller.warsmash.fdfparser.FDFParser.Vector4ElementContext; +import com.etheller.warsmash.parsers.fdf.datamodel.AnchorDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.FontDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.FrameDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.FramePoint; +import com.etheller.warsmash.parsers.fdf.datamodel.SetPointDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.TextJustify; +import com.etheller.warsmash.parsers.fdf.datamodel.Vector2Definition; +import com.etheller.warsmash.parsers.fdf.datamodel.Vector4Definition; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FloatFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FontFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringPairFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.TextJustifyFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector2FrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector4FrameDefinitionField; + +public class FrameDefinitionFieldVisitor extends FDFBaseVisitor { + private static final int JUSTIFY_OFFSET = "JUSTIFY".length(); + private final FrameDefinition frameDefinition; + private final FrameDefinitionVisitor frameDefinitionVisitor; + + public FrameDefinitionFieldVisitor(final FrameDefinition frameDefinition, + final FrameDefinitionVisitor frameDefinitionVisitor) { + this.frameDefinition = frameDefinition; + this.frameDefinitionVisitor = frameDefinitionVisitor; + } + + @Override + public Void visitStringElement(final StringElementContext ctx) { + String text = ctx.STRING_LITERAL().getText(); + text = text.substring(1, text.length() - 1); + this.frameDefinition.set(ctx.ID().getText(), new StringFrameDefinitionField(text)); + return null; + } + + @Override + public Void visitStringPairElement(final StringPairElementContext ctx) { + String first = ctx.STRING_LITERAL(0).getText(); + first = first.substring(1, first.length() - 1); + String second = ctx.STRING_LITERAL(1).getText(); + second = second.substring(1, second.length() - 1); + this.frameDefinition.set(ctx.ID().getText(), new StringPairFrameDefinitionField(first, second)); + return super.visitStringPairElement(ctx); + } + + @Override + public Void visitFloatElement(final FloatElementContext ctx) { + this.frameDefinition.set(ctx.ID().getText(), + new FloatFrameDefinitionField(Float.parseFloat(ctx.FLOAT().getText()))); + return null; + } + + @Override + public Void visitFlagElement(final FlagElementContext ctx) { + this.frameDefinition.add(ctx.ID().getText()); + return null; + } + + @Override + public Void visitVector2Element(final Vector2ElementContext ctx) { + this.frameDefinition.set(ctx.ID().getText(), + new Vector2FrameDefinitionField(new Vector2Definition(Float.parseFloat(ctx.FLOAT(0).getText()), + Float.parseFloat(ctx.FLOAT(1).getText())))); + return null; + } + + @Override + public Void visitVector3Element(final Vector3ElementContext ctx) { + this.frameDefinition.set(ctx.ID().getText(), + new Vector4FrameDefinitionField(new Vector4Definition(Float.parseFloat(ctx.FLOAT(0).getText()), + Float.parseFloat(ctx.FLOAT(1).getText()), Float.parseFloat(ctx.FLOAT(2).getText()), 1.0f))); + return null; + } + + @Override + public Void visitVector4Element(final Vector4ElementContext ctx) { + this.frameDefinition.set(ctx.ID().getText(), + new Vector4FrameDefinitionField(new Vector4Definition(Float.parseFloat(ctx.FLOAT(0).getText()), + Float.parseFloat(ctx.FLOAT(1).getText()), Float.parseFloat(ctx.FLOAT(2).getText()), + Float.parseFloat(ctx.FLOAT(3).getText())))); + return null; + } + + @Override + public Void visitVector4CommaElement(final Vector4CommaElementContext ctx) { + this.frameDefinition.set(ctx.ID().getText(), + new Vector4FrameDefinitionField(new Vector4Definition(Float.parseFloat(ctx.FLOAT(0).getText()), + Float.parseFloat(ctx.FLOAT(1).getText()), Float.parseFloat(ctx.FLOAT(2).getText()), + Float.parseFloat(ctx.FLOAT(3).getText())))); + return null; + } + + @Override + public Void visitSetPointElement(final SetPointElementContext ctx) { + String other = ctx.STRING_LITERAL().getText(); + other = other.substring(1, other.length() - 1); + final SetPointDefinition setPointDefinition = new SetPointDefinition( + FramePoint.valueOf(ctx.frame_point(0).getText()), other, + FramePoint.valueOf(ctx.frame_point(1).getText()), Float.parseFloat(ctx.FLOAT(0).getText()), + Float.parseFloat(ctx.FLOAT(1).getText())); + this.frameDefinition.add(setPointDefinition); + return null; + } + + @Override + public Void visitAnchorElement(final AnchorElementContext ctx) { + final AnchorDefinition anchorDefinition = new AnchorDefinition(FramePoint.valueOf(ctx.frame_point().getText()), + Float.parseFloat(ctx.FLOAT(0).getText()), Float.parseFloat(ctx.FLOAT(1).getText())); + this.frameDefinition.add(anchorDefinition); + return null; + } + + @Override + public Void visitTextJustifyElement(final TextJustifyElementContext ctx) { + final TextJustify justify = TextJustify.valueOf(ctx.text_justify().getText().substring(JUSTIFY_OFFSET)); + this.frameDefinition.set(ctx.ID().getText(), new TextJustifyFrameDefinitionField(justify)); + return null; + } + + @Override + public Void visitFontElement(final FontElementContext ctx) { + String text = ctx.STRING_LITERAL(0).getText(); + text = text.substring(1, text.length() - 1); + this.frameDefinition.set(ctx.ID().getText(), new FontFrameDefinitionField( + new FontDefinition(text, Float.parseFloat(ctx.FLOAT().getText()), ctx.STRING_LITERAL(1).getText()))); + return null; + } + + @Override + public Void visitSimpleFontElement(final SimpleFontElementContext ctx) { + String text = ctx.STRING_LITERAL().getText(); + text = text.substring(1, text.length() - 1); + this.frameDefinition.set(ctx.ID().getText(), + new FontFrameDefinitionField(new FontDefinition(text, Float.parseFloat(ctx.FLOAT().getText()), null))); + return null; + } + + @Override + public Void visitFrameFrameElement(final FrameFrameElementContext ctx) { + this.frameDefinition.add(this.frameDefinitionVisitor.visit(ctx)); + return null; + } +} diff --git a/fdfparser/src/com/etheller/warsmash/fdfparser/FrameDefinitionVisitor.java b/fdfparser/src/com/etheller/warsmash/fdfparser/FrameDefinitionVisitor.java new file mode 100644 index 0000000..f9f0f45 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/fdfparser/FrameDefinitionVisitor.java @@ -0,0 +1,224 @@ +package com.etheller.warsmash.fdfparser; + +import java.util.List; + +import org.antlr.v4.runtime.tree.TerminalNode; + +import com.etheller.warsmash.fdfparser.FDFParser.AnonymousCompDefinitionContext; +import com.etheller.warsmash.fdfparser.FDFParser.AnonymousCompSubTypeDefinitionContext; +import com.etheller.warsmash.fdfparser.FDFParser.AnonymousCompSubTypeDefinitionWithChildrenContext; +import com.etheller.warsmash.fdfparser.FDFParser.CompDefinitionContext; +import com.etheller.warsmash.fdfparser.FDFParser.CompSubTypeDefinitionContext; +import com.etheller.warsmash.fdfparser.FDFParser.CompSubTypeDefinitionWithChildrenContext; +import com.etheller.warsmash.fdfparser.FDFParser.FrameDefinitionContext; +import com.etheller.warsmash.fdfparser.FDFParser.FrameSubTypeDefinitionContext; +import com.etheller.warsmash.fdfparser.FDFParser.FrameSubTypeDefinitionWithChildrenContext; +import com.etheller.warsmash.fdfparser.FDFParser.Frame_elementContext; +import com.etheller.warsmash.fdfparser.FDFParser.IncludeStatementContext; +import com.etheller.warsmash.fdfparser.FDFParser.StringListStatementContext; +import com.etheller.warsmash.parsers.fdf.datamodel.FrameClass; +import com.etheller.warsmash.parsers.fdf.datamodel.FrameDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.FrameTemplateEnvironment; + +public class FrameDefinitionVisitor extends FDFBaseVisitor { + private final FrameTemplateEnvironment templates; + private final FDFParserBuilder fdfParserBuilder; + + public FrameDefinitionVisitor(final FrameTemplateEnvironment templates, final FDFParserBuilder fdfParserBuilder) { + this.templates = templates; + this.fdfParserBuilder = fdfParserBuilder; + } + + @Override + public FrameDefinition visitStringListStatement(final StringListStatementContext ctx) { + final List ids = ctx.ID(); + final List strings = ctx.STRING_LITERAL(); + for (int i = 0; i < ids.size(); i++) { + final String id = ids.get(i).getText(); + String value = strings.get(i).getText(); + value = unquote(value); + this.templates.addDecoratedString(id, value); + } + return null; + } + + @Override + public FrameDefinition visitIncludeStatement(final IncludeStatementContext ctx) { + final String includeFilePath = unquote(ctx.STRING_LITERAL().getText()); + final FDFParser parser = this.fdfParserBuilder.build(includeFilePath); + visit(parser.program()); + return null; + } + + private String unquote(String includeFilePath) { + includeFilePath = includeFilePath.substring(1, includeFilePath.length() - 1); + return includeFilePath; + } + + @Override + public FrameDefinition visitFrameDefinition(final FrameDefinitionContext ctx) { + final String type = unquote(ctx.STRING_LITERAL(0).getText()); + final String name = unquote(ctx.STRING_LITERAL(1).getText()); + final FrameDefinition frameDefinition = new FrameDefinition(FrameClass.Frame, type, name); + final FrameDefinitionFieldVisitor fieldVisitor = new FrameDefinitionFieldVisitor(frameDefinition, this); + for (final Frame_elementContext element : ctx.frame_element()) { + fieldVisitor.visit(element); + } + this.templates.put(name, frameDefinition); + return frameDefinition; + } + + @Override + public FrameDefinition visitFrameSubTypeDefinition(final FrameSubTypeDefinitionContext ctx) { + final String type = unquote(ctx.STRING_LITERAL(0).getText()); + final String name = unquote(ctx.STRING_LITERAL(1).getText()); + final String parent = unquote(ctx.STRING_LITERAL(2).getText()); + final FrameDefinition frameDefinition = new FrameDefinition(FrameClass.Frame, type, name); + // INHERITS + final FrameDefinition inheritParent = this.templates.getFrame(parent); + if (inheritParent == null) { + throw new IllegalStateException( + "\"" + name + "\" cannot inherit from \"" + parent + "\" because it does not exist!"); + } + frameDefinition.inheritFrom(inheritParent, false); + + final FrameDefinitionFieldVisitor fieldVisitor = new FrameDefinitionFieldVisitor(frameDefinition, this); + for (final Frame_elementContext element : ctx.frame_element()) { + fieldVisitor.visit(element); + } + this.templates.put(name, frameDefinition); + return frameDefinition; + } + + @Override + public FrameDefinition visitFrameSubTypeDefinitionWithChildren( + final FrameSubTypeDefinitionWithChildrenContext ctx) { + final String type = unquote(ctx.STRING_LITERAL(0).getText()); + final String name = unquote(ctx.STRING_LITERAL(1).getText()); + final String parent = unquote(ctx.STRING_LITERAL(2).getText()); + final FrameDefinition frameDefinition = new FrameDefinition(FrameClass.Frame, type, name); + // INHERITS + final FrameDefinition inheritParent = this.templates.getFrame(parent); + if (inheritParent == null) { + throw new IllegalStateException( + "\"" + name + "\" cannot inherit from \"" + parent + "\" because it does not exist!"); + } + frameDefinition.inheritFrom(inheritParent, true); + + final FrameDefinitionFieldVisitor fieldVisitor = new FrameDefinitionFieldVisitor(frameDefinition, this); + for (final Frame_elementContext element : ctx.frame_element()) { + fieldVisitor.visit(element); + } + this.templates.put(name, frameDefinition); + return frameDefinition; + } + + @Override + public FrameDefinition visitAnonymousCompDefinition(final AnonymousCompDefinitionContext ctx) { + final FrameDefinition frameDefinition = new FrameDefinition( + FrameClass.valueOf(ctx.frame_type_qualifier().getText()), null, null); + final FrameDefinitionFieldVisitor fieldVisitor = new FrameDefinitionFieldVisitor(frameDefinition, this); + for (final Frame_elementContext element : ctx.frame_element()) { + fieldVisitor.visit(element); + } + return frameDefinition; + } + + @Override + public FrameDefinition visitAnonymousCompSubTypeDefinition(final AnonymousCompSubTypeDefinitionContext ctx) { + final String parent = unquote(ctx.STRING_LITERAL().getText()); + final FrameClass frameClass = FrameClass.valueOf(ctx.frame_type_qualifier().getText()); + final FrameDefinition frameDefinition = new FrameDefinition(frameClass, null, null); + // INHERITS + final FrameDefinition inheritParent = this.templates.getFrame(parent); + if (inheritParent == null) { + throw new IllegalStateException( + "" + frameClass + " cannot inherit from \"" + parent + "\" because it does not exist!"); + } + frameDefinition.inheritFrom(inheritParent, false); + + final FrameDefinitionFieldVisitor fieldVisitor = new FrameDefinitionFieldVisitor(frameDefinition, this); + for (final Frame_elementContext element : ctx.frame_element()) { + fieldVisitor.visit(element); + } + return frameDefinition; + } + + @Override + public FrameDefinition visitAnonymousCompSubTypeDefinitionWithChildren( + final AnonymousCompSubTypeDefinitionWithChildrenContext ctx) { + final String parent = unquote(ctx.STRING_LITERAL().getText()); + final FrameClass frameClass = FrameClass.valueOf(ctx.frame_type_qualifier().getText()); + final FrameDefinition frameDefinition = new FrameDefinition(frameClass, null, null); + // INHERITS + final FrameDefinition inheritParent = this.templates.getFrame(parent); + if (inheritParent == null) { + throw new IllegalStateException( + "" + frameClass + " cannot inherit from \"" + parent + "\" because it does not exist!"); + } + frameDefinition.inheritFrom(inheritParent, true); + + final FrameDefinitionFieldVisitor fieldVisitor = new FrameDefinitionFieldVisitor(frameDefinition, this); + for (final Frame_elementContext element : ctx.frame_element()) { + fieldVisitor.visit(element); + } + return frameDefinition; + } + + @Override + public FrameDefinition visitCompDefinition(final CompDefinitionContext ctx) { + final String name = unquote(ctx.STRING_LITERAL().getText()); + final FrameDefinition frameDefinition = new FrameDefinition( + FrameClass.valueOf(ctx.frame_type_qualifier().getText()), null, name); + final FrameDefinitionFieldVisitor fieldVisitor = new FrameDefinitionFieldVisitor(frameDefinition, this); + for (final Frame_elementContext element : ctx.frame_element()) { + fieldVisitor.visit(element); + } + this.templates.put(name, frameDefinition); + return frameDefinition; + } + + @Override + public FrameDefinition visitCompSubTypeDefinition(final CompSubTypeDefinitionContext ctx) { + final String name = unquote(ctx.STRING_LITERAL(0).getText()); + final String parent = unquote(ctx.STRING_LITERAL(1).getText()); + final FrameDefinition frameDefinition = new FrameDefinition( + FrameClass.valueOf(ctx.frame_type_qualifier().getText()), null, name); + // INHERITS + final FrameDefinition inheritParent = this.templates.getFrame(parent); + if (inheritParent == null) { + throw new IllegalStateException( + "\"" + name + "\" cannot inherit from \"" + parent + "\" because it does not exist!"); + } + frameDefinition.inheritFrom(inheritParent, false); + + final FrameDefinitionFieldVisitor fieldVisitor = new FrameDefinitionFieldVisitor(frameDefinition, this); + for (final Frame_elementContext element : ctx.frame_element()) { + fieldVisitor.visit(element); + } + this.templates.put(name, frameDefinition); + return frameDefinition; + } + + @Override + public FrameDefinition visitCompSubTypeDefinitionWithChildren(final CompSubTypeDefinitionWithChildrenContext ctx) { + final String name = unquote(ctx.STRING_LITERAL(0).getText()); + final String parent = unquote(ctx.STRING_LITERAL(1).getText()); + final FrameDefinition frameDefinition = new FrameDefinition( + FrameClass.valueOf(ctx.frame_type_qualifier().getText()), null, name); + // INHERITS + final FrameDefinition inheritParent = this.templates.getFrame(parent); + if (inheritParent == null) { + throw new IllegalStateException( + "\"" + name + "\" cannot inherit from \"" + parent + "\" because it does not exist!"); + } + frameDefinition.inheritFrom(inheritParent, true); + + final FrameDefinitionFieldVisitor fieldVisitor = new FrameDefinitionFieldVisitor(frameDefinition, this); + for (final Frame_elementContext element : ctx.frame_element()) { + fieldVisitor.visit(element); + } + this.templates.put(name, frameDefinition); + return frameDefinition; + } +} diff --git a/fdfparser/src/com/etheller/warsmash/fdfparser/Main.java b/fdfparser/src/com/etheller/warsmash/fdfparser/Main.java new file mode 100644 index 0000000..c2c4f81 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/fdfparser/Main.java @@ -0,0 +1,48 @@ +package com.etheller.warsmash.fdfparser; + +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; + +import com.etheller.warsmash.parsers.fdf.datamodel.FrameDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.FrameTemplateEnvironment; + +public class Main { + public static final boolean REPORT_SYNTAX_ERRORS = true; + + public static void main(final String[] args) { + if (args.length < 1) { + System.err.println("Usage: "); + return; + } + try { + final BaseErrorListener errorListener = new BaseErrorListener() { + @Override + public void syntaxError(final Recognizer recognizer, final Object offendingSymbol, final int line, + final int charPositionInLine, final String msg, final RecognitionException e) { + if (!REPORT_SYNTAX_ERRORS) { + return; + } + + String sourceName = recognizer.getInputStream().getSourceName(); + if (!sourceName.isEmpty()) { + sourceName = String.format("%s:%d:%d: ", sourceName, line, charPositionInLine); + } + + System.err.println(sourceName + "line " + line + ":" + charPositionInLine + " " + msg); + } + }; + final FrameTemplateEnvironment templates = new FrameTemplateEnvironment(); + final TestFDFParserBuilder testFDFParserBuilder = new TestFDFParserBuilder(errorListener); + final FrameDefinitionVisitor fdfVisitor = new FrameDefinitionVisitor(templates, testFDFParserBuilder); + final FDFParser firstFileParser = testFDFParserBuilder.build(args[0]); + fdfVisitor.visit(firstFileParser.program()); + final FrameDefinition bnetChat = templates.getFrame("BattleNetTextAreaTemplate"); + System.out.println("Value of BattleNetTextAreaTemplate: " + bnetChat); + } + catch (final Exception exc) { + System.err.println(exc.getMessage()); + } + } + +} diff --git a/fdfparser/src/com/etheller/warsmash/fdfparser/TestFDFParserBuilder.java b/fdfparser/src/com/etheller/warsmash/fdfparser/TestFDFParserBuilder.java new file mode 100644 index 0000000..df363f1 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/fdfparser/TestFDFParserBuilder.java @@ -0,0 +1,27 @@ +package com.etheller.warsmash.fdfparser; + +import java.io.IOException; + +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; + +public class TestFDFParserBuilder implements FDFParserBuilder { + private final BaseErrorListener errorListener; + + public TestFDFParserBuilder(final BaseErrorListener errorListener) { + this.errorListener = errorListener; + } + + @Override + public FDFParser build(final String path) { + FDFLexer lexer; + try { + lexer = new FDFLexer(CharStreams.fromFileName(path)); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + return new FDFParser(new CommonTokenStream(lexer)); + } +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/AnchorDefinition.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/AnchorDefinition.java new file mode 100644 index 0000000..1385223 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/AnchorDefinition.java @@ -0,0 +1,30 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +public class AnchorDefinition { + private final FramePoint myPoint; + private final float x; + private final float y; + + public AnchorDefinition(final FramePoint myPoint, final float x, final float y) { + this.myPoint = myPoint; + this.x = x; + this.y = y; + } + + public FramePoint getMyPoint() { + return this.myPoint; + } + + public float getX() { + return this.x; + } + + public float getY() { + return this.y; + } + + @Override + public String toString() { + return "AnchorDefinition [myPoint=" + this.myPoint + ", x=" + this.x + ", y=" + this.y + "]"; + } +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/BackdropCornerFlags.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/BackdropCornerFlags.java new file mode 100644 index 0000000..116f939 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/BackdropCornerFlags.java @@ -0,0 +1,24 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +import java.util.EnumSet; + +public enum BackdropCornerFlags { + UL, + UR, + BL, + BR, + T, + L, + B, + R; + + public static EnumSet parseCornerFlags(final String cornerFlags) { + final EnumSet set = EnumSet.noneOf(BackdropCornerFlags.class); + for (final String flag : cornerFlags.split("\\|")) { + if (!"".equals(flag)) { + set.add(BackdropCornerFlags.valueOf(flag)); + } + } + return set; + } +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/ControlStyle.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/ControlStyle.java new file mode 100644 index 0000000..8762a24 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/ControlStyle.java @@ -0,0 +1,19 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +import java.util.EnumSet; + +public enum ControlStyle { + AUTOTRACK, + HIGHLIGHTONFOCUS, + HIGHLIGHTONMOUSEOVER; + + public static EnumSet parseControlStyle(final String controlStyles) { + final EnumSet set = EnumSet.noneOf(ControlStyle.class); + for (final String flag : controlStyles.split("\\|")) { + if (!"".equals(flag)) { + set.add(ControlStyle.valueOf(flag)); + } + } + return set; + } +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FontDefinition.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FontDefinition.java new file mode 100644 index 0000000..4f0b0f8 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FontDefinition.java @@ -0,0 +1,25 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +public class FontDefinition { + private final String fontName; + private final float fontSize; + private final String extra; + + public FontDefinition(final String fontName, final float fontSize, final String extra) { + this.fontName = fontName; + this.fontSize = fontSize; + this.extra = extra; + } + + public String getFontName() { + return this.fontName; + } + + public float getFontSize() { + return this.fontSize; + } + + public String getExtra() { + return this.extra; + } +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FontFlags.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FontFlags.java new file mode 100644 index 0000000..99fef40 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FontFlags.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +public enum FontFlags { + FIXEDSIZE; +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FrameClass.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FrameClass.java new file mode 100644 index 0000000..ad62408 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FrameClass.java @@ -0,0 +1,8 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +public enum FrameClass { + Frame, + String, + Texture, + Layer; +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FrameDefinition.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FrameDefinition.java new file mode 100644 index 0000000..14ecf1d --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FrameDefinition.java @@ -0,0 +1,163 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringPairFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.visitor.GetFloatFieldVisitor; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.visitor.GetFontFieldVisitor; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.visitor.GetStringFieldVisitor; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.visitor.GetStringPairFieldVisitor; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.visitor.GetTextJustifyFieldVisitor; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.visitor.GetVector2FieldVisitor; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.visitor.GetVector4FieldVisitor; + +/** + * Pretty sure this is probably not how it works in-game but this silly + * everything class might help get a prototype running fast until I have a + * better understanding of how I want these classes designed. + */ +public class FrameDefinition { + private final FrameClass frameClass; + private final String frameType; + private final String name; + private final List innerFrames = new ArrayList<>(); + private final Set flags = new HashSet<>(); + private final Map nameToField = new HashMap<>(); + private final List setPoints = new ArrayList<>(); + private final List anchors = new ArrayList<>(); + + public FrameDefinition(final FrameClass frameClass, final String frameType, final String name) { + this.frameClass = frameClass; + this.frameType = frameType; + this.name = name; + } + + public void inheritFrom(final FrameDefinition other, final boolean withChildren) { + this.flags.addAll(other.flags); + this.nameToField.putAll(other.nameToField); + if (withChildren) { + this.innerFrames.addAll(other.innerFrames); + } + } + + public void set(final String fieldName, final FrameDefinitionField value) { + this.nameToField.put(fieldName, value); + } + + public void add(final FrameDefinition childDefition) { + this.innerFrames.add(childDefition); + } + + public void add(final SetPointDefinition setPointDefinition) { + this.setPoints.add(setPointDefinition); + } + + public void add(final AnchorDefinition anchorDefinition) { + this.anchors.add(anchorDefinition); + } + + public void add(final String flag) { + this.flags.add(flag); + } + + public boolean has(final String flag) { + return this.flags.contains(flag); + } + + public FrameDefinitionField get(final String fieldName) { + return this.nameToField.get(fieldName); + } + + @Override + public String toString() { + return "FrameDefinition [frameClass=" + this.frameClass + ", frameType=" + this.frameType + ", name=" + + this.name + ", innerFrames=" + this.innerFrames + ", flags=" + this.flags + ", nameToField=" + + this.nameToField + ", setPoints=" + this.setPoints + ", anchors=" + this.anchors + "]"; + } + + public String getFrameType() { + return this.frameType; + } + + public String getName() { + return this.name; + } + + public FrameClass getFrameClass() { + return this.frameClass; + } + + public List getInnerFrames() { + return this.innerFrames; + } + + public List getAnchors() { + return this.anchors; + } + + public List getSetPoints() { + return this.setPoints; + } + + public String getString(final String id) { + final FrameDefinitionField frameDefinitionField = this.nameToField.get(id); + if (frameDefinitionField != null) { + return frameDefinitionField.visit(GetStringFieldVisitor.INSTANCE); + } + return null; + } + + public StringPairFrameDefinitionField getStringPair(final String id) { + final FrameDefinitionField frameDefinitionField = this.nameToField.get(id); + if (frameDefinitionField != null) { + return frameDefinitionField.visit(GetStringPairFieldVisitor.INSTANCE); + } + return null; + } + + public Float getFloat(final String id) { + final FrameDefinitionField frameDefinitionField = this.nameToField.get(id); + if (frameDefinitionField != null) { + return frameDefinitionField.visit(GetFloatFieldVisitor.INSTANCE); + } + return null; + } + + public Vector4Definition getVector4(final String id) { + final FrameDefinitionField frameDefinitionField = this.nameToField.get(id); + if (frameDefinitionField != null) { + return frameDefinitionField.visit(GetVector4FieldVisitor.INSTANCE); + } + return null; + } + + public Vector2Definition getVector2(final String id) { + final FrameDefinitionField frameDefinitionField = this.nameToField.get(id); + if (frameDefinitionField != null) { + return frameDefinitionField.visit(GetVector2FieldVisitor.INSTANCE); + } + return null; + } + + public FontDefinition getFont(final String id) { + final FrameDefinitionField frameDefinitionField = this.nameToField.get(id); + if (frameDefinitionField != null) { + return frameDefinitionField.visit(GetFontFieldVisitor.INSTANCE); + } + return null; + } + + public TextJustify getTextJustify(final String id) { + final FrameDefinitionField frameDefinitionField = this.nameToField.get(id); + if (frameDefinitionField != null) { + return frameDefinitionField.visit(GetTextJustifyFieldVisitor.INSTANCE); + } + return null; + } +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FrameEvent.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FrameEvent.java new file mode 100644 index 0000000..3e4472b --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FrameEvent.java @@ -0,0 +1,20 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +public enum FrameEvent { + CONTROL_CLICK, + MOUSE_ENTER, + MOUSE_LEAVE, + MOUSE_UP, + MOUSE_DOWN, + MOUSE_WHEEL, + CHECKBOX_CHECKED, + CHECKBOX_UNCHECKED, + EDITBOX_TEXT_CHANGED, + POPUPMENU_ITEM_CHANGED, + MOUSE_DOUBLECLICK, + SPRITE_ANIM_UPDATE, + SLIDER_VALUE_CHANGED, + DIALOG_CANCEL, + DIALOG_ACCEPT, + EDITBOX_ENTER +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FramePoint.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FramePoint.java new file mode 100644 index 0000000..2a9229a --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FramePoint.java @@ -0,0 +1,13 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +public enum FramePoint { + TOPLEFT, + TOP, + TOPRIGHT, + LEFT, + CENTER, + RIGHT, + BOTTOMLEFT, + BOTTOM, + BOTTOMRIGHT; +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FrameTemplateEnvironment.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FrameTemplateEnvironment.java new file mode 100644 index 0000000..ba9da1b --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/FrameTemplateEnvironment.java @@ -0,0 +1,29 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +import java.util.HashMap; +import java.util.Map; + +public class FrameTemplateEnvironment { + private final Map idToDecoratedString = new HashMap<>(); + private final Map idToFrame = new HashMap<>(); + + public void addDecoratedString(final String id, final String value) { + this.idToDecoratedString.put(id, value); + } + + public String getDecoratedString(final String id) { + final String decoratedString = this.idToDecoratedString.get(id); + if (decoratedString != null) { + return decoratedString; + } + return id; + } + + public void put(final String id, final FrameDefinition frame) { + this.idToFrame.put(id, frame); + } + + public FrameDefinition getFrame(final String id) { + return this.idToFrame.get(id); + } +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/HighlightAlphaMode.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/HighlightAlphaMode.java new file mode 100644 index 0000000..4766c42 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/HighlightAlphaMode.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +public enum HighlightAlphaMode { + ADD; +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/HighlightType.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/HighlightType.java new file mode 100644 index 0000000..f03ce4a --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/HighlightType.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +public enum HighlightType { + FILETEXTURE; +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/SetPointDefinition.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/SetPointDefinition.java new file mode 100644 index 0000000..bbfbe0b --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/SetPointDefinition.java @@ -0,0 +1,38 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +public class SetPointDefinition { + private final FramePoint myPoint; + private final String other; + private final FramePoint otherPoint; + private final float x; + private final float y; + + public SetPointDefinition(final FramePoint myPoint, final String other, final FramePoint otherPoint, final float x, + final float y) { + this.myPoint = myPoint; + this.other = other; + this.otherPoint = otherPoint; + this.x = x; + this.y = y; + } + + public FramePoint getMyPoint() { + return this.myPoint; + } + + public String getOther() { + return this.other; + } + + public FramePoint getOtherPoint() { + return this.otherPoint; + } + + public float getX() { + return this.x; + } + + public float getY() { + return this.y; + } +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/TextJustify.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/TextJustify.java new file mode 100644 index 0000000..ae30591 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/TextJustify.java @@ -0,0 +1,10 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +public enum TextJustify { + TOP, + MIDDLE, + BOTTOM, + LEFT, + CENTER, + RIGHT; +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/Vector2Definition.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/Vector2Definition.java new file mode 100644 index 0000000..209ce72 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/Vector2Definition.java @@ -0,0 +1,28 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +public class Vector2Definition { + private float x; + private float y; + + public Vector2Definition(final float x, final float y) { + this.x = x; + this.y = y; + } + + public float getX() { + return this.x; + } + + public float getY() { + return this.y; + } + + public void setX(final float x) { + this.x = x; + } + + public void setY(final float y) { + this.y = y; + } + +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/Vector3Definition.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/Vector3Definition.java new file mode 100644 index 0000000..22aa15d --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/Vector3Definition.java @@ -0,0 +1,23 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +public class Vector3Definition { + private final float x, y, z; + + public Vector3Definition(final float x, final float y, final float z) { + this.x = x; + this.y = y; + this.z = z; + } + + public float getX() { + return this.x; + } + + public float getY() { + return this.y; + } + + public float getZ() { + return this.z; + } +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/Vector4Definition.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/Vector4Definition.java new file mode 100644 index 0000000..1ea78fa --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/Vector4Definition.java @@ -0,0 +1,52 @@ +package com.etheller.warsmash.parsers.fdf.datamodel; + +public class Vector4Definition { + private float x, y, z, w; + + public Vector4Definition(final float x, final float y, final float z, final float w) { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + public float getX() { + return this.x; + } + + public float getY() { + return this.y; + } + + public float getZ() { + return this.z; + } + + public float getW() { + return this.w; + } + + public void setX(final float x) { + this.x = x; + } + + public void setY(final float y) { + this.y = y; + } + + public void setZ(final float z) { + this.z = z; + } + + public void setW(final float w) { + this.w = w; + } + + public void set(final float x2, final float y2, final float z2, final float w2) { + this.x = x2; + this.y = y2; + this.z = z2; + this.w = w2; + + } +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/FloatFrameDefinitionField.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/FloatFrameDefinitionField.java new file mode 100644 index 0000000..5218833 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/FloatFrameDefinitionField.java @@ -0,0 +1,19 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields; + +public class FloatFrameDefinitionField implements FrameDefinitionField { + private final float value; + + public FloatFrameDefinitionField(final float value) { + this.value = value; + } + + public float getValue() { + return this.value; + } + + @Override + public TYPE visit(final FrameDefinitionFieldVisitor visitor) { + return visitor.accept(this); + } + +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/FontFrameDefinitionField.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/FontFrameDefinitionField.java new file mode 100644 index 0000000..d6ca65c --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/FontFrameDefinitionField.java @@ -0,0 +1,21 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields; + +import com.etheller.warsmash.parsers.fdf.datamodel.FontDefinition; + +public class FontFrameDefinitionField implements FrameDefinitionField { + private final FontDefinition value; + + public FontFrameDefinitionField(final FontDefinition value) { + this.value = value; + } + + public FontDefinition getValue() { + return this.value; + } + + @Override + public TYPE visit(final FrameDefinitionFieldVisitor visitor) { + return visitor.accept(this); + } + +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/FrameDefinitionField.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/FrameDefinitionField.java new file mode 100644 index 0000000..f0b84ee --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/FrameDefinitionField.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields; + +public interface FrameDefinitionField { + TYPE visit(FrameDefinitionFieldVisitor visitor); +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/FrameDefinitionFieldVisitor.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/FrameDefinitionFieldVisitor.java new file mode 100644 index 0000000..1049510 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/FrameDefinitionFieldVisitor.java @@ -0,0 +1,19 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields; + +public interface FrameDefinitionFieldVisitor { + TYPE accept(StringFrameDefinitionField field); + + TYPE accept(StringPairFrameDefinitionField field); + + TYPE accept(FloatFrameDefinitionField field); + + TYPE accept(Vector3FrameDefinitionField field); + + TYPE accept(Vector4FrameDefinitionField field); + + TYPE accept(Vector2FrameDefinitionField field); + + TYPE accept(FontFrameDefinitionField field); + + TYPE accept(TextJustifyFrameDefinitionField field); +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/StringFrameDefinitionField.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/StringFrameDefinitionField.java new file mode 100644 index 0000000..c2c903e --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/StringFrameDefinitionField.java @@ -0,0 +1,18 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields; + +public class StringFrameDefinitionField implements FrameDefinitionField { + private final String value; + + public StringFrameDefinitionField(final String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + + @Override + public TYPE visit(final FrameDefinitionFieldVisitor visitor) { + return visitor.accept(this); + } +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/StringPairFrameDefinitionField.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/StringPairFrameDefinitionField.java new file mode 100644 index 0000000..9cf192b --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/StringPairFrameDefinitionField.java @@ -0,0 +1,24 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields; + +public class StringPairFrameDefinitionField implements FrameDefinitionField { + private final String first; + private final String second; + + public StringPairFrameDefinitionField(final String first, final String second) { + this.first = first; + this.second = second; + } + + public String getFirst() { + return this.first; + } + + public String getSecond() { + return this.second; + } + + @Override + public TYPE visit(final FrameDefinitionFieldVisitor visitor) { + return visitor.accept(this); + } +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/TextJustifyFrameDefinitionField.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/TextJustifyFrameDefinitionField.java new file mode 100644 index 0000000..c0aa295 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/TextJustifyFrameDefinitionField.java @@ -0,0 +1,21 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields; + +import com.etheller.warsmash.parsers.fdf.datamodel.TextJustify; + +public class TextJustifyFrameDefinitionField implements FrameDefinitionField { + private final TextJustify value; + + public TextJustifyFrameDefinitionField(final TextJustify value) { + this.value = value; + } + + public TextJustify getValue() { + return this.value; + } + + @Override + public TYPE visit(final FrameDefinitionFieldVisitor visitor) { + return visitor.accept(this); + } + +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/Vector2FrameDefinitionField.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/Vector2FrameDefinitionField.java new file mode 100644 index 0000000..e07f798 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/Vector2FrameDefinitionField.java @@ -0,0 +1,20 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields; + +import com.etheller.warsmash.parsers.fdf.datamodel.Vector2Definition; + +public class Vector2FrameDefinitionField implements FrameDefinitionField { + private final Vector2Definition value; + + public Vector2FrameDefinitionField(final Vector2Definition value) { + this.value = value; + } + + public Vector2Definition getValue() { + return this.value; + } + + @Override + public TYPE visit(final FrameDefinitionFieldVisitor visitor) { + return visitor.accept(this); + } +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/Vector3FrameDefinitionField.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/Vector3FrameDefinitionField.java new file mode 100644 index 0000000..da09089 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/Vector3FrameDefinitionField.java @@ -0,0 +1,21 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields; + +import com.etheller.warsmash.parsers.fdf.datamodel.Vector3Definition; + +public class Vector3FrameDefinitionField implements FrameDefinitionField { + private final Vector3Definition value; + + public Vector3FrameDefinitionField(final Vector3Definition value) { + this.value = value; + } + + public Vector3Definition getValue() { + return this.value; + } + + @Override + public TYPE visit(final FrameDefinitionFieldVisitor visitor) { + return visitor.accept(this); + } + +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/Vector4FrameDefinitionField.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/Vector4FrameDefinitionField.java new file mode 100644 index 0000000..94bc3b6 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/Vector4FrameDefinitionField.java @@ -0,0 +1,20 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields; + +import com.etheller.warsmash.parsers.fdf.datamodel.Vector4Definition; + +public class Vector4FrameDefinitionField implements FrameDefinitionField { + private final Vector4Definition value; + + public Vector4FrameDefinitionField(final Vector4Definition value) { + this.value = value; + } + + public Vector4Definition getValue() { + return this.value; + } + + @Override + public TYPE visit(final FrameDefinitionFieldVisitor visitor) { + return visitor.accept(this); + } +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetFloatFieldVisitor.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetFloatFieldVisitor.java new file mode 100644 index 0000000..38a3084 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetFloatFieldVisitor.java @@ -0,0 +1,56 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields.visitor; + +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FloatFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FontFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FrameDefinitionFieldVisitor; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringPairFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.TextJustifyFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector2FrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector3FrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector4FrameDefinitionField; + +public class GetFloatFieldVisitor implements FrameDefinitionFieldVisitor { + public static GetFloatFieldVisitor INSTANCE = new GetFloatFieldVisitor(); + + @Override + public Float accept(final StringFrameDefinitionField field) { + return null; + } + + @Override + public Float accept(final StringPairFrameDefinitionField field) { + return null; + } + + @Override + public Float accept(final FloatFrameDefinitionField field) { + return field.getValue(); + } + + @Override + public Float accept(final Vector3FrameDefinitionField field) { + return null; + } + + @Override + public Float accept(final Vector4FrameDefinitionField field) { + return null; + } + + @Override + public Float accept(final Vector2FrameDefinitionField field) { + return null; + } + + @Override + public Float accept(final FontFrameDefinitionField field) { + return null; + } + + @Override + public Float accept(final TextJustifyFrameDefinitionField field) { + return null; + } + +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetFontFieldVisitor.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetFontFieldVisitor.java new file mode 100644 index 0000000..44f9ece --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetFontFieldVisitor.java @@ -0,0 +1,57 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields.visitor; + +import com.etheller.warsmash.parsers.fdf.datamodel.FontDefinition; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FloatFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FontFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FrameDefinitionFieldVisitor; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringPairFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.TextJustifyFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector2FrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector3FrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector4FrameDefinitionField; + +public class GetFontFieldVisitor implements FrameDefinitionFieldVisitor { + public static GetFontFieldVisitor INSTANCE = new GetFontFieldVisitor(); + + @Override + public FontDefinition accept(final StringFrameDefinitionField field) { + return null; + } + + @Override + public FontDefinition accept(final StringPairFrameDefinitionField field) { + return null; + } + + @Override + public FontDefinition accept(final FloatFrameDefinitionField field) { + return null; + } + + @Override + public FontDefinition accept(final Vector3FrameDefinitionField field) { + return null; + } + + @Override + public FontDefinition accept(final Vector4FrameDefinitionField field) { + return null; + } + + @Override + public FontDefinition accept(final Vector2FrameDefinitionField field) { + return null; + } + + @Override + public FontDefinition accept(final FontFrameDefinitionField field) { + return field.getValue(); + } + + @Override + public FontDefinition accept(final TextJustifyFrameDefinitionField field) { + return null; + } + +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetStringFieldVisitor.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetStringFieldVisitor.java new file mode 100644 index 0000000..b90b83d --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetStringFieldVisitor.java @@ -0,0 +1,56 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields.visitor; + +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FloatFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FontFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FrameDefinitionFieldVisitor; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringPairFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.TextJustifyFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector2FrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector3FrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector4FrameDefinitionField; + +public class GetStringFieldVisitor implements FrameDefinitionFieldVisitor { + public static GetStringFieldVisitor INSTANCE = new GetStringFieldVisitor(); + + @Override + public String accept(final StringFrameDefinitionField field) { + return field.getValue(); + } + + @Override + public String accept(final StringPairFrameDefinitionField field) { + return null; + } + + @Override + public String accept(final FloatFrameDefinitionField field) { + return null; + } + + @Override + public String accept(final Vector3FrameDefinitionField field) { + return null; + } + + @Override + public String accept(final Vector4FrameDefinitionField field) { + return null; + } + + @Override + public String accept(final Vector2FrameDefinitionField field) { + return null; + } + + @Override + public String accept(final FontFrameDefinitionField field) { + return null; + } + + @Override + public String accept(final TextJustifyFrameDefinitionField field) { + return null; + } + +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetStringPairFieldVisitor.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetStringPairFieldVisitor.java new file mode 100644 index 0000000..0b9f34c --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetStringPairFieldVisitor.java @@ -0,0 +1,56 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields.visitor; + +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FloatFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FontFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FrameDefinitionFieldVisitor; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringPairFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.TextJustifyFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector2FrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector3FrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector4FrameDefinitionField; + +public class GetStringPairFieldVisitor implements FrameDefinitionFieldVisitor { + public static GetStringPairFieldVisitor INSTANCE = new GetStringPairFieldVisitor(); + + @Override + public StringPairFrameDefinitionField accept(final StringFrameDefinitionField field) { + return null; + } + + @Override + public StringPairFrameDefinitionField accept(final StringPairFrameDefinitionField field) { + return field; + } + + @Override + public StringPairFrameDefinitionField accept(final FloatFrameDefinitionField field) { + return null; + } + + @Override + public StringPairFrameDefinitionField accept(final Vector3FrameDefinitionField field) { + return null; + } + + @Override + public StringPairFrameDefinitionField accept(final Vector4FrameDefinitionField field) { + return null; + } + + @Override + public StringPairFrameDefinitionField accept(final Vector2FrameDefinitionField field) { + return null; + } + + @Override + public StringPairFrameDefinitionField accept(final FontFrameDefinitionField field) { + return null; + } + + @Override + public StringPairFrameDefinitionField accept(final TextJustifyFrameDefinitionField field) { + return null; + } + +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetTextJustifyFieldVisitor.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetTextJustifyFieldVisitor.java new file mode 100644 index 0000000..bb45533 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetTextJustifyFieldVisitor.java @@ -0,0 +1,57 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields.visitor; + +import com.etheller.warsmash.parsers.fdf.datamodel.TextJustify; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FloatFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FontFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FrameDefinitionFieldVisitor; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringPairFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.TextJustifyFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector2FrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector3FrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector4FrameDefinitionField; + +public class GetTextJustifyFieldVisitor implements FrameDefinitionFieldVisitor { + public static GetTextJustifyFieldVisitor INSTANCE = new GetTextJustifyFieldVisitor(); + + @Override + public TextJustify accept(final StringFrameDefinitionField field) { + return null; + } + + @Override + public TextJustify accept(final StringPairFrameDefinitionField field) { + return null; + } + + @Override + public TextJustify accept(final FloatFrameDefinitionField field) { + return null; + } + + @Override + public TextJustify accept(final Vector3FrameDefinitionField field) { + return null; + } + + @Override + public TextJustify accept(final Vector4FrameDefinitionField field) { + return null; + } + + @Override + public TextJustify accept(final Vector2FrameDefinitionField field) { + return null; + } + + @Override + public TextJustify accept(final FontFrameDefinitionField field) { + return null; + } + + @Override + public TextJustify accept(final TextJustifyFrameDefinitionField field) { + return field.getValue(); + } + +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetVector2FieldVisitor.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetVector2FieldVisitor.java new file mode 100644 index 0000000..e49490d --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetVector2FieldVisitor.java @@ -0,0 +1,57 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields.visitor; + +import com.etheller.warsmash.parsers.fdf.datamodel.Vector2Definition; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FloatFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FontFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FrameDefinitionFieldVisitor; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringPairFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.TextJustifyFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector2FrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector3FrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector4FrameDefinitionField; + +public class GetVector2FieldVisitor implements FrameDefinitionFieldVisitor { + public static GetVector2FieldVisitor INSTANCE = new GetVector2FieldVisitor(); + + @Override + public Vector2Definition accept(final StringFrameDefinitionField field) { + return null; + } + + @Override + public Vector2Definition accept(final StringPairFrameDefinitionField field) { + return null; + } + + @Override + public Vector2Definition accept(final FloatFrameDefinitionField field) { + return null; + } + + @Override + public Vector2Definition accept(final Vector3FrameDefinitionField field) { + return null; + } + + @Override + public Vector2Definition accept(final Vector2FrameDefinitionField field) { + return field.getValue(); + } + + @Override + public Vector2Definition accept(final Vector4FrameDefinitionField field) { + return null; + } + + @Override + public Vector2Definition accept(final FontFrameDefinitionField field) { + return null; + } + + @Override + public Vector2Definition accept(final TextJustifyFrameDefinitionField field) { + return null; + } + +} diff --git a/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetVector4FieldVisitor.java b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetVector4FieldVisitor.java new file mode 100644 index 0000000..7bc33c7 --- /dev/null +++ b/fdfparser/src/com/etheller/warsmash/parsers/fdf/datamodel/fields/visitor/GetVector4FieldVisitor.java @@ -0,0 +1,57 @@ +package com.etheller.warsmash.parsers.fdf.datamodel.fields.visitor; + +import com.etheller.warsmash.parsers.fdf.datamodel.Vector4Definition; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FloatFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FontFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.FrameDefinitionFieldVisitor; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.StringPairFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.TextJustifyFrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector2FrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector3FrameDefinitionField; +import com.etheller.warsmash.parsers.fdf.datamodel.fields.Vector4FrameDefinitionField; + +public class GetVector4FieldVisitor implements FrameDefinitionFieldVisitor { + public static GetVector4FieldVisitor INSTANCE = new GetVector4FieldVisitor(); + + @Override + public Vector4Definition accept(final StringFrameDefinitionField field) { + return null; + } + + @Override + public Vector4Definition accept(final StringPairFrameDefinitionField field) { + return null; + } + + @Override + public Vector4Definition accept(final FloatFrameDefinitionField field) { + return null; + } + + @Override + public Vector4Definition accept(final Vector3FrameDefinitionField field) { + return null; + } + + @Override + public Vector4Definition accept(final Vector4FrameDefinitionField field) { + return field.getValue(); + } + + @Override + public Vector4Definition accept(final Vector2FrameDefinitionField field) { + return null; + } + + @Override + public Vector4Definition accept(final FontFrameDefinitionField field) { + return null; + } + + @Override + public Vector4Definition accept(final TextJustifyFrameDefinitionField field) { + return null; + } + +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..ff329ac --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.daemon=true +org.gradle.jvmargs=-Xms128m -Xmx1500m +org.gradle.configureondemand=false diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..bafc550 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9f7b58b --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 09 23:06:52 EDT 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4453cce --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; + 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" + which java >/dev/null 2>&1 || 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 + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@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=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@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= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +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 init + +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 + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +: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 %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jars/blp-iio-plugin.jar b/jars/blp-iio-plugin.jar new file mode 100644 index 0000000..f72f214 Binary files /dev/null and b/jars/blp-iio-plugin.jar differ diff --git a/jassparser/.gitignore b/jassparser/.gitignore new file mode 100644 index 0000000..84c048a --- /dev/null +++ b/jassparser/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/jassparser/antlr-src/Jass.g4 b/jassparser/antlr-src/Jass.g4 new file mode 100644 index 0000000..cab4935 --- /dev/null +++ b/jassparser/antlr-src/Jass.g4 @@ -0,0 +1,200 @@ +/** + * Define a grammar called Hello + */ +grammar Jass; + +@header { + package com.etheller.interpreter; +} + + +program : + newlines + | + newlines_opt + typeDefinitionBlock + (block)* + (functionBlock)* + ; + +typeDefinition : + TYPE ID EXTENDS ID newlines + ; + +type : + ID # BasicType + | + ID ARRAY # ArrayType + | + NOTHING # NothingType + ; + +global : + CONSTANT? type ID newlines # BasicGlobal + | + CONSTANT? type ID assignTail newlines # DefinitionGlobal + ; + +assignTail: + EQUALS expression; + +expression: + ID # ReferenceExpression + | + STRING_LITERAL #StringLiteralExpression + | + INTEGER #IntegerLiteralExpression + | + FUNCTION ID #FunctionReferenceExpression + | + NULL # NullExpression + | + TRUE # TrueExpression + | + FALSE # FalseExpression + | + ID '[' expression ']' # ArrayReferenceExpression + | + functionExpression # FunctionCallExpression + | + '(' expression ')' # ParentheticalExpression + | + NOT expression # NotExpression + ; + +functionExpression: + ID '(' argsList ')' + | + ID '(' ')' + ; + +argsList: + expression # SingleArgument + | + expression ',' argsList # ListArgument + ; + +//#booleanExpression: +// simpleArithmeticExpression # PassBooleanThroughExpression +// | + +statement: + CALL functionExpression newlines #CallStatement + | + SET ID EQUALS expression newlines #SetStatement + | + SET ID '[' expression ']' EQUALS expression newlines # ArrayedAssignmentStatement + | + RETURN expression newlines # ReturnStatement + | + IF ifStatementPartial # IfStatement + ; + +ifStatementPartial: + expression THEN newlines statements ENDIF newlines # SimpleIfStatement + | + expression THEN newlines statements ELSE newlines statements ENDIF newlines # IfElseStatement + | + expression THEN newlines statements ELSEIF ifStatementPartial # IfElseIfStatement + ; + +param: + type ID; + +paramList: + param # SingleParameter + | + param ',' paramList # ListParameter + | + NOTHING # NothingParameter + ; + +globalsBlock : + GLOBALS newlines (global)* ENDGLOBALS newlines ; + +typeDefinitionBlock : + (typeDefinition)* + ; + +nativeBlock: + CONSTANT? NATIVE ID TAKES paramList RETURNS type newlines + ; + +block: + globalsBlock + | + nativeBlock + ; + +functionBlock: + FUNCTION ID TAKES paramList RETURNS type newlines statements ENDFUNCTION newlines + ; + +statements: + (statement)* + ; + +newlines: + pnewlines + | + EOF; + +newlines_opt: + pnewlines + | + EOF + | + ; + +pnewlines: + NEWLINE + | + NEWLINE newlines + ; + +EQUALS : '='; + + +GLOBALS : 'globals' ; // globals +ENDGLOBALS : 'endglobals' ; // end globals block + +NATIVE : 'native' ; + +FUNCTION : 'function' ; // function +TAKES : 'takes' ; // takes +RETURNS : 'returns' ; +ENDFUNCTION : 'endfunction' ; // endfunction +NOTHING : 'nothing' ; + +CALL : 'call' ; +SET : 'set' ; +RETURN : 'return' ; + +ARRAY : 'array' ; + +TYPE : 'type'; + +EXTENDS : 'extends'; + +IF : 'if'; +THEN : 'then'; +ELSE : 'else'; +ENDIF : 'endif'; +ELSEIF : 'elseif'; +CONSTANT : 'constant'; + +STRING_LITERAL : ('"'.*?'"'); + +INTEGER : [0]|([1-9][0-9]*) ; + +NULL : 'null' ; +TRUE : 'true' ; +FALSE : 'false' ; + +NOT : 'not'; + +ID : ([a-zA-Z_][a-zA-Z_0-9]*) ; // match identifiers + +WS : [ \t]+ -> skip ; // skip spaces, tabs + +NEWLINE : '//'.*?'\r\n' | '//'.*?'\n' | '//'.*?'\r' | '\r' '\n' | '\n' | '\r'; diff --git a/jassparser/build.gradle b/jassparser/build.gradle new file mode 100644 index 0000000..e12bfa4 --- /dev/null +++ b/jassparser/build.gradle @@ -0,0 +1,49 @@ +apply plugin: "antlr" + +sourceCompatibility = 1.8 +[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' + +sourceSets.main.java.srcDirs = [ "src/", "build/generated-src" ] +sourceSets.main.antlr.srcDirs = [ "antlr-src/" ] + +project.ext.mainClassName = "com.etheller.warsmash.jassparser.Main" + +task run(dependsOn: classes, type: JavaExec) { + main = project.mainClassName + classpath = sourceSets.main.runtimeClasspath + standardInput = System.in + ignoreExitValue = true +} + +task dist(type: Jar) { + from files(sourceSets.main.output.classesDir) + from files(sourceSets.main.output.resourcesDir) + from {configurations.compile.collect {zipTree(it)}} + + manifest { + attributes 'Main-Class': project.mainClassName + } +} + +dist.dependsOn classes + +eclipse.project { + name = appName + "-jassparser" +} + +task afterEclipseImport(description: "Post processing after project generation", group: "IDE") { + doLast { + def classpath = new XmlParser().parse(file(".classpath")) + def writer = new FileWriter(file(".classpath")) + def printer = new XmlNodePrinter(new PrintWriter(writer)) + printer.setPreserveWhitespace(true) + printer.print(classpath) + } +} + + +generateGrammarSource { + maxHeapSize = "64m" + arguments += ["-visitor", "-no-listener"] + outputDirectory = file("build/generated-src/com/etheller/warsmash/jassparser") +} \ No newline at end of file diff --git a/jassparser/src/com/etheller/interpreter/ast/Assignable.java b/jassparser/src/com/etheller/interpreter/ast/Assignable.java new file mode 100644 index 0000000..0bc7262 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/Assignable.java @@ -0,0 +1,30 @@ +package com.etheller.interpreter.ast; + +import com.etheller.interpreter.ast.value.JassType; +import com.etheller.interpreter.ast.value.JassValue; +import com.etheller.interpreter.ast.value.visitor.JassTypeGettingValueVisitor; + +public class Assignable { + private JassValue value; + private final JassType type; + + public Assignable(final JassType type) { + this.type = type; + } + + public void setValue(final JassValue value) { + final JassType valueType = value.visit(JassTypeGettingValueVisitor.getInstance()); + if (!this.type.isAssignableFrom(valueType)) { + throw new RuntimeException("Incompatible types " + valueType.getName() + " != " + this.type.getName()); + } + this.value = value; + } + + public JassValue getValue() { + return this.value; + } + + public JassType getType() { + return this.type; + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/JassRunner.java b/jassparser/src/com/etheller/interpreter/ast/JassRunner.java new file mode 100644 index 0000000..5470c69 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/JassRunner.java @@ -0,0 +1,50 @@ +package com.etheller.interpreter.ast; + +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; + +import com.etheller.interpreter.JassLexer; +import com.etheller.interpreter.JassParser; +import com.etheller.interpreter.ast.visitors.JassProgramVisitor; + +public class JassRunner { + public static final boolean REPORT_SYNTAX_ERRORS = true; + + public static void main(final String[] args) { + if (args.length < 1) { + System.err.println("Usage: [...]"); + return; + } + final JassProgramVisitor jassProgramVisitor = new JassProgramVisitor(); + for (final String arg : args) { + try { + final JassLexer lexer = new JassLexer(CharStreams.fromFileName(arg)); + final JassParser parser = new JassParser(new CommonTokenStream(lexer)); + parser.addErrorListener(new BaseErrorListener() { + @Override + public void syntaxError(final Recognizer recognizer, final Object offendingSymbol, + final int line, final int charPositionInLine, final String msg, + final RecognitionException e) { + if (!REPORT_SYNTAX_ERRORS) { + return; + } + + String sourceName = recognizer.getInputStream().getSourceName(); + if (!sourceName.isEmpty()) { + sourceName = String.format("%s:%d:%d: ", sourceName, line, charPositionInLine); + } + + System.err.println(sourceName + "line " + line + ":" + charPositionInLine + " " + msg); + } + }); + jassProgramVisitor.visit(parser.program()); + } catch (final Exception e) { + e.printStackTrace(); + } + } + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/expression/ArrayRefJassExpression.java b/jassparser/src/com/etheller/interpreter/ast/expression/ArrayRefJassExpression.java new file mode 100644 index 0000000..175cc19 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/expression/ArrayRefJassExpression.java @@ -0,0 +1,41 @@ +package com.etheller.interpreter.ast.expression; + +import com.etheller.interpreter.ast.Assignable; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.ArrayJassValue; +import com.etheller.interpreter.ast.value.JassValue; +import com.etheller.interpreter.ast.value.visitor.ArrayJassValueVisitor; +import com.etheller.interpreter.ast.value.visitor.IntegerJassValueVisitor; + +public class ArrayRefJassExpression implements JassExpression { + private final String identifier; + private final JassExpression indexExpression; + + public ArrayRefJassExpression(final String identifier, final JassExpression indexExpression) { + this.identifier = identifier; + this.indexExpression = indexExpression; + } + + @Override + public JassValue evaluate(final GlobalScope globalScope, final LocalScope localScope, + final TriggerExecutionScope triggerScope) { + Assignable variable = localScope.getAssignableLocal(this.identifier); + final JassValue index = this.indexExpression.evaluate(globalScope, localScope, triggerScope); + if (variable == null) { + variable = globalScope.getAssignableGlobal(this.identifier); + } + if (variable.getValue() == null) { + throw new RuntimeException("Unable to use subscript on uninitialized variable"); + } + final ArrayJassValue arrayValue = variable.getValue().visit(ArrayJassValueVisitor.getInstance()); + if (arrayValue != null) { + return arrayValue.get(index.visit(IntegerJassValueVisitor.getInstance())); + } + else { + throw new RuntimeException("Not an array"); + } + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/expression/FunctionCallJassExpression.java b/jassparser/src/com/etheller/interpreter/ast/expression/FunctionCallJassExpression.java new file mode 100644 index 0000000..7d87b6a --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/expression/FunctionCallJassExpression.java @@ -0,0 +1,36 @@ +package com.etheller.interpreter.ast.expression; + +import java.util.ArrayList; +import java.util.List; + +import com.etheller.interpreter.ast.function.JassFunction; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.JassValue; + +public class FunctionCallJassExpression implements JassExpression { + private final String functionName; + private final List arguments; + + public FunctionCallJassExpression(final String functionName, final List arguments) { + this.functionName = functionName; + this.arguments = arguments; + } + + @Override + public JassValue evaluate(final GlobalScope globalScope, final LocalScope localScope, + final TriggerExecutionScope triggerScope) { + final JassFunction functionByName = globalScope.getFunctionByName(this.functionName); + if (functionByName == null) { + throw new RuntimeException("Undefined function: " + this.functionName); + } + final List evaluatedExpressions = new ArrayList<>(); + for (final JassExpression expr : this.arguments) { + final JassValue evaluatedExpression = expr.evaluate(globalScope, localScope, triggerScope); + evaluatedExpressions.add(evaluatedExpression); + } + return functionByName.call(evaluatedExpressions, globalScope, triggerScope); + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/expression/FunctionReferenceJassExpression.java b/jassparser/src/com/etheller/interpreter/ast/expression/FunctionReferenceJassExpression.java new file mode 100644 index 0000000..3fac3ff --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/expression/FunctionReferenceJassExpression.java @@ -0,0 +1,27 @@ +package com.etheller.interpreter.ast.expression; + +import com.etheller.interpreter.ast.function.JassFunction; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.CodeJassValue; +import com.etheller.interpreter.ast.value.JassValue; + +public class FunctionReferenceJassExpression implements JassExpression { + private final String identifier; + + public FunctionReferenceJassExpression(final String identifier) { + this.identifier = identifier; + } + + @Override + public JassValue evaluate(final GlobalScope globalScope, final LocalScope localScope, + final TriggerExecutionScope triggerScope) { + final JassFunction functionByName = globalScope.getFunctionByName(this.identifier); + if (functionByName == null) { + throw new RuntimeException("Unable to find function: " + this.identifier); + } + return new CodeJassValue(functionByName); + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/expression/JassExpression.java b/jassparser/src/com/etheller/interpreter/ast/expression/JassExpression.java new file mode 100644 index 0000000..d9e16ea --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/expression/JassExpression.java @@ -0,0 +1,10 @@ +package com.etheller.interpreter.ast.expression; + +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.JassValue; + +public interface JassExpression { + JassValue evaluate(GlobalScope globalScope, LocalScope localScope, TriggerExecutionScope triggerScope); +} diff --git a/jassparser/src/com/etheller/interpreter/ast/expression/LiteralJassExpression.java b/jassparser/src/com/etheller/interpreter/ast/expression/LiteralJassExpression.java new file mode 100644 index 0000000..426c3d3 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/expression/LiteralJassExpression.java @@ -0,0 +1,21 @@ +package com.etheller.interpreter.ast.expression; + +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.JassValue; + +public class LiteralJassExpression implements JassExpression { + private final JassValue value; + + public LiteralJassExpression(final JassValue value) { + this.value = value; + } + + @Override + public JassValue evaluate(final GlobalScope globalScope, final LocalScope localScope, + final TriggerExecutionScope triggerScope) { + return this.value; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/expression/NotJassExpression.java b/jassparser/src/com/etheller/interpreter/ast/expression/NotJassExpression.java new file mode 100644 index 0000000..eff46df --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/expression/NotJassExpression.java @@ -0,0 +1,21 @@ +package com.etheller.interpreter.ast.expression; + +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.JassValue; +import com.etheller.interpreter.ast.value.visitor.NotJassValueVisitor; + +public class NotJassExpression implements JassExpression { + private final JassExpression expression; + + public NotJassExpression(final JassExpression expression) { + this.expression = expression; + } + + @Override + public JassValue evaluate(final GlobalScope globalScope, final LocalScope localScope, + final TriggerExecutionScope triggerScope) { + return this.expression.evaluate(globalScope, localScope, triggerScope).visit(NotJassValueVisitor.getInstance()); + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/expression/ReferenceJassExpression.java b/jassparser/src/com/etheller/interpreter/ast/expression/ReferenceJassExpression.java new file mode 100644 index 0000000..2df0831 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/expression/ReferenceJassExpression.java @@ -0,0 +1,26 @@ +package com.etheller.interpreter.ast.expression; + +import com.etheller.interpreter.ast.Assignable; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.JassValue; + +public class ReferenceJassExpression implements JassExpression { + private final String identifier; + + public ReferenceJassExpression(final String identifier) { + this.identifier = identifier; + } + + @Override + public JassValue evaluate(final GlobalScope globalScope, final LocalScope localScope, + final TriggerExecutionScope triggerScope) { + final Assignable local = localScope.getAssignableLocal(this.identifier); + if (local == null) { + return globalScope.getGlobal(this.identifier); + } + return local.getValue(); + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/function/AbstractJassFunction.java b/jassparser/src/com/etheller/interpreter/ast/function/AbstractJassFunction.java new file mode 100644 index 0000000..af9107f --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/function/AbstractJassFunction.java @@ -0,0 +1,51 @@ +package com.etheller.interpreter.ast.function; + +import java.util.List; + +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.JassType; +import com.etheller.interpreter.ast.value.JassValue; +import com.etheller.interpreter.ast.value.visitor.JassTypeGettingValueVisitor; + +/** + * Not a native + * + * @author Eric + * + */ +public abstract class AbstractJassFunction implements JassFunction { + protected final List parameters; + protected final JassType returnType; + + public AbstractJassFunction(final List parameters, final JassType returnType) { + this.parameters = parameters; + this.returnType = returnType; + } + + @Override + public final JassValue call(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope) { + if (arguments.size() != this.parameters.size()) { + throw new RuntimeException("Invalid number of arguments passed to function"); + } + final LocalScope localScope = new LocalScope(); + for (int i = 0; i < this.parameters.size(); i++) { + final JassParameter parameter = this.parameters.get(i); + final JassValue argument = arguments.get(i); + if (!parameter.matchesType(argument)) { + System.err.println( + parameter.getType() + " != " + argument.visit(JassTypeGettingValueVisitor.getInstance())); + throw new RuntimeException( + "Invalid type " + argument.visit(JassTypeGettingValueVisitor.getInstance()).getName() + + " for specified argument " + parameter.getType().getName()); + } + localScope.createLocal(parameter.getIdentifier(), parameter.getType(), argument); + } + return innerCall(arguments, globalScope, triggerScope, localScope); + } + + protected abstract JassValue innerCall(final List arguments, final GlobalScope globalScope, + TriggerExecutionScope triggerScope, final LocalScope localScope); +} diff --git a/jassparser/src/com/etheller/interpreter/ast/function/JassFunction.java b/jassparser/src/com/etheller/interpreter/ast/function/JassFunction.java new file mode 100644 index 0000000..b4878ae --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/function/JassFunction.java @@ -0,0 +1,11 @@ +package com.etheller.interpreter.ast.function; + +import java.util.List; + +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.JassValue; + +public interface JassFunction { + JassValue call(List arguments, GlobalScope globalScope, TriggerExecutionScope triggerScope); +} diff --git a/jassparser/src/com/etheller/interpreter/ast/function/JassNativeManager.java b/jassparser/src/com/etheller/interpreter/ast/function/JassNativeManager.java new file mode 100644 index 0000000..931c918 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/function/JassNativeManager.java @@ -0,0 +1,37 @@ +package com.etheller.interpreter.ast.function; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.value.JassType; + +public class JassNativeManager { + private final Map nameToNativeCode; + private final Set registeredNativeNames = new HashSet<>(); + + public JassNativeManager() { + this.nameToNativeCode = new HashMap<>(); + } + + public void createNative(final String name, final JassFunction nativeCode) { + this.nameToNativeCode.put(name, nativeCode); + } + + public void registerNativeCode(final String name, final List parameters, final JassType returnType, + final GlobalScope globals) { + if (this.registeredNativeNames.contains(name)) { + throw new RuntimeException("Native already registered: " + name); + } + final JassFunction nativeCode = this.nameToNativeCode.remove(name); + globals.defineFunction(name, new NativeJassFunction(parameters, returnType, name, nativeCode)); + this.registeredNativeNames.add(name); + } + + public void checkUnregisteredNatives() { + // TODO maybe do this later + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/function/JassParameter.java b/jassparser/src/com/etheller/interpreter/ast/function/JassParameter.java new file mode 100644 index 0000000..f9c460c --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/function/JassParameter.java @@ -0,0 +1,28 @@ +package com.etheller.interpreter.ast.function; + +import com.etheller.interpreter.ast.value.JassType; +import com.etheller.interpreter.ast.value.JassValue; +import com.etheller.interpreter.ast.value.visitor.JassTypeGettingValueVisitor; + +public class JassParameter { + private final JassType type; + private final String identifier; + + public JassParameter(final JassType type, final String identifier) { + this.type = type; + this.identifier = identifier; + } + + public String getIdentifier() { + return this.identifier; + } + + public JassType getType() { + return this.type; + } + + public boolean matchesType(final JassValue value) { + final JassType valueType = value.visit(JassTypeGettingValueVisitor.getInstance()); + return this.type.isAssignableFrom(valueType); + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/function/NativeJassFunction.java b/jassparser/src/com/etheller/interpreter/ast/function/NativeJassFunction.java new file mode 100644 index 0000000..1cfbf0d --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/function/NativeJassFunction.java @@ -0,0 +1,27 @@ +package com.etheller.interpreter.ast.function; + +import java.util.List; + +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.JassType; +import com.etheller.interpreter.ast.value.JassValue; + +public class NativeJassFunction extends AbstractJassFunction { + private final String name; + private final JassFunction implementation; + + public NativeJassFunction(final List parameters, final JassType returnType, final String name, + final JassFunction impl) { + super(parameters, returnType); + this.name = name; + this.implementation = impl; + } + + @Override + protected JassValue innerCall(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope, final LocalScope localScope) { + return this.implementation.call(arguments, globalScope, triggerScope); + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/function/UserJassFunction.java b/jassparser/src/com/etheller/interpreter/ast/function/UserJassFunction.java new file mode 100644 index 0000000..92dff1f --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/function/UserJassFunction.java @@ -0,0 +1,45 @@ +package com.etheller.interpreter.ast.function; + +import java.util.List; + +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.statement.JassStatement; +import com.etheller.interpreter.ast.value.JassType; +import com.etheller.interpreter.ast.value.JassValue; +import com.etheller.interpreter.ast.value.visitor.JassTypeGettingValueVisitor; + +/** + * Not a native + * + * @author Eric + * + */ +public final class UserJassFunction extends AbstractJassFunction { + private final List statements; + + public UserJassFunction(final List statements, final List parameters, + final JassType returnType) { + super(parameters, returnType); + this.statements = statements; + } + + @Override + public JassValue innerCall(final List arguments, final GlobalScope globalScope, + final TriggerExecutionScope triggerScope, final LocalScope localScope) { + for (final JassStatement statement : this.statements) { + final JassValue returnValue = statement.execute(globalScope, localScope, triggerScope); + if (returnValue != null) { + if (returnValue.visit(JassTypeGettingValueVisitor.getInstance()) != this.returnType) { + throw new RuntimeException("Invalid return type"); + } + return returnValue; + } + } + if (JassType.NOTHING != this.returnType) { + throw new RuntimeException("Invalid return type"); + } + return null; + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/scope/GlobalScope.java b/jassparser/src/com/etheller/interpreter/ast/scope/GlobalScope.java new file mode 100644 index 0000000..45262e5 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/scope/GlobalScope.java @@ -0,0 +1,145 @@ +package com.etheller.interpreter.ast.scope; + +import java.util.HashMap; +import java.util.Map; + +import com.etheller.interpreter.ast.Assignable; +import com.etheller.interpreter.ast.function.JassFunction; +import com.etheller.interpreter.ast.value.ArrayJassType; +import com.etheller.interpreter.ast.value.ArrayJassValue; +import com.etheller.interpreter.ast.value.HandleJassType; +import com.etheller.interpreter.ast.value.JassType; +import com.etheller.interpreter.ast.value.JassValue; +import com.etheller.interpreter.ast.value.PrimitiveJassType; +import com.etheller.interpreter.ast.value.visitor.ArrayPrimitiveTypeVisitor; +import com.etheller.interpreter.ast.value.visitor.HandleJassTypeVisitor; +import com.etheller.interpreter.ast.value.visitor.HandleTypeSuperTypeLoadingVisitor; + +public final class GlobalScope { + private final Map globals = new HashMap<>(); + private final Map functions = new HashMap<>(); + private final Map types = new HashMap<>(); + private final HandleTypeSuperTypeLoadingVisitor handleTypeSuperTypeLoadingVisitor = new HandleTypeSuperTypeLoadingVisitor(); + + public final HandleJassType handleType; + + private static int lineNumber; + + public GlobalScope() { + this.handleType = registerHandleType("handle");// the handle type + registerPrimitiveType(JassType.BOOLEAN); + registerPrimitiveType(JassType.INTEGER); + registerPrimitiveType(JassType.CODE); + registerPrimitiveType(JassType.NOTHING); + registerPrimitiveType(JassType.REAL); + registerPrimitiveType(JassType.STRING); + } + + public static void setLineNumber(final int lineNo) { + lineNumber = lineNo; + } + + public static int getLineNumber() { + return lineNumber; + } + + public HandleJassType registerHandleType(final String name) { + final HandleJassType handleJassType = new HandleJassType(null, name); + this.types.put(name, handleJassType); + return handleJassType; + } + + private void registerPrimitiveType(final PrimitiveJassType type) { + this.types.put(type.getName(), type); + } + + public void createGlobalArray(final String name, final JassType type) { + final Assignable assignable = new Assignable(type); + assignable.setValue(new ArrayJassValue((ArrayJassType) type)); // TODO less bad code + this.globals.put(name, assignable); + } + + public void createGlobal(final String name, final JassType type) { + this.globals.put(name, new Assignable(type)); + } + + public void createGlobal(final String name, final JassType type, final JassValue value) { + final Assignable assignable = new Assignable(type); + assignable.setValue(value); + this.globals.put(name, assignable); + } + + public void setGlobal(final String name, final JassValue value) { + final Assignable assignable = this.globals.get(name); + if (assignable == null) { + throw new RuntimeException("Undefined global: " + name); + } + if (assignable.getType().visit(ArrayPrimitiveTypeVisitor.getInstance()) != null) { + throw new RuntimeException("Unable to assign array variable: " + name); + } + assignable.setValue(value); + } + + public JassValue getGlobal(final String name) { + final Assignable global = this.globals.get(name); + if (global == null) { + throw new RuntimeException("Undefined global: " + name); + } + return global.getValue(); + } + + public Assignable getAssignableGlobal(final String name) { + return this.globals.get(name); + } + + public void defineFunction(final String name, final JassFunction function) { + this.functions.put(name, function); + } + + public JassFunction getFunctionByName(final String name) { + return this.functions.get(name); + } + + public JassType parseType(final String text) { + final JassType type = this.types.get(text); + if (type != null) { + return type; + } + else { + throw new RuntimeException("Unknown type: " + text); + } + } + + public JassType parseArrayType(final String primitiveTypeName) { + final String arrayTypeName = primitiveTypeName + " array"; + JassType arrayType = this.types.get(arrayTypeName); + if (arrayType == null) { + arrayType = new ArrayJassType(parseType(primitiveTypeName)); + this.types.put(arrayTypeName, arrayType); + } + return arrayType; + } + + public void loadTypeDefinition(final String type, final String supertype) { + final JassType superType = this.types.get(supertype); + if (superType != null) { + final HandleJassType handleSuperType = superType.visit(HandleJassTypeVisitor.getInstance()); + if (handleSuperType != null) { + final JassType jassType = this.types.get(type); + if (jassType != null) { + jassType.visit(this.handleTypeSuperTypeLoadingVisitor.reset(handleSuperType)); + } + else { + throw new RuntimeException( + "unable to declare type " + type + " because it does not exist natively"); + } + } + else { + throw new RuntimeException("type " + type + " cannot extend primitive type " + supertype); + } + } + else { + throw new RuntimeException("type " + type + " cannot extend unknown type " + supertype); + } + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/scope/LocalScope.java b/jassparser/src/com/etheller/interpreter/ast/scope/LocalScope.java new file mode 100644 index 0000000..1aea3d7 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/scope/LocalScope.java @@ -0,0 +1,42 @@ +package com.etheller.interpreter.ast.scope; + +import java.util.HashMap; +import java.util.Map; + +import com.etheller.interpreter.ast.Assignable; +import com.etheller.interpreter.ast.value.JassType; +import com.etheller.interpreter.ast.value.JassValue; + +public final class LocalScope { + private final Map locals = new HashMap<>(); + + public void createLocal(final String name, final JassType type) { + this.locals.put(name, new Assignable(type)); + } + + public void createLocal(final String name, final JassType type, final JassValue value) { + final Assignable assignable = new Assignable(type); + assignable.setValue(value); + this.locals.put(name, assignable); + } + + public void setLocal(final String name, final JassValue value) { + final Assignable assignable = this.locals.get(name); + if (assignable == null) { + throw new RuntimeException("Undefined local variable: " + name); + } + assignable.setValue(value); + } + + public JassValue getLocal(final String name) { + final Assignable local = this.locals.get(name); + if (local == null) { + throw new RuntimeException("Undefined local variable: " + name); + } + return local.getValue(); + } + + public Assignable getAssignableLocal(final String name) { + return this.locals.get(name); + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/scope/TriggerExecutionScope.java b/jassparser/src/com/etheller/interpreter/ast/scope/TriggerExecutionScope.java new file mode 100644 index 0000000..e9799ed --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/scope/TriggerExecutionScope.java @@ -0,0 +1,18 @@ +package com.etheller.interpreter.ast.scope; + +import com.etheller.interpreter.ast.scope.trigger.Trigger; + +public class TriggerExecutionScope { + public static final TriggerExecutionScope EMPTY = new TriggerExecutionScope(null); + + private final Trigger triggeringTrigger; + + public TriggerExecutionScope(final Trigger triggeringTrigger) { + this.triggeringTrigger = triggeringTrigger; + } + + public Trigger getTriggeringTrigger() { + return this.triggeringTrigger; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/scope/TypeDefinition.java b/jassparser/src/com/etheller/interpreter/ast/scope/TypeDefinition.java new file mode 100644 index 0000000..1331a96 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/scope/TypeDefinition.java @@ -0,0 +1,11 @@ +package com.etheller.interpreter.ast.scope; + +public class TypeDefinition { + private final String name; + private final String supertype; + + public TypeDefinition(final String name, final String supertype) { + this.name = name; + this.supertype = supertype; + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/scope/trigger/Trigger.java b/jassparser/src/com/etheller/interpreter/ast/scope/trigger/Trigger.java new file mode 100644 index 0000000..6c46ad3 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/scope/trigger/Trigger.java @@ -0,0 +1,74 @@ +package com.etheller.interpreter.ast.scope.trigger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.etheller.interpreter.ast.function.JassFunction; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; + +public class Trigger { + private final List conditions = new ArrayList<>(); + private final List actions = new ArrayList<>(); + private int evalCount; + private int execCount; + private boolean enabled = true; + // used for eval + private transient final TriggerExecutionScope triggerExecutionScope = new TriggerExecutionScope(this); + + public int addAction(final JassFunction function) { + final int index = this.actions.size(); + this.actions.add(function); + return index; + } + + public int addCondition(final TriggerBooleanExpression boolexpr) { + final int index = this.conditions.size(); + this.conditions.add(boolexpr); + return index; + } + + public void removeCondition(final TriggerBooleanExpression boolexpr) { + this.conditions.remove(boolexpr); + } + + public void removeConditionAtIndex(final int conditionIndex) { + this.conditions.remove(conditionIndex); + } + + public int getEvalCount() { + return this.evalCount; + } + + public int getExecCount() { + return this.execCount; + } + + public boolean evaluate(final GlobalScope globalScope, final TriggerExecutionScope triggerScope) { + for (final TriggerBooleanExpression condition : this.conditions) { + if (!condition.evaluate(globalScope, triggerScope)) { + return false; + } + } + return true; + } + + public void execute(final GlobalScope globalScope) { + for (final JassFunction action : this.actions) { + action.call(Collections.emptyList(), globalScope, this.triggerExecutionScope); + } + } + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(final boolean enabled) { + this.enabled = enabled; + } + + public void destroy() { + + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/scope/trigger/TriggerBooleanExpression.java b/jassparser/src/com/etheller/interpreter/ast/scope/trigger/TriggerBooleanExpression.java new file mode 100644 index 0000000..8463317 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/scope/trigger/TriggerBooleanExpression.java @@ -0,0 +1,8 @@ +package com.etheller.interpreter.ast.scope.trigger; + +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; + +public interface TriggerBooleanExpression { + boolean evaluate(GlobalScope globalScope, TriggerExecutionScope triggerScope); +} diff --git a/jassparser/src/com/etheller/interpreter/ast/statement/JassArrayedAssignmentStatement.java b/jassparser/src/com/etheller/interpreter/ast/statement/JassArrayedAssignmentStatement.java new file mode 100644 index 0000000..5b80156 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/statement/JassArrayedAssignmentStatement.java @@ -0,0 +1,50 @@ +package com.etheller.interpreter.ast.statement; + +import com.etheller.interpreter.ast.Assignable; +import com.etheller.interpreter.ast.expression.JassExpression; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.ArrayJassValue; +import com.etheller.interpreter.ast.value.JassValue; +import com.etheller.interpreter.ast.value.visitor.ArrayJassValueVisitor; +import com.etheller.interpreter.ast.value.visitor.IntegerJassValueVisitor; + +public class JassArrayedAssignmentStatement implements JassStatement { + private final String identifier; + private final JassExpression indexExpression; + private final JassExpression expression; + private final int lineNo; + + public JassArrayedAssignmentStatement(final int lineNo, final String identifier, + final JassExpression indexExpression, final JassExpression expression) { + this.lineNo = lineNo; + this.identifier = identifier; + this.indexExpression = indexExpression; + this.expression = expression; + } + + @Override + public JassValue execute(final GlobalScope globalScope, final LocalScope localScope, + final TriggerExecutionScope triggerScope) { + globalScope.setLineNumber(this.lineNo); + Assignable variable = localScope.getAssignableLocal(this.identifier); + final JassValue index = this.indexExpression.evaluate(globalScope, localScope, triggerScope); + if (variable == null) { + variable = globalScope.getAssignableGlobal(this.identifier); + } + if (variable.getValue() == null) { + throw new RuntimeException("Unable to assign uninitialized array"); + } + final ArrayJassValue arrayValue = variable.getValue().visit(ArrayJassValueVisitor.getInstance()); + if (arrayValue != null) { + arrayValue.set(index.visit(IntegerJassValueVisitor.getInstance()), + this.expression.evaluate(globalScope, localScope, triggerScope)); + } + else { + throw new RuntimeException("Not an array"); + } + return null; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/statement/JassCallStatement.java b/jassparser/src/com/etheller/interpreter/ast/statement/JassCallStatement.java new file mode 100644 index 0000000..a5e9635 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/statement/JassCallStatement.java @@ -0,0 +1,42 @@ +package com.etheller.interpreter.ast.statement; + +import java.util.ArrayList; +import java.util.List; + +import com.etheller.interpreter.ast.expression.JassExpression; +import com.etheller.interpreter.ast.function.JassFunction; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.JassValue; + +public class JassCallStatement implements JassStatement { + private final int lineNo; + private final String functionName; + private final List arguments; + + public JassCallStatement(final int lineNo, final String functionName, final List arguments) { + this.lineNo = lineNo; + this.functionName = functionName; + this.arguments = arguments; + } + + @Override + public JassValue execute(final GlobalScope globalScope, final LocalScope localScope, + final TriggerExecutionScope triggerScope) { + globalScope.setLineNumber(this.lineNo); + final JassFunction functionByName = globalScope.getFunctionByName(this.functionName); + if (functionByName == null) { + throw new RuntimeException("Undefined function: " + this.functionName); + } + final List evaluatedExpressions = new ArrayList<>(); + for (final JassExpression expr : this.arguments) { + final JassValue evaluatedExpression = expr.evaluate(globalScope, localScope, triggerScope); + evaluatedExpressions.add(evaluatedExpression); + } + functionByName.call(evaluatedExpressions, globalScope, triggerScope); + // throw away return value + return null; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/statement/JassIfElseIfStatement.java b/jassparser/src/com/etheller/interpreter/ast/statement/JassIfElseIfStatement.java new file mode 100644 index 0000000..5f1f208 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/statement/JassIfElseIfStatement.java @@ -0,0 +1,45 @@ +package com.etheller.interpreter.ast.statement; + +import java.util.List; + +import com.etheller.interpreter.ast.expression.JassExpression; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.JassValue; +import com.etheller.interpreter.ast.value.visitor.BooleanJassValueVisitor; + +public class JassIfElseIfStatement implements JassStatement { + private final int lineNo; + private final JassExpression condition; + private final List thenStatements; + private final JassStatement elseifTail; + + public JassIfElseIfStatement(final int lineNo, final JassExpression condition, + final List thenStatements, final JassStatement elseifTail) { + this.lineNo = lineNo; + this.condition = condition; + this.thenStatements = thenStatements; + this.elseifTail = elseifTail; + } + + @Override + public JassValue execute(final GlobalScope globalScope, final LocalScope localScope, + final TriggerExecutionScope triggerScope) { + globalScope.setLineNumber(this.lineNo); + if (this.condition.evaluate(globalScope, localScope, triggerScope) + .visit(BooleanJassValueVisitor.getInstance())) { + for (final JassStatement statement : this.thenStatements) { + final JassValue returnValue = statement.execute(globalScope, localScope, triggerScope); + if (returnValue != null) { + return returnValue; + } + } + } + else { + return this.elseifTail.execute(globalScope, localScope, triggerScope); + } + return null; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/statement/JassIfElseStatement.java b/jassparser/src/com/etheller/interpreter/ast/statement/JassIfElseStatement.java new file mode 100644 index 0000000..20b9719 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/statement/JassIfElseStatement.java @@ -0,0 +1,50 @@ +package com.etheller.interpreter.ast.statement; + +import java.util.List; + +import com.etheller.interpreter.ast.expression.JassExpression; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.JassValue; +import com.etheller.interpreter.ast.value.visitor.BooleanJassValueVisitor; + +public class JassIfElseStatement implements JassStatement { + private final int lineNo; + private final JassExpression condition; + private final List thenStatements; + private final List elseStatements; + + public JassIfElseStatement(final int lineNo, final JassExpression condition, + final List thenStatements, final List elseStatements) { + this.lineNo = lineNo; + this.condition = condition; + this.thenStatements = thenStatements; + this.elseStatements = elseStatements; + } + + @Override + public JassValue execute(final GlobalScope globalScope, final LocalScope localScope, + final TriggerExecutionScope triggerScope) { + globalScope.setLineNumber(this.lineNo); + if (this.condition.evaluate(globalScope, localScope, triggerScope) + .visit(BooleanJassValueVisitor.getInstance())) { + for (final JassStatement statement : this.thenStatements) { + final JassValue returnValue = statement.execute(globalScope, localScope, triggerScope); + if (returnValue != null) { + return returnValue; + } + } + } + else { + for (final JassStatement statement : this.elseStatements) { + final JassValue returnValue = statement.execute(globalScope, localScope, triggerScope); + if (returnValue != null) { + return returnValue; + } + } + } + return null; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/statement/JassIfStatement.java b/jassparser/src/com/etheller/interpreter/ast/statement/JassIfStatement.java new file mode 100644 index 0000000..abe42a0 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/statement/JassIfStatement.java @@ -0,0 +1,47 @@ +package com.etheller.interpreter.ast.statement; + +import java.util.List; + +import com.etheller.interpreter.ast.expression.JassExpression; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.JassValue; +import com.etheller.interpreter.ast.value.visitor.BooleanJassValueVisitor; + +public class JassIfStatement implements JassStatement { + private final int lineNo; + private final JassExpression condition; + private final List thenStatements; + + public JassIfStatement(final int lineNo, final JassExpression condition, final List thenStatements) { + this.lineNo = lineNo; + this.condition = condition; + this.thenStatements = thenStatements; + } + + public JassExpression getCondition() { + return this.condition; + } + + public List getThenStatements() { + return this.thenStatements; + } + + @Override + public JassValue execute(final GlobalScope globalScope, final LocalScope localScope, + final TriggerExecutionScope triggerScope) { + globalScope.setLineNumber(this.lineNo); + if (this.condition.evaluate(globalScope, localScope, triggerScope) + .visit(BooleanJassValueVisitor.getInstance())) { + for (final JassStatement statement : this.thenStatements) { + final JassValue returnValue = statement.execute(globalScope, localScope, triggerScope); + if (returnValue != null) { + return returnValue; + } + } + } + return null; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/statement/JassReturnStatement.java b/jassparser/src/com/etheller/interpreter/ast/statement/JassReturnStatement.java new file mode 100644 index 0000000..adb1b4e --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/statement/JassReturnStatement.java @@ -0,0 +1,25 @@ +package com.etheller.interpreter.ast.statement; + +import com.etheller.interpreter.ast.expression.JassExpression; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.JassValue; + +public class JassReturnStatement implements JassStatement { + private final int lineNo; + private final JassExpression expression; + + public JassReturnStatement(final int lineNo, final JassExpression expression) { + this.lineNo = lineNo; + this.expression = expression; + } + + @Override + public JassValue execute(final GlobalScope globalScope, final LocalScope localScope, + final TriggerExecutionScope triggerScope) { + globalScope.setLineNumber(this.lineNo); + return this.expression.evaluate(globalScope, localScope, triggerScope); + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/statement/JassSetStatement.java b/jassparser/src/com/etheller/interpreter/ast/statement/JassSetStatement.java new file mode 100644 index 0000000..e8249d4 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/statement/JassSetStatement.java @@ -0,0 +1,35 @@ +package com.etheller.interpreter.ast.statement; + +import com.etheller.interpreter.ast.Assignable; +import com.etheller.interpreter.ast.expression.JassExpression; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.JassValue; + +public class JassSetStatement implements JassStatement { + private final int lineNo; + private final String identifier; + private final JassExpression expression; + + public JassSetStatement(final int lineNo, final String identifier, final JassExpression expression) { + this.lineNo = lineNo; + this.identifier = identifier; + this.expression = expression; + } + + @Override + public JassValue execute(final GlobalScope globalScope, final LocalScope localScope, + final TriggerExecutionScope triggerScope) { + globalScope.setLineNumber(this.lineNo); + final Assignable local = localScope.getAssignableLocal(this.identifier); + if (local != null) { + local.setValue(this.expression.evaluate(globalScope, localScope, triggerScope)); + } + else { + globalScope.setGlobal(this.identifier, this.expression.evaluate(globalScope, localScope, triggerScope)); + } + return null; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/statement/JassStatement.java b/jassparser/src/com/etheller/interpreter/ast/statement/JassStatement.java new file mode 100644 index 0000000..5c2099d --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/statement/JassStatement.java @@ -0,0 +1,12 @@ +package com.etheller.interpreter.ast.statement; + +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.value.JassValue; + +public interface JassStatement { + // When a value is returned, this indicates a RETURN statement, + // and will end outer execution + JassValue execute(GlobalScope globalScope, LocalScope localScope, TriggerExecutionScope triggerScope); +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/ArrayJassType.java b/jassparser/src/com/etheller/interpreter/ast/value/ArrayJassType.java new file mode 100644 index 0000000..109a559 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/ArrayJassType.java @@ -0,0 +1,30 @@ +package com.etheller.interpreter.ast.value; + +public class ArrayJassType implements JassType { + private final JassType primitiveType; + private final String name; + + public ArrayJassType(final JassType primitiveType) { + this.primitiveType = primitiveType; + this.name = primitiveType.getName() + " array"; + } + + @Override + public boolean isAssignableFrom(final JassType value) { + return value == this; + } + + public JassType getPrimitiveType() { + return this.primitiveType; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public TYPE visit(final JassTypeVisitor visitor) { + return visitor.accept(this); + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/ArrayJassValue.java b/jassparser/src/com/etheller/interpreter/ast/value/ArrayJassValue.java new file mode 100644 index 0000000..158d8d3 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/ArrayJassValue.java @@ -0,0 +1,34 @@ +package com.etheller.interpreter.ast.value; + +import com.etheller.interpreter.ast.value.visitor.JassTypeGettingValueVisitor; + +public class ArrayJassValue implements JassValue { + private final JassValue[] data = new JassValue[8192]; // that's the array size in JASS + private final ArrayJassType type; + + public ArrayJassValue(final ArrayJassType type) { + this.type = type; + } + + @Override + public TYPE visit(final JassValueVisitor visitor) { + return visitor.accept(this); + } + + public void set(final int index, final JassValue value) { + if (value.visit(JassTypeGettingValueVisitor.getInstance()) != type.getPrimitiveType()) { + throw new IllegalStateException( + "Illegal type for assignment to " + type.getPrimitiveType().getName() + " array"); + } + data[index] = value; + } + + public JassValue get(final int index) { + return data[index]; + } + + public ArrayJassType getType() { + return type; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/BooleanJassValue.java b/jassparser/src/com/etheller/interpreter/ast/value/BooleanJassValue.java new file mode 100644 index 0000000..602a8f5 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/BooleanJassValue.java @@ -0,0 +1,39 @@ +package com.etheller.interpreter.ast.value; + +public class BooleanJassValue implements JassValue { + public static final BooleanJassValue TRUE = new BooleanJassValue(true); + public static final BooleanJassValue FALSE = new BooleanJassValue(false); + + private final boolean value; + + public BooleanJassValue(final boolean value) { + this.value = value; + } + + public boolean getValue() { + return this.value; + } + + @Override + public TYPE visit(final JassValueVisitor visitor) { + return visitor.accept(this); + } + + public static BooleanJassValue inverse(final BooleanJassValue value) { + if (value.value) { + return FALSE; + } + else { + return TRUE; + } + } + + public static BooleanJassValue of(final boolean flag) { + if (flag) { + return TRUE; + } + else { + return FALSE; + } + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/CodeJassValue.java b/jassparser/src/com/etheller/interpreter/ast/value/CodeJassValue.java new file mode 100644 index 0000000..2858184 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/CodeJassValue.java @@ -0,0 +1,21 @@ +package com.etheller.interpreter.ast.value; + +import com.etheller.interpreter.ast.function.JassFunction; + +public class CodeJassValue implements JassValue { + private final JassFunction value; + + public CodeJassValue(final JassFunction value) { + this.value = value; + } + + public JassFunction getValue() { + return value; + } + + @Override + public TYPE visit(final JassValueVisitor visitor) { + return visitor.accept(this); + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/HandleJassType.java b/jassparser/src/com/etheller/interpreter/ast/value/HandleJassType.java new file mode 100644 index 0000000..bd025fb --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/HandleJassType.java @@ -0,0 +1,43 @@ +package com.etheller.interpreter.ast.value; + +import com.etheller.interpreter.ast.value.visitor.SuperTypeVisitor; + +public class HandleJassType implements JassType { + private HandleJassType superType; + private final String name; + + public HandleJassType(final HandleJassType superType, final String name) { + this.superType = superType; + this.name = name; + } + + @Override + public boolean isAssignableFrom(JassType valueType) { + while (valueType != null) { + if (this == valueType) { + return true; + } + valueType = valueType.visit(SuperTypeVisitor.getInstance()); + } + return false; + } + + @Override + public String getName() { + return this.name; + } + + public HandleJassType getSuperType() { + return this.superType; + } + + public void setSuperType(final HandleJassType superType) { + this.superType = superType; + } + + @Override + public TYPE visit(final JassTypeVisitor visitor) { + return visitor.accept(this); + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/HandleJassValue.java b/jassparser/src/com/etheller/interpreter/ast/value/HandleJassValue.java new file mode 100644 index 0000000..78032fd --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/HandleJassValue.java @@ -0,0 +1,25 @@ +package com.etheller.interpreter.ast.value; + +public class HandleJassValue implements JassValue { + private final HandleJassType type; + private final Object javaValue; + + public HandleJassValue(final HandleJassType type, final Object javaValue) { + this.type = type; + this.javaValue = javaValue; + } + + public HandleJassType getType() { + return this.type; + } + + public Object getJavaValue() { + return this.javaValue; + } + + @Override + public TYPE visit(final JassValueVisitor visitor) { + return visitor.accept(this); + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/IntegerJassValue.java b/jassparser/src/com/etheller/interpreter/ast/value/IntegerJassValue.java new file mode 100644 index 0000000..bec6cfe --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/IntegerJassValue.java @@ -0,0 +1,18 @@ +package com.etheller.interpreter.ast.value; + +public class IntegerJassValue implements JassValue { + private final int value; + + public IntegerJassValue(final int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + @Override + public TYPE visit(final JassValueVisitor visitor) { + return visitor.accept(this); + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/JassType.java b/jassparser/src/com/etheller/interpreter/ast/value/JassType.java new file mode 100644 index 0000000..c72141e --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/JassType.java @@ -0,0 +1,16 @@ +package com.etheller.interpreter.ast.value; + +public interface JassType { + TYPE visit(JassTypeVisitor visitor); + + String getName(); // used for error messages + + boolean isAssignableFrom(JassType value); + + public static final PrimitiveJassType INTEGER = new PrimitiveJassType("integer"); + public static final PrimitiveJassType STRING = new PrimitiveJassType("string"); + public static final PrimitiveJassType CODE = new PrimitiveJassType("code"); + public static final PrimitiveJassType REAL = new RealJassType("real"); + public static final PrimitiveJassType BOOLEAN = new PrimitiveJassType("boolean"); + public static final PrimitiveJassType NOTHING = new PrimitiveJassType("nothing"); +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/JassTypeVisitor.java b/jassparser/src/com/etheller/interpreter/ast/value/JassTypeVisitor.java new file mode 100644 index 0000000..4d25c5a --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/JassTypeVisitor.java @@ -0,0 +1,9 @@ +package com.etheller.interpreter.ast.value; + +public interface JassTypeVisitor { + TYPE accept(PrimitiveJassType primitiveType); + + TYPE accept(ArrayJassType arrayType); + + TYPE accept(HandleJassType type); +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/JassValue.java b/jassparser/src/com/etheller/interpreter/ast/value/JassValue.java new file mode 100644 index 0000000..c1c0c43 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/JassValue.java @@ -0,0 +1,5 @@ +package com.etheller.interpreter.ast.value; + +public interface JassValue { + TYPE visit(JassValueVisitor visitor); +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/JassValueVisitor.java b/jassparser/src/com/etheller/interpreter/ast/value/JassValueVisitor.java new file mode 100644 index 0000000..db03052 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/JassValueVisitor.java @@ -0,0 +1,17 @@ +package com.etheller.interpreter.ast.value; + +public interface JassValueVisitor { + TYPE accept(IntegerJassValue value); + + TYPE accept(RealJassValue value); + + TYPE accept(BooleanJassValue value); + + TYPE accept(StringJassValue value); + + TYPE accept(CodeJassValue value); + + TYPE accept(ArrayJassValue value); + + TYPE accept(HandleJassValue value); +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/PrimitiveJassType.java b/jassparser/src/com/etheller/interpreter/ast/value/PrimitiveJassType.java new file mode 100644 index 0000000..38bd250 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/PrimitiveJassType.java @@ -0,0 +1,25 @@ +package com.etheller.interpreter.ast.value; + +public class PrimitiveJassType implements JassType { + private final String name; + + public PrimitiveJassType(final String name) { + this.name = name; + } + + @Override + public boolean isAssignableFrom(final JassType value) { + return value == this; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public TYPE visit(final JassTypeVisitor visitor) { + return visitor.accept(this); + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/RealJassType.java b/jassparser/src/com/etheller/interpreter/ast/value/RealJassType.java new file mode 100644 index 0000000..3e2d66d --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/RealJassType.java @@ -0,0 +1,16 @@ +package com.etheller.interpreter.ast.value; + +public class RealJassType extends PrimitiveJassType { + + public RealJassType(final String name) { + super(name); + } + + @Override + public boolean isAssignableFrom(final JassType value) { + if (value == JassType.INTEGER) { + return true; + } + return super.isAssignableFrom(value); + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/RealJassValue.java b/jassparser/src/com/etheller/interpreter/ast/value/RealJassValue.java new file mode 100644 index 0000000..49636dc --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/RealJassValue.java @@ -0,0 +1,18 @@ +package com.etheller.interpreter.ast.value; + +public class RealJassValue implements JassValue { + private final double value; + + public RealJassValue(final double value) { + this.value = value; + } + + public double getValue() { + return value; + } + + @Override + public TYPE visit(final JassValueVisitor visitor) { + return visitor.accept(this); + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/StringJassValue.java b/jassparser/src/com/etheller/interpreter/ast/value/StringJassValue.java new file mode 100644 index 0000000..9cbcbb8 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/StringJassValue.java @@ -0,0 +1,24 @@ +package com.etheller.interpreter.ast.value; + +public class StringJassValue implements JassValue { + private final String value; + + public static StringJassValue of(final String value) { + // later this could do that dumb thing jass does with making sure we dont create + // duplicate instances, maybe + return new StringJassValue(value); + } + + public StringJassValue(final String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + + @Override + public TYPE visit(final JassValueVisitor visitor) { + return visitor.accept(this); + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/visitor/ArrayJassValueVisitor.java b/jassparser/src/com/etheller/interpreter/ast/value/visitor/ArrayJassValueVisitor.java new file mode 100644 index 0000000..68a75b5 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/visitor/ArrayJassValueVisitor.java @@ -0,0 +1,54 @@ +package com.etheller.interpreter.ast.value.visitor; + +import com.etheller.interpreter.ast.value.ArrayJassValue; +import com.etheller.interpreter.ast.value.BooleanJassValue; +import com.etheller.interpreter.ast.value.CodeJassValue; +import com.etheller.interpreter.ast.value.HandleJassValue; +import com.etheller.interpreter.ast.value.IntegerJassValue; +import com.etheller.interpreter.ast.value.JassValueVisitor; +import com.etheller.interpreter.ast.value.RealJassValue; +import com.etheller.interpreter.ast.value.StringJassValue; + +public class ArrayJassValueVisitor implements JassValueVisitor { + private static final ArrayJassValueVisitor INSTANCE = new ArrayJassValueVisitor(); + + public static ArrayJassValueVisitor getInstance() { + return INSTANCE; + } + + @Override + public ArrayJassValue accept(final IntegerJassValue value) { + return null; + } + + @Override + public ArrayJassValue accept(final RealJassValue value) { + return null; + } + + @Override + public ArrayJassValue accept(final BooleanJassValue value) { + return null; + } + + @Override + public ArrayJassValue accept(final StringJassValue value) { + return null; + } + + @Override + public ArrayJassValue accept(final CodeJassValue value) { + return null; + } + + @Override + public ArrayJassValue accept(final ArrayJassValue value) { + return value; + } + + @Override + public ArrayJassValue accept(final HandleJassValue value) { + return null; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/visitor/ArrayPrimitiveTypeVisitor.java b/jassparser/src/com/etheller/interpreter/ast/value/visitor/ArrayPrimitiveTypeVisitor.java new file mode 100644 index 0000000..11bb06e --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/visitor/ArrayPrimitiveTypeVisitor.java @@ -0,0 +1,31 @@ +package com.etheller.interpreter.ast.value.visitor; + +import com.etheller.interpreter.ast.value.ArrayJassType; +import com.etheller.interpreter.ast.value.HandleJassType; +import com.etheller.interpreter.ast.value.JassType; +import com.etheller.interpreter.ast.value.JassTypeVisitor; +import com.etheller.interpreter.ast.value.PrimitiveJassType; + +public class ArrayPrimitiveTypeVisitor implements JassTypeVisitor { + private static final ArrayPrimitiveTypeVisitor INSTANCE = new ArrayPrimitiveTypeVisitor(); + + public static ArrayPrimitiveTypeVisitor getInstance() { + return INSTANCE; + } + + @Override + public JassType accept(final PrimitiveJassType primitiveType) { + return null; + } + + @Override + public JassType accept(final ArrayJassType arrayType) { + return arrayType.getPrimitiveType(); + } + + @Override + public JassType accept(final HandleJassType type) { + return null; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/visitor/BooleanJassValueVisitor.java b/jassparser/src/com/etheller/interpreter/ast/value/visitor/BooleanJassValueVisitor.java new file mode 100644 index 0000000..2b7f995 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/visitor/BooleanJassValueVisitor.java @@ -0,0 +1,54 @@ +package com.etheller.interpreter.ast.value.visitor; + +import com.etheller.interpreter.ast.value.ArrayJassValue; +import com.etheller.interpreter.ast.value.BooleanJassValue; +import com.etheller.interpreter.ast.value.CodeJassValue; +import com.etheller.interpreter.ast.value.HandleJassValue; +import com.etheller.interpreter.ast.value.IntegerJassValue; +import com.etheller.interpreter.ast.value.JassValueVisitor; +import com.etheller.interpreter.ast.value.RealJassValue; +import com.etheller.interpreter.ast.value.StringJassValue; + +public class BooleanJassValueVisitor implements JassValueVisitor { + private static final BooleanJassValueVisitor INSTANCE = new BooleanJassValueVisitor(); + + public static BooleanJassValueVisitor getInstance() { + return INSTANCE; + } + + @Override + public Boolean accept(final IntegerJassValue value) { + throw new IllegalStateException("Unable to convert " + value + " to boolean"); + } + + @Override + public Boolean accept(final RealJassValue value) { + throw new IllegalStateException("Unable to convert " + value + " to boolean"); + } + + @Override + public Boolean accept(final BooleanJassValue value) { + return value.getValue(); + } + + @Override + public Boolean accept(final StringJassValue value) { + throw new IllegalStateException("Unable to convert " + value + " to boolean"); + } + + @Override + public Boolean accept(final CodeJassValue value) { + throw new IllegalStateException("Unable to convert " + value + " to boolean"); + } + + @Override + public Boolean accept(final ArrayJassValue value) { + throw new IllegalStateException("Unable to convert " + value + " to boolean"); + } + + @Override + public Boolean accept(final HandleJassValue value) { + throw new IllegalStateException("Unable to convert " + value + " to boolean"); + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/visitor/HandleJassTypeVisitor.java b/jassparser/src/com/etheller/interpreter/ast/value/visitor/HandleJassTypeVisitor.java new file mode 100644 index 0000000..3f4e75a --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/visitor/HandleJassTypeVisitor.java @@ -0,0 +1,30 @@ +package com.etheller.interpreter.ast.value.visitor; + +import com.etheller.interpreter.ast.value.ArrayJassType; +import com.etheller.interpreter.ast.value.HandleJassType; +import com.etheller.interpreter.ast.value.JassTypeVisitor; +import com.etheller.interpreter.ast.value.PrimitiveJassType; + +public class HandleJassTypeVisitor implements JassTypeVisitor { + private static final HandleJassTypeVisitor INSTANCE = new HandleJassTypeVisitor(); + + public static HandleJassTypeVisitor getInstance() { + return INSTANCE; + } + + @Override + public HandleJassType accept(final PrimitiveJassType primitiveType) { + return null; + } + + @Override + public HandleJassType accept(final ArrayJassType arrayType) { + return null; + } + + @Override + public HandleJassType accept(final HandleJassType type) { + return type; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/visitor/HandleTypeSuperTypeLoadingVisitor.java b/jassparser/src/com/etheller/interpreter/ast/value/visitor/HandleTypeSuperTypeLoadingVisitor.java new file mode 100644 index 0000000..a2a4de7 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/visitor/HandleTypeSuperTypeLoadingVisitor.java @@ -0,0 +1,32 @@ +package com.etheller.interpreter.ast.value.visitor; + +import com.etheller.interpreter.ast.value.ArrayJassType; +import com.etheller.interpreter.ast.value.HandleJassType; +import com.etheller.interpreter.ast.value.JassTypeVisitor; +import com.etheller.interpreter.ast.value.PrimitiveJassType; + +public class HandleTypeSuperTypeLoadingVisitor implements JassTypeVisitor { + private HandleJassType superType; + + public HandleTypeSuperTypeLoadingVisitor reset(final HandleJassType superType) { + this.superType = superType; + return this; + } + + @Override + public Void accept(final PrimitiveJassType primitiveType) { + return null; + } + + @Override + public Void accept(final ArrayJassType arrayType) { + return null; + } + + @Override + public Void accept(final HandleJassType type) { + type.setSuperType(this.superType); + return null; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/visitor/IntegerJassValueVisitor.java b/jassparser/src/com/etheller/interpreter/ast/value/visitor/IntegerJassValueVisitor.java new file mode 100644 index 0000000..2edae0e --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/visitor/IntegerJassValueVisitor.java @@ -0,0 +1,54 @@ +package com.etheller.interpreter.ast.value.visitor; + +import com.etheller.interpreter.ast.value.ArrayJassValue; +import com.etheller.interpreter.ast.value.BooleanJassValue; +import com.etheller.interpreter.ast.value.CodeJassValue; +import com.etheller.interpreter.ast.value.HandleJassValue; +import com.etheller.interpreter.ast.value.IntegerJassValue; +import com.etheller.interpreter.ast.value.JassValueVisitor; +import com.etheller.interpreter.ast.value.RealJassValue; +import com.etheller.interpreter.ast.value.StringJassValue; + +public class IntegerJassValueVisitor implements JassValueVisitor { + private static final IntegerJassValueVisitor INSTANCE = new IntegerJassValueVisitor(); + + public static IntegerJassValueVisitor getInstance() { + return INSTANCE; + } + + @Override + public Integer accept(final IntegerJassValue value) { + return value.getValue(); + } + + @Override + public Integer accept(final RealJassValue value) { + return (int) value.getValue(); + } + + @Override + public Integer accept(final BooleanJassValue value) { + return 0; + } + + @Override + public Integer accept(final StringJassValue value) { + return 0; + } + + @Override + public Integer accept(final CodeJassValue value) { + return 0; + } + + @Override + public Integer accept(final ArrayJassValue value) { + return 0; + } + + @Override + public Integer accept(final HandleJassValue value) { + return 0; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/visitor/JassFunctionJassValueVisitor.java b/jassparser/src/com/etheller/interpreter/ast/value/visitor/JassFunctionJassValueVisitor.java new file mode 100644 index 0000000..ee5f6ba --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/visitor/JassFunctionJassValueVisitor.java @@ -0,0 +1,55 @@ +package com.etheller.interpreter.ast.value.visitor; + +import com.etheller.interpreter.ast.function.JassFunction; +import com.etheller.interpreter.ast.value.ArrayJassValue; +import com.etheller.interpreter.ast.value.BooleanJassValue; +import com.etheller.interpreter.ast.value.CodeJassValue; +import com.etheller.interpreter.ast.value.HandleJassValue; +import com.etheller.interpreter.ast.value.IntegerJassValue; +import com.etheller.interpreter.ast.value.JassValueVisitor; +import com.etheller.interpreter.ast.value.RealJassValue; +import com.etheller.interpreter.ast.value.StringJassValue; + +public class JassFunctionJassValueVisitor implements JassValueVisitor { + private static final JassFunctionJassValueVisitor INSTANCE = new JassFunctionJassValueVisitor(); + + public static JassFunctionJassValueVisitor getInstance() { + return INSTANCE; + } + + @Override + public JassFunction accept(final IntegerJassValue value) { + return null; + } + + @Override + public JassFunction accept(final RealJassValue value) { + return null; + } + + @Override + public JassFunction accept(final BooleanJassValue value) { + return null; + } + + @Override + public JassFunction accept(final StringJassValue value) { + return null; + } + + @Override + public JassFunction accept(final CodeJassValue value) { + return value.getValue(); + } + + @Override + public JassFunction accept(final ArrayJassValue value) { + return null; + } + + @Override + public JassFunction accept(final HandleJassValue value) { + return null; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/visitor/JassTypeGettingValueVisitor.java b/jassparser/src/com/etheller/interpreter/ast/value/visitor/JassTypeGettingValueVisitor.java new file mode 100644 index 0000000..95a5c95 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/visitor/JassTypeGettingValueVisitor.java @@ -0,0 +1,55 @@ +package com.etheller.interpreter.ast.value.visitor; + +import com.etheller.interpreter.ast.value.ArrayJassValue; +import com.etheller.interpreter.ast.value.BooleanJassValue; +import com.etheller.interpreter.ast.value.CodeJassValue; +import com.etheller.interpreter.ast.value.HandleJassValue; +import com.etheller.interpreter.ast.value.IntegerJassValue; +import com.etheller.interpreter.ast.value.JassType; +import com.etheller.interpreter.ast.value.JassValueVisitor; +import com.etheller.interpreter.ast.value.RealJassValue; +import com.etheller.interpreter.ast.value.StringJassValue; + +public class JassTypeGettingValueVisitor implements JassValueVisitor { + public static JassTypeGettingValueVisitor INSTANCE = new JassTypeGettingValueVisitor(); + + public static JassTypeGettingValueVisitor getInstance() { + return INSTANCE; + } + + @Override + public JassType accept(final IntegerJassValue value) { + return JassType.INTEGER; + } + + @Override + public JassType accept(final RealJassValue value) { + return JassType.REAL; + } + + @Override + public JassType accept(final BooleanJassValue value) { + return JassType.BOOLEAN; + } + + @Override + public JassType accept(final StringJassValue value) { + return JassType.STRING; + } + + @Override + public JassType accept(final CodeJassValue value) { + return JassType.CODE; + } + + @Override + public JassType accept(final ArrayJassValue value) { + return value.getType(); + } + + @Override + public JassType accept(final HandleJassValue value) { + return value.getType(); + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/visitor/NotJassValueVisitor.java b/jassparser/src/com/etheller/interpreter/ast/value/visitor/NotJassValueVisitor.java new file mode 100644 index 0000000..f0d9f47 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/visitor/NotJassValueVisitor.java @@ -0,0 +1,56 @@ +package com.etheller.interpreter.ast.value.visitor; + +import com.etheller.interpreter.ast.value.ArrayJassValue; +import com.etheller.interpreter.ast.value.BooleanJassValue; +import com.etheller.interpreter.ast.value.CodeJassValue; +import com.etheller.interpreter.ast.value.HandleJassValue; +import com.etheller.interpreter.ast.value.IntegerJassValue; +import com.etheller.interpreter.ast.value.JassValue; +import com.etheller.interpreter.ast.value.JassValueVisitor; +import com.etheller.interpreter.ast.value.RealJassValue; +import com.etheller.interpreter.ast.value.StringJassValue; + +public class NotJassValueVisitor implements JassValueVisitor { + private static final NotJassValueVisitor INSTANCE = new NotJassValueVisitor(); + + public static NotJassValueVisitor getInstance() { + return INSTANCE; + } + + @Override + public JassValue accept(final IntegerJassValue value) { + throw new IllegalStateException("Unable to apply not keyword to a variable of type integer"); + } + + @Override + public JassValue accept(final RealJassValue value) { + throw new IllegalStateException("Unable to apply not keyword to a variable of type real"); + } + + @Override + public JassValue accept(final BooleanJassValue value) { + return BooleanJassValue.inverse(value); + } + + @Override + public JassValue accept(final StringJassValue value) { + throw new IllegalStateException("Unable to apply not keyword to a variable of type string"); + } + + @Override + public JassValue accept(final CodeJassValue value) { + throw new IllegalStateException("Unable to apply not keyword to a variable of type code"); + } + + @Override + public JassValue accept(final ArrayJassValue value) { + throw new IllegalStateException("Unable to apply not keyword to a variable of an array type"); + } + + @Override + public JassValue accept(final HandleJassValue value) { + throw new IllegalStateException( + "Unable to apply not keyword to a variable of type " + value.getType().getName()); + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/visitor/ObjectJassValueVisitor.java b/jassparser/src/com/etheller/interpreter/ast/value/visitor/ObjectJassValueVisitor.java new file mode 100644 index 0000000..d8da310 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/visitor/ObjectJassValueVisitor.java @@ -0,0 +1,54 @@ +package com.etheller.interpreter.ast.value.visitor; + +import com.etheller.interpreter.ast.value.ArrayJassValue; +import com.etheller.interpreter.ast.value.BooleanJassValue; +import com.etheller.interpreter.ast.value.CodeJassValue; +import com.etheller.interpreter.ast.value.HandleJassValue; +import com.etheller.interpreter.ast.value.IntegerJassValue; +import com.etheller.interpreter.ast.value.JassValueVisitor; +import com.etheller.interpreter.ast.value.RealJassValue; +import com.etheller.interpreter.ast.value.StringJassValue; + +public class ObjectJassValueVisitor implements JassValueVisitor { + private static final ObjectJassValueVisitor INSTANCE = new ObjectJassValueVisitor(); + + public static ObjectJassValueVisitor getInstance() { + return (ObjectJassValueVisitor) INSTANCE; + } + + @Override + public T accept(final IntegerJassValue value) { + return null; + } + + @Override + public T accept(final RealJassValue value) { + return null; + } + + @Override + public T accept(final BooleanJassValue value) { + return null; + } + + @Override + public T accept(final StringJassValue value) { + return null; + } + + @Override + public T accept(final CodeJassValue value) { + return null; + } + + @Override + public T accept(final ArrayJassValue value) { + return null; + } + + @Override + public T accept(final HandleJassValue value) { + return (T) value.getJavaValue(); + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/visitor/RealJassValueVisitor.java b/jassparser/src/com/etheller/interpreter/ast/value/visitor/RealJassValueVisitor.java new file mode 100644 index 0000000..f404581 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/visitor/RealJassValueVisitor.java @@ -0,0 +1,54 @@ +package com.etheller.interpreter.ast.value.visitor; + +import com.etheller.interpreter.ast.value.ArrayJassValue; +import com.etheller.interpreter.ast.value.BooleanJassValue; +import com.etheller.interpreter.ast.value.CodeJassValue; +import com.etheller.interpreter.ast.value.HandleJassValue; +import com.etheller.interpreter.ast.value.IntegerJassValue; +import com.etheller.interpreter.ast.value.JassValueVisitor; +import com.etheller.interpreter.ast.value.RealJassValue; +import com.etheller.interpreter.ast.value.StringJassValue; + +public class RealJassValueVisitor implements JassValueVisitor { + private static final RealJassValueVisitor INSTANCE = new RealJassValueVisitor(); + + public static RealJassValueVisitor getInstance() { + return INSTANCE; + } + + @Override + public Double accept(final IntegerJassValue value) { + return Double.valueOf(value.getValue()); + } + + @Override + public Double accept(final RealJassValue value) { + return value.getValue(); + } + + @Override + public Double accept(final BooleanJassValue value) { + return 0.0; + } + + @Override + public Double accept(final StringJassValue value) { + return 0.0; + } + + @Override + public Double accept(final CodeJassValue value) { + return 0.0; + } + + @Override + public Double accept(final ArrayJassValue value) { + return 0.0; + } + + @Override + public Double accept(final HandleJassValue value) { + return 0.0; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/visitor/StringJassValueVisitor.java b/jassparser/src/com/etheller/interpreter/ast/value/visitor/StringJassValueVisitor.java new file mode 100644 index 0000000..f9ef72f --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/visitor/StringJassValueVisitor.java @@ -0,0 +1,54 @@ +package com.etheller.interpreter.ast.value.visitor; + +import com.etheller.interpreter.ast.value.ArrayJassValue; +import com.etheller.interpreter.ast.value.BooleanJassValue; +import com.etheller.interpreter.ast.value.CodeJassValue; +import com.etheller.interpreter.ast.value.HandleJassValue; +import com.etheller.interpreter.ast.value.IntegerJassValue; +import com.etheller.interpreter.ast.value.JassValueVisitor; +import com.etheller.interpreter.ast.value.RealJassValue; +import com.etheller.interpreter.ast.value.StringJassValue; + +public class StringJassValueVisitor implements JassValueVisitor { + private static final StringJassValueVisitor INSTANCE = new StringJassValueVisitor(); + + public static StringJassValueVisitor getInstance() { + return INSTANCE; + } + + @Override + public String accept(final IntegerJassValue value) { + return null; + } + + @Override + public String accept(final RealJassValue value) { + return null; + } + + @Override + public String accept(final BooleanJassValue value) { + return null; + } + + @Override + public String accept(final StringJassValue value) { + return value.getValue(); + } + + @Override + public String accept(final CodeJassValue value) { + return null; + } + + @Override + public String accept(final ArrayJassValue value) { + return null; + } + + @Override + public String accept(final HandleJassValue value) { + return null; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/value/visitor/SuperTypeVisitor.java b/jassparser/src/com/etheller/interpreter/ast/value/visitor/SuperTypeVisitor.java new file mode 100644 index 0000000..2a9e336 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/value/visitor/SuperTypeVisitor.java @@ -0,0 +1,30 @@ +package com.etheller.interpreter.ast.value.visitor; + +import com.etheller.interpreter.ast.value.ArrayJassType; +import com.etheller.interpreter.ast.value.HandleJassType; +import com.etheller.interpreter.ast.value.JassTypeVisitor; +import com.etheller.interpreter.ast.value.PrimitiveJassType; + +public class SuperTypeVisitor implements JassTypeVisitor { + private static final SuperTypeVisitor INSTANCE = new SuperTypeVisitor(); + + public static SuperTypeVisitor getInstance() { + return INSTANCE; + } + + @Override + public HandleJassType accept(final PrimitiveJassType primitiveType) { + return null; + } + + @Override + public HandleJassType accept(final ArrayJassType arrayType) { + return null; + } + + @Override + public HandleJassType accept(final HandleJassType type) { + return type.getSuperType(); + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/visitors/ArgumentExpressionHandler.java b/jassparser/src/com/etheller/interpreter/ast/visitors/ArgumentExpressionHandler.java new file mode 100644 index 0000000..8ec596e --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/visitors/ArgumentExpressionHandler.java @@ -0,0 +1,15 @@ +package com.etheller.interpreter.ast.visitors; + +public class ArgumentExpressionHandler { + protected JassArgumentsVisitor argumentsVisitor; + protected JassExpressionVisitor expressionVisitor; + + public void setJassArgumentsVisitor(final JassArgumentsVisitor jassArgumentsVisitor) { + this.argumentsVisitor = jassArgumentsVisitor; + } + + public void setJassExpressionVisitor(final JassExpressionVisitor jassExpressionVisitor) { + this.expressionVisitor = jassExpressionVisitor; + } + +} diff --git a/jassparser/src/com/etheller/interpreter/ast/visitors/JassArgumentsVisitor.java b/jassparser/src/com/etheller/interpreter/ast/visitors/JassArgumentsVisitor.java new file mode 100644 index 0000000..cd44e48 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/visitors/JassArgumentsVisitor.java @@ -0,0 +1,31 @@ +package com.etheller.interpreter.ast.visitors; + +import java.util.LinkedList; +import java.util.List; + +import com.etheller.interpreter.JassBaseVisitor; +import com.etheller.interpreter.JassParser.ListArgumentContext; +import com.etheller.interpreter.JassParser.SingleArgumentContext; +import com.etheller.interpreter.ast.expression.JassExpression; + +public class JassArgumentsVisitor extends JassBaseVisitor> { + private final ArgumentExpressionHandler argumentExpressionHandler; + + public JassArgumentsVisitor(final ArgumentExpressionHandler argumentExpressionHandler) { + this.argumentExpressionHandler = argumentExpressionHandler; + } + + @Override + public List visitSingleArgument(final SingleArgumentContext ctx) { + final List list = new LinkedList<>(); + list.add(this.argumentExpressionHandler.expressionVisitor.visit(ctx.expression())); + return list; + } + + @Override + public List visitListArgument(final ListArgumentContext ctx) { + final List list = visit(ctx.argsList()); + list.add(0, this.argumentExpressionHandler.expressionVisitor.visit(ctx.expression())); + return list; + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/visitors/JassExpressionVisitor.java b/jassparser/src/com/etheller/interpreter/ast/visitors/JassExpressionVisitor.java new file mode 100644 index 0000000..cb5e077 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/visitors/JassExpressionVisitor.java @@ -0,0 +1,91 @@ +package com.etheller.interpreter.ast.visitors; + +import java.util.Collections; +import java.util.List; + +import com.etheller.interpreter.JassBaseVisitor; +import com.etheller.interpreter.JassParser.ArgsListContext; +import com.etheller.interpreter.JassParser.ArrayReferenceExpressionContext; +import com.etheller.interpreter.JassParser.FalseExpressionContext; +import com.etheller.interpreter.JassParser.FunctionCallExpressionContext; +import com.etheller.interpreter.JassParser.FunctionReferenceExpressionContext; +import com.etheller.interpreter.JassParser.IntegerLiteralExpressionContext; +import com.etheller.interpreter.JassParser.NotExpressionContext; +import com.etheller.interpreter.JassParser.ParentheticalExpressionContext; +import com.etheller.interpreter.JassParser.ReferenceExpressionContext; +import com.etheller.interpreter.JassParser.StringLiteralExpressionContext; +import com.etheller.interpreter.JassParser.TrueExpressionContext; +import com.etheller.interpreter.ast.expression.ArrayRefJassExpression; +import com.etheller.interpreter.ast.expression.FunctionCallJassExpression; +import com.etheller.interpreter.ast.expression.FunctionReferenceJassExpression; +import com.etheller.interpreter.ast.expression.JassExpression; +import com.etheller.interpreter.ast.expression.LiteralJassExpression; +import com.etheller.interpreter.ast.expression.NotJassExpression; +import com.etheller.interpreter.ast.expression.ReferenceJassExpression; +import com.etheller.interpreter.ast.value.BooleanJassValue; +import com.etheller.interpreter.ast.value.IntegerJassValue; +import com.etheller.interpreter.ast.value.StringJassValue; + +public class JassExpressionVisitor extends JassBaseVisitor { + private final ArgumentExpressionHandler argumentExpressionHandler; + + public JassExpressionVisitor(final ArgumentExpressionHandler argumentExpressionHandler) { + this.argumentExpressionHandler = argumentExpressionHandler; + } + + @Override + public JassExpression visitReferenceExpression(final ReferenceExpressionContext ctx) { + return new ReferenceJassExpression(ctx.ID().getText()); + } + + @Override + public JassExpression visitParentheticalExpression(final ParentheticalExpressionContext ctx) { + return visit(ctx.expression()); + } + + @Override + public JassExpression visitStringLiteralExpression(final StringLiteralExpressionContext ctx) { + final String stringLiteralText = ctx.STRING_LITERAL().getText(); + final String parsedString = stringLiteralText.substring(1, stringLiteralText.length() - 1).replace("\\\\", + "\\"); + return new LiteralJassExpression(new StringJassValue(parsedString)); + } + + @Override + public JassExpression visitIntegerLiteralExpression(final IntegerLiteralExpressionContext ctx) { + return new LiteralJassExpression(new IntegerJassValue(Integer.parseInt(ctx.INTEGER().getText()))); + } + + @Override + public JassExpression visitFunctionReferenceExpression(final FunctionReferenceExpressionContext ctx) { + return new FunctionReferenceJassExpression(ctx.ID().getText()); + } + + @Override + public JassExpression visitArrayReferenceExpression(final ArrayReferenceExpressionContext ctx) { + return new ArrayRefJassExpression(ctx.ID().getText(), visit(ctx.expression())); + } + + @Override + public JassExpression visitFalseExpression(final FalseExpressionContext ctx) { + return new LiteralJassExpression(BooleanJassValue.FALSE); + } + + @Override + public JassExpression visitTrueExpression(final TrueExpressionContext ctx) { + return new LiteralJassExpression(BooleanJassValue.TRUE); + } + + @Override + public JassExpression visitNotExpression(final NotExpressionContext ctx) { + return new NotJassExpression(visit(ctx.expression())); + } + + @Override + public JassExpression visitFunctionCallExpression(final FunctionCallExpressionContext ctx) { + final ArgsListContext argsList = ctx.functionExpression().argsList(); + final List arguments = argsList == null ? Collections.emptyList() + : this.argumentExpressionHandler.argumentsVisitor.visit(argsList); + return new FunctionCallJassExpression(ctx.functionExpression().ID().getText(), arguments); + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/visitors/JassGlobalsVisitor.java b/jassparser/src/com/etheller/interpreter/ast/visitors/JassGlobalsVisitor.java new file mode 100644 index 0000000..f5fcfc2 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/visitors/JassGlobalsVisitor.java @@ -0,0 +1,51 @@ +package com.etheller.interpreter.ast.visitors; + +import com.etheller.interpreter.JassBaseVisitor; +import com.etheller.interpreter.JassParser.BasicGlobalContext; +import com.etheller.interpreter.JassParser.DefinitionGlobalContext; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.LocalScope; +import com.etheller.interpreter.ast.value.JassType; +import com.etheller.interpreter.ast.value.visitor.ArrayPrimitiveTypeVisitor; + +public class JassGlobalsVisitor extends JassBaseVisitor { + private static final LocalScope EMPTY_LOCAL_SCOPE = new LocalScope(); + private final GlobalScope globals; + private final JassTypeVisitor jassTypeVisitor; + private final JassExpressionVisitor jassExpressionVisitor; + + public JassGlobalsVisitor(final GlobalScope globals, final JassTypeVisitor jassTypeVisitor, + final JassExpressionVisitor jassExpressionVisitor) { + this.globals = globals; + this.jassTypeVisitor = jassTypeVisitor; + this.jassExpressionVisitor = jassExpressionVisitor; + } + + @Override + public Void visitBasicGlobal(final BasicGlobalContext ctx) { + final JassType type = this.jassTypeVisitor.visit(ctx.type()); + final JassType arrayPrimType = type.visit(ArrayPrimitiveTypeVisitor.getInstance()); + if (arrayPrimType != null) { + this.globals.createGlobalArray(ctx.ID().getText(), type); + } + else { + this.globals.createGlobal(ctx.ID().getText(), type); + } + return null; + } + + @Override + public Void visitDefinitionGlobal(final DefinitionGlobalContext ctx) { + final JassType type = this.jassTypeVisitor.visit(ctx.type()); + final JassType arrayPrimType = type.visit(ArrayPrimitiveTypeVisitor.getInstance()); + if (arrayPrimType != null) { + this.globals.createGlobalArray(ctx.ID().getText(), type); + } + else { + this.globals.createGlobal(ctx.ID().getText(), type, + this.jassExpressionVisitor.visit(ctx.assignTail().expression()).evaluate(this.globals, + EMPTY_LOCAL_SCOPE, JassProgramVisitor.EMPTY_TRIGGER_SCOPE)); + } + return null; + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/visitors/JassParametersVisitor.java b/jassparser/src/com/etheller/interpreter/ast/visitors/JassParametersVisitor.java new file mode 100644 index 0000000..89cde26 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/visitors/JassParametersVisitor.java @@ -0,0 +1,38 @@ +package com.etheller.interpreter.ast.visitors; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import com.etheller.interpreter.JassBaseVisitor; +import com.etheller.interpreter.JassParser.ListParameterContext; +import com.etheller.interpreter.JassParser.NothingParameterContext; +import com.etheller.interpreter.JassParser.SingleParameterContext; +import com.etheller.interpreter.ast.function.JassParameter; + +public class JassParametersVisitor extends JassBaseVisitor> { + private final JassTypeVisitor typeVisitor; + + public JassParametersVisitor(final JassTypeVisitor typeVisitor) { + this.typeVisitor = typeVisitor; + } + + @Override + public List visitSingleParameter(final SingleParameterContext ctx) { + final List list = new LinkedList<>(); + list.add(new JassParameter(typeVisitor.visit(ctx.param().type()), ctx.param().ID().getText())); + return list; + } + + @Override + public List visitListParameter(final ListParameterContext ctx) { + final List list = visit(ctx.paramList()); + list.add(0, new JassParameter(typeVisitor.visit(ctx.param().type()), ctx.param().ID().getText())); + return list; + } + + @Override + public List visitNothingParameter(final NothingParameterContext ctx) { + return Collections.EMPTY_LIST; + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/visitors/JassProgramVisitor.java b/jassparser/src/com/etheller/interpreter/ast/visitors/JassProgramVisitor.java new file mode 100644 index 0000000..2c5e0d2 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/visitors/JassProgramVisitor.java @@ -0,0 +1,106 @@ +package com.etheller.interpreter.ast.visitors; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.etheller.interpreter.JassBaseVisitor; +import com.etheller.interpreter.JassParser.BlockContext; +import com.etheller.interpreter.JassParser.FunctionBlockContext; +import com.etheller.interpreter.JassParser.GlobalContext; +import com.etheller.interpreter.JassParser.ProgramContext; +import com.etheller.interpreter.JassParser.StatementContext; +import com.etheller.interpreter.JassParser.TypeDefinitionContext; +import com.etheller.interpreter.ast.function.JassFunction; +import com.etheller.interpreter.ast.function.JassNativeManager; +import com.etheller.interpreter.ast.function.UserJassFunction; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.scope.TriggerExecutionScope; +import com.etheller.interpreter.ast.statement.JassStatement; + +public class JassProgramVisitor extends JassBaseVisitor { + public static final TriggerExecutionScope EMPTY_TRIGGER_SCOPE = new TriggerExecutionScope(null); + private final GlobalScope globals = new GlobalScope(); + private final JassNativeManager jassNativeManager = new JassNativeManager(); + private final JassTypeVisitor jassTypeVisitor = new JassTypeVisitor(this.globals); + private final ArgumentExpressionHandler argumentExpressionHandler = new ArgumentExpressionHandler(); + private final JassExpressionVisitor jassExpressionVisitor = new JassExpressionVisitor( + this.argumentExpressionHandler); + private final JassArgumentsVisitor jassArgumentsVisitor = new JassArgumentsVisitor(this.argumentExpressionHandler); + { + this.argumentExpressionHandler.setJassArgumentsVisitor(this.jassArgumentsVisitor); + this.argumentExpressionHandler.setJassExpressionVisitor(this.jassExpressionVisitor); + } + private final JassGlobalsVisitor jassGlobalsVisitor = new JassGlobalsVisitor(this.globals, this.jassTypeVisitor, + this.jassExpressionVisitor); + private final JassParametersVisitor jassParametersVisitor = new JassParametersVisitor(this.jassTypeVisitor); + private final JassStatementVisitor jassStatementVisitor = new JassStatementVisitor(this.argumentExpressionHandler); + + @Override + public Void visitBlock(final BlockContext ctx) { + if (ctx.globalsBlock() != null) { + for (final GlobalContext globalContext : ctx.globalsBlock().global()) { + this.jassGlobalsVisitor.visit(globalContext); + } + } + else if (ctx.nativeBlock() != null) { + final String text = ctx.nativeBlock().ID().getText(); + System.out.println("Registering native: " + text); + this.jassNativeManager.registerNativeCode(text, + this.jassParametersVisitor.visit(ctx.nativeBlock().paramList()), + this.jassTypeVisitor.visit(ctx.nativeBlock().type()), this.globals); + } + return null; + } + + @Override + public Void visitFunctionBlock(final FunctionBlockContext ctx) { + final List statements = new ArrayList<>(); + for (final StatementContext statementContext : ctx.statements().statement()) { + statements.add(this.jassStatementVisitor.visit(statementContext)); + } + final UserJassFunction userJassFunction = new UserJassFunction(statements, + this.jassParametersVisitor.visit(ctx.paramList()), this.jassTypeVisitor.visit(ctx.type())); + this.globals.defineFunction(ctx.ID().getText(), userJassFunction); + return null; + } + + @Override + public Void visitProgram(final ProgramContext ctx) { + for (final TypeDefinitionContext typeDefinitionContext : ctx.typeDefinitionBlock().typeDefinition()) { + this.globals.loadTypeDefinition(typeDefinitionContext.ID(0).getText(), + typeDefinitionContext.ID(1).getText()); + } + for (final BlockContext blockContext : ctx.block()) { + visit(blockContext); + } + for (final FunctionBlockContext functionBlockContext : ctx.functionBlock()) { + final List statements = new ArrayList<>(); + for (final StatementContext statementContext : functionBlockContext.statements().statement()) { + statements.add(this.jassStatementVisitor.visit(statementContext)); + } + final UserJassFunction userJassFunction = new UserJassFunction(statements, + this.jassParametersVisitor.visit(functionBlockContext.paramList()), + this.jassTypeVisitor.visit(functionBlockContext.type())); + this.globals.defineFunction(functionBlockContext.ID().getText(), userJassFunction); + } + final JassFunction mainFunction = this.globals.getFunctionByName("main"); + if (mainFunction != null) { + try { + mainFunction.call(Collections.EMPTY_LIST, this.globals, EMPTY_TRIGGER_SCOPE); + } + catch (final Exception exc) { + throw new RuntimeException("Exception on Line " + GlobalScope.getLineNumber(), exc); + } + } + return null; + } + + public GlobalScope getGlobals() { + return this.globals; + } + + public JassNativeManager getJassNativeManager() { + return this.jassNativeManager; + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/visitors/JassStatementVisitor.java b/jassparser/src/com/etheller/interpreter/ast/visitors/JassStatementVisitor.java new file mode 100644 index 0000000..14ac829 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/visitors/JassStatementVisitor.java @@ -0,0 +1,91 @@ +package com.etheller.interpreter.ast.visitors; + +import java.util.ArrayList; +import java.util.List; + +import com.etheller.interpreter.JassBaseVisitor; +import com.etheller.interpreter.JassParser.ArrayedAssignmentStatementContext; +import com.etheller.interpreter.JassParser.CallStatementContext; +import com.etheller.interpreter.JassParser.IfElseIfStatementContext; +import com.etheller.interpreter.JassParser.IfElseStatementContext; +import com.etheller.interpreter.JassParser.ReturnStatementContext; +import com.etheller.interpreter.JassParser.SetStatementContext; +import com.etheller.interpreter.JassParser.SimpleIfStatementContext; +import com.etheller.interpreter.JassParser.StatementContext; +import com.etheller.interpreter.ast.statement.JassArrayedAssignmentStatement; +import com.etheller.interpreter.ast.statement.JassCallStatement; +import com.etheller.interpreter.ast.statement.JassIfElseIfStatement; +import com.etheller.interpreter.ast.statement.JassIfElseStatement; +import com.etheller.interpreter.ast.statement.JassIfStatement; +import com.etheller.interpreter.ast.statement.JassReturnStatement; +import com.etheller.interpreter.ast.statement.JassSetStatement; +import com.etheller.interpreter.ast.statement.JassStatement; + +public class JassStatementVisitor extends JassBaseVisitor { + private final ArgumentExpressionHandler argumentExpressionHandler; + + public JassStatementVisitor(final ArgumentExpressionHandler argumentExpressionHandler) { + this.argumentExpressionHandler = argumentExpressionHandler; + } + + @Override + public JassStatement visitCallStatement(final CallStatementContext ctx) { + return new JassCallStatement(ctx.getStart().getLine(), ctx.functionExpression().ID().getText(), + this.argumentExpressionHandler.argumentsVisitor.visit(ctx.functionExpression().argsList())); + } + + @Override + public JassStatement visitSetStatement(final SetStatementContext ctx) { + return new JassSetStatement(ctx.getStart().getLine(), ctx.ID().getText(), + this.argumentExpressionHandler.expressionVisitor.visit(ctx.expression())); + } + + @Override + public JassStatement visitReturnStatement(final ReturnStatementContext ctx) { + return new JassReturnStatement(ctx.getStart().getLine(), + this.argumentExpressionHandler.expressionVisitor.visit(ctx.expression())); + } + + @Override + public JassStatement visitIfElseIfStatement(final IfElseIfStatementContext ctx) { + final List thenStatements = new ArrayList<>(); + for (final StatementContext statementCtx : ctx.statements().statement()) { + thenStatements.add(visit(statementCtx)); + } + final JassStatement elseIfTail = visit(ctx.ifStatementPartial()); + return new JassIfElseIfStatement(ctx.getStart().getLine(), + this.argumentExpressionHandler.expressionVisitor.visit(ctx.expression()), thenStatements, elseIfTail); + } + + @Override + public JassStatement visitIfElseStatement(final IfElseStatementContext ctx) { + final List thenStatements = new ArrayList<>(); + for (final StatementContext statementCtx : ctx.statements(0).statement()) { + thenStatements.add(visit(statementCtx)); + } + final List elseStatements = new ArrayList<>(); + for (final StatementContext statementCtx : ctx.statements(1).statement()) { + elseStatements.add(visit(statementCtx)); + } + return new JassIfElseStatement(ctx.getStart().getLine(), + this.argumentExpressionHandler.expressionVisitor.visit(ctx.expression()), thenStatements, + elseStatements); + } + + @Override + public JassStatement visitSimpleIfStatement(final SimpleIfStatementContext ctx) { + final List thenStatements = new ArrayList<>(); + for (final StatementContext statementCtx : ctx.statements().statement()) { + thenStatements.add(visit(statementCtx)); + } + return new JassIfStatement(ctx.getStart().getLine(), + this.argumentExpressionHandler.expressionVisitor.visit(ctx.expression()), thenStatements); + } + + @Override + public JassStatement visitArrayedAssignmentStatement(final ArrayedAssignmentStatementContext ctx) { + return new JassArrayedAssignmentStatement(ctx.getStart().getLine(), ctx.ID().getText(), + this.argumentExpressionHandler.expressionVisitor.visit(ctx.expression(0)), + this.argumentExpressionHandler.expressionVisitor.visit(ctx.expression(1))); + } +} diff --git a/jassparser/src/com/etheller/interpreter/ast/visitors/JassTypeVisitor.java b/jassparser/src/com/etheller/interpreter/ast/visitors/JassTypeVisitor.java new file mode 100644 index 0000000..d4530b0 --- /dev/null +++ b/jassparser/src/com/etheller/interpreter/ast/visitors/JassTypeVisitor.java @@ -0,0 +1,31 @@ +package com.etheller.interpreter.ast.visitors; + +import com.etheller.interpreter.JassBaseVisitor; +import com.etheller.interpreter.JassParser.ArrayTypeContext; +import com.etheller.interpreter.JassParser.BasicTypeContext; +import com.etheller.interpreter.JassParser.NothingTypeContext; +import com.etheller.interpreter.ast.scope.GlobalScope; +import com.etheller.interpreter.ast.value.JassType; + +public class JassTypeVisitor extends JassBaseVisitor { + private final GlobalScope globals; + + public JassTypeVisitor(final GlobalScope globals) { + this.globals = globals; + } + + @Override + public JassType visitArrayType(final ArrayTypeContext ctx) { + return globals.parseArrayType(ctx.ID().getText()); + } + + @Override + public JassType visitBasicType(final BasicTypeContext ctx) { + return globals.parseType(ctx.ID().getText()); + } + + @Override + public JassType visitNothingType(final NothingTypeContext ctx) { + return JassType.NOTHING; + } +} diff --git a/resources/Scripts/common.jui b/resources/Scripts/common.jui new file mode 100644 index 0000000..34f4ec7 --- /dev/null +++ b/resources/Scripts/common.jui @@ -0,0 +1,162 @@ +//============================================================================ +// User Interface scripts for the Warsmash mod engine. This is +// an attempt to get these defined in an external import that +// a map can override. Unfortunately, although I would like for +// this to be a JASS file, it is not very consistent with the +// notion of the JASS2 VM to define UI, because handles are +// mostly network synced. So, we will assume the contents +// of this file are run in a special client-only JASS VM +// once I implement networking, so that this script will not +// cause a desync as it would if it was in the standard game +// JASS files. For that reason, it will have '.jui' extension +// to signify it runs in this modified JASS VM, instead of +// standard '.j' as used for JASS2 script files. +// +// Right now there is some duplicated code from common.j -- +// maybe later on we will load that stuff first, then this +// one in the same variable space? +// + +type framehandle extends handle +type framepointtype extends handle +type trigger extends handle +type triggeraction extends handle +type triggercondition extends handle +type boolexpr extends handle +type conditionfunc extends boolexpr + +native LogError takes string message returns nothing +constant native ConvertFramePointType takes integer i returns framepointtype + +globals + +//=================================================== +// UI API constants +//=================================================== + + constant framepointtype FRAMEPOINT_TOPLEFT = ConvertFramePointType(0) + constant framepointtype FRAMEPOINT_TOP = ConvertFramePointType(1) + constant framepointtype FRAMEPOINT_TOPRIGHT = ConvertFramePointType(2) + constant framepointtype FRAMEPOINT_LEFT = ConvertFramePointType(3) + constant framepointtype FRAMEPOINT_CENTER = ConvertFramePointType(4) + constant framepointtype FRAMEPOINT_RIGHT = ConvertFramePointType(5) + constant framepointtype FRAMEPOINT_BOTTOMLEFT = ConvertFramePointType(6) + constant framepointtype FRAMEPOINT_BOTTOM = ConvertFramePointType(7) + constant framepointtype FRAMEPOINT_BOTTOMRIGHT = ConvertFramePointType(8) +endglobals + +//=================================================== +// UI API +//=================================================== + +// Loads an entry from the file "UI\war3skins.txt" and returns +// it as the current GAMEUI. The default possible +// strings are "Human", "Orc", "NightElf", and "Undead". +// Some UI FDF templates will use this information as +// the source for lookup strings as a means to change +// their style. +// Calling this more than once will probably crash the game, +// or something, so only call it once on startup. +native CreateRootFrame takes string skinName returns framehandle + +// Loads the (T)able (O)f (C)ontents file. +// This must be a simple text document with each line +// having only a filepath of a .FDF frame template +// definition file to load. Unlike War3 engine, +// I do not require the file to include one extra +// blank line at the end. +// +// We typically call this first during UI setup, and +// only once for a given mod. +native LoadTOCFile takes string TOCFile returns framehandle + +// Spawn a SIMPLEFRAME element that was defined in a FDF +// template onto the screen. The "name" field must match +// the name of a template to spawn, loaded with LoadTOCFile. +// The create context is pretty pointless, but I think +// they use it so that they have an integer tag on the +// "Attack 1" and "Attack 2" ui components. I was trying +// to keep parity with the FDF UI APIs from 1.31, imagine +// that. +native CreateSimpleFrame takes string name, framehandle owner, integer createContext returns framehandle + +// Spawn a FRAME element that was defined in an FDF +// template onto the screen. The "name" field must +// match the name of a template to spawn, loaded with LoadTOCFile. +// As noted on CreateSimpleFrame, for now createContext is pointless. +// Likewise for priority -- until it fixed. +native CreateFrame takes string name, framehandle owner, integer priority, integer createContext returns framehandle + + +// Set the absolute point (often called Anchor) for the frame handle. +// See FDF template files for examples +native FrameSetAnchor takes framehandle frame, framepointtype point, real x, real y returns nothing + +// Tasyen said: "takes one point of a Frame unbound that point and places it to a specific coordinates on the screen." +native FrameSetAbsPoint takes framehandle frame, framepointtype point, real x, real y returns nothing + +// Set the relative point (called SetPoint in FDF templates) for the frame handle. +// See FDF template files for examples +// Tasyen said: places a point of FrameA relative to a point of FrameB. When FrameB moves FrameA's point will keep this rule and moves with it. +// Note for Project Warsmash: "When FrameB moves..." might not be true... call FramePositionBounds() for now +native FrameSetPoint takes framehandle frame, framepointtype point, framehandle relative, framepointtype relativePoint, real x, real y returns nothing + + +// Created for Warsmash engine, not a part of 1.31 UI apis, +// and at some point it might be removed. Basically +// this function will apply Anchors and SetPoints assigned +// to the frame handle and all its children and resolve where +// they should go onscreen. Generally in my experience, +// War3 will do this automatically in their FDF system. +native FramePositionBounds takes framehandle frame returns nothing + +// Used to lookup fields in the Skin data, for example +// SkinGetField("TimeOfDayIndicator") will return the +// string "UI\\Console\\Human\\HumanUI-TimeIndicator.mdl" +// when the "Human" skin was loaded with CreateRootFrame +native SkinGetField takes string field returns string + +// Sets the text value on a String Frame, currently it crashes otherwise +native FrameSetText takes framehandle frame, string text returns nothing + +// Sets the text color on a String Frame, currently it crashes otherwise +native FrameSetTextColor takes framehandle frame, integer color returns nothing + +native ConvertColor takes integer a, integer r, integer g, integer b returns integer + + +// Gets a previously created Frame using its name ( from an FDF template file that was +// previously spawned ). See previous notes about createContext. Mostly pointless? +native GetFrameByName takes string name, integer createContext returns framehandle + +//============================================================================ +// Native trigger interface +// +native CreateTrigger takes nothing returns trigger +native DestroyTrigger takes trigger whichTrigger returns nothing +native EnableTrigger takes trigger whichTrigger returns nothing +native DisableTrigger takes trigger whichTrigger returns nothing +native IsTriggerEnabled takes trigger whichTrigger returns boolean + +native TriggerAddCondition takes trigger whichTrigger, boolexpr condition returns triggercondition +native TriggerRemoveCondition takes trigger whichTrigger, triggercondition whichCondition returns nothing +native TriggerClearConditions takes trigger whichTrigger returns nothing + +native TriggerAddAction takes trigger whichTrigger, code actionFunc returns triggeraction +native TriggerRemoveAction takes trigger whichTrigger, triggeraction whichAction returns nothing +native TriggerClearActions takes trigger whichTrigger returns nothing +native TriggerEvaluate takes trigger whichTrigger returns boolean +native TriggerExecute takes trigger whichTrigger returns nothing + + +//============================================================================ +// Boolean Expr API ( for compositing trigger conditions and unit filter funcs...) +//============================================================================ +native And takes boolexpr operandA, boolexpr operandB returns boolexpr +native Or takes boolexpr operandA, boolexpr operandB returns boolexpr +native Not takes boolexpr operand returns boolexpr +native Condition takes code func returns conditionfunc +native DestroyCondition takes conditionfunc c returns nothing +native Filter takes code func returns filterfunc +native DestroyFilter takes filterfunc f returns nothing +native DestroyBoolExpr takes boolexpr e returns nothing \ No newline at end of file diff --git a/resources/Scripts/melee.jui b/resources/Scripts/melee.jui new file mode 100644 index 0000000..9d3c9a4 --- /dev/null +++ b/resources/Scripts/melee.jui @@ -0,0 +1,73 @@ + +globals +// Defaults for testing: + constant string SKIN = "NightElf" +// Major UI components + framehandle ROOT_FRAME + framehandle CONSOLE_UI + framehandle RESOURCE_BAR + framehandle RESOURCE_BAR_GOLD_TEXT + framehandle RESOURCE_BAR_LUMBER_TEXT + framehandle RESOURCE_BAR_SUPPLY_TEXT + framehandle RESOURCE_BAR_UPKEEP_TEXT + framehandle TIME_INDICATOR + framehandle SIMPLE_INFO_PANEL_UNIT_DETAIL + framehandle UNIT_PORTRAIT + framehandle UNIT_LIFE_TEXT + framehandle UNIT_MANA_TEXT +endglobals + + +function main takes nothing returns nothing + // ================================= + // Load skins and templates + // ================================= + set ROOT_FRAME = CreateRootFrame(SKIN) + if not LoadTOCFile("UI\\FrameDef\\FrameDef.toc") then + call LogError("Unable to load FrameDef.toc") + endif + if not LoadTOCFile("UI\\FrameDef\\SmashFrameDef.toc") then + call LogError("Unable to load SmashFrameDef.toc") + endif + // ================================= + // Load major UI components + // ================================= + // Console UI is the background with the racial theme + set CONSOLE_UI = CreateSimpleFrame("ConsoleUI", ROOT_FRAME, 0) + // Resource bar is a 3 part bar with Gold, Lumber, and Food. + // Its template does not specify where to put it, so we must + // put it in the "TOPRIGHT" corner. + set RESOURCE_BAR = CreateSimpleFrame("ResourceBarFrame", CONSOLE_UI, 0) + call FrameSetPoint(RESOURCE_BAR, FRAMEPOINT_TOPRIGHT, CONSOLE_UI, FRAMEPOINT_TOPRIGHT, 0, 0) + + // Create the Time Indicator (clock) + set TIME_INDICATOR = CreateFrame("TimeOfDayIndicator", ROOT_FRAME, 0, 0) + + // Create the unit portrait stuff (for now this doesn't actually create the 3D, only HP/mana) + set UNIT_PORTRAIT = CreateSimpleFrame("UnitPortrait", CONSOLE_UI, 0) + set UNIT_LIFE_TEXT = GetFrameByName("UnitPortraitHitPointText", 0) + set UNIT_MANA_TEXT = GetFrameByName("UnitPortraitManaPointText", 0) + + // Set default values + call FrameSetText(UNIT_LIFE_TEXT, "706 / 725") + call FrameSetText(UNIT_MANA_TEXT, "405 / 405") + + + // Retrieve inflated sub-frames and store references + set RESOURCE_BAR_GOLD_TEXT = GetFrameByName("ResourceBarGoldText", 0) + set RESOURCE_BAR_LUMBER_TEXT = GetFrameByName("ResourceBarLumberText", 0) + set RESOURCE_BAR_SUPPLY_TEXT = GetFrameByName("ResourceBarSupplyText", 0) + set RESOURCE_BAR_UPKEEP_TEXT = GetFrameByName("ResourceBarUpkeepText", 0) + + // Set default values + call FrameSetText(RESOURCE_BAR_GOLD_TEXT, "500") + call FrameSetText(RESOURCE_BAR_LUMBER_TEXT, "150") + call FrameSetText(RESOURCE_BAR_SUPPLY_TEXT, "5/10") + call FrameSetText(RESOURCE_BAR_UPKEEP_TEXT, "No Upkeep") + call FrameSetTextColor(RESOURCE_BAR_UPKEEP_TEXT, ConvertColor(255, 0, 255, 0)) + + // Assemble the UI and resolve the location of every component that + // has Anchors and SetPoints (maybe in future version this call + // wont be necessary!) + call FramePositionBounds(ROOT_FRAME) +endfunction \ No newline at end of file diff --git a/resources/UI/FrameDef/SmashFrameDef.toc b/resources/UI/FrameDef/SmashFrameDef.toc new file mode 100644 index 0000000..26aa133 --- /dev/null +++ b/resources/UI/FrameDef/SmashFrameDef.toc @@ -0,0 +1,4 @@ +UI\FrameDef\SmashUI\TimeOfDayIndicator.fdf +UI\FrameDef\SmashUI\UnitPortrait.fdf +UI\FrameDef\SmashUI\InventoryCover.fdf +UI\FrameDef\SmashUI\ToolTip.fdf diff --git a/resources/UI/FrameDef/SmashUI/InventoryCover.fdf b/resources/UI/FrameDef/SmashUI/InventoryCover.fdf new file mode 100644 index 0000000..9bad994 --- /dev/null +++ b/resources/UI/FrameDef/SmashUI/InventoryCover.fdf @@ -0,0 +1,14 @@ +Frame "SIMPLEFRAME" "SmashConsoleInventoryCover" { + DecorateFileNames, + SetAllPoints, + + // The top of the UI console + Texture "SmashConsoleInventoryCoverTexture" { + File "ConsoleInventoryCoverTexture", + Width 0.128, + Height 0.256, + AlphaMode "ALPHAKEY", + Anchor BOTTOMLEFT,0.472,0.0, + } + +} diff --git a/resources/UI/FrameDef/SmashUI/SmashConsoleUI.fdf b/resources/UI/FrameDef/SmashUI/SmashConsoleUI.fdf new file mode 100644 index 0000000..9ba9949 --- /dev/null +++ b/resources/UI/FrameDef/SmashUI/SmashConsoleUI.fdf @@ -0,0 +1,81 @@ +// I had to override this because the Blizzard version is missing the "ConsoleTexture01Top" names. +Frame "SIMPLEFRAME" "ConsoleUI" { + DecorateFileNames, + + // The top of the UI console + Texture "ConsoleTexture01Top" { + File "ConsoleTexture01", + Width 0.256, + Height 0.032, + TexCoord 0, 1, 0, 0.125, + AlphaMode "ALPHAKEY", + Anchor TOPLEFT,0,0, + } + Texture "ConsoleTexture02Top" { + File "ConsoleTexture02", + Width 0.087, + Height 0.032, + TexCoord 0, 0.33984375, 0, 0.125, + AlphaMode "ALPHAKEY", + Anchor TOPLEFT,0.256, 0, + } + Texture "ConsoleTexture02Top" { + File "ConsoleTexture02", + Width 0.053, + Height 0.032, + TexCoord 0.79296875, 1, 0, 0.125, + AlphaMode "ALPHAKEY", + Anchor TOPRIGHT,-0.288, 0, + } + Texture "ConsoleTexture03Top" { + File "ConsoleTexture03", + Width 0.256, + Height 0.032, + TexCoord 0, 1, 0, 0.125, + AlphaMode "ALPHAKEY", + Anchor TOPRIGHT,-0.032, 0, + } + Texture "ConsoleTexture04Top" { + File "ConsoleTexture04", + Width 0.032, + Height 0.032, + TexCoord 0, 1, 0, 0.125, + AlphaMode "ALPHAKEY", + Anchor TOPRIGHT,0,0, + } + + // The bottom of the UI console + Texture "ConsoleTexture01Bottom" { + File "ConsoleTexture01", + Width 0.256, + Height 0.176, + TexCoord 0, 1, 0.3125, 1, + AlphaMode "ALPHAKEY", + Anchor BOTTOMLEFT,0,0, + } + Texture "ConsoleTexture02Bottom" { + File "ConsoleTexture02", + Width 0.256, + Height 0.15, + TexCoord 0, 1, 0.4140625, 1, + AlphaMode "ALPHAKEY", + Anchor BOTTOMLEFT,0.256,0, + } + Texture "ConsoleTexture03Bottom" { + File "ConsoleTexture03", + Width 0.256, + Height 0.176, + TexCoord 0, 1, 0.3125, 1, + AlphaMode "ALPHAKEY", + Anchor BOTTOMRIGHT,-0.032,0.0, + } + Texture "ConsoleTexture04Bottom" { + File "ConsoleTexture04", + Width 0.032, + Height 0.176, + TexCoord 0, 1, 0.3125, 1, + AlphaMode "ALPHAKEY", + Anchor BOTTOMRIGHT,0,0, + } + +} diff --git a/resources/UI/FrameDef/SmashUI/TimeOfDayIndicator.fdf b/resources/UI/FrameDef/SmashUI/TimeOfDayIndicator.fdf new file mode 100644 index 0000000..9480e6e --- /dev/null +++ b/resources/UI/FrameDef/SmashUI/TimeOfDayIndicator.fdf @@ -0,0 +1,6 @@ + +Frame "SPRITE" "TimeOfDayIndicator" { + DecorateFileNames, + BackgroundArt "TimeOfDayIndicator", + SetPoint BOTTOMLEFT,"ConsoleUI",BOTTOMLEFT,0,0, +} \ No newline at end of file diff --git a/resources/UI/FrameDef/SmashUI/ToolTip.fdf b/resources/UI/FrameDef/SmashUI/ToolTip.fdf new file mode 100644 index 0000000..bb30920 --- /dev/null +++ b/resources/UI/FrameDef/SmashUI/ToolTip.fdf @@ -0,0 +1,67 @@ +/* + * ToolTip.fdf + * --------------------- + * These are some definitions to help us externalize the art of the tooltip. + * We want to use the following: + + ToolTipBackground=UI\Widgets\ToolTips\Human\human-tooltip-background.blp + ToolTipBorder=UI\Widgets\ToolTips\Human\human-tooltip-border.blp + ToolTipGoldIcon=UI\Widgets\ToolTips\Human\ToolTipGoldIcon.blp + ToolTipLumberIcon=UI\Widgets\ToolTips\Human\ToolTipLumberIcon.blp + ToolTipStonesIcon=UI\Widgets\ToolTips\Human\ToolTipStonesIcon.blp + ToolTipManaIcon=UI\Widgets\ToolTips\Human\ToolTipManaIcon.blp + ToolTipSupplyIcon=UI\Widgets\ToolTips\Human\ToolTipSupplyIcon.blp + */ + +Frame "FRAME" "SmashToolTip" { + Frame "BACKDROP" "SmashToolTipBackdrop" { + SetAllPoints, + DecorateFileNames, + BackdropTileBackground, + BackdropBackground "ToolTipBackground", + BackdropCornerFlags "UL|UR|BL|BR|T|L|B|R", + BackdropCornerSize 0.008, + BackdropBackgroundSize 0.036, + BackdropBackgroundInsets 0.0025 0.0025 0.0025 0.0025, + BackdropEdgeFile "ToolTipBorder", + BackdropBlendAll, + } + Frame "TEXT" "SmashToolTipText" { + DecorateFileNames, + FrameFont "MasterFont", 0.010, "", + FontJustificationH JUSTIFYLEFT, + FontJustificationV JUSTIFYTOP, + FontFlags "FIXEDSIZE", + FontColor 1.0 1.0 1.0 1.0, + } + Frame "TEXT" "SmashUberTipText" { + DecorateFileNames, + FrameFont "MasterFont", 0.010, "", + FontJustificationH JUSTIFYLEFT, + FontJustificationV JUSTIFYTOP, + FontFlags "FIXEDSIZE", + FontColor 1.0 1.0 1.0 1.0, + } +} +Frame "SIMPLEFRAME" "SmashToolTipIconResource" { + DecorateFileNames, + Height 0.010, + + // --- icon ------------------------------------------------------------- + Texture "SmashToolTipIconResourceBackdrop" { + Anchor LEFT, 0.0, 0.0 + Width 0.008, + Height 0.008, + File "ToolTipStonesIcon", + } + + // --- label ------------------------------------------------------------ + String "SmashToolTipIconResourceLabel" { + SetPoint LEFT, "SmashToolTipIconResourceBackdrop", RIGHT, 0.001, 0.000, + FontJustificationH JUSTIFYLEFT, + FontJustificationV JUSTIFYMIDDLE, + FontColor 0.99 0.827 0.0705 1.0, + Font "InfoPanelTextFont",0.0085, + Text "275", + } +} \ No newline at end of file diff --git a/resources/UI/FrameDef/SmashUI/UnitPortrait.fdf b/resources/UI/FrameDef/SmashUI/UnitPortrait.fdf new file mode 100644 index 0000000..eb3aa55 --- /dev/null +++ b/resources/UI/FrameDef/SmashUI/UnitPortrait.fdf @@ -0,0 +1,44 @@ +/* + * UnitPortrait.fdf + * --------------------- + * Right now the actual 3d portrait is hardcoded like the + * original game, eventually that should be a config file + * like this so that a map can override it. + */ + +String "UnitPortraitTextTemplate" { + Font "MasterFont",0.011, + Height 0.01640625, + TextLength 20, +} + +Frame "SIMPLEFRAME" "UnitPortrait" { + DecorateFileNames, + SetPoint BOTTOMLEFT,"ConsoleUI",BOTTOMLEFT,0.211,0, + Width 0.0835, + Height 0.114, + + Frame "SIMPLEFRAME" "UnitPortraitModel" { + DecorateFileNames, + SetPoint BOTTOM,"UnitPortrait",BOTTOM,0,0.0285, + Width 0.0835, + Height 0.085, + + //Texture { + //File "IdlePeon", + // AlphaMode "ALPHAKEY", + //} + } + + String "UnitPortraitHitPointText" INHERITS "UnitPortraitTextTemplate" { + Anchor BOTTOM, 0, 0.014, + FontJustificationH JUSTIFYCENTER, + FontColor 0.0 1.0 0.0 1.0, + } + + String "UnitPortraitManaPointText" INHERITS "UnitPortraitTextTemplate" { + Anchor BOTTOM, 0, -0.0005, + FontJustificationH JUSTIFYCENTER, + FontColor 1.0 1.0 1.0 1.0, + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..5d318b0 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include 'desktop', 'core', 'fdfparser', 'jassparser' \ No newline at end of file