aboutsummaryrefslogtreecommitdiff
path: root/engine/src/core-plugins/com/jme3
diff options
context:
space:
mode:
authorScott Barta <sbarta@google.com>2012-03-01 12:35:35 -0800
committerScott Barta <sbarta@google.com>2012-03-01 12:40:08 -0800
commit59b2e6871c65f58fdad78cd7229c292f6a177578 (patch)
tree2d4e7bfc05b93f40b34675d77e403dd1c25efafd /engine/src/core-plugins/com/jme3
parentf9b30489e75ac1eabc365064959804e99534f7ab (diff)
downloadjmonkeyengine-59b2e6871c65f58fdad78cd7229c292f6a177578.tar.gz
Adds the jMonkeyEngine library to the build.
Adds the jMonkeyEngine open source 3D game engine to the build. This is built as a static library and is only used by the Finsky client. Change-Id: I06a3f054df7b8a67757267d884854f70c5a16ca0
Diffstat (limited to 'engine/src/core-plugins/com/jme3')
-rw-r--r--engine/src/core-plugins/com/jme3/asset/plugins/ClasspathLocator.java117
-rw-r--r--engine/src/core-plugins/com/jme3/asset/plugins/FileLocator.java104
-rw-r--r--engine/src/core-plugins/com/jme3/asset/plugins/HttpZipLocator.java367
-rw-r--r--engine/src/core-plugins/com/jme3/asset/plugins/HttpZipLocator.java~355
-rw-r--r--engine/src/core-plugins/com/jme3/asset/plugins/UrlAssetInfo.java65
-rw-r--r--engine/src/core-plugins/com/jme3/asset/plugins/UrlLocator.java84
-rw-r--r--engine/src/core-plugins/com/jme3/asset/plugins/ZipLocator.java87
-rw-r--r--engine/src/core-plugins/com/jme3/audio/plugins/WAVLoader.java191
-rw-r--r--engine/src/core-plugins/com/jme3/export/binary/BinaryClassField.java101
-rw-r--r--engine/src/core-plugins/com/jme3/export/binary/BinaryClassObject.java46
-rw-r--r--engine/src/core-plugins/com/jme3/export/binary/BinaryExporter.java401
-rw-r--r--engine/src/core-plugins/com/jme3/export/binary/BinaryIdContentPair.java60
-rw-r--r--engine/src/core-plugins/com/jme3/export/binary/BinaryImporter.java361
-rw-r--r--engine/src/core-plugins/com/jme3/export/binary/BinaryInputCapsule.java1380
-rw-r--r--engine/src/core-plugins/com/jme3/export/binary/BinaryOutputCapsule.java944
-rw-r--r--engine/src/core-plugins/com/jme3/export/binary/ByteUtils.java486
-rw-r--r--engine/src/core-plugins/com/jme3/font/plugins/BitmapFontLoader.java181
-rw-r--r--engine/src/core-plugins/com/jme3/material/plugins/J3MLoader.java530
-rw-r--r--engine/src/core-plugins/com/jme3/scene/plugins/MTLLoader.java325
-rw-r--r--engine/src/core-plugins/com/jme3/scene/plugins/OBJLoader.java593
-rw-r--r--engine/src/core-plugins/com/jme3/shader/plugins/GLSLLoader.java213
-rw-r--r--engine/src/core-plugins/com/jme3/texture/plugins/DDSLoader.java827
-rw-r--r--engine/src/core-plugins/com/jme3/texture/plugins/DXTFlipper.java318
-rw-r--r--engine/src/core-plugins/com/jme3/texture/plugins/HDRLoader.java332
-rw-r--r--engine/src/core-plugins/com/jme3/texture/plugins/ImageFlipper.java77
-rw-r--r--engine/src/core-plugins/com/jme3/texture/plugins/PFMLoader.java152
-rw-r--r--engine/src/core-plugins/com/jme3/texture/plugins/TGALoader.java517
27 files changed, 9214 insertions, 0 deletions
diff --git a/engine/src/core-plugins/com/jme3/asset/plugins/ClasspathLocator.java b/engine/src/core-plugins/com/jme3/asset/plugins/ClasspathLocator.java
new file mode 100644
index 0000000..f3570de
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/asset/plugins/ClasspathLocator.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.asset.plugins;
+
+import com.jme3.asset.*;
+import com.jme3.system.JmeSystem;
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.logging.Logger;
+
+/**
+ * The <code>ClasspathLocator</code> looks up an asset in the classpath.
+ * @author Kirill Vainer
+ */
+public class ClasspathLocator implements AssetLocator {
+
+ private static final Logger logger = Logger.getLogger(ClasspathLocator.class.getName());
+ private String root = "";
+
+ public ClasspathLocator(){
+ }
+
+ public void setRootPath(String rootPath) {
+ this.root = rootPath;
+ if (root.equals("/"))
+ root = "";
+ else if (root.length() > 1){
+ if (root.startsWith("/")){
+ root = root.substring(1);
+ }
+ if (!root.endsWith("/"))
+ root += "/";
+ }
+ }
+
+ public AssetInfo locate(AssetManager manager, AssetKey key) {
+ URL url;
+ String name = key.getName();
+ if (name.startsWith("/"))
+ name = name.substring(1);
+
+ name = root + name;
+// if (!name.startsWith(root)){
+// name = root + name;
+// }
+
+ if (JmeSystem.isLowPermissions()){
+ url = ClasspathLocator.class.getResource("/" + name);
+ }else{
+ url = Thread.currentThread().getContextClassLoader().getResource(name);
+ }
+ if (url == null)
+ return null;
+
+ if (url.getProtocol().equals("file")){
+ try {
+ String path = new File(url.toURI()).getCanonicalPath();
+
+ // convert to / for windows
+ if (File.separatorChar == '\\'){
+ path = path.replace('\\', '/');
+ }
+
+ // compare path
+ if (!path.endsWith(name)){
+ throw new AssetNotFoundException("Asset name doesn't match requirements.\n"+
+ "\"" + path + "\" doesn't match \"" + name + "\"");
+ }
+ } catch (URISyntaxException ex) {
+ throw new AssetLoadException("Error converting URL to URI", ex);
+ } catch (IOException ex){
+ throw new AssetLoadException("Failed to get canonical path for " + url, ex);
+ }
+ }
+
+ try{
+ return UrlAssetInfo.create(manager, key, url);
+ }catch (IOException ex){
+ // This is different handling than URL locator
+ // since classpath locating would return null at the getResource()
+ // call, otherwise there's a more critical error...
+ throw new AssetLoadException("Failed to read URL " + url, ex);
+ }
+ }
+}
diff --git a/engine/src/core-plugins/com/jme3/asset/plugins/FileLocator.java b/engine/src/core-plugins/com/jme3/asset/plugins/FileLocator.java
new file mode 100644
index 0000000..f448fa3
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/asset/plugins/FileLocator.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.asset.plugins;
+
+import com.jme3.asset.*;
+import java.io.*;
+
+/**
+ * <code>FileLocator</code> allows you to specify a folder where to
+ * look for assets.
+ * @author Kirill Vainer
+ */
+public class FileLocator implements AssetLocator {
+
+ private File root;
+
+ public void setRootPath(String rootPath) {
+ if (rootPath == null)
+ throw new NullPointerException();
+
+ try {
+ root = new File(rootPath).getCanonicalFile();
+ if (!root.isDirectory()){
+ throw new IllegalArgumentException("Given root path \"" + root + "\" not a directory");
+ }
+ } catch (IOException ex) {
+ throw new AssetLoadException("Root path is invalid", ex);
+ }
+ }
+
+ private static class AssetInfoFile extends AssetInfo {
+
+ private File file;
+
+ public AssetInfoFile(AssetManager manager, AssetKey key, File file){
+ super(manager, key);
+ this.file = file;
+ }
+
+ @Override
+ public InputStream openStream() {
+ try{
+ return new FileInputStream(file);
+ }catch (FileNotFoundException ex){
+ // NOTE: Can still happen even if file.exists() is true, e.g.
+ // permissions issue and similar
+ throw new AssetLoadException("Failed to open file: " + file, ex);
+ }
+ }
+ }
+
+ public AssetInfo locate(AssetManager manager, AssetKey key) {
+ String name = key.getName();
+ File file = new File(root, name);
+ if (file.exists() && file.isFile()){
+ try {
+ // Now, check asset name requirements
+ String canonical = file.getCanonicalPath();
+ String absolute = file.getAbsolutePath();
+ if (!canonical.endsWith(absolute)){
+ throw new AssetNotFoundException("Asset name doesn't match requirements.\n"+
+ "\"" + canonical + "\" doesn't match \"" + absolute + "\"");
+ }
+ } catch (IOException ex) {
+ throw new AssetLoadException("Failed to get file canonical path " + file, ex);
+ }
+
+ return new AssetInfoFile(manager, key, file);
+ }else{
+ return null;
+ }
+ }
+
+}
diff --git a/engine/src/core-plugins/com/jme3/asset/plugins/HttpZipLocator.java b/engine/src/core-plugins/com/jme3/asset/plugins/HttpZipLocator.java
new file mode 100644
index 0000000..0637221
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/asset/plugins/HttpZipLocator.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.asset.plugins;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetKey;
+import com.jme3.asset.AssetLocator;
+import com.jme3.asset.AssetManager;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.util.HashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+import java.util.zip.ZipEntry;
+
+public class HttpZipLocator implements AssetLocator {
+
+ private static final Logger logger = Logger.getLogger(HttpZipLocator.class.getName());
+
+ private URL zipUrl;
+ private String rootPath = "";
+ private int numEntries;
+ private int tableOffset;
+ private int tableLength;
+ private HashMap<String, ZipEntry2> entries;
+
+ private static final ByteBuffer byteBuf = ByteBuffer.allocate(250);
+ private static final CharBuffer charBuf = CharBuffer.allocate(250);
+ private static final CharsetDecoder utf8Decoder;
+
+ public static final long LOCSIG = 0x4034b50, EXTSIG = 0x8074b50,
+ CENSIG = 0x2014b50, ENDSIG = 0x6054b50;
+
+ public static final int LOCHDR = 30, EXTHDR = 16, CENHDR = 46, ENDHDR = 22,
+ LOCVER = 4, LOCFLG = 6, LOCHOW = 8, LOCTIM = 10, LOCCRC = 14,
+ LOCSIZ = 18, LOCLEN = 22, LOCNAM = 26, LOCEXT = 28, EXTCRC = 4,
+ EXTSIZ = 8, EXTLEN = 12, CENVEM = 4, CENVER = 6, CENFLG = 8,
+ CENHOW = 10, CENTIM = 12, CENCRC = 16, CENSIZ = 20, CENLEN = 24,
+ CENNAM = 28, CENEXT = 30, CENCOM = 32, CENDSK = 34, CENATT = 36,
+ CENATX = 38, CENOFF = 42, ENDSUB = 8, ENDTOT = 10, ENDSIZ = 12,
+ ENDOFF = 16, ENDCOM = 20;
+
+ static {
+ Charset utf8 = Charset.forName("UTF-8");
+ utf8Decoder = utf8.newDecoder();
+ }
+
+ private static class ZipEntry2 {
+ String name;
+ int length;
+ int offset;
+ int compSize;
+ long crc;
+ boolean deflate;
+
+ @Override
+ public String toString(){
+ return "ZipEntry[name=" + name +
+ ", length=" + length +
+ ", compSize=" + compSize +
+ ", offset=" + offset + "]";
+ }
+ }
+
+ private static int get16(byte[] b, int off) {
+ return (b[off++] & 0xff) |
+ ((b[off] & 0xff) << 8);
+ }
+
+ private static int get32(byte[] b, int off) {
+ return (b[off++] & 0xff) |
+ ((b[off++] & 0xff) << 8) |
+ ((b[off++] & 0xff) << 16) |
+ ((b[off] & 0xff) << 24);
+ }
+
+ private static long getu32(byte[] b, int off) throws IOException{
+ return (b[off++]&0xff) |
+ ((b[off++]&0xff) << 8) |
+ ((b[off++]&0xff) << 16) |
+ (((long)(b[off]&0xff)) << 24);
+ }
+
+ private static String getUTF8String(byte[] b, int off, int len) throws CharacterCodingException {
+ StringBuilder sb = new StringBuilder();
+
+ int read = 0;
+ while (read < len){
+ // Either read n remaining bytes in b or 250 if n is higher.
+ int toRead = Math.min(len - read, byteBuf.capacity());
+
+ boolean endOfInput = toRead < byteBuf.capacity();
+
+ // read 'toRead' bytes into byteBuf
+ byteBuf.put(b, off + read, toRead);
+
+ // set limit to position and set position to 0
+ // so data can be decoded
+ byteBuf.flip();
+
+ // decode data in byteBuf
+ CoderResult result = utf8Decoder.decode(byteBuf, charBuf, endOfInput);
+
+ // if the result is not an underflow its an error
+ // that cannot be handled.
+ // if the error is an underflow and its the end of input
+ // then the decoder expects more bytes but there are no more => error
+ if (!result.isUnderflow() || !endOfInput){
+ result.throwException();
+ }
+
+ // flip the char buf to get the string just decoded
+ charBuf.flip();
+
+ // append the decoded data into the StringBuilder
+ sb.append(charBuf.toString());
+
+ // clear buffers for next use
+ byteBuf.clear();
+ charBuf.clear();
+
+ read += toRead;
+ }
+
+ return sb.toString();
+ }
+
+ private InputStream readData(int offset, int length) throws IOException{
+ HttpURLConnection conn = (HttpURLConnection) zipUrl.openConnection();
+ conn.setDoOutput(false);
+ conn.setUseCaches(false);
+ conn.setInstanceFollowRedirects(false);
+ String range = "-";
+ if (offset != Integer.MAX_VALUE){
+ range = offset + range;
+ }
+ if (length != Integer.MAX_VALUE){
+ if (offset != Integer.MAX_VALUE){
+ range = range + (offset + length - 1);
+ }else{
+ range = range + length;
+ }
+ }
+
+ conn.setRequestProperty("Range", "bytes=" + range);
+ conn.connect();
+ if (conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL){
+ return conn.getInputStream();
+ }else if (conn.getResponseCode() == HttpURLConnection.HTTP_OK){
+ throw new IOException("Your server does not support HTTP feature Content-Range. Please contact your server administrator.");
+ }else{
+ throw new IOException(conn.getResponseCode() + " " + conn.getResponseMessage());
+ }
+ }
+
+ private int readTableEntry(byte[] table, int offset) throws IOException{
+ if (get32(table, offset) != CENSIG){
+ throw new IOException("Central directory error, expected 'PK12'");
+ }
+
+ int nameLen = get16(table, offset + CENNAM);
+ int extraLen = get16(table, offset + CENEXT);
+ int commentLen = get16(table, offset + CENCOM);
+ int newOffset = offset + CENHDR + nameLen + extraLen + commentLen;
+
+ int flags = get16(table, offset + CENFLG);
+ if ((flags & 1) == 1){
+ // ignore this entry, it uses encryption
+ return newOffset;
+ }
+
+ int method = get16(table, offset + CENHOW);
+ if (method != ZipEntry.DEFLATED && method != ZipEntry.STORED){
+ // ignore this entry, it uses unknown compression method
+ return newOffset;
+ }
+
+ String name = getUTF8String(table, offset + CENHDR, nameLen);
+ if (name.charAt(name.length()-1) == '/'){
+ // ignore this entry, it is directory node
+ // or it has no name (?)
+ return newOffset;
+ }
+
+ ZipEntry2 entry = new ZipEntry2();
+ entry.name = name;
+ entry.deflate = (method == ZipEntry.DEFLATED);
+ entry.crc = getu32(table, offset + CENCRC);
+ entry.length = get32(table, offset + CENLEN);
+ entry.compSize = get32(table, offset + CENSIZ);
+ entry.offset = get32(table, offset + CENOFF);
+
+ // we want offset directly into file data ..
+ // move the offset forward to skip the LOC header
+ entry.offset += LOCHDR + nameLen + extraLen;
+
+ entries.put(entry.name, entry);
+
+ return newOffset;
+ }
+
+ private void fillByteArray(byte[] array, InputStream source) throws IOException{
+ int total = 0;
+ int length = array.length;
+ while (total < length) {
+ int read = source.read(array, total, length - total);
+ if (read < 0)
+ throw new IOException("Failed to read entire array");
+
+ total += read;
+ }
+ }
+
+ private void readCentralDirectory() throws IOException{
+ InputStream in = readData(tableOffset, tableLength);
+ byte[] header = new byte[tableLength];
+
+ // Fix for "PK12 bug in town.zip": sometimes
+ // not entire byte array will be read with InputStream.read()
+ // (especially for big headers)
+ fillByteArray(header, in);
+
+// in.read(header);
+ in.close();
+
+ entries = new HashMap<String, ZipEntry2>(numEntries);
+ int offset = 0;
+ for (int i = 0; i < numEntries; i++){
+ offset = readTableEntry(header, offset);
+ }
+ }
+
+ private void readEndHeader() throws IOException{
+
+// InputStream in = readData(Integer.MAX_VALUE, ENDHDR);
+// byte[] header = new byte[ENDHDR];
+// fillByteArray(header, in);
+// in.close();
+//
+// if (get32(header, 0) != ENDSIG){
+// throw new IOException("End header error, expected 'PK56'");
+// }
+
+ // Fix for "PK56 bug in town.zip":
+ // If there's a zip comment inside the end header,
+ // PK56 won't appear in the -22 position relative to the end of the
+ // file!
+ // In that case, we have to search for it.
+ // Increase search space to 200 bytes
+
+ InputStream in = readData(Integer.MAX_VALUE, 200);
+ byte[] header = new byte[200];
+ fillByteArray(header, in);
+ in.close();
+
+ int offset = -1;
+ for (int i = 200 - 22; i >= 0; i--){
+ if (header[i] == (byte) (ENDSIG & 0xff)
+ && get32(header, i) == ENDSIG){
+ // found location
+ offset = i;
+ break;
+ }
+ }
+ if (offset == -1)
+ throw new IOException("Cannot find Zip End Header in file!");
+
+ numEntries = get16(header, offset + ENDTOT);
+ tableLength = get32(header, offset + ENDSIZ);
+ tableOffset = get32(header, offset + ENDOFF);
+ }
+
+ public void load(URL url) throws IOException {
+ if (!url.getProtocol().equals("http"))
+ throw new UnsupportedOperationException();
+
+ zipUrl = url;
+ readEndHeader();
+ readCentralDirectory();
+ }
+
+ private InputStream openStream(ZipEntry2 entry) throws IOException{
+ InputStream in = readData(entry.offset, entry.compSize);
+ if (entry.deflate){
+ return new InflaterInputStream(in, new Inflater(true));
+ }
+ return in;
+ }
+
+ public InputStream openStream(String name) throws IOException{
+ ZipEntry2 entry = entries.get(name);
+ if (entry == null)
+ throw new RuntimeException("Entry not found: "+name);
+
+ return openStream(entry);
+ }
+
+ public void setRootPath(String path){
+ if (!rootPath.equals(path)){
+ rootPath = path;
+ try {
+ load(new URL(path));
+ } catch (IOException ex) {
+ logger.log(Level.WARNING, "Failed to set root path "+path, ex);
+ }
+ }
+ }
+
+ public AssetInfo locate(AssetManager manager, AssetKey key){
+ final ZipEntry2 entry = entries.get(key.getName());
+ if (entry == null)
+ return null;
+
+ return new AssetInfo(manager, key){
+ @Override
+ public InputStream openStream() {
+ try {
+ return HttpZipLocator.this.openStream(entry);
+ } catch (IOException ex) {
+ logger.log(Level.WARNING, "Error retrieving "+entry.name, ex);
+ return null;
+ }
+ }
+ };
+ }
+
+}
diff --git a/engine/src/core-plugins/com/jme3/asset/plugins/HttpZipLocator.java~ b/engine/src/core-plugins/com/jme3/asset/plugins/HttpZipLocator.java~
new file mode 100644
index 0000000..f5fbfd1
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/asset/plugins/HttpZipLocator.java~
@@ -0,0 +1,355 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.asset.plugins;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetKey;
+import com.jme3.asset.AssetLocator;
+import com.jme3.asset.AssetManager;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.util.HashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+import java.util.zip.ZipEntry;
+
+public class HttpZipLocator implements AssetLocator {
+
+ private static final Logger logger = Logger.getLogger(HttpZipLocator.class.getName());
+
+ private URL zipUrl;
+ private String rootPath = "";
+ private int numEntries;
+ private int tableOffset;
+ private int tableLength;
+ private HashMap<String, ZipEntry2> entries;
+
+ private static final ByteBuffer byteBuf = ByteBuffer.allocate(250);
+ private static final CharBuffer charBuf = CharBuffer.allocate(250);
+ private static final CharsetDecoder utf8Decoder;
+
+ static {
+ Charset utf8 = Charset.forName("UTF-8");
+ utf8Decoder = utf8.newDecoder();
+ }
+
+ private static class ZipEntry2 {
+ String name;
+ int length;
+ int offset;
+ int compSize;
+ long crc;
+ boolean deflate;
+
+ @Override
+ public String toString(){
+ return "ZipEntry[name=" + name +
+ ", length=" + length +
+ ", compSize=" + compSize +
+ ", offset=" + offset + "]";
+ }
+ }
+
+ private static int get16(byte[] b, int off) {
+ return (b[off++] & 0xff) |
+ ((b[off] & 0xff) << 8);
+ }
+
+ private static int get32(byte[] b, int off) {
+ return (b[off++] & 0xff) |
+ ((b[off++] & 0xff) << 8) |
+ ((b[off++] & 0xff) << 16) |
+ ((b[off] & 0xff) << 24);
+ }
+
+ private static long getu32(byte[] b, int off) throws IOException{
+ return (b[off++]&0xff) |
+ ((b[off++]&0xff) << 8) |
+ ((b[off++]&0xff) << 16) |
+ (((long)(b[off]&0xff)) << 24);
+ }
+
+ private static String getUTF8String(byte[] b, int off, int len) throws CharacterCodingException {
+ StringBuilder sb = new StringBuilder();
+
+ int read = 0;
+ while (read < len){
+ // Either read n remaining bytes in b or 250 if n is higher.
+ int toRead = Math.min(len - read, byteBuf.capacity());
+
+ boolean endOfInput = toRead < byteBuf.capacity();
+
+ // read 'toRead' bytes into byteBuf
+ byteBuf.put(b, off + read, toRead);
+
+ // set limit to position and set position to 0
+ // so data can be decoded
+ byteBuf.flip();
+
+ // decode data in byteBuf
+ CoderResult result = utf8Decoder.decode(byteBuf, charBuf, endOfInput);
+
+ // if the result is not an underflow its an error
+ // that cannot be handled.
+ // if the error is an underflow and its the end of input
+ // then the decoder expects more bytes but there are no more => error
+ if (!result.isUnderflow() || !endOfInput){
+ result.throwException();
+ }
+
+ // flip the char buf to get the string just decoded
+ charBuf.flip();
+
+ // append the decoded data into the StringBuilder
+ sb.append(charBuf.toString());
+
+ // clear buffers for next use
+ byteBuf.clear();
+ charBuf.clear();
+
+ read += toRead;
+ }
+
+ return sb.toString();
+ }
+
+ private InputStream readData(int offset, int length) throws IOException{
+ HttpURLConnection conn = (HttpURLConnection) zipUrl.openConnection();
+ conn.setDoOutput(false);
+ conn.setUseCaches(false);
+ conn.setInstanceFollowRedirects(false);
+ String range = "-";
+ if (offset != Integer.MAX_VALUE){
+ range = offset + range;
+ }
+ if (length != Integer.MAX_VALUE){
+ if (offset != Integer.MAX_VALUE){
+ range = range + (offset + length - 1);
+ }else{
+ range = range + length;
+ }
+ }
+
+ conn.setRequestProperty("Range", "bytes=" + range);
+ conn.connect();
+ if (conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL){
+ return conn.getInputStream();
+ }else if (conn.getResponseCode() == HttpURLConnection.HTTP_OK){
+ throw new IOException("Your server does not support HTTP feature Content-Range. Please contact your server administrator.");
+ }else{
+ throw new IOException(conn.getResponseCode() + " " + conn.getResponseMessage());
+ }
+ }
+
+ private int readTableEntry(byte[] table, int offset) throws IOException{
+ if (get32(table, offset) != ZipEntry.CENSIG){
+ throw new IOException("Central directory error, expected 'PK12'");
+ }
+
+ int nameLen = get16(table, offset + ZipEntry.CENNAM);
+ int extraLen = get16(table, offset + ZipEntry.CENEXT);
+ int commentLen = get16(table, offset + ZipEntry.CENCOM);
+ int newOffset = offset + ZipEntry.CENHDR + nameLen + extraLen + commentLen;
+
+ int flags = get16(table, offset + ZipEntry.CENFLG);
+ if ((flags & 1) == 1){
+ // ignore this entry, it uses encryption
+ return newOffset;
+ }
+
+ int method = get16(table, offset + ZipEntry.CENHOW);
+ if (method != ZipEntry.DEFLATED && method != ZipEntry.STORED){
+ // ignore this entry, it uses unknown compression method
+ return newOffset;
+ }
+
+ String name = getUTF8String(table, offset + ZipEntry.CENHDR, nameLen);
+ if (name.charAt(name.length()-1) == '/'){
+ // ignore this entry, it is directory node
+ // or it has no name (?)
+ return newOffset;
+ }
+
+ ZipEntry2 entry = new ZipEntry2();
+ entry.name = name;
+ entry.deflate = (method == ZipEntry.DEFLATED);
+ entry.crc = getu32(table, offset + ZipEntry.CENCRC);
+ entry.length = get32(table, offset + ZipEntry.CENLEN);
+ entry.compSize = get32(table, offset + ZipEntry.CENSIZ);
+ entry.offset = get32(table, offset + ZipEntry.CENOFF);
+
+ // we want offset directly into file data ..
+ // move the offset forward to skip the LOC header
+ entry.offset += ZipEntry.LOCHDR + nameLen + extraLen;
+
+ entries.put(entry.name, entry);
+
+ return newOffset;
+ }
+
+ private void fillByteArray(byte[] array, InputStream source) throws IOException{
+ int total = 0;
+ int length = array.length;
+ while (total < length) {
+ int read = source.read(array, total, length - total);
+ if (read < 0)
+ throw new IOException("Failed to read entire array");
+
+ total += read;
+ }
+ }
+
+ private void readCentralDirectory() throws IOException{
+ InputStream in = readData(tableOffset, tableLength);
+ byte[] header = new byte[tableLength];
+
+ // Fix for "PK12 bug in town.zip": sometimes
+ // not entire byte array will be read with InputStream.read()
+ // (especially for big headers)
+ fillByteArray(header, in);
+
+// in.read(header);
+ in.close();
+
+ entries = new HashMap<String, ZipEntry2>(numEntries);
+ int offset = 0;
+ for (int i = 0; i < numEntries; i++){
+ offset = readTableEntry(header, offset);
+ }
+ }
+
+ private void readEndHeader() throws IOException{
+
+// InputStream in = readData(Integer.MAX_VALUE, ZipEntry.ENDHDR);
+// byte[] header = new byte[ZipEntry.ENDHDR];
+// fillByteArray(header, in);
+// in.close();
+//
+// if (get32(header, 0) != ZipEntry.ENDSIG){
+// throw new IOException("End header error, expected 'PK56'");
+// }
+
+ // Fix for "PK56 bug in town.zip":
+ // If there's a zip comment inside the end header,
+ // PK56 won't appear in the -22 position relative to the end of the
+ // file!
+ // In that case, we have to search for it.
+ // Increase search space to 200 bytes
+
+ InputStream in = readData(Integer.MAX_VALUE, 200);
+ byte[] header = new byte[200];
+ fillByteArray(header, in);
+ in.close();
+
+ int offset = -1;
+ for (int i = 200 - 22; i >= 0; i--){
+ if (header[i] == (byte) (ZipEntry.ENDSIG & 0xff)
+ && get32(header, i) == ZipEntry.ENDSIG){
+ // found location
+ offset = i;
+ break;
+ }
+ }
+ if (offset == -1)
+ throw new IOException("Cannot find Zip End Header in file!");
+
+ numEntries = get16(header, offset + ZipEntry.ENDTOT);
+ tableLength = get32(header, offset + ZipEntry.ENDSIZ);
+ tableOffset = get32(header, offset + ZipEntry.ENDOFF);
+ }
+
+ public void load(URL url) throws IOException {
+ if (!url.getProtocol().equals("http"))
+ throw new UnsupportedOperationException();
+
+ zipUrl = url;
+ readEndHeader();
+ readCentralDirectory();
+ }
+
+ private InputStream openStream(ZipEntry2 entry) throws IOException{
+ InputStream in = readData(entry.offset, entry.compSize);
+ if (entry.deflate){
+ return new InflaterInputStream(in, new Inflater(true));
+ }
+ return in;
+ }
+
+ public InputStream openStream(String name) throws IOException{
+ ZipEntry2 entry = entries.get(name);
+ if (entry == null)
+ throw new RuntimeException("Entry not found: "+name);
+
+ return openStream(entry);
+ }
+
+ public void setRootPath(String path){
+ if (!rootPath.equals(path)){
+ rootPath = path;
+ try {
+ load(new URL(path));
+ } catch (IOException ex) {
+ logger.log(Level.WARNING, "Failed to set root path "+path, ex);
+ }
+ }
+ }
+
+ public AssetInfo locate(AssetManager manager, AssetKey key){
+ final ZipEntry2 entry = entries.get(key.getName());
+ if (entry == null)
+ return null;
+
+ return new AssetInfo(manager, key){
+ @Override
+ public InputStream openStream() {
+ try {
+ return HttpZipLocator.this.openStream(entry);
+ } catch (IOException ex) {
+ logger.log(Level.WARNING, "Error retrieving "+entry.name, ex);
+ return null;
+ }
+ }
+ };
+ }
+
+}
diff --git a/engine/src/core-plugins/com/jme3/asset/plugins/UrlAssetInfo.java b/engine/src/core-plugins/com/jme3/asset/plugins/UrlAssetInfo.java
new file mode 100644
index 0000000..941764f
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/asset/plugins/UrlAssetInfo.java
@@ -0,0 +1,65 @@
+package com.jme3.asset.plugins;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetKey;
+import com.jme3.asset.AssetLoadException;
+import com.jme3.asset.AssetManager;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+
+/**
+ * Handles loading of assets from a URL
+ *
+ * @author Kirill Vainer
+ */
+public class UrlAssetInfo extends AssetInfo {
+
+ private URL url;
+ private InputStream in;
+
+ public static UrlAssetInfo create(AssetManager assetManager, AssetKey key, URL url) throws IOException {
+ // Check if URL can be reached. This will throw
+ // IOException which calling code will handle.
+ URLConnection conn = url.openConnection();
+ conn.setUseCaches(false);
+ InputStream in = conn.getInputStream();
+
+ // For some reason url cannot be reached?
+ if (in == null){
+ return null;
+ }else{
+ return new UrlAssetInfo(assetManager, key, url, in);
+ }
+ }
+
+ private UrlAssetInfo(AssetManager assetManager, AssetKey key, URL url, InputStream in) throws IOException {
+ super(assetManager, key);
+ this.url = url;
+ this.in = in;
+ }
+
+ public boolean hasInitialConnection(){
+ return in != null;
+ }
+
+ @Override
+ public InputStream openStream() {
+ if (in != null){
+ // Reuse the already existing stream (only once)
+ InputStream in2 = in;
+ in = null;
+ return in2;
+ }else{
+ // Create a new stream for subsequent invocations.
+ try {
+ URLConnection conn = url.openConnection();
+ conn.setUseCaches(false);
+ return conn.getInputStream();
+ } catch (IOException ex) {
+ throw new AssetLoadException("Failed to read URL " + url, ex);
+ }
+ }
+ }
+}
diff --git a/engine/src/core-plugins/com/jme3/asset/plugins/UrlLocator.java b/engine/src/core-plugins/com/jme3/asset/plugins/UrlLocator.java
new file mode 100644
index 0000000..e770930
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/asset/plugins/UrlLocator.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.asset.plugins;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetKey;
+import com.jme3.asset.AssetLocator;
+import com.jme3.asset.AssetManager;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <code>UrlLocator</code> is a locator that combines a root URL
+ * and the given path in the AssetKey to construct a new URL
+ * that allows locating the asset.
+ * @author Kirill Vainer
+ */
+public class UrlLocator implements AssetLocator {
+
+ private static final Logger logger = Logger.getLogger(UrlLocator.class.getName());
+ private URL root;
+
+ public void setRootPath(String rootPath) {
+ try {
+ this.root = new URL(rootPath);
+ } catch (MalformedURLException ex) {
+ throw new IllegalArgumentException("Invalid rootUrl specified", ex);
+ }
+ }
+
+ public AssetInfo locate(AssetManager manager, AssetKey key) {
+ String name = key.getName();
+ try{
+ //TODO: remove workaround for SDK
+// URL url = new URL(root, name);
+ if(name.startsWith("/")){
+ name = name.substring(1);
+ }
+ URL url = new URL(root.toExternalForm() + name);
+ return UrlAssetInfo.create(manager, key, url);
+ }catch (FileNotFoundException e){
+ return null;
+ }catch (IOException ex){
+ logger.log(Level.WARNING, "Error while locating " + name, ex);
+ return null;
+ }
+ }
+
+
+}
diff --git a/engine/src/core-plugins/com/jme3/asset/plugins/ZipLocator.java b/engine/src/core-plugins/com/jme3/asset/plugins/ZipLocator.java
new file mode 100644
index 0000000..ddfdfe8
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/asset/plugins/ZipLocator.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.asset.plugins;
+
+import com.jme3.asset.*;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.logging.Logger;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * <code>ZipLocator</code> is a locator that looks up resources in a .ZIP file.
+ * @author Kirill Vainer
+ */
+public class ZipLocator implements AssetLocator {
+
+ private ZipFile zipfile;
+ private static final Logger logger = Logger.getLogger(ZipLocator.class.getName());
+
+ private class JarAssetInfo extends AssetInfo {
+
+ private final ZipEntry entry;
+
+ public JarAssetInfo(AssetManager manager, AssetKey key, ZipEntry entry){
+ super(manager, key);
+ this.entry = entry;
+ }
+
+ public InputStream openStream(){
+ try{
+ return zipfile.getInputStream(entry);
+ }catch (IOException ex){
+ throw new AssetLoadException("Failed to load zip entry: "+entry, ex);
+ }
+ }
+ }
+
+ public void setRootPath(String rootPath) {
+ try{
+ zipfile = new ZipFile(new File(rootPath), ZipFile.OPEN_READ);
+ }catch (IOException ex){
+ throw new AssetLoadException("Failed to open zip file: " + rootPath, ex);
+ }
+ }
+
+ public AssetInfo locate(AssetManager manager, AssetKey key) {
+ String name = key.getName();
+ ZipEntry entry = zipfile.getEntry(name);
+ if (entry == null)
+ return null;
+
+ return new JarAssetInfo(manager, key, entry);
+ }
+
+}
diff --git a/engine/src/core-plugins/com/jme3/audio/plugins/WAVLoader.java b/engine/src/core-plugins/com/jme3/audio/plugins/WAVLoader.java
new file mode 100644
index 0000000..ab6a19b
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/audio/plugins/WAVLoader.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.audio.plugins;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetLoader;
+import com.jme3.audio.AudioBuffer;
+import com.jme3.audio.AudioData;
+import com.jme3.audio.AudioKey;
+import com.jme3.audio.AudioStream;
+import com.jme3.util.BufferUtils;
+import com.jme3.util.LittleEndien;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class WAVLoader implements AssetLoader {
+
+ private static final Logger logger = Logger.getLogger(WAVLoader.class.getName());
+
+ // all these are in big endian
+ private static final int i_RIFF = 0x46464952;
+ private static final int i_WAVE = 0x45564157;
+ private static final int i_fmt = 0x20746D66;
+ private static final int i_data = 0x61746164;
+
+ private boolean readStream = false;
+
+ private AudioBuffer audioBuffer;
+ private AudioStream audioStream;
+ private AudioData audioData;
+ private int bytesPerSec;
+ private float duration;
+
+ private LittleEndien in;
+
+ private void readFormatChunk(int size) throws IOException{
+ // if other compressions are supported, size doesn't have to be 16
+// if (size != 16)
+// logger.warning("Expected size of format chunk to be 16");
+
+ int compression = in.readShort();
+ if (compression != 1){
+ throw new IOException("WAV Loader only supports PCM wave files");
+ }
+
+ int channels = in.readShort();
+ int sampleRate = in.readInt();
+
+ bytesPerSec = in.readInt(); // used to calculate duration
+
+ int bytesPerSample = in.readShort();
+ int bitsPerSample = in.readShort();
+
+ int expectedBytesPerSec = (bitsPerSample * channels * sampleRate) / 8;
+ if (expectedBytesPerSec != bytesPerSec){
+ logger.log(Level.WARNING, "Expected {0} bytes per second, got {1}",
+ new Object[]{expectedBytesPerSec, bytesPerSec});
+ }
+
+ if (bitsPerSample != 8 && bitsPerSample != 16)
+ throw new IOException("Only 8 and 16 bits per sample are supported!");
+
+ if ( (bitsPerSample / 8) * channels != bytesPerSample)
+ throw new IOException("Invalid bytes per sample value");
+
+ if (bytesPerSample * sampleRate != bytesPerSec)
+ throw new IOException("Invalid bytes per second value");
+
+ audioData.setupFormat(channels, bitsPerSample, sampleRate);
+
+ int remaining = size - 16;
+ if (remaining > 0){
+ in.skipBytes(remaining);
+ }
+ }
+
+ private void readDataChunkForBuffer(int len) throws IOException {
+ ByteBuffer data = BufferUtils.createByteBuffer(len);
+ byte[] buf = new byte[512];
+ int read = 0;
+ while ( (read = in.read(buf)) > 0){
+ data.put(buf, 0, Math.min(read, data.remaining()) );
+ }
+ data.flip();
+ audioBuffer.updateData(data);
+ in.close();
+ }
+
+ private void readDataChunkForStream(int len) throws IOException {
+ audioStream.updateData(in, duration);
+ }
+
+ private AudioData load(InputStream inputStream, boolean stream) throws IOException{
+ this.in = new LittleEndien(inputStream);
+
+ int sig = in.readInt();
+ if (sig != i_RIFF)
+ throw new IOException("File is not a WAVE file");
+
+ // skip size
+ in.readInt();
+ if (in.readInt() != i_WAVE)
+ throw new IOException("WAVE File does not contain audio");
+
+ readStream = stream;
+ if (readStream){
+ audioStream = new AudioStream();
+ audioData = audioStream;
+ }else{
+ audioBuffer = new AudioBuffer();
+ audioData = audioBuffer;
+ }
+
+ while (true) {
+ int type = in.readInt();
+ int len = in.readInt();
+
+ switch (type) {
+ case i_fmt:
+ readFormatChunk(len);
+ break;
+ case i_data:
+ // Compute duration based on data chunk size
+ duration = len / bytesPerSec;
+
+ if (readStream) {
+ readDataChunkForStream(len);
+ } else {
+ readDataChunkForBuffer(len);
+ }
+ return audioData;
+ default:
+ int skipped = in.skipBytes(len);
+ if (skipped <= 0) {
+ return null;
+ }
+ break;
+ }
+ }
+ }
+
+ public Object load(AssetInfo info) throws IOException {
+ AudioData data;
+ InputStream inputStream = null;
+ try {
+ inputStream = info.openStream();
+ data = load(inputStream, ((AudioKey)info.getKey()).isStream());
+ if (data instanceof AudioStream){
+ inputStream = null;
+ }
+ return data;
+ } finally {
+ if (inputStream != null){
+ inputStream.close();
+ }
+ }
+ }
+}
diff --git a/engine/src/core-plugins/com/jme3/export/binary/BinaryClassField.java b/engine/src/core-plugins/com/jme3/export/binary/BinaryClassField.java
new file mode 100644
index 0000000..20da0e7
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/export/binary/BinaryClassField.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.export.binary;
+
+class BinaryClassField {
+
+ public static final byte BYTE = 0;
+ public static final byte BYTE_1D = 1;
+ public static final byte BYTE_2D = 2;
+
+ public static final byte INT = 10;
+ public static final byte INT_1D = 11;
+ public static final byte INT_2D = 12;
+
+ public static final byte FLOAT = 20;
+ public static final byte FLOAT_1D = 21;
+ public static final byte FLOAT_2D = 22;
+
+ public static final byte DOUBLE = 30;
+ public static final byte DOUBLE_1D = 31;
+ public static final byte DOUBLE_2D = 32;
+
+ public static final byte LONG = 40;
+ public static final byte LONG_1D = 41;
+ public static final byte LONG_2D = 42;
+
+ public static final byte SHORT = 50;
+ public static final byte SHORT_1D = 51;
+ public static final byte SHORT_2D = 52;
+
+ public static final byte BOOLEAN = 60;
+ public static final byte BOOLEAN_1D = 61;
+ public static final byte BOOLEAN_2D = 62;
+
+ public static final byte STRING = 70;
+ public static final byte STRING_1D = 71;
+ public static final byte STRING_2D = 72;
+
+ public static final byte BITSET = 80;
+
+ public static final byte SAVABLE = 90;
+ public static final byte SAVABLE_1D = 91;
+ public static final byte SAVABLE_2D = 92;
+
+ public static final byte SAVABLE_ARRAYLIST = 100;
+ public static final byte SAVABLE_ARRAYLIST_1D = 101;
+ public static final byte SAVABLE_ARRAYLIST_2D = 102;
+
+ public static final byte SAVABLE_MAP = 105;
+ public static final byte STRING_SAVABLE_MAP = 106;
+ public static final byte INT_SAVABLE_MAP = 107;
+
+ public static final byte FLOATBUFFER_ARRAYLIST = 110;
+ public static final byte BYTEBUFFER_ARRAYLIST = 111;
+
+ public static final byte FLOATBUFFER = 120;
+ public static final byte INTBUFFER = 121;
+ public static final byte BYTEBUFFER = 122;
+ public static final byte SHORTBUFFER = 123;
+
+
+ byte type;
+ String name;
+ byte alias;
+
+ BinaryClassField(String name, byte alias, byte type) {
+ this.name = name;
+ this.alias = alias;
+ this.type = type;
+ }
+}
diff --git a/engine/src/core-plugins/com/jme3/export/binary/BinaryClassObject.java b/engine/src/core-plugins/com/jme3/export/binary/BinaryClassObject.java
new file mode 100644
index 0000000..e66a945
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/export/binary/BinaryClassObject.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.export.binary;
+
+import java.util.HashMap;
+
+class BinaryClassObject {
+
+ // When exporting, use nameFields field, importing use aliasFields.
+ HashMap<String, BinaryClassField> nameFields;
+ HashMap<Byte, BinaryClassField> aliasFields;
+
+ byte[] alias;
+ String className;
+ int[] classHierarchyVersions;
+}
diff --git a/engine/src/core-plugins/com/jme3/export/binary/BinaryExporter.java b/engine/src/core-plugins/com/jme3/export/binary/BinaryExporter.java
new file mode 100644
index 0000000..b9ec78d
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/export/binary/BinaryExporter.java
@@ -0,0 +1,401 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.export.binary;
+
+import com.jme3.export.FormatVersion;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.Savable;
+import com.jme3.export.SavableClassUtil;
+import com.jme3.math.FastMath;
+import java.io.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Exports to the jME Binary Format. Format descriptor: (each numbered item
+ * denotes a series of bytes that follows sequentially one after the next.)
+ * <p>
+ * 1. "number of classes" - four bytes - int value representing the number of
+ * entries in the class lookup table.
+ * </p>
+ * <p>
+ * CLASS TABLE: There will be X blocks each consisting of numbers 2 thru 9,
+ * where X = the number read in 1.
+ * </p>
+ * <p>
+ * 2. "class alias" - 1...X bytes, where X = ((int) FastMath.log(aliasCount,
+ * 256) + 1) - an alias used when writing object data to match an object to its
+ * appropriate object class type.
+ * </p>
+ * <p>
+ * 3. "full class name size" - four bytes - int value representing number of
+ * bytes to read in for next field.
+ * </p>
+ * <p>
+ * 4. "full class name" - 1...X bytes representing a String value, where X = the
+ * number read in 3. The String is the fully qualified class name of the Savable
+ * class, eg "<code>com.jme.math.Vector3f</code>"
+ * </p>
+ * <p>
+ * 5. "number of fields" - four bytes - int value representing number of blocks
+ * to read in next (numbers 6 - 9), where each block represents a field in this
+ * class.
+ * </p>
+ * <p>
+ * 6. "field alias" - 1 byte - the alias used when writing out fields in a
+ * class. Because it is a single byte, a single class can not save out more than
+ * a total of 256 fields.
+ * </p>
+ * <p>
+ * 7. "field type" - 1 byte - a value representing the type of data a field
+ * contains. This value is taken from the static fields of
+ * <code>com.jme.util.export.binary.BinaryClassField</code>.
+ * </p>
+ * <p>
+ * 8. "field name size" - 4 bytes - int value representing the size of the next
+ * field.
+ * </p>
+ * <p>
+ * 9. "field name" - 1...X bytes representing a String value, where X = the
+ * number read in 8. The String is the full String value used when writing the
+ * current field.
+ * </p>
+ * <p>
+ * 10. "number of unique objects" - four bytes - int value representing the
+ * number of data entries in this file.
+ * </p>
+ * <p>
+ * DATA LOOKUP TABLE: There will be X blocks each consisting of numbers 11 and
+ * 12, where X = the number read in 10.
+ * </p>
+ * <p>
+ * 11. "data id" - four bytes - int value identifying a single unique object
+ * that was saved in this data file.
+ * </p>
+ * <p>
+ * 12. "data location" - four bytes - int value representing the offset in the
+ * object data portion of this file where the object identified in 11 is
+ * located.
+ * </p>
+ * <p>
+ * 13. "future use" - four bytes - hardcoded int value 1.
+ * </p>
+ * <p>
+ * 14. "root id" - four bytes - int value identifying the top level object.
+ * </p>
+ * <p>
+ * OBJECT DATA SECTION: There will be X blocks each consisting of numbers 15
+ * thru 19, where X = the number of unique location values named in 12.
+ * <p>
+ * 15. "class alias" - see 2.
+ * </p>
+ * <p>
+ * 16. "data length" - four bytes - int value representing the length in bytes
+ * of data stored in fields 17 and 18 for this object.
+ * </p>
+ * <p>
+ * FIELD ENTRY: There will be X blocks each consisting of numbers 18 and 19
+ * </p>
+ * <p>
+ * 17. "field alias" - see 6.
+ * </p>
+ * <p>
+ * 18. "field data" - 1...X bytes representing the field data. The data length
+ * is dependent on the field type and contents.
+ * </p>
+ *
+ * @author Joshua Slack
+ */
+
+public class BinaryExporter implements JmeExporter {
+ private static final Logger logger = Logger.getLogger(BinaryExporter.class
+ .getName());
+
+ protected int aliasCount = 1;
+ protected int idCount = 1;
+
+ protected IdentityHashMap<Savable, BinaryIdContentPair> contentTable
+ = new IdentityHashMap<Savable, BinaryIdContentPair>();
+
+ protected HashMap<Integer, Integer> locationTable
+ = new HashMap<Integer, Integer>();
+
+ // key - class name, value = bco
+ private HashMap<String, BinaryClassObject> classes
+ = new HashMap<String, BinaryClassObject>();
+
+ private ArrayList<Savable> contentKeys = new ArrayList<Savable>();
+
+ public static boolean debug = false;
+ public static boolean useFastBufs = true;
+
+ public BinaryExporter() {
+ }
+
+ public static BinaryExporter getInstance() {
+ return new BinaryExporter();
+ }
+
+ public boolean save(Savable object, OutputStream os) throws IOException {
+ // reset some vars
+ aliasCount = 1;
+ idCount = 1;
+ classes.clear();
+ contentTable.clear();
+ locationTable.clear();
+ contentKeys.clear();
+
+ // write signature and version
+ os.write(ByteUtils.convertToBytes(FormatVersion.SIGNATURE));
+ os.write(ByteUtils.convertToBytes(FormatVersion.VERSION));
+
+ int id = processBinarySavable(object);
+
+ // write out tag table
+ int classTableSize = 0;
+ int classNum = classes.keySet().size();
+ int aliasSize = ((int) FastMath.log(classNum, 256) + 1); // make all
+ // aliases a
+ // fixed width
+
+ os.write(ByteUtils.convertToBytes(classNum));
+ for (String key : classes.keySet()) {
+ BinaryClassObject bco = classes.get(key);
+
+ // write alias
+ byte[] aliasBytes = fixClassAlias(bco.alias,
+ aliasSize);
+ os.write(aliasBytes);
+ classTableSize += aliasSize;
+
+ // jME3 NEW: Write class hierarchy version numbers
+ os.write( bco.classHierarchyVersions.length );
+ for (int version : bco.classHierarchyVersions){
+ os.write(ByteUtils.convertToBytes(version));
+ }
+ classTableSize += 1 + bco.classHierarchyVersions.length * 4;
+
+ // write classname size & classname
+ byte[] classBytes = key.getBytes();
+ os.write(ByteUtils.convertToBytes(classBytes.length));
+ os.write(classBytes);
+ classTableSize += 4 + classBytes.length;
+
+ // for each field, write alias, type, and name
+ os.write(ByteUtils.convertToBytes(bco.nameFields.size()));
+ for (String fieldName : bco.nameFields.keySet()) {
+ BinaryClassField bcf = bco.nameFields.get(fieldName);
+ os.write(bcf.alias);
+ os.write(bcf.type);
+
+ // write classname size & classname
+ byte[] fNameBytes = fieldName.getBytes();
+ os.write(ByteUtils.convertToBytes(fNameBytes.length));
+ os.write(fNameBytes);
+ classTableSize += 2 + 4 + fNameBytes.length;
+ }
+ }
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ // write out data to a seperate stream
+ int location = 0;
+ // keep track of location for each piece
+ HashMap<String, ArrayList<BinaryIdContentPair>> alreadySaved = new HashMap<String, ArrayList<BinaryIdContentPair>>(
+ contentTable.size());
+ for (Savable savable : contentKeys) {
+ // look back at previous written data for matches
+ String savableName = savable.getClass().getName();
+ BinaryIdContentPair pair = contentTable.get(savable);
+ ArrayList<BinaryIdContentPair> bucket = alreadySaved
+ .get(savableName + getChunk(pair));
+ int prevLoc = findPrevMatch(pair, bucket);
+ if (prevLoc != -1) {
+ locationTable.put(pair.getId(), prevLoc);
+ continue;
+ }
+
+ locationTable.put(pair.getId(), location);
+ if (bucket == null) {
+ bucket = new ArrayList<BinaryIdContentPair>();
+ alreadySaved.put(savableName + getChunk(pair), bucket);
+ }
+ bucket.add(pair);
+ byte[] aliasBytes = fixClassAlias(classes.get(savableName).alias, aliasSize);
+ out.write(aliasBytes);
+ location += aliasSize;
+ BinaryOutputCapsule cap = contentTable.get(savable).getContent();
+ out.write(ByteUtils.convertToBytes(cap.bytes.length));
+ location += 4; // length of bytes
+ out.write(cap.bytes);
+ location += cap.bytes.length;
+ }
+
+ // write out location table
+ // tag/location
+ int numLocations = locationTable.keySet().size();
+ os.write(ByteUtils.convertToBytes(numLocations));
+ int locationTableSize = 0;
+ for (Integer key : locationTable.keySet()) {
+ os.write(ByteUtils.convertToBytes(key));
+ os.write(ByteUtils.convertToBytes(locationTable.get(key)));
+ locationTableSize += 8;
+ }
+
+ // write out number of root ids - hardcoded 1 for now
+ os.write(ByteUtils.convertToBytes(1));
+
+ // write out root id
+ os.write(ByteUtils.convertToBytes(id));
+
+ // append stream to the output stream
+ out.writeTo(os);
+
+
+ out = null;
+ os = null;
+
+ if (debug ) {
+ logger.info("Stats:");
+ logger.log(Level.INFO, "classes: {0}", classNum);
+ logger.log(Level.INFO, "class table: {0} bytes", classTableSize);
+ logger.log(Level.INFO, "objects: {0}", numLocations);
+ logger.log(Level.INFO, "location table: {0} bytes", locationTableSize);
+ logger.log(Level.INFO, "data: {0} bytes", location);
+ }
+
+ return true;
+ }
+
+ protected String getChunk(BinaryIdContentPair pair) {
+ return new String(pair.getContent().bytes, 0, Math.min(64, pair
+ .getContent().bytes.length));
+ }
+
+ protected int findPrevMatch(BinaryIdContentPair oldPair,
+ ArrayList<BinaryIdContentPair> bucket) {
+ if (bucket == null)
+ return -1;
+ for (int x = bucket.size(); --x >= 0;) {
+ BinaryIdContentPair pair = bucket.get(x);
+ if (pair.getContent().equals(oldPair.getContent()))
+ return locationTable.get(pair.getId());
+ }
+ return -1;
+ }
+
+ protected byte[] fixClassAlias(byte[] bytes, int width) {
+ if (bytes.length != width) {
+ byte[] newAlias = new byte[width];
+ for (int x = width - bytes.length; x < width; x++)
+ newAlias[x] = bytes[x - bytes.length];
+ return newAlias;
+ }
+ return bytes;
+ }
+
+ public boolean save(Savable object, File f) throws IOException {
+ File parentDirectory = f.getParentFile();
+ if(parentDirectory != null && !parentDirectory.exists()) {
+ parentDirectory.mkdirs();
+ }
+
+ FileOutputStream fos = new FileOutputStream(f);
+ boolean rVal = save(object, fos);
+ fos.close();
+ return rVal;
+ }
+
+ public BinaryOutputCapsule getCapsule(Savable object) {
+ return contentTable.get(object).getContent();
+ }
+
+ private BinaryClassObject createClassObject(Class clazz) throws IOException{
+ BinaryClassObject bco = new BinaryClassObject();
+ bco.alias = generateTag();
+ bco.nameFields = new HashMap<String, BinaryClassField>();
+ bco.classHierarchyVersions = SavableClassUtil.getSavableVersions(clazz);
+
+ classes.put(clazz.getName(), bco);
+
+ return bco;
+ }
+
+ public int processBinarySavable(Savable object) throws IOException {
+ if (object == null) {
+ return -1;
+ }
+ Class<? extends Savable> clazz = object.getClass();
+ BinaryClassObject bco = classes.get(object.getClass().getName());
+ // is this class been looked at before? in tagTable?
+ if (bco == null) {
+ bco = createClassObject(object.getClass());
+ }
+
+ // is object in contentTable?
+ if (contentTable.get(object) != null) {
+ return (contentTable.get(object).getId());
+ }
+ BinaryIdContentPair newPair = generateIdContentPair(bco);
+ BinaryIdContentPair old = contentTable.put(object, newPair);
+ if (old == null) {
+ contentKeys.add(object);
+ }
+ object.write(this);
+ newPair.getContent().finish();
+ return newPair.getId();
+
+ }
+
+ protected byte[] generateTag() {
+ int width = ((int) FastMath.log(aliasCount, 256) + 1);
+ int count = aliasCount;
+ aliasCount++;
+ byte[] bytes = new byte[width];
+ for (int x = width - 1; x >= 0; x--) {
+ int pow = (int) FastMath.pow(256, x);
+ int factor = count / pow;
+ bytes[width - x - 1] = (byte) factor;
+ count %= pow;
+ }
+ return bytes;
+ }
+
+ protected BinaryIdContentPair generateIdContentPair(BinaryClassObject bco) {
+ BinaryIdContentPair pair = new BinaryIdContentPair(idCount++,
+ new BinaryOutputCapsule(this, bco));
+ return pair;
+ }
+} \ No newline at end of file
diff --git a/engine/src/core-plugins/com/jme3/export/binary/BinaryIdContentPair.java b/engine/src/core-plugins/com/jme3/export/binary/BinaryIdContentPair.java
new file mode 100644
index 0000000..b999a3c
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/export/binary/BinaryIdContentPair.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.export.binary;
+
+class BinaryIdContentPair {
+
+ private int id;
+ private BinaryOutputCapsule content;
+
+ BinaryIdContentPair(int id, BinaryOutputCapsule content) {
+ this.id = id;
+ this.content = content;
+ }
+
+ BinaryOutputCapsule getContent() {
+ return content;
+ }
+
+ void setContent(BinaryOutputCapsule content) {
+ this.content = content;
+ }
+
+ int getId() {
+ return id;
+ }
+
+ void setId(int id) {
+ this.id = id;
+ }
+}
diff --git a/engine/src/core-plugins/com/jme3/export/binary/BinaryImporter.java b/engine/src/core-plugins/com/jme3/export/binary/BinaryImporter.java
new file mode 100644
index 0000000..7f94ba4
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/export/binary/BinaryImporter.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.export.binary;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetManager;
+import com.jme3.export.*;
+import com.jme3.math.FastMath;
+import java.io.*;
+import java.net.URL;
+import java.nio.ByteOrder;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author Joshua Slack
+ * @author Kirill Vainer - Version number, Fast buffer reading
+ */
+public final class BinaryImporter implements JmeImporter {
+ private static final Logger logger = Logger.getLogger(BinaryImporter.class
+ .getName());
+
+ private AssetManager assetManager;
+
+ //Key - alias, object - bco
+ private HashMap<String, BinaryClassObject> classes
+ = new HashMap<String, BinaryClassObject>();
+ //Key - id, object - the savable
+ private HashMap<Integer, Savable> contentTable
+ = new HashMap<Integer, Savable>();
+ //Key - savable, object - capsule
+ private IdentityHashMap<Savable, BinaryInputCapsule> capsuleTable
+ = new IdentityHashMap<Savable, BinaryInputCapsule>();
+ //Key - id, opject - location in the file
+ private HashMap<Integer, Integer> locationTable
+ = new HashMap<Integer, Integer>();
+
+ public static boolean debug = false;
+
+ private byte[] dataArray;
+ private int aliasWidth;
+ private int formatVersion;
+
+ private static final boolean fastRead = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN;
+
+ public BinaryImporter() {
+ }
+
+ public int getFormatVersion(){
+ return formatVersion;
+ }
+
+ public static boolean canUseFastBuffers(){
+ return fastRead;
+ }
+
+ public static BinaryImporter getInstance() {
+ return new BinaryImporter();
+ }
+
+ public void setAssetManager(AssetManager manager){
+ this.assetManager = manager;
+ }
+
+ public AssetManager getAssetManager(){
+ return assetManager;
+ }
+
+ public Object load(AssetInfo info){
+// if (!(info.getKey() instanceof ModelKey))
+// throw new IllegalArgumentException("Model assets must be loaded using a ModelKey");
+
+ assetManager = info.getManager();
+
+ InputStream is = null;
+ try {
+ is = info.openStream();
+ Savable s = load(is);
+
+ return s;
+ } catch (IOException ex) {
+ logger.log(Level.SEVERE, "An error occured while loading jME binary object", ex);
+ } finally {
+ if (is != null){
+ try {
+ is.close();
+ } catch (IOException ex) {}
+ }
+ }
+ return null;
+ }
+
+ public Savable load(InputStream is) throws IOException {
+ return load(is, null, null);
+ }
+
+ public Savable load(InputStream is, ReadListener listener) throws IOException {
+ return load(is, listener, null);
+ }
+
+ public Savable load(InputStream is, ReadListener listener, ByteArrayOutputStream baos) throws IOException {
+ contentTable.clear();
+ BufferedInputStream bis = new BufferedInputStream(is);
+
+ int numClasses;
+
+ // Try to read signature
+ int maybeSignature = ByteUtils.readInt(bis);
+ if (maybeSignature == FormatVersion.SIGNATURE){
+ // this is a new version J3O file
+ formatVersion = ByteUtils.readInt(bis);
+ numClasses = ByteUtils.readInt(bis);
+
+ // check if this binary is from the future
+ if (formatVersion > FormatVersion.VERSION){
+ throw new IOException("The binary file is of newer version than expected! " +
+ formatVersion + " > " + FormatVersion.VERSION);
+ }
+ }else{
+ // this is an old version J3O file
+ // the signature was actually the class count
+ numClasses = maybeSignature;
+
+ // 0 indicates version before we started adding
+ // version numbers
+ formatVersion = 0;
+ }
+
+ int bytes = 4;
+ aliasWidth = ((int)FastMath.log(numClasses, 256) + 1);
+
+ classes.clear();
+ for(int i = 0; i < numClasses; i++) {
+ String alias = readString(bis, aliasWidth);
+
+ // jME3 NEW: Read class version number
+ int[] classHierarchyVersions;
+ if (formatVersion >= 1){
+ int classHierarchySize = bis.read();
+ classHierarchyVersions = new int[classHierarchySize];
+ for (int j = 0; j < classHierarchySize; j++){
+ classHierarchyVersions[j] = ByteUtils.readInt(bis);
+ }
+ }else{
+ classHierarchyVersions = new int[]{ 0 };
+ }
+
+ // read classname and classname size
+ int classLength = ByteUtils.readInt(bis);
+ String className = readString(bis, classLength);
+
+ BinaryClassObject bco = new BinaryClassObject();
+ bco.alias = alias.getBytes();
+ bco.className = className;
+ bco.classHierarchyVersions = classHierarchyVersions;
+
+ int fields = ByteUtils.readInt(bis);
+ bytes += (8 + aliasWidth + classLength);
+
+ bco.nameFields = new HashMap<String, BinaryClassField>(fields);
+ bco.aliasFields = new HashMap<Byte, BinaryClassField>(fields);
+ for (int x = 0; x < fields; x++) {
+ byte fieldAlias = (byte)bis.read();
+ byte fieldType = (byte)bis.read();
+
+ int fieldNameLength = ByteUtils.readInt(bis);
+ String fieldName = readString(bis, fieldNameLength);
+ BinaryClassField bcf = new BinaryClassField(fieldName, fieldAlias, fieldType);
+ bco.nameFields.put(fieldName, bcf);
+ bco.aliasFields.put(fieldAlias, bcf);
+ bytes += (6 + fieldNameLength);
+ }
+ classes.put(alias, bco);
+ }
+ if (listener != null) listener.readBytes(bytes);
+
+ int numLocs = ByteUtils.readInt(bis);
+ bytes = 4;
+
+ capsuleTable.clear();
+ locationTable.clear();
+ for(int i = 0; i < numLocs; i++) {
+ int id = ByteUtils.readInt(bis);
+ int loc = ByteUtils.readInt(bis);
+ locationTable.put(id, loc);
+ bytes += 8;
+ }
+
+ @SuppressWarnings("unused")
+ int numbIDs = ByteUtils.readInt(bis); // XXX: NOT CURRENTLY USED
+ int id = ByteUtils.readInt(bis);
+ bytes += 8;
+ if (listener != null) listener.readBytes(bytes);
+
+ if (baos == null) {
+ baos = new ByteArrayOutputStream(bytes);
+ } else {
+ baos.reset();
+ }
+ int size = -1;
+ byte[] cache = new byte[4096];
+ while((size = bis.read(cache)) != -1) {
+ baos.write(cache, 0, size);
+ if (listener != null) listener.readBytes(size);
+ }
+ bis = null;
+
+ dataArray = baos.toByteArray();
+ baos = null;
+
+ Savable rVal = readObject(id);
+ if (debug) {
+ logger.info("Importer Stats: ");
+ logger.log(Level.INFO, "Tags: {0}", numClasses);
+ logger.log(Level.INFO, "Objects: {0}", numLocs);
+ logger.log(Level.INFO, "Data Size: {0}", dataArray.length);
+ }
+ dataArray = null;
+ return rVal;
+ }
+
+ public Savable load(URL f) throws IOException {
+ return load(f, null);
+ }
+
+ public Savable load(URL f, ReadListener listener) throws IOException {
+ InputStream is = f.openStream();
+ Savable rVal = load(is, listener);
+ is.close();
+ return rVal;
+ }
+
+ public Savable load(File f) throws IOException {
+ return load(f, null);
+ }
+
+ public Savable load(File f, ReadListener listener) throws IOException {
+ FileInputStream fis = new FileInputStream(f);
+ Savable rVal = load(fis, listener);
+ fis.close();
+ return rVal;
+ }
+
+ public Savable load(byte[] data) throws IOException {
+ ByteArrayInputStream bais = new ByteArrayInputStream(data);
+ Savable rVal = load(bais);
+ bais.close();
+ return rVal;
+ }
+
+ @Override
+ public InputCapsule getCapsule(Savable id) {
+ return capsuleTable.get(id);
+ }
+
+ protected String readString(InputStream f, int length) throws IOException {
+ byte[] data = new byte[length];
+ for(int j = 0; j < length; j++) {
+ data[j] = (byte)f.read();
+ }
+
+ return new String(data);
+ }
+
+ protected String readString(int length, int offset) throws IOException {
+ byte[] data = new byte[length];
+ for(int j = 0; j < length; j++) {
+ data[j] = dataArray[j+offset];
+ }
+
+ return new String(data);
+ }
+
+ public Savable readObject(int id) {
+
+ if(contentTable.get(id) != null) {
+ return contentTable.get(id);
+ }
+
+ try {
+ int loc = locationTable.get(id);
+
+ String alias = readString(aliasWidth, loc);
+ loc+=aliasWidth;
+
+ BinaryClassObject bco = classes.get(alias);
+
+ if(bco == null) {
+ logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "NULL class object: " + alias);
+ return null;
+ }
+
+ int dataLength = ByteUtils.convertIntFromBytes(dataArray, loc);
+ loc+=4;
+
+ Savable out = null;
+ if (assetManager != null) {
+ out = SavableClassUtil.fromName(bco.className, assetManager.getClassLoaders());
+ } else {
+ out = SavableClassUtil.fromName(bco.className);
+ }
+
+ BinaryInputCapsule cap = new BinaryInputCapsule(this, out, bco);
+ cap.setContent(dataArray, loc, loc+dataLength);
+
+ capsuleTable.put(out, cap);
+ contentTable.put(id, out);
+
+ out.read(this);
+
+ capsuleTable.remove(out);
+
+ return out;
+
+ } catch (IOException e) {
+ logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "Exception", e);
+ return null;
+ } catch (ClassNotFoundException e) {
+ logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "Exception", e);
+ return null;
+ } catch (InstantiationException e) {
+ logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "Exception", e);
+ return null;
+ } catch (IllegalAccessException e) {
+ logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "Exception", e);
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/engine/src/core-plugins/com/jme3/export/binary/BinaryInputCapsule.java b/engine/src/core-plugins/com/jme3/export/binary/BinaryInputCapsule.java
new file mode 100644
index 0000000..cebd764
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/export/binary/BinaryInputCapsule.java
@@ -0,0 +1,1380 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.export.binary;
+
+import com.jme3.export.InputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.export.SavableClassUtil;
+import com.jme3.util.BufferUtils;
+import com.jme3.util.IntMap;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.ShortBuffer;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author Joshua Slack
+ */
+final class BinaryInputCapsule implements InputCapsule {
+
+ private static final Logger logger = Logger
+ .getLogger(BinaryInputCapsule.class.getName());
+
+ protected BinaryImporter importer;
+ protected BinaryClassObject cObj;
+ protected Savable savable;
+ protected HashMap<Byte, Object> fieldData;
+
+ protected int index = 0;
+
+ public BinaryInputCapsule(BinaryImporter importer, Savable savable, BinaryClassObject bco) {
+ this.importer = importer;
+ this.cObj = bco;
+ this.savable = savable;
+ }
+
+ public void setContent(byte[] content, int start, int limit) {
+ fieldData = new HashMap<Byte, Object>();
+ for (index = start; index < limit;) {
+ byte alias = content[index];
+
+ index++;
+
+ try {
+ byte type = cObj.aliasFields.get(alias).type;
+ Object value = null;
+
+ switch (type) {
+ case BinaryClassField.BITSET: {
+ value = readBitSet(content);
+ break;
+ }
+ case BinaryClassField.BOOLEAN: {
+ value = readBoolean(content);
+ break;
+ }
+ case BinaryClassField.BOOLEAN_1D: {
+ value = readBooleanArray(content);
+ break;
+ }
+ case BinaryClassField.BOOLEAN_2D: {
+ value = readBooleanArray2D(content);
+ break;
+ }
+ case BinaryClassField.BYTE: {
+ value = readByte(content);
+ break;
+ }
+ case BinaryClassField.BYTE_1D: {
+ value = readByteArray(content);
+ break;
+ }
+ case BinaryClassField.BYTE_2D: {
+ value = readByteArray2D(content);
+ break;
+ }
+ case BinaryClassField.BYTEBUFFER: {
+ value = readByteBuffer(content);
+ break;
+ }
+ case BinaryClassField.DOUBLE: {
+ value = readDouble(content);
+ break;
+ }
+ case BinaryClassField.DOUBLE_1D: {
+ value = readDoubleArray(content);
+ break;
+ }
+ case BinaryClassField.DOUBLE_2D: {
+ value = readDoubleArray2D(content);
+ break;
+ }
+ case BinaryClassField.FLOAT: {
+ value = readFloat(content);
+ break;
+ }
+ case BinaryClassField.FLOAT_1D: {
+ value = readFloatArray(content);
+ break;
+ }
+ case BinaryClassField.FLOAT_2D: {
+ value = readFloatArray2D(content);
+ break;
+ }
+ case BinaryClassField.FLOATBUFFER: {
+ value = readFloatBuffer(content);
+ break;
+ }
+ case BinaryClassField.FLOATBUFFER_ARRAYLIST: {
+ value = readFloatBufferArrayList(content);
+ break;
+ }
+ case BinaryClassField.BYTEBUFFER_ARRAYLIST: {
+ value = readByteBufferArrayList(content);
+ break;
+ }
+ case BinaryClassField.INT: {
+ value = readInt(content);
+ break;
+ }
+ case BinaryClassField.INT_1D: {
+ value = readIntArray(content);
+ break;
+ }
+ case BinaryClassField.INT_2D: {
+ value = readIntArray2D(content);
+ break;
+ }
+ case BinaryClassField.INTBUFFER: {
+ value = readIntBuffer(content);
+ break;
+ }
+ case BinaryClassField.LONG: {
+ value = readLong(content);
+ break;
+ }
+ case BinaryClassField.LONG_1D: {
+ value = readLongArray(content);
+ break;
+ }
+ case BinaryClassField.LONG_2D: {
+ value = readLongArray2D(content);
+ break;
+ }
+ case BinaryClassField.SAVABLE: {
+ value = readSavable(content);
+ break;
+ }
+ case BinaryClassField.SAVABLE_1D: {
+ value = readSavableArray(content);
+ break;
+ }
+ case BinaryClassField.SAVABLE_2D: {
+ value = readSavableArray2D(content);
+ break;
+ }
+ case BinaryClassField.SAVABLE_ARRAYLIST: {
+ value = readSavableArray(content);
+ break;
+ }
+ case BinaryClassField.SAVABLE_ARRAYLIST_1D: {
+ value = readSavableArray2D(content);
+ break;
+ }
+ case BinaryClassField.SAVABLE_ARRAYLIST_2D: {
+ value = readSavableArray3D(content);
+ break;
+ }
+ case BinaryClassField.SAVABLE_MAP: {
+ value = readSavableMap(content);
+ break;
+ }
+ case BinaryClassField.STRING_SAVABLE_MAP: {
+ value = readStringSavableMap(content);
+ break;
+ }
+ case BinaryClassField.INT_SAVABLE_MAP: {
+ value = readIntSavableMap(content);
+ break;
+ }
+ case BinaryClassField.SHORT: {
+ value = readShort(content);
+ break;
+ }
+ case BinaryClassField.SHORT_1D: {
+ value = readShortArray(content);
+ break;
+ }
+ case BinaryClassField.SHORT_2D: {
+ value = readShortArray2D(content);
+ break;
+ }
+ case BinaryClassField.SHORTBUFFER: {
+ value = readShortBuffer(content);
+ break;
+ }
+ case BinaryClassField.STRING: {
+ value = readString(content);
+ break;
+ }
+ case BinaryClassField.STRING_1D: {
+ value = readStringArray(content);
+ break;
+ }
+ case BinaryClassField.STRING_2D: {
+ value = readStringArray2D(content);
+ break;
+ }
+
+ default:
+ // skip put statement
+ continue;
+ }
+
+ fieldData.put(alias, value);
+
+ } catch (IOException e) {
+ logger.logp(Level.SEVERE, this.getClass().toString(),
+ "setContent(byte[] content)", "Exception", e);
+ }
+ }
+ }
+
+ public int getSavableVersion(Class<? extends Savable> desiredClass){
+ return SavableClassUtil.getSavedSavableVersion(savable, desiredClass,
+ cObj.classHierarchyVersions, importer.getFormatVersion());
+ }
+
+ public BitSet readBitSet(String name, BitSet defVal) throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (BitSet) fieldData.get(field.alias);
+ }
+
+ public boolean readBoolean(String name, boolean defVal) throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return ((Boolean) fieldData.get(field.alias)).booleanValue();
+ }
+
+ public boolean[] readBooleanArray(String name, boolean[] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (boolean[]) fieldData.get(field.alias);
+ }
+
+ public boolean[][] readBooleanArray2D(String name, boolean[][] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (boolean[][]) fieldData.get(field.alias);
+ }
+
+ public byte readByte(String name, byte defVal) throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return ((Byte) fieldData.get(field.alias)).byteValue();
+ }
+
+ public byte[] readByteArray(String name, byte[] defVal) throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (byte[]) fieldData.get(field.alias);
+ }
+
+ public byte[][] readByteArray2D(String name, byte[][] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (byte[][]) fieldData.get(field.alias);
+ }
+
+ public ByteBuffer readByteBuffer(String name, ByteBuffer defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (ByteBuffer) fieldData.get(field.alias);
+ }
+
+ @SuppressWarnings("unchecked")
+ public ArrayList<ByteBuffer> readByteBufferArrayList(String name,
+ ArrayList<ByteBuffer> defVal) throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (ArrayList<ByteBuffer>) fieldData.get(field.alias);
+ }
+
+ public double readDouble(String name, double defVal) throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return ((Double) fieldData.get(field.alias)).doubleValue();
+ }
+
+ public double[] readDoubleArray(String name, double[] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (double[]) fieldData.get(field.alias);
+ }
+
+ public double[][] readDoubleArray2D(String name, double[][] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (double[][]) fieldData.get(field.alias);
+ }
+
+ public float readFloat(String name, float defVal) throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return ((Float) fieldData.get(field.alias)).floatValue();
+ }
+
+ public float[] readFloatArray(String name, float[] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (float[]) fieldData.get(field.alias);
+ }
+
+ public float[][] readFloatArray2D(String name, float[][] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (float[][]) fieldData.get(field.alias);
+ }
+
+ public FloatBuffer readFloatBuffer(String name, FloatBuffer defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (FloatBuffer) fieldData.get(field.alias);
+ }
+
+ @SuppressWarnings("unchecked")
+ public ArrayList<FloatBuffer> readFloatBufferArrayList(String name,
+ ArrayList<FloatBuffer> defVal) throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (ArrayList<FloatBuffer>) fieldData.get(field.alias);
+ }
+
+ public int readInt(String name, int defVal) throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return ((Integer) fieldData.get(field.alias)).intValue();
+ }
+
+ public int[] readIntArray(String name, int[] defVal) throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (int[]) fieldData.get(field.alias);
+ }
+
+ public int[][] readIntArray2D(String name, int[][] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (int[][]) fieldData.get(field.alias);
+ }
+
+ public IntBuffer readIntBuffer(String name, IntBuffer defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (IntBuffer) fieldData.get(field.alias);
+ }
+
+ public long readLong(String name, long defVal) throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return ((Long) fieldData.get(field.alias)).longValue();
+ }
+
+ public long[] readLongArray(String name, long[] defVal) throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (long[]) fieldData.get(field.alias);
+ }
+
+ public long[][] readLongArray2D(String name, long[][] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (long[][]) fieldData.get(field.alias);
+ }
+
+ public Savable readSavable(String name, Savable defVal) throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ Object value = fieldData.get(field.alias);
+ if (value == null)
+ return null;
+ else if (value instanceof ID) {
+ value = importer.readObject(((ID) value).id);
+ fieldData.put(field.alias, value);
+ return (Savable) value;
+ } else
+ return defVal;
+ }
+
+ public Savable[] readSavableArray(String name, Savable[] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ Object[] values = (Object[]) fieldData.get(field.alias);
+ if (values instanceof ID[]) {
+ values = resolveIDs(values);
+ fieldData.put(field.alias, values);
+ return (Savable[]) values;
+ } else
+ return defVal;
+ }
+
+ private Savable[] resolveIDs(Object[] values) {
+ if (values != null) {
+ Savable[] savables = new Savable[values.length];
+ for (int i = 0; i < values.length; i++) {
+ final ID id = (ID) values[i];
+ savables[i] = id != null ? importer.readObject(id.id) : null;
+ }
+ return savables;
+ } else {
+ return null;
+ }
+ }
+
+ public Savable[][] readSavableArray2D(String name, Savable[][] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null ||!fieldData.containsKey(field.alias))
+ return defVal;
+ Object[][] values = (Object[][]) fieldData.get(field.alias);
+ if (values instanceof ID[][]) {
+ Savable[][] savables = new Savable[values.length][];
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] != null) {
+ savables[i] = resolveIDs(values[i]);
+ } else savables[i] = null;
+ }
+ values = savables;
+ fieldData.put(field.alias, values);
+ }
+ return (Savable[][]) values;
+ }
+
+ public Savable[][][] readSavableArray3D(String name, Savable[][][] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ Object[][][] values = (Object[][][]) fieldData.get(field.alias);
+ if (values instanceof ID[][][]) {
+ Savable[][][] savables = new Savable[values.length][][];
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] != null) {
+ savables[i] = new Savable[values[i].length][];
+ for (int j = 0; j < values[i].length; j++) {
+ savables[i][j] = resolveIDs(values[i][j]);
+ }
+ } else savables[i] = null;
+ }
+ fieldData.put(field.alias, savables);
+ return savables;
+ } else
+ return defVal;
+ }
+
+ private ArrayList<Savable> savableArrayListFromArray(Savable[] savables) {
+ if(savables == null) {
+ return null;
+ }
+ ArrayList<Savable> arrayList = new ArrayList<Savable>(savables.length);
+ for (int x = 0; x < savables.length; x++) {
+ arrayList.add(savables[x]);
+ }
+ return arrayList;
+ }
+
+ // Assumes array of size 2 arrays where pos 0 is key and pos 1 is value.
+ private Map<Savable, Savable> savableMapFrom2DArray(Savable[][] savables) {
+ if(savables == null) {
+ return null;
+ }
+ Map<Savable, Savable> map = new HashMap<Savable, Savable>(savables.length);
+ for (int x = 0; x < savables.length; x++) {
+ map.put(savables[x][0], savables[x][1]);
+ }
+ return map;
+ }
+
+ private Map<String, Savable> stringSavableMapFromKV(String[] keys, Savable[] values) {
+ if(keys == null || values == null) {
+ return null;
+ }
+
+ Map<String, Savable> map = new HashMap<String, Savable>(keys.length);
+ for (int x = 0; x < keys.length; x++)
+ map.put(keys[x], values[x]);
+
+ return map;
+ }
+
+ private IntMap<Savable> intSavableMapFromKV(int[] keys, Savable[] values) {
+ if(keys == null || values == null) {
+ return null;
+ }
+
+ IntMap<Savable> map = new IntMap<Savable>(keys.length);
+ for (int x = 0; x < keys.length; x++)
+ map.put(keys[x], values[x]);
+
+ return map;
+ }
+
+ public ArrayList readSavableArrayList(String name, ArrayList defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ Object value = fieldData.get(field.alias);
+ if (value instanceof ID[]) {
+ // read Savable array and convert to ArrayList
+ Savable[] savables = readSavableArray(name, null);
+ value = savableArrayListFromArray(savables);
+ fieldData.put(field.alias, value);
+ }
+ return (ArrayList) value;
+ }
+
+ public ArrayList[] readSavableArrayListArray(String name, ArrayList[] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ Object value = fieldData.get(field.alias);
+ if (value instanceof ID[][]) {
+ // read 2D Savable array and convert to ArrayList array
+ Savable[][] savables = readSavableArray2D(name, null);
+ if (savables != null) {
+ ArrayList[] arrayLists = new ArrayList[savables.length];
+ for (int i = 0; i < savables.length; i++) {
+ arrayLists[i] = savableArrayListFromArray(savables[i]);
+ }
+ value = arrayLists;
+ } else
+ value = defVal;
+ fieldData.put(field.alias, value);
+ }
+ return (ArrayList[]) value;
+ }
+
+ public ArrayList[][] readSavableArrayListArray2D(String name,
+ ArrayList[][] defVal) throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ Object value = fieldData.get(field.alias);
+ if (value instanceof ID[][][]) {
+ // read 3D Savable array and convert to 2D ArrayList array
+ Savable[][][] savables = readSavableArray3D(name, null);
+ if (savables != null && savables.length > 0) {
+ ArrayList[][] arrayLists = new ArrayList[savables.length][];
+ for (int i = 0; i < savables.length; i++) {
+ arrayLists[i] = new ArrayList[savables[i].length];
+ for (int j = 0; j < savables[i].length; j++) {
+ arrayLists[i][j] = savableArrayListFromArray(savables[i][j]);
+ }
+ }
+ value = arrayLists;
+ } else
+ value = defVal;
+ fieldData.put(field.alias, value);
+ }
+ return (ArrayList[][]) value;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Map<? extends Savable, ? extends Savable> readSavableMap(String name, Map<? extends Savable, ? extends Savable> defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ Object value = fieldData.get(field.alias);
+ if (value instanceof ID[][]) {
+ // read Savable array and convert to Map
+ Savable[][] savables = readSavableArray2D(name, null);
+ value = savableMapFrom2DArray(savables);
+ fieldData.put(field.alias, value);
+ }
+ return (Map<? extends Savable, ? extends Savable>) value;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Map<String, ? extends Savable> readStringSavableMap(String name, Map<String, ? extends Savable> defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ Object value = fieldData.get(field.alias);
+ if (value instanceof StringIDMap) {
+ // read Savable array and convert to Map values
+ StringIDMap in = (StringIDMap) value;
+ Savable[] values = resolveIDs(in.values);
+ value = stringSavableMapFromKV(in.keys, values);
+ fieldData.put(field.alias, value);
+ }
+ return (Map<String, Savable>) value;
+ }
+
+ @SuppressWarnings("unchecked")
+ public IntMap<? extends Savable> readIntSavableMap(String name, IntMap<? extends Savable> defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ Object value = fieldData.get(field.alias);
+ if (value instanceof IntIDMap) {
+ // read Savable array and convert to Map values
+ IntIDMap in = (IntIDMap) value;
+ Savable[] values = resolveIDs(in.values);
+ value = intSavableMapFromKV(in.keys, values);
+ fieldData.put(field.alias, value);
+ }
+ return (IntMap<Savable>) value;
+ }
+
+ public short readShort(String name, short defVal) throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return ((Short) fieldData.get(field.alias)).shortValue();
+ }
+
+ public short[] readShortArray(String name, short[] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (short[]) fieldData.get(field.alias);
+ }
+
+ public short[][] readShortArray2D(String name, short[][] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (short[][]) fieldData.get(field.alias);
+ }
+
+ public ShortBuffer readShortBuffer(String name, ShortBuffer defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (ShortBuffer) fieldData.get(field.alias);
+ }
+
+ public String readString(String name, String defVal) throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (String) fieldData.get(field.alias);
+ }
+
+ public String[] readStringArray(String name, String[] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (String[]) fieldData.get(field.alias);
+ }
+
+ public String[][] readStringArray2D(String name, String[][] defVal)
+ throws IOException {
+ BinaryClassField field = cObj.nameFields.get(name);
+ if (field == null || !fieldData.containsKey(field.alias))
+ return defVal;
+ return (String[][]) fieldData.get(field.alias);
+ }
+
+ // byte primitive
+
+ protected byte readByte(byte[] content) throws IOException {
+ byte value = content[index];
+ index++;
+ return value;
+ }
+
+ protected byte readByteForBuffer(byte[] content) throws IOException {
+ byte value = content[index];
+ index++;
+ return value;
+ }
+
+ protected byte[] readByteArray(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ byte[] value = new byte[length];
+ for (int x = 0; x < length; x++)
+ value[x] = readByte(content);
+ return value;
+ }
+
+ protected byte[][] readByteArray2D(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ byte[][] value = new byte[length][];
+ for (int x = 0; x < length; x++)
+ value[x] = readByteArray(content);
+ return value;
+ }
+
+ // int primitive
+
+ protected int readIntForBuffer(byte[] content){
+ int number = ((content[index+3] & 0xFF) << 24)
+ + ((content[index+2] & 0xFF) << 16)
+ + ((content[index+1] & 0xFF) << 8)
+ + (content[index] & 0xFF);
+ index += 4;
+ return number;
+ }
+
+ protected int readInt(byte[] content) throws IOException {
+ byte[] bytes = inflateFrom(content, index);
+ index += 1 + bytes.length;
+ bytes = ByteUtils.rightAlignBytes(bytes, 4);
+ int value = ByteUtils.convertIntFromBytes(bytes);
+ if (value == BinaryOutputCapsule.NULL_OBJECT
+ || value == BinaryOutputCapsule.DEFAULT_OBJECT)
+ index -= 4;
+ return value;
+ }
+
+ protected int[] readIntArray(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ int[] value = new int[length];
+ for (int x = 0; x < length; x++)
+ value[x] = readInt(content);
+ return value;
+ }
+
+ protected int[][] readIntArray2D(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ int[][] value = new int[length][];
+ for (int x = 0; x < length; x++)
+ value[x] = readIntArray(content);
+ return value;
+ }
+
+ // float primitive
+
+ protected float readFloat(byte[] content) throws IOException {
+ float value = ByteUtils.convertFloatFromBytes(content, index);
+ index += 4;
+ return value;
+ }
+
+ protected float readFloatForBuffer(byte[] content) throws IOException {
+ int number = readIntForBuffer(content);
+ return Float.intBitsToFloat(number);
+ }
+
+ protected float[] readFloatArray(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ float[] value = new float[length];
+ for (int x = 0; x < length; x++)
+ value[x] = readFloat(content);
+ return value;
+ }
+
+ protected float[][] readFloatArray2D(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ float[][] value = new float[length][];
+ for (int x = 0; x < length; x++)
+ value[x] = readFloatArray(content);
+ return value;
+ }
+
+ // double primitive
+
+ protected double readDouble(byte[] content) throws IOException {
+ double value = ByteUtils.convertDoubleFromBytes(content, index);
+ index += 8;
+ return value;
+ }
+
+ protected double[] readDoubleArray(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ double[] value = new double[length];
+ for (int x = 0; x < length; x++)
+ value[x] = readDouble(content);
+ return value;
+ }
+
+ protected double[][] readDoubleArray2D(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ double[][] value = new double[length][];
+ for (int x = 0; x < length; x++)
+ value[x] = readDoubleArray(content);
+ return value;
+ }
+
+ // long primitive
+
+ protected long readLong(byte[] content) throws IOException {
+ byte[] bytes = inflateFrom(content, index);
+ index += 1 + bytes.length;
+ bytes = ByteUtils.rightAlignBytes(bytes, 8);
+ long value = ByteUtils.convertLongFromBytes(bytes);
+ return value;
+ }
+
+ protected long[] readLongArray(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ long[] value = new long[length];
+ for (int x = 0; x < length; x++)
+ value[x] = readLong(content);
+ return value;
+ }
+
+ protected long[][] readLongArray2D(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ long[][] value = new long[length][];
+ for (int x = 0; x < length; x++)
+ value[x] = readLongArray(content);
+ return value;
+ }
+
+ // short primitive
+
+ protected short readShort(byte[] content) throws IOException {
+ short value = ByteUtils.convertShortFromBytes(content, index);
+ index += 2;
+ return value;
+ }
+
+ protected short readShortForBuffer(byte[] content) throws IOException {
+ short number = (short) ((content[index+0] & 0xFF)
+ + ((content[index+1] & 0xFF) << 8));
+ index += 2;
+ return number;
+ }
+
+ protected short[] readShortArray(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ short[] value = new short[length];
+ for (int x = 0; x < length; x++)
+ value[x] = readShort(content);
+ return value;
+ }
+
+ protected short[][] readShortArray2D(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ short[][] value = new short[length][];
+ for (int x = 0; x < length; x++)
+ value[x] = readShortArray(content);
+ return value;
+ }
+
+ // boolean primitive
+
+ protected boolean readBoolean(byte[] content) throws IOException {
+ boolean value = ByteUtils.convertBooleanFromBytes(content, index);
+ index += 1;
+ return value;
+ }
+
+ protected boolean[] readBooleanArray(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ boolean[] value = new boolean[length];
+ for (int x = 0; x < length; x++)
+ value[x] = readBoolean(content);
+ return value;
+ }
+
+ protected boolean[][] readBooleanArray2D(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ boolean[][] value = new boolean[length][];
+ for (int x = 0; x < length; x++)
+ value[x] = readBooleanArray(content);
+ return value;
+ }
+
+ /*
+ * UTF-8 crash course:
+ *
+ * UTF-8 codepoints map to UTF-16 codepoints and vv, which is what Java uses for it's Strings.
+ * (so a UTF-8 codepoint can contain all possible values for a Java char)
+ *
+ * A UTF-8 codepoint can be 1, 2 or 3 bytes long. How long a codepint is can be told by reading the first byte:
+ * b < 0x80, 1 byte
+ * (b & 0xC0) == 0xC0, 2 bytes
+ * (b & 0xE0) == 0xE0, 3 bytes
+ *
+ * However there is an additional restriction to UTF-8, to enable you to find the start of a UTF-8 codepoint,
+ * if you start reading at a random point in a UTF-8 byte stream. That's why UTF-8 requires for the second and third byte of
+ * a multibyte codepoint:
+ * (b & 0x80) == 0x80 (in other words, first bit must be 1)
+ */
+ private final static int UTF8_START = 0; // next byte should be the start of a new
+ private final static int UTF8_2BYTE = 2; // next byte should be the second byte of a 2 byte codepoint
+ private final static int UTF8_3BYTE_1 = 3; // next byte should be the second byte of a 3 byte codepoint
+ private final static int UTF8_3BYTE_2 = 4; // next byte should be the third byte of a 3 byte codepoint
+ private final static int UTF8_ILLEGAL = 10; // not an UTF8 string
+
+ // String
+ protected String readString(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+
+ /*
+ * @see ISSUE 276
+ *
+ * We'll transfer the bytes into a seperate byte array.
+ * While we do that we'll take the opportunity to check if the byte data is valid UTF-8.
+ *
+ * If it is not UTF-8 it is most likely saved with the BinaryOutputCapsule bug, that saves Strings using their native
+ * encoding. Unfortunatly there is no way to know what encoding was used, so we'll parse using the most common one in
+ * that case; latin-1 aka ISO8859_1
+ *
+ * Encoding of "low" ASCII codepoint (in plain speak: when no special characters are used) will usually look the same
+ * for UTF-8 and the other 1 byte codepoint encodings (espc true for numbers and regular letters of the alphabet). So these
+ * are valid UTF-8 and will give the same result (at most a few charakters will appear different, such as the euro sign).
+ *
+ * However, when "high" codepoints are used (any codepoint that over 0x7F, in other words where the first bit is a 1) it's
+ * a different matter and UTF-8 and the 1 byte encoding greatly will differ, as well as most 1 byte encodings relative to each
+ * other.
+ *
+ * It is impossible to detect which one-byte encoding is used. Since UTF8 and practically all 1-byte encodings share the most
+ * used characters (the "none-high" ones) parsing them will give the same result. However, not all byte sequences are legal in
+ * UTF-8 (see explantion above). If not UTF-8 encoded content is detected we therefor fallback on latin1. We also log a warning.
+ *
+ * By this method we detect all use of 1 byte encoding if they:
+ * - use a "high" codepoint after a "low" codepoint or a sequence of codepoints that is valid as UTF-8 bytes, that starts with 1000
+ * - use a "low" codepoint after a "high" codepoint
+ * - use a "low" codepoint after "high" codepoint, after a "high" codepoint that starts with 1110
+ *
+ * In practise this means that unless 2 or 3 "high" codepoints are used after each other in proper order, we'll detect the string
+ * was not originally UTF-8 encoded.
+ *
+ */
+ byte[] bytes = new byte[length];
+ int utf8State = UTF8_START;
+ int b;
+ for (int x = 0; x < length; x++) {
+ bytes[x] = content[index++];
+ b = (int) bytes[x] & 0xFF; // unsign our byte
+
+ switch (utf8State) {
+ case UTF8_START:
+ if (b < 0x80) {
+ // good
+ }
+ else if ((b & 0xC0) == 0xC0) {
+ utf8State = UTF8_2BYTE;
+ }
+ else if ((b & 0xE0) == 0xE0) {
+ utf8State = UTF8_3BYTE_1;
+ }
+ else {
+ utf8State = UTF8_ILLEGAL;
+ }
+ break;
+ case UTF8_3BYTE_1:
+ case UTF8_3BYTE_2:
+ case UTF8_2BYTE:
+ if ((b & 0x80) == 0x80)
+ utf8State = utf8State == UTF8_3BYTE_1 ? UTF8_3BYTE_2 : UTF8_START;
+ else
+ utf8State = UTF8_ILLEGAL;
+ break;
+ }
+ }
+
+ try {
+ // even though so far the parsing might have been a legal UTF-8 sequence, only if a codepoint is fully given is it correct UTF-8
+ if (utf8State == UTF8_START) {
+ // Java misspells UTF-8 as UTF8 for official use in java.lang
+ return new String(bytes, "UTF8");
+ }
+ else {
+ logger.log(
+ Level.WARNING,
+ "Your export has been saved with an incorrect encoding for it's String fields which means it might not load correctly " +
+ "due to encoding issues. You should probably re-export your work. See ISSUE 276 in the jME issue tracker."
+ );
+ // We use ISO8859_1 to be consistent across platforms. We could default to native encoding, but this would lead to inconsistent
+ // behaviour across platforms!
+ // Developers that have previously saved their exports using the old exporter (wich uses native encoding), can temporarly
+ // remove the ""ISO8859_1" parameter, and change the above if statement to "if (false)".
+ // They should then import and re-export their models using the same enviroment they were orginally created in.
+ return new String(bytes, "ISO8859_1");
+ }
+ } catch (UnsupportedEncodingException uee) {
+ // as a last resort fall back to platform native.
+ // JavaDoc is vague about what happens when a decoding a String that contains un undecodable sequence
+ // it also doesn't specify which encodings have to be supported (though UTF-8 and ISO8859 have been in the SUN JRE since at least 1.1)
+ logger.log(
+ Level.SEVERE,
+ "Your export has been saved with an incorrect encoding or your version of Java is unable to decode the stored string. " +
+ "While your export may load correctly by falling back, using it on different platforms or java versions might lead to "+
+ "very strange inconsitenties. You should probably re-export your work. See ISSUE 276 in the jME issue tracker."
+ );
+ return new String(bytes);
+ }
+ }
+
+ protected String[] readStringArray(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ String[] value = new String[length];
+ for (int x = 0; x < length; x++)
+ value[x] = readString(content);
+ return value;
+ }
+
+ protected String[][] readStringArray2D(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ String[][] value = new String[length][];
+ for (int x = 0; x < length; x++)
+ value[x] = readStringArray(content);
+ return value;
+ }
+
+ // BitSet
+
+ protected BitSet readBitSet(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ BitSet value = new BitSet(length);
+ for (int x = 0; x < length; x++)
+ value.set(x, readBoolean(content));
+ return value;
+ }
+
+ // INFLATOR for int and long
+
+ protected static byte[] inflateFrom(byte[] contents, int index) {
+ byte firstByte = contents[index];
+ if (firstByte == BinaryOutputCapsule.NULL_OBJECT)
+ return ByteUtils.convertToBytes(BinaryOutputCapsule.NULL_OBJECT);
+ else if (firstByte == BinaryOutputCapsule.DEFAULT_OBJECT)
+ return ByteUtils.convertToBytes(BinaryOutputCapsule.DEFAULT_OBJECT);
+ else if (firstByte == 0)
+ return new byte[0];
+ else {
+ byte[] rVal = new byte[firstByte];
+ for (int x = 0; x < rVal.length; x++)
+ rVal[x] = contents[x + 1 + index];
+ return rVal;
+ }
+ }
+
+ // BinarySavable
+
+ protected ID readSavable(byte[] content) throws IOException {
+ int id = readInt(content);
+ if (id == BinaryOutputCapsule.NULL_OBJECT) {
+ return null;
+ }
+
+ return new ID(id);
+ }
+
+ // BinarySavable array
+
+ protected ID[] readSavableArray(byte[] content) throws IOException {
+ int elements = readInt(content);
+ if (elements == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ ID[] rVal = new ID[elements];
+ for (int x = 0; x < elements; x++) {
+ rVal[x] = readSavable(content);
+ }
+ return rVal;
+ }
+
+ protected ID[][] readSavableArray2D(byte[] content) throws IOException {
+ int elements = readInt(content);
+ if (elements == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ ID[][] rVal = new ID[elements][];
+ for (int x = 0; x < elements; x++) {
+ rVal[x] = readSavableArray(content);
+ }
+ return rVal;
+ }
+
+ protected ID[][][] readSavableArray3D(byte[] content) throws IOException {
+ int elements = readInt(content);
+ if (elements == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ ID[][][] rVal = new ID[elements][][];
+ for (int x = 0; x < elements; x++) {
+ rVal[x] = readSavableArray2D(content);
+ }
+ return rVal;
+ }
+
+ // BinarySavable map
+
+ protected ID[][] readSavableMap(byte[] content) throws IOException {
+ int elements = readInt(content);
+ if (elements == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ ID[][] rVal = new ID[elements][];
+ for (int x = 0; x < elements; x++) {
+ rVal[x] = readSavableArray(content);
+ }
+ return rVal;
+ }
+
+ protected StringIDMap readStringSavableMap(byte[] content) throws IOException {
+ int elements = readInt(content);
+ if (elements == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ String[] keys = readStringArray(content);
+ ID[] values = readSavableArray(content);
+ StringIDMap rVal = new StringIDMap();
+ rVal.keys = keys;
+ rVal.values = values;
+ return rVal;
+ }
+
+ protected IntIDMap readIntSavableMap(byte[] content) throws IOException {
+ int elements = readInt(content);
+ if (elements == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+ int[] keys = readIntArray(content);
+ ID[] values = readSavableArray(content);
+ IntIDMap rVal = new IntIDMap();
+ rVal.keys = keys;
+ rVal.values = values;
+ return rVal;
+ }
+
+
+ // ArrayList<FloatBuffer>
+
+ protected ArrayList<FloatBuffer> readFloatBufferArrayList(byte[] content)
+ throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT) {
+ return null;
+ }
+ ArrayList<FloatBuffer> rVal = new ArrayList<FloatBuffer>(length);
+ for (int x = 0; x < length; x++) {
+ rVal.add(readFloatBuffer(content));
+ }
+ return rVal;
+ }
+
+ // ArrayList<ByteBuffer>
+
+ protected ArrayList<ByteBuffer> readByteBufferArrayList(byte[] content)
+ throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT) {
+ return null;
+ }
+ ArrayList<ByteBuffer> rVal = new ArrayList<ByteBuffer>(length);
+ for (int x = 0; x < length; x++) {
+ rVal.add(readByteBuffer(content));
+ }
+ return rVal;
+ }
+
+ // NIO BUFFERS
+ // float buffer
+
+ protected FloatBuffer readFloatBuffer(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+
+ if (BinaryImporter.canUseFastBuffers()){
+ ByteBuffer value = BufferUtils.createByteBuffer(length * 4);
+ value.put(content, index, length * 4).rewind();
+ index += length * 4;
+ return value.asFloatBuffer();
+ }else{
+ FloatBuffer value = BufferUtils.createFloatBuffer(length);
+ for (int x = 0; x < length; x++) {
+ value.put(readFloatForBuffer(content));
+ }
+ value.rewind();
+ return value;
+ }
+ }
+
+ // int buffer
+
+ protected IntBuffer readIntBuffer(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+
+ if (BinaryImporter.canUseFastBuffers()){
+ ByteBuffer value = BufferUtils.createByteBuffer(length * 4);
+ value.put(content, index, length * 4).rewind();
+ index += length * 4;
+ return value.asIntBuffer();
+ }else{
+ IntBuffer value = BufferUtils.createIntBuffer(length);
+ for (int x = 0; x < length; x++) {
+ value.put(readIntForBuffer(content));
+ }
+ value.rewind();
+ return value;
+ }
+ }
+
+ // byte buffer
+
+ protected ByteBuffer readByteBuffer(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+
+ if (BinaryImporter.canUseFastBuffers()){
+ ByteBuffer value = BufferUtils.createByteBuffer(length);
+ value.put(content, index, length).rewind();
+ index += length;
+ return value;
+ }else{
+ ByteBuffer value = BufferUtils.createByteBuffer(length);
+ for (int x = 0; x < length; x++) {
+ value.put(readByteForBuffer(content));
+ }
+ value.rewind();
+ return value;
+ }
+ }
+
+ // short buffer
+
+ protected ShortBuffer readShortBuffer(byte[] content) throws IOException {
+ int length = readInt(content);
+ if (length == BinaryOutputCapsule.NULL_OBJECT)
+ return null;
+
+ if (BinaryImporter.canUseFastBuffers()){
+ ByteBuffer value = BufferUtils.createByteBuffer(length * 2);
+ value.put(content, index, length * 2).rewind();
+ index += length * 2;
+ return value.asShortBuffer();
+ }else{
+ ShortBuffer value = BufferUtils.createShortBuffer(length);
+ for (int x = 0; x < length; x++) {
+ value.put(readShortForBuffer(content));
+ }
+ value.rewind();
+ return value;
+ }
+ }
+
+ static private class ID {
+ public int id;
+
+ public ID(int id) {
+ this.id = id;
+ }
+ }
+
+ static private class StringIDMap {
+ public String[] keys;
+ public ID[] values;
+ }
+
+ static private class IntIDMap {
+ public int[] keys;
+ public ID[] values;
+ }
+
+ public <T extends Enum<T>> T readEnum(String name, Class<T> enumType, T defVal) throws IOException {
+ String eVal = readString(name, defVal != null ? defVal.name() : null);
+ if (eVal != null) {
+ return Enum.valueOf(enumType, eVal);
+ } else {
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/engine/src/core-plugins/com/jme3/export/binary/BinaryOutputCapsule.java b/engine/src/core-plugins/com/jme3/export/binary/BinaryOutputCapsule.java
new file mode 100644
index 0000000..4466db5
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/export/binary/BinaryOutputCapsule.java
@@ -0,0 +1,944 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.export.binary;
+
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.util.IntMap;
+import com.jme3.util.IntMap.Entry;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.ShortBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Map;
+
+/**
+ * @author Joshua Slack
+ */
+final class BinaryOutputCapsule implements OutputCapsule {
+
+ public static final int NULL_OBJECT = -1;
+ public static final int DEFAULT_OBJECT = -2;
+
+ public static byte[] NULL_BYTES = new byte[] { (byte) -1 };
+ public static byte[] DEFAULT_BYTES = new byte[] { (byte) -2 };
+
+ protected ByteArrayOutputStream baos;
+ protected byte[] bytes;
+ protected BinaryExporter exporter;
+ protected BinaryClassObject cObj;
+
+ public BinaryOutputCapsule(BinaryExporter exporter, BinaryClassObject bco) {
+ this.baos = new ByteArrayOutputStream();
+ this.exporter = exporter;
+ this.cObj = bco;
+ }
+
+ public void write(byte value, String name, byte defVal) throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.BYTE);
+ write(value);
+ }
+
+ public void write(byte[] value, String name, byte[] defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.BYTE_1D);
+ write(value);
+ }
+
+ public void write(byte[][] value, String name, byte[][] defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.BYTE_2D);
+ write(value);
+ }
+
+ public void write(int value, String name, int defVal) throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.INT);
+ write(value);
+ }
+
+ public void write(int[] value, String name, int[] defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.INT_1D);
+ write(value);
+ }
+
+ public void write(int[][] value, String name, int[][] defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.INT_2D);
+ write(value);
+ }
+
+ public void write(float value, String name, float defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.FLOAT);
+ write(value);
+ }
+
+ public void write(float[] value, String name, float[] defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.FLOAT_1D);
+ write(value);
+ }
+
+ public void write(float[][] value, String name, float[][] defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.FLOAT_2D);
+ write(value);
+ }
+
+ public void write(double value, String name, double defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.DOUBLE);
+ write(value);
+ }
+
+ public void write(double[] value, String name, double[] defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.DOUBLE_1D);
+ write(value);
+ }
+
+ public void write(double[][] value, String name, double[][] defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.DOUBLE_2D);
+ write(value);
+ }
+
+ public void write(long value, String name, long defVal) throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.LONG);
+ write(value);
+ }
+
+ public void write(long[] value, String name, long[] defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.LONG_1D);
+ write(value);
+ }
+
+ public void write(long[][] value, String name, long[][] defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.LONG_2D);
+ write(value);
+ }
+
+ public void write(short value, String name, short defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.SHORT);
+ write(value);
+ }
+
+ public void write(short[] value, String name, short[] defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.SHORT_1D);
+ write(value);
+ }
+
+ public void write(short[][] value, String name, short[][] defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.SHORT_2D);
+ write(value);
+ }
+
+ public void write(boolean value, String name, boolean defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.BOOLEAN);
+ write(value);
+ }
+
+ public void write(boolean[] value, String name, boolean[] defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.BOOLEAN_1D);
+ write(value);
+ }
+
+ public void write(boolean[][] value, String name, boolean[][] defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.BOOLEAN_2D);
+ write(value);
+ }
+
+ public void write(String value, String name, String defVal)
+ throws IOException {
+ if (value == null ? defVal == null : value.equals(defVal))
+ return;
+ writeAlias(name, BinaryClassField.STRING);
+ write(value);
+ }
+
+ public void write(String[] value, String name, String[] defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.STRING_1D);
+ write(value);
+ }
+
+ public void write(String[][] value, String name, String[][] defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.STRING_2D);
+ write(value);
+ }
+
+ public void write(BitSet value, String name, BitSet defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.BITSET);
+ write(value);
+ }
+
+ public void write(Savable object, String name, Savable defVal)
+ throws IOException {
+ if (object == defVal)
+ return;
+ writeAlias(name, BinaryClassField.SAVABLE);
+ write(object);
+ }
+
+ public void write(Savable[] objects, String name, Savable[] defVal)
+ throws IOException {
+ if (objects == defVal)
+ return;
+ writeAlias(name, BinaryClassField.SAVABLE_1D);
+ write(objects);
+ }
+
+ public void write(Savable[][] objects, String name, Savable[][] defVal)
+ throws IOException {
+ if (objects == defVal)
+ return;
+ writeAlias(name, BinaryClassField.SAVABLE_2D);
+ write(objects);
+ }
+
+ public void write(FloatBuffer value, String name, FloatBuffer defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.FLOATBUFFER);
+ write(value);
+ }
+
+ public void write(IntBuffer value, String name, IntBuffer defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.INTBUFFER);
+ write(value);
+ }
+
+ public void write(ByteBuffer value, String name, ByteBuffer defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.BYTEBUFFER);
+ write(value);
+ }
+
+ public void write(ShortBuffer value, String name, ShortBuffer defVal)
+ throws IOException {
+ if (value == defVal)
+ return;
+ writeAlias(name, BinaryClassField.SHORTBUFFER);
+ write(value);
+ }
+
+ public void writeFloatBufferArrayList(ArrayList<FloatBuffer> array,
+ String name, ArrayList<FloatBuffer> defVal) throws IOException {
+ if (array == defVal)
+ return;
+ writeAlias(name, BinaryClassField.FLOATBUFFER_ARRAYLIST);
+ writeFloatBufferArrayList(array);
+ }
+
+ public void writeByteBufferArrayList(ArrayList<ByteBuffer> array,
+ String name, ArrayList<ByteBuffer> defVal) throws IOException {
+ if (array == defVal)
+ return;
+ writeAlias(name, BinaryClassField.BYTEBUFFER_ARRAYLIST);
+ writeByteBufferArrayList(array);
+ }
+
+ public void writeSavableArrayList(ArrayList array, String name,
+ ArrayList defVal) throws IOException {
+ if (array == defVal)
+ return;
+ writeAlias(name, BinaryClassField.SAVABLE_ARRAYLIST);
+ writeSavableArrayList(array);
+ }
+
+ public void writeSavableArrayListArray(ArrayList[] array, String name,
+ ArrayList[] defVal) throws IOException {
+ if (array == defVal)
+ return;
+ writeAlias(name, BinaryClassField.SAVABLE_ARRAYLIST_1D);
+ writeSavableArrayListArray(array);
+ }
+
+ public void writeSavableArrayListArray2D(ArrayList[][] array, String name,
+ ArrayList[][] defVal) throws IOException {
+ if (array == defVal)
+ return;
+ writeAlias(name, BinaryClassField.SAVABLE_ARRAYLIST_2D);
+ writeSavableArrayListArray2D(array);
+ }
+
+ public void writeSavableMap(Map<? extends Savable, ? extends Savable> map,
+ String name, Map<? extends Savable, ? extends Savable> defVal)
+ throws IOException {
+ if (map == defVal)
+ return;
+ writeAlias(name, BinaryClassField.SAVABLE_MAP);
+ writeSavableMap(map);
+ }
+
+ public void writeStringSavableMap(Map<String, ? extends Savable> map,
+ String name, Map<String, ? extends Savable> defVal)
+ throws IOException {
+ if (map == defVal)
+ return;
+ writeAlias(name, BinaryClassField.STRING_SAVABLE_MAP);
+ writeStringSavableMap(map);
+ }
+
+ public void writeIntSavableMap(IntMap<? extends Savable> map,
+ String name, IntMap<? extends Savable> defVal)
+ throws IOException {
+ if (map == defVal)
+ return;
+ writeAlias(name, BinaryClassField.INT_SAVABLE_MAP);
+ writeIntSavableMap(map);
+ }
+
+ protected void writeAlias(String name, byte fieldType) throws IOException {
+ if (cObj.nameFields.get(name) == null)
+ generateAlias(name, fieldType);
+
+ byte alias = cObj.nameFields.get(name).alias;
+ write(alias);
+ }
+
+ // XXX: The generation of aliases is limited to 256 possible values.
+ // If we run into classes with more than 256 fields, we need to expand this.
+ // But I mean, come on...
+ protected void generateAlias(String name, byte type) {
+ byte alias = (byte) cObj.nameFields.size();
+ cObj.nameFields.put(name, new BinaryClassField(name, alias, type));
+ }
+
+ @Override
+ public boolean equals(Object arg0) {
+ if (!(arg0 instanceof BinaryOutputCapsule))
+ return false;
+
+ byte[] other = ((BinaryOutputCapsule) arg0).bytes;
+ if (bytes.length != other.length)
+ return false;
+ return Arrays.equals(bytes, other);
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+ hash = 23 * hash + Arrays.hashCode(this.bytes);
+ return hash;
+ }
+
+ public void finish() {
+ // renamed to finish as 'finalize' in java.lang.Object should not be
+ // overridden like this
+ // - finalize should not be called directly but is called by garbage
+ // collection!!!
+ bytes = baos.toByteArray();
+ baos = null;
+ }
+
+ // byte primitive
+
+ protected void write(byte value) throws IOException {
+ baos.write(value);
+ }
+
+ protected void writeForBuffer(byte value) throws IOException {
+ baos.write(value);
+ }
+
+ protected void write(byte[] value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.length);
+ baos.write(value);
+ }
+
+ protected void write(byte[][] value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.length);
+ for (int x = 0; x < value.length; x++)
+ write(value[x]);
+ }
+
+ // int primitive
+
+ protected void write(int value) throws IOException {
+ baos.write(deflate(ByteUtils.convertToBytes(value)));
+ }
+
+ protected void writeForBuffer(int value) throws IOException {
+ byte[] byteArray = new byte[4];
+ byteArray[0] = (byte) value;
+ byteArray[1] = (byte) (value >> 8);
+ byteArray[2] = (byte) (value >> 16);
+ byteArray[3] = (byte) (value >> 24);
+ baos.write(byteArray);
+ }
+
+ protected void write(int[] value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.length);
+ for (int x = 0; x < value.length; x++)
+ write(value[x]);
+ }
+
+ protected void write(int[][] value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.length);
+ for (int x = 0; x < value.length; x++)
+ write(value[x]);
+ }
+
+ // float primitive
+
+ protected void write(float value) throws IOException {
+ baos.write(ByteUtils.convertToBytes(value));
+ }
+
+ protected void writeForBuffer(float value) throws IOException {
+ int integer = Float.floatToIntBits(value);
+ writeForBuffer(integer);
+ }
+
+ protected void write(float[] value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.length);
+ for (int x = 0; x < value.length; x++)
+ write(value[x]);
+ }
+
+ protected void write(float[][] value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.length);
+ for (int x = 0; x < value.length; x++)
+ write(value[x]);
+ }
+
+ // double primitive
+
+ protected void write(double value) throws IOException {
+ baos.write(ByteUtils.convertToBytes(value));
+ }
+
+ protected void write(double[] value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.length);
+ for (int x = 0; x < value.length; x++)
+ write(value[x]);
+ }
+
+ protected void write(double[][] value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.length);
+ for (int x = 0; x < value.length; x++)
+ write(value[x]);
+ }
+
+ // long primitive
+
+ protected void write(long value) throws IOException {
+ baos.write(deflate(ByteUtils.convertToBytes(value)));
+ }
+
+ protected void write(long[] value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.length);
+ for (int x = 0; x < value.length; x++)
+ write(value[x]);
+ }
+
+ protected void write(long[][] value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.length);
+ for (int x = 0; x < value.length; x++)
+ write(value[x]);
+ }
+
+ // short primitive
+
+ protected void write(short value) throws IOException {
+ baos.write(ByteUtils.convertToBytes(value));
+ }
+
+ protected void writeForBuffer(short value) throws IOException {
+ byte[] byteArray = new byte[2];
+ byteArray[0] = (byte) value;
+ byteArray[1] = (byte) (value >> 8);
+ baos.write(byteArray);
+ }
+
+ protected void write(short[] value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.length);
+ for (int x = 0; x < value.length; x++)
+ write(value[x]);
+ }
+
+ protected void write(short[][] value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.length);
+ for (int x = 0; x < value.length; x++)
+ write(value[x]);
+ }
+
+ // boolean primitive
+
+ protected void write(boolean value) throws IOException {
+ baos.write(ByteUtils.convertToBytes(value));
+ }
+
+ protected void write(boolean[] value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.length);
+ for (int x = 0; x < value.length; x++)
+ write(value[x]);
+ }
+
+ protected void write(boolean[][] value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.length);
+ for (int x = 0; x < value.length; x++)
+ write(value[x]);
+ }
+
+ // String
+
+ protected void write(String value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ // write our output as UTF-8. Java misspells UTF-8 as UTF8 for official use in java.lang
+ byte[] bytes = value.getBytes("UTF8");
+ write(bytes.length);
+ baos.write(bytes);
+ }
+
+ protected void write(String[] value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.length);
+ for (int x = 0; x < value.length; x++)
+ write(value[x]);
+ }
+
+ protected void write(String[][] value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.length);
+ for (int x = 0; x < value.length; x++)
+ write(value[x]);
+ }
+
+ // BitSet
+
+ protected void write(BitSet value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(value.size());
+ // TODO: MAKE THIS SMALLER
+ for (int x = 0, max = value.size(); x < max; x++)
+ write(value.get(x));
+ }
+
+ // DEFLATOR for int and long
+
+ protected static byte[] deflate(byte[] bytes) {
+ int size = bytes.length;
+ if (size == 4) {
+ int possibleMagic = ByteUtils.convertIntFromBytes(bytes);
+ if (possibleMagic == NULL_OBJECT)
+ return NULL_BYTES;
+ else if (possibleMagic == DEFAULT_OBJECT)
+ return DEFAULT_BYTES;
+ }
+ for (int x = 0; x < bytes.length; x++) {
+ if (bytes[x] != 0)
+ break;
+ size--;
+ }
+ if (size == 0)
+ return new byte[1];
+
+ byte[] rVal = new byte[1 + size];
+ rVal[0] = (byte) size;
+ for (int x = 1; x < rVal.length; x++)
+ rVal[x] = bytes[bytes.length - size - 1 + x];
+
+ return rVal;
+ }
+
+ // BinarySavable
+
+ protected void write(Savable object) throws IOException {
+ if (object == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ int id = exporter.processBinarySavable(object);
+ write(id);
+ }
+
+ // BinarySavable array
+
+ protected void write(Savable[] objects) throws IOException {
+ if (objects == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(objects.length);
+ for (int x = 0; x < objects.length; x++) {
+ write(objects[x]);
+ }
+ }
+
+ protected void write(Savable[][] objects) throws IOException {
+ if (objects == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(objects.length);
+ for (int x = 0; x < objects.length; x++) {
+ write(objects[x]);
+ }
+ }
+
+ // ArrayList<BinarySavable>
+
+ protected void writeSavableArrayList(ArrayList array) throws IOException {
+ if (array == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(array.size());
+ for (Object bs : array) {
+ write((Savable) bs);
+ }
+ }
+
+ protected void writeSavableArrayListArray(ArrayList[] array)
+ throws IOException {
+ if (array == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(array.length);
+ for (ArrayList bs : array) {
+ writeSavableArrayList(bs);
+ }
+ }
+
+ protected void writeSavableArrayListArray2D(ArrayList[][] array)
+ throws IOException {
+ if (array == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(array.length);
+ for (ArrayList[] bs : array) {
+ writeSavableArrayListArray(bs);
+ }
+ }
+
+ // Map<BinarySavable, BinarySavable>
+
+ protected void writeSavableMap(
+ Map<? extends Savable, ? extends Savable> array) throws IOException {
+ if (array == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(array.size());
+ for (Savable key : array.keySet()) {
+ write(new Savable[] { key, array.get(key) });
+ }
+ }
+
+ protected void writeStringSavableMap(Map<String, ? extends Savable> array)
+ throws IOException {
+ if (array == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(array.size());
+
+ // write String array for keys
+ String[] keys = array.keySet().toArray(new String[] {});
+ write(keys);
+
+ // write Savable array for values
+ Savable[] values = array.values().toArray(new Savable[] {});
+ write(values);
+ }
+
+ protected void writeIntSavableMap(IntMap<? extends Savable> array)
+ throws IOException {
+ if (array == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(array.size());
+
+ int[] keys = new int[array.size()];
+ Savable[] values = new Savable[keys.length];
+ int i = 0;
+ for (Entry<? extends Savable> entry : array){
+ keys[i] = entry.getKey();
+ values[i] = entry.getValue();
+ i++;
+ }
+
+ // write String array for keys
+ write(keys);
+
+ // write Savable array for values
+ write(values);
+ }
+
+ // ArrayList<FloatBuffer>
+
+ protected void writeFloatBufferArrayList(ArrayList<FloatBuffer> array)
+ throws IOException {
+ if (array == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(array.size());
+ for (FloatBuffer buf : array) {
+ write(buf);
+ }
+ }
+
+ // ArrayList<FloatBuffer>
+
+ protected void writeByteBufferArrayList(ArrayList<ByteBuffer> array)
+ throws IOException {
+ if (array == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ write(array.size());
+ for (ByteBuffer buf : array) {
+ write(buf);
+ }
+ }
+
+ // NIO BUFFERS
+ // float buffer
+
+ protected void write(FloatBuffer value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ value.rewind();
+ int length = value.limit();
+ write(length);
+ for (int x = 0; x < length; x++) {
+ writeForBuffer(value.get());
+ }
+ value.rewind();
+ }
+
+ // int buffer
+
+ protected void write(IntBuffer value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ value.rewind();
+ int length = value.limit();
+ write(length);
+
+ for (int x = 0; x < length; x++) {
+ writeForBuffer(value.get());
+ }
+ value.rewind();
+ }
+
+ // byte buffer
+
+ protected void write(ByteBuffer value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ value.rewind();
+ int length = value.limit();
+ write(length);
+ for (int x = 0; x < length; x++) {
+ writeForBuffer(value.get());
+ }
+ value.rewind();
+ }
+
+ // short buffer
+
+ protected void write(ShortBuffer value) throws IOException {
+ if (value == null) {
+ write(NULL_OBJECT);
+ return;
+ }
+ value.rewind();
+ int length = value.limit();
+ write(length);
+ for (int x = 0; x < length; x++) {
+ writeForBuffer(value.get());
+ }
+ value.rewind();
+ }
+
+ public void write(Enum value, String name, Enum defVal) throws IOException {
+ if (value == defVal)
+ return;
+ if (value == null) {
+ return;
+ } else {
+ write(value.name(), name, null);
+ }
+ }
+} \ No newline at end of file
diff --git a/engine/src/core-plugins/com/jme3/export/binary/ByteUtils.java b/engine/src/core-plugins/com/jme3/export/binary/ByteUtils.java
new file mode 100644
index 0000000..bb835d0
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/export/binary/ByteUtils.java
@@ -0,0 +1,486 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.export.binary;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * <code>ByteUtils</code> is a helper class for converting numeric primitives
+ * to and from byte representations.
+ *
+ * @author Joshua Slack
+ */
+public class ByteUtils {
+
+ /**
+ * Takes an InputStream and returns the complete byte content of it
+ *
+ * @param inputStream
+ * The input stream to read from
+ * @return The byte array containing the data from the input stream
+ * @throws java.io.IOException
+ * thrown if there is a problem reading from the input stream
+ * provided
+ */
+ public static byte[] getByteContent(InputStream inputStream)
+ throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream(
+ 16 * 1024);
+ byte[] buffer = new byte[1024];
+ int byteCount = -1;
+ byte[] data = null;
+
+ // Read the byte content into the output stream first
+ while ((byteCount = inputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, byteCount);
+ }
+
+ // Set data with byte content from stream
+ data = outputStream.toByteArray();
+
+ // Release resources
+ outputStream.close();
+
+ return data;
+ }
+
+
+ // ********** byte <> short METHODS **********
+
+ /**
+ * Writes a short out to an OutputStream.
+ *
+ * @param outputStream
+ * The OutputStream the short will be written to
+ * @param value
+ * The short to write
+ * @throws IOException
+ * Thrown if there is a problem writing to the OutputStream
+ */
+ public static void writeShort(OutputStream outputStream, short value)
+ throws IOException {
+ byte[] byteArray = convertToBytes(value);
+
+ outputStream.write(byteArray);
+
+ return;
+ }
+
+ public static byte[] convertToBytes(short value) {
+ byte[] byteArray = new byte[2];
+
+ byteArray[0] = (byte) (value >> 8);
+ byteArray[1] = (byte) value;
+ return byteArray;
+ }
+
+ /**
+ * Read in a short from an InputStream
+ *
+ * @param inputStream
+ * The InputStream used to read the short
+ * @return A short, which is the next 2 bytes converted from the InputStream
+ * @throws IOException
+ * Thrown if there is a problem reading from the InputStream
+ */
+ public static short readShort(InputStream inputStream) throws IOException {
+ byte[] byteArray = new byte[2];
+
+ // Read in the next 2 bytes
+ inputStream.read(byteArray);
+
+ short number = convertShortFromBytes(byteArray);
+
+ return number;
+ }
+
+ public static short convertShortFromBytes(byte[] byteArray) {
+ return convertShortFromBytes(byteArray, 0);
+ }
+
+ public static short convertShortFromBytes(byte[] byteArray, int offset) {
+ // Convert it to a short
+ short number = (short) ((byteArray[offset+1] & 0xFF) + ((byteArray[offset+0] & 0xFF) << 8));
+ return number;
+ }
+
+
+ // ********** byte <> int METHODS **********
+
+ /**
+ * Writes an integer out to an OutputStream.
+ *
+ * @param outputStream
+ * The OutputStream the integer will be written to
+ * @param integer
+ * The integer to write
+ * @throws IOException
+ * Thrown if there is a problem writing to the OutputStream
+ */
+ public static void writeInt(OutputStream outputStream, int integer)
+ throws IOException {
+ byte[] byteArray = convertToBytes(integer);
+
+ outputStream.write(byteArray);
+
+ return;
+ }
+
+ public static byte[] convertToBytes(int integer) {
+ byte[] byteArray = new byte[4];
+
+ byteArray[0] = (byte) (integer >> 24);
+ byteArray[1] = (byte) (integer >> 16);
+ byteArray[2] = (byte) (integer >> 8);
+ byteArray[3] = (byte) integer;
+ return byteArray;
+ }
+
+ /**
+ * Read in an integer from an InputStream
+ *
+ * @param inputStream
+ * The InputStream used to read the integer
+ * @return An int, which is the next 4 bytes converted from the InputStream
+ * @throws IOException
+ * Thrown if there is a problem reading from the InputStream
+ */
+ public static int readInt(InputStream inputStream) throws IOException {
+ byte[] byteArray = new byte[4];
+
+ // Read in the next 4 bytes
+ inputStream.read(byteArray);
+
+ int number = convertIntFromBytes(byteArray);
+
+ return number;
+ }
+
+ public static int convertIntFromBytes(byte[] byteArray) {
+ return convertIntFromBytes(byteArray, 0);
+ }
+
+ public static int convertIntFromBytes(byte[] byteArray, int offset) {
+ // Convert it to an int
+ int number = ((byteArray[offset] & 0xFF) << 24)
+ + ((byteArray[offset+1] & 0xFF) << 16) + ((byteArray[offset+2] & 0xFF) << 8)
+ + (byteArray[offset+3] & 0xFF);
+ return number;
+ }
+
+
+ // ********** byte <> long METHODS **********
+
+ /**
+ * Writes a long out to an OutputStream.
+ *
+ * @param outputStream
+ * The OutputStream the long will be written to
+ * @param value
+ * The long to write
+ * @throws IOException
+ * Thrown if there is a problem writing to the OutputStream
+ */
+ public static void writeLong(OutputStream outputStream, long value)
+ throws IOException {
+ byte[] byteArray = convertToBytes(value);
+
+ outputStream.write(byteArray);
+
+ return;
+ }
+
+ public static byte[] convertToBytes(long n) {
+ byte[] bytes = new byte[8];
+
+ bytes[7] = (byte) (n);
+ n >>>= 8;
+ bytes[6] = (byte) (n);
+ n >>>= 8;
+ bytes[5] = (byte) (n);
+ n >>>= 8;
+ bytes[4] = (byte) (n);
+ n >>>= 8;
+ bytes[3] = (byte) (n);
+ n >>>= 8;
+ bytes[2] = (byte) (n);
+ n >>>= 8;
+ bytes[1] = (byte) (n);
+ n >>>= 8;
+ bytes[0] = (byte) (n);
+
+ return bytes;
+ }
+
+ /**
+ * Read in a long from an InputStream
+ *
+ * @param inputStream
+ * The InputStream used to read the long
+ * @return A long, which is the next 8 bytes converted from the InputStream
+ * @throws IOException
+ * Thrown if there is a problem reading from the InputStream
+ */
+ public static long readLong(InputStream inputStream) throws IOException {
+ byte[] byteArray = new byte[8];
+
+ // Read in the next 8 bytes
+ inputStream.read(byteArray);
+
+ long number = convertLongFromBytes(byteArray);
+
+ return number;
+ }
+
+ public static long convertLongFromBytes(byte[] bytes) {
+ return convertLongFromBytes(bytes, 0);
+ }
+
+ public static long convertLongFromBytes(byte[] bytes, int offset) {
+ // Convert it to an long
+ return ((((long) bytes[offset+7]) & 0xFF)
+ + ((((long) bytes[offset+6]) & 0xFF) << 8)
+ + ((((long) bytes[offset+5]) & 0xFF) << 16)
+ + ((((long) bytes[offset+4]) & 0xFF) << 24)
+ + ((((long) bytes[offset+3]) & 0xFF) << 32)
+ + ((((long) bytes[offset+2]) & 0xFF) << 40)
+ + ((((long) bytes[offset+1]) & 0xFF) << 48)
+ + ((((long) bytes[offset+0]) & 0xFF) << 56));
+ }
+
+
+ // ********** byte <> double METHODS **********
+
+ /**
+ * Writes a double out to an OutputStream.
+ *
+ * @param outputStream
+ * The OutputStream the double will be written to
+ * @param value
+ * The double to write
+ * @throws IOException
+ * Thrown if there is a problem writing to the OutputStream
+ */
+ public static void writeDouble(OutputStream outputStream, double value)
+ throws IOException {
+ byte[] byteArray = convertToBytes(value);
+
+ outputStream.write(byteArray);
+
+ return;
+ }
+
+ public static byte[] convertToBytes(double n) {
+ long bits = Double.doubleToLongBits(n);
+ return convertToBytes(bits);
+ }
+
+ /**
+ * Read in a double from an InputStream
+ *
+ * @param inputStream
+ * The InputStream used to read the double
+ * @return A double, which is the next 8 bytes converted from the InputStream
+ * @throws IOException
+ * Thrown if there is a problem reading from the InputStream
+ */
+ public static double readDouble(InputStream inputStream) throws IOException {
+ byte[] byteArray = new byte[8];
+
+ // Read in the next 8 bytes
+ inputStream.read(byteArray);
+
+ double number = convertDoubleFromBytes(byteArray);
+
+ return number;
+ }
+
+ public static double convertDoubleFromBytes(byte[] bytes) {
+ return convertDoubleFromBytes(bytes, 0);
+ }
+
+ public static double convertDoubleFromBytes(byte[] bytes, int offset) {
+ // Convert it to a double
+ long bits = convertLongFromBytes(bytes, offset);
+ return Double.longBitsToDouble(bits);
+ }
+
+ // ********** byte <> float METHODS **********
+
+ /**
+ * Writes an float out to an OutputStream.
+ *
+ * @param outputStream
+ * The OutputStream the float will be written to
+ * @param fVal
+ * The float to write
+ * @throws IOException
+ * Thrown if there is a problem writing to the OutputStream
+ */
+ public static void writeFloat(OutputStream outputStream, float fVal)
+ throws IOException {
+ byte[] byteArray = convertToBytes(fVal);
+
+ outputStream.write(byteArray);
+
+ return;
+ }
+
+ public static byte[] convertToBytes(float f) {
+ int temp = Float.floatToIntBits(f);
+ return convertToBytes(temp);
+ }
+
+ /**
+ * Read in a float from an InputStream
+ *
+ * @param inputStream
+ * The InputStream used to read the float
+ * @return A float, which is the next 4 bytes converted from the InputStream
+ * @throws IOException
+ * Thrown if there is a problem reading from the InputStream
+ */
+ public static float readFloat(InputStream inputStream) throws IOException {
+ byte[] byteArray = new byte[4];
+
+ // Read in the next 4 bytes
+ inputStream.read(byteArray);
+
+ float number = convertFloatFromBytes(byteArray);
+
+ return number;
+ }
+
+ public static float convertFloatFromBytes(byte[] byteArray) {
+ return convertFloatFromBytes(byteArray, 0);
+ }
+ public static float convertFloatFromBytes(byte[] byteArray, int offset) {
+ // Convert it to an int
+ int number = convertIntFromBytes(byteArray, offset);
+ return Float.intBitsToFloat(number);
+ }
+
+
+
+ // ********** byte <> boolean METHODS **********
+
+ /**
+ * Writes a boolean out to an OutputStream.
+ *
+ * @param outputStream
+ * The OutputStream the boolean will be written to
+ * @param bVal
+ * The boolean to write
+ * @throws IOException
+ * Thrown if there is a problem writing to the OutputStream
+ */
+ public static void writeBoolean(OutputStream outputStream, boolean bVal)
+ throws IOException {
+ byte[] byteArray = convertToBytes(bVal);
+
+ outputStream.write(byteArray);
+
+ return;
+ }
+
+ public static byte[] convertToBytes(boolean b) {
+ byte[] rVal = new byte[1];
+ rVal[0] = b ? (byte)1 : (byte)0;
+ return rVal;
+ }
+
+ /**
+ * Read in a boolean from an InputStream
+ *
+ * @param inputStream
+ * The InputStream used to read the boolean
+ * @return A boolean, which is the next byte converted from the InputStream (iow, byte != 0)
+ * @throws IOException
+ * Thrown if there is a problem reading from the InputStream
+ */
+ public static boolean readBoolean(InputStream inputStream) throws IOException {
+ byte[] byteArray = new byte[1];
+
+ // Read in the next byte
+ inputStream.read(byteArray);
+
+ return convertBooleanFromBytes(byteArray);
+ }
+
+ public static boolean convertBooleanFromBytes(byte[] byteArray) {
+ return convertBooleanFromBytes(byteArray, 0);
+ }
+ public static boolean convertBooleanFromBytes(byte[] byteArray, int offset) {
+ return byteArray[offset] != 0;
+ }
+
+
+ /**
+ * Properly reads in data from the given stream until the specified number
+ * of bytes have been read.
+ *
+ * @param store
+ * the byte array to store in. Should have a length > bytes
+ * @param bytes
+ * the number of bytes to read.
+ * @param is
+ * the stream to read from
+ * @return the store array for chaining purposes
+ * @throws IOException
+ * if an error occurs while reading from the stream
+ * @throws ArrayIndexOutOfBoundsException
+ * if bytes greater than the length of the store.
+ */
+ public static byte[] readData(byte[] store, int bytes, InputStream is) throws IOException {
+ for (int i = 0; i < bytes; i++) {
+ store[i] = (byte)is.read();
+ }
+ return store;
+ }
+
+ public static byte[] rightAlignBytes(byte[] bytes, int width) {
+ if (bytes.length != width) {
+ byte[] rVal = new byte[width];
+ for (int x = width - bytes.length; x < width; x++) {
+ rVal[x] = bytes[x - (width - bytes.length)];
+ }
+ return rVal;
+ }
+
+ return bytes;
+ }
+
+}
diff --git a/engine/src/core-plugins/com/jme3/font/plugins/BitmapFontLoader.java b/engine/src/core-plugins/com/jme3/font/plugins/BitmapFontLoader.java
new file mode 100644
index 0000000..688627d
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/font/plugins/BitmapFontLoader.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.font.plugins;
+
+import com.jme3.asset.*;
+import com.jme3.font.BitmapCharacter;
+import com.jme3.font.BitmapCharacterSet;
+import com.jme3.font.BitmapFont;
+import com.jme3.material.Material;
+import com.jme3.material.MaterialDef;
+import com.jme3.material.RenderState.BlendMode;
+import com.jme3.texture.Texture;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+public class BitmapFontLoader implements AssetLoader {
+
+ private BitmapFont load(AssetManager assetManager, String folder, InputStream in) throws IOException{
+ MaterialDef spriteMat =
+ (MaterialDef) assetManager.loadAsset(new AssetKey("Common/MatDefs/Misc/Unshaded.j3md"));
+
+ BitmapCharacterSet charSet = new BitmapCharacterSet();
+ Material[] matPages = null;
+ BitmapFont font = new BitmapFont();
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+ String regex = "[\\s=]+";
+
+ font.setCharSet(charSet);
+ while (reader.ready()){
+ String line = reader.readLine();
+ String[] tokens = line.split(regex);
+ if (tokens[0].equals("info")){
+ // Get rendered size
+ for (int i = 1; i < tokens.length; i++){
+ if (tokens[i].equals("size")){
+ charSet.setRenderedSize(Integer.parseInt(tokens[i + 1]));
+ }
+ }
+ }else if (tokens[0].equals("common")){
+ // Fill out BitmapCharacterSet fields
+ for (int i = 1; i < tokens.length; i++){
+ String token = tokens[i];
+ if (token.equals("lineHeight")){
+ charSet.setLineHeight(Integer.parseInt(tokens[i + 1]));
+ }else if (token.equals("base")){
+ charSet.setBase(Integer.parseInt(tokens[i + 1]));
+ }else if (token.equals("scaleW")){
+ charSet.setWidth(Integer.parseInt(tokens[i + 1]));
+ }else if (token.equals("scaleH")){
+ charSet.setHeight(Integer.parseInt(tokens[i + 1]));
+ }else if (token.equals("pages")){
+ // number of texture pages
+ matPages = new Material[Integer.parseInt(tokens[i + 1])];
+ font.setPages(matPages);
+ }
+ }
+ }else if (tokens[0].equals("page")){
+ int index = -1;
+ Texture tex = null;
+
+ for (int i = 1; i < tokens.length; i++){
+ String token = tokens[i];
+ if (token.equals("id")){
+ index = Integer.parseInt(tokens[i + 1]);
+ }else if (token.equals("file")){
+ String file = tokens[i + 1];
+ if (file.startsWith("\"")){
+ file = file.substring(1, file.length()-1);
+ }
+ TextureKey key = new TextureKey(folder + file, true);
+ key.setGenerateMips(false);
+ tex = assetManager.loadTexture(key);
+ tex.setMagFilter(Texture.MagFilter.Bilinear);
+ tex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
+ }
+ }
+ // set page
+ if (index >= 0 && tex != null){
+ Material mat = new Material(spriteMat);
+ mat.setTexture("ColorMap", tex);
+ mat.setBoolean("VertexColor", true);
+ mat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
+ matPages[index] = mat;
+ }
+ }else if (tokens[0].equals("char")){
+ // New BitmapCharacter
+ BitmapCharacter ch = null;
+ for (int i = 1; i < tokens.length; i++){
+ String token = tokens[i];
+ if (token.equals("id")){
+ int index = Integer.parseInt(tokens[i + 1]);
+ ch = new BitmapCharacter();
+ charSet.addCharacter(index, ch);
+ }else if (token.equals("x")){
+ ch.setX(Integer.parseInt(tokens[i + 1]));
+ }else if (token.equals("y")){
+ ch.setY(Integer.parseInt(tokens[i + 1]));
+ }else if (token.equals("width")){
+ ch.setWidth(Integer.parseInt(tokens[i + 1]));
+ }else if (token.equals("height")){
+ ch.setHeight(Integer.parseInt(tokens[i + 1]));
+ }else if (token.equals("xoffset")){
+ ch.setXOffset(Integer.parseInt(tokens[i + 1]));
+ }else if (token.equals("yoffset")){
+ ch.setYOffset(Integer.parseInt(tokens[i + 1]));
+ }else if (token.equals("xadvance")){
+ ch.setXAdvance(Integer.parseInt(tokens[i + 1]));
+ } else if (token.equals("page")) {
+ ch.setPage(Integer.parseInt(tokens[i + 1]));
+ }
+ }
+ }else if (tokens[0].equals("kerning")){
+ // Build kerning list
+ int index = 0;
+ int second = 0;
+ int amount = 0;
+
+ for (int i = 1; i < tokens.length; i++){
+ if (tokens[i].equals("first")){
+ index = Integer.parseInt(tokens[i + 1]);
+ }else if (tokens[i].equals("second")){
+ second = Integer.parseInt(tokens[i + 1]);
+ }else if (tokens[i].equals("amount")){
+ amount = Integer.parseInt(tokens[i + 1]);
+ }
+ }
+
+ BitmapCharacter ch = charSet.getCharacter(index);
+ ch.addKerning(second, amount);
+ }
+ }
+ return font;
+ }
+
+ public Object load(AssetInfo info) throws IOException {
+ InputStream in = null;
+ try {
+ in = info.openStream();
+ BitmapFont font = load(info.getManager(), info.getKey().getFolder(), in);
+ return font;
+ } finally {
+ if (in != null){
+ in.close();
+ }
+ }
+ }
+
+}
diff --git a/engine/src/core-plugins/com/jme3/material/plugins/J3MLoader.java b/engine/src/core-plugins/com/jme3/material/plugins/J3MLoader.java
new file mode 100644
index 0000000..e07d3e4
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/material/plugins/J3MLoader.java
@@ -0,0 +1,530 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.material.plugins;
+
+import com.jme3.asset.*;
+import com.jme3.material.RenderState.BlendMode;
+import com.jme3.material.RenderState.FaceCullMode;
+import com.jme3.material.*;
+import com.jme3.material.TechniqueDef.LightMode;
+import com.jme3.material.TechniqueDef.ShadowMode;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.shader.VarType;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture.WrapMode;
+import com.jme3.texture.Texture2D;
+import com.jme3.util.PlaceholderAssets;
+import com.jme3.util.blockparser.BlockLanguageParser;
+import com.jme3.util.blockparser.Statement;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class J3MLoader implements AssetLoader {
+
+ private static final Logger logger = Logger.getLogger(J3MLoader.class.getName());
+
+ private AssetManager assetManager;
+ private AssetKey key;
+
+ private MaterialDef materialDef;
+ private Material material;
+ private TechniqueDef technique;
+ private RenderState renderState;
+
+ private String shaderLang;
+ private String vertName;
+ private String fragName;
+
+ private static final String whitespacePattern = "\\p{javaWhitespace}+";
+
+ public J3MLoader(){
+ }
+
+ private void throwIfNequal(String expected, String got) throws IOException {
+ if (expected == null)
+ throw new IOException("Expected a statement, got '"+got+"'!");
+
+ if (!expected.equals(got))
+ throw new IOException("Expected '"+expected+"', got '"+got+"'!");
+ }
+
+ // <TYPE> <LANG> : <SOURCE>
+ private void readShaderStatement(String statement) throws IOException {
+ String[] split = statement.split(":");
+ if (split.length != 2){
+ throw new IOException("Shader statement syntax incorrect" + statement);
+ }
+ String[] typeAndLang = split[0].split(whitespacePattern);
+ if (typeAndLang.length != 2){
+ throw new IOException("Shader statement syntax incorrect: " + statement);
+ }
+ shaderLang = typeAndLang[1];
+ if (typeAndLang[0].equals("VertexShader")){
+ vertName = split[1].trim();
+ }else if (typeAndLang[0].equals("FragmentShader")){
+ fragName = split[1].trim();
+ }
+ }
+
+ // LightMode <MODE>
+ private void readLightMode(String statement) throws IOException{
+ String[] split = statement.split(whitespacePattern);
+ if (split.length != 2){
+ throw new IOException("LightMode statement syntax incorrect");
+ }
+ LightMode lm = LightMode.valueOf(split[1]);
+ technique.setLightMode(lm);
+ }
+
+ // ShadowMode <MODE>
+ private void readShadowMode(String statement) throws IOException{
+ String[] split = statement.split(whitespacePattern);
+ if (split.length != 2){
+ throw new IOException("ShadowMode statement syntax incorrect");
+ }
+ ShadowMode sm = ShadowMode.valueOf(split[1]);
+ technique.setShadowMode(sm);
+ }
+
+ private Object readValue(VarType type, String value) throws IOException{
+ if (type.isTextureType()){
+// String texturePath = readString("[\n;(//)(\\})]");
+ String texturePath = value.trim();
+ boolean flipY = false;
+ boolean repeat = false;
+ if (texturePath.startsWith("Flip Repeat ")){
+ texturePath = texturePath.substring(12).trim();
+ flipY = true;
+ repeat = true;
+ }else if (texturePath.startsWith("Flip ")){
+ texturePath = texturePath.substring(5).trim();
+ flipY = true;
+ }else if (texturePath.startsWith("Repeat ")){
+ texturePath = texturePath.substring(7).trim();
+ repeat = true;
+ }
+
+ TextureKey texKey = new TextureKey(texturePath, flipY);
+ texKey.setAsCube(type == VarType.TextureCubeMap);
+ texKey.setGenerateMips(true);
+
+ Texture tex;
+ try {
+ tex = assetManager.loadTexture(texKey);
+ } catch (AssetNotFoundException ex){
+ logger.log(Level.WARNING, "Cannot locate {0} for material {1}", new Object[]{texKey, key});
+ tex = null;
+ }
+ if (tex != null){
+ if (repeat){
+ tex.setWrap(WrapMode.Repeat);
+ }
+ }else{
+ tex = new Texture2D(PlaceholderAssets.getPlaceholderImage());
+ }
+ return tex;
+ }else{
+ String[] split = value.trim().split(whitespacePattern);
+ switch (type){
+ case Float:
+ if (split.length != 1){
+ throw new IOException("Float value parameter must have 1 entry: " + value);
+ }
+ return Float.parseFloat(split[0]);
+ case Vector2:
+ if (split.length != 2){
+ throw new IOException("Vector2 value parameter must have 2 entries: " + value);
+ }
+ return new Vector2f(Float.parseFloat(split[0]),
+ Float.parseFloat(split[1]));
+ case Vector3:
+ if (split.length != 3){
+ throw new IOException("Vector3 value parameter must have 3 entries: " + value);
+ }
+ return new Vector3f(Float.parseFloat(split[0]),
+ Float.parseFloat(split[1]),
+ Float.parseFloat(split[2]));
+ case Vector4:
+ if (split.length != 4){
+ throw new IOException("Vector4 value parameter must have 4 entries: " + value);
+ }
+ return new ColorRGBA(Float.parseFloat(split[0]),
+ Float.parseFloat(split[1]),
+ Float.parseFloat(split[2]),
+ Float.parseFloat(split[3]));
+ case Int:
+ if (split.length != 1){
+ throw new IOException("Int value parameter must have 1 entry: " + value);
+ }
+ return Integer.parseInt(split[0]);
+ case Boolean:
+ if (split.length != 1){
+ throw new IOException("Boolean value parameter must have 1 entry: " + value);
+ }
+ return Boolean.parseBoolean(split[0]);
+ default:
+ throw new UnsupportedOperationException("Unknown type: "+type);
+ }
+ }
+ }
+
+ // <TYPE> <NAME> [ "(" <FFBINDING> ")" ] [ ":" <DEFAULTVAL> ]
+ private void readParam(String statement) throws IOException{
+ String name;
+ String defaultVal = null;
+ FixedFuncBinding ffBinding = null;
+
+ String[] split = statement.split(":");
+
+ // Parse default val
+ if (split.length == 1){
+ // Doesn't contain default value
+ }else{
+ if (split.length != 2){
+ throw new IOException("Parameter statement syntax incorrect");
+ }
+ statement = split[0].trim();
+ defaultVal = split[1].trim();
+ }
+
+ // Parse ffbinding
+ int startParen = statement.indexOf("(");
+ if (startParen != -1){
+ // get content inside parentheses
+ int endParen = statement.indexOf(")", startParen);
+ String bindingStr = statement.substring(startParen+1, endParen).trim();
+ try {
+ ffBinding = FixedFuncBinding.valueOf(bindingStr);
+ } catch (IllegalArgumentException ex){
+ throw new IOException("FixedFuncBinding '" +
+ split[1] + "' does not exist!");
+ }
+ statement = statement.substring(0, startParen);
+ }
+
+ // Parse type + name
+ split = statement.split(whitespacePattern);
+ if (split.length != 2){
+ throw new IOException("Parameter statement syntax incorrect");
+ }
+
+ VarType type;
+ if (split[0].equals("Color")){
+ type = VarType.Vector4;
+ }else{
+ type = VarType.valueOf(split[0]);
+ }
+
+ name = split[1];
+
+ Object defaultValObj = null;
+ if (defaultVal != null){
+ defaultValObj = readValue(type, defaultVal);
+ }
+
+ materialDef.addMaterialParam(type, name, defaultValObj, ffBinding);
+ }
+
+ private void readValueParam(String statement) throws IOException{
+ // Use limit=1 incase filename contains colons
+ String[] split = statement.split(":", 2);
+ if (split.length != 2){
+ throw new IOException("Value parameter statement syntax incorrect");
+ }
+ String name = split[0].trim();
+
+ // parse value
+ MatParam p = material.getMaterialDef().getMaterialParam(name);
+ if (p == null){
+ throw new IOException("The material parameter: "+name+" is undefined.");
+ }
+
+ Object valueObj = readValue(p.getVarType(), split[1]);
+ if (p.getVarType().isTextureType()){
+ material.setTextureParam(name, p.getVarType(), (Texture) valueObj);
+ }else{
+ material.setParam(name, p.getVarType(), valueObj);
+ }
+ }
+
+ private void readMaterialParams(List<Statement> paramsList) throws IOException{
+ for (Statement statement : paramsList){
+ readParam(statement.getLine());
+ }
+ }
+
+ private void readExtendingMaterialParams(List<Statement> paramsList) throws IOException{
+ for (Statement statement : paramsList){
+ readValueParam(statement.getLine());
+ }
+ }
+
+ private void readWorldParams(List<Statement> worldParams) throws IOException{
+ for (Statement statement : worldParams){
+ technique.addWorldParam(statement.getLine());
+ }
+ }
+
+ private boolean parseBoolean(String word){
+ return word != null && word.equals("On");
+ }
+
+ private void readRenderStateStatement(String statement) throws IOException{
+ String[] split = statement.split(whitespacePattern);
+ if (split[0].equals("Wireframe")){
+ renderState.setWireframe(parseBoolean(split[1]));
+ }else if (split[0].equals("FaceCull")){
+ renderState.setFaceCullMode(FaceCullMode.valueOf(split[1]));
+ }else if (split[0].equals("DepthWrite")){
+ renderState.setDepthWrite(parseBoolean(split[1]));
+ }else if (split[0].equals("DepthTest")){
+ renderState.setDepthTest(parseBoolean(split[1]));
+ }else if (split[0].equals("Blend")){
+ renderState.setBlendMode(BlendMode.valueOf(split[1]));
+ }else if (split[0].equals("AlphaTestFalloff")){
+ renderState.setAlphaTest(true);
+ renderState.setAlphaFallOff(Float.parseFloat(split[1]));
+ }else if (split[0].equals("PolyOffset")){
+ float factor = Float.parseFloat(split[1]);
+ float units = Float.parseFloat(split[2]);
+ renderState.setPolyOffset(factor, units);
+ }else if (split[0].equals("ColorWrite")){
+ renderState.setColorWrite(parseBoolean(split[1]));
+ }else if (split[0].equals("PointSprite")){
+ renderState.setPointSprite(parseBoolean(split[1]));
+ }else{
+ throwIfNequal(null, split[0]);
+ }
+ }
+
+ private void readAdditionalRenderState(List<Statement> renderStates) throws IOException{
+ renderState = material.getAdditionalRenderState();
+ for (Statement statement : renderStates){
+ readRenderStateStatement(statement.getLine());
+ }
+ renderState = null;
+ }
+
+ private void readRenderState(List<Statement> renderStates) throws IOException{
+ renderState = new RenderState();
+ for (Statement statement : renderStates){
+ readRenderStateStatement(statement.getLine());
+ }
+ technique.setRenderState(renderState);
+ renderState = null;
+ }
+
+ // <DEFINENAME> [ ":" <PARAMNAME> ]
+ private void readDefine(String statement) throws IOException{
+ String[] split = statement.split(":");
+ if (split.length == 1){
+ // add preset define
+ technique.addShaderPresetDefine(split[0].trim(), VarType.Boolean, true);
+ }else if (split.length == 2){
+ technique.addShaderParamDefine(split[1].trim(), split[0].trim());
+ }else{
+ throw new IOException("Define syntax incorrect");
+ }
+ }
+
+ private void readDefines(List<Statement> defineList) throws IOException{
+ for (Statement statement : defineList){
+ readDefine(statement.getLine());
+ }
+
+ }
+
+ private void readTechniqueStatement(Statement statement) throws IOException{
+ String[] split = statement.getLine().split("[ \\{]");
+ if (split[0].equals("VertexShader") ||
+ split[0].equals("FragmentShader")){
+ readShaderStatement(statement.getLine());
+ }else if (split[0].equals("LightMode")){
+ readLightMode(statement.getLine());
+ }else if (split[0].equals("ShadowMode")){
+ readShadowMode(statement.getLine());
+ }else if (split[0].equals("WorldParameters")){
+ readWorldParams(statement.getContents());
+ }else if (split[0].equals("RenderState")){
+ readRenderState(statement.getContents());
+ }else if (split[0].equals("Defines")){
+ readDefines(statement.getContents());
+ }else{
+ throwIfNequal(null, split[0]);
+ }
+ }
+
+ private void readTransparentStatement(String statement) throws IOException{
+ String[] split = statement.split(whitespacePattern);
+ if (split.length != 2){
+ throw new IOException("Transparent statement syntax incorrect");
+ }
+ material.setTransparent(parseBoolean(split[1]));
+ }
+
+ private void readTechnique(Statement techStat) throws IOException{
+ String[] split = techStat.getLine().split(whitespacePattern);
+ if (split.length == 1){
+ technique = new TechniqueDef(null);
+ }else if (split.length == 2){
+ technique = new TechniqueDef(split[1]);
+ }else{
+ throw new IOException("Technique statement syntax incorrect");
+ }
+
+ for (Statement statement : techStat.getContents()){
+ readTechniqueStatement(statement);
+ }
+
+ if (vertName != null && fragName != null){
+ technique.setShaderFile(vertName, fragName, shaderLang);
+ }
+
+ materialDef.addTechniqueDef(technique);
+ technique = null;
+ vertName = null;
+ fragName = null;
+ shaderLang = null;
+ }
+
+ private void loadFromRoot(List<Statement> roots) throws IOException{
+ if (roots.size() == 2){
+ Statement exception = roots.get(0);
+ String line = exception.getLine();
+ if (line.startsWith("Exception")){
+ throw new AssetLoadException(line.substring("Exception ".length()));
+ }else{
+ throw new IOException("In multiroot material, expected first statement to be 'Exception'");
+ }
+ }else if (roots.size() != 1){
+ throw new IOException("Too many roots in J3M/J3MD file");
+ }
+
+ boolean extending = false;
+ Statement materialStat = roots.get(0);
+ String materialName = materialStat.getLine();
+ if (materialName.startsWith("MaterialDef")){
+ materialName = materialName.substring("MaterialDef ".length()).trim();
+ extending = false;
+ }else if (materialName.startsWith("Material")){
+ materialName = materialName.substring("Material ".length()).trim();
+ extending = true;
+ }else{
+ throw new IOException("Specified file is not a Material file");
+ }
+
+ String[] split = materialName.split(":", 2);
+
+ if (materialName.equals("")){
+ throw new IOException("Material name cannot be empty");
+ }
+
+ if (split.length == 2){
+ if (!extending){
+ throw new IOException("Must use 'Material' when extending.");
+ }
+
+ String extendedMat = split[1].trim();
+
+ MaterialDef def = (MaterialDef) assetManager.loadAsset(new AssetKey(extendedMat));
+ if (def == null)
+ throw new IOException("Extended material "+extendedMat+" cannot be found.");
+
+ material = new Material(def);
+ material.setKey(key);
+// material.setAssetName(fileName);
+ }else if (split.length == 1){
+ if (extending){
+ throw new IOException("Expected ':', got '{'");
+ }
+ materialDef = new MaterialDef(assetManager, materialName);
+ // NOTE: pass file name for defs so they can be loaded later
+ materialDef.setAssetName(key.getName());
+ }else{
+ throw new IOException("Cannot use colon in material name/path");
+ }
+
+ for (Statement statement : materialStat.getContents()){
+ split = statement.getLine().split("[ \\{]");
+ String statType = split[0];
+ if (extending){
+ if (statType.equals("MaterialParameters")){
+ readExtendingMaterialParams(statement.getContents());
+ }else if (statType.equals("AdditionalRenderState")){
+ readAdditionalRenderState(statement.getContents());
+ }else if (statType.equals("Transparent")){
+ readTransparentStatement(statement.getLine());
+ }
+ }else{
+ if (statType.equals("Technique")){
+ readTechnique(statement);
+ }else if (statType.equals("MaterialParameters")){
+ readMaterialParams(statement.getContents());
+ }else{
+ throw new IOException("Expected material statement, got '"+statType+"'");
+ }
+ }
+ }
+ }
+
+ public Object load(AssetInfo info) throws IOException {
+ this.assetManager = info.getManager();
+
+ InputStream in = info.openStream();
+ try {
+ key = info.getKey();
+ loadFromRoot(BlockLanguageParser.parse(in));
+ } finally {
+ if (in != null){
+ in.close();
+ }
+ }
+
+ if (material != null){
+ if (!(info.getKey() instanceof MaterialKey)){
+ throw new IOException("Material instances must be loaded via MaterialKey");
+ }
+ // material implementation
+ return material;
+ }else{
+ // material definition
+ return materialDef;
+ }
+ }
+
+}
diff --git a/engine/src/core-plugins/com/jme3/scene/plugins/MTLLoader.java b/engine/src/core-plugins/com/jme3/scene/plugins/MTLLoader.java
new file mode 100644
index 0000000..1f701cd
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/scene/plugins/MTLLoader.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.scene.plugins;
+
+import com.jme3.asset.*;
+import com.jme3.material.Material;
+import com.jme3.material.MaterialList;
+import com.jme3.material.RenderState.BlendMode;
+import com.jme3.math.ColorRGBA;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture.WrapMode;
+import com.jme3.texture.Texture2D;
+import com.jme3.util.PlaceholderAssets;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Locale;
+import java.util.NoSuchElementException;
+import java.util.Scanner;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class MTLLoader implements AssetLoader {
+
+ private static final Logger logger = Logger.getLogger(MTLLoader.class.getName());
+
+ protected Scanner scan;
+ protected MaterialList matList;
+ //protected Material material;
+ protected AssetManager assetManager;
+ protected String folderName;
+ protected AssetKey key;
+
+ protected Texture diffuseMap, normalMap, specularMap, alphaMap;
+ protected ColorRGBA ambient = new ColorRGBA();
+ protected ColorRGBA diffuse = new ColorRGBA();
+ protected ColorRGBA specular = new ColorRGBA();
+ protected float shininess = 16;
+ protected boolean shadeless;
+ protected String matName;
+ protected float alpha = 1;
+ protected boolean transparent = false;
+ protected boolean disallowTransparency = false;
+ protected boolean disallowAmbient = false;
+ protected boolean disallowSpecular = false;
+
+ public void reset(){
+ scan = null;
+ matList = null;
+// material = null;
+
+ resetMaterial();
+ }
+
+ protected ColorRGBA readColor(){
+ ColorRGBA v = new ColorRGBA();
+ v.set(scan.nextFloat(), scan.nextFloat(), scan.nextFloat(), 1.0f);
+ return v;
+ }
+
+ protected String nextStatement(){
+ scan.useDelimiter("\n");
+ String result = scan.next();
+ scan.useDelimiter("\\p{javaWhitespace}+");
+ return result;
+ }
+
+ protected boolean skipLine(){
+ try {
+ scan.skip(".*\r{0,1}\n");
+ return true;
+ } catch (NoSuchElementException ex){
+ // EOF
+ return false;
+ }
+ }
+
+ protected void resetMaterial(){
+ ambient.set(ColorRGBA.DarkGray);
+ diffuse.set(ColorRGBA.LightGray);
+ specular.set(ColorRGBA.Black);
+ shininess = 16;
+ disallowTransparency = false;
+ disallowAmbient = false;
+ disallowSpecular = false;
+ shadeless = false;
+ transparent = false;
+ matName = null;
+ diffuseMap = null;
+ specularMap = null;
+ normalMap = null;
+ alphaMap = null;
+ alpha = 1;
+ }
+
+ protected void createMaterial(){
+ Material material;
+
+ if (alpha < 1f && transparent && !disallowTransparency){
+ diffuse.a = alpha;
+ }
+
+ if (shadeless){
+ material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+ material.setColor("Color", diffuse.clone());
+ material.setTexture("ColorMap", diffuseMap);
+ // TODO: Add handling for alpha map?
+ }else{
+ material = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+ material.setBoolean("UseMaterialColors", true);
+ material.setColor("Ambient", ambient.clone());
+ material.setColor("Diffuse", diffuse.clone());
+ material.setColor("Specular", specular.clone());
+ material.setFloat("Shininess", shininess); // prevents "premature culling" bug
+
+ if (diffuseMap != null) material.setTexture("DiffuseMap", diffuseMap);
+ if (specularMap != null) material.setTexture("SpecularMap", specularMap);
+ if (normalMap != null) material.setTexture("NormalMap", normalMap);
+ if (alphaMap != null) material.setTexture("AlphaMap", alphaMap);
+ }
+
+ if (transparent && !disallowTransparency){
+ material.setTransparent(true);
+ material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
+ material.getAdditionalRenderState().setAlphaTest(true);
+ material.getAdditionalRenderState().setAlphaFallOff(0.01f);
+ }
+
+ matList.put(matName, material);
+ }
+
+ protected void startMaterial(String name){
+ if (matName != null){
+ // material is already in cache, generate it
+ createMaterial();
+ }
+
+ // now, reset the params and set the name to start a new material
+ resetMaterial();
+ matName = name;
+ }
+
+ protected Texture loadTexture(String path){
+ String[] split = path.trim().split("\\p{javaWhitespace}+");
+
+ // will crash if path is an empty string
+ path = split[split.length-1];
+
+ String name = new File(path).getName();
+ TextureKey texKey = new TextureKey(folderName + name);
+ texKey.setGenerateMips(true);
+ Texture texture;
+ try {
+ texture = assetManager.loadTexture(texKey);
+ texture.setWrap(WrapMode.Repeat);
+ } catch (AssetNotFoundException ex){
+ logger.log(Level.WARNING, "Cannot locate {0} for material {1}", new Object[]{texKey, key});
+ texture = new Texture2D(PlaceholderAssets.getPlaceholderImage());
+ }
+ return texture;
+ }
+
+ protected boolean readLine(){
+ if (!scan.hasNext()){
+ return false;
+ }
+
+ String cmd = scan.next().toLowerCase();
+ if (cmd.startsWith("#")){
+ // skip entire comment until next line
+ return skipLine();
+ }else if (cmd.equals("newmtl")){
+ String name = scan.next();
+ startMaterial(name);
+ }else if (cmd.equals("ka")){
+ ambient.set(readColor());
+ }else if (cmd.equals("kd")){
+ diffuse.set(readColor());
+ }else if (cmd.equals("ks")){
+ specular.set(readColor());
+ }else if (cmd.equals("ns")){
+ float shiny = scan.nextFloat();
+ if (shiny >= 1){
+ shininess = shiny; /* (128f / 1000f)*/
+ if (specular.equals(ColorRGBA.Black)){
+ specular.set(ColorRGBA.White);
+ }
+ }else{
+ // For some reason blender likes to export Ns 0 statements
+ // Ignore Ns 0 instead of setting it
+ }
+
+ }else if (cmd.equals("d") || cmd.equals("tr")){
+ alpha = scan.nextFloat();
+ transparent = true;
+ }else if (cmd.equals("map_ka")){
+ // ignore it for now
+ return skipLine();
+ }else if (cmd.equals("map_kd")){
+ String path = nextStatement();
+ diffuseMap = loadTexture(path);
+ }else if (cmd.equals("map_bump") || cmd.equals("bump")){
+ if (normalMap == null){
+ String path = nextStatement();
+ normalMap = loadTexture(path);
+ }
+ }else if (cmd.equals("map_ks")){
+ String path = nextStatement();
+ specularMap = loadTexture(path);
+ if (specularMap != null){
+ // NOTE: since specular color is modulated with specmap
+ // make sure we have it set
+ if (specular.equals(ColorRGBA.Black)){
+ specular.set(ColorRGBA.White);
+ }
+ }
+ }else if (cmd.equals("map_d")){
+ String path = scan.next();
+ alphaMap = loadTexture(path);
+ transparent = true;
+ }else if (cmd.equals("illum")){
+ int mode = scan.nextInt();
+
+ switch (mode){
+ case 0:
+ // no lighting
+ shadeless = true;
+ disallowTransparency = true;
+ break;
+ case 1:
+ disallowSpecular = true;
+ disallowTransparency = true;
+ break;
+ case 2:
+ case 3:
+ case 5:
+ case 8:
+ disallowTransparency = true;
+ break;
+ case 4:
+ case 6:
+ case 7:
+ case 9:
+ // Enable transparency
+ // Works best if diffuse map has an alpha channel
+ transparent = true;
+ break;
+ }
+ }else if (cmd.equals("ke") || cmd.equals("ni")){
+ // Ni: index of refraction - unsupported in jME
+ // Ke: emission color
+ return skipLine();
+ }else{
+ logger.log(Level.WARNING, "Unknown statement in MTL! {0}", cmd);
+ return skipLine();
+ }
+
+ return true;
+ }
+
+ @SuppressWarnings("empty-statement")
+ public Object load(AssetInfo info) throws IOException{
+ reset();
+
+ this.key = info.getKey();
+ this.assetManager = info.getManager();
+ folderName = info.getKey().getFolder();
+ matList = new MaterialList();
+
+ InputStream in = null;
+ try {
+ in = info.openStream();
+ scan = new Scanner(in);
+ scan.useLocale(Locale.US);
+
+ while (readLine());
+ } finally {
+ if (in != null){
+ in.close();
+ }
+ }
+
+ if (matName != null){
+ // still have a material in the vars
+ createMaterial();
+ resetMaterial();
+ }
+
+ MaterialList list = matList;
+
+
+
+ return list;
+ }
+}
diff --git a/engine/src/core-plugins/com/jme3/scene/plugins/OBJLoader.java b/engine/src/core-plugins/com/jme3/scene/plugins/OBJLoader.java
new file mode 100644
index 0000000..3ce7f52
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/scene/plugins/OBJLoader.java
@@ -0,0 +1,593 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.scene.plugins;
+
+import com.jme3.asset.*;
+import com.jme3.material.Material;
+import com.jme3.material.MaterialList;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.queue.RenderQueue.Bucket;
+import com.jme3.scene.Mesh.Mode;
+import com.jme3.scene.*;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.scene.mesh.IndexBuffer;
+import com.jme3.scene.mesh.IndexIntBuffer;
+import com.jme3.scene.mesh.IndexShortBuffer;
+import com.jme3.util.BufferUtils;
+import com.jme3.util.IntMap;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.ShortBuffer;
+import java.util.Map.Entry;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Reads OBJ format models.
+ */
+public final class OBJLoader implements AssetLoader {
+
+ private static final Logger logger = Logger.getLogger(OBJLoader.class.getName());
+
+ protected final ArrayList<Vector3f> verts = new ArrayList<Vector3f>();
+ protected final ArrayList<Vector2f> texCoords = new ArrayList<Vector2f>();
+ protected final ArrayList<Vector3f> norms = new ArrayList<Vector3f>();
+
+ protected final ArrayList<Face> faces = new ArrayList<Face>();
+ protected final HashMap<String, ArrayList<Face>> matFaces = new HashMap<String, ArrayList<Face>>();
+
+ protected String currentMatName;
+ protected String currentObjectName;
+
+ protected final HashMap<Vertex, Integer> vertIndexMap = new HashMap<Vertex, Integer>(100);
+ protected final IntMap<Vertex> indexVertMap = new IntMap<Vertex>(100);
+ protected int curIndex = 0;
+ protected int objectIndex = 0;
+ protected int geomIndex = 0;
+
+ protected Scanner scan;
+ protected ModelKey key;
+ protected AssetManager assetManager;
+ protected MaterialList matList;
+
+ protected String objName;
+ protected Node objNode;
+
+ protected static class Vertex {
+
+ Vector3f v;
+ Vector2f vt;
+ Vector3f vn;
+ int index;
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Vertex other = (Vertex) obj;
+ if (this.v != other.v && (this.v == null || !this.v.equals(other.v))) {
+ return false;
+ }
+ if (this.vt != other.vt && (this.vt == null || !this.vt.equals(other.vt))) {
+ return false;
+ }
+ if (this.vn != other.vn && (this.vn == null || !this.vn.equals(other.vn))) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 5;
+ hash = 53 * hash + (this.v != null ? this.v.hashCode() : 0);
+ hash = 53 * hash + (this.vt != null ? this.vt.hashCode() : 0);
+ hash = 53 * hash + (this.vn != null ? this.vn.hashCode() : 0);
+ return hash;
+ }
+ }
+
+ protected static class Face {
+ Vertex[] verticies;
+ }
+
+ protected class ObjectGroup {
+
+ final String objectName;
+
+ public ObjectGroup(String objectName){
+ this.objectName = objectName;
+ }
+
+ public Spatial createGeometry(){
+ Node groupNode = new Node(objectName);
+
+// if (matFaces.size() > 0){
+// for (Entry<String, ArrayList<Face>> entry : matFaces.entrySet()){
+// ArrayList<Face> materialFaces = entry.getValue();
+// if (materialFaces.size() > 0){
+// Geometry geom = createGeometry(materialFaces, entry.getKey());
+// objNode.attachChild(geom);
+// }
+// }
+// }else if (faces.size() > 0){
+// // generate final geometry
+// Geometry geom = createGeometry(faces, null);
+// objNode.attachChild(geom);
+// }
+
+ return groupNode;
+ }
+ }
+
+ public void reset(){
+ verts.clear();
+ texCoords.clear();
+ norms.clear();
+ faces.clear();
+ matFaces.clear();
+
+ vertIndexMap.clear();
+ indexVertMap.clear();
+
+ currentMatName = null;
+ matList = null;
+ curIndex = 0;
+ geomIndex = 0;
+ scan = null;
+ }
+
+ protected void findVertexIndex(Vertex vert){
+ Integer index = vertIndexMap.get(vert);
+ if (index != null){
+ vert.index = index.intValue();
+ }else{
+ vert.index = curIndex++;
+ vertIndexMap.put(vert, vert.index);
+ indexVertMap.put(vert.index, vert);
+ }
+ }
+
+ protected Face[] quadToTriangle(Face f){
+ assert f.verticies.length == 4;
+
+ Face[] t = new Face[]{ new Face(), new Face() };
+ t[0].verticies = new Vertex[3];
+ t[1].verticies = new Vertex[3];
+
+ Vertex v0 = f.verticies[0];
+ Vertex v1 = f.verticies[1];
+ Vertex v2 = f.verticies[2];
+ Vertex v3 = f.verticies[3];
+
+ // find the pair of verticies that is closest to each over
+ // v0 and v2
+ // OR
+ // v1 and v3
+ float d1 = v0.v.distanceSquared(v2.v);
+ float d2 = v1.v.distanceSquared(v3.v);
+ if (d1 < d2){
+ // put an edge in v0, v2
+ t[0].verticies[0] = v0;
+ t[0].verticies[1] = v1;
+ t[0].verticies[2] = v3;
+
+ t[1].verticies[0] = v1;
+ t[1].verticies[1] = v2;
+ t[1].verticies[2] = v3;
+ }else{
+ // put an edge in v1, v3
+ t[0].verticies[0] = v0;
+ t[0].verticies[1] = v1;
+ t[0].verticies[2] = v2;
+
+ t[1].verticies[0] = v0;
+ t[1].verticies[1] = v2;
+ t[1].verticies[2] = v3;
+ }
+
+ return t;
+ }
+
+ private ArrayList<Vertex> vertList = new ArrayList<Vertex>();
+
+ protected void readFace(){
+ Face f = new Face();
+ vertList.clear();
+
+ String line = scan.nextLine().trim();
+ String[] verticies = line.split("\\s");
+ for (String vertex : verticies){
+ int v = 0;
+ int vt = 0;
+ int vn = 0;
+
+ String[] split = vertex.split("/");
+ if (split.length == 1){
+ v = Integer.parseInt(split[0].trim());
+ }else if (split.length == 2){
+ v = Integer.parseInt(split[0].trim());
+ vt = Integer.parseInt(split[1].trim());
+ }else if (split.length == 3 && !split[1].equals("")){
+ v = Integer.parseInt(split[0].trim());
+ vt = Integer.parseInt(split[1].trim());
+ vn = Integer.parseInt(split[2].trim());
+ }else if (split.length == 3){
+ v = Integer.parseInt(split[0].trim());
+ vn = Integer.parseInt(split[2].trim());
+ }
+
+ Vertex vx = new Vertex();
+ vx.v = verts.get(v - 1);
+
+ if (vt > 0)
+ vx.vt = texCoords.get(vt - 1);
+
+ if (vn > 0)
+ vx.vn = norms.get(vn - 1);
+
+ vertList.add(vx);
+ }
+
+ if (vertList.size() > 4 || vertList.size() <= 2)
+ logger.warning("Edge or polygon detected in OBJ. Ignored.");
+
+ f.verticies = new Vertex[vertList.size()];
+ for (int i = 0; i < vertList.size(); i++){
+ f.verticies[i] = vertList.get(i);
+ }
+
+ if (matList != null && matFaces.containsKey(currentMatName)){
+ matFaces.get(currentMatName).add(f);
+ }else{
+ faces.add(f); // faces that belong to the default material
+ }
+ }
+
+ protected Vector3f readVector3(){
+ Vector3f v = new Vector3f();
+
+ v.set(Float.parseFloat(scan.next()),
+ Float.parseFloat(scan.next()),
+ Float.parseFloat(scan.next()));
+
+ return v;
+ }
+
+ protected Vector2f readVector2(){
+ Vector2f v = new Vector2f();
+
+ String line = scan.nextLine().trim();
+ String[] split = line.split("\\s");
+ v.setX( Float.parseFloat(split[0].trim()) );
+ v.setY( Float.parseFloat(split[1].trim()) );
+
+// v.setX(scan.nextFloat());
+// if (scan.hasNextFloat()){
+// v.setY(scan.nextFloat());
+// if (scan.hasNextFloat()){
+// scan.nextFloat(); // ignore
+// }
+// }
+
+ return v;
+ }
+
+ protected void loadMtlLib(String name) throws IOException{
+ if (!name.toLowerCase().endsWith(".mtl"))
+ throw new IOException("Expected .mtl file! Got: " + name);
+
+ // NOTE: Cut off any relative/absolute paths
+ name = new File(name).getName();
+ AssetKey mtlKey = new AssetKey(key.getFolder() + name);
+ try {
+ matList = (MaterialList) assetManager.loadAsset(mtlKey);
+ } catch (AssetNotFoundException ex){
+ logger.log(Level.WARNING, "Cannot locate {0} for model {1}", new Object[]{name, key});
+ }
+
+ if (matList != null){
+ // create face lists for every material
+ for (String matName : matList.keySet()){
+ matFaces.put(matName, new ArrayList<Face>());
+ }
+ }
+ }
+
+ protected boolean nextStatement(){
+ try {
+ scan.skip(".*\r{0,1}\n");
+ return true;
+ } catch (NoSuchElementException ex){
+ // EOF
+ return false;
+ }
+ }
+
+ protected boolean readLine() throws IOException{
+ if (!scan.hasNext()){
+ return false;
+ }
+
+ String cmd = scan.next();
+ if (cmd.startsWith("#")){
+ // skip entire comment until next line
+ return nextStatement();
+ }else if (cmd.equals("v")){
+ // vertex position
+ verts.add(readVector3());
+ }else if (cmd.equals("vn")){
+ // vertex normal
+ norms.add(readVector3());
+ }else if (cmd.equals("vt")){
+ // texture coordinate
+ texCoords.add(readVector2());
+ }else if (cmd.equals("f")){
+ // face, can be triangle, quad, or polygon (unsupported)
+ readFace();
+ }else if (cmd.equals("usemtl")){
+ // use material from MTL lib for the following faces
+ currentMatName = scan.next();
+// if (!matList.containsKey(currentMatName))
+// throw new IOException("Cannot locate material " + currentMatName + " in MTL file!");
+
+ }else if (cmd.equals("mtllib")){
+ // specify MTL lib to use for this OBJ file
+ String mtllib = scan.nextLine().trim();
+ loadMtlLib(mtllib);
+ }else if (cmd.equals("s") || cmd.equals("g")){
+ return nextStatement();
+ }else{
+ // skip entire command until next line
+ logger.log(Level.WARNING, "Unknown statement in OBJ! {0}", cmd);
+ return nextStatement();
+ }
+
+ return true;
+ }
+
+ protected Geometry createGeometry(ArrayList<Face> faceList, String matName) throws IOException{
+ if (faceList.isEmpty())
+ throw new IOException("No geometry data to generate mesh");
+
+ // Create mesh from the faces
+ Mesh mesh = constructMesh(faceList);
+
+ Geometry geom = new Geometry(objName + "-geom-" + (geomIndex++), mesh);
+
+ Material material = null;
+ if (matName != null && matList != null){
+ // Get material from material list
+ material = matList.get(matName);
+ }
+ if (material == null){
+ // create default material
+ material = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+ material.setFloat("Shininess", 64);
+ }
+ geom.setMaterial(material);
+ if (material.isTransparent())
+ geom.setQueueBucket(Bucket.Transparent);
+ else
+ geom.setQueueBucket(Bucket.Opaque);
+
+ if (material.getMaterialDef().getName().contains("Lighting")
+ && mesh.getFloatBuffer(Type.Normal) == null){
+ logger.log(Level.WARNING, "OBJ mesh {0} doesn't contain normals! "
+ + "It might not display correctly", geom.getName());
+ }
+
+ return geom;
+ }
+
+ protected Mesh constructMesh(ArrayList<Face> faceList){
+ Mesh m = new Mesh();
+ m.setMode(Mode.Triangles);
+
+ boolean hasTexCoord = false;
+ boolean hasNormals = false;
+
+ ArrayList<Face> newFaces = new ArrayList<Face>(faceList.size());
+ for (int i = 0; i < faceList.size(); i++){
+ Face f = faceList.get(i);
+
+ for (Vertex v : f.verticies){
+ findVertexIndex(v);
+
+ if (!hasTexCoord && v.vt != null)
+ hasTexCoord = true;
+ if (!hasNormals && v.vn != null)
+ hasNormals = true;
+ }
+
+ if (f.verticies.length == 4){
+ Face[] t = quadToTriangle(f);
+ newFaces.add(t[0]);
+ newFaces.add(t[1]);
+ }else{
+ newFaces.add(f);
+ }
+ }
+
+ FloatBuffer posBuf = BufferUtils.createFloatBuffer(vertIndexMap.size() * 3);
+ FloatBuffer normBuf = null;
+ FloatBuffer tcBuf = null;
+
+ if (hasNormals){
+ normBuf = BufferUtils.createFloatBuffer(vertIndexMap.size() * 3);
+ }
+ if (hasTexCoord){
+ tcBuf = BufferUtils.createFloatBuffer(vertIndexMap.size() * 2);
+ }
+
+ IndexBuffer indexBuf = null;
+ if (vertIndexMap.size() >= 65536){
+ // too many verticies: use intbuffer instead of shortbuffer
+ IntBuffer ib = BufferUtils.createIntBuffer(newFaces.size() * 3);
+ m.setBuffer(VertexBuffer.Type.Index, 3, ib);
+ indexBuf = new IndexIntBuffer(ib);
+ }else{
+ ShortBuffer sb = BufferUtils.createShortBuffer(newFaces.size() * 3);
+ m.setBuffer(VertexBuffer.Type.Index, 3, sb);
+ indexBuf = new IndexShortBuffer(sb);
+ }
+
+ int numFaces = newFaces.size();
+ for (int i = 0; i < numFaces; i++){
+ Face f = newFaces.get(i);
+ if (f.verticies.length != 3)
+ continue;
+
+ Vertex v0 = f.verticies[0];
+ Vertex v1 = f.verticies[1];
+ Vertex v2 = f.verticies[2];
+
+ posBuf.position(v0.index * 3);
+ posBuf.put(v0.v.x).put(v0.v.y).put(v0.v.z);
+ posBuf.position(v1.index * 3);
+ posBuf.put(v1.v.x).put(v1.v.y).put(v1.v.z);
+ posBuf.position(v2.index * 3);
+ posBuf.put(v2.v.x).put(v2.v.y).put(v2.v.z);
+
+ if (normBuf != null){
+ if (v0.vn != null){
+ normBuf.position(v0.index * 3);
+ normBuf.put(v0.vn.x).put(v0.vn.y).put(v0.vn.z);
+ normBuf.position(v1.index * 3);
+ normBuf.put(v1.vn.x).put(v1.vn.y).put(v1.vn.z);
+ normBuf.position(v2.index * 3);
+ normBuf.put(v2.vn.x).put(v2.vn.y).put(v2.vn.z);
+ }
+ }
+
+ if (tcBuf != null){
+ if (v0.vt != null){
+ tcBuf.position(v0.index * 2);
+ tcBuf.put(v0.vt.x).put(v0.vt.y);
+ tcBuf.position(v1.index * 2);
+ tcBuf.put(v1.vt.x).put(v1.vt.y);
+ tcBuf.position(v2.index * 2);
+ tcBuf.put(v2.vt.x).put(v2.vt.y);
+ }
+ }
+
+ int index = i * 3; // current face * 3 = current index
+ indexBuf.put(index, v0.index);
+ indexBuf.put(index+1, v1.index);
+ indexBuf.put(index+2, v2.index);
+ }
+
+ m.setBuffer(VertexBuffer.Type.Position, 3, posBuf);
+ m.setBuffer(VertexBuffer.Type.Normal, 3, normBuf);
+ m.setBuffer(VertexBuffer.Type.TexCoord, 2, tcBuf);
+ // index buffer was set on creation
+
+ m.setStatic();
+ m.updateBound();
+ m.updateCounts();
+ //m.setInterleaved();
+
+ // clear data generated face statements
+ // to prepare for next mesh
+ vertIndexMap.clear();
+ indexVertMap.clear();
+ curIndex = 0;
+
+ return m;
+ }
+
+ @SuppressWarnings("empty-statement")
+ public Object load(AssetInfo info) throws IOException{
+ reset();
+
+ key = (ModelKey) info.getKey();
+ assetManager = info.getManager();
+ objName = key.getName();
+
+ String folderName = key.getFolder();
+ String ext = key.getExtension();
+ objName = objName.substring(0, objName.length() - ext.length() - 1);
+ if (folderName != null && folderName.length() > 0){
+ objName = objName.substring(folderName.length());
+ }
+
+ objNode = new Node(objName + "-objnode");
+
+ if (!(info.getKey() instanceof ModelKey))
+ throw new IllegalArgumentException("Model assets must be loaded using a ModelKey");
+
+ InputStream in = null;
+ try {
+ in = info.openStream();
+
+ scan = new Scanner(in);
+ scan.useLocale(Locale.US);
+
+ while (readLine());
+ } finally {
+ if (in != null){
+ in.close();
+ }
+ }
+
+ if (matFaces.size() > 0){
+ for (Entry<String, ArrayList<Face>> entry : matFaces.entrySet()){
+ ArrayList<Face> materialFaces = entry.getValue();
+ if (materialFaces.size() > 0){
+ Geometry geom = createGeometry(materialFaces, entry.getKey());
+ objNode.attachChild(geom);
+ }
+ }
+ }else if (faces.size() > 0){
+ // generate final geometry
+ Geometry geom = createGeometry(faces, null);
+ objNode.attachChild(geom);
+ }
+
+ if (objNode.getQuantity() == 1)
+ // only 1 geometry, so no need to send node
+ return objNode.getChild(0);
+ else
+ return objNode;
+ }
+
+}
diff --git a/engine/src/core-plugins/com/jme3/shader/plugins/GLSLLoader.java b/engine/src/core-plugins/com/jme3/shader/plugins/GLSLLoader.java
new file mode 100644
index 0000000..f9073b7
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/shader/plugins/GLSLLoader.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.shader.plugins;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetKey;
+import com.jme3.asset.AssetLoader;
+import com.jme3.asset.AssetManager;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.*;
+
+/**
+ * GLSL File parser that supports #import pre-processor statement
+ */
+public class GLSLLoader implements AssetLoader {
+
+ private AssetManager owner;
+ private Map<String, DependencyNode> dependCache = new HashMap<String, DependencyNode>();
+
+ private class DependencyNode {
+
+ private String shaderSource;
+ private String shaderName;
+
+ private final Set<DependencyNode> dependsOn = new HashSet<DependencyNode>();
+ private final Set<DependencyNode> dependOnMe = new HashSet<DependencyNode>();
+
+ public DependencyNode(String shaderName){
+ this.shaderName = shaderName;
+ }
+
+ public void setSource(String source){
+ this.shaderSource = source;
+ }
+
+ public void addDependency(DependencyNode node){
+ if (this.dependsOn.contains(node))
+ return; // already contains dependency
+
+// System.out.println(shaderName + " depend on "+node.shaderName);
+ this.dependsOn.add(node);
+ node.dependOnMe.add(this);
+ }
+
+ }
+
+ private class GlslDependKey extends AssetKey<InputStream> {
+ public GlslDependKey(String name){
+ super(name);
+ }
+ @Override
+ public boolean shouldCache(){
+ return false;
+ }
+ }
+
+ private DependencyNode loadNode(InputStream in, String nodeName) throws IOException{
+ DependencyNode node = new DependencyNode(nodeName);
+ if (in == null)
+ throw new IOException("Dependency "+nodeName+" cannot be found.");
+
+ StringBuilder sb = new StringBuilder();
+ BufferedReader r = new BufferedReader(new InputStreamReader(in));
+ while (r.ready()){
+ String ln = r.readLine();
+ if (ln.startsWith("#import ")){
+ ln = ln.substring(8).trim();
+ if (ln.startsWith("\"") && ln.endsWith("\"") && ln.length() > 3){
+ // import user code
+ // remove quotes to get filename
+ ln = ln.substring(1, ln.length()-1);
+ if (ln.equals(nodeName))
+ throw new IOException("Node depends on itself.");
+
+ // check cache first
+ DependencyNode dependNode = dependCache.get(ln);
+ if (dependNode == null){
+ GlslDependKey key = new GlslDependKey(ln);
+ // make sure not to register an input stream with
+ // the cache..
+ InputStream stream = (InputStream) owner.loadAsset(key);
+ dependNode = loadNode(stream, ln);
+ }
+ node.addDependency(dependNode);
+ }
+// }else if (ln.startsWith("uniform") || ln.startsWith("varying") || ln.startsWith("attribute")){
+// // these variables are included as dependencies as well
+// DependencyNode dependNode = dependCache.get(ln);
+// if (dependNode == null){
+// // the source and name are the same for variable dependencies
+// dependNode = new DependencyNode(ln);
+// dependNode.setSource(ln);
+// dependCache.put(ln, dependNode);
+// }
+// node.addDependency(dependNode);
+ }else{
+ sb.append(ln).append('\n');
+ }
+ }
+ r.close();
+
+ node.setSource(sb.toString());
+ dependCache.put(nodeName, node);
+ return node;
+ }
+
+ private DependencyNode nextIndependentNode(List<DependencyNode> checkedNodes){
+ Collection<DependencyNode> allNodes = dependCache.values();
+ if (allNodes == null || allNodes.isEmpty())
+ return null;
+
+ for (DependencyNode node : allNodes){
+ if (node.dependsOn.isEmpty()){
+ return node;
+ }
+ }
+
+ // circular dependency found..
+ for (DependencyNode node : allNodes){
+ System.out.println(node.shaderName);
+ }
+ throw new RuntimeException("Circular dependency.");
+ }
+
+ private String resolveDependencies(DependencyNode root){
+ StringBuilder sb = new StringBuilder();
+
+ List<DependencyNode> checkedNodes = new ArrayList<DependencyNode>();
+ checkedNodes.add(root);
+ while (true){
+ DependencyNode indepnNode = nextIndependentNode(checkedNodes);
+ if (indepnNode == null)
+ break;
+
+ sb.append(indepnNode.shaderSource).append('\n');
+ dependCache.remove(indepnNode.shaderName);
+
+ // take out this dependency
+ for (Iterator<DependencyNode> iter = indepnNode.dependOnMe.iterator();
+ iter.hasNext();){
+ DependencyNode dependNode = iter.next();
+ iter.remove();
+ dependNode.dependsOn.remove(indepnNode);
+ }
+ }
+
+// System.out.println(sb.toString());
+// System.out.println("--------------------------------------------------");
+
+ return sb.toString();
+ }
+
+ /**
+ *
+ * @param owner
+ * @param in
+ * @param extension
+ * @param key
+ * @return
+ * @throws java.io.IOException
+ */
+ public Object load(AssetInfo info) throws IOException {
+ // The input stream provided is for the vertex shader,
+ // to retrieve the fragment shader, use the content manager
+ this.owner = info.getManager();
+ if (info.getKey().getExtension().equals("glsllib")){
+ // NOTE: Loopback, GLSLLIB is loaded by this loader
+ // and needs data as InputStream
+ return info.openStream();
+ }else{
+ // GLSLLoader wants result as String for
+ // fragment shader
+ DependencyNode rootNode = loadNode(info.openStream(), "[main]");
+ String code = resolveDependencies(rootNode);
+ dependCache.clear();
+ return code;
+ }
+ }
+
+}
diff --git a/engine/src/core-plugins/com/jme3/texture/plugins/DDSLoader.java b/engine/src/core-plugins/com/jme3/texture/plugins/DDSLoader.java
new file mode 100644
index 0000000..897c9eb
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/texture/plugins/DDSLoader.java
@@ -0,0 +1,827 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.texture.plugins;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetLoader;
+import com.jme3.asset.TextureKey;
+import com.jme3.texture.Image;
+import com.jme3.texture.Image.Format;
+import com.jme3.texture.Texture.Type;
+import com.jme3.util.BufferUtils;
+import com.jme3.util.LittleEndien;
+import java.io.DataInput;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ *
+ * <code>DDSLoader</code> is an image loader that reads in a DirectX DDS file.
+ * Supports DXT1, DXT3, DXT5, RGB, RGBA, Grayscale, Alpha pixel formats.
+ * 2D images, mipmapped 2D images, and cubemaps.
+ *
+ * @author Gareth Jenkins-Jones
+ * @author Kirill Vainer
+ * @version $Id: DDSLoader.java,v 2.0 2008/8/15
+ */
+public class DDSLoader implements AssetLoader {
+
+ private static final Logger logger = Logger.getLogger(DDSLoader.class.getName());
+ private static final boolean forceRGBA = false;
+ private static final int DDSD_MANDATORY = 0x1007;
+ private static final int DDSD_MANDATORY_DX10 = 0x6;
+ private static final int DDSD_MIPMAPCOUNT = 0x20000;
+ private static final int DDSD_LINEARSIZE = 0x80000;
+ private static final int DDSD_DEPTH = 0x800000;
+ private static final int DDPF_ALPHAPIXELS = 0x1;
+ private static final int DDPF_FOURCC = 0x4;
+ private static final int DDPF_RGB = 0x40;
+ // used by compressonator to mark grayscale images, red channel mask is used for data and bitcount is 8
+ private static final int DDPF_GRAYSCALE = 0x20000;
+ // used by compressonator to mark alpha images, alpha channel mask is used for data and bitcount is 8
+ private static final int DDPF_ALPHA = 0x2;
+ // used by NVTextureTools to mark normal images.
+ private static final int DDPF_NORMAL = 0x80000000;
+ private static final int SWIZZLE_xGxR = 0x78477852;
+ private static final int DDSCAPS_COMPLEX = 0x8;
+ private static final int DDSCAPS_TEXTURE = 0x1000;
+ private static final int DDSCAPS_MIPMAP = 0x400000;
+ private static final int DDSCAPS2_CUBEMAP = 0x200;
+ private static final int DDSCAPS2_VOLUME = 0x200000;
+ private static final int PF_DXT1 = 0x31545844;
+ private static final int PF_DXT3 = 0x33545844;
+ private static final int PF_DXT5 = 0x35545844;
+ private static final int PF_ATI1 = 0x31495441;
+ private static final int PF_ATI2 = 0x32495441; // 0x41544932;
+ private static final int PF_DX10 = 0x30315844; // a DX10 format
+ private static final int DX10DIM_BUFFER = 0x1,
+ DX10DIM_TEXTURE1D = 0x2,
+ DX10DIM_TEXTURE2D = 0x3,
+ DX10DIM_TEXTURE3D = 0x4;
+ private static final int DX10MISC_GENERATE_MIPS = 0x1,
+ DX10MISC_TEXTURECUBE = 0x4;
+ private static final double LOG2 = Math.log(2);
+ private int width;
+ private int height;
+ private int depth;
+ private int flags;
+ private int pitchOrSize;
+ private int mipMapCount;
+ private int caps1;
+ private int caps2;
+ private boolean directx10;
+ private boolean compressed;
+ private boolean texture3D;
+ private boolean grayscaleOrAlpha;
+ private boolean normal;
+ private Format pixelFormat;
+ private int bpp;
+ private int[] sizes;
+ private int redMask, greenMask, blueMask, alphaMask;
+ private DataInput in;
+
+ public DDSLoader() {
+ }
+
+ public Object load(AssetInfo info) throws IOException {
+ if (!(info.getKey() instanceof TextureKey)) {
+ throw new IllegalArgumentException("Texture assets must be loaded using a TextureKey");
+ }
+
+ InputStream stream = null;
+ try {
+ stream = info.openStream();
+ in = new LittleEndien(stream);
+ loadHeader();
+ if (texture3D) {
+ ((TextureKey) info.getKey()).setTextureTypeHint(Type.ThreeDimensional);
+ } else if (depth > 1) {
+ ((TextureKey) info.getKey()).setTextureTypeHint(Type.CubeMap);
+ }
+ ArrayList<ByteBuffer> data = readData(((TextureKey) info.getKey()).isFlipY());
+ return new Image(pixelFormat, width, height, depth, data, sizes);
+ } finally {
+ if (stream != null){
+ stream.close();
+ }
+ }
+ }
+
+ public Image load(InputStream stream) throws IOException {
+ in = new LittleEndien(stream);
+ loadHeader();
+ ArrayList<ByteBuffer> data = readData(false);
+ return new Image(pixelFormat, width, height, depth, data, sizes);
+ }
+
+ private void loadDX10Header() throws IOException {
+ int dxgiFormat = in.readInt();
+ if (dxgiFormat != 83) {
+ throw new IOException("Only DXGI_FORMAT_BC5_UNORM "
+ + "is supported for DirectX10 DDS! Got: " + dxgiFormat);
+ }
+ pixelFormat = Format.LATC;
+ bpp = 8;
+ compressed = true;
+
+ int resDim = in.readInt();
+ if (resDim == DX10DIM_TEXTURE3D) {
+ texture3D = true;
+ }
+ int miscFlag = in.readInt();
+ int arraySize = in.readInt();
+ if (is(miscFlag, DX10MISC_TEXTURECUBE)) {
+ // mark texture as cube
+ if (arraySize != 6) {
+ throw new IOException("Cubemaps should consist of 6 images!");
+ }
+ }
+
+ in.skipBytes(4); // skip reserved value
+ }
+
+ /**
+ * Reads the header (first 128 bytes) of a DDS File
+ */
+ private void loadHeader() throws IOException {
+ if (in.readInt() != 0x20534444 || in.readInt() != 124) {
+ throw new IOException("Not a DDS file");
+ }
+
+ flags = in.readInt();
+
+ if (!is(flags, DDSD_MANDATORY) && !is(flags, DDSD_MANDATORY_DX10)) {
+ throw new IOException("Mandatory flags missing");
+ }
+
+ height = in.readInt();
+ width = in.readInt();
+ pitchOrSize = in.readInt();
+ depth = in.readInt();
+ mipMapCount = in.readInt();
+ in.skipBytes(44);
+ pixelFormat = null;
+ directx10 = false;
+ readPixelFormat();
+ caps1 = in.readInt();
+ caps2 = in.readInt();
+ in.skipBytes(12);
+ texture3D = false;
+
+ if (!directx10) {
+ if (!is(caps1, DDSCAPS_TEXTURE)) {
+ throw new IOException("File is not a texture");
+ }
+
+ if (depth <= 0) {
+ depth = 1;
+ }
+
+ if (is(caps2, DDSCAPS2_CUBEMAP)) {
+ depth = 6; // somewhat of a hack, force loading 6 textures if a cubemap
+ }
+
+ if (is(caps2, DDSCAPS2_VOLUME)) {
+ texture3D = true;
+ }
+ }
+
+ int expectedMipmaps = 1 + (int) Math.ceil(Math.log(Math.max(height, width)) / LOG2);
+
+ if (is(caps1, DDSCAPS_MIPMAP)) {
+ if (!is(flags, DDSD_MIPMAPCOUNT)) {
+ mipMapCount = expectedMipmaps;
+ } else if (mipMapCount != expectedMipmaps) {
+ // changed to warning- images often do not have the required amount,
+ // or specify that they have mipmaps but include only the top level..
+ logger.log(Level.WARNING, "Got {0} mipmaps, expected {1}",
+ new Object[]{mipMapCount, expectedMipmaps});
+ }
+ } else {
+ mipMapCount = 1;
+ }
+
+ if (directx10) {
+ loadDX10Header();
+ }
+
+ loadSizes();
+ }
+
+ /**
+ * Reads the PixelFormat structure in a DDS file
+ */
+ private void readPixelFormat() throws IOException {
+ int pfSize = in.readInt();
+ if (pfSize != 32) {
+ throw new IOException("Pixel format size is " + pfSize + ", not 32");
+ }
+
+ int pfFlags = in.readInt();
+ normal = is(pfFlags, DDPF_NORMAL);
+
+ if (is(pfFlags, DDPF_FOURCC)) {
+ compressed = true;
+ int fourcc = in.readInt();
+ int swizzle = in.readInt();
+ in.skipBytes(16);
+
+ switch (fourcc) {
+ case PF_DXT1:
+ bpp = 4;
+ if (is(pfFlags, DDPF_ALPHAPIXELS)) {
+ pixelFormat = Image.Format.DXT1A;
+ } else {
+ pixelFormat = Image.Format.DXT1;
+ }
+ break;
+ case PF_DXT3:
+ bpp = 8;
+ pixelFormat = Image.Format.DXT3;
+ break;
+ case PF_DXT5:
+ bpp = 8;
+ pixelFormat = Image.Format.DXT5;
+ if (swizzle == SWIZZLE_xGxR) {
+ normal = true;
+ }
+ break;
+ case PF_ATI1:
+ bpp = 4;
+ pixelFormat = Image.Format.LTC;
+ break;
+ case PF_ATI2:
+ bpp = 8;
+ pixelFormat = Image.Format.LATC;
+ break;
+ case PF_DX10:
+ compressed = false;
+ directx10 = true;
+ // exit here, the rest of the structure is not valid
+ // the real format will be available in the DX10 header
+ return;
+ default:
+ throw new IOException("Unknown fourcc: " + string(fourcc) + ", " + Integer.toHexString(fourcc));
+ }
+
+ int size = ((width + 3) / 4) * ((height + 3) / 4) * bpp * 2;
+
+ if (is(flags, DDSD_LINEARSIZE)) {
+ if (pitchOrSize == 0) {
+ logger.warning("Must use linear size with fourcc");
+ pitchOrSize = size;
+ } else if (pitchOrSize != size) {
+ logger.log(Level.WARNING, "Expected size = {0}, real = {1}",
+ new Object[]{size, pitchOrSize});
+ }
+ } else {
+ pitchOrSize = size;
+ }
+ } else {
+ compressed = false;
+
+ // skip fourCC
+ in.readInt();
+
+ bpp = in.readInt();
+ redMask = in.readInt();
+ greenMask = in.readInt();
+ blueMask = in.readInt();
+ alphaMask = in.readInt();
+
+ if (is(pfFlags, DDPF_RGB)) {
+ if (is(pfFlags, DDPF_ALPHAPIXELS)) {
+ pixelFormat = Format.RGBA8;
+ } else {
+ pixelFormat = Format.RGB8;
+ }
+ } else if (is(pfFlags, DDPF_GRAYSCALE) && is(pfFlags, DDPF_ALPHAPIXELS)) {
+ switch (bpp) {
+ case 16:
+ pixelFormat = Format.Luminance8Alpha8;
+ break;
+ case 32:
+ pixelFormat = Format.Luminance16Alpha16;
+ break;
+ default:
+ throw new IOException("Unsupported GrayscaleAlpha BPP: " + bpp);
+ }
+ grayscaleOrAlpha = true;
+ } else if (is(pfFlags, DDPF_GRAYSCALE)) {
+ switch (bpp) {
+ case 8:
+ pixelFormat = Format.Luminance8;
+ break;
+ case 16:
+ pixelFormat = Format.Luminance16;
+ break;
+ default:
+ throw new IOException("Unsupported Grayscale BPP: " + bpp);
+ }
+ grayscaleOrAlpha = true;
+ } else if (is(pfFlags, DDPF_ALPHA)) {
+ switch (bpp) {
+ case 8:
+ pixelFormat = Format.Alpha8;
+ break;
+ case 16:
+ pixelFormat = Format.Alpha16;
+ break;
+ default:
+ throw new IOException("Unsupported Alpha BPP: " + bpp);
+ }
+ grayscaleOrAlpha = true;
+ } else {
+ throw new IOException("Unknown PixelFormat in DDS file");
+ }
+
+ int size = (bpp / 8 * width);
+
+ if (is(flags, DDSD_LINEARSIZE)) {
+ if (pitchOrSize == 0) {
+ logger.warning("Linear size said to contain valid value but does not");
+ pitchOrSize = size;
+ } else if (pitchOrSize != size) {
+ logger.log(Level.WARNING, "Expected size = {0}, real = {1}",
+ new Object[]{size, pitchOrSize});
+ }
+ } else {
+ pitchOrSize = size;
+ }
+ }
+ }
+
+ /**
+ * Computes the sizes of each mipmap level in bytes, and stores it in sizes_[].
+ */
+ private void loadSizes() {
+ int mipWidth = width;
+ int mipHeight = height;
+
+ sizes = new int[mipMapCount];
+ int outBpp = pixelFormat.getBitsPerPixel();
+ for (int i = 0; i < mipMapCount; i++) {
+ int size;
+ if (compressed) {
+ size = ((mipWidth + 3) / 4) * ((mipHeight + 3) / 4) * outBpp * 2;
+ } else {
+ size = mipWidth * mipHeight * outBpp / 8;
+ }
+
+ sizes[i] = ((size + 3) / 4) * 4;
+
+ mipWidth = Math.max(mipWidth / 2, 1);
+ mipHeight = Math.max(mipHeight / 2, 1);
+ }
+ }
+
+ /**
+ * Flips the given image data on the Y axis.
+ * @param data Data array containing image data (without mipmaps)
+ * @param scanlineSize Size of a single scanline = width * bytesPerPixel
+ * @param height Height of the image in pixels
+ * @return The new data flipped by the Y axis
+ */
+ public byte[] flipData(byte[] data, int scanlineSize, int height) {
+ byte[] newData = new byte[data.length];
+
+ for (int y = 0; y < height; y++) {
+ System.arraycopy(data, y * scanlineSize,
+ newData, (height - y - 1) * scanlineSize,
+ scanlineSize);
+ }
+
+ return newData;
+ }
+
+ /**
+ * Reads a grayscale image with mipmaps from the InputStream
+ * @param flip Flip the loaded image by Y axis
+ * @param totalSize Total size of the image in bytes including the mipmaps
+ * @return A ByteBuffer containing the grayscale image data with mips.
+ * @throws java.io.IOException If an error occured while reading from InputStream
+ */
+ public ByteBuffer readGrayscale2D(boolean flip, int totalSize) throws IOException {
+ ByteBuffer buffer = BufferUtils.createByteBuffer(totalSize);
+
+ if (bpp == 8) {
+ logger.finest("Source image format: R8");
+ }
+
+ assert bpp == pixelFormat.getBitsPerPixel();
+
+ int mipWidth = width;
+ int mipHeight = height;
+
+ for (int mip = 0; mip < mipMapCount; mip++) {
+ byte[] data = new byte[sizes[mip]];
+ in.readFully(data);
+ if (flip) {
+ data = flipData(data, mipWidth * bpp / 8, mipHeight);
+ }
+ buffer.put(data);
+
+ mipWidth = Math.max(mipWidth / 2, 1);
+ mipHeight = Math.max(mipHeight / 2, 1);
+ }
+
+ return buffer;
+ }
+
+ /**
+ * Reads an uncompressed RGB or RGBA image.
+ *
+ * @param flip Flip the image on the Y axis
+ * @param totalSize Size of the image in bytes including mipmaps
+ * @return ByteBuffer containing image data with mipmaps in the format specified by pixelFormat_
+ * @throws java.io.IOException If an error occured while reading from InputStream
+ */
+ public ByteBuffer readRGB2D(boolean flip, int totalSize) throws IOException {
+ int redCount = count(redMask),
+ blueCount = count(blueMask),
+ greenCount = count(greenMask),
+ alphaCount = count(alphaMask);
+
+ if (redMask == 0x00FF0000 && greenMask == 0x0000FF00 && blueMask == 0x000000FF) {
+ if (alphaMask == 0xFF000000 && bpp == 32) {
+ logger.finest("Data source format: BGRA8");
+ } else if (bpp == 24) {
+ logger.finest("Data source format: BGR8");
+ }
+ }
+
+ int sourcebytesPP = bpp / 8;
+ int targetBytesPP = pixelFormat.getBitsPerPixel() / 8;
+
+ ByteBuffer dataBuffer = BufferUtils.createByteBuffer(totalSize);
+
+ int mipWidth = width;
+ int mipHeight = height;
+
+ int offset = 0;
+ byte[] b = new byte[sourcebytesPP];
+ for (int mip = 0; mip < mipMapCount; mip++) {
+ for (int y = 0; y < mipHeight; y++) {
+ for (int x = 0; x < mipWidth; x++) {
+ in.readFully(b);
+
+ int i = byte2int(b);
+
+ byte red = (byte) (((i & redMask) >> redCount));
+ byte green = (byte) (((i & greenMask) >> greenCount));
+ byte blue = (byte) (((i & blueMask) >> blueCount));
+ byte alpha = (byte) (((i & alphaMask) >> alphaCount));
+
+ if (flip) {
+ dataBuffer.position(offset + ((mipHeight - y - 1) * mipWidth + x) * targetBytesPP);
+ }
+ //else
+ // dataBuffer.position(offset + (y * width + x) * targetBytesPP);
+
+ if (alphaMask == 0) {
+ dataBuffer.put(red).put(green).put(blue);
+ } else {
+ dataBuffer.put(red).put(green).put(blue).put(alpha);
+ }
+ }
+ }
+
+ offset += mipWidth * mipHeight * targetBytesPP;
+
+ mipWidth = Math.max(mipWidth / 2, 1);
+ mipHeight = Math.max(mipHeight / 2, 1);
+ }
+
+ return dataBuffer;
+ }
+
+ /**
+ * Reads a DXT compressed image from the InputStream
+ *
+ * @param totalSize Total size of the image in bytes, including mipmaps
+ * @return ByteBuffer containing compressed DXT image in the format specified by pixelFormat_
+ * @throws java.io.IOException If an error occured while reading from InputStream
+ */
+ public ByteBuffer readDXT2D(boolean flip, int totalSize) throws IOException {
+ logger.finest("Source image format: DXT");
+
+ ByteBuffer buffer = BufferUtils.createByteBuffer(totalSize);
+
+ int mipWidth = width;
+ int mipHeight = height;
+
+ for (int mip = 0; mip < mipMapCount; mip++) {
+ if (flip) {
+ byte[] data = new byte[sizes[mip]];
+ in.readFully(data);
+ ByteBuffer wrapped = ByteBuffer.wrap(data);
+ wrapped.rewind();
+ ByteBuffer flipped = DXTFlipper.flipDXT(wrapped, mipWidth, mipHeight, pixelFormat);
+ buffer.put(flipped);
+ } else {
+ byte[] data = new byte[sizes[mip]];
+ in.readFully(data);
+ buffer.put(data);
+ }
+
+ mipWidth = Math.max(mipWidth / 2, 1);
+ mipHeight = Math.max(mipHeight / 2, 1);
+ }
+ buffer.rewind();
+
+ return buffer;
+ }
+
+ /**
+ * Reads a grayscale image with mipmaps from the InputStream
+ * @param flip Flip the loaded image by Y axis
+ * @param totalSize Total size of the image in bytes including the mipmaps
+ * @return A ByteBuffer containing the grayscale image data with mips.
+ * @throws java.io.IOException If an error occured while reading from InputStream
+ */
+ public ByteBuffer readGrayscale3D(boolean flip, int totalSize) throws IOException {
+ ByteBuffer buffer = BufferUtils.createByteBuffer(totalSize * depth);
+
+ if (bpp == 8) {
+ logger.finest("Source image format: R8");
+ }
+
+ assert bpp == pixelFormat.getBitsPerPixel();
+
+
+ for (int i = 0; i < depth; i++) {
+ int mipWidth = width;
+ int mipHeight = height;
+
+ for (int mip = 0; mip < mipMapCount; mip++) {
+ byte[] data = new byte[sizes[mip]];
+ in.readFully(data);
+ if (flip) {
+ data = flipData(data, mipWidth * bpp / 8, mipHeight);
+ }
+ buffer.put(data);
+
+ mipWidth = Math.max(mipWidth / 2, 1);
+ mipHeight = Math.max(mipHeight / 2, 1);
+ }
+ }
+ buffer.rewind();
+ return buffer;
+ }
+
+ /**
+ * Reads an uncompressed RGB or RGBA image.
+ *
+ * @param flip Flip the image on the Y axis
+ * @param totalSize Size of the image in bytes including mipmaps
+ * @return ByteBuffer containing image data with mipmaps in the format specified by pixelFormat_
+ * @throws java.io.IOException If an error occured while reading from InputStream
+ */
+ public ByteBuffer readRGB3D(boolean flip, int totalSize) throws IOException {
+ int redCount = count(redMask),
+ blueCount = count(blueMask),
+ greenCount = count(greenMask),
+ alphaCount = count(alphaMask);
+
+ if (redMask == 0x00FF0000 && greenMask == 0x0000FF00 && blueMask == 0x000000FF) {
+ if (alphaMask == 0xFF000000 && bpp == 32) {
+ logger.finest("Data source format: BGRA8");
+ } else if (bpp == 24) {
+ logger.finest("Data source format: BGR8");
+ }
+ }
+
+ int sourcebytesPP = bpp / 8;
+ int targetBytesPP = pixelFormat.getBitsPerPixel() / 8;
+
+ ByteBuffer dataBuffer = BufferUtils.createByteBuffer(totalSize * depth);
+
+ for (int k = 0; k < depth; k++) {
+ // ByteBuffer dataBuffer = BufferUtils.createByteBuffer(totalSize);
+ int mipWidth = width;
+ int mipHeight = height;
+ int offset = k * totalSize;
+ byte[] b = new byte[sourcebytesPP];
+ for (int mip = 0; mip < mipMapCount; mip++) {
+ for (int y = 0; y < mipHeight; y++) {
+ for (int x = 0; x < mipWidth; x++) {
+ in.readFully(b);
+
+ int i = byte2int(b);
+
+ byte red = (byte) (((i & redMask) >> redCount));
+ byte green = (byte) (((i & greenMask) >> greenCount));
+ byte blue = (byte) (((i & blueMask) >> blueCount));
+ byte alpha = (byte) (((i & alphaMask) >> alphaCount));
+
+ if (flip) {
+ dataBuffer.position(offset + ((mipHeight - y - 1) * mipWidth + x) * targetBytesPP);
+ }
+ //else
+ // dataBuffer.position(offset + (y * width + x) * targetBytesPP);
+
+ if (alphaMask == 0) {
+ dataBuffer.put(red).put(green).put(blue);
+ } else {
+ dataBuffer.put(red).put(green).put(blue).put(alpha);
+ }
+ }
+ }
+
+ offset += (mipWidth * mipHeight * targetBytesPP);
+
+ mipWidth = Math.max(mipWidth / 2, 1);
+ mipHeight = Math.max(mipHeight / 2, 1);
+ }
+ }
+ dataBuffer.rewind();
+ return dataBuffer;
+ }
+
+ /**
+ * Reads a DXT compressed image from the InputStream
+ *
+ * @param totalSize Total size of the image in bytes, including mipmaps
+ * @return ByteBuffer containing compressed DXT image in the format specified by pixelFormat_
+ * @throws java.io.IOException If an error occured while reading from InputStream
+ */
+ public ByteBuffer readDXT3D(boolean flip, int totalSize) throws IOException {
+ logger.finest("Source image format: DXT");
+
+ ByteBuffer bufferAll = BufferUtils.createByteBuffer(totalSize * depth);
+
+ for (int i = 0; i < depth; i++) {
+ ByteBuffer buffer = BufferUtils.createByteBuffer(totalSize);
+ int mipWidth = width;
+ int mipHeight = height;
+ for (int mip = 0; mip < mipMapCount; mip++) {
+ if (flip) {
+ byte[] data = new byte[sizes[mip]];
+ in.readFully(data);
+ ByteBuffer wrapped = ByteBuffer.wrap(data);
+ wrapped.rewind();
+ ByteBuffer flipped = DXTFlipper.flipDXT(wrapped, mipWidth, mipHeight, pixelFormat);
+ flipped.rewind();
+ buffer.put(flipped);
+ } else {
+ byte[] data = new byte[sizes[mip]];
+ in.readFully(data);
+ buffer.put(data);
+ }
+
+ mipWidth = Math.max(mipWidth / 2, 1);
+ mipHeight = Math.max(mipHeight / 2, 1);
+ }
+ buffer.rewind();
+ bufferAll.put(buffer);
+ }
+
+ return bufferAll;
+ }
+
+ /**
+ * Reads the image data from the InputStream in the required format.
+ * If the file contains a cubemap image, it is loaded as 6 ByteBuffers
+ * (potentially containing mipmaps if they were specified), otherwise
+ * a single ByteBuffer is returned for a 2D image.
+ *
+ * @param flip Flip the image data or not.
+ * For cubemaps, each of the cubemap faces is flipped individually.
+ * If the image is DXT compressed, no flipping is done.
+ * @return An ArrayList containing a single ByteBuffer for a 2D image, or 6 ByteBuffers for a cubemap.
+ * The cubemap ByteBuffer order is PositiveX, NegativeX, PositiveY, NegativeY, PositiveZ, NegativeZ.
+ *
+ * @throws java.io.IOException If an error occured while reading from the stream.
+ */
+ public ArrayList<ByteBuffer> readData(boolean flip) throws IOException {
+ int totalSize = 0;
+
+ for (int i = 0; i < sizes.length; i++) {
+ totalSize += sizes[i];
+ }
+
+ ArrayList<ByteBuffer> allMaps = new ArrayList<ByteBuffer>();
+ if (depth > 1 && !texture3D) {
+ for (int i = 0; i < depth; i++) {
+ if (compressed) {
+ allMaps.add(readDXT2D(flip, totalSize));
+ } else if (grayscaleOrAlpha) {
+ allMaps.add(readGrayscale2D(flip, totalSize));
+ } else {
+ allMaps.add(readRGB2D(flip, totalSize));
+ }
+ }
+ } else if (texture3D) {
+ if (compressed) {
+ allMaps.add(readDXT3D(flip, totalSize));
+ } else if (grayscaleOrAlpha) {
+ allMaps.add(readGrayscale3D(flip, totalSize));
+ } else {
+ allMaps.add(readRGB3D(flip, totalSize));
+ }
+
+ } else {
+ if (compressed) {
+ allMaps.add(readDXT2D(flip, totalSize));
+ } else if (grayscaleOrAlpha) {
+ allMaps.add(readGrayscale2D(flip, totalSize));
+ } else {
+ allMaps.add(readRGB2D(flip, totalSize));
+ }
+ }
+
+ return allMaps;
+ }
+
+ /**
+ * Checks if flags contains the specified mask
+ */
+ private static boolean is(int flags, int mask) {
+ return (flags & mask) == mask;
+ }
+
+ /**
+ * Counts the amount of bits needed to shift till bitmask n is at zero
+ * @param n Bitmask to test
+ */
+ private static int count(int n) {
+ if (n == 0) {
+ return 0;
+ }
+
+ int i = 0;
+ while ((n & 0x1) == 0) {
+ n = n >> 1;
+ i++;
+ if (i > 32) {
+ throw new RuntimeException(Integer.toHexString(n));
+ }
+ }
+
+ return i;
+ }
+
+ /**
+ * Converts a 1 to 4 sized byte array to an integer
+ */
+ private static int byte2int(byte[] b) {
+ if (b.length == 1) {
+ return b[0] & 0xFF;
+ } else if (b.length == 2) {
+ return (b[0] & 0xFF) | ((b[1] & 0xFF) << 8);
+ } else if (b.length == 3) {
+ return (b[0] & 0xFF) | ((b[1] & 0xFF) << 8) | ((b[2] & 0xFF) << 16);
+ } else if (b.length == 4) {
+ return (b[0] & 0xFF) | ((b[1] & 0xFF) << 8) | ((b[2] & 0xFF) << 16) | ((b[3] & 0xFF) << 24);
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Converts a int representing a FourCC into a String
+ */
+ private static String string(int value) {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append((char) (value & 0xFF));
+ buf.append((char) ((value & 0xFF00) >> 8));
+ buf.append((char) ((value & 0xFF0000) >> 16));
+ buf.append((char) ((value & 0xFF00000) >> 24));
+
+ return buf.toString();
+ }
+}
diff --git a/engine/src/core-plugins/com/jme3/texture/plugins/DXTFlipper.java b/engine/src/core-plugins/com/jme3/texture/plugins/DXTFlipper.java
new file mode 100644
index 0000000..ff90f03
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/texture/plugins/DXTFlipper.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.texture.plugins;
+
+import com.jme3.math.FastMath;
+import com.jme3.texture.Image.Format;
+import com.jme3.util.BufferUtils;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * DXTFlipper is a utility class used to flip along Y axis DXT compressed textures.
+ *
+ * @author Kirill Vainer
+ */
+public class DXTFlipper {
+
+ private static final ByteBuffer bb = ByteBuffer.allocate(8);
+
+ static {
+ bb.order(ByteOrder.LITTLE_ENDIAN);
+ }
+
+ private static long readCode5(long data, int x, int y){
+ long shift = (4 * y + x) * 3;
+ long mask = 0x7;
+ mask <<= shift;
+ long code = data & mask;
+ code >>= shift;
+ return code;
+ }
+
+ private static long writeCode5(long data, int x, int y, long code){
+ long shift = (4 * y + x) * 3;
+ long mask = 0x7;
+ code = (code & mask) << shift;
+ mask <<= shift;
+ mask = ~mask;
+ data &= mask;
+ data |= code; // write new code
+ return data;
+ }
+
+ private static void flipDXT5Block(byte[] block, int h){
+ if (h == 1)
+ return;
+
+ byte c0 = block[0];
+ byte c1 = block[1];
+
+ bb.clear();
+ bb.put(block, 2, 6).flip();
+ bb.clear();
+ long l = bb.getLong();
+ long n = l;
+
+ if (h == 2){
+ n = writeCode5(n, 0, 0, readCode5(l, 0, 1));
+ n = writeCode5(n, 1, 0, readCode5(l, 1, 1));
+ n = writeCode5(n, 2, 0, readCode5(l, 2, 1));
+ n = writeCode5(n, 3, 0, readCode5(l, 3, 1));
+
+ n = writeCode5(n, 0, 1, readCode5(l, 0, 0));
+ n = writeCode5(n, 1, 1, readCode5(l, 1, 0));
+ n = writeCode5(n, 2, 1, readCode5(l, 2, 0));
+ n = writeCode5(n, 3, 1, readCode5(l, 3, 0));
+ }else{
+ n = writeCode5(n, 0, 0, readCode5(l, 0, 3));
+ n = writeCode5(n, 1, 0, readCode5(l, 1, 3));
+ n = writeCode5(n, 2, 0, readCode5(l, 2, 3));
+ n = writeCode5(n, 3, 0, readCode5(l, 3, 3));
+
+ n = writeCode5(n, 0, 1, readCode5(l, 0, 2));
+ n = writeCode5(n, 1, 1, readCode5(l, 1, 2));
+ n = writeCode5(n, 2, 1, readCode5(l, 2, 2));
+ n = writeCode5(n, 3, 1, readCode5(l, 3, 2));
+
+ n = writeCode5(n, 0, 2, readCode5(l, 0, 1));
+ n = writeCode5(n, 1, 2, readCode5(l, 1, 1));
+ n = writeCode5(n, 2, 2, readCode5(l, 2, 1));
+ n = writeCode5(n, 3, 2, readCode5(l, 3, 1));
+
+ n = writeCode5(n, 0, 3, readCode5(l, 0, 0));
+ n = writeCode5(n, 1, 3, readCode5(l, 1, 0));
+ n = writeCode5(n, 2, 3, readCode5(l, 2, 0));
+ n = writeCode5(n, 3, 3, readCode5(l, 3, 0));
+ }
+
+ bb.clear();
+ bb.putLong(n);
+ bb.clear();
+ bb.get(block, 2, 6).flip();
+
+ assert c0 == block[0] && c1 == block[1];
+ }
+
+ private static void flipDXT3Block(byte[] block, int h){
+ if (h == 1)
+ return;
+
+ // first row
+ byte tmp0 = block[0];
+ byte tmp1 = block[1];
+
+ if (h == 2){
+ block[0] = block[2];
+ block[1] = block[3];
+
+ block[2] = tmp0;
+ block[3] = tmp1;
+ }else{
+ // write last row to first row
+ block[0] = block[6];
+ block[1] = block[7];
+
+ // write first row to last row
+ block[6] = tmp0;
+ block[7] = tmp1;
+
+ // 2nd row
+ tmp0 = block[2];
+ tmp1 = block[3];
+
+ // write 3rd row to 2nd
+ block[2] = block[4];
+ block[3] = block[5];
+
+ // write 2nd row to 3rd
+ block[4] = tmp0;
+ block[5] = tmp1;
+ }
+ }
+
+ /**
+ * Flips a DXT color block or a DXT3 alpha block
+ * @param block
+ * @param h
+ */
+ private static void flipDXT1orDXTA3Block(byte[] block, int h){
+ byte tmp;
+ switch (h){
+ case 1:
+ return;
+ case 2:
+ // keep header intact (the two colors)
+ // header takes 4 bytes
+
+ // flip only two top rows
+ tmp = block[4+1];
+ block[4+1] = block[4+0];
+ block[4+0] = tmp;
+ return;
+ default:
+ // keep header intact (the two colors)
+ // header takes 4 bytes
+
+ // flip first & fourth row
+ tmp = block[4+3];
+ block[4+3] = block[4+0];
+ block[4+0] = tmp;
+
+ // flip second and third row
+ tmp = block[4+2];
+ block[4+2] = block[4+1];
+ block[4+1] = tmp;
+ return;
+ }
+ }
+
+ public static ByteBuffer flipDXT(ByteBuffer img, int w, int h, Format format){
+ int blocksX = (int) FastMath.ceil((float)w / 4f);
+ int blocksY = (int) FastMath.ceil((float)h / 4f);
+
+ int type;
+ switch (format){
+ case DXT1:
+ case DXT1A:
+ type = 1;
+ break;
+ case DXT3:
+ type = 2;
+ break;
+ case DXT5:
+ type = 3;
+ break;
+ case LATC:
+ type = 4;
+ break;
+ case LTC:
+ type = 5;
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+
+ // DXT1 uses 8 bytes per block,
+ // DXT3, DXT5, LATC use 16 bytes per block
+ int bpb = type == 1 || type == 5 ? 8 : 16;
+
+ ByteBuffer retImg = BufferUtils.createByteBuffer(blocksX * blocksY * bpb);
+
+ if (h == 1){
+ retImg.put(img);
+ retImg.rewind();
+ return retImg;
+ }else if (h == 2){
+ byte[] colorBlock = new byte[8];
+ byte[] alphaBlock = type != 1 && type != 5 ? new byte[8] : null;
+ for (int x = 0; x < blocksX; x++){
+ // prepeare for block reading
+ int blockByteOffset = x * bpb;
+ img.position(blockByteOffset);
+ img.limit(blockByteOffset + bpb);
+
+ img.get(colorBlock);
+ if (type == 4 || type == 5)
+ flipDXT5Block(colorBlock, h);
+ else
+ flipDXT1orDXTA3Block(colorBlock, h);
+
+ // write block (no need to flip block indexes, only pixels
+ // inside block
+ retImg.put(colorBlock);
+
+ if (alphaBlock != null){
+ img.get(alphaBlock);
+ switch (type){
+ case 2:
+ flipDXT3Block(alphaBlock, h); break;
+ case 3:
+ case 4:
+ flipDXT5Block(alphaBlock, h);
+ break;
+ }
+ retImg.put(alphaBlock);
+ }
+ }
+ retImg.rewind();
+ return retImg;
+ }else if (h >= 4){
+ byte[] colorBlock = new byte[8];
+ byte[] alphaBlock = type != 1 && type != 5 ? new byte[8] : null;
+ for (int y = 0; y < blocksY; y++){
+ for (int x = 0; x < blocksX; x++){
+ // prepeare for block reading
+ int blockIdx = y * blocksX + x;
+ int blockByteOffset = blockIdx * bpb;
+
+ img.position(blockByteOffset);
+ img.limit(blockByteOffset + bpb);
+
+ blockIdx = (blocksY - y - 1) * blocksX + x;
+ blockByteOffset = blockIdx * bpb;
+
+ retImg.position(blockByteOffset);
+ retImg.limit(blockByteOffset + bpb);
+
+ if (alphaBlock != null){
+ img.get(alphaBlock);
+ switch (type){
+ case 2:
+ flipDXT3Block(alphaBlock, h);
+ break;
+ case 3:
+ case 4:
+ flipDXT5Block(alphaBlock, h);
+ break;
+ }
+ retImg.put(alphaBlock);
+ }
+
+ img.get(colorBlock);
+ if (type == 4 || type == 5)
+ flipDXT5Block(colorBlock, h);
+ else
+ flipDXT1orDXTA3Block(colorBlock, h);
+
+ retImg.put(colorBlock);
+ }
+ }
+ retImg.limit(retImg.capacity());
+ retImg.position(0);
+ return retImg;
+ }else{
+ return null;
+ }
+ }
+
+}
diff --git a/engine/src/core-plugins/com/jme3/texture/plugins/HDRLoader.java b/engine/src/core-plugins/com/jme3/texture/plugins/HDRLoader.java
new file mode 100644
index 0000000..4758ecf
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/texture/plugins/HDRLoader.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.texture.plugins;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetLoader;
+import com.jme3.asset.TextureKey;
+import com.jme3.math.FastMath;
+import com.jme3.texture.Image;
+import com.jme3.texture.Image.Format;
+import com.jme3.util.BufferUtils;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class HDRLoader implements AssetLoader {
+
+ private static final Logger logger = Logger.getLogger(HDRLoader.class.getName());
+
+ private boolean writeRGBE = false;
+ private ByteBuffer rleTempBuffer;
+ private ByteBuffer dataStore;
+ private final float[] tempF = new float[3];
+
+ public HDRLoader(boolean writeRGBE){
+ this.writeRGBE = writeRGBE;
+ }
+
+ public HDRLoader(){
+ }
+
+ public static void convertFloatToRGBE(byte[] rgbe, float red, float green, float blue){
+ double max = red;
+ if (green > max) max = green;
+ if (blue > max) max = blue;
+ if (max < 1.0e-32){
+ rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0;
+ }else{
+ double exp = Math.ceil( Math.log10(max) / Math.log10(2) );
+ double divider = Math.pow(2.0, exp);
+ rgbe[0] = (byte) ((red / divider) * 255.0);
+ rgbe[1] = (byte) ((green / divider) * 255.0);
+ rgbe[2] = (byte) ((blue / divider) * 255.0);
+ rgbe[3] = (byte) (exp + 128.0);
+ }
+ }
+
+ public static void convertRGBEtoFloat(byte[] rgbe, float[] rgbf){
+ int R = rgbe[0] & 0xFF,
+ G = rgbe[1] & 0xFF,
+ B = rgbe[2] & 0xFF,
+ E = rgbe[3] & 0xFF;
+
+ float e = (float) Math.pow(2f, E - (128 + 8) );
+ rgbf[0] = R * e;
+ rgbf[1] = G * e;
+ rgbf[2] = B * e;
+ }
+
+ public static void convertRGBEtoFloat2(byte[] rgbe, float[] rgbf){
+ int R = rgbe[0] & 0xFF,
+ G = rgbe[1] & 0xFF,
+ B = rgbe[2] & 0xFF,
+ E = rgbe[3] & 0xFF;
+
+ float e = (float) Math.pow(2f, E - 128);
+ rgbf[0] = (R / 256.0f) * e;
+ rgbf[1] = (G / 256.0f) * e;
+ rgbf[2] = (B / 256.0f) * e;
+ }
+
+ public static void convertRGBEtoFloat3(byte[] rgbe, float[] rgbf){
+ int R = rgbe[0] & 0xFF,
+ G = rgbe[1] & 0xFF,
+ B = rgbe[2] & 0xFF,
+ E = rgbe[3] & 0xFF;
+
+ float e = (float) Math.pow(2f, E - (128 + 8) );
+ rgbf[0] = R * e;
+ rgbf[1] = G * e;
+ rgbf[2] = B * e;
+ }
+
+ private short flip(int in){
+ return (short) ((in << 8 & 0xFF00) | (in >> 8));
+ }
+
+ private void writeRGBE(byte[] rgbe){
+ if (writeRGBE){
+ dataStore.put(rgbe);
+ }else{
+ convertRGBEtoFloat(rgbe, tempF);
+ dataStore.putShort(FastMath.convertFloatToHalf(tempF[0]))
+ .putShort(FastMath.convertFloatToHalf(tempF[1])).
+ putShort(FastMath.convertFloatToHalf(tempF[2]));
+ }
+ }
+
+ private String readString(InputStream is) throws IOException{
+ StringBuilder sb = new StringBuilder();
+ while (true){
+ int i = is.read();
+ if (i == 0x0a || i == -1) // new line or EOF
+ return sb.toString();
+
+ sb.append((char)i);
+ }
+ }
+
+ private boolean decodeScanlineRLE(InputStream in, int width) throws IOException{
+ // must deocde RLE data into temp buffer before converting to float
+ if (rleTempBuffer == null){
+ rleTempBuffer = BufferUtils.createByteBuffer(width * 4);
+ }else{
+ rleTempBuffer.clear();
+ if (rleTempBuffer.remaining() < width * 4)
+ rleTempBuffer = BufferUtils.createByteBuffer(width * 4);
+ }
+
+ // read each component seperately
+ for (int i = 0; i < 4; i++) {
+ // read WIDTH bytes for the channel
+ for (int j = 0; j < width;) {
+ int code = in.read();
+ if (code > 128) { // run
+ code -= 128;
+ int val = in.read();
+ while ((code--) != 0) {
+ rleTempBuffer.put( (j++) * 4 + i , (byte)val);
+ //scanline[j++][i] = val;
+ }
+ } else { // non-run
+ while ((code--) != 0) {
+ int val = in.read();
+ rleTempBuffer.put( (j++) * 4 + i, (byte)val);
+ //scanline[j++][i] = in.read();
+ }
+ }
+ }
+ }
+
+ rleTempBuffer.rewind();
+ byte[] rgbe = new byte[4];
+// float[] temp = new float[3];
+
+ // decode temp buffer into float data
+ for (int i = 0; i < width; i++){
+ rleTempBuffer.get(rgbe);
+ writeRGBE(rgbe);
+ }
+
+ return true;
+ }
+
+ private boolean decodeScanlineUncompressed(InputStream in, int width) throws IOException{
+ byte[] rgbe = new byte[4];
+
+ for (int i = 0; i < width; i+=3){
+ if (in.read(rgbe) < 1)
+ return false;
+
+ writeRGBE(rgbe);
+ }
+ return true;
+ }
+
+ private void decodeScanline(InputStream in, int width) throws IOException{
+ if (width < 8 || width > 0x7fff){
+ // too short/long for RLE compression
+ decodeScanlineUncompressed(in, width);
+ }
+
+ // check format
+ byte[] data = new byte[4];
+ in.read(data);
+ if (data[0] != 0x02 || data[1] != 0x02 || (data[2] & 0x80) != 0){
+ // not RLE data
+ decodeScanlineUncompressed(in, width-1);
+ }else{
+ // check scanline width
+ int readWidth = (data[2] & 0xFF) << 8 | (data[3] & 0xFF);
+ if (readWidth != width)
+ throw new IOException("Illegal scanline width in HDR file: "+width+" != "+readWidth);
+
+ // RLE data
+ decodeScanlineRLE(in, width);
+ }
+ }
+
+ public Image load(InputStream in, boolean flipY) throws IOException{
+ float gamma = -1f;
+ float exposure = -1f;
+ float[] colorcorr = new float[]{ -1f, -1f, -1f };
+
+ int width = -1, height = -1;
+ boolean verifiedFormat = false;
+
+ while (true){
+ String ln = readString(in);
+ ln = ln.trim();
+ if (ln.startsWith("#") || ln.equals("")){
+ if (ln.equals("#?RADIANCE") || ln.equals("#?RGBE"))
+ verifiedFormat = true;
+
+ continue; // comment or empty statement
+ } else if (ln.startsWith("+") || ln.startsWith("-")){
+ // + or - mark image resolution and start of data
+ String[] resData = ln.split("\\s");
+ if (resData.length != 4){
+ throw new IOException("Invalid resolution string in HDR file");
+ }
+
+ if (!resData[0].equals("-Y") || !resData[2].equals("+X")){
+ logger.warning("Flipping/Rotating attributes ignored!");
+ }
+
+ //if (resData[0].endsWith("X")){
+ // first width then height
+ // width = Integer.parseInt(resData[1]);
+ // height = Integer.parseInt(resData[3]);
+ //}else{
+ width = Integer.parseInt(resData[3]);
+ height = Integer.parseInt(resData[1]);
+ //}
+
+ break;
+ } else {
+ // regular command
+ int index = ln.indexOf("=");
+ if (index < 1){
+ logger.log(Level.FINE, "Ignored string: {0}", ln);
+ continue;
+ }
+
+ String var = ln.substring(0, index).trim().toLowerCase();
+ String value = ln.substring(index+1).trim().toLowerCase();
+ if (var.equals("format")){
+ if (!value.equals("32-bit_rle_rgbe") && !value.equals("32-bit_rle_xyze")){
+ throw new IOException("Unsupported format in HDR picture");
+ }
+ }else if (var.equals("exposure")){
+ exposure = Float.parseFloat(value);
+ }else if (var.equals("gamma")){
+ gamma = Float.parseFloat(value);
+ }else{
+ logger.log(Level.WARNING, "HDR Command ignored: {0}", ln);
+ }
+ }
+ }
+
+ assert width != -1 && height != -1;
+
+ if (!verifiedFormat)
+ logger.warning("Unsure if specified image is Radiance HDR");
+
+ // some HDR images can get pretty big
+ System.gc();
+
+ // each pixel times size of component times # of components
+ Format pixelFormat;
+ if (writeRGBE){
+ pixelFormat = Format.RGBA8;
+ }else{
+ pixelFormat = Format.RGB16F;
+ }
+
+ dataStore = BufferUtils.createByteBuffer(width * height * pixelFormat.getBitsPerPixel());
+
+ int bytesPerPixel = pixelFormat.getBitsPerPixel() / 8;
+ int scanLineBytes = bytesPerPixel * width;
+ for (int y = height - 1; y >= 0; y--) {
+ if (flipY)
+ dataStore.position(scanLineBytes * y);
+
+ decodeScanline(in, width);
+ }
+ in.close();
+
+ dataStore.rewind();
+ return new Image(pixelFormat, width, height, dataStore);
+ }
+
+ public Object load(AssetInfo info) throws IOException {
+ if (!(info.getKey() instanceof TextureKey))
+ throw new IllegalArgumentException("Texture assets must be loaded using a TextureKey");
+
+ boolean flip = ((TextureKey) info.getKey()).isFlipY();
+ InputStream in = null;
+ try {
+ in = info.openStream();
+ Image img = load(in, flip);
+ return img;
+ } finally {
+ if (in != null){
+ in.close();
+ }
+ }
+ }
+
+}
diff --git a/engine/src/core-plugins/com/jme3/texture/plugins/ImageFlipper.java b/engine/src/core-plugins/com/jme3/texture/plugins/ImageFlipper.java
new file mode 100644
index 0000000..53978e9
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/texture/plugins/ImageFlipper.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.texture.plugins;
+
+import com.jme3.texture.Image;
+import com.jme3.util.BufferUtils;
+import java.nio.ByteBuffer;
+
+/**
+ * ImageFlipper is a utility class used to flip images across the Y axis.
+ * Due to the standard of where the image origin is between OpenGL and
+ * other software, this class is required.
+ *
+ * @author Kirill Vainer
+ */
+public class ImageFlipper {
+
+ public static void flipImage(Image img, int index){
+ if (img.getFormat().isCompressed())
+ throw new UnsupportedOperationException("Flipping compressed " +
+ "images is unsupported.");
+
+ int w = img.getWidth();
+ int h = img.getHeight();
+ int halfH = h / 2;
+
+ // bytes per pixel
+ int bpp = img.getFormat().getBitsPerPixel() / 8;
+ int scanline = w * bpp;
+
+ ByteBuffer data = img.getData(index);
+ ByteBuffer temp = BufferUtils.createByteBuffer(scanline);
+
+ data.rewind();
+ for (int y = 0; y < halfH; y++){
+ int oppY = h - y - 1;
+ // read in scanline
+ data.position(y * scanline);
+ data.limit(data.position() + scanline);
+
+ temp.rewind();
+ temp.put(data);
+
+ }
+ }
+
+}
diff --git a/engine/src/core-plugins/com/jme3/texture/plugins/PFMLoader.java b/engine/src/core-plugins/com/jme3/texture/plugins/PFMLoader.java
new file mode 100644
index 0000000..082dc83
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/texture/plugins/PFMLoader.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.texture.plugins;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetLoader;
+import com.jme3.asset.TextureKey;
+import com.jme3.texture.Image;
+import com.jme3.texture.Image.Format;
+import com.jme3.util.BufferUtils;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.logging.Logger;
+
+public class PFMLoader implements AssetLoader {
+
+ private static final Logger logger = Logger.getLogger(PFMLoader.class.getName());
+
+ private String readString(InputStream is) throws IOException{
+ StringBuilder sb = new StringBuilder();
+ while (true){
+ int i = is.read();
+ if (i == 0x0a || i == -1) // new line or EOF
+ return sb.toString();
+
+ sb.append((char)i);
+ }
+ }
+
+ private void flipScanline(byte[] scanline){
+ for (int i = 0; i < scanline.length; i+=4){
+ // flip first and fourth bytes
+ byte tmp = scanline[i+3];
+ scanline[i+3] = scanline[i+0];
+ scanline[i+0] = tmp;
+
+ // flip second and third bytes
+ tmp = scanline[i+2];
+ scanline[i+2] = scanline[i+1];
+ scanline[i+1] = tmp;
+ }
+ }
+
+ private Image load(InputStream in, boolean needYFlip) throws IOException{
+ Format format = null;
+
+ String fmtStr = readString(in);
+ if (fmtStr.equals("PF")){
+ format = Format.RGB32F;
+ }else if (fmtStr.equals("Pf")){
+ format = Format.Luminance32F;
+ }else{
+ throw new IOException("File is not PFM format");
+ }
+
+ String sizeStr = readString(in);
+ int spaceIdx = sizeStr.indexOf(" ");
+ if (spaceIdx <= 0 || spaceIdx >= sizeStr.length() - 1)
+ throw new IOException("Invalid size syntax in PFM file");
+
+ int width = Integer.parseInt(sizeStr.substring(0,spaceIdx));
+ int height = Integer.parseInt(sizeStr.substring(spaceIdx+1));
+
+ if (width <= 0 || height <= 0)
+ throw new IOException("Invalid size specified in PFM file");
+
+ String scaleStr = readString(in);
+ float scale = Float.parseFloat(scaleStr);
+ ByteOrder order = scale < 0 ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN;
+ boolean needEndienFlip = order != ByteOrder.nativeOrder();
+
+ // make sure all unneccessary stuff gets deleted from heap
+ // before allocating large amount of memory
+ System.gc();
+
+ int bytesPerPixel = format.getBitsPerPixel() / 8;
+ int scanLineBytes = bytesPerPixel * width;
+
+ ByteBuffer imageData = BufferUtils.createByteBuffer(width * height * bytesPerPixel);
+ byte[] scanline = new byte[width * bytesPerPixel];
+
+ for (int y = height - 1; y >= 0; y--) {
+ if (!needYFlip)
+ imageData.position(scanLineBytes * y);
+
+ int read = 0;
+ int off = 0;
+ do {
+ read = in.read(scanline, off, scanline.length - off);
+ off += read;
+ } while (read > 0);
+
+ if (needEndienFlip){
+ flipScanline(scanline);
+ }
+
+ imageData.put(scanline);
+ }
+ imageData.rewind();
+
+ return new Image(format, width, height, imageData);
+ }
+
+ public Object load(AssetInfo info) throws IOException {
+ if (!(info.getKey() instanceof TextureKey))
+ throw new IllegalArgumentException("Texture assets must be loaded using a TextureKey");
+
+ InputStream in = null;
+ try {
+ in = info.openStream();
+ return load(in, ((TextureKey)info.getKey()).isFlipY());
+ } finally {
+ if (in != null){
+ in.close();
+ }
+ }
+
+ }
+
+}
diff --git a/engine/src/core-plugins/com/jme3/texture/plugins/TGALoader.java b/engine/src/core-plugins/com/jme3/texture/plugins/TGALoader.java
new file mode 100644
index 0000000..bbd51d6
--- /dev/null
+++ b/engine/src/core-plugins/com/jme3/texture/plugins/TGALoader.java
@@ -0,0 +1,517 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.texture.plugins;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetLoader;
+import com.jme3.asset.TextureKey;
+import com.jme3.math.FastMath;
+import com.jme3.texture.Image;
+import com.jme3.texture.Image.Format;
+import com.jme3.util.BufferUtils;
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * <code>TextureManager</code> provides static methods for building a
+ * <code>Texture</code> object. Typically, the information supplied is the
+ * filename and the texture properties.
+ *
+ * @author Mark Powell
+ * @author Joshua Slack - cleaned, commented, added ability to read 16bit true color and color-mapped TGAs.
+ * @author Kirill Vainer - ported to jME3
+ * @version $Id: TGALoader.java 4131 2009-03-19 20:15:28Z blaine.dev $
+ */
+public final class TGALoader implements AssetLoader {
+
+ // 0 - no image data in file
+ public static final int TYPE_NO_IMAGE = 0;
+
+ // 1 - uncompressed, color-mapped image
+ public static final int TYPE_COLORMAPPED = 1;
+
+ // 2 - uncompressed, true-color image
+ public static final int TYPE_TRUECOLOR = 2;
+
+ // 3 - uncompressed, black and white image
+ public static final int TYPE_BLACKANDWHITE = 3;
+
+ // 9 - run-length encoded, color-mapped image
+ public static final int TYPE_COLORMAPPED_RLE = 9;
+
+ // 10 - run-length encoded, true-color image
+ public static final int TYPE_TRUECOLOR_RLE = 10;
+
+ // 11 - run-length encoded, black and white image
+ public static final int TYPE_BLACKANDWHITE_RLE = 11;
+
+ public Object load(AssetInfo info) throws IOException{
+ if (!(info.getKey() instanceof TextureKey))
+ throw new IllegalArgumentException("Texture assets must be loaded using a TextureKey");
+
+ boolean flip = ((TextureKey)info.getKey()).isFlipY();
+ InputStream in = null;
+ try {
+ in = info.openStream();
+ Image img = load(in, flip);
+ return img;
+ } finally {
+ if (in != null){
+ in.close();
+ }
+ }
+ }
+
+ /**
+ * <code>loadImage</code> is a manual image loader which is entirely
+ * independent of AWT. OUT: RGB888 or RGBA8888 Image object
+ *
+ * @return <code>Image</code> object that contains the
+ * image, either as a RGB888 or RGBA8888
+ * @param flip
+ * Flip the image vertically
+ * @param exp32
+ * Add a dummy Alpha channel to 24b RGB image.
+ * @param fis
+ * InputStream of an uncompressed 24b RGB or 32b RGBA TGA
+ * @throws java.io.IOException
+ */
+ public static Image load(InputStream in, boolean flip) throws IOException {
+ boolean flipH = false;
+
+ // open a stream to the file
+ DataInputStream dis = new DataInputStream(new BufferedInputStream(in));
+
+ // ---------- Start Reading the TGA header ---------- //
+ // length of the image id (1 byte)
+ int idLength = dis.readUnsignedByte();
+
+ // Type of color map (if any) included with the image
+ // 0 - no color map data is included
+ // 1 - a color map is included
+ int colorMapType = dis.readUnsignedByte();
+
+ // Type of image being read:
+ int imageType = dis.readUnsignedByte();
+
+ // Read Color Map Specification (5 bytes)
+ // Index of first color map entry (if we want to use it, uncomment and remove extra read.)
+// short cMapStart = flipEndian(dis.readShort());
+ dis.readShort();
+ // number of entries in the color map
+ short cMapLength = flipEndian(dis.readShort());
+ // number of bits per color map entry
+ int cMapDepth = dis.readUnsignedByte();
+
+ // Read Image Specification (10 bytes)
+ // horizontal coordinate of lower left corner of image. (if we want to use it, uncomment and remove extra read.)
+// int xOffset = flipEndian(dis.readShort());
+ dis.readShort();
+ // vertical coordinate of lower left corner of image. (if we want to use it, uncomment and remove extra read.)
+// int yOffset = flipEndian(dis.readShort());
+ dis.readShort();
+ // width of image - in pixels
+ int width = flipEndian(dis.readShort());
+ // height of image - in pixels
+ int height = flipEndian(dis.readShort());
+ // bits per pixel in image.
+ int pixelDepth = dis.readUnsignedByte();
+ int imageDescriptor = dis.readUnsignedByte();
+ if ((imageDescriptor & 32) != 0) // bit 5 : if 1, flip top/bottom ordering
+ flip = !flip;
+ if ((imageDescriptor & 16) != 0) // bit 4 : if 1, flip left/right ordering
+ flipH = !flipH;
+
+ // ---------- Done Reading the TGA header ---------- //
+
+ // Skip image ID
+ if (idLength > 0)
+ in.skip(idLength);
+
+ ColorMapEntry[] cMapEntries = null;
+ if (colorMapType != 0) {
+ // read the color map.
+ int bytesInColorMap = (cMapDepth * cMapLength) >> 3;
+ int bitsPerColor = Math.min(cMapDepth/3 , 8);
+
+ byte[] cMapData = new byte[bytesInColorMap];
+ in.read(cMapData);
+
+ // Only go to the trouble of constructing the color map
+ // table if this is declared a color mapped image.
+ if (imageType == TYPE_COLORMAPPED || imageType == TYPE_COLORMAPPED_RLE) {
+ cMapEntries = new ColorMapEntry[cMapLength];
+ int alphaSize = cMapDepth - (3*bitsPerColor);
+ float scalar = 255f / (FastMath.pow(2, bitsPerColor) - 1);
+ float alphaScalar = 255f / (FastMath.pow(2, alphaSize) - 1);
+ for (int i = 0; i < cMapLength; i++) {
+ ColorMapEntry entry = new ColorMapEntry();
+ int offset = cMapDepth * i;
+ entry.red = (byte)(int)(getBitsAsByte(cMapData, offset, bitsPerColor) * scalar);
+ entry.green = (byte)(int)(getBitsAsByte(cMapData, offset+bitsPerColor, bitsPerColor) * scalar);
+ entry.blue = (byte)(int)(getBitsAsByte(cMapData, offset+(2*bitsPerColor), bitsPerColor) * scalar);
+ if (alphaSize <= 0)
+ entry.alpha = (byte)255;
+ else
+ entry.alpha = (byte)(int)(getBitsAsByte(cMapData, offset+(3*bitsPerColor), alphaSize) * alphaScalar);
+
+ cMapEntries[i] = entry;
+ }
+ }
+ }
+
+
+ // Allocate image data array
+ Format format;
+ byte[] rawData = null;
+ int dl;
+ if (pixelDepth == 32) {
+ rawData = new byte[width * height * 4];
+ dl = 4;
+ } else {
+ rawData = new byte[width * height * 3];
+ dl = 3;
+ }
+ int rawDataIndex = 0;
+
+ if (imageType == TYPE_TRUECOLOR) {
+ byte red = 0;
+ byte green = 0;
+ byte blue = 0;
+ byte alpha = 0;
+
+ // Faster than doing a 16-or-24-or-32 check on each individual pixel,
+ // just make a seperate loop for each.
+ if (pixelDepth == 16) {
+ byte[] data = new byte[2];
+ float scalar = 255f/31f;
+ for (int i = 0; i <= (height - 1); i++) {
+ if (!flip)
+ rawDataIndex = (height - 1 - i) * width * dl;
+ for (int j = 0; j < width; j++) {
+ data[1] = dis.readByte();
+ data[0] = dis.readByte();
+ rawData[rawDataIndex++] = (byte)(int)(getBitsAsByte(data, 1, 5) * scalar);
+ rawData[rawDataIndex++] = (byte)(int)(getBitsAsByte(data, 6, 5) * scalar);
+ rawData[rawDataIndex++] = (byte)(int)(getBitsAsByte(data, 11, 5) * scalar);
+ if (dl == 4) {
+ // create an alpha channel
+ alpha = getBitsAsByte(data, 0, 1);
+ if (alpha == 1) alpha = (byte)255;
+ rawData[rawDataIndex++] = alpha;
+ }
+ }
+ }
+
+ format = dl == 4 ? Format.RGBA8 : Format.RGB8;
+ } else if (pixelDepth == 24){
+ for (int y = 0; y < height; y++) {
+ if (!flip)
+ rawDataIndex = (height - 1 - y) * width * dl;
+ else
+ rawDataIndex = y * width * dl;
+
+ dis.readFully(rawData, rawDataIndex, width * dl);
+// for (int x = 0; x < width; x++) {
+ //read scanline
+// blue = dis.readByte();
+// green = dis.readByte();
+// red = dis.readByte();
+// rawData[rawDataIndex++] = red;
+// rawData[rawDataIndex++] = green;
+// rawData[rawDataIndex++] = blue;
+// }
+ }
+ format = Format.BGR8;
+ } else if (pixelDepth == 32){
+ for (int i = 0; i <= (height - 1); i++) {
+ if (!flip)
+ rawDataIndex = (height - 1 - i) * width * dl;
+
+ for (int j = 0; j < width; j++) {
+ blue = dis.readByte();
+ green = dis.readByte();
+ red = dis.readByte();
+ alpha = dis.readByte();
+ rawData[rawDataIndex++] = red;
+ rawData[rawDataIndex++] = green;
+ rawData[rawDataIndex++] = blue;
+ rawData[rawDataIndex++] = alpha;
+ }
+ }
+ format = Format.RGBA8;
+ }else{
+ throw new IOException("Unsupported TGA true color depth: "+pixelDepth);
+ }
+ } else if( imageType == TYPE_TRUECOLOR_RLE ) {
+ byte red = 0;
+ byte green = 0;
+ byte blue = 0;
+ byte alpha = 0;
+ // Faster than doing a 16-or-24-or-32 check on each individual pixel,
+ // just make a seperate loop for each.
+ if( pixelDepth == 32 ){
+ for( int i = 0; i <= ( height - 1 ); ++i ){
+ if( !flip ){
+ rawDataIndex = ( height - 1 - i ) * width * dl;
+ }
+
+ for( int j = 0; j < width; ++j ){
+ // Get the number of pixels the next chunk covers (either packed or unpacked)
+ int count = dis.readByte();
+ if( ( count & 0x80 ) != 0 ){
+ // Its an RLE packed block - use the following 1 pixel for the next <count> pixels
+ count &= 0x07f;
+ j += count;
+ blue = dis.readByte();
+ green = dis.readByte();
+ red = dis.readByte();
+ alpha = dis.readByte();
+ while( count-- >= 0 ){
+ rawData[rawDataIndex++] = red;
+ rawData[rawDataIndex++] = green;
+ rawData[rawDataIndex++] = blue;
+ rawData[rawDataIndex++] = alpha;
+ }
+ } else{
+ // Its not RLE packed, but the next <count> pixels are raw.
+ j += count;
+ while( count-- >= 0 ){
+ blue = dis.readByte();
+ green = dis.readByte();
+ red = dis.readByte();
+ alpha = dis.readByte();
+ rawData[rawDataIndex++] = red;
+ rawData[rawDataIndex++] = green;
+ rawData[rawDataIndex++] = blue;
+ rawData[rawDataIndex++] = alpha;
+ }
+ }
+ }
+ }
+ format = Format.RGBA8;
+ } else if( pixelDepth == 24 ){
+ for( int i = 0; i <= ( height - 1 ); i++ ){
+ if( !flip ){
+ rawDataIndex = ( height - 1 - i ) * width * dl;
+ }
+ for( int j = 0; j < width; ++j ){
+ // Get the number of pixels the next chunk covers (either packed or unpacked)
+ int count = dis.readByte();
+ if( ( count & 0x80 ) != 0 ){
+ // Its an RLE packed block - use the following 1 pixel for the next <count> pixels
+ count &= 0x07f;
+ j += count;
+ blue = dis.readByte();
+ green = dis.readByte();
+ red = dis.readByte();
+ while( count-- >= 0 ){
+ rawData[rawDataIndex++] = red;
+ rawData[rawDataIndex++] = green;
+ rawData[rawDataIndex++] = blue;
+ }
+ } else{
+ // Its not RLE packed, but the next <count> pixels are raw.
+ j += count;
+ while( count-- >= 0 ){
+ blue = dis.readByte();
+ green = dis.readByte();
+ red = dis.readByte();
+ rawData[rawDataIndex++] = red;
+ rawData[rawDataIndex++] = green;
+ rawData[rawDataIndex++] = blue;
+ }
+ }
+ }
+ }
+ format = Format.RGB8;
+ } else if( pixelDepth == 16 ){
+ byte[] data = new byte[ 2 ];
+ float scalar = 255f / 31f;
+ for( int i = 0; i <= ( height - 1 ); i++ ){
+ if( !flip ){
+ rawDataIndex = ( height - 1 - i ) * width * dl;
+ }
+ for( int j = 0; j < width; j++ ){
+ // Get the number of pixels the next chunk covers (either packed or unpacked)
+ int count = dis.readByte();
+ if( ( count & 0x80 ) != 0 ){
+ // Its an RLE packed block - use the following 1 pixel for the next <count> pixels
+ count &= 0x07f;
+ j += count;
+ data[1] = dis.readByte();
+ data[0] = dis.readByte();
+ blue = (byte) (int) ( getBitsAsByte( data, 1, 5 ) * scalar );
+ green = (byte) (int) ( getBitsAsByte( data, 6, 5 ) * scalar );
+ red = (byte) (int) ( getBitsAsByte( data, 11, 5 ) * scalar );
+ while( count-- >= 0 ){
+ rawData[rawDataIndex++] = red;
+ rawData[rawDataIndex++] = green;
+ rawData[rawDataIndex++] = blue;
+ }
+ } else{
+ // Its not RLE packed, but the next <count> pixels are raw.
+ j += count;
+ while( count-- >= 0 ){
+ data[1] = dis.readByte();
+ data[0] = dis.readByte();
+ blue = (byte) (int) ( getBitsAsByte( data, 1, 5 ) * scalar );
+ green = (byte) (int) ( getBitsAsByte( data, 6, 5 ) * scalar );
+ red = (byte) (int) ( getBitsAsByte( data, 11, 5 ) * scalar );
+ rawData[rawDataIndex++] = red;
+ rawData[rawDataIndex++] = green;
+ rawData[rawDataIndex++] = blue;
+ }
+ }
+ }
+ }
+ format = Format.RGB8;
+ } else{
+ throw new IOException( "Unsupported TGA true color depth: " + pixelDepth );
+ }
+
+ } else if( imageType == TYPE_COLORMAPPED ){
+ int bytesPerIndex = pixelDepth / 8;
+
+ if (bytesPerIndex == 1) {
+ for (int i = 0; i <= (height - 1); i++) {
+ if (!flip)
+ rawDataIndex = (height - 1 - i) * width * dl;
+ for (int j = 0; j < width; j++) {
+ int index = dis.readUnsignedByte();
+ if (index >= cMapEntries.length || index < 0)
+ throw new IOException("TGA: Invalid color map entry referenced: "+index);
+
+ ColorMapEntry entry = cMapEntries[index];
+ rawData[rawDataIndex++] = entry.red;
+ rawData[rawDataIndex++] = entry.green;
+ rawData[rawDataIndex++] = entry.blue;
+ if (dl == 4) {
+ rawData[rawDataIndex++] = entry.alpha;
+ }
+
+ }
+ }
+ } else if (bytesPerIndex == 2) {
+ for (int i = 0; i <= (height - 1); i++) {
+ if (!flip)
+ rawDataIndex = (height - 1 - i) * width * dl;
+ for (int j = 0; j < width; j++) {
+ int index = flipEndian(dis.readShort());
+ if (index >= cMapEntries.length || index < 0)
+ throw new IOException("TGA: Invalid color map entry referenced: "+index);
+
+ ColorMapEntry entry = cMapEntries[index];
+ rawData[rawDataIndex++] = entry.red;
+ rawData[rawDataIndex++] = entry.green;
+ rawData[rawDataIndex++] = entry.blue;
+ if (dl == 4) {
+ rawData[rawDataIndex++] = entry.alpha;
+ }
+ }
+ }
+ } else {
+ throw new IOException("TGA: unknown colormap indexing size used: "+bytesPerIndex);
+ }
+
+ format = dl == 4 ? Format.RGBA8 : Format.RGB8;
+ } else {
+ throw new IOException("Grayscale TGA not supported");
+ }
+
+
+ in.close();
+ // Get a pointer to the image memory
+ ByteBuffer scratch = BufferUtils.createByteBuffer(rawData.length);
+ scratch.clear();
+ scratch.put(rawData);
+ scratch.rewind();
+ // Create the Image object
+ Image textureImage = new Image();
+ textureImage.setFormat(format);
+ textureImage.setWidth(width);
+ textureImage.setHeight(height);
+ textureImage.setData(scratch);
+ return textureImage;
+ }
+
+ private static byte getBitsAsByte(byte[] data, int offset, int length) {
+ int offsetBytes = offset / 8;
+ int indexBits = offset % 8;
+ int rVal = 0;
+
+ // start at data[offsetBytes]... spill into next byte as needed.
+ for (int i = length; --i >=0;) {
+ byte b = data[offsetBytes];
+ int test = indexBits == 7 ? 1 : 2 << (6-indexBits);
+ if ((b & test) != 0) {
+ if (i == 0)
+ rVal++;
+ else
+ rVal += (2 << i-1);
+ }
+ indexBits++;
+ if (indexBits == 8) {
+ indexBits = 0;
+ offsetBytes++;
+ }
+ }
+
+ return (byte)rVal;
+ }
+
+ /**
+ * <code>flipEndian</code> is used to flip the endian bit of the header
+ * file.
+ *
+ * @param signedShort
+ * the bit to flip.
+ * @return the flipped bit.
+ */
+ private static short flipEndian(short signedShort) {
+ int input = signedShort & 0xFFFF;
+ return (short) (input << 8 | (input & 0xFF00) >>> 8);
+ }
+
+ static class ColorMapEntry {
+ byte red, green, blue, alpha;
+
+ @Override
+ public String toString() {
+ return "entry: "+red+","+green+","+blue+","+alpha;
+ }
+ }
+}