From f0da595d203bd7dd178bc6583c1cd7ade3563822 Mon Sep 17 00:00:00 2001 From: Retera Date: Sun, 8 Sep 2019 16:39:20 -0500 Subject: [PATCH] Upgrade code --- .gitignore | 128 ++++ build.gradle | 5 + core/build.gradle | 2 +- .../etheller/warsmash/WarsmashGdxGame.java | 45 +- .../warsmash/parsers/mdlx/AnimatedObject.java | 21 +- .../warsmash/parsers/mdlx/AnimationMap.java | 65 +- .../warsmash/parsers/mdlx/Attachment.java | 99 +++ .../etheller/warsmash/parsers/mdlx/Bone.java | 88 +++ .../warsmash/parsers/mdlx/Camera.java | 128 ++++ .../warsmash/parsers/mdlx/CollisionShape.java | 167 +++++ .../warsmash/parsers/mdlx/EventObject.java | 77 +++ .../warsmash/parsers/mdlx/Extent.java | 47 ++ .../warsmash/parsers/mdlx/GenericObject.java | 263 ++++++- .../warsmash/parsers/mdlx/Geoset.java | 319 +++++++++ .../parsers/mdlx/GeosetAnimation.java | 102 +++ .../warsmash/parsers/mdlx/Helper.java | 29 + .../etheller/warsmash/parsers/mdlx/Layer.java | 195 ++++++ .../etheller/warsmash/parsers/mdlx/Light.java | 166 +++++ .../warsmash/parsers/mdlx/Material.java | 121 ++++ .../parsers/mdlx/MdlTokenInputStream.java | 22 +- .../parsers/mdlx/MdlTokenOutputStream.java | 38 ++ .../warsmash/parsers/mdlx/MdlxBlock.java | 16 + .../parsers/mdlx/MdlxBlockDescriptor.java | 125 ++++ .../warsmash/parsers/mdlx/MdlxModel.java | 642 ++++++++++++++++++ .../warsmash/parsers/mdlx/MdlxTest.java | 64 ++ .../parsers/mdlx/ParticleEmitter.java | 193 ++++++ .../parsers/mdlx/ParticleEmitter2.java | 425 ++++++++++++ .../warsmash/parsers/mdlx/RibbonEmitter.java | 176 +++++ .../warsmash/parsers/mdlx/Sequence.java | 104 +++ .../warsmash/parsers/mdlx/Texture.java | 81 +++ .../parsers/mdlx/TextureAnimation.java | 57 ++ .../warsmash/parsers/mdlx/UnknownChunk.java | 35 + .../mdlx/mdl/GhostwolfTokenInputStream.java | 232 +++++++ .../mdlx/mdl/GhostwolfTokenOutputStream.java | 261 +++++++ .../mdlx/timeline/FloatArrayKeyFrame.java | 2 +- .../parsers/mdlx/timeline/FloatKeyFrame.java | 2 +- .../parsers/mdlx/timeline/Timeline.java | 8 +- .../parsers/mdlx/timeline/UInt32KeyFrame.java | 8 +- .../warsmash/parsers/terrain/Corner.java | 27 + .../warsmash/parsers/terrain/Terrain.java | 13 + .../warsmash/parsers/terrain/TilePathing.java | 15 + .../etheller/warsmash/util/Descriptor.java | 6 + .../etheller/warsmash/util/ImageUtils.java | 85 +++ .../com/etheller/warsmash/util/MdlUtils.java | 190 ++++++ .../etheller/warsmash/util/ParseUtils.java | 119 +++- .../warsmash/util/RenderMathUtils.java | 290 ++++++++ .../com/etheller/warsmash/util/Vector4.java | 573 ++++++++++++++++ .../warsmash/viewer/BoundingShape.java | 116 ++++ .../com/etheller/warsmash/viewer/Bucket.java | 28 + .../com/etheller/warsmash/viewer/Camera.java | 314 +++++++++ .../com/etheller/warsmash/viewer/Model.java | 9 + .../warsmash/viewer/ModelInstance.java | 5 + .../etheller/warsmash/viewer/ModelView.java | 68 ++ .../com/etheller/warsmash/viewer/Scene.java | 5 + .../etheller/warsmash/viewer/SceneNode.java | 245 +++++++ .../warsmash/viewer/SkeletalNode.java | 118 ++++ .../com/etheller/warsmash/viewer/Viewer.java | 7 + .../etheller/warsmash/viewer/ViewerNode.java | 65 ++ .../com/hiveworkshop/wc3/mpq/Codebase.java | 12 + .../hiveworkshop/wc3/mpq/FileCodebase.java | 35 + jars/blp-iio-plugin.jar | Bin 0 -> 63630 bytes 61 files changed, 6825 insertions(+), 78 deletions(-) create mode 100644 .gitignore create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/Attachment.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/Bone.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/Camera.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/CollisionShape.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/EventObject.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/Extent.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/Geoset.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/GeosetAnimation.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/Helper.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/Layer.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/Light.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/Material.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/MdlxBlock.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/MdlxBlockDescriptor.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/MdlxModel.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/MdlxTest.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/ParticleEmitter.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/ParticleEmitter2.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/RibbonEmitter.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/Sequence.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/Texture.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/TextureAnimation.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/UnknownChunk.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/mdl/GhostwolfTokenInputStream.java create mode 100644 core/src/com/etheller/warsmash/parsers/mdlx/mdl/GhostwolfTokenOutputStream.java create mode 100644 core/src/com/etheller/warsmash/parsers/terrain/Corner.java create mode 100644 core/src/com/etheller/warsmash/parsers/terrain/Terrain.java create mode 100644 core/src/com/etheller/warsmash/parsers/terrain/TilePathing.java create mode 100644 core/src/com/etheller/warsmash/util/Descriptor.java create mode 100644 core/src/com/etheller/warsmash/util/ImageUtils.java create mode 100644 core/src/com/etheller/warsmash/util/RenderMathUtils.java create mode 100644 core/src/com/etheller/warsmash/util/Vector4.java create mode 100644 core/src/com/etheller/warsmash/viewer/BoundingShape.java create mode 100644 core/src/com/etheller/warsmash/viewer/Bucket.java create mode 100644 core/src/com/etheller/warsmash/viewer/Camera.java create mode 100644 core/src/com/etheller/warsmash/viewer/Model.java create mode 100644 core/src/com/etheller/warsmash/viewer/ModelInstance.java create mode 100644 core/src/com/etheller/warsmash/viewer/ModelView.java create mode 100644 core/src/com/etheller/warsmash/viewer/Scene.java create mode 100644 core/src/com/etheller/warsmash/viewer/SceneNode.java create mode 100644 core/src/com/etheller/warsmash/viewer/SkeletalNode.java create mode 100644 core/src/com/etheller/warsmash/viewer/Viewer.java create mode 100644 core/src/com/etheller/warsmash/viewer/ViewerNode.java create mode 100644 core/src/com/hiveworkshop/wc3/mpq/Codebase.java create mode 100644 core/src/com/hiveworkshop/wc3/mpq/FileCodebase.java create mode 100644 jars/blp-iio-plugin.jar 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/build.gradle b/build.gradle index 66947a9..5a5d981 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,9 @@ 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/" } @@ -51,6 +54,7 @@ project(":desktop") { compile "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-desktop" compile "com.google.guava:guava:23.5-jre" implementation 'com.github.inwc3:wc3libs:-SNAPSHOT' + compile files(fileTree(dir:'../jars', includes: ['*.jar'])) } } @@ -65,6 +69,7 @@ project(":core") { compile "com.badlogicgames.gdx:gdx-freetype:$gdxVersion" compile "com.google.guava:guava:23.5-jre" implementation 'com.github.inwc3:wc3libs:-SNAPSHOT' + compile files(fileTree(dir:'../jars', includes: ['*.jar'])) } } diff --git a/core/build.gradle b/core/build.gradle index 42ff9fd..13c049a 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,6 +1,6 @@ apply plugin: "java" -sourceCompatibility = 1.7 +sourceCompatibility = 1.8 [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' sourceSets.main.java.srcDirs = [ "src/" ] diff --git a/core/src/com/etheller/warsmash/WarsmashGdxGame.java b/core/src/com/etheller/warsmash/WarsmashGdxGame.java index b9a6a16..c3260a0 100644 --- a/core/src/com/etheller/warsmash/WarsmashGdxGame.java +++ b/core/src/com/etheller/warsmash/WarsmashGdxGame.java @@ -1,27 +1,62 @@ package com.etheller.warsmash; -import java.nio.charset.Charset; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; 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.Texture.TextureFilter; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.etheller.warsmash.util.ImageUtils; import com.etheller.warsmash.util.War3ID; +import com.hiveworkshop.wc3.mpq.Codebase; +import com.hiveworkshop.wc3.mpq.FileCodebase; public class WarsmashGdxGame extends ApplicationAdapter { + private SpriteBatch batch; + private BitmapFont font; + private Codebase codebase; + private Texture texture; @Override public void create() { + this.codebase = new FileCodebase(new File("C:/MPQBuild/War3.mpq/war3.mpq")); + final War3ID id = War3ID.fromString("ipea"); - System.out.println(id.getValue()); - for (final byte b : "Hello World".getBytes(Charset.forName("utf-8"))) { - System.out.println(b + " - " + (char) b); + try { + final String path = "terrainart\\lordaeronsummer\\lords_dirt.blp"; + final boolean has = this.codebase.has(path); + final BufferedImage img = ImageIO.read(this.codebase.getResourceAsStream(path)); + this.texture = ImageUtils.getTexture(ImageUtils.forceBufferedImagesRGB(img)); + this.texture.setFilter(TextureFilter.Linear, TextureFilter.Linear); } + catch (final IOException e) { + throw new RuntimeException(e); + } + this.batch = new SpriteBatch(); + this.font = new BitmapFont(); } @Override public void render() { - Gdx.gl.glClearColor(1, 0, 0, 1); + Gdx.gl.glClearColor(0, 1, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + final int srcFunc = this.batch.getBlendSrcFunc(); + final int dstFunc = this.batch.getBlendDstFunc(); + + this.batch.enableBlending(); + this.batch.begin(); +// this.font.draw(this.batch, "Hello World", 100, 100); + this.batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA); + this.batch.draw(this.texture, 0, 0); + this.batch.end(); + this.batch.setBlendFunction(srcFunc, dstFunc); } @Override diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/AnimatedObject.java b/core/src/com/etheller/warsmash/parsers/mdlx/AnimatedObject.java index 2af1da3..c15855a 100644 --- a/core/src/com/etheller/warsmash/parsers/mdlx/AnimatedObject.java +++ b/core/src/com/etheller/warsmash/parsers/mdlx/AnimatedObject.java @@ -6,6 +6,7 @@ import java.util.Iterator; import java.util.List; import com.etheller.warsmash.parsers.mdlx.timeline.Timeline; +import com.etheller.warsmash.util.MdlUtils; import com.etheller.warsmash.util.War3ID; import com.google.common.io.LittleEndianDataInputStream; import com.google.common.io.LittleEndianDataOutputStream; @@ -14,8 +15,8 @@ import com.google.common.io.LittleEndianDataOutputStream; * Based on the works of Chananya Freiman. * */ -public class AnimatedObject implements Chunk { - private final List timelines; +public abstract class AnimatedObject implements Chunk, MdlxBlock { + protected final List timelines; public AnimatedObject() { this.timelines = new ArrayList<>(); @@ -41,20 +42,20 @@ public class AnimatedObject implements Chunk { } public Iterator readAnimatedBlock(final MdlTokenInputStream stream) { - return new TransformedAnimatedBlockIterator(stream.readBlock()); + return new TransformedAnimatedBlockIterator(stream.readBlock().iterator()); } - public void readTimeline(final MdlTokenInputStream stream, final War3ID name) throws IOException { - final Timeline timeline = AnimationMap.ID_TO_TAG.get(name).getImplementation().createTimeline(); + public void readTimeline(final MdlTokenInputStream stream, final AnimationMap name) throws IOException { + final Timeline timeline = name.getImplementation().createTimeline(); - timeline.readMdl(stream, name); + timeline.readMdl(stream, name.getWar3id()); this.timelines.add(timeline); } - public boolean writeTimeline(final MdlTokenOutputStream stream, final War3ID name) throws IOException { + public boolean writeTimeline(final MdlTokenOutputStream stream, final AnimationMap name) throws IOException { for (final Timeline timeline : this.timelines) { - if (timeline.getName().equals(name)) { + if (timeline.getName().equals(name.getWar3id())) { timeline.writeMdl(stream); return true; } @@ -93,8 +94,8 @@ public class AnimatedObject implements Chunk { @Override public String next() { final String token = this.delegate.next(); - if (token.equals("static") && hasNext()) { - return "static " + 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/etheller/warsmash/parsers/mdlx/AnimationMap.java b/core/src/com/etheller/warsmash/parsers/mdlx/AnimationMap.java index a8b871e..ec9110f 100644 --- a/core/src/com/etheller/warsmash/parsers/mdlx/AnimationMap.java +++ b/core/src/com/etheller/warsmash/parsers/mdlx/AnimationMap.java @@ -3,6 +3,7 @@ package com.etheller.warsmash.parsers.mdlx; import java.util.HashMap; import java.util.Map; +import com.etheller.warsmash.util.MdlUtils; import com.etheller.warsmash.util.War3ID; /** @@ -15,33 +16,33 @@ import com.etheller.warsmash.util.War3ID; */ public enum AnimationMap { // Layer - KMTF("TextureId", TimelineDescriptor.UINT32_TIMELINE), - KMTA("Alpha", TimelineDescriptor.FLOAT_TIMELINE), + KMTF(MdlUtils.TOKEN_TEXTURE_ID, TimelineDescriptor.UINT32_TIMELINE), + KMTA(MdlUtils.TOKEN_ALPHA, TimelineDescriptor.FLOAT_TIMELINE), // TextureAnimation - KTAT("Translation", TimelineDescriptor.VECTOR3_TIMELINE), - KTAR("Rotation", TimelineDescriptor.VECTOR4_TIMELINE), - KTAS("Scaling", TimelineDescriptor.VECTOR3_TIMELINE), + KTAT(MdlUtils.TOKEN_TRANSLATION, TimelineDescriptor.VECTOR3_TIMELINE), + KTAR(MdlUtils.TOKEN_ROTATION, TimelineDescriptor.VECTOR4_TIMELINE), + KTAS(MdlUtils.TOKEN_SCALING, TimelineDescriptor.VECTOR3_TIMELINE), // GeosetAnimation - KGAO("Alpha", TimelineDescriptor.FLOAT_TIMELINE), - KGAC("Color", TimelineDescriptor.VECTOR3_TIMELINE), + KGAO(MdlUtils.TOKEN_ALPHA, TimelineDescriptor.FLOAT_TIMELINE), + KGAC(MdlUtils.TOKEN_COLOR, TimelineDescriptor.VECTOR3_TIMELINE), // Light - KLAS("AttenuationStart", TimelineDescriptor.FLOAT_TIMELINE), - KLAE("AttenuationEnd", TimelineDescriptor.FLOAT_TIMELINE), - KLAC("Color", TimelineDescriptor.VECTOR3_TIMELINE), - KLAI("Intensity", TimelineDescriptor.FLOAT_TIMELINE), - KLBI("AmbientIntensity", TimelineDescriptor.FLOAT_TIMELINE), - KLBC("AmbientColor", TimelineDescriptor.VECTOR3_TIMELINE), - KLAV("Visibility", TimelineDescriptor.FLOAT_TIMELINE), + KLAS(MdlUtils.TOKEN_ATTENUATION_START, TimelineDescriptor.FLOAT_TIMELINE), + KLAE(MdlUtils.TOKEN_ATTENUATION_END, TimelineDescriptor.FLOAT_TIMELINE), + KLAC(MdlUtils.TOKEN_COLOR, TimelineDescriptor.VECTOR3_TIMELINE), + KLAI(MdlUtils.TOKEN_INTENSITY, TimelineDescriptor.FLOAT_TIMELINE), + KLBI(MdlUtils.TOKEN_AMB_INTENSITY, TimelineDescriptor.FLOAT_TIMELINE), + KLBC(MdlUtils.TOKEN_AMB_COLOR, TimelineDescriptor.VECTOR3_TIMELINE), + KLAV(MdlUtils.TOKEN_VISIBILITY, TimelineDescriptor.FLOAT_TIMELINE), // Attachment - KATV("Visibility", TimelineDescriptor.FLOAT_TIMELINE), + KATV(MdlUtils.TOKEN_VISIBILITY, TimelineDescriptor.FLOAT_TIMELINE), // ParticleEmitter - KPEE("EmissionRate", TimelineDescriptor.FLOAT_TIMELINE), - KPEG("Gravity", TimelineDescriptor.FLOAT_TIMELINE), - KPLN("Longitude", TimelineDescriptor.FLOAT_TIMELINE), - KPLT("Latitude", TimelineDescriptor.FLOAT_TIMELINE), - KPEL("LifeSpan", TimelineDescriptor.FLOAT_TIMELINE), - KPES("Speed", TimelineDescriptor.FLOAT_TIMELINE), - KPEV("Visibility", TimelineDescriptor.FLOAT_TIMELINE), + KPEE(MdlUtils.TOKEN_EMISSION_RATE, TimelineDescriptor.FLOAT_TIMELINE), + KPEG(MdlUtils.TOKEN_GRAVITY, TimelineDescriptor.FLOAT_TIMELINE), + KPLN(MdlUtils.TOKEN_LONGITUDE, TimelineDescriptor.FLOAT_TIMELINE), + KPLT(MdlUtils.TOKEN_LATITUDE, TimelineDescriptor.FLOAT_TIMELINE), + KPEL(MdlUtils.TOKEN_LIFE_SPAN, TimelineDescriptor.FLOAT_TIMELINE), + KPES(MdlUtils.TOKEN_INIT_VELOCITY, TimelineDescriptor.FLOAT_TIMELINE), + KPEV(MdlUtils.TOKEN_VISIBILITY, TimelineDescriptor.FLOAT_TIMELINE), // ParticleEmitter2 KP2S("Speed", TimelineDescriptor.FLOAT_TIMELINE), KP2R("Variation", TimelineDescriptor.FLOAT_TIMELINE), @@ -59,19 +60,21 @@ public enum AnimationMap { KRTX("TextureSlot", TimelineDescriptor.UINT32_TIMELINE), KRVS("Visibility", TimelineDescriptor.FLOAT_TIMELINE), // Camera - KCTR("Translation", TimelineDescriptor.VECTOR3_TIMELINE), - KTTR("Translation", TimelineDescriptor.VECTOR3_TIMELINE), - KCRL("Rotation", TimelineDescriptor.UINT32_TIMELINE), + KCTR(MdlUtils.TOKEN_TRANSLATION, TimelineDescriptor.VECTOR3_TIMELINE), + KTTR(MdlUtils.TOKEN_TRANSLATION, TimelineDescriptor.VECTOR3_TIMELINE), + KCRL(MdlUtils.TOKEN_ROTATION, TimelineDescriptor.UINT32_TIMELINE), // GenericObject - KGTR("Translation", TimelineDescriptor.VECTOR3_TIMELINE), - KGRT("Rotation", TimelineDescriptor.VECTOR4_TIMELINE), - KGSC("Scaling", TimelineDescriptor.VECTOR3_TIMELINE); + KGTR(MdlUtils.TOKEN_TRANSLATION, TimelineDescriptor.VECTOR3_TIMELINE), + KGRT(MdlUtils.TOKEN_ROTATION, TimelineDescriptor.VECTOR4_TIMELINE), + KGSC(MdlUtils.TOKEN_SCALING, TimelineDescriptor.VECTOR3_TIMELINE); private final String mdlToken; private final TimelineDescriptor implementation; + private final War3ID war3id; private AnimationMap(final String mdlToken, final TimelineDescriptor implementation) { this.mdlToken = mdlToken; this.implementation = implementation; + this.war3id = War3ID.fromString(this.name()); } public String getMdlToken() { @@ -82,11 +85,15 @@ public enum AnimationMap { 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(War3ID.fromString(tag.name()), tag); + ID_TO_TAG.put(tag.getWar3id(), tag); } } } diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/Attachment.java b/core/src/com/etheller/warsmash/parsers/mdlx/Attachment.java new file mode 100644 index 0000000..85d65b0 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/Attachment.java @@ -0,0 +1,99 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; + +import com.etheller.warsmash.util.MdlUtils; +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class Attachment extends GenericObject { + private String path = ""; + private int attachmentId; + + public Attachment() { + super(0x800); + } + + /** + * Restricts us to only be able to parse models on one thread at a time, in + * return for high performance. + */ + private static final byte[] PATH_BYTES_HEAP = new byte[260]; + + @Override + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + final long size = ParseUtils.readUInt32(stream); + + super.readMdx(stream); + + this.path = ParseUtils.readString(stream, PATH_BYTES_HEAP); + this.attachmentId = stream.readInt(); + + this.readTimelines(stream, size - this.getByteLength()); + } + + @Override + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, getByteLength()); + + super.writeMdx(stream); + + final byte[] bytes = this.path.getBytes(ParseUtils.UTF8); + stream.write(bytes); + for (int i = 0; i < (260 - bytes.length); i++) { + stream.write((byte) 0); + } + stream.writeInt(this.attachmentId); // Used to be Int32 in JS + + this.writeNonGenericAnimationChunks(stream); + } + + @Override + public void readMdl(final MdlTokenInputStream stream) throws IOException { + 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)) { + this.readTimeline(stream, AnimationMap.KATV); + } + else { + throw new IOException("Unknown token in Attachment " + this.name + ": " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream) throws IOException { + stream.startObjectBlock(MdlUtils.TOKEN_ATTACHMENT, this.name); + this.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); + } + + this.writeTimeline(stream, AnimationMap.KATV); + + this.writeGenericTimelines(stream); + stream.endBlock(); + } + + @Override + public long getByteLength() { + return 268 + super.getByteLength(); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/Bone.java b/core/src/com/etheller/warsmash/parsers/mdlx/Bone.java new file mode 100644 index 0000000..8c4fe7a --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/Bone.java @@ -0,0 +1,88 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; + +import com.etheller.warsmash.util.MdlUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class Bone extends GenericObject { + private int geosetId = -1; + private int geosetAnimationId = -1; + + public Bone() { + super(0x100); + } + + @Override + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + super.readMdx(stream); + + this.geosetId = stream.readInt(); + this.geosetAnimationId = stream.readInt(); + } + + @Override + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + super.writeMdx(stream); + stream.writeInt(this.geosetId); + stream.writeInt(this.geosetAnimationId); + } + + @Override + public void readMdl(final MdlTokenInputStream stream) { + 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) throws IOException { + stream.startObjectBlock(MdlUtils.TOKEN_BONE, this.name); + this.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); + } + + this.writeGenericTimelines(stream); + stream.endBlock(); + } + + @Override + public long getByteLength() { + return 8 + super.getByteLength(); + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/Camera.java b/core/src/com/etheller/warsmash/parsers/mdlx/Camera.java new file mode 100644 index 0000000..1c3c363 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/Camera.java @@ -0,0 +1,128 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; + +import com.etheller.warsmash.util.MdlUtils; +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class Camera extends AnimatedObject { + protected String name; + private final float[] position; + private float fieldOfView; + private float farClippingPlane; + private float nearClippingPlane; + private final float[] targetPosition; + + public Camera() { + this.position = new float[3]; + this.targetPosition = new float[3]; + } + + /** + * Restricts us to only be able to parse models on one thread at a time, in + * return for high performance. + */ + private static final byte[] NAME_BYTES_HEAP = new byte[80]; + + @Override + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + final long size = ParseUtils.readUInt32(stream); + + this.name = ParseUtils.readString(stream, NAME_BYTES_HEAP); + ParseUtils.readFloatArray(stream, this.position); + this.fieldOfView = stream.readFloat(); + this.farClippingPlane = stream.readFloat(); + this.nearClippingPlane = stream.readFloat(); + ParseUtils.readFloatArray(stream, this.targetPosition); + + readTimelines(stream, size - 120); + } + + @Override + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, getByteLength()); + final byte[] bytes = this.name.getBytes(ParseUtils.UTF8); + stream.write(bytes); + for (int i = 0; i < (80 - bytes.length); i++) { + stream.write((byte) 0); + } + ParseUtils.writeFloatArray(stream, this.position); + stream.writeFloat(this.fieldOfView); + stream.writeFloat(this.farClippingPlane); + stream.writeFloat(this.nearClippingPlane); + ParseUtils.writeFloatArray(stream, this.targetPosition); + + writeTimelines(stream); + } + + @Override + public void readMdl(final MdlTokenInputStream stream) throws IOException { + 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 IllegalStateException("Unknown token in Camera " + this.name + ": " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream) throws IOException { + 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() { + return 120 + super.getByteLength(); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/CollisionShape.java b/core/src/com/etheller/warsmash/parsers/mdlx/CollisionShape.java new file mode 100644 index 0000000..7e82a21 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/CollisionShape.java @@ -0,0 +1,167 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; + +import com.etheller.warsmash.util.MdlUtils; +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class CollisionShape extends GenericObject { + public static enum Type { + PLANE, + BOX, + SPHERE, + CYLINDER; + + private static final Type[] VALUES = values(); + + private final boolean boundsRadius; + + private Type() { + this.boundsRadius = false; + } + + private Type(final boolean boundsRadius) { + this.boundsRadius = boundsRadius; + } + + public boolean isBoundsRadius() { + return this.boundsRadius; + } + + public static Type from(final int index) { + return VALUES[index]; + } + } + + private CollisionShape.Type type; + private final float[][] vertices = { new float[3], new float[3] }; + private float boundsRadius; + + public CollisionShape() { + super(0x2000); + } + + @Override + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + super.readMdx(stream); + + final long typeIndex = ParseUtils.readUInt32(stream); + this.type = CollisionShape.Type.from((int) typeIndex); + ParseUtils.readFloatArray(stream, this.vertices[0]); + + if (this.type != Type.SPHERE) { + ParseUtils.readFloatArray(stream, this.vertices[1]); + } + if ((this.type == Type.SPHERE) || (this.type == Type.CYLINDER)) { + this.boundsRadius = stream.readFloat(); + } + } + + @Override + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + super.writeMdx(stream); + + ParseUtils.writeUInt32(stream, this.type == null ? -1 : this.type.ordinal()); + ParseUtils.writeFloatArray(stream, this.vertices[0]); + if (this.type != CollisionShape.Type.SPHERE) { + ParseUtils.writeFloatArray(stream, this.vertices[1]); + } + if ((this.type == Type.SPHERE) || (this.type == Type.CYLINDER)) { + stream.writeFloat(this.boundsRadius); + } + } + + @Override + public void readMdl(final MdlTokenInputStream stream) { + for (final String token : super.readMdlGeneric(stream)) { + switch (token) { + case MdlUtils.TOKEN_PLANE: + this.type = Type.PLANE; + break; + case MdlUtils.TOKEN_BOX: + this.type = Type.BOX; + 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) throws IOException { + stream.startObjectBlock(MdlUtils.TOKEN_COLLISION_SHAPE, this.name); + writeGenericHeader(stream); + String type; + int vertices = 2; + switch (this.type) { + case PLANE: + type = MdlUtils.TOKEN_PLANE; + break; + case BOX: + type = MdlUtils.TOKEN_BOX; + 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() { + long size = 16 + super.getByteLength(); + + if (this.type != Type.SPHERE) { + size += 12; + } + + if ((this.type == Type.SPHERE) || (this.type == Type.CYLINDER)) { + size += 4; + } + + return size; + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/EventObject.java b/core/src/com/etheller/warsmash/parsers/mdlx/EventObject.java new file mode 100644 index 0000000..5e4a62a --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/EventObject.java @@ -0,0 +1,77 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; + +import com.etheller.warsmash.util.MdlUtils; +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 EventObject extends GenericObject { + private static final War3ID KEVT = War3ID.fromString("KEVT"); + private int globalSequenceId = -1; + private long[] keyFrames = { 1 }; + + public EventObject() { + super(0x400); + } + + @Override + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + super.readMdx(stream); + stream.readInt(); // KEVT skipped + final long count = ParseUtils.readUInt32(stream); + this.globalSequenceId = stream.readInt(); + + this.keyFrames = new long[(int) count]; + for (int i = 0; i < count; i++) { + this.keyFrames[i] = stream.readInt(); + } + } + + @Override + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + super.writeMdx(stream); + ParseUtils.writeWar3ID(stream, KEVT); + ParseUtils.writeUInt32(stream, this.keyFrames.length); + stream.writeInt(this.globalSequenceId); + for (int i = 0; i < this.keyFrames.length; i++) { + ParseUtils.writeUInt32(stream, this.keyFrames[i]); + } + } + + @Override + public void readMdl(final MdlTokenInputStream stream) { + 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) throws IOException { + 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() { + return 12 + (this.keyFrames.length * 4) + super.getByteLength(); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/Extent.java b/core/src/com/etheller/warsmash/parsers/mdlx/Extent.java new file mode 100644 index 0000000..3be2e48 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/Extent.java @@ -0,0 +1,47 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; + +import com.etheller.warsmash.util.MdlUtils; +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class Extent { + protected float boundsRadius = 0; + protected final float[] min = new float[3]; + protected final float[] max = new float[3]; + + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + this.boundsRadius = stream.readFloat(); + ParseUtils.readFloatArray(stream, this.min); + ParseUtils.readFloatArray(stream, this.max); + } + + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + stream.writeFloat(this.boundsRadius); + ParseUtils.writeFloatArray(stream, this.min); + ParseUtils.writeFloatArray(stream, 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 void setBoundsRadius(final float boundsRadius) { + this.boundsRadius = boundsRadius; + } + + public float getBoundsRadius() { + return this.boundsRadius; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/GenericObject.java b/core/src/com/etheller/warsmash/parsers/mdlx/GenericObject.java index 45353a2..b5018b0 100644 --- a/core/src/com/etheller/warsmash/parsers/mdlx/GenericObject.java +++ b/core/src/com/etheller/warsmash/parsers/mdlx/GenericObject.java @@ -1,11 +1,13 @@ package com.etheller.warsmash.parsers.mdlx; import java.io.IOException; -import java.nio.charset.Charset; import java.util.Iterator; +import java.util.List; import com.etheller.warsmash.parsers.mdlx.timeline.Timeline; +import com.etheller.warsmash.util.MdlUtils; import com.etheller.warsmash.util.ParseUtils; +import com.etheller.warsmash.util.War3ID; import com.google.common.io.LittleEndianDataInputStream; import com.google.common.io.LittleEndianDataOutputStream; @@ -19,11 +21,10 @@ import com.google.common.io.LittleEndianDataOutputStream; * Based on the works of Chananya Freiman. */ public abstract class GenericObject extends AnimatedObject implements Chunk { - private static final Charset UTF8 = Charset.forName("utf-8"); - private String name; + protected String name; private int objectId; private int parentId; - private int flags; + protected int flags; /** * Restricts us to only be able to parse models on one thread at a time, in @@ -38,19 +39,21 @@ public abstract class GenericObject extends AnimatedObject implements Chunk { this.flags = flags; } + @Override public void readMdx(final LittleEndianDataInputStream stream) throws IOException { - final long size = ParseUtils.parseUInt32(stream); - stream.read(NAME_BYTES_HEAP); - this.name = new String(NAME_BYTES_HEAP, UTF8); + final long size = ParseUtils.readUInt32(stream); + this.name = ParseUtils.readString(stream, NAME_BYTES_HEAP); this.objectId = stream.readInt(); this.parentId = stream.readInt(); - this.flags = stream.readInt(); + this.flags = stream.readInt(); // Used to be Int32 in JS readTimelines(stream, size - 96); } + @Override public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { - stream.writeInt((int) getGenericByteLength()); - final byte[] bytes = this.name.getBytes(UTF8); + ParseUtils.writeUInt32(stream, getGenericByteLength()); + final byte[] bytes = this.name.getBytes(ParseUtils.UTF8); + stream.write(bytes); for (int i = 0; i < (80 - bytes.length); i++) { stream.write((byte) 0); } @@ -69,7 +72,66 @@ public abstract class GenericObject extends AnimatedObject implements Chunk { } } - protected abstract Iterable eachTimeline(boolean generic); + protected final Iterable readMdlGeneric(final MdlTokenInputStream stream) { + this.name = stream.read(); + return new Iterable() { + @Override + public Iterator iterator() { + return new WrappedMdlTokenIterator(GenericObject.this.readAnimatedBlock(stream), GenericObject.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) throws IOException { + this.writeTimeline(stream, AnimationMap.KGTR); + this.writeTimeline(stream, AnimationMap.KGRT); + this.writeTimeline(stream, AnimationMap.KGSC); + } + + public Iterable eachTimeline(final boolean generic) { + return new TimelineMaskingIterable(generic); + } public long getGenericByteLength() { long size = 96; @@ -84,23 +146,188 @@ public abstract class GenericObject extends AnimatedObject implements Chunk { return 96 + super.getByteLength(); } - private static final class WrappedMdlTokenIterator implements Iterator { - private final Iterator delegate; + private final class TimelineMaskingIterable implements Iterable { + private final boolean generic; - public WrappedMdlTokenIterator(final Iterator delegate) { - this.delegate = delegate; + private TimelineMaskingIterable(final boolean generic) { + this.generic = generic; + } + + @Override + public Iterator iterator() { + return new TimelineMaskingIterator(this.generic, GenericObject.this.timelines); + } + } + + private static final class TimelineMaskingIterator implements Iterator { + private final boolean wantGeneric; + private final Iterator delegate; + private boolean hasNext; + private Timeline next; + + public TimelineMaskingIterator(final boolean wantGeneric, final List timelines) { + this.wantGeneric = wantGeneric; + this.delegate = timelines.iterator(); + scanUntilNext(); + } + + private boolean isGeneric(final Timeline timeline) { + final War3ID name = timeline.getName(); + final boolean generic = AnimationMap.KGTR.getWar3id().equals(name) + || AnimationMap.KGRT.getWar3id().equals(name) || AnimationMap.KGSC.getWar3id().equals(name); + return generic; + } + + private void scanUntilNext() { + boolean hasNext = false; + if (hasNext = this.delegate.hasNext()) { + do { + this.next = this.delegate.next(); + } + while ((isGeneric(this.next) != this.wantGeneric) && (hasNext = this.delegate.hasNext())); + } + if (!hasNext) { + this.next = null; + } } @Override public boolean hasNext() { - return this.delegate.hasNext(); + return this.next != null; + } + + @Override + public Timeline next() { + final Timeline last = this.next; + scanUntilNext(); + return last; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Remove is not supported"); + } + + } + + private static final class WrappedMdlTokenIterator implements Iterator { + private final Iterator delegate; + private final GenericObject updatingObject; + private final MdlTokenInputStream stream; + private String next; + private boolean hasLoaded = false; + + public WrappedMdlTokenIterator(final Iterator delegate, final GenericObject 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() { - final String token = this.delegate.next(); + if (!this.hasLoaded) { + this.next = read(); + } + this.hasLoaded = false; + return this.next; + } - return null; + 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: + try { + this.updatingObject.readTimeline(this.stream, AnimationMap.KGTR); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + token = null; + break; + case MdlUtils.TOKEN_ROTATION: + try { + this.updatingObject.readTimeline(this.stream, AnimationMap.KGRT); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + token = null; + break; + case MdlUtils.TOKEN_SCALING: + try { + this.updatingObject.readTimeline(this.stream, AnimationMap.KGSC); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + token = null; + break; + default: + break InteriorParsing; + } + } + while (this.delegate.hasNext()); + return token; } } diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/Geoset.java b/core/src/com/etheller/warsmash/parsers/mdlx/Geoset.java new file mode 100644 index 0000000..eb8b55d --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/Geoset.java @@ -0,0 +1,319 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.etheller.warsmash.util.MdlUtils; +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 Geoset implements MdlxBlock, Chunk { + 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 UVAS = War3ID.fromString("UVAS"); + private static final War3ID UVBS = War3ID.fromString("UVBS"); + private float[] vertices; + private float[] normals; + private long[] faceTypeGroups; // unsigned int[] + private long[] faceGroups; // unsigned int[] + private int[] faces; // unsigned short[] + private short[] vertexGroups; // unsigned byte[] + private long[] matrixGroups; // unsigned int[] + private long[] matrixIndices; // unsigned int[] + private long materialId = 0; + private long selectionGroup = 0; + private long selectionFlags = 0; + private final Extent extent = new Extent(); + private Extent[] sequenceExtents; + private float[][] uvSets; + + @Override + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + final long mySize = ParseUtils.readUInt32(stream); + final int vrtx = stream.readInt(); // skip VRTX + this.vertices = ParseUtils.readFloatArray(stream, (int) (ParseUtils.readUInt32(stream) * 3)); + final int nrms = stream.readInt(); // skip NRMS + this.normals = ParseUtils.readFloatArray(stream, (int) (ParseUtils.readUInt32(stream) * 3)); + final int ptyp = stream.readInt(); // skip PTYP + this.faceTypeGroups = ParseUtils.readUInt32Array(stream, (int) ParseUtils.readUInt32(stream)); + stream.readInt(); // skip PCNT + this.faceGroups = ParseUtils.readUInt32Array(stream, (int) ParseUtils.readUInt32(stream)); + stream.readInt(); // skip PVTX + this.faces = ParseUtils.readUInt16Array(stream, (int) ParseUtils.readUInt32(stream)); + stream.readInt(); // skip GNDX + this.vertexGroups = ParseUtils.readUInt8Array(stream, (int) ParseUtils.readUInt32(stream)); + stream.readInt(); // skip MTGC + this.matrixGroups = ParseUtils.readUInt32Array(stream, (int) ParseUtils.readUInt32(stream)); + stream.readInt(); // skip MATS + this.matrixIndices = ParseUtils.readUInt32Array(stream, (int) ParseUtils.readUInt32(stream)); + this.materialId = ParseUtils.readUInt32(stream); + this.selectionGroup = ParseUtils.readUInt32(stream); + this.selectionFlags = ParseUtils.readUInt32(stream); + this.extent.readMdx(stream); + + final long numExtents = ParseUtils.readUInt32(stream); + this.sequenceExtents = new Extent[(int) numExtents]; + for (int i = 0; i < numExtents; i++) { + final Extent extent = new Extent(); + extent.readMdx(stream); + this.sequenceExtents[i] = extent; + } + + stream.readInt(); // skip UVAS + + final long numUVLayers = ParseUtils.readUInt32(stream); + this.uvSets = new float[(int) numUVLayers][]; + for (int i = 0; i < numUVLayers; i++) { + stream.readInt(); // skip UVBS + this.uvSets[i] = ParseUtils.readFloatArray(stream, (int) (ParseUtils.readUInt32(stream) * 2)); + } + } + + @Override + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, this.getByteLength()); + ParseUtils.writeWar3ID(stream, VRTX); + ParseUtils.writeUInt32(stream, this.vertices.length / 3); + ParseUtils.writeFloatArray(stream, this.vertices); + ParseUtils.writeWar3ID(stream, NRMS); + ParseUtils.writeUInt32(stream, this.normals.length / 3); + ParseUtils.writeFloatArray(stream, this.normals); + ParseUtils.writeWar3ID(stream, PTYP); + ParseUtils.writeUInt32(stream, this.faceTypeGroups.length); + ParseUtils.writeUInt32Array(stream, this.faceTypeGroups); + ParseUtils.writeWar3ID(stream, PCNT); + ParseUtils.writeUInt32(stream, this.faceGroups.length); + ParseUtils.writeUInt32Array(stream, this.faceGroups); + ParseUtils.writeWar3ID(stream, PVTX); + ParseUtils.writeUInt32(stream, this.faces.length); + ParseUtils.writeUInt16Array(stream, this.faces); + ParseUtils.writeWar3ID(stream, GNDX); + ParseUtils.writeUInt32(stream, this.vertexGroups.length); + ParseUtils.writeUInt8Array(stream, this.vertexGroups); + ParseUtils.writeWar3ID(stream, MTGC); + ParseUtils.writeUInt32(stream, this.matrixGroups.length); + ParseUtils.writeUInt32Array(stream, this.matrixGroups); + ParseUtils.writeWar3ID(stream, MATS); + ParseUtils.writeUInt32(stream, this.matrixIndices.length); + ParseUtils.writeUInt32Array(stream, this.matrixIndices); + ParseUtils.writeUInt32(stream, this.materialId); + ParseUtils.writeUInt32(stream, this.selectionGroup); + ParseUtils.writeUInt32(stream, this.selectionFlags); + this.extent.writeMdx(stream); + ParseUtils.writeUInt32(stream, this.sequenceExtents.length); + + for (final Extent sequenceExtent : this.sequenceExtents) { + sequenceExtent.writeMdx(stream); + } + + ParseUtils.writeWar3ID(stream, UVAS); + ParseUtils.writeUInt32(stream, this.uvSets.length); + for (final float[] uvSet : this.uvSets) { + ParseUtils.writeWar3ID(stream, UVBS); + ParseUtils.writeUInt32(stream, uvSet.length / 2); + ParseUtils.writeFloatArray(stream, uvSet); + } + } + + @Override + public void readMdl(final MdlTokenInputStream stream) { + this.uvSets = new float[0][]; + final List sequenceExtents = new ArrayList<>(); + for (final String token : stream.readBlock()) { + 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.shortValue(); + } + } + break; + case MdlUtils.TOKEN_FACES: + // For now hardcoded for triangles, until I see a model with something + // different. + 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.intValue(); + } + this.matrixGroups = new long[groups.size()]; + i = 0; + for (final Integer group : groups) { + this.matrixGroups[i++] = group.intValue(); + } + } + 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 Extent extent = new Extent(); + + 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; + } + } + + 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; + default: + throw new RuntimeException("Unknown token in Geoset: " + token); + } + } + this.sequenceExtents = sequenceExtents.toArray(new Extent[sequenceExtents.size()]); + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream) { + 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); + } + + stream.startBlock(MdlUtils.TOKEN_VERTEX_GROUP); + for (int i = 0; i < this.vertexGroups.length; i++) { + stream.writeLine(this.vertexGroups[i] + ","); + } + 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 StringBuffer facesBuffer = new StringBuffer(); + 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 Extent 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"); + } + stream.endBlock(); + } + + @Override + public long getByteLength() { + 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.length * 28); + for (final float[] uvSet : this.uvSets) { + size += 8 + (uvSet.length * 4); + } + return size; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/GeosetAnimation.java b/core/src/com/etheller/warsmash/parsers/mdlx/GeosetAnimation.java new file mode 100644 index 0000000..9375a6e --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/GeosetAnimation.java @@ -0,0 +1,102 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; +import java.util.Iterator; + +import com.etheller.warsmash.util.MdlUtils; +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class GeosetAnimation extends AnimatedObject { + private float alpha = 1; + private int flags = 0; + private final float[] color = { 1, 1, 1 }; + private int geosetId = -1; + + @Override + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + final long size = ParseUtils.readUInt32(stream); + + this.alpha = stream.readFloat(); + this.flags = stream.readInt();// ParseUtils.readUInt32(stream); + ParseUtils.readFloatArray(stream, this.color); + this.geosetId = stream.readInt(); + + readTimelines(stream, size - 28); + } + + @Override + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, getByteLength()); + stream.writeFloat(this.alpha); + stream.writeInt(this.flags);// ParseUtils.writeUInt32(stream, this.flags); + ParseUtils.writeFloatArray(stream, this.color); + stream.writeInt(this.geosetId); + + writeTimelines(stream); + } + + @Override + public void readMdl(final MdlTokenInputStream stream) throws IOException { + 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: + this.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 IllegalStateException("Unknown token in GeosetAnimation: " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream) throws IOException { + stream.startBlock(MdlUtils.TOKEN_GEOSETANIM); + + if ((this.flags & 0x1) != 0) { + stream.writeFlag(MdlUtils.TOKEN_DROP_SHADOW); + } + + if (!this.writeTimeline(stream, AnimationMap.KGAO)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_ALPHA, this.alpha); + } + + if ((this.flags & 0x2) != 0) { + if (!this.writeTimeline(stream, AnimationMap.KGAC) + && ((this.color[0] != 0) || (this.color[1] != 0) || (this.color[2] != 0))) { + stream.writeColor(MdlUtils.TOKEN_STATIC_COLOR + " ", this.color); // TODO why the space? + } + } + + 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() { + return 28 + super.getByteLength(); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/Helper.java b/core/src/com/etheller/warsmash/parsers/mdlx/Helper.java new file mode 100644 index 0000000..6426f28 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/Helper.java @@ -0,0 +1,29 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; + +import com.etheller.warsmash.util.MdlUtils; + +public class Helper extends GenericObject { + + public Helper() { + 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) { + for (final String token : readMdlGeneric(stream)) { + throw new IllegalStateException("Unknown token in Helper: " + token); + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream) throws IOException { + stream.startObjectBlock(MdlUtils.TOKEN_HELPER, this.name); + writeGenericHeader(stream); + writeGenericTimelines(stream); + stream.endBlock(); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/Layer.java b/core/src/com/etheller/warsmash/parsers/mdlx/Layer.java new file mode 100644 index 0000000..3ba2673 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/Layer.java @@ -0,0 +1,195 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; +import java.util.Iterator; + +import com.etheller.warsmash.util.MdlUtils; +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class Layer extends AnimatedObject { + // 0: none + // 1: transparent + // 2: blend + // 3: additive + // 4: add alpha + // 5: modulate + // 6: modulate 2x + public static enum FilterMode { + NONE("None"), + TRANSPARENT("Transparent"), + BLEND("Blend"), + ADDITIVE("Additive"), + ADDALPHA("AddAlpha"), + MODULATE("Modulate"), + MODULATE2X("Modulate2x"); + + String mdlText; + + FilterMode(final String str) { + this.mdlText = str; + } + + public String getMdlText() { + return this.mdlText; + } + + public static FilterMode fromId(final int id) { + return values()[id]; + } + + public static int nameToId(final String name) { + for (final FilterMode mode : values()) { + if (mode.getMdlText().equals(name)) { + return mode.ordinal(); + } + } + return -1; + } + + @Override + public String toString() { + return getMdlText(); + } + } + + private FilterMode filterMode; + private int flags = 0; + private int textureId = -1; + private int textureAnimationId = -1; + private long coordId = 0; + private float alpha = 1; + + @Override + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + final long size = ParseUtils.readUInt32(stream); + + this.filterMode = FilterMode.fromId((int) ParseUtils.readUInt32(stream)); + this.flags = stream.readInt(); // UInt32 in JS + this.textureId = stream.readInt(); + this.textureAnimationId = stream.readInt(); + this.coordId = ParseUtils.readUInt32(stream); + this.alpha = stream.readFloat(); + + readTimelines(stream, size - 28); + } + + @Override + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, getByteLength()); + ParseUtils.writeUInt32(stream, this.filterMode.ordinal()); + ParseUtils.writeUInt32(stream, this.flags); + stream.writeInt(this.textureId); + stream.writeInt(this.textureAnimationId); + ParseUtils.writeUInt32(stream, this.coordId); + stream.writeFloat(this.alpha); + + writeTimelines(stream); + } + + @Override + public void readMdl(final MdlTokenInputStream stream) throws IOException { + 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 |= 0x100; + break; + 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; + default: + throw new IllegalStateException("Unknown token in Layer: " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream) throws IOException { + stream.startBlock(MdlUtils.TOKEN_LAYER); + + stream.writeAttrib(MdlUtils.TOKEN_FILTER_MODE, this.filterMode.getMdlText()); + + 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 (!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); + } + + stream.endBlock(); + } + + @Override + public long getByteLength() { + return 28 + super.getByteLength(); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/Light.java b/core/src/com/etheller/warsmash/parsers/mdlx/Light.java new file mode 100644 index 0000000..6b802e0 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/Light.java @@ -0,0 +1,166 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; + +import com.etheller.warsmash.util.MdlUtils; +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class Light extends GenericObject { + + private int type = -1; + private final float[] attenuation = new float[2]; + private final float[] color = new float[3]; + private float intensity = 0; + private final float[] ambientColor = new float[3]; + private float ambientIntensity = 0; + + public Light() { + super(0x200); + } + + @Override + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + final long size = ParseUtils.readUInt32(stream); + + super.readMdx(stream); + + this.type = stream.readInt(); // UInt32 in JS + ParseUtils.readFloatArray(stream, this.attenuation); + ParseUtils.readFloatArray(stream, this.color); + this.intensity = stream.readFloat(); + ParseUtils.readFloatArray(stream, this.ambientColor); + this.ambientIntensity = stream.readFloat(); + + readTimelines(stream, size - this.getByteLength()); + } + + @Override + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, getByteLength()); + + super.writeMdx(stream); + + ParseUtils.writeUInt32(stream, this.type); + ParseUtils.writeFloatArray(stream, this.attenuation); + ParseUtils.writeFloatArray(stream, this.color); + stream.writeFloat(this.intensity); + ParseUtils.writeFloatArray(stream, this.ambientColor); + stream.writeFloat(this.ambientIntensity); + + writeNonGenericAnimationChunks(stream); + } + + @Override + public void readMdl(final MdlTokenInputStream stream) throws IOException { + for (final String token : super.readMdlGeneric(stream)) { + switch (token) { + case MdlUtils.TOKEN_OMNIDIRECTIONAL: + this.type = 0; + break; + case MdlUtils.TOKEN_DIRECTIONAL: + this.type = 1; + break; + case MdlUtils.TOKEN_AMBIENT: + this.type = 2; + 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 IllegalStateException("Unknown token in Light: " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream) throws IOException { + stream.startObjectBlock(MdlUtils.TOKEN_LIGHT, this.name); + writeGenericHeader(stream); + + switch (this.type) { + case 0: + stream.writeFlag(MdlUtils.TOKEN_OMNIDIRECTIONAL); + break; + case 1: + stream.writeFlag(MdlUtils.TOKEN_DIRECTIONAL); + break; + case 2: + stream.writeFlag(MdlUtils.TOKEN_AMBIENT); + break; + default: + throw new IllegalStateException("Unable to save Light of type: " + this.type); + } + + 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() { + return 48 + super.getByteLength(); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/Material.java b/core/src/com/etheller/warsmash/parsers/mdlx/Material.java new file mode 100644 index 0000000..46cb097 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/Material.java @@ -0,0 +1,121 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.etheller.warsmash.util.MdlUtils; +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 Material implements MdlxBlock, Chunk { + private static final War3ID LAYS = War3ID.fromString("LAYS"); + private int priorityPlane = 0; + private int flags; + private final List layers = new ArrayList<>(); + + @Override + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + ParseUtils.readUInt32(stream); // Don't care about the size + + this.priorityPlane = stream.readInt();// ParseUtils.readUInt32(stream); + this.flags = stream.readInt();// ParseUtils.readUInt32(stream); + + stream.readInt(); // skip LAYS + + final long layerCount = ParseUtils.readUInt32(stream); + for (int i = 0; i < layerCount; i++) { + final Layer layer = new Layer(); + layer.readMdx(stream); + this.layers.add(layer); + } + } + + @Override + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, getByteLength()); + stream.writeInt(this.priorityPlane); // was UInt32 in JS, but I *really* thought I used -1 in a past model + stream.writeInt(this.flags); // UInt32 in JS + ParseUtils.writeWar3ID(stream, LAYS); + ParseUtils.writeUInt32(stream, this.layers.size()); + + for (final Layer layer : this.layers) { + layer.writeMdx(stream); + } + } + + @Override + public void readMdl(final MdlTokenInputStream stream) throws IOException { + 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 MdlUtils.TOKEN_LAYER: { + final Layer layer = new Layer(); + layer.readMdl(stream); + this.layers.add(layer); + break; + } + default: + throw new IllegalStateException("Unknown token in Material: " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream) throws IOException { + 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); + } + + for (final Layer layer : this.layers) { + layer.writeMdl(stream); + } + + stream.endBlock(); + } + + @Override + public long getByteLength() { + long size = 20; + + for (final Layer layer : this.layers) { + size += layer.getByteLength(); + } + + return size; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/MdlTokenInputStream.java b/core/src/com/etheller/warsmash/parsers/mdlx/MdlTokenInputStream.java index bb01f82..0049b8e 100644 --- a/core/src/com/etheller/warsmash/parsers/mdlx/MdlTokenInputStream.java +++ b/core/src/com/etheller/warsmash/parsers/mdlx/MdlTokenInputStream.java @@ -1,7 +1,5 @@ package com.etheller.warsmash.parsers.mdlx; -import java.util.Iterator; - public interface MdlTokenInputStream { String read(); @@ -11,9 +9,27 @@ public interface MdlTokenInputStream { float readFloat(); + void readIntArray(long[] values); + + float[] readFloatArray(float[] values); // is this same as read keyframe??? + void readKeyframe(float[] values); + float[] readVectorArray(float[] array, int vectorLength); + + int[] readUInt16Array(int[] values); + + short[] readUInt8Array(short[] values); + String peek(); - Iterator readBlock(); + // needs crazy generator function behavior that I can call this multiple times + // and it allocates a new iterator that is changing the same underlying + // stream position, and needs nesting of blocks within blocks + // (see crazy transcribed generator in GenericObject, only makes good sense + // in JS) + Iterable readBlock(); + + void readColor(float[] color); + } diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/MdlTokenOutputStream.java b/core/src/com/etheller/warsmash/parsers/mdlx/MdlTokenOutputStream.java index a2a07b9..511430c 100644 --- a/core/src/com/etheller/warsmash/parsers/mdlx/MdlTokenOutputStream.java +++ b/core/src/com/etheller/warsmash/parsers/mdlx/MdlTokenOutputStream.java @@ -11,11 +11,49 @@ public interface MdlTokenOutputStream { void unindent(); + void startObjectBlock(String name, String objectName); + void startBlock(String name, int blockSize); + void startBlock(String name); + void writeFlag(String token); + void writeFlagUInt32(long flag); + void writeAttrib(String string, int globalSequenceId); + void writeAttribUInt32(String attribName, long uInt); + + void writeAttrib(String string, String value); + + void writeFloatAttrib(String attribName, float value); + + // if this matches writeAttrib(String,String), + // then remove it + void writeStringAttrib(String attribName, String value); + + void writeFloatArrayAttrib(String attribName, float[] floatArray); + + void writeLongSubArrayAttrib(String attribName, long[] array, int startIndexInclusive, int endIndexExclusive); + + void writeFloatArray(float[] floatArray); + + void writeVectorArray(String token, float[] vectors, int vectorLength); + void endBlock(); + + void endBlockComma(); + + void writeLine(String string); + + void startBlock(String tokenFaces, int sizeNumberProbably, int length); + + void writeColor(String tokenStaticColor, float[] color); + + void writeArrayAttrib(String tokenAlpha, short[] uint8Array); + + void writeArrayAttrib(String tokenAlpha, int[] uint16Array); + + void writeArrayAttrib(String tokenAlpha, long[] uint32Array); } diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/MdlxBlock.java b/core/src/com/etheller/warsmash/parsers/mdlx/MdlxBlock.java new file mode 100644 index 0000000..680cbec --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/MdlxBlock.java @@ -0,0 +1,16 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; + +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public interface MdlxBlock { + void readMdx(final LittleEndianDataInputStream stream) throws IOException; + + void writeMdx(final LittleEndianDataOutputStream stream) throws IOException; + + void readMdl(final MdlTokenInputStream stream) throws IOException; + + void writeMdl(final MdlTokenOutputStream stream) throws IOException; +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/MdlxBlockDescriptor.java b/core/src/com/etheller/warsmash/parsers/mdlx/MdlxBlockDescriptor.java new file mode 100644 index 0000000..2844003 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/MdlxBlockDescriptor.java @@ -0,0 +1,125 @@ +package com.etheller.warsmash.parsers.mdlx; + +import com.etheller.warsmash.util.Descriptor; + +public interface MdlxBlockDescriptor extends Descriptor { + + public static final MdlxBlockDescriptor ATTACHMENT = new MdlxBlockDescriptor() { + @Override + public Attachment create() { + return new Attachment(); + } + }; + + public static final MdlxBlockDescriptor BONE = new MdlxBlockDescriptor() { + @Override + public Bone create() { + return new Bone(); + } + }; + + public static final MdlxBlockDescriptor CAMERA = new MdlxBlockDescriptor() { + @Override + public Camera create() { + return new Camera(); + } + }; + + public static final MdlxBlockDescriptor COLLISION_SHAPE = new MdlxBlockDescriptor() { + @Override + public CollisionShape create() { + return new CollisionShape(); + } + }; + + public static final MdlxBlockDescriptor EVENT_OBJECT = new MdlxBlockDescriptor() { + @Override + public EventObject create() { + return new EventObject(); + } + }; + + public static final MdlxBlockDescriptor GEOSET = new MdlxBlockDescriptor() { + @Override + public Geoset create() { + return new Geoset(); + } + }; + + public static final MdlxBlockDescriptor GEOSET_ANIMATION = new MdlxBlockDescriptor() { + @Override + public GeosetAnimation create() { + return new GeosetAnimation(); + } + }; + + public static final MdlxBlockDescriptor HELPER = new MdlxBlockDescriptor() { + @Override + public Helper create() { + return new Helper(); + } + }; + + public static final MdlxBlockDescriptor LIGHT = new MdlxBlockDescriptor() { + @Override + public Light create() { + return new Light(); + } + }; + + public static final MdlxBlockDescriptor LAYER = new MdlxBlockDescriptor() { + @Override + public Layer create() { + return new Layer(); + } + }; + + public static final MdlxBlockDescriptor MATERIAL = new MdlxBlockDescriptor() { + @Override + public Material create() { + return new Material(); + } + }; + + public static final MdlxBlockDescriptor PARTICLE_EMITTER = new MdlxBlockDescriptor() { + @Override + public ParticleEmitter create() { + return new ParticleEmitter(); + } + }; + + public static final MdlxBlockDescriptor PARTICLE_EMITTER2 = new MdlxBlockDescriptor() { + @Override + public ParticleEmitter2 create() { + return new ParticleEmitter2(); + } + }; + + public static final MdlxBlockDescriptor RIBBON_EMITTER = new MdlxBlockDescriptor() { + @Override + public RibbonEmitter create() { + return new RibbonEmitter(); + } + }; + + public static final MdlxBlockDescriptor SEQUENCE = new MdlxBlockDescriptor() { + @Override + public Sequence create() { + return new Sequence(); + } + }; + + public static final MdlxBlockDescriptor TEXTURE = new MdlxBlockDescriptor() { + @Override + public Texture create() { + return new Texture(); + } + }; + + public static final MdlxBlockDescriptor TEXTURE_ANIMATION = new MdlxBlockDescriptor() { + @Override + public TextureAnimation create() { + return new TextureAnimation(); + } + }; +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/MdlxModel.java b/core/src/com/etheller/warsmash/parsers/mdlx/MdlxModel.java new file mode 100644 index 0000000..e5bc993 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/MdlxModel.java @@ -0,0 +1,642 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import com.badlogic.gdx.utils.StreamUtils; +import com.etheller.warsmash.parsers.mdlx.mdl.GhostwolfTokenInputStream; +import com.etheller.warsmash.parsers.mdlx.mdl.GhostwolfTokenOutputStream; +import com.etheller.warsmash.util.MdlUtils; +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 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 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 int version = 800; + private 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} + */ + private String animationFile = ""; + private final Extent extent = new Extent(); + private long blendTime = 0; + private final List sequences = new ArrayList(); + private final List globalSequences = new ArrayList<>(); + private final List materials = new ArrayList<>(); + private final List textures = new ArrayList<>(); + private final List textureAnimations = new ArrayList<>(); + private final List geosets = new ArrayList<>(); + private final List geosetAnimations = new ArrayList<>(); + private final List bones = new ArrayList<>(); + private final List lights = new ArrayList<>(); + private final List helpers = new ArrayList<>(); + private final List attachments = new ArrayList<>(); + private final List pivotPoints = new ArrayList<>(); + private final List particleEmitters = new ArrayList<>(); + private final List particleEmitters2 = new ArrayList<>(); + private final List ribbonEmitters = new ArrayList<>(); + private final List cameras = new ArrayList<>(); + private final List eventObjects = new ArrayList<>(); + private final List collisionShapes = new ArrayList<>(); + private final List unknownChunks = new ArrayList<>(); + + public MdlxModel(final InputStream buffer) throws IOException { + if (buffer != null) { + // In ghostwolf JS, this function called load() + // which decided whether the buffer was an MDL. + loadMdx(buffer); + } + } + + public void loadMdx(final InputStream buffer) throws IOException { + final LittleEndianDataInputStream stream = new LittleEndianDataInputStream(buffer); + if (Integer.reverseBytes(stream.readInt()) != MDLX) { + throw new IllegalStateException("WrongMagicNumber"); + } + + while (stream.available() > 0) { + final int tag = Integer.reverseBytes(stream.readInt()); + final long size = ParseUtils.readUInt32(stream); + + switch (tag) { + case VERS: + loadVersionChunk(stream); + break; + case MODL: + loadModelChunk(stream); + break; + case SEQS: + loadStaticObjects(this.sequences, MdlxBlockDescriptor.SEQUENCE, stream, size / 132); + break; + case GLBS: + loadGlobalSequenceChunk(stream, size); + break; + case MTLS: + loadDynamicObjects(this.materials, MdlxBlockDescriptor.MATERIAL, stream, size); + break; + case TEXS: + loadStaticObjects(this.textures, MdlxBlockDescriptor.TEXTURE, stream, size / 268); + break; + case TXAN: + loadDynamicObjects(this.textureAnimations, MdlxBlockDescriptor.TEXTURE_ANIMATION, stream, size); + break; + case GEOS: + loadDynamicObjects(this.geosets, MdlxBlockDescriptor.GEOSET, stream, size); + break; + case GEOA: + loadDynamicObjects(this.geosetAnimations, MdlxBlockDescriptor.GEOSET_ANIMATION, stream, size); + break; + case BONE: + loadDynamicObjects(this.bones, MdlxBlockDescriptor.BONE, stream, size); + break; + case LITE: + loadDynamicObjects(this.lights, MdlxBlockDescriptor.LIGHT, stream, size); + break; + case HELP: + loadDynamicObjects(this.helpers, MdlxBlockDescriptor.HELPER, stream, size); + break; + case ATCH: + loadDynamicObjects(this.attachments, MdlxBlockDescriptor.ATTACHMENT, stream, size); + break; + case PIVT: + loadPivotPointChunk(stream, size); + break; + case PREM: + loadDynamicObjects(this.particleEmitters, MdlxBlockDescriptor.PARTICLE_EMITTER, stream, size); + break; + case PRE2: + loadDynamicObjects(this.particleEmitters2, MdlxBlockDescriptor.PARTICLE_EMITTER2, stream, size); + break; + case RIBB: + loadDynamicObjects(this.ribbonEmitters, MdlxBlockDescriptor.RIBBON_EMITTER, stream, size); + break; + case CAMS: + loadDynamicObjects(this.cameras, MdlxBlockDescriptor.CAMERA, stream, size); + break; + case EVTS: + loadDynamicObjects(this.eventObjects, MdlxBlockDescriptor.EVENT_OBJECT, stream, size); + break; + case CLID: + loadDynamicObjects(this.collisionShapes, MdlxBlockDescriptor.COLLISION_SHAPE, stream, size); + break; + default: + this.unknownChunks.add(new UnknownChunk(stream, size, new War3ID(tag))); + } + } + + } + + private void loadVersionChunk(final LittleEndianDataInputStream stream) throws IOException { + this.version = (int) ParseUtils.readUInt32(stream); + } + + /** + * Restricts us to only be able to parse models on one thread at a time, in + * return for high performance. + */ + private static final byte[] NAME_BYTES_HEAP = new byte[80]; + private static final byte[] ANIMATION_FILE_BYTES_HEAP = new byte[260]; + + private void loadModelChunk(final LittleEndianDataInputStream stream) throws IOException { + this.name = ParseUtils.readString(stream, NAME_BYTES_HEAP); + this.animationFile = ParseUtils.readString(stream, ANIMATION_FILE_BYTES_HEAP); + this.extent.readMdx(stream); + this.blendTime = ParseUtils.readUInt32(stream); + } + + private void loadStaticObjects(final List out, final MdlxBlockDescriptor constructor, + final LittleEndianDataInputStream stream, final long count) throws IOException { + for (int i = 0; i < count; i++) { + final E object = constructor.create(); + + object.readMdx(stream); + + out.add(object); + } + } + + private void loadGlobalSequenceChunk(final LittleEndianDataInputStream stream, final long size) throws IOException { + for (long i = 0, l = size / 4; i < l; i++) { + this.globalSequences.add(ParseUtils.readUInt32(stream)); + } + } + + private void loadDynamicObjects(final List out, + final MdlxBlockDescriptor constructor, final LittleEndianDataInputStream stream, final long size) + throws IOException { + long totalSize = 0; + while (totalSize < size) { + final E object = constructor.create(); + + object.readMdx(stream); + + totalSize += object.getByteLength(); + + out.add(object); + } + } + + private void loadPivotPointChunk(final LittleEndianDataInputStream stream, final long size) throws IOException { + for (long i = 0, l = size / 12; i < l; i++) { + this.pivotPoints.add(ParseUtils.readFloatArray(stream, 3)); + } + } + + public void saveMdx(final OutputStream outputStream) throws IOException { + final LittleEndianDataOutputStream stream = new LittleEndianDataOutputStream(outputStream); + stream.writeInt(Integer.reverseBytes(MDLX)); + this.saveVersionChunk(stream); + this.saveModelChunk(stream); + this.saveStaticObjectChunk(stream, SEQS, this.sequences, 132); + this.saveGlobalSequenceChunk(stream); + this.saveDynamicObjectChunk(stream, MTLS, this.materials); + this.saveStaticObjectChunk(stream, TEXS, this.textures, 268); + this.saveDynamicObjectChunk(stream, TXAN, this.textureAnimations); + this.saveDynamicObjectChunk(stream, GEOS, this.geosets); + this.saveDynamicObjectChunk(stream, GEOA, this.geosetAnimations); + this.saveDynamicObjectChunk(stream, BONE, this.bones); + this.saveDynamicObjectChunk(stream, LITE, this.lights); + this.saveDynamicObjectChunk(stream, HELP, this.helpers); + this.saveDynamicObjectChunk(stream, ATCH, this.attachments); + this.savePivotPointChunk(stream); + this.saveDynamicObjectChunk(stream, PREM, this.particleEmitters); + this.saveDynamicObjectChunk(stream, PRE2, this.particleEmitters2); + this.saveDynamicObjectChunk(stream, RIBB, this.ribbonEmitters); + this.saveDynamicObjectChunk(stream, CAMS, this.cameras); + this.saveDynamicObjectChunk(stream, EVTS, this.eventObjects); + this.saveDynamicObjectChunk(stream, CLID, this.collisionShapes); + + for (final UnknownChunk chunk : this.unknownChunks) { + chunk.writeMdx(stream); + } + } + + private void saveVersionChunk(final LittleEndianDataOutputStream stream) throws IOException { + stream.writeInt(Integer.reverseBytes(VERS)); + ParseUtils.writeUInt32(stream, 4); + ParseUtils.writeUInt32(stream, this.version); + } + + private void saveModelChunk(final LittleEndianDataOutputStream stream) throws IOException { + stream.writeInt(Integer.reverseBytes(MODL)); + ParseUtils.writeUInt32(stream, 372); + final byte[] bytes = this.name.getBytes(ParseUtils.UTF8); + stream.write(bytes); + for (int i = 0; i < (NAME_BYTES_HEAP.length - bytes.length); i++) { + stream.write((byte) 0); + } + final byte[] animationFileBytes = this.animationFile.getBytes(ParseUtils.UTF8); + stream.write(animationFileBytes); + for (int i = 0; i < (ANIMATION_FILE_BYTES_HEAP.length - animationFileBytes.length); i++) { + stream.write((byte) 0); + } + this.extent.writeMdx(stream); + ParseUtils.writeUInt32(stream, this.blendTime); + } + + private void saveStaticObjectChunk(final LittleEndianDataOutputStream stream, final int name, + final List objects, final long size) throws IOException { + if (!objects.isEmpty()) { + stream.writeInt(Integer.reverseBytes(name)); + ParseUtils.writeUInt32(stream, objects.size() * size); + + for (final E object : objects) { + object.writeMdx(stream); + } + } + } + + private void saveGlobalSequenceChunk(final LittleEndianDataOutputStream stream) throws IOException { + if (!this.globalSequences.isEmpty()) { + stream.writeInt(Integer.reverseBytes(GLBS)); + ParseUtils.writeUInt32(stream, this.globalSequences.size() * 4); + + for (final Long globalSequence : this.globalSequences) { + ParseUtils.writeUInt32(stream, globalSequence); + } + } + } + + private void saveDynamicObjectChunk(final LittleEndianDataOutputStream stream, + final int name, final List objects) throws IOException { + if (!objects.isEmpty()) { + stream.writeInt(Integer.reverseBytes(name)); + ParseUtils.writeUInt32(stream, getObjectsByteLength(objects)); + + for (final E object : objects) { + object.writeMdx(stream); + } + } + } + + private void savePivotPointChunk(final LittleEndianDataOutputStream stream) throws IOException { + if (this.pivotPoints.size() > 0) { + stream.writeInt(Integer.reverseBytes(PIVT)); + ParseUtils.writeUInt32(stream, this.pivotPoints.size() * 12); + + for (final float[] pivotPoint : this.pivotPoints) { + ParseUtils.writeFloatArray(stream, pivotPoint); + } + } + } + + public void loadMdl(final InputStream inputStream) throws IOException { + final byte[] array = StreamUtils.copyStreamToByteArray(inputStream); + loadMdl(ByteBuffer.wrap(array)); + } + + public void loadMdl(final ByteBuffer inputStream) throws IOException { + String token; + final MdlTokenInputStream stream = new GhostwolfTokenInputStream(inputStream); + + while ((token = stream.read()) != null) { + switch (token) { + case MdlUtils.TOKEN_VERSION: + this.loadVersionBlock(stream); + break; + case MdlUtils.TOKEN_MODEL: + this.loadModelBlock(stream); + break; + case MdlUtils.TOKEN_SEQUENCES: + this.loadNumberedObjectBlock(this.sequences, MdlxBlockDescriptor.SEQUENCE, MdlUtils.TOKEN_ANIM, stream); + break; + case MdlUtils.TOKEN_GLOBAL_SEQUENCES: + this.loadGlobalSequenceBlock(stream); + break; + case MdlUtils.TOKEN_TEXTURES: + this.loadNumberedObjectBlock(this.textures, MdlxBlockDescriptor.TEXTURE, MdlUtils.TOKEN_BITMAP, stream); + break; + case MdlUtils.TOKEN_MATERIALS: + this.loadNumberedObjectBlock(this.materials, MdlxBlockDescriptor.MATERIAL, MdlUtils.TOKEN_MATERIAL, + stream); + break; + case MdlUtils.TOKEN_TEXTURE_ANIMS: + this.loadNumberedObjectBlock(this.textureAnimations, MdlxBlockDescriptor.TEXTURE_ANIMATION, + MdlUtils.TOKEN_TEXTURE_ANIM, stream); + break; + case MdlUtils.TOKEN_GEOSET: + this.loadObject(this.geosets, MdlxBlockDescriptor.GEOSET, stream); + break; + case MdlUtils.TOKEN_GEOSETANIM: + this.loadObject(this.geosetAnimations, MdlxBlockDescriptor.GEOSET_ANIMATION, stream); + break; + case MdlUtils.TOKEN_BONE: + this.loadObject(this.bones, MdlxBlockDescriptor.BONE, stream); + break; + case MdlUtils.TOKEN_LIGHT: + this.loadObject(this.lights, MdlxBlockDescriptor.LIGHT, stream); + break; + case MdlUtils.TOKEN_HELPER: + this.loadObject(this.helpers, MdlxBlockDescriptor.HELPER, stream); + break; + case MdlUtils.TOKEN_ATTACHMENT: + this.loadObject(this.attachments, MdlxBlockDescriptor.ATTACHMENT, stream); + break; + case MdlUtils.TOKEN_PIVOT_POINTS: + this.loadPivotPointBlock(stream); + break; + case MdlUtils.TOKEN_PARTICLE_EMITTER: + this.loadObject(this.particleEmitters, MdlxBlockDescriptor.PARTICLE_EMITTER, stream); + break; + case MdlUtils.TOKEN_PARTICLE_EMITTER2: + this.loadObject(this.particleEmitters2, MdlxBlockDescriptor.PARTICLE_EMITTER2, stream); + break; + case MdlUtils.TOKEN_RIBBON_EMITTER: + this.loadObject(this.ribbonEmitters, MdlxBlockDescriptor.RIBBON_EMITTER, stream); + break; + case MdlUtils.TOKEN_CAMERA: + this.loadObject(this.cameras, MdlxBlockDescriptor.CAMERA, stream); + break; + case MdlUtils.TOKEN_EVENT_OBJECT: + this.loadObject(this.eventObjects, MdlxBlockDescriptor.EVENT_OBJECT, stream); + break; + case MdlUtils.TOKEN_COLLISION_SHAPE: + this.loadObject(this.collisionShapes, MdlxBlockDescriptor.COLLISION_SHAPE, stream); + break; + default: + throw new IllegalStateException("Unsupported block: " + token); + } + } + } + + private void loadVersionBlock(final MdlTokenInputStream stream) { + for (final String token : stream.readBlock()) { + switch (token) { + case MdlUtils.TOKEN_FORMAT_VERSION: + this.version = stream.readInt(); + break; + default: + 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) + throws IOException { + 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); + + 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) throws IOException { + final E object = descriptor.create(); + + object.readMdl(stream); + + 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(); // } + } + + public void saveMdl(final OutputStream outputStream) throws IOException { + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream))) { + final MdlTokenOutputStream stream = new GhostwolfTokenOutputStream(writer); + this.saveVersionBlock(stream); + this.saveModelBlock(stream); + this.saveStaticObjectsBlock(stream, MdlUtils.TOKEN_SEQUENCES, this.sequences); + this.saveGlobalSequenceBlock(stream); + this.saveStaticObjectsBlock(stream, MdlUtils.TOKEN_TEXTURES, this.textures); + this.saveStaticObjectsBlock(stream, MdlUtils.TOKEN_MATERIALS, this.materials); + this.saveStaticObjectsBlock(stream, MdlUtils.TOKEN_TEXTURE_ANIMS, this.textureAnimations); + this.saveObjects(stream, this.geosets); + this.saveObjects(stream, this.geosetAnimations); + this.saveObjects(stream, this.bones); + this.saveObjects(stream, this.lights); + this.saveObjects(stream, this.helpers); + this.saveObjects(stream, this.attachments); + this.savePivotPointBlock(stream); + this.saveObjects(stream, this.particleEmitters); + this.saveObjects(stream, this.particleEmitters2); + this.saveObjects(stream, this.ribbonEmitters); + this.saveObjects(stream, this.cameras); + this.saveObjects(stream, this.eventObjects); + this.saveObjects(stream, this.collisionShapes); + } + } + + 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) throws IOException { + if (!objects.isEmpty()) { + stream.startBlock(name, objects.size()); + + for (final MdlxBlock object : objects) { + object.writeMdl(stream); + } + + 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) + throws IOException { + for (final MdlxBlock object : objects) { + object.writeMdl(stream); + } + } + + 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 long getByteLength() { + long size = 396; + + size += getStaticObjectsChunkByteLength(this.sequences, 132); + size += this.getStaticObjectsChunkByteLength(this.globalSequences, 4); + size += this.getDynamicObjectsChunkByteLength(this.materials); + size += this.getStaticObjectsChunkByteLength(this.textures, 268); + size += this.getDynamicObjectsChunkByteLength(this.textureAnimations); + size += this.getDynamicObjectsChunkByteLength(this.geosets); + size += this.getDynamicObjectsChunkByteLength(this.geosetAnimations); + size += this.getDynamicObjectsChunkByteLength(this.bones); + size += this.getDynamicObjectsChunkByteLength(this.lights); + size += this.getDynamicObjectsChunkByteLength(this.helpers); + size += this.getDynamicObjectsChunkByteLength(this.attachments); + size += this.getStaticObjectsChunkByteLength(this.pivotPoints, 12); + size += this.getDynamicObjectsChunkByteLength(this.particleEmitters); + size += this.getDynamicObjectsChunkByteLength(this.particleEmitters2); + size += this.getDynamicObjectsChunkByteLength(this.ribbonEmitters); + size += this.getDynamicObjectsChunkByteLength(this.cameras); + size += this.getDynamicObjectsChunkByteLength(this.eventObjects); + size += this.getDynamicObjectsChunkByteLength(this.collisionShapes); + size += this.getObjectsByteLength(this.unknownChunks); + + return size; + } + + private long getObjectsByteLength(final List objects) { + long size = 0; + for (final E object : objects) { + size += object.getByteLength(); + } + return size; + } + + private long getDynamicObjectsChunkByteLength(final List objects) { + if (!objects.isEmpty()) { + return 8 + this.getObjectsByteLength(objects); + } + + return 0; + } + + private long getStaticObjectsChunkByteLength(final List objects, final long size) { + if (!objects.isEmpty()) { + return 8 + (objects.size() * size); + } + + return 0; + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/MdlxTest.java b/core/src/com/etheller/warsmash/parsers/mdlx/MdlxTest.java new file mode 100644 index 0000000..ca2e8da --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/MdlxTest.java @@ -0,0 +1,64 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +public class MdlxTest { + + public static void main(final String[] args) { + try (FileInputStream stream = new FileInputStream( + new File("C:\\Users\\micro\\OneDrive\\Documents\\Warcraft III\\Models\\ArcaneEpic13.mdx"))) { + final MdlxModel model = new MdlxModel(stream); + try (FileOutputStream mdlStream = new FileOutputStream(new File( + "C:\\Users\\micro\\OneDrive\\Documents\\Warcraft III\\Models\\Test\\MyOutAutomated.mdl"))) { + + model.saveMdl(mdlStream); + } + } + catch (final FileNotFoundException e) { + e.printStackTrace(); + } + catch (final IOException e) { + e.printStackTrace(); + } + + System.out.println("Created MDL, now reparsing to MDX"); + + try (FileInputStream stream = new FileInputStream( + new File("C:\\Users\\micro\\OneDrive\\Documents\\Warcraft III\\Models\\Test\\MyOutAutomated.mdl"))) { + final MdlxModel model = new MdlxModel(null); + model.loadMdl(stream); + try (FileOutputStream mdlStream = new FileOutputStream(new File( + "C:\\Users\\micro\\OneDrive\\Documents\\Warcraft III\\Models\\Test\\MyOutAutomatedMDX.mdx"))) { + + model.saveMdx(mdlStream); + } + } + catch (final FileNotFoundException e) { + e.printStackTrace(); + } + catch (final IOException e) { + e.printStackTrace(); + } + + try (FileInputStream stream = new FileInputStream( + new File("C:\\Users\\micro\\OneDrive\\Documents\\Warcraft III\\Models\\Test\\MyOutAutomatedMDX.mdx"))) { + final MdlxModel model = new MdlxModel(stream); + try (FileOutputStream mdlStream = new FileOutputStream(new File( + "C:\\Users\\micro\\OneDrive\\Documents\\Warcraft III\\Models\\Test\\MyOutAutomatedMDXBack2MDL.mdl"))) { + + model.saveMdl(mdlStream); + } + } + catch (final FileNotFoundException e) { + e.printStackTrace(); + } + catch (final IOException e) { + e.printStackTrace(); + } + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/ParticleEmitter.java b/core/src/com/etheller/warsmash/parsers/mdlx/ParticleEmitter.java new file mode 100644 index 0000000..37d00c5 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/ParticleEmitter.java @@ -0,0 +1,193 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; +import java.util.Iterator; + +import com.etheller.warsmash.util.MdlUtils; +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class ParticleEmitter extends GenericObject { + private float emissionRate = 0; + private float gravity = 0; + private float longitude = 0; + private float latitude = 0; + private String path = ""; + private float lifeSpan = 0; + private float speed = 0; + + public ParticleEmitter() { + super(0x1000); + } + + /** + * Restricts us to only be able to parse models on one thread at a time, in + * return for high performance. + */ + private static final byte[] PATH_BYTES_HEAP = new byte[260]; + + @Override + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + final long size = ParseUtils.readUInt32(stream); + + super.readMdx(stream); + + this.emissionRate = stream.readFloat(); + this.gravity = stream.readFloat(); + this.longitude = stream.readFloat(); + this.latitude = stream.readFloat(); + this.path = ParseUtils.readString(stream, PATH_BYTES_HEAP); + this.lifeSpan = stream.readFloat(); + this.speed = stream.readFloat(); + + readTimelines(stream, size - this.getByteLength()); + } + + @Override + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, getByteLength()); + + super.writeMdx(stream); + + stream.writeFloat(this.emissionRate); + stream.writeFloat(this.gravity); + stream.writeFloat(this.longitude); + stream.writeFloat(this.latitude); + final byte[] bytes = this.path.getBytes(ParseUtils.UTF8); + stream.write(bytes); + for (int i = 0; i < (PATH_BYTES_HEAP.length - bytes.length); i++) { + stream.write((byte) 0); + } + stream.writeFloat(this.lifeSpan); + stream.writeFloat(this.speed); + + writeNonGenericAnimationChunks(stream); + } + + @Override + public void readMdl(final MdlTokenInputStream stream) throws IOException { + 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 IllegalStateException( + "Unknown token in ParticleEmitter " + this.name + "'s Particle: " + subToken); + } + } + break; + default: + throw new IllegalStateException("Unknown token in ParticleEmitter " + this.name + ": " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream) throws IOException { + 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 (!this.writeTimeline(stream, AnimationMap.KPEE)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_EMISSION_RATE, this.emissionRate); + } + + if (!this.writeTimeline(stream, AnimationMap.KPEG)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_GRAVITY, this.gravity); + } + + if (!this.writeTimeline(stream, AnimationMap.KPLN)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_LONGITUDE, this.longitude); + } + + if (!this.writeTimeline(stream, AnimationMap.KPLT)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_LATITUDE, this.latitude); + } + + this.writeTimeline(stream, AnimationMap.KPEV); + + stream.startBlock(MdlUtils.TOKEN_PARTICLE); + + if (!this.writeTimeline(stream, AnimationMap.KPEL)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_LIFE_SPAN, this.lifeSpan); + } + + if (!this.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() { + return 288 + super.getByteLength(); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/ParticleEmitter2.java b/core/src/com/etheller/warsmash/parsers/mdlx/ParticleEmitter2.java new file mode 100644 index 0000000..dd321be --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/ParticleEmitter2.java @@ -0,0 +1,425 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; + +import com.etheller.warsmash.util.MdlUtils; +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class ParticleEmitter2 extends GenericObject { + // 0: blend + // 1: additive + // 2: modulate + // 3: modulate 2x + // 4: alphakey + public static enum FilterMode { + BLEND("Blend"), + ADDITIVE("Additive"), + MODULATE("Modulate"), + MODULATE2X("Modulate2x"), + ALPHAKEY("AlphaKey"); + + String mdlText; + + FilterMode(final String str) { + this.mdlText = str; + } + + public String getMdlText() { + return this.mdlText; + } + + public static FilterMode fromId(final int id) { + return values()[id]; + } + + public static int nameToId(final String name) { + for (final FilterMode mode : values()) { + if (mode.getMdlText().equals(name)) { + return mode.ordinal(); + } + } + return -1; + } + + @Override + public String toString() { + return getMdlText(); + } + } + + private float speed = 0; + private float variation = 0; + private float latitude = 0; + private float gravity = 0; + private float lifeSpan = 0; + private float emissionRate = 0; + private float length; + private float width; + private FilterMode filterMode = FilterMode.BLEND; + private long rows = 0; + private long columns = 0; + private long headOrTail = 0; + private float tailLength = 0; + private float timeMiddle = 0; + private final float[][] segmentColors = new float[3][3]; + private final short[] segmentAlphas = new short[3]; // unsigned byte[] + private final float[] segmentScaling = new float[3]; + private final long[][] headIntervals = new long[2][3]; + private final long[][] tailIntervals = new long[2][3]; + private int textureId = -1; + private long squirt = 0; + private int priorityPlane = 0; + private long replaceableId = 0; + + public ParticleEmitter2() { + super(0x1000); + } + + @Override + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + final long size = ParseUtils.readUInt32(stream); + + super.readMdx(stream); + + this.speed = stream.readFloat(); + this.variation = stream.readFloat(); + this.latitude = stream.readFloat(); + this.gravity = stream.readFloat(); + this.lifeSpan = stream.readFloat(); + this.emissionRate = stream.readFloat(); + this.length = stream.readFloat(); + this.width = stream.readFloat(); + this.filterMode = FilterMode.fromId((int) (ParseUtils.readUInt32(stream))); + this.rows = ParseUtils.readUInt32(stream); + this.columns = ParseUtils.readUInt32(stream); + this.headOrTail = ParseUtils.readUInt32(stream); + this.tailLength = stream.readFloat(); + this.timeMiddle = stream.readFloat(); + ParseUtils.readFloatArray(stream, this.segmentColors[0]); + ParseUtils.readFloatArray(stream, this.segmentColors[1]); + ParseUtils.readFloatArray(stream, this.segmentColors[2]); + ParseUtils.readUInt8Array(stream, this.segmentAlphas); + ParseUtils.readFloatArray(stream, this.segmentScaling); + ParseUtils.readUInt32Array(stream, this.headIntervals[0]); + ParseUtils.readUInt32Array(stream, this.headIntervals[1]); + ParseUtils.readUInt32Array(stream, this.tailIntervals[0]); + ParseUtils.readUInt32Array(stream, this.tailIntervals[1]); + this.textureId = stream.readInt(); + this.squirt = ParseUtils.readUInt32(stream); + this.priorityPlane = stream.readInt(); + this.replaceableId = ParseUtils.readUInt32(stream); + + readTimelines(stream, size - this.getByteLength()); + } + + @Override + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, getByteLength()); + + super.writeMdx(stream); + + stream.writeFloat(this.speed); + stream.writeFloat(this.variation); + stream.writeFloat(this.latitude); + stream.writeFloat(this.gravity); + stream.writeFloat(this.lifeSpan); + stream.writeFloat(this.emissionRate); + stream.writeFloat(this.length); + stream.writeFloat(this.width); + ParseUtils.writeUInt32(stream, this.filterMode.ordinal()); + ParseUtils.writeUInt32(stream, this.rows); + ParseUtils.writeUInt32(stream, this.columns); + ParseUtils.writeUInt32(stream, this.headOrTail); + stream.writeFloat(this.tailLength); + stream.writeFloat(this.timeMiddle); + ParseUtils.writeFloatArray(stream, this.segmentColors[0]); + ParseUtils.writeFloatArray(stream, this.segmentColors[1]); + ParseUtils.writeFloatArray(stream, this.segmentColors[2]); + ParseUtils.writeUInt8Array(stream, this.segmentAlphas); + ParseUtils.writeFloatArray(stream, this.segmentScaling); + ParseUtils.writeUInt32Array(stream, this.headIntervals[0]); + ParseUtils.writeUInt32Array(stream, this.headIntervals[1]); + ParseUtils.writeUInt32Array(stream, this.tailIntervals[0]); + ParseUtils.writeUInt32Array(stream, this.tailIntervals[1]); + stream.writeInt(this.textureId); + ParseUtils.writeUInt32(stream, this.squirt); + stream.writeInt(this.priorityPlane); + ParseUtils.writeUInt32(stream, this.replaceableId); + + writeNonGenericAnimationChunks(stream); + } + + @Override + public void readMdl(final MdlTokenInputStream stream) throws IOException { + 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 = 0; + break; + case MdlUtils.TOKEN_TAIL: + this.headOrTail = 1; + break; + case MdlUtils.TOKEN_BOTH: + this.headOrTail = 2; + 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 IllegalStateException("Unknown token in ParticleEmitter2 " + this.name + ": " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream) throws IOException { + 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 (!this.writeTimeline(stream, AnimationMap.KP2S)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_SPEED, this.speed); + } + + if (!this.writeTimeline(stream, AnimationMap.KP2R)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_VARIATION, this.variation); + } + + if (!this.writeTimeline(stream, AnimationMap.KP2L)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_LATITUDE, this.latitude); + } + + if (!this.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 (!this.writeTimeline(stream, AnimationMap.KP2E)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_EMISSION_RATE, this.emissionRate); + } + + if (!this.writeTimeline(stream, AnimationMap.KP2W)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_WIDTH, this.width); + } + + if (!this.writeTimeline(stream, AnimationMap.KP2N)) { + stream.writeFloatAttrib(MdlUtils.TOKEN_STATIC_LENGTH, this.length); + } + + stream.writeFlag(this.filterMode.getMdlText()); + + stream.writeAttribUInt32(MdlUtils.TOKEN_ROWS, this.rows); + stream.writeAttribUInt32(MdlUtils.TOKEN_COLUMNS, this.columns); + + switch ((int) this.headOrTail) { + case 0: + stream.writeFlag(MdlUtils.TOKEN_HEAD); + break; + case 1: + stream.writeFlag(MdlUtils.TOKEN_TAIL); + break; + case 2: + stream.writeFlag(MdlUtils.TOKEN_BOTH); + break; + default: + throw new IllegalStateException("Bad headOrTail value when saving MDL: " + this.headOrTail); + } + + 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() { + return 175 + super.getByteLength(); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/RibbonEmitter.java b/core/src/com/etheller/warsmash/parsers/mdlx/RibbonEmitter.java new file mode 100644 index 0000000..f5a756a --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/RibbonEmitter.java @@ -0,0 +1,176 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; + +import com.etheller.warsmash.util.MdlUtils; +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class RibbonEmitter extends GenericObject { + private float heightAbove = 0; + private float heightBelow = 0; + private float alpha = 0; + private final float[] color = new float[3]; + private float lifeSpan = 0; + private long textureSlot = 0; + private long emissionRate = 0; + private long rows = 0; + private long columns = 0; + private int materialId = 0; + private float gravity = 0; + + public RibbonEmitter() { + super(0x4000); + } + + @Override + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + final long size = ParseUtils.readUInt32(stream); + + super.readMdx(stream); + + this.heightAbove = stream.readFloat(); + this.heightBelow = stream.readFloat(); + this.alpha = stream.readFloat(); + ParseUtils.readFloatArray(stream, this.color); + this.lifeSpan = stream.readFloat(); + this.textureSlot = ParseUtils.readUInt32(stream); + this.emissionRate = ParseUtils.readUInt32(stream); + this.rows = ParseUtils.readUInt32(stream); + this.columns = ParseUtils.readUInt32(stream); + this.materialId = stream.readInt(); + this.gravity = stream.readFloat(); + + readTimelines(stream, size - getByteLength()); + } + + @Override + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, getByteLength()); + + super.writeMdx(stream); + + stream.writeFloat(this.heightAbove); + stream.writeFloat(this.heightBelow); + stream.writeFloat(this.alpha); + ParseUtils.writeFloatArray(stream, this.color); + stream.writeFloat(this.lifeSpan); + ParseUtils.writeUInt32(stream, this.textureSlot); + ParseUtils.writeUInt32(stream, this.emissionRate); + ParseUtils.writeUInt32(stream, this.rows); + ParseUtils.writeUInt32(stream, this.columns); + stream.writeInt(this.materialId); + stream.writeFloat(this.gravity); + + writeNonGenericAnimationChunks(stream); + } + + @Override + public void readMdl(final MdlTokenInputStream stream) throws IOException { + 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 IllegalStateException("Unknown token in RibbonEmitter " + this.name + ": " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream) throws IOException { + 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() { + return 56 + super.getByteLength(); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/Sequence.java b/core/src/com/etheller/warsmash/parsers/mdlx/Sequence.java new file mode 100644 index 0000000..18f2b1a --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/Sequence.java @@ -0,0 +1,104 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; + +import com.etheller.warsmash.util.MdlUtils; +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class Sequence implements MdlxBlock { + private String name = ""; + private final long[] interval = new long[2]; + private float moveSpeed = 0; + private int flags = 0; + private float rarity = 0; + private long syncPoint = 0; + private final Extent extent = new Extent(); + + /** + * Restricts us to only be able to parse models on one thread at a time, in + * return for high performance. + */ + private static final byte[] NAME_BYTES_HEAP = new byte[80]; + + @Override + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + this.name = ParseUtils.readString(stream, NAME_BYTES_HEAP); + ParseUtils.readUInt32Array(stream, this.interval); + this.moveSpeed = stream.readFloat(); + this.flags = (int) ParseUtils.readUInt32(stream); + this.rarity = stream.readFloat(); + this.syncPoint = ParseUtils.readUInt32(stream); + this.extent.readMdx(stream); + } + + @Override + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + final byte[] bytes = this.name.getBytes(ParseUtils.UTF8); + stream.write(bytes); + for (int i = 0; i < (NAME_BYTES_HEAP.length - bytes.length); i++) { + stream.write((byte) 0); + } + ParseUtils.writeUInt32Array(stream, this.interval); + stream.writeFloat(this.moveSpeed); + ParseUtils.writeUInt32(stream, this.flags); + stream.writeFloat(this.rarity); + ParseUtils.writeUInt32(stream, this.syncPoint); + this.extent.writeMdx(stream); + } + + @Override + public void readMdl(final MdlTokenInputStream stream) { + 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) { + 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(); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/Texture.java b/core/src/com/etheller/warsmash/parsers/mdlx/Texture.java new file mode 100644 index 0000000..68ea94b --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/Texture.java @@ -0,0 +1,81 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; + +import com.etheller.warsmash.util.MdlUtils; +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class Texture implements MdlxBlock { + private int replaceableId = 0; + private String path = ""; + private int flags = 0; + + /** + * Restricts us to only be able to parse models on one thread at a time, in + * return for high performance. + */ + private static final byte[] PATH_BYTES_HEAP = new byte[260]; + + @Override + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + this.replaceableId = (int) ParseUtils.readUInt32(stream); + this.path = ParseUtils.readString(stream, PATH_BYTES_HEAP); + this.flags = (int) ParseUtils.readUInt32(stream); + } + + @Override + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, this.replaceableId); + final byte[] bytes = this.path.getBytes(ParseUtils.UTF8); + stream.write(bytes); + for (int i = 0; i < (PATH_BYTES_HEAP.length - bytes.length); i++) { + stream.write((byte) 0); + } + ParseUtils.writeUInt32(stream, this.flags); + } + + @Override + public void readMdl(final MdlTokenInputStream stream) throws IOException { + 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.flags |= 0x1; + break; + case MdlUtils.TOKEN_WRAP_HEIGHT: + this.flags |= 0x2; + break; + default: + throw new IllegalStateException("Unknown token in Texture: " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream) throws IOException { + 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.flags & 0x1) != 0) { + stream.writeFlag(MdlUtils.TOKEN_WRAP_WIDTH); + } + + if ((this.flags & 0x2) != 0) { + stream.writeFlag(MdlUtils.TOKEN_WRAP_HEIGHT); + } + + stream.endBlock(); + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/TextureAnimation.java b/core/src/com/etheller/warsmash/parsers/mdlx/TextureAnimation.java new file mode 100644 index 0000000..a5b55c1 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/TextureAnimation.java @@ -0,0 +1,57 @@ +package com.etheller.warsmash.parsers.mdlx; + +import java.io.IOException; + +import com.etheller.warsmash.util.MdlUtils; +import com.etheller.warsmash.util.ParseUtils; +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; + +public class TextureAnimation extends AnimatedObject { + + @Override + public void readMdx(final LittleEndianDataInputStream stream) throws IOException { + final long size = ParseUtils.readUInt32(stream); + this.readTimelines(stream, size - 4); + } + + @Override + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeUInt32(stream, this.getByteLength()); + this.writeTimelines(stream); + } + + @Override + public void readMdl(final MdlTokenInputStream stream) throws IOException { + for (final String token : stream.readBlock()) { + switch (token) { + case MdlUtils.TOKEN_TRANSLATION: + this.readTimeline(stream, AnimationMap.KTAT); + break; + case MdlUtils.TOKEN_ROTATION: + this.readTimeline(stream, AnimationMap.KTAR); + break; + case MdlUtils.TOKEN_SCALING: + this.readTimeline(stream, AnimationMap.KTAS); + break; + default: + throw new IllegalStateException("Unknown token in TextureAnimation: " + token); + } + } + } + + @Override + public void writeMdl(final MdlTokenOutputStream stream) throws IOException { + stream.startBlock(MdlUtils.TOKEN_TVERTEX_ANIM_SPACE); + this.writeTimeline(stream, AnimationMap.KTAT); + this.writeTimeline(stream, AnimationMap.KTAR); + this.writeTimeline(stream, AnimationMap.KTAS); + stream.endBlock(); + } + + @Override + public long getByteLength() { + return 4 + super.getByteLength(); + } + +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/UnknownChunk.java b/core/src/com/etheller/warsmash/parsers/mdlx/UnknownChunk.java new file mode 100644 index 0000000..a46fdcb --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/UnknownChunk.java @@ -0,0 +1,35 @@ +package com.etheller.warsmash.parsers.mdlx; + +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 UnknownChunk implements Chunk { + private final short[] chunk; + private final War3ID tag; + + public UnknownChunk(final LittleEndianDataInputStream stream, final long size, final War3ID tag) + throws IOException { + System.err.println("Loading unknown chunk: " + tag); + this.chunk = ParseUtils.readUInt8Array(stream, (int) size); + this.tag = tag; + } + + public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { + ParseUtils.writeWar3ID(stream, this.tag); + // 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 + ParseUtils.writeUInt32(stream, this.chunk.length * Byte.BYTES); + ParseUtils.writeUInt8Array(stream, this.chunk); + } + + @Override + public long getByteLength() { + return 8 + (this.chunk.length * Byte.BYTES); + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/mdl/GhostwolfTokenInputStream.java b/core/src/com/etheller/warsmash/parsers/mdlx/mdl/GhostwolfTokenInputStream.java new file mode 100644 index 0000000..21076c5 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/mdl/GhostwolfTokenInputStream.java @@ -0,0 +1,232 @@ +package com.etheller.warsmash.parsers.mdlx.mdl; + +import java.nio.ByteBuffer; +import java.util.Iterator; + +import com.etheller.warsmash.parsers.mdlx.MdlTokenInputStream; + +public class GhostwolfTokenInputStream implements MdlTokenInputStream { + private final ByteBuffer buffer; + private int index; + private final int ident; + private final int fractionDigits; + + public GhostwolfTokenInputStream(final ByteBuffer buffer) { + this.buffer = buffer; + this.index = 0; + this.ident = 0; // Used for writing blocks nicely. + this.fractionDigits = 6; // The number of fraction digits when writing floats. + } + + @Override + 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; + } + + @Override + public String peek() { + final int index = this.index; + final String value = this.read(); + + this.index = index; + return value; + } + + @Override + public long readUInt32() { + return Long.parseLong(this.read()); + } + + @Override + public int readInt() { + return Integer.parseInt(this.read()); + } + + @Override + public float readFloat() { + return Float.parseFloat(this.read()); + } + + @Override + public void readIntArray(final long[] values) { + this.read(); // { + + for (int i = 0, l = values.length; i < l; i++) { + values[i] = this.readInt(); + } + + this.read(); // } + } + + @Override + public float[] readFloatArray(final float[] values) { + this.read(); // { + + for (int i = 0, l = values.length; i < l; i++) { + values[i] = this.readFloat(); + } + + this.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 {Float32Array|Uint32Array} value + */ + @Override + public void readKeyframe(final float[] values) { + if (values.length == 1) { + values[0] = this.readFloat(); + } + else { + this.readFloatArray(values); + } + } + + @Override + public float[] readVectorArray(final float[] array, final int vectorLength) { + this.read(); // { + + for (int i = 0, l = array.length / vectorLength; i < l; i++) { + this.read(); // { + + for (int j = 0; j < vectorLength; j++) { + array[(i * vectorLength) + j] = this.readFloat(); + } + + this.read(); // } + } + + this.read(); // } + return array; + } + + @Override + public Iterable readBlock() { + this.read(); // { + return new Iterable() { + @Override + public Iterator iterator() { + 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("}"); + } + }; + } + }; + } + + @Override + public int[] readUInt16Array(final int[] values) { + this.read(); // { + + for (int i = 0, l = values.length; i < l; i++) { + values[i] = this.readInt(); + } + + this.read(); // } + + return values; + } + + @Override + public short[] readUInt8Array(final short[] values) { + this.read(); // { + + for (int i = 0, l = values.length; i < l; i++) { + values[i] = Short.parseShort(this.read()); + } + + this.read(); // } + + return values; + } + + @Override + public void readColor(final float[] color) { + this.read(); // { + + color[2] = this.readFloat(); + color[1] = this.readFloat(); + color[0] = this.readFloat(); + + this.read(); // } + } +} diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/mdl/GhostwolfTokenOutputStream.java b/core/src/com/etheller/warsmash/parsers/mdlx/mdl/GhostwolfTokenOutputStream.java new file mode 100644 index 0000000..d154b20 --- /dev/null +++ b/core/src/com/etheller/warsmash/parsers/mdlx/mdl/GhostwolfTokenOutputStream.java @@ -0,0 +1,261 @@ +package com.etheller.warsmash.parsers.mdlx.mdl; + +import java.io.IOException; + +import com.etheller.warsmash.parsers.mdlx.MdlTokenOutputStream; + +public class GhostwolfTokenOutputStream implements MdlTokenOutputStream { + private final Appendable buffer; + private final int index; + private int ident; + private final int fractionDigits; + + public GhostwolfTokenOutputStream(final Appendable appendable) { + this.buffer = appendable; + this.index = 0; + this.ident = 0; // Used for writing blocks nicely. + this.fractionDigits = 6; // The number of fraction digits when writing floats. + } + + @Override + public void writeKeyframe(final String prefix, final long uInt32Value) { + writeAttribUInt32(prefix, uInt32Value); + } + + @Override + public void writeKeyframe(final String prefix, final float floatValue) { + writeFloatAttrib(prefix, floatValue); + } + + @Override + public void writeKeyframe(final String prefix, final float[] floatArrayValues) { + writeFloatArrayAttrib(prefix, floatArrayValues); + } + + @Override + public void indent() { + this.ident += 1; + } + + @Override + public void unindent() { + this.ident -= 1; + } + + @Override + public void startObjectBlock(final String name, final String objectName) { + this.writeLine(name + " \"" + objectName + "\" {"); + this.ident += 1; + } + + @Override + public void startBlock(final String name, final int blockSize) { + this.writeLine(name + " " + blockSize + " {" + ""); + this.ident += 1; + } + + @Override + public void startBlock(final String name) { + this.writeLine(name + " {" + ""); + this.ident += 1; + } + + @Override + public void writeFlag(final String token) { + this.writeLine(token + ","); + } + + @Override + public void writeFlagUInt32(final long flag) { + this.writeLine(flag + ","); + } + + @Override + public void writeAttrib(final String string, final int globalSequenceId) { + writeLine(string + " " + globalSequenceId + ","); + } + + @Override + public void writeAttribUInt32(final String attribName, final long uInt) { + writeLine(attribName + " " + uInt + ","); + } + + @Override + public void writeAttrib(final String string, final String value) { + writeLine(string + " " + value + ","); + } + + @Override + public void writeFloatAttrib(final String attribName, final float value) { + writeLine(attribName + " " + value + ","); + } + + @Override + public void writeStringAttrib(final String attribName, final String value) { + writeLine(attribName + " \"" + value + "\","); + + } + + @Override + public void writeFloatArrayAttrib(final String attribName, final float[] floatArray) { + this.writeLine(attribName + " { " + formatFloatArray(floatArray) + " },"); + } + + @Override + public void writeLongSubArrayAttrib(final String attribName, final long[] array, final int startIndexInclusive, + final int endIndexExclusive) { + this.writeLine(attribName + " { " + formatLongSubArray(array, startIndexInclusive, endIndexExclusive) + " },"); + } + + @Override + public void writeFloatArray(final float[] floatArray) { + this.writeLine("{ " + formatFloatArray(floatArray) + " },"); + } + + public void writeFloatSubArray(final float[] floatArray, final int startIndexInclusive, + final int endIndexExclusive) { + this.writeLine("{ " + formatFloatSubArray(floatArray, startIndexInclusive, endIndexExclusive) + " },"); + } + + @Override + public void writeVectorArray(final String token, final float[] vectors, final int vectorLength) { + this.startBlock(token, vectors.length / vectorLength); + + for (int i = 0, l = vectors.length; i < l; i += vectorLength) { + this.writeFloatSubArray(vectors, i, i + vectorLength); + } + + this.endBlock(); + } + + @Override + public void endBlock() { + this.ident -= 1; + this.writeLine("}"); + } + + @Override + public void endBlockComma() { + this.ident -= 1; + this.writeLine("},"); + } + + @Override + public void writeLine(final String string) { + try { + for (int i = 0; i < this.ident; i++) { + this.buffer.append('\t'); + } + this.buffer.append(string); + this.buffer.append('\n'); + } + catch (final IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void startBlock(final String tokenFaces, final int sizeNumberProbably, final int length) { + this.writeLine(tokenFaces + " " + sizeNumberProbably + " " + length + " {" + ""); + this.ident += 1; + } + + @Override + public void writeColor(final String tokenStaticColor, final float[] color) { + this.writeLine(tokenStaticColor + " { " + color[2] + ", " + color[1] + ", " + color[0] + " },"); + } + + @Override + public void writeArrayAttrib(final String tokenAlpha, final short[] uint8Array) { + this.writeLine(tokenAlpha + " { " + formatShortArray(uint8Array) + " },"); + } + + @Override + public void writeArrayAttrib(final String tokenAlpha, final int[] uint16Array) { + this.writeLine(tokenAlpha + " { " + formatIntArray(uint16Array) + " },"); + } + + @Override + public void writeArrayAttrib(final String tokenAlpha, final long[] uint32Array) { + this.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 (int i = 0, l = value.length; i < l; i++) { + if (stringBuilder.length() > 0) { + stringBuilder.append(", "); + } + stringBuilder.append(formatFloat(value[i])); + } + return stringBuilder.toString(); + } + + private String formatLongArray(final long[] value) { + final StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0, l = value.length; i < l; i++) { + if (stringBuilder.length() > 0) { + stringBuilder.append(", "); + } + stringBuilder.append(value[i]); + } + return stringBuilder.toString(); + } + + private String formatShortArray(final short[] value) { + final StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0, l = value.length; i < l; i++) { + if (stringBuilder.length() > 0) { + stringBuilder.append(", "); + } + stringBuilder.append(value[i]); + } + return stringBuilder.toString(); + } + + private String formatIntArray(final int[] value) { + final StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0, l = value.length; i < l; i++) { + if (stringBuilder.length() > 0) { + stringBuilder.append(", "); + } + stringBuilder.append(value[i]); + } + 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/etheller/warsmash/parsers/mdlx/timeline/FloatArrayKeyFrame.java b/core/src/com/etheller/warsmash/parsers/mdlx/timeline/FloatArrayKeyFrame.java index 3af812e..41120ba 100644 --- a/core/src/com/etheller/warsmash/parsers/mdlx/timeline/FloatArrayKeyFrame.java +++ b/core/src/com/etheller/warsmash/parsers/mdlx/timeline/FloatArrayKeyFrame.java @@ -39,7 +39,7 @@ public class FloatArrayKeyFrame implements KeyFrame { @Override public void readMdx(final LittleEndianDataInputStream stream, final InterpolationType interpolationType) throws IOException { - this.time = ParseUtils.parseUInt32(stream); + this.time = ParseUtils.readUInt32(stream); ParseUtils.readFloatArray(stream, this.value); if (interpolationType.tangential()) { ParseUtils.readFloatArray(stream, this.inTan); diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/timeline/FloatKeyFrame.java b/core/src/com/etheller/warsmash/parsers/mdlx/timeline/FloatKeyFrame.java index b03788b..5bb3254 100644 --- a/core/src/com/etheller/warsmash/parsers/mdlx/timeline/FloatKeyFrame.java +++ b/core/src/com/etheller/warsmash/parsers/mdlx/timeline/FloatKeyFrame.java @@ -33,7 +33,7 @@ public class FloatKeyFrame implements KeyFrame { @Override public void readMdx(final LittleEndianDataInputStream stream, final InterpolationType interpolationType) throws IOException { - this.time = ParseUtils.parseUInt32(stream); + this.time = ParseUtils.readUInt32(stream); this.value = stream.readFloat(); if (interpolationType.tangential()) { this.inTan = stream.readFloat(); diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/timeline/Timeline.java b/core/src/com/etheller/warsmash/parsers/mdlx/timeline/Timeline.java index 8f51d85..97d8f77 100644 --- a/core/src/com/etheller/warsmash/parsers/mdlx/timeline/Timeline.java +++ b/core/src/com/etheller/warsmash/parsers/mdlx/timeline/Timeline.java @@ -32,7 +32,7 @@ public abstract class Timeline implements Chunk { public void readMdx(final LittleEndianDataInputStream stream, final War3ID name) throws IOException { this.name = name; - final long keyFrameCount = ParseUtils.parseUInt32(stream); + final long keyFrameCount = ParseUtils.readUInt32(stream); this.interpolationType = InterpolationType.VALUES[stream.readInt()]; this.globalSequenceId = stream.readInt(); @@ -47,7 +47,7 @@ public abstract class Timeline implements Chunk { } public void writeMdx(final LittleEndianDataOutputStream stream) throws IOException { - stream.writeInt(this.name.getValue()); + stream.writeInt(Integer.reverseBytes(this.name.getValue())); stream.writeInt(this.keyFrames.size()); stream.writeInt(this.interpolationType.ordinal()); stream.writeInt(this.globalSequenceId); @@ -86,7 +86,7 @@ public abstract class Timeline implements Chunk { this.interpolationType = interpolationType; - if (stream.peek().equals("GlobalSeqId")) { + if (stream.peek().equals(MdlUtils.TOKEN_GLOBAL_SEQ_ID)) { stream.read(); this.globalSequenceId = stream.readInt(); } @@ -130,7 +130,7 @@ public abstract class Timeline implements Chunk { stream.writeFlag(token); if (this.globalSequenceId != -1) { - stream.writeAttrib("GlobalSeqId", this.globalSequenceId); + stream.writeAttrib(MdlUtils.TOKEN_GLOBAL_SEQ_ID, this.globalSequenceId); } for (final KeyFrame keyFrame : this.keyFrames) { diff --git a/core/src/com/etheller/warsmash/parsers/mdlx/timeline/UInt32KeyFrame.java b/core/src/com/etheller/warsmash/parsers/mdlx/timeline/UInt32KeyFrame.java index 0f74c21..a858378 100644 --- a/core/src/com/etheller/warsmash/parsers/mdlx/timeline/UInt32KeyFrame.java +++ b/core/src/com/etheller/warsmash/parsers/mdlx/timeline/UInt32KeyFrame.java @@ -33,11 +33,11 @@ public class UInt32KeyFrame implements KeyFrame { @Override public void readMdx(final LittleEndianDataInputStream stream, final InterpolationType interpolationType) throws IOException { - this.time = ParseUtils.parseUInt32(stream); - this.value = ParseUtils.parseUInt32(stream); + this.time = ParseUtils.readUInt32(stream); + this.value = ParseUtils.readUInt32(stream); if (interpolationType.tangential()) { - this.inTan = ParseUtils.parseUInt32(stream); - this.outTan = ParseUtils.parseUInt32(stream); + this.inTan = ParseUtils.readUInt32(stream); + this.outTan = ParseUtils.readUInt32(stream); } } 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/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/ImageUtils.java b/core/src/com/etheller/warsmash/util/ImageUtils.java new file mode 100644 index 0000000..71dbb0b --- /dev/null +++ b/core/src/com/etheller/warsmash/util/ImageUtils.java @@ -0,0 +1,85 @@ +package com.etheller.warsmash.util; + +import java.awt.Graphics2D; +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 com.badlogic.gdx.graphics.Pixmap; +import com.badlogic.gdx.graphics.Pixmap.Format; +import com.badlogic.gdx.graphics.Texture; + +/** + * Uses AWT stuff + * + */ +public final class ImageUtils { + private static final int BYTES_PER_PIXEL = 4; + + public static Texture getTexture(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)); + } + } + return new Texture(pixmap); + } + + /** + * 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); + final Graphics2D graphic = lRGB.createGraphics(); + try { + graphic.drawImage(in, 0, 0, null); + } + finally { + graphic.dispose(); + } + + // 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/MdlUtils.java b/core/src/com/etheller/warsmash/util/MdlUtils.java index 08eb95b..7712229 100644 --- a/core/src/com/etheller/warsmash/util/MdlUtils.java +++ b/core/src/com/etheller/warsmash/util/MdlUtils.java @@ -1,8 +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_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_TEXTURE_ANIM = "TextureAnim"; + 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 index 933b6cb..951df08 100644 --- a/core/src/com/etheller/warsmash/util/ParseUtils.java +++ b/core/src/com/etheller/warsmash/util/ParseUtils.java @@ -1,13 +1,24 @@ package com.etheller.warsmash.util; 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 long parseUInt32(final LittleEndianDataInputStream stream) throws IOException { - return stream.readInt() & 0xFFFFFFFF; + 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) @@ -17,10 +28,114 @@ public class ParseUtils { } } + 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 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 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; + } } 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..96feea1 --- /dev/null +++ b/core/src/com/etheller/warsmash/util/RenderMathUtils.java @@ -0,0 +1,290 @@ +package com.etheller.warsmash.util; + +import com.badlogic.gdx.math.Matrix4; +import com.badlogic.gdx.math.Quaternion; +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.math.Vector3; + +public enum RenderMathUtils { + ; + 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); + + // 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.M01] = (xy + wz) * sx; + out.val[Matrix4.M02] = (xz - wy) * sx; + out.val[Matrix4.M03] = 0; + out.val[Matrix4.M10] = (xy - wz) * sy; + out.val[Matrix4.M11] = (1 - (xx + zz)) * sy; + out.val[Matrix4.M12] = (yz + wx) * sy; + out.val[Matrix4.M13] = 0; + out.val[Matrix4.M20] = (xz + wy) * sz; + out.val[Matrix4.M21] = (yz - wx) * sz; + out.val[Matrix4.M22] = (1 - (xx + yy)) * sz; + out.val[Matrix4.M23] = 0; + out.val[Matrix4.M30] = (v.x + pivot.x) - ((out.val[Matrix4.M00] * pivot.x) + (out.val[Matrix4.M10] * pivot.y) + + (out.val[Matrix4.M20] * pivot.z)); + out.val[Matrix4.M31] = (v.y + pivot.y) - ((out.val[Matrix4.M01] * pivot.x) + (out.val[Matrix4.M11] * pivot.y) + + (out.val[Matrix4.M21] * pivot.z)); + out.val[Matrix4.M32] = (v.z + pivot.z) - ((out.val[Matrix4.M02] * pivot.x) + (out.val[Matrix4.M12] * 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.M01] = (xy + wz) * sx; + out.val[Matrix4.M02] = (xz - wy) * sx; + out.val[Matrix4.M03] = 0; + out.val[Matrix4.M10] = (xy - wz) * sy; + out.val[Matrix4.M11] = (1 - (xx + zz)) * sy; + out.val[Matrix4.M12] = (yz + wx) * sy; + out.val[Matrix4.M13] = 0; + out.val[Matrix4.M20] = (xz + wy) * sz; + out.val[Matrix4.M21] = (yz - wx) * sz; + out.val[Matrix4.M22] = (1 - (xx + yy)) * sz; + out.val[Matrix4.M23] = 0; + out.val[Matrix4.M30] = v.x; + out.val[Matrix4.M31] = v.y; + out.val[Matrix4.M32] = 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.M01] = 0; + out.val[Matrix4.M02] = 0; + out.val[Matrix4.M03] = 0; + out.val[Matrix4.M10] = 0; + out.val[Matrix4.M11] = f; + out.val[Matrix4.M12] = 0; + out.val[Matrix4.M13] = 0; + out.val[Matrix4.M20] = 0; + out.val[Matrix4.M21] = 0; + out.val[Matrix4.M23] = -1; + out.val[Matrix4.M30] = 0; + out.val[Matrix4.M31] = 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.M32] = (2 * far * near) * nf; + } + else { + out.val[Matrix4.M22] = -1; + out.val[Matrix4.M32] = -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.M01] = 0; + out.val[Matrix4.M02] = 0; + out.val[Matrix4.M03] = 0; + + out.val[Matrix4.M10] = 0; + out.val[Matrix4.M11] = -2 * bt; + out.val[Matrix4.M12] = 0; + out.val[Matrix4.M13] = 0; + + out.val[Matrix4.M20] = 0; + out.val[Matrix4.M21] = 0; + out.val[Matrix4.M22] = 2 * nf; + out.val[Matrix4.M23] = 0; + + out.val[Matrix4.M30] = (left + right) * lr; + out.val[Matrix4.M31] = (top + bottom) * bt; + out.val[Matrix4.M32] = (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 = 1 - ((2 * (v.y - viewport.y)) / viewport.height); + 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; + } +} 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..f80af83 --- /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.M10] * y) + (m[Matrix4.M20] * z) + (m[Matrix4.M30] * w); + out.y = (m[Matrix4.M01] * x) + (m[Matrix4.M11] * y) + (m[Matrix4.M21] * z) + (m[Matrix4.M31] * w); + out.z = (m[Matrix4.M02] * x) + (m[Matrix4.M12] * y) + (m[Matrix4.M22] * z) + (m[Matrix4.M32] * w); + out.w = (m[Matrix4.M03] * x) + (m[Matrix4.M13] * y) + (m[Matrix4.M23] * z) + (m[Matrix4.M33] * w); + return out; + } +} \ No newline at end of file diff --git a/core/src/com/etheller/warsmash/viewer/BoundingShape.java b/core/src/com/etheller/warsmash/viewer/BoundingShape.java new file mode 100644 index 0000000..d0d379b --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer/BoundingShape.java @@ -0,0 +1,116 @@ +package com.etheller.warsmash.viewer; + +import com.badlogic.gdx.math.Quaternion; +import com.badlogic.gdx.math.Vector3; + +public class BoundingShape extends SceneNode { + private final float[] min = new float[] { -1, -1, -1 }; + private final float[] max = new float[] { 1, 1, 1 }; + private float radius = (float) Math.sqrt(2); + + public void fromBounds(final float[] min, final float[] max) { + System.arraycopy(min, 0, this.min, 0, this.min.length); + System.arraycopy(max, 0, this.max, 0, this.max.length); + + final float dX = max[0] - min[0]; + final float dY = max[1] - min[1]; + final float dZ = max[2] - min[2]; + + this.radius = (float) Math.sqrt((dX * dX) + (dY * dY) + (dZ * dZ)) / 2; + } + + public void fromRadius(final float radius) { + final float s = (float) (radius * Math.cos(radius)); + this.min[0] = this.min[1] = this.min[2] = s; + this.max[0] = this.max[1] = this.max[2] = s; + this.radius = radius; + } + + public void fromVertices(final float[] vertices) { + final float[] min = new float[] { 1E9f, 1E9f, 1E9f }; + final float[] max = new float[] { -1E9f, -1E9f, -1E9f }; + + for (int i = 0, l = vertices.length; i < l; i += 3) { + final float x = vertices[i]; + final float y = vertices[i + 1]; + final float z = vertices[i + 2]; + + if (x > max[0]) { + max[0] = x; + } + if (x < min[0]) { + min[0] = x; + } + if (y > max[1]) { + max[1] = y; + } + if (y < min[1]) { + min[1] = y; + } + if (z > max[2]) { + max[2] = z; + } + if (z < min[2]) { + min[2] = z; + } + } + + fromBounds(min, max); + } + + public Vector3 getPositiveVertex(final Vector3 out, final Vector3 normal) { + if (normal.x >= 0) { + out.x = this.max[0]; + } + else { + out.x = this.min[0]; + } + if (normal.y >= 0) { + out.y = this.max[1]; + } + else { + out.y = this.min[1]; + } + if (normal.z >= 0) { + out.z = this.max[2]; + } + else { + out.z = this.min[2]; + } + + return out; + } + + public Vector3 getNegativeVertex(final Vector3 out, final Vector3 normal) { + if (normal.x >= 0) { + out.x = this.min[0]; + } + else { + out.x = this.max[0]; + } + if (normal.y >= 0) { + out.y = this.min[1]; + } + else { + out.y = this.max[1]; + } + if (normal.z >= 0) { + out.z = this.min[2]; + } + else { + out.z = this.max[2]; + } + + return out; + } + + @Override + protected void updateObject(final Scene scene) { + } + + @Override + protected void convertBasis(final Quaternion computedRotation) { + // TODO ??? + } + +} diff --git a/core/src/com/etheller/warsmash/viewer/Bucket.java b/core/src/com/etheller/warsmash/viewer/Bucket.java new file mode 100644 index 0000000..98955a1 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer/Bucket.java @@ -0,0 +1,28 @@ +package com.etheller.warsmash.viewer; + +import com.badlogic.gdx.graphics.GL20; +import com.etheller.warsmash.viewer.ModelView.SceneData; + +public class Bucket { + private final ModelView modelView; + private final Model model; + private final int count; + + public Bucket(final ModelView modelView) { + final Model model = modelView.model; + final GL20 gl = model.getViewer().gl; + + this.modelView = modelView; + this.model = model; + this.count = 0; + +// this.instanceIdBuffer = + } + + public int fill(final SceneData data, final int baseInstance, final Scene scene) { + // Make believe the bucket is now filled with data for all instances. + // This is because if a non-specific bucket implementation is supplied, + // instancing isn't used, so batching is irrelevant. + return data.instances.size(); + } +} diff --git a/core/src/com/etheller/warsmash/viewer/Camera.java b/core/src/com/etheller/warsmash/viewer/Camera.java new file mode 100644 index 0000000..db37d39 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer/Camera.java @@ -0,0 +1,314 @@ +package com.etheller.warsmash.viewer; + +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(); + + private final Rectangle rect; + + private boolean isPerspective; + private float fov; + private float aspect; + + private boolean isOrtho; + private float leftClipPlane; + private float rightClipPlane; + private float bottomClipPlane; + private float topClipPlane; + + private float nearClipPlane; + private float farClipPlane; + + private final Vector3 location; + private final Quaternion rotation; + + public Quaternion inverseRotation; + private final Matrix4 worldMatrix; + private final Matrix4 projectionMatrix; + private final Matrix4 worldProjectionMatrix; + private final Matrix4 inverseWorldMatrix; + private final Matrix4 inverseRotationMatrix; + private final Matrix4 inverseWorldProjectionMatrix; + private final Vector3 directionX; + private final Vector3 directionY; + private final Vector3 directionZ; + private final Vector3[] vectors; + private final Vector3[] billboardedVectors; + + private 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.worldMatrix = new Matrix4(); + this.projectionMatrix = new Matrix4(); + this.worldProjectionMatrix = new Matrix4(); + this.inverseWorldMatrix = new Matrix4(); + this.inverseRotationMatrix = new Matrix4(); + this.inverseWorldProjectionMatrix = 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; + } + + public void viewport(final Rectangle viewport) { + this.rect.set(viewport); + + this.aspect = viewport.width / viewport.height; + + this.dirty = true; + } + + 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) { + this.dirty = true; + + final Vector3 location = this.location; + final Quaternion rotation = this.rotation; + final Quaternion inverseRotation = this.inverseRotation; + final Matrix4 worldMatrix = this.worldMatrix; + final Matrix4 projectionMatrix = this.projectionMatrix; + final Matrix4 worldProjectionMatrix = this.worldProjectionMatrix; + 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(projectionMatrix.val); + worldMatrix.translate(vectorHeap.set(location).scl(-1)); + inverseRotation.set(rotation).conjugate(); + + // World projection matrix + // World space -> NDC space + worldProjectionMatrix.set(projectionMatrix).mul(worldMatrix); + + // Recalculate the camera's frustum planes + RenderMathUtils.unpackPlanes(this.planes, worldProjectionMatrix); + + // Inverse world matrix + // Camera space -> world space + this.inverseWorldMatrix.set(worldMatrix).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.inverseWorldProjectionMatrix.set(worldProjectionMatrix); + this.inverseWorldProjectionMatrix.inv(); + + for (int i = 0; i < 7; i++) { + billboardedVectors[i].set(vectors[i]); + inverseRotation.transform(billboardedVectors[i]); + } + } + } + + 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.inverseWorldMatrix); + } + + public Vector3 worldToCamera(final Vector3 out, final Vector3 v) { + return out.set(v).prj(this.worldMatrix); + } + + 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.inverseWorldProjectionMatrix, viewport); + + // Intersection on the far-plane + RenderMathUtils.unproject(b, c.set(x, y, 1), this.inverseWorldProjectionMatrix, 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/viewer/Model.java b/core/src/com/etheller/warsmash/viewer/Model.java new file mode 100644 index 0000000..5c5d35a --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer/Model.java @@ -0,0 +1,9 @@ +package com.etheller.warsmash.viewer; + +public abstract class Model { + private ModelView modelView; + + public boolean ok; + + public abstract Viewer getViewer(); +} diff --git a/core/src/com/etheller/warsmash/viewer/ModelInstance.java b/core/src/com/etheller/warsmash/viewer/ModelInstance.java new file mode 100644 index 0000000..9404c32 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer/ModelInstance.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.viewer; + +public class ModelInstance { + +} diff --git a/core/src/com/etheller/warsmash/viewer/ModelView.java b/core/src/com/etheller/warsmash/viewer/ModelView.java new file mode 100644 index 0000000..e4004a6 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer/ModelView.java @@ -0,0 +1,68 @@ +package com.etheller.warsmash.viewer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +public abstract class ModelView { + protected final Model model; + protected final HashSet instanceSet; + protected final HashMap sceneData; + protected final int renderedInstances; + protected final int renderedParticles; + protected final int renderedBuckets; + protected final int renderedCalls; + + public ModelView(final Model model) { + this.model = model; + + this.instanceSet = new HashSet<>(); + this.sceneData = new HashMap<>(); + + this.renderedInstances = 0; + this.renderedParticles = 0; + this.renderedBuckets = 0; + this.renderedCalls = 0; + } + + public abstract Object getShallowCopy(); + + public abstract void applyShallowCopy(final Object view); + + @Override + public abstract boolean equals(Object view); + +// public boo + public void addSceneData(final ModelInstance instance, final Scene scene) { + if (this.model.ok && (scene != null)) { + SceneData data = this.sceneData.get(scene); + + if (data == null) { + data = this.createSceneData(scene); + + this.sceneData.put(scene, data); + } + + } + } + + private SceneData createSceneData(final Scene scene) { + return new SceneData(scene, this); + } + + public static final class SceneData { + public final Scene scene; + public final ModelView modelView; + public final int baseIndex = 0; + public final List instances = new ArrayList<>(); + public final List buckets = new ArrayList<>(); + public final int usedBuckets = 0; + + public SceneData(final Scene scene, final ModelView modelView) { + this.scene = scene; + this.modelView = modelView; + } + + } +} diff --git a/core/src/com/etheller/warsmash/viewer/Scene.java b/core/src/com/etheller/warsmash/viewer/Scene.java new file mode 100644 index 0000000..f1dcf53 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer/Scene.java @@ -0,0 +1,5 @@ +package com.etheller.warsmash.viewer; + +public abstract class Scene { + public Camera camera; +} diff --git a/core/src/com/etheller/warsmash/viewer/SceneNode.java b/core/src/com/etheller/warsmash/viewer/SceneNode.java new file mode 100644 index 0000000..cc89faf --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer/SceneNode.java @@ -0,0 +1,245 @@ +package com.etheller.warsmash.viewer; + +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 SceneNode extends ViewerNode { + + public SceneNode() { + } + + public SceneNode setPivot(final float[] pivot) { + this.pivot.set(pivot); + this.dirty = true; + return this; + } + + public SceneNode setLocation(final float[] location) { + this.localLocation.set(location); + this.dirty = true; + return this; + } + + public SceneNode setRotation(final float[] rotation) { + this.localRotation.set(rotation[0], rotation[1], rotation[2], rotation[3]); + this.dirty = true; + return this; + } + + public SceneNode setScale(final float[] varying) { + this.localScale.set(varying); + this.dirty = true; + return this; + } + + public SceneNode setUniformScale(final float uniform) { + this.localScale.set(uniform, uniform, uniform); + this.dirty = true; + return this; + } + + public SceneNode 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 SceneNode 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 SceneNode movePivot(final float[] offset) { + this.pivot.add(offset[0], offset[1], offset[2]); + + this.dirty = true; + + return this; + } + + public SceneNode move(final float[] offset) { + this.localLocation.add(offset[0], offset[1], offset[2]); + + this.dirty = true; + + return this; + } + + public SceneNode rotate(final Quaternion rotation) { + RenderMathUtils.mul(this.localRotation, this.localRotation, rotation); + + this.dirty = true; + + return this; + } + + public SceneNode rotateLocal(final Quaternion rotation) { + RenderMathUtils.mul(this.localRotation, rotation, this.localRotation); + + this.dirty = true; + + return this; + } + + public SceneNode 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 SceneNode uniformScale(final float scale) { + this.localScale.x *= scale; + this.localScale.y *= scale; + this.localScale.z *= scale; + + this.dirty = true; + + return this; + } + + public SceneNode setParent(final ViewerNode 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 ViewerNode 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; + 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.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(this.localRotation, computedLocation, computedScaling, + this.localMatrix); + + RenderMathUtils.mul(this.worldMatrix, parent.worldMatrix, this.localMatrix); + + RenderMathUtils.mul(this.worldRotation, parent.worldRotation, this.localRotation); + } + 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.M30]; + this.worldLocation.y = this.worldMatrix.val[Matrix4.M31]; + this.worldLocation.z = this.worldMatrix.val[Matrix4.M32]; + + // 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 Scene scene) { + if (this.dirty || ((this.parent != null) && this.parent.wasDirty)) { + this.dirty = true; + this.wasDirty = true; + this.recalculateTransformation(); + } + else { + this.wasDirty = false; + } + + this.updateObject(scene); + this.updateChildren(scene); + } + + protected abstract void updateObject(Scene scene); + + protected void updateChildren(final Scene scene) { + for (int i = 0, l = this.children.size(); i < l; i++) { + this.children.get(i).update(scene); + } + } + + protected abstract void convertBasis(Quaternion computedRotation); + +} diff --git a/core/src/com/etheller/warsmash/viewer/SkeletalNode.java b/core/src/com/etheller/warsmash/viewer/SkeletalNode.java new file mode 100644 index 0000000..bd2a1fd --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer/SkeletalNode.java @@ -0,0 +1,118 @@ +package com.etheller.warsmash.viewer; + +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 SkeletalNode extends ViewerNode { + + private final Object object; + + private final boolean billboarded = false; + private final boolean billboardedX = false; + private final boolean billboardedY = false; + private final boolean billboardedZ = false; + + public SkeletalNode() { + this.object = null; + } + + public void recalculateTransformation(final Scene scene) { + final Quaternion computedRotation; + Vector3 computedScaling; + + 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 { + computedScaling = this.localScale; + + final Vector3 parentScale = this.parent.worldScale; + this.worldScale.x = parentScale.x * this.worldScale.x; + this.worldScale.y = parentScale.y * this.worldScale.y; + this.worldScale.z = parentScale.z * this.worldScale.z; + } + + if (this.billboarded) { + computedRotation = rotationHeap; + + computedRotation.set(this.parent.inverseWorldRotation); + computedRotation.mul(scene.camera.inverseRotation); + + this.convertBasis(computedRotation); + } + else { + computedRotation = this.localRotation; + } + + RenderMathUtils.fromRotationTranslationScaleOrigin(computedRotation, this.localLocation, 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.M10] * y) + + (this.worldMatrix.val[Matrix4.M20] * z) + this.worldMatrix.val[Matrix4.M30]; + this.worldLocation.y = (this.worldMatrix.val[Matrix4.M01] * x) + (this.worldMatrix.val[Matrix4.M11] * y) + + (this.worldMatrix.val[Matrix4.M21] * z) + this.worldMatrix.val[Matrix4.M31]; + this.worldLocation.z = (this.worldMatrix.val[Matrix4.M02] * x) + (this.worldMatrix.val[Matrix4.M12] * y) + + (this.worldMatrix.val[Matrix4.M22] * z) + this.worldMatrix.val[Matrix4.M32]; + + // Inverse world location + this.inverseWorldLocation.x = -this.worldLocation.x; + this.inverseWorldLocation.y = -this.worldLocation.y; + this.inverseWorldLocation.z = -this.worldLocation.z; + } + + protected void updateChildren(final Scene scene) { + for (int i = 0, l = this.children.size(); i < l; i++) { + this.children.get(i).update(scene); + } + } + + protected abstract void convertBasis(Quaternion computedRotation); + + 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/viewer/Viewer.java b/core/src/com/etheller/warsmash/viewer/Viewer.java new file mode 100644 index 0000000..f1d0232 --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer/Viewer.java @@ -0,0 +1,7 @@ +package com.etheller.warsmash.viewer; + +import com.badlogic.gdx.graphics.GL20; + +public abstract class Viewer { + public GL20 gl; +} diff --git a/core/src/com/etheller/warsmash/viewer/ViewerNode.java b/core/src/com/etheller/warsmash/viewer/ViewerNode.java new file mode 100644 index 0000000..5bdd33d --- /dev/null +++ b/core/src/com/etheller/warsmash/viewer/ViewerNode.java @@ -0,0 +1,65 @@ +package com.etheller.warsmash.viewer; + +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; + +public abstract class ViewerNode { + protected static final Vector3 locationHeap = new Vector3(); + protected static final Quaternion rotationHeap = new Quaternion(); + protected static final Vector3 scalingHeap = new Vector3(); + + protected final Vector3 pivot; + protected final Vector3 localLocation; + protected final Quaternion localRotation; + protected final Vector3 localScale; + protected final Vector3 worldLocation; + protected final Quaternion worldRotation; + protected final Vector3 worldScale; + protected final Vector3 inverseWorldLocation; + protected final Quaternion inverseWorldRotation; + protected final Vector3 inverseWorldScale; + protected final Matrix4 localMatrix; + protected final Matrix4 worldMatrix; + protected final boolean dontInheritTranslation; + protected final boolean dontInheritRotation; + protected final boolean dontInheritScaling; + protected boolean visible; + protected boolean wasDirty; + protected boolean dirty; + + protected ViewerNode parent; + + protected final List children; + + public ViewerNode() { + 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(); + 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.dontInheritTranslation = false; + this.dontInheritRotation = false; + this.dontInheritScaling = false; + this.visible = true; + this.wasDirty = false; + this.dirty = true; + this.children = new ArrayList<>(); + } + + public abstract void update(Scene scene); +} diff --git a/core/src/com/hiveworkshop/wc3/mpq/Codebase.java b/core/src/com/hiveworkshop/wc3/mpq/Codebase.java new file mode 100644 index 0000000..61a7d5c --- /dev/null +++ b/core/src/com/hiveworkshop/wc3/mpq/Codebase.java @@ -0,0 +1,12 @@ +package com.hiveworkshop.wc3.mpq; + +import java.io.File; +import java.io.InputStream; + +public interface Codebase { + InputStream getResourceAsStream(String filepath); + + File getFile(String filepath); + + boolean has(String filepath); +} diff --git a/core/src/com/hiveworkshop/wc3/mpq/FileCodebase.java b/core/src/com/hiveworkshop/wc3/mpq/FileCodebase.java new file mode 100644 index 0000000..ae5d443 --- /dev/null +++ b/core/src/com/hiveworkshop/wc3/mpq/FileCodebase.java @@ -0,0 +1,35 @@ +package com.hiveworkshop.wc3.mpq; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; + +public class FileCodebase implements Codebase { + private final File sourceDirectory; + + public FileCodebase(final File sourceDirectory) { + this.sourceDirectory = sourceDirectory; + } + + @Override + public InputStream getResourceAsStream(final String filepath) { + try { + return new FileInputStream(new File(this.sourceDirectory.getPath() + File.separatorChar + filepath)); + } + catch (final FileNotFoundException e) { + throw new RuntimeException(e); + } + } + + @Override + public File getFile(final String filepath) { + return new File(this.sourceDirectory.getPath() + File.separatorChar + filepath); + } + + @Override + public boolean has(final String filepath) { + return new File(this.sourceDirectory.getPath() + File.separatorChar + filepath).exists(); + } + +} diff --git a/jars/blp-iio-plugin.jar b/jars/blp-iio-plugin.jar new file mode 100644 index 0000000000000000000000000000000000000000..f2d085a59d48ce8564683c2ef4a699344ebccf4f GIT binary patch literal 63630 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

RBtHeS{8k3%Xk6rDF^ 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