module Game.Waddle.Load
(load) where
import Game.Waddle.Types
import Control.Exception
import Text.Printf
import Data.Bits
import Data.Int
import Data.Word
import Data.CaseInsensitive(CI, mk)
import Data.Map(Map)
import qualified Data.Map as Map
import Data.Binary.Get
import Control.Monad
import Control.Applicative
import Data.ByteString(ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as BSL
getInt32le :: Get Int32
getInt32le = fromIntegral <$> getWord32le
getInt16le :: Get Int16
getInt16le = fromIntegral <$> getWord16le
getWadHeader :: Get WadHeader
getWadHeader =
WadHeader <$>
getByteString 4 <*>
getInt32le <*>
getInt32le
getWadEntry :: Get WadEntry
getWadEntry =
WadEntry <$>
getInt32le <*>
getInt32le <*>
(trimNUL <$> getByteString 8)
getWadEntryList :: Int -> Int -> Get [WadEntry]
getWadEntryList offset cnt = skip offset >> sequence (replicate cnt getWadEntry)
trimNUL :: ByteString -> ByteString
trimNUL s =
case BS.findIndex (== 0) s of
Nothing -> s
Just i -> BS.take i s
load :: FilePath -> IO Wad
load fp = do
wadContent <- BS.readFile fp
header@WadHeader{..} <- runGet' "WAD header" getWadHeader wadContent
wadEntries <- runGet' "WAD directory"
(getWadEntryList
(fromIntegral wadHeaderDirectoryOffset)
(fromIntegral wadHeaderLumpCount))
wadContent
let lumpLookup = mkLookup wadEntries wadContent
wadLumps = mkWadLumps wadEntries wadContent
p <- parseWad wadEntries wadLumps lumpLookup
return $ Wad {wadHeader = header,
wadDirectory = wadEntries,
wadLumps = wadLumps,
wadLumpLookup = lumpLookup,
wadFlats = psFlats p,
wadSprites = psSprites p,
wadPatches = psPatches p,
wadTextures = psTextures p,
wadPNames = psPNames p,
wadLevels = psLevels p,
wadColormap = psColormap p,
wadPalettes = psPalettes p}
where
mkWadLumps entries content =
map (\ WadEntry{..} ->
(BS.take (fromIntegral wadEntrySize) (BS.drop (fromIntegral wadEntryOffset) content))) entries
mkLookup entries content =
foldr (\ WadEntry{..} tab ->
Map.insert
(mk wadEntryName)
(BS.take (fromIntegral wadEntrySize) (BS.drop (fromIntegral wadEntryOffset) content))
tab)
Map.empty entries
data PState
= NoState
| InLevel LumpName
| InSprites
| InFlats
| InPatches
deriving (Show)
data ExtPicture = ExtPicture {
extPictureWidth :: Int16,
extPictureHeight :: Int16,
extPictureLeftOffset :: Int16,
extPictureTopOffset :: Int16,
extPictureColStarts :: [Int32]
}
deriving (Show)
getExtPicture :: Get ExtPicture
getExtPicture = do
w <- getInt16le
h <- getInt16le
l <- getInt16le
t <- getInt16le
colStarts <- replicateM (fromIntegral w) getInt32le
return $ ExtPicture w h l t colStarts
data ExtPost
= ExtPostEnd
| ExtPost {
extPostTop :: Word8,
extPostCount :: Word8,
extPostPixels :: ByteString,
extPostNext :: ExtPost
}
deriving (Show)
getExtPost :: Get ExtPost
getExtPost = do
top <- getWord8
case top of
255 ->
return ExtPostEnd
_ -> do
cnt <- getWord8
_skip1 <- getWord8
px <- getByteString (fromIntegral cnt)
_skip2 <- getWord8
e <- getExtPost
return $ ExtPost top cnt px e
convertPosts :: ExtPost -> [Post]
convertPosts ExtPostEnd = []
convertPosts ExtPost{..} =
Post {postTop = extPostTop,
postPixels = extPostPixels} : convertPosts extPostNext
convertPicture :: ExtPicture -> [ExtPost] -> Picture
convertPicture ExtPicture{..} extPosts =
Picture {pictureWidth = fromIntegral extPictureWidth,
pictureHeight = fromIntegral extPictureHeight,
pictureLeftOffset = fromIntegral extPictureLeftOffset,
pictureTopOffset = fromIntegral extPictureTopOffset,
picturePosts = map convertPosts extPosts}
getThingList :: Int -> Get [Thing]
getThingList cnt =
sequence (replicate cnt $
Thing <$>
getInt16le <*>
getInt16le <*>
getInt16le <*>
(thingTypeFromNumber <$> getInt16le) <*>
getInt16le)
getVertexList :: Int -> Get [Vertex]
getVertexList cnt =
sequence (replicate cnt $
Vertex <$>
getInt16le <*>
getInt16le)
getLineDefList :: Int -> Get [LineDef]
getLineDefList cnt =
sequence (replicate cnt $
LineDef <$>
getInt16le <*>
getInt16le <*>
getInt16le <*>
getInt16le <*>
getInt16le <*>
getInt16le <*>
(toMB <$> getInt16le))
where
toMB n | n < 0 = Nothing
toMB n = Just n
getSideDefList :: Int -> Get [SideDef]
getSideDefList cnt =
sequence (replicate cnt $
SideDef <$>
getInt16le <*>
getInt16le <*>
(trimNUL <$> getByteString 8) <*>
(trimNUL <$> getByteString 8) <*>
(trimNUL <$> getByteString 8) <*>
getInt16le)
getSeg :: Get Seg
getSeg =
Seg <$>
getInt16le <*>
getInt16le <*>
getInt16le <*>
getInt16le <*>
getInt16le <*>
getInt16le
getSegList :: Int -> Get [Seg]
getSegList cnt =
sequence (replicate cnt getSeg)
getSSector :: Get SSector
getSSector =
SSector <$>
getInt16le <*>
getInt16le
getSSectorList :: Int -> Get [SSector]
getSSectorList cnt =
sequence (replicate cnt getSSector)
getSectorList :: Int -> Get [Sector]
getSectorList cnt =
sequence (replicate cnt $
Sector <$>
getInt16le <*>
getInt16le <*>
(trimNUL <$> getByteString 8) <*>
(trimNUL <$> getByteString 8) <*>
getInt16le <*>
getInt16le <*>
getInt16le)
getNode :: Get Node
getNode =
Node <$>
getInt16le <*>
getInt16le <*>
getInt16le <*>
getInt16le <*>
getInt16le <*>
getInt16le <*>
getInt16le <*>
getInt16le <*>
getInt16le <*>
getInt16le <*>
getInt16le <*>
getInt16le <*>
(lr <$> getWord16le) <*>
(lr <$> getWord16le)
where
lr x = if x .&. 0x8000 == 0
then Left (fromIntegral x)
else Right (fromIntegral (x .&. 0x7fff))
getNodeList :: Int -> Get [Node]
getNodeList cnt =
sequence (replicate cnt getNode)
getBlocklists :: Int -> Get [Blocklist]
getBlocklists cnt | cnt == 0 = return []
getBlocklists cnt = do
0 <- getInt16le
vals <- getVals
rest <- getBlocklists (cnt 1)
return $ vals : rest
where
getVals :: Get Blocklist
getVals = do
i <- getInt16le
case i of
1 -> return []
_ -> do
r <- getVals
return $ i : r
getBlockmap :: Get Blockmap
getBlockmap = do
ox <- getInt16le
oy <- getInt16le
cols <- getInt16le
rows <- getInt16le
offsets <- sequence (replicate (fromIntegral (cols * rows)) getWord16le)
blocklists <- getBlocklists (fromIntegral (cols * rows))
return Blockmap {
blockmapOriginX = ox,
blockmapOriginY = oy,
blockmapColumns = cols,
blockmapRows = rows,
blockmapOffsets = offsets,
blockmapBlocklists = blocklists
}
getPatchDescriptor :: Get PatchDescriptor
getPatchDescriptor =
PatchDescriptor <$>
(fromIntegral <$> getWord16le) <*>
(fromIntegral <$> getWord16le) <*>
(fromIntegral <$> getWord16le) <*>
(fromIntegral <$> getWord16le) <*>
(fromIntegral <$> getWord16le)
getTexture :: Get Texture
getTexture = do
n <- trimNUL <$> getByteString 8
0 <- getWord16le
0 <- getWord16le
w <- fromIntegral <$> getWord16le
h <- fromIntegral <$> getWord16le
0 <- getWord16le
0 <- getWord16le
pdCnt <- fromIntegral <$> getWord16le
pDescs <- sequence (replicate pdCnt getPatchDescriptor)
return Texture {
textureName = n,
textureWidth = w,
textureHeight = h,
texturePatchDescriptors = pDescs
}
getTextures :: Get (Map (CI LumpName) Texture)
getTextures = do
cnt <- fromIntegral <$> getWord32le
_ <- sequence (replicate cnt getWord32le)
textures <- sequence (replicate cnt getTexture)
return $ Map.fromList $ map (\ tex@Texture{..} -> (mk textureName, tex)) textures
getPNames :: Get (Map Int LumpName)
getPNames = do
cnt <- fromIntegral <$> getWord32le
names <- sequence (replicate cnt (getByteString 8))
return $ Map.fromList (zip [0..] (map trimNUL names))
data ParseState = ParseState {
psState :: PState,
psLumpLookup :: Map (CI LumpName) ByteString,
psMaps :: Map (CI LumpName) Level,
psSprites :: Map (CI LumpName) Sprite,
psFlats :: Map (CI LumpName) Flat,
psPatches :: Map (CI LumpName) Patch,
psPNames :: Map Int LumpName,
psTextures :: Map (CI LumpName) Texture,
psLevels :: Map (CI LumpName) Level,
psThings :: [Thing],
psVertices :: [Vertex],
psLineDefs :: [LineDef],
psSideDefs :: [SideDef],
psSegs :: [Seg],
psSSectors :: [SSector],
psSectors :: [Sector],
psNodes :: [Node],
psReject :: Maybe Reject,
psBlockmap :: Maybe Blockmap,
psPalettes :: Maybe Palettes,
psColormap :: Maybe Colormap
}
initParseState :: Map (CI LumpName) ByteString -> ParseState
initParseState lu = ParseState {
psState = NoState,
psLumpLookup = lu,
psMaps = Map.empty,
psSprites = Map.empty,
psFlats = Map.empty,
psPatches = Map.empty,
psPNames = Map.empty,
psTextures = Map.empty,
psLevels = Map.empty,
psThings = [],
psVertices = [],
psLineDefs = [],
psSideDefs = [],
psSegs = [],
psSSectors = [],
psSectors = [],
psNodes = [],
psReject = Nothing,
psBlockmap = Nothing,
psPalettes = Nothing,
psColormap = Nothing
}
runGet' :: String -> Get a -> ByteString -> IO a
runGet' ctxt get bs =
case runGetOrFail get (BSL.fromChunks [bs]) of
Left (_, _, err) -> throwIO $ WadExceptionDecodeError ctxt err
Right (_, _, r) -> return r
parseWad :: [WadEntry] -> [ByteString] -> Map (CI LumpName) ByteString -> IO ParseState
parseWad entries lumps lumpMap = foldM parseStep (initParseState lumpMap) (zip lumps entries)
where
parseStep ps@ParseState{psState = NoState}
lumpWe@(_, WadEntry{..}) = do
case wadEntryName of
"F_START" -> return ps{psState = InFlats}
"S_START" -> return ps{psState = InSprites}
"P_START" -> return ps{psState = InPatches}
"PLAYPAL" -> parsePalettes ps lumpWe
"COLORMAP" -> parseColormap ps lumpWe
"ENDOOM" -> return ps
"DEMO1" -> return ps
"DEMO2" -> return ps
"DEMO3" -> return ps
"TEXTURE1" -> parseTextures ps lumpWe
"TEXTURE2" -> parseTextures ps lumpWe
"PNAMES" -> parsePNames ps lumpWe
"GENMIDI" -> return ps
"HELP" -> return ps
"HELP1" -> return ps
"VICTORY2" -> return ps
"PFUB1" -> return ps
"PFUB2" -> return ps
"END0" -> return ps
"END1" -> return ps
"END2" -> return ps
"END3" -> return ps
"END4" -> return ps
"END5" -> return ps
"END6" -> return ps
"ENDPIC" -> return ps
"TITLEPIC" -> return ps
"CREDIT" -> return ps
"BOSSBACK" -> return ps
_ | "AMMNUM" `BS8.isPrefixOf` wadEntryName -> return ps
"STBAR" -> return ps
"INTERPIC" -> return ps
"_DEUTEX_" -> return ps
_ | "STGNUM" `BS8.isPrefixOf` wadEntryName -> return ps
_ | "BRDR" `BS8.isPrefixOf` wadEntryName -> return ps
_ | "WI" `BS8.isPrefixOf` wadEntryName -> return ps
_ | "ST" `BS8.isPrefixOf` wadEntryName -> return ps
_ | "M_" `BS8.isPrefixOf` wadEntryName -> return ps
_ | "D" `BS8.isPrefixOf` wadEntryName -> return ps
_ | "CWILV" `BS8.isPrefixOf` wadEntryName -> return ps
_ | wadEntryName `elem` knownMapNames -> return ps{psState = InLevel wadEntryName}
_ -> do
printf "unrecognized lump: %s at %d\n" (BS8.unpack wadEntryName)
wadEntryOffset :: IO ()
return ps
parseStep ps@ParseState{psState = InLevel curLevel} lumpWe@(lump, WadEntry{..}) = do
case wadEntryName of
"THINGS" -> do
things <- runGet' "THINGS lump"
(getThingList (fromIntegral $ wadEntrySize `div` 10)) lump
return ps{psThings = things}
"LINEDEFS" -> do
lineDefs <- runGet' "LINEDEFS lump"
(getLineDefList (fromIntegral $ wadEntrySize `div` 14)) lump
return ps{psLineDefs = lineDefs}
"SIDEDEFS" -> do
sideDefs <- runGet' "SIDEDEF lump"
(getSideDefList (fromIntegral $ wadEntrySize `div` 30)) lump
return ps{psSideDefs = sideDefs}
"VERTEXES" -> do
vertices <- runGet' "VERTEXES lump"
(getVertexList (fromIntegral $ wadEntrySize `div` 4)) lump
return ps{psVertices = vertices}
"SEGS" -> do
segs <- runGet' "SEGS lump"
(getSegList (fromIntegral $ wadEntrySize `div` 12)) lump
return ps{psSegs = segs}
"SSECTORS" -> do
ssectors <- runGet' "SSECTORS lump"
(getSSectorList (fromIntegral $ wadEntrySize `div` 4)) lump
return ps{psSSectors = ssectors}
"NODES" -> do
nodes <- runGet' "NODES lump"
(getNodeList (fromIntegral $ wadEntrySize `div` 28)) lump
return ps{psNodes = nodes}
"SECTORS" -> do
sectors <- runGet' "SECTORS lump"
(getSectorList (fromIntegral $ wadEntrySize `div` 26)) lump
return ps{psSectors = sectors}
"REJECT" ->
return ps{psReject = Just $ Reject lump}
"BLOCKMAP" -> do
blockmap <- runGet' "BLOCKMAP lump"
getBlockmap lump
return ps{psBlockmap = Just blockmap}
_ -> parseStep ps{psState = NoState,
psThings = [],
psLineDefs = [],
psSideDefs = [],
psVertices = [],
psSegs = [],
psSSectors = [],
psNodes = [],
psSectors = [],
psReject = Nothing,
psBlockmap = Nothing,
psLevels = Map.insert (mk curLevel)
Level {
levelName = curLevel,
levelThings = psThings ps,
levelLineDefs = psLineDefs ps,
levelSideDefs = psSideDefs ps,
levelVertices = psVertices ps,
levelSegs = psSegs ps,
levelSSectors = psSSectors ps,
levelNodes = psNodes ps,
levelSectors = psSectors ps,
levelReject = psReject ps,
levelBlockmap = psBlockmap ps
} (psLevels ps)
}
lumpWe
parseStep ps@ParseState{psState = InSprites} lumpWe@(_, WadEntry{..}) = do
case wadEntryName of
"S_END" -> return ps{psState = NoState}
_ -> parseSprite ps lumpWe
parseStep ps@ParseState{psState = InFlats} lumpWe@(_, WadEntry{..}) = do
case wadEntryName of
"F_END" -> return ps{psState = NoState}
"F1_START" -> return ps
"F1_END" -> return ps
"F2_START" -> return ps
"F2_END" -> return ps
"F3_START" -> return ps
"F3_END" -> return ps
_ -> parseFlat ps lumpWe
parseStep ps@ParseState{psState = InPatches} lumpWe@(_, WadEntry{..}) = do
case wadEntryName of
"P_END" -> return ps{psState = NoState}
"P1_START" -> return ps
"P1_END" -> return ps
"P2_START" -> return ps
"P2_END" -> return ps
_ -> parsePatch ps lumpWe
parseColormap ps (lump, WadEntry{..}) = do
cm <- runGet' "COLORMAP lump"
(sequence (replicate 34 $ getByteString 256)) lump
return ps{psColormap = Just $ Colormap cm}
parsePalettes ps (lump, WadEntry{..}) = do
pals <- runGet' "PLAYPAL lump"
(sequence
(replicate 14 $
(sequence
(replicate 256
((,,) <$> getWord8 <*> getWord8 <*> getWord8)))))
lump
return ps{psPalettes = Just $ Palettes pals}
parsePNames ps (lump, WadEntry{..}) = do
pnames <- runGet' "PNAMES lump" getPNames lump
forM_ (Map.toList pnames) $ \ (_, n) ->
case Map.lookup (mk n) (psLumpLookup ps) of
Just _ -> return ()
Nothing -> throwIO $ WadExceptionFormatError "PNAMES lump"
("reference to non-existant patch lump: " ++ BS8.unpack n)
return ps{psPNames = pnames}
parseTextures ps (lump, WadEntry{..}) = do
ts <- runGet' (BS8.unpack wadEntryName ++ " lump") getTextures lump
return ps{psTextures = Map.union ts (psTextures ps)}
parseSprite ps e@(_, WadEntry{..}) = do
pic <- parsePicture ps e
let sprite = Sprite{spriteName = wadEntryName,
spritePicture = pic}
return ps{psSprites = Map.insert (mk wadEntryName) sprite (psSprites ps)}
parsePatch ps e@(_, WadEntry{..}) = do
pic <- parsePicture ps e
let patch = Patch{patchName = wadEntryName,
patchPicture = pic}
return ps{psPatches = Map.insert (mk wadEntryName) patch (psPatches ps)}
parseFlat ps (lump, WadEntry{..}) = do
unless (BS.length lump == 4096) $
throwIO $ WadExceptionDecodeError "flat" $ BS8.unpack wadEntryName ++
": flat has wrong size (expected=4096, actual=" ++ show (BS.length lump) ++ ")"
let flat = Flat {flatName = wadEntryName, flatData = lump}
return ps{psFlats = Map.insert (mk wadEntryName) flat (psFlats ps)}
parsePicture _ (lump, WadEntry{..}) = do
pic@ExtPicture{..} <- runGet' "picture" getExtPicture lump
posts <- mapM (parseColumn lump) extPictureColStarts
return $ convertPicture pic posts
where
parseColumn s colStart =
runGet' "picture post" getExtPost (BS.drop (fromIntegral colStart) s)
knownMapNames :: [ByteString]
knownMapNames = [BS8.pack (printf "E%dM%d" ep mp) | ep <- [1..4::Int], mp <- [1..9::Int]] ++
[BS8.pack (printf "MAP%02d" i) | i <- [1..32::Int]]