tttool-0.1: 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.Strict
import Control.Monad.State.Strict
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
iro <- getAddress $ putInitialRegs ttInitialRegs
mft <- getAddress $ putAudioTable ttAudioXor ttAudioFiles
ipllo <- getAddress $ putOffsets putWord16 $ map putPlayList ttWelcome
return ()
putGameTable :: SPut
putGameTable = mdo
putWord32 1 -- Hardcoded empty
putWord32 offset
offset <- getAddress $ do
putWord16 253
putWord16 0
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 (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 (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 (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)