packages feed

minecraft-data-0.1.0.0: Minecraft/Anvil.hs

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Minecraft.Anvil where

import Control.Monad (when, replicateM)
import Control.Monad.State.Strict (execStateT, modify')
import Codec.Compression.Zlib (decompress, compress)
import Data.Bits (shiftR, shiftL, (.&.), (.|.))
import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString      as B
import qualified Data.ByteString.Lazy as LB
import Data.Data (Data, Typeable)
import Data.List (mapAccumL)
import Data.Map (Map)
import qualified Data.Map as Map
import Data.NBT (NBT)
import Data.Serialize (Serialize(..), Get, Put, decodeLazy, encodeLazy, getLazyByteString, getWord8, getWord32be, putLazyByteString, putWord8, putWord32be)
import Data.Time.Clock.POSIX (POSIXTime)
import Data.Vector (Vector)
import qualified Data.Vector as Vector
import Data.Word  (Word8, Word32)
import GHC.Generics
import Pipes.ByteString (fromHandle, toHandle)
import Pipes.Cereal  (decodeGet, encodePut)
import Pipes.Parse (runStateT)
import Pipes (Pipe, runEffect, (>->), await, each, yield)
import System.IO (Handle, hSeek, SeekMode(AbsoluteSeek))

{-
regionX :: Double -- ^ chunkX
        -> Int
regionX x = floor (x / 32.0)

regionY :: Double -- ^ chunkY
        -> Int
regionY y = foor (y / 32.0)
-}

type ChunkX = Int
type ChunkZ = Int
type RegionX = Int
type RegionZ = Int

type ChunkMap = Map (ChunkX, ChunkZ) (ChunkData, POSIXTime)

regionX :: ChunkX
        -> RegionX
regionX x = x `shiftR` 5

regionZ :: ChunkZ
        -> RegionZ
regionZ z = z `shiftR` 5

chunkIndex :: (ChunkX, ChunkZ)
           -> Int
chunkIndex (x, z) = (x `pmod` 32) + ((z `pmod` 32) * 32)
  where
    pmod n m =
      let n' = n `mod` m
      in if (n' >= 0)
         then n'
         else n' + m

type Word24 = Word32

-- | ChunkLocation
--
-- ChunkLocation 0 0 means chunk is not present in the file
data ChunkLocation = ChunkLocation
  { chunkOffset :: Word24 -- ^ number of 4KiB sectors from start of file
  , chunkLength :: Word8 -- ^ length of chunk -- units are 4KiB sectors, rounded up
  }
  deriving (Eq, Ord, Read, Show, Data, Typeable, Generic)

-- | ChunkLocation 0 0 means chunk is not present in the file
emptyChunkLocation :: ChunkLocation
emptyChunkLocation = ChunkLocation 0 0

putChunkLocation :: ChunkLocation -> Put
putChunkLocation (ChunkLocation offset len) =
  do putWord8 (fromIntegral ((offset `shiftR` 16) .&. 0xFF))
     putWord8 (fromIntegral ((offset `shiftR` 8) .&. 0xFF))
     putWord8 (fromIntegral (offset .&. 0xFF))
     putWord8 len

getChunkLocation :: Get ChunkLocation
getChunkLocation =
  do b2  <- fromIntegral <$> getWord8
     b1  <- fromIntegral <$> getWord8
     b0  <- fromIntegral <$> getWord8
     len <- fromIntegral <$> getWord8
     pure (ChunkLocation ((b2 `shiftL` 16) .|. (b1 `shiftL` 8) .|. b0) len)

instance Serialize ChunkLocation where
  put = putChunkLocation
  get = getChunkLocation

data AnvilHeader = AnvilHeader
  { locations  :: Vector ChunkLocation
  , timestamps :: Vector POSIXTime
  }
  deriving (Eq, Ord, Show, Data, Typeable, Generic)

showAnvilHeader :: AnvilHeader -> String
showAnvilHeader ah = show $ AnvilHeader { locations = Vector.filter (/= emptyChunkLocation) (locations ah)
                                        , timestamps = Vector.filter (/= 0) (timestamps ah)
                                        }

-- guessing on the timestamp format a bit
putTimestamp :: POSIXTime -> Put
putTimestamp t = putWord32be (round t)

getTimestamp :: Get POSIXTime
getTimestamp =
  do l <- getWord32be
     pure $ (realToFrac l)

putAnvilHeader :: AnvilHeader -> Put
putAnvilHeader (AnvilHeader locations timestamps)
  | (Vector.length locations /= 1024) || (Vector.length timestamps /= 1024) =
    error "putAnvilHeader: locations and timestamps fields must be exactly 1024 entries long."
  | otherwise =
      do mapM_ putChunkLocation (Vector.toList locations)
         mapM_ putTimestamp (Vector.toList timestamps)

getAnvilHeader :: Get AnvilHeader
getAnvilHeader =
  do locations  <- replicateM 1024 getChunkLocation
     timestamps <- replicateM 1024 getTimestamp
     pure $ AnvilHeader (Vector.fromList locations) (Vector.fromList timestamps)

instance Serialize AnvilHeader where
  put = putAnvilHeader
  get = getAnvilHeader

emptyAnvilHeader :: AnvilHeader
emptyAnvilHeader =
  AnvilHeader { locations  = Vector.replicate 1024 emptyChunkLocation
              , timestamps = Vector.replicate 1024 0
              }

-- | make an 'AnvilHeader'
--
-- assumes the chunks will be written in the same order as they appear in the list
mkAnvilHeader :: [((ChunkX, ChunkZ), (ChunkData, POSIXTime))]
                 -> AnvilHeader
mkAnvilHeader chunks =
  let timestamps' = map (\(i, (_, t)) -> (chunkIndex i, t)) chunks
      locations'   = snd $ mapAccumL mkLocation 0x2 chunks -- ^ first chunk is at sector 0x2, after the AnvilHeader
  in AnvilHeader { locations  = (locations emptyAnvilHeader) Vector.// locations'
                 , timestamps = (timestamps emptyAnvilHeader) Vector.// timestamps'
                 }
  where
    mkLocation :: Word24 -> ((ChunkX, ChunkZ), (ChunkData, POSIXTime)) -> (Word24, (Int, ChunkLocation))
    mkLocation offset (chunkPos, (chunkData, _)) =
      let paddedSectorLength = ((chunkDataLength chunkData) + 4 + 4095) `div` 4096
          offset' = offset + paddedSectorLength
      in (offset', (chunkIndex chunkPos, ChunkLocation offset (fromIntegral paddedSectorLength)))

data CompressionType
  = GZip -- ^ unused in practice
  | Zlib
  deriving (Eq, Ord, Read, Show, Data, Typeable, Generic)

-- | ChunkData
--
-- Compressed chunk data
data ChunkData = ChunkData
  { chunkDataLength      :: Word32 -- ^ length of chunkData + 1
  , chunkDataCompression :: CompressionType -- ^ compression type
  , chunkData            :: ByteString -- ^ compressed data (length - 1)
  }
  deriving (Eq, Ord, Read, Show, Data, Typeable, Generic)

putChunkData :: ChunkData -> Put
putChunkData cd =
  do putWord32be (chunkDataLength cd)
     case chunkDataCompression cd of
       GZip -> putWord8 1
       Zlib -> putWord8 2
     putLazyByteString (chunkData cd)

getChunkData :: Get ChunkData
getChunkData =
  do len  <- getWord32be
     comp <- do w <- getWord8
                case w of
                  1 -> pure GZip
                  2 -> pure Zlib
                  _ -> error $ "Unknown compression code in getChunkData: " ++ show (len, w)
     bs   <- getLazyByteString (fromIntegral (len - 1))
     pure $ ChunkData len comp bs

instance Serialize ChunkData where
  put = putChunkData
  get = getChunkData

-- | read 'ChunkData' from a Seekable 'Handle'
readChunkData :: Handle -> ChunkLocation -> IO (Maybe ChunkData)
readChunkData h chunkLocation
  | chunkLocation == emptyChunkLocation = pure Nothing
  | otherwise =
      do hSeek h AbsoluteSeek (fromIntegral $ ((chunkOffset chunkLocation) * 4096))
         (r, _) <- runStateT (decodeGet getChunkData) (fromHandle h)
         case r of
           (Left err) -> error err
           (Right cd) -> pure (Just cd)

-- | write 'ChunkData' to a Seekable 'Handle'
--
-- FIXME: chunks are supposed to be guaranteed less than 1MB
{-
writeChunkData :: Handle -> ChunkData -> IO ()
writeChunkData h chunkData =
  do runEffect $ (encodePut (putChunkData chunkData)) >-> (toHandle h)
     let padding = 4096 - ((chunkDataLength chunkData + 4) `mod` 4096)
     when (padding > 0) (B.hPutStr h (B.replicate (fromIntegral padding) 0))
-}

writeChunkData :: (Monad m) => Pipe ChunkData B.ByteString m ()
writeChunkData  = do
  chunkData <- await
  encodePut (putChunkData chunkData)
  let padding = 4096 - ((chunkDataLength chunkData + 4) `mod` 4096)
  when (padding > 0) (yield (B.replicate (fromIntegral padding) 0))

{-

  do bytes <- execStateT (runEffect $ (encodePut (putChunkData chunkData)) >-> counter >-> (toHandle h)) 0
     pure bytes
       where
         counter =
           do bs <- await
              modify' $ \i -> i + B.length bs
              yield bs
-}
{-
readChunkData :: Handle -> ChunkLocation -> IO (Maybe ChunkData)
readChunkData h chunkLocation
  | chunkLocation == emptyChunkLocation = pure Nothing
  | otherwise =
      do hSeek h AbsoluteSeek (fromIntegral $ ((chunkOffset chunkLocation) * 4096))
         (r, _) <- runStateT (decodeGet getChunkData) (fromHandle h)
         case r of
           (Left err) -> error err
           (Right cd) -> pure (Just cd)
-}
decompressChunkData :: ChunkData -> Either String NBT
decompressChunkData cd
  | chunkDataCompression cd == Zlib =
    decodeLazy (decompress (chunkData cd))
  | otherwise = error $ "decompressChunkData not implemented for " ++ show (chunkDataCompression cd)

-- | NBT needs to be a Chunk
compressChunkData :: NBT -> ChunkData
compressChunkData nbt =
  let d = compress (encodeLazy nbt)
  in ChunkData { chunkDataLength      = 1 + fromIntegral (LB.length d)
               , chunkDataCompression = Zlib
               , chunkData            = d
               }

writeChunkMap :: Handle -> ChunkMap -> IO ()
writeChunkMap h chunkMap =
  do hSeek h AbsoluteSeek 0
     let chunks = Map.toAscList chunkMap
     runEffect $ (encodePut (putAnvilHeader (mkAnvilHeader chunks))) >-> (toHandle h)
     runEffect $ (each $ map (fst . snd) chunks) >-> writeChunkData >-> (toHandle h)
--     runEffect $ (encodePut (putAnvilHeader emptyAnvilHeader)) >-> (toHandle h)
     -- [(ChunkPos, ChunkData)] -> [(ChunkPos, Length)]
     -- chunks' <- mapM (\(chunkPos, (cd, modTime)) -> do i <- writeChunkData h cd ; pure (chunkPos, (i, modTime))) chunks
     pure ()