packages feed

tttool-1.6: src/GMEWriter.hs

{-# LANGUAGE RankNTypes, RecursiveDo, RecordWildCards, GADTs, GeneralizedNewtypeDeriving, ScopedTypeVariables #-}

module GMEWriter (writeTipToiFile) where

import qualified Data.ByteString.Lazy as B
import qualified Data.Binary.Builder as Br
import Text.Printf
import Control.Monad
import Control.Applicative (Applicative)
import qualified Data.Map as M
import Control.Monad.Writer.Lazy
import Control.Monad.State.Lazy
import Debug.Trace

import Types
import Constants
import Cypher


-- Assembling .gme files

-- Assembly monad
-- We need a data structure that we can extract its length from before we know its values
-- So we will use a lazy pair of length (Int) and builder

newtype SPutM a = SPutM (StateT Word32 (Writer Br.Builder) a)
    deriving (Functor, Applicative, Monad, MonadFix)
type SPut = SPutM ()

putWord8 :: Word8 -> SPut
putWord8 w = SPutM (tell (Br.singleton w) >> modify (+1))

putWord16 :: Word16 -> SPut
putWord16 w = SPutM (tell (Br.putWord16le w) >> modify (+2))

putWord32 :: Word32 -> SPut
putWord32 w = SPutM (tell (Br.putWord32le w) >> modify (+4))

putBS :: B.ByteString -> SPut
putBS bs = SPutM (tell (Br.fromLazyByteString bs) >> modify (+ fromIntegral (B.length bs)))

putArray :: Integral n => (n -> SPut) -> [SPut] -> SPut
putArray h xs = do
    h (fromIntegral (length xs))
    sequence_ xs

data FunSplit m where
    FunSplit :: forall m a . (a -> m ()) -> m a -> FunSplit m


mapFstMapSnd :: forall m. MonadFix m => [FunSplit m] -> m ()
mapFstMapSnd xs = go xs (return ())
  where
    go :: [FunSplit m] -> m b -> m b
    go [] cont = cont
    go (FunSplit f s:xs) cont = mdo
        f v
        (v,vs) <- go xs $ do
            vs <- cont
            v <- s
            return (v,vs)
        return vs

offsetsAndThen :: [SPut] -> SPut
offsetsAndThen = mapFstMapSnd . map go
    where go x = FunSplit putWord32 (getAddress x)

putOffsets :: Integral n => (n -> SPut) -> [SPut] -> SPut
putOffsets h xs = mdo
    h (fromIntegral (length xs))
    offsetsAndThen xs

seek :: Word32 -> SPut
seek to = SPutM $ do
    now <- get
    when (now > to) $ do
        fail $ printf "Cannot seek to 0x%08X, already at 0x%08X" to now
    tell $ (Br.fromLazyByteString (B.replicate (fromIntegral (to-now)) 0))
    modify (+ (to-now))

-- Puts something, returning the offset to the beginning of it.
getAddress :: SPut -> SPutM Word32
getAddress (SPutM what) = SPutM $ do
    a <- get
    what
    return a

runSPut :: SPut -> B.ByteString
--runSPut (SPutM act) = Br.toLazyByteString $ evalState (execWriterT act) 0
runSPut (SPutM act) = Br.toLazyByteString $ execWriter (evalStateT act 0)


putTipToiFile :: TipToiFile -> SPut
putTipToiFile (TipToiFile {..}) = mdo
    putWord32 sto
    putWord32 mft
    putWord32 0x238b
    putWord32 ast -- Additional script table
    putWord32 gto -- Game table offset
    putWord32 ttProductId
    putWord32 iro
    putWord32 ttRawXor
    putWord8 $ fromIntegral (B.length ttComment)
    putBS ttComment
    putBS ttDate
    seek 0x0071 -- Just to be safe
    putWord32 ipllo
    seek 0x0200 -- Just to be safe
    sto <- getAddress $ putScriptTable ttScripts
    ast <- getAddress $ putWord16 0x00 -- For now, no additional script table
    gto <- getAddress $ putGameTable ttGames
    iro <- getAddress $ putInitialRegs ttInitialRegs
    mft <- getAddress $ putAudioTable ttAudioXor ttAudioFiles
    ipllo <- getAddress $ putPlayListList ttWelcome
    return ()

putGameTable :: [Game] -> SPut
putGameTable games = putOffsets putWord32 $ map putGame games

putGame :: Game -> SPut
putGame (Game6 {..}) = mdo
    putWord16 6
    putWord16 (fromIntegral (length gSubgames) - gBonusSubgameCount)
    putWord16 gRounds
    putWord16 gBonusSubgameCount
    putWord16 gBonusRounds
    putWord16 gBonusTarget
    putWord16 gUnknownI
    putWord16 gEarlyRounds
    putWord16 gUnknownQ
    putWord16 gRepeatLastMedia
    putWord16 gUnknownX
    putWord16 gUnknownW
    putWord16 gUnknownV
    putWord32 spl
    putWord32 repl
    putWord32 fpl
    putWord32 rspl
    putWord32 lrspl
    putWord32 rspl2
    putWord32 lrspl2
    mapM_ putWord32 sgo
    mapM_ putWord16 gTargetScores
    mapM_ putWord16 gBonusTargetScores
    mapM_ putWord32 fpll
    mapM_ putWord32 fpll2
    putWord32 gilo


    spl   <- getAddress $ putPlayListList gStartPlayList
    repl  <- getAddress $ putPlayListList gRoundEndPlayList
    fpl   <- getAddress $ putPlayListList gFinishPlayList
    rspl  <- getAddress $ putPlayListList gRoundStartPlayList
    lrspl <- getAddress $ putPlayListList gLaterRoundStartPlayList
    rspl2  <- getAddress $ putPlayListList gRoundStartPlayList2
    lrspl2 <- getAddress $ putPlayListList gLaterRoundStartPlayList2
    fpll  <- mapM (getAddress . putPlayListList) gFinishPlayLists
    fpll2  <- mapM (getAddress . putPlayListList) gBonusFinishPlayLists

    sgo <- mapM (getAddress . putSubGame) gSubgames

    gilo <- getAddress $ putGameIdList gBonusSubgameIds

    return ()

putGame (Game253) = mdo
    putWord16 253

putGame g = mdo
    putWord16 (gameType g)
    putWord16 (fromIntegral $ length (gSubgames g))
    putWord16 (gRounds g)
    putWord16 (gUnknownC g)
    putWord16 (gEarlyRounds g)
    putWord16 (gRepeatLastMedia g)
    putWord16 (gUnknownX g)
    putWord16 (gUnknownW g)
    putWord16 (gUnknownV g)
    putWord32 spl
    putWord32 repl
    putWord32 fpl
    putWord32 rspl
    putWord32 lrspl
    mapM_ putWord32 sgo
    mapM_ putWord16 (gTargetScores g)
    mapM_ putWord32 fpll

    case g of
        Game7 {..} -> mdo
            putWord32 sggo
            sggo <- getAddress $ do
                putOffsets putWord16 $ map putGameIdList gSubgameGroups
            return ()
        Game8 {..} -> mdo
            putWord32 gso
            putWord32 gs
            putWord32 gse1
            putWord32 gse2

            gso <- getAddress $ putOidList gGameSelectOIDs
            gs  <- getAddress $ putArray putWord16 $ map putWord16 gGameSelect
            gse1 <- getAddress $ putPlayListList gGameSelectErrors1
            gse2 <- getAddress $ putPlayListList gGameSelectErrors2

            return ()
        Game9 {..} -> mdo
            mapM_ putWord32 epll
            epll <- mapM (getAddress . putPlayListList) gExtraPlayLists
            return ()
        Game10 {..} -> mdo
            mapM_ putWord32 epll
            epll <- mapM (getAddress . putPlayListList) gExtraPlayLists
            return ()
        Game16 {..} -> mdo
            putWord32 eoids
            mapM_ putWord32 epll
            eoids <- getAddress $ putOidList gExtraOIDs
            epll <- mapM (getAddress . putPlayListList) gExtraPlayLists
            return ()
        _ -> return ()

    spl   <- getAddress $ putPlayListList $ gStartPlayList g
    repl  <- getAddress $ putPlayListList $ gRoundEndPlayList g
    fpl   <- getAddress $ putPlayListList $ gFinishPlayList g
    rspl  <- getAddress $ putPlayListList $ gRoundStartPlayList g
    lrspl <- getAddress $ putPlayListList $ gLaterRoundStartPlayList g
    fpll  <- mapM (getAddress . putPlayListList) (gFinishPlayLists g)

    sgo <- mapM (getAddress . putSubGame) (gSubgames g)
    return ()



putPlayListList :: [PlayList] -> SPut
putPlayListList playlistlist = do
    putOffsets putWord16 $ map putPlayList playlistlist

putGameIdList :: [GameId] -> SPut
putGameIdList = putArray putWord16 . map (putWord16 . (+1))

putOidList :: [GameId] -> SPut
putOidList = putArray putWord16 . map putWord16

putSubGame :: SubGame -> SPut
putSubGame (SubGame {..}) = mdo
    putBS sgUnknown
    putArray putWord16 $ map putWord16 sgOids1
    putArray putWord16 $ map putWord16 sgOids2
    putArray putWord16 $ map putWord16 sgOids3
    mapM_ putWord32 pll 
    pll  <- mapM (getAddress . putPlayListList) sgPlaylist
    return ()

putScriptTable :: [(Word16, Maybe [Line ResReg])] -> SPut
putScriptTable [] = error "Cannot create file with an empty script table"
putScriptTable scripts = mdo
    putWord32 (fromIntegral last)
    putWord32 (fromIntegral first)
    mapFstMapSnd (map go [first .. last])
    return ()
  where
    go i = case M.lookup i m of
        Just (Just l) -> FunSplit putWord32 (getAddress $ putLines l)
        _ -> FunSplit (\_ -> putWord32 0xFFFFFFFF) (return ())
    m = M.fromList scripts
    first = fst (M.findMin m)
    last = fst (M.findMax m)

putInitialRegs :: [Word16] -> SPut
putInitialRegs = putArray putWord16 . map putWord16

putLines :: [Line ResReg] -> SPut
putLines = putOffsets putWord16 . map putLine

putLine :: Line ResReg -> SPut
putLine (Line _ conds acts idx) = do
    putArray putWord16 $ map putCond conds
    putArray putWord16 $ map putCommand acts
    putPlayList idx

putPlayList :: PlayList -> SPut
putPlayList = putArray putWord16 . map putWord16

putCond :: Conditional ResReg -> SPut
putCond (Cond v1 o v2) = do
    putTVal v1
    putCondOp o
    putTVal v2

putTVal :: TVal ResReg -> SPut
putTVal (Reg n) = do
    putWord8 0
    putWord16 n
putTVal (Const n) = do
    putWord8 1
    putWord16 n

putCondOp :: CondOp -> SPut
putCondOp Eq  = mapM_ putWord8 [0xF9, 0xFF]
putCondOp Gt  = mapM_ putWord8 [0xFA, 0xFF]
putCondOp Lt  = mapM_ putWord8 [0xFB, 0xFF]
putCondOp GEq = mapM_ putWord8 [0xFD, 0xFF]
putCondOp LEq = mapM_ putWord8 [0xFE, 0xFF]
putCondOp NEq = mapM_ putWord8 [0xFF, 0xFF]
putCondOp (Unknowncond b) = putBS b


putCommand :: Command ResReg -> SPut
putCommand (ArithOp o r v) = do
    putWord16 r
    mapM_ putWord8 $ arithOpCode o
    putTVal v
putCommand (Neg r) = do
    putWord16 r
    mapM_ putWord8 [0xF8, 0xFF]
    putTVal (Const 0)
putCommand RandomVariant = do
    putWord16 0
    mapM_ putWord8 [0xE0, 0xFF]
    putTVal (Const 0)
putCommand PlayAllVariant = do
    putWord16 0
    mapM_ putWord8 [0xE1, 0xFF]
    putTVal (Const 0)
putCommand (Play n) = do
    putWord16 0
    mapM_ putWord8 [0xE8, 0xFF]
    putTVal (Const (fromIntegral n))
putCommand (Random a b) = do
    putWord16 0
    mapM_ putWord8 [0x00, 0xFC]
    putTVal (Const (lowhigh a b))
putCommand (PlayAll a b) = do
    putWord16 0
    mapM_ putWord8 [0x00, 0xFB]
    putTVal (Const (lowhigh a b))
putCommand (Game n) = do
    putWord16 0
    mapM_ putWord8 [0x00, 0xFD]
    putTVal (Const n)
putCommand Cancel = do
    putWord16 0
    mapM_ putWord8 [0xFF, 0xFA]
    putTVal (Const 0xFFFF)
putCommand (Jump v) = do
    putWord16 0
    mapM_ putWord8 [0xFF, 0xF8]
    putTVal v
putCommand (Timer r v) = do
    putWord16 r
    mapM_ putWord8 [0x00, 0xFF]
    putTVal v
putCommand (NamedJump s) = error "putCommand: Unresolved NamedJump"
putCommand (Unknown b r v) = do
    putWord16 r
    putBS b
    putTVal v

putAudioTable :: Word8 -> [B.ByteString] -> SPut
putAudioTable x as = mapFstMapSnd
    [ FunSplit (\o -> putWord32 o >> putWord32 (fromIntegral (B.length a)))
               (getAddress (putBS (cypher x a)))
    | a <- as ]

lowhigh :: Word8 -> Word8 -> Word16
lowhigh a b = fromIntegral a + fromIntegral b * 2^8

writeTipToiFile :: TipToiFile -> B.ByteString
writeTipToiFile tt = runSPut (putTipToiFile tt)