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 0000000..f2d085a Binary files /dev/null and b/jars/blp-iio-plugin.jar differ