sourceafFantomMappy::MapLoader.fan


** Loads `MappyMap` instances from '.FMP' data streams. 
** See `MapViewer` for details on rendering a `MappyMap` to the screen.
@Js
class MapLoader {
    private static const Log log := MapLoader#.pod.log
    
    @NoDoc
    protected Str:Type          chunkLoaders    := [:]

    @NoDoc
    protected Int               mapSize

    
    
    // ---- Constructors ------------------------------------------------------

    internal new make() {
        // by having instances of MapLoaders they become self initialising and
        // the ChunkLoader instances (below) get automatically garbage collected
        registerChunkLoaders
    }



    // ---- Public Methods ----------------------------------------------------

    ** Create a `MappyMap` from the given input stream of '.FMP' data.
    ** 
    ** Note this method closes the input stream.
    static MappyMap loadMap(InStream fmpStream) {
        return MapLoader().doLoadMap(fmpStream)
    }



    // ---- Protected Methods -------------------------------------------------

    ** Performs the actual FMP map loading. 
    private MappyMap doLoadMap(InStream fmpStream) {
        log.debug("loadMap() - Starting...")
        log.info("Loading Map from InputStream")

        try {
            map := MappyMap()
    
            // load the map header
            chkMapHeader := createChunkLoader(fmpStream, Endian.big)
            if (chkMapHeader.chunkName != "FORM")
                throw FantomMappyErr("Map does not start with chunk [FORM] :: ChunkIf = [$chkMapHeader.chunkName]")
    
            // find out how many bytes are contained in the map data
            chkMapHeader.loadChunk(map)
            bytesToRead := mapSize - 4
    
            // read chunks while there's still chunks to read
            while (bytesToRead > 0) {
    
                // find and create the next chunk loader
                chunkLoader := createChunkLoader(fmpStream, map.mapHeader.endian)
    
                // load the chunk
                chunkLoader.loadChunk(map)
    
                // check that all the data was loaded
                if (chunkLoader.bytesRemaining > 0) {
                    log.warn("Chunk [$chunkLoader.chunkName] still has [$chunkLoader.bytesRemaining] bytes remaining, skipping...");
                }
    
                // the countdown continues
                bytesToRead -= 8    // for the chunk header
                bytesToRead -= chunkLoader.chunkSize
            }
            
            log.debug("loadMap() - Done.")
            return map
            
        } finally {
            fmpStream.close
        }
    }

    
    
     // ---- Internal Methods -------------------------------------------------
    
    ** Factory method for producing instances of ChunkLoaders.
    ** Note: This method does not close the given input stream.
    internal ChunkLoader createChunkLoader(InStream fmpStream, Endian endian) {
        log.debug("createChunkLoader() - Starting...")

        ChunkLoader?    chunkLoader     := null
        Str?            errorMessage    := null

        try {
            errorMessage    = "Could not read chunkId"
            chunkId         := fmpStream.readChars(4)

            errorMessage    = "Could not read chunk length for ChunkId [$chunkId]"
            chunkSize       := fmpStream.readU4 // ignore the LSB / MSB thing for chunk lengths
            
            if (chunkId == "FORM")  // a little fudge
                mapSize     = chunkSize
            
            errorMessage    = "Could not read raw chunk data for ChunkId [$chunkId]"
            buf             := fmpStream.readBufFully(null, chunkId == "FORM" ? 4 : chunkSize)

            // set a default return value
            errorMessage    = "Problem creating DefaultChunkLoader"
            chunkLoader     = DefaultChunkLoader(chunkId, buf, endian)

            // find actual ChunkLoader
            errorMessage    = "Could not find ChunkLoader for ChunkId [$chunkId]"
            chunkLoaderType := chunkLoaders.getOrThrow(chunkId)

            errorMessage    = "Problem initialising ChunkLoader for ChunkId [$chunkId]"
            chunkLoader     = chunkLoaderType.make([chunkId, buf, endian])

        } catch (Err e) {
            // always return a default chunk loader if we can, so we may
            // continue reading the rest of the map. Not finding a particular
            // ChunkLoader is going to be a common problem.
            if (chunkLoader != null) {
                log.warn(errorMessage)
            } else {
                throw ChunkLoadErr(errorMessage, e)
            }
        }

        log.info(chunkLoader.toStr)
        log.debug("createChunkLoader() - Done.")
        return chunkLoader
    }



    ** This is where we hard code chunk loader class references to chunk names.
    ** We do this so that (J2ME) obfuscaters can rename classes. It also allows
    ** us to delete empty chunk loader classes like ChunkLoaderLRYx. 
    internal Void registerChunkLoaders() {

        chunkLoaders["ANDT"] = ChunkLoaderANDT#
        chunkLoaders["ATHR"] = ChunkLoaderATHR#
//      chunkLoaders["BGFX"] = ChunkLoaderBGFX#
        chunkLoaders["BKDT"] = ChunkLoaderBKDT#
//      chunkLoaders["CMAP"] = ChunkLoaderCMAP#
        chunkLoaders["FORM"] = ChunkLoaderFORM#
        chunkLoaders["MPHD"] = ChunkLoaderMPHD#
//      chunkLoaders["OBDT"] = ChunkLoaderOBDT#
//      chunkLoaders["OBFN"] = ChunkLoaderOBFN#
//      chunkLoaders["TSTR"] = ChunkLoaderTSTR#

        chunkLoaders["BODY"] = ChunkLoaderBODY#
        chunkLoaders["LYR1"] = ChunkLoaderBODY#
        chunkLoaders["LYR2"] = ChunkLoaderBODY#
        chunkLoaders["LYR3"] = ChunkLoaderBODY#
        chunkLoaders["LYR4"] = ChunkLoaderBODY#
        chunkLoaders["LYR5"] = ChunkLoaderBODY#
        chunkLoaders["LYR6"] = ChunkLoaderBODY#
        chunkLoaders["LYR7"] = ChunkLoaderBODY#
    }
}

@Js
internal class DefaultChunkLoader : ChunkLoader {
    
    new make(Str chunkName, Buf chunkData, Endian endian) : super(chunkName, chunkData, endian) {}
        
    override Void loadChunk(MappyMap map) {
        // prevent the warning log
        super.skip(super.bytesRemaining)
    }
}