Savable problem

Hello everyone,
I’ve implemented saving and loading for my game, but I’m getting this exception:
> java.lang.ClassCastException: com.jme3.export.binary.BinaryInputCapsule$ID cannot be cast to java.lang.Integer

	at com.jme3.export.binary.BinaryInputCapsule.readInt(BinaryInputCapsule.java:401)
	at de.rbgs.steel_builds.World.World.read(World.java:1012)
	at com.jme3.export.binary.BinaryImporter.readObject(BinaryImporter.java:342)
	at com.jme3.export.binary.BinaryImporter.load(BinaryImporter.java:242)
	at com.jme3.export.binary.BinaryImporter.load(BinaryImporter.java:125)
	at jme3tools.savegame.SaveGame.loadGame(SaveGame.java:184)
	at jme3tools.savegame.SaveGame.loadGame(SaveGame.java:128)
	at de.rbgs.steel_builds.Main.GameStates.GameState.init(GameState.java:187)
	at de.rbgs.steel_builds.Main.GameStates.GenericState.initialize(GenericState.java:67)
	at com.jme3.app.state.AppStateManager.initializePending(AppStateManager.java:251)
	at com.jme3.app.state.AppStateManager.update(AppStateManager.java:281)
	at com.jme3.app.SimpleApplication.update(SimpleApplication.java:236)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop(LwjglAbstractDisplay.java:151)
	at com.jme3.system.lwjgl.LwjglDisplay.runLoop(LwjglDisplay.java:193)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:232)
	at java.lang.Thread.run(Thread.java:745)

I’m sure this should work, here are the saving and loading methods of World.class:

    @Override
    public void write(JmeExporter ex) throws IOException
    {
        OutputCapsule out = ex.getCapsule(this);
        
        out.write(mapSize, "mapSize", -1);
        out.write(height, "height", -1);
        for(int i = 0; i < mapSize; i++)
        {
            for(int j = 0; j < mapSize; j++)
            {
                for(int k = 0; k < height; k++)
                {
                    out.write(terrain[i][j][k], "terrain" + i + j + k, null);
                }
            }
        }
        
        out.write(buildings.size(), "buildingsAmount", -1);
        for(int i = 0; i < buildings.size(); i++)
        {
            out.write(buildings.get(i), "building_" + i, null);
        }
        
        out.write(lastFreeBuildingId, "lastFreeBuildingId", -1);
    }

    @Override
    public void read(JmeImporter im) throws IOException
    {
        InputCapsule in = im.getCapsule(this);
        
        mapSize = in.readInt("mapSize", -1); //This is line 1012 which it's crashing on
        height = in.readInt("height", -1);
        
        terrain = new TerrainBlock[mapSize][mapSize][height];
        for(int i = 0; i < mapSize; i++)
        {
            for(int j = 0; j < mapSize; j++)
            {
                for(int k = 0; k < height; k++)
                {
                    terrain[i][j][k] = (TerrainBlock) in.readSavable("terrain" + i + j + k, null);
                }
            }
        }
        
        buildings = new ArrayList<WorldObject>();
        int buildingsAmount = in.readInt("buildingsAmount", -1);
        for(int i = 0; i < buildingsAmount; i++)
        {
            buildings.add((WorldObject) in.readSavable("building_" + i, null));
        }
        
        this.lastFreeBuildingId = in.readInt("lastFreeBuildingId", -1);
    }

And the mapSize variable is just a private int, so I simply don’t get why it crashes.
Any help would be appreciated.

What is this line?

What are the types of all of these fields?

…debugging through a soda straw is very difficult.

I had put a comment in the code.
And I said the field is an int. :stuck_out_tongue:

Bump?

I think maybe we need to see some of this class? I have never used this capsule stuff but I think there is a problem in here…

Does adding a constructor for the class with only super(); in it solve the problem?

The saving and loading methods with the problem line are right there…
And I already have a constructor with no arguments.

I highly doubt that the real problem is the line where the error is appearing. The import/export capsule system is very sensitive to write/read order - you must be reading in the exact data that you wrote out in the exact order that you wrote it. The code you’ve posted appears to be doing that, so the bug that’s causing this behaviour probably isn’t in the code you’ve posted. I’d suggest attaching a debugger and stepping through all the parts of your code that write to the exporter, and then stepping through all the parts that read from the importer. My guess is that you’ll find that your code writes some data during the save that it doesn’t properly read during the load.

I would rather use the Debugger for jmes importer Code, looking at the line 401 and such

The reason it happens is this:

Basically, don’t use more than 256 fields per class.

1 Like

Erm…
So what do I do?
I need to save the blocks in my world…

Save some other way maybe, without using JMEs built in? Alternatively, what about splitting the map into separate parts? Lastly I guess you could do what the comment said and extend it yourself, though I’d personally avoid this one :stuck_out_tongue:

Well,
I’d like to avoid making something myself…
But I don’t know about extending this either… I’ll have to think about it for a while.
I could try extending savable and changing that to using a copy-paste and then changed variant of the output capsule and have my world implement the class that extends savable and then see if the save/load system accepts that. :confused:

Why not write the array as a single field?

1 Like

So much this.

I’ve written a post about why writing the (sparse) world as a 3dimensional array is worse than just a List of those classes containing a Vector3f which you could later on use in conjunction with Dictionaries or even expand into your Class structure.

The reason is that you have a huge space/memory consumption because the empty space won’t really be left out (I am not sure whether jme just doesn’t serialize null variables though).

So that was actually to answer why there is no Savable for three-dimensional arrays but you can get away with serializing 2d arrays as long as you have less than 255 as world size. But again, a list/single array is probably the best if you have some empty blocks. :slight_smile:

In microbenchmarking, a flat one dimensional array and then calculating an index runs at about the same speed (often slightly better) than a full 3D array. If the index calculation is a private method (or inline) then it turns out that’s the same or cheaper than three separate index of out bounds checks. It’s also way easier to serialize, compress, etc… and it takes much less memory. (Each array dimension means there are array pointers.)

Here is an example I’m building into my mblock library:

/*
 * $Id$
 * 
 * Copyright (c) 2016, Simsilica, LLC
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions 
 * are met:
 * 
 * 1. Redistributions of source code must retain the above copyright 
 *    notice, this list of conditions and the following disclaimer.
 * 
 * 2. 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.
 * 
 * 3. Neither the name of the copyright holder 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 HOLDER 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.simsilica.mblock;

import java.util.Arrays;

import com.google.common.base.MoreObjects;

import com.simsilica.mathd.*;

/**
 *  A flat-array based implementation of the CellData interface.
 *
 *  @author    Paul Speed
 */
public class CellArray implements CellData {

    private final int xSize;
    private final int ySize;
    private final int zSize;
    private int[] array;
    
    public CellArray( int size ) {
        this(size, size, size);
    }
    
    public CellArray( int xSize, int ySize, int zSize ) {
        this.xSize = xSize;
        this.ySize = ySize;
        this.zSize = zSize;
        array = new int[xSize * ySize * zSize];
    }

    public final void clear() {
        Arrays.fill(array, 0);
    }
 
    public final int getSizeX() {
        return xSize;
    }

    public final int getSizeY() {
        return ySize;
    }
    
    public final int getSizeZ() {
        return zSize;
    }

    public Vec3i getSize() {
        return new Vec3i(xSize, ySize, zSize);
    }

    public final int[] getArray() {
        return array;
    }
    
    private int index( int x, int y, int z ) {
        // We store things as a sequence of y strips, basically.  This 
        // makes it better for run-length encoding and so on.
        return (x * ySize * zSize) + (z * ySize) + y;    
    }

    @Override
    public final int getCell( int x, int y, int z ) {
        return array[index(x, y, z)];
    }
 
    @Override
    public final int getCell( int x, int y, int z, int defaultValue ) {
        if( x < 0 || y < 0 || z < 0 ) {
            return defaultValue;
        }
        if( x >= xSize || y >= ySize || z >= zSize ) {
            return defaultValue;
        }
        return getCell(x, y, z); 
    }    
       
    @Override
    public final int getCell( int x, int y, int z, Direction dir, int defaultValue ) {
        Vec3i v = dir.getVec3i();
        return getCell(x + v.x, y + v.y, z + v.z, defaultValue); 
    }
    
    @Override
    public final void setCell( int x, int y, int z, int type ) {
        array[index(x, y, z)] = type;
    }
    
    @Override
    public String toString() {
        return MoreObjects.toStringHelper(getClass().getSimpleName())
                .add("xSize", xSize)
                .add("ySize", ySize)
                .add("zSize", zSize)
                .toString();
    }    
}

Yep a single list is the most efficient way. I do exactly that for my 2D tile array, just save the tile infos need to load reconstruct my level. In my case it is model, position/rotation and speed factor (swamp slower than on concrete floor).
In my case I use jackson