diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,6 @@
+# Change log
+
+Octane uses [Semantic Versioning][]. The change log is available [on GitHub][].
+
+[semantic versioning]: http://semver.org/spec/v2.0.0.html
+[on github]: https://github.com/tfausak/octane/releases
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Taylor Fausak <taylor@fausak.me>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+# [Octane][]
+
+[![Build badge]][build status]
+
+Octane parses [Rocket League][] replays.
+
+## Usage
+
+1.  Install [Stack][].
+
+2.  Install Octane with `stack build`.
+
+3.  Parse a replay with `stack exec octane path/to/the.replay`.
+
+[octane]: https://github.com/tfausak/octane
+[build badge]: https://img.shields.io/travis/tfausak/octane/main.svg
+[build status]: https://travis-ci.org/tfausak/octane
+[rocket league]: http://rocketleague.psyonix.com
+[stack]: http://haskellstack.org
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,4 @@
+import qualified Distribution.Simple
+
+main :: IO ()
+main = Distribution.Simple.defaultMain
diff --git a/executable/Executable.hs b/executable/Executable.hs
new file mode 100644
--- /dev/null
+++ b/executable/Executable.hs
@@ -0,0 +1,4 @@
+import qualified Octane
+
+main :: IO ()
+main = Octane.main
diff --git a/library/Octane.hs b/library/Octane.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane.hs
@@ -0,0 +1,4 @@
+module Octane (module Octane) where
+
+import Octane.Main as Octane
+import Octane.Types as Octane
diff --git a/library/Octane/Core.hs b/library/Octane/Core.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Core.hs
@@ -0,0 +1,17 @@
+module Octane.Core (module Octane.Core) where
+
+import Control.Monad as Octane.Core (replicateM, when)
+import Data.Binary as Octane.Core (Binary, Get, Put, decodeFileOrFail, encode, get, getWord8, put, putWord8)
+import Data.Binary.Get as Octane.Core (ByteOffset, getByteString, getWord32le, getWord64le)
+import Data.Binary.IEEE754 as Octane.Core (getFloat32le, putFloat32le)
+import Data.Binary.Put as Octane.Core (putByteString, putWord32le, putWord64le)
+import Data.ByteString as Octane.Core (ByteString)
+import Data.Char as Octane.Core (isLatin1)
+import Data.Function as Octane.Core ((&))
+import Data.Int as Octane.Core (Int32, Int64)
+import Data.IntMap as Octane.Core (IntMap)
+import Data.Map as Octane.Core (Map)
+import Data.Text as Octane.Core (Text)
+import Data.Text.Encoding as Octane.Core (decodeLatin1, decodeUtf16LE, encodeUtf16LE)
+import System.Environment as Octane.Core (getArgs)
+import System.IO as Octane.Core (hPutStrLn, stderr, stdout)
diff --git a/library/Octane/Main.hs b/library/Octane/Main.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Main.hs
@@ -0,0 +1,163 @@
+module Octane.Main (main) where
+
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as BSL
+import qualified Data.IntMap as IntMap
+import qualified Data.Map as Map
+import Octane.Core
+import Octane.Types
+
+main :: IO ()
+main = do
+    files <- getArgs
+    contents <- mapM BS.readFile files
+    results <- mapM decodeFileOrFail files
+    mapM_ debug (zip3 files contents results)
+
+debug :: (String, ByteString, Either (ByteOffset, String) Replay) -> IO ()
+debug (file, contents, result) = case result of
+    Left (offset, message) -> hPutStrLn stderr
+        (file ++ " @ byte " ++ show offset ++ " - " ++ message)
+    Right replay -> do
+        putStrLn file
+
+        let inputSize = contents & BS.length & fromIntegral
+        let outputSize = replay & encode & BSL.length
+        when (inputSize /= outputSize) $ do
+            hPutStrLn stderr
+                ( "input size ("
+                ++ show inputSize
+                ++ ") not equal to output size ("
+                ++ show outputSize ++ ")!"
+                )
+        putStrLn ""
+
+        putStrLn "# SIZE 1 #\n"
+        print (getInt32LE (replaySize1 replay))
+        putStrLn ""
+
+        putStrLn "# CRC 1 #\n"
+        print (getInt32LE (replayCRC1 replay))
+        putStrLn ""
+
+        putStrLn "# VERSION #\n"
+        print (getInt32LE (replayVersion1 replay))
+        print (getInt32LE (replayVersion2 replay))
+        putStrLn ""
+
+        putStrLn "# LABEL #\n"
+        print (getPCString (replayLabel replay))
+        putStrLn ""
+
+        putStrLn "# PROPERTIES #\n"
+        mapM_
+            (\ (name, property) -> do
+                putStr (show (getPCString name) ++ "\t=> ")
+                debugProperty property)
+            (Map.assocs (getTable (replayProperties replay)))
+        putStrLn ""
+
+        putStrLn "# SIZE 2 #\n"
+        print (getInt32LE (replaySize2 replay))
+        putStrLn ""
+
+        putStrLn "# CRC 2 #\n"
+        print (getInt32LE (replayCRC2 replay))
+        putStrLn ""
+
+        putStrLn "# EFFECTS #\n"
+        mapM_
+            (\ effect -> do
+                print (getPCString effect))
+            (getList (replayEffects replay))
+        putStrLn ""
+
+        putStrLn "# KEY FRAMES #\n"
+        mapM_
+            (\ keyFrame -> do
+                putStrLn (show (getFloat32LE (keyFrameTime keyFrame)) ++ "s @ frame " ++ show (getInt32LE (keyFrameFrame keyFrame)) ++ " - bit " ++ show (getInt32LE (keyFramePosition keyFrame))))
+            (getList (replayKeyFrames replay))
+        putStrLn ""
+
+        putStrLn "# FRAMES #\n"
+        putStrLn (show (BS.length (replayFrames replay)) ++ " bytes")
+        putStrLn ""
+
+        putStrLn "# MESSAGES #\n"
+        mapM_
+            (\ message -> do
+                putStrLn (show (getPCString (messageName message)) ++ " @ frame " ++ show (getInt32LE (messageFrame message)) ++ ": " ++ show (getPCString (messageContent message))))
+            (getList (replayMessages replay))
+        putStrLn ""
+
+        putStrLn "# MARKS #\n"
+        mapM_
+            (\ goal -> do
+                putStrLn (show (getPCString (markLabel goal)) ++ " @ frame " ++ show (getInt32LE (markFrame goal))))
+            (getList (replayMarks replay))
+        putStrLn ""
+
+        putStrLn "# PACKAGES #\n"
+        mapM_
+            (\ package -> do
+                print (getPCString package))
+            (getList (replayPackages replay))
+        putStrLn ""
+
+        putStrLn "# OBJECTS #\n"
+        mapM_
+            (\ (index, object) -> do
+                putStrLn (show index ++ "\t=> " ++ show (getPCString object)))
+            (IntMap.assocs (getObjectMap (replayObjectMap replay)))
+        putStrLn ""
+
+        putStrLn "# NAMES #\n"
+        mapM_
+            (\ name -> do
+                print (getPCString name))
+            (getList (replayNames replay))
+        putStrLn ""
+
+        putStrLn "# ACTORS #\n"
+        mapM_
+            (\ (index, actor) -> do
+                putStrLn (show index ++ "\t=> " ++ show (getPCString actor)))
+            (IntMap.assocs (getActorMap (replayActorMap replay)))
+        putStrLn ""
+
+        putStrLn "# CACHE #\n"
+        mapM_
+            (\ cacheItem -> do
+                let a = cacheItem & cacheItemTag & getInt32LE & fromIntegral
+                let b = replay & replayObjectMap & getObjectMap & IntMap.lookup a & fmap getPCString
+                putStrLn ("ID:\t" ++ show a ++ " (" ++ show b ++ ")")
+                putStrLn ("Start:\t" ++ show (getInt32LE (cacheItemStart cacheItem)))
+                putStrLn ("End:\t" ++ show (getInt32LE (cacheItemEnd cacheItem)))
+                putStrLn "Properties:"
+                mapM_
+                    (\ cacheProperty -> do
+                        let c = cacheProperty & cachePropertyIndex & getInt32LE & fromIntegral
+                        let d = replay & replayObjectMap & getObjectMap & IntMap.lookup c & fmap getPCString
+                        putStrLn ("- " ++ show (getInt32LE (cachePropertyTag cacheProperty)) ++ "\t=> " ++ show c ++ " (" ++ show d ++ ")"))
+                    (getList (cacheItemCacheProperties cacheItem))
+                putStrLn "")
+            (getList (replayCacheItems replay))
+
+debugProperty :: Property -> IO ()
+debugProperty property = case property of
+    ArrayProperty _ (NewList array) -> do
+        putStrLn "[array]"
+        mapM_
+            (\ (NewTable table) -> mapM_
+                (\ (NewPCString k, v) -> do
+                    putStr ("\t" ++ show k ++ "\t=> ")
+                    debugProperty v)
+                (Map.assocs table) >> putStrLn "")
+            array
+    BoolProperty _ (NewBoolean value) -> print value
+    ByteProperty _ (NewPCString key, NewPCString value) -> putStrLn (show key ++ ": " ++ show value)
+    FloatProperty _ (NewFloat32LE value) -> print value
+    IntProperty _ (NewInt32LE value) -> print value
+    NameProperty _ (NewPCString value) -> putStrLn (show value ++ " [name]")
+    QWordProperty _ (NewInt64LE value) -> print value
+    StrProperty _ (NewPCString value) -> print value
diff --git a/library/Octane/Types.hs b/library/Octane/Types.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types.hs
@@ -0,0 +1,19 @@
+module Octane.Types (module Octane.Types) where
+
+import Octane.Types.Actor as Octane.Types
+import Octane.Types.ActorMap as Octane.Types
+import Octane.Types.Boolean as Octane.Types
+import Octane.Types.CacheItem as Octane.Types
+import Octane.Types.CacheProperty as Octane.Types
+import Octane.Types.Float32LE as Octane.Types
+import Octane.Types.Int32LE as Octane.Types
+import Octane.Types.Int64LE as Octane.Types
+import Octane.Types.KeyFrame as Octane.Types
+import Octane.Types.List as Octane.Types
+import Octane.Types.Mark as Octane.Types
+import Octane.Types.Message as Octane.Types
+import Octane.Types.ObjectMap as Octane.Types
+import Octane.Types.PCString as Octane.Types
+import Octane.Types.Property as Octane.Types
+import Octane.Types.Replay as Octane.Types
+import Octane.Types.Table as Octane.Types
diff --git a/library/Octane/Types/Actor.hs b/library/Octane/Types/Actor.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/Actor.hs
@@ -0,0 +1,23 @@
+module Octane.Types.Actor (Actor(..)) where
+
+import Octane.Core
+import Octane.Types.Int32LE
+import Octane.Types.PCString
+
+data Actor = NewActor
+    { actorName :: PCString
+    , actorTag :: Int32LE
+    } deriving (Show)
+
+instance Binary Actor where
+    get = do
+        name <- get
+        tag <- get
+        return NewActor
+            { actorName = name
+            , actorTag = tag
+            }
+
+    put actor = do
+        actor & actorName & put
+        actor & actorTag & put
diff --git a/library/Octane/Types/ActorMap.hs b/library/Octane/Types/ActorMap.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/ActorMap.hs
@@ -0,0 +1,32 @@
+module Octane.Types.ActorMap (ActorMap(..)) where
+
+import qualified Data.IntMap as IntMap
+import Octane.Core
+import Octane.Types.Actor
+import Octane.Types.Int32LE
+import Octane.Types.List
+import Octane.Types.PCString
+
+newtype ActorMap = NewActorMap
+    { getActorMap :: IntMap.IntMap PCString
+    } deriving (Show)
+
+instance Binary ActorMap where
+    get = do
+        (NewList actors) <- get
+        actors & map toTuple & IntMap.fromList & NewActorMap & return
+
+    put (NewActorMap actors) = do
+        actors & IntMap.assocs & map fromTuple & NewList & put
+
+toTuple :: Actor -> (Int, PCString)
+toTuple actor =
+    ( actor & actorTag & getInt32LE & fromIntegral
+    , actor & actorName
+    )
+
+fromTuple :: (Int, PCString) -> Actor
+fromTuple (tag, name) = NewActor
+    { actorName = name
+    , actorTag = tag & fromIntegral & NewInt32LE
+    }
diff --git a/library/Octane/Types/Boolean.hs b/library/Octane/Types/Boolean.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/Boolean.hs
@@ -0,0 +1,17 @@
+module Octane.Types.Boolean (Boolean(..)) where
+
+import Octane.Core
+
+newtype Boolean = NewBoolean
+    { getBoolean :: Bool
+    } deriving (Eq, Show)
+
+instance Binary Boolean where
+    get = do
+        boolean <- getWord8
+        if boolean > 1
+        then fail "out of bounds"
+        else boolean & fromIntegral & toEnum & NewBoolean & return
+
+    put (NewBoolean boolean) = do
+        boolean & fromEnum & fromIntegral & putWord8
diff --git a/library/Octane/Types/CacheItem.hs b/library/Octane/Types/CacheItem.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/CacheItem.hs
@@ -0,0 +1,32 @@
+module Octane.Types.CacheItem (CacheItem(..)) where
+
+import Octane.Core
+import Octane.Types.CacheProperty
+import Octane.Types.Int32LE
+import Octane.Types.List
+
+data CacheItem = NewCacheItem
+    { cacheItemTag :: Int32LE
+    , cacheItemStart :: Int32LE
+    , cacheItemEnd :: Int32LE
+    , cacheItemCacheProperties :: List CacheProperty
+    } deriving (Show)
+
+instance Binary CacheItem where
+    get = do
+        tag <- get
+        start <- get
+        end <- get
+        cacheProperties <- get
+        return NewCacheItem
+            { cacheItemTag = tag
+            , cacheItemStart = start
+            , cacheItemEnd = end
+            , cacheItemCacheProperties = cacheProperties
+            }
+
+    put cacheItem = do
+        cacheItem & cacheItemTag & put
+        cacheItem & cacheItemStart & put
+        cacheItem & cacheItemEnd & put
+        cacheItem & cacheItemCacheProperties & put
diff --git a/library/Octane/Types/CacheProperty.hs b/library/Octane/Types/CacheProperty.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/CacheProperty.hs
@@ -0,0 +1,22 @@
+module Octane.Types.CacheProperty (CacheProperty(..)) where
+
+import Octane.Core
+import Octane.Types.Int32LE
+
+data CacheProperty = NewCacheProperty
+    { cachePropertyIndex :: Int32LE
+    , cachePropertyTag :: Int32LE
+    } deriving (Show)
+
+instance Binary CacheProperty where
+    get = do
+        index <- get
+        tag <- get
+        return NewCacheProperty
+            { cachePropertyIndex = index
+            , cachePropertyTag = tag
+            }
+
+    put cacheProperty = do
+        cacheProperty & cachePropertyIndex & put
+        cacheProperty & cachePropertyTag & put
diff --git a/library/Octane/Types/Float32LE.hs b/library/Octane/Types/Float32LE.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/Float32LE.hs
@@ -0,0 +1,15 @@
+module Octane.Types.Float32LE (Float32LE(..)) where
+
+import Octane.Core
+
+newtype Float32LE = NewFloat32LE
+    { getFloat32LE :: Float
+    } deriving (Show)
+
+instance Binary Float32LE where
+    get = do
+        float <- getFloat32le
+        float & NewFloat32LE & return
+
+    put (NewFloat32LE float) = do
+        float & putFloat32le
diff --git a/library/Octane/Types/Int32LE.hs b/library/Octane/Types/Int32LE.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/Int32LE.hs
@@ -0,0 +1,15 @@
+module Octane.Types.Int32LE (Int32LE(..)) where
+
+import Octane.Core
+
+newtype Int32LE = NewInt32LE
+    { getInt32LE :: Int32
+    } deriving (Show)
+
+instance Binary Int32LE where
+    get = do
+        int <- getWord32le
+        int & fromIntegral & NewInt32LE & return
+
+    put (NewInt32LE int) = do
+        int & fromIntegral & putWord32le
diff --git a/library/Octane/Types/Int64LE.hs b/library/Octane/Types/Int64LE.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/Int64LE.hs
@@ -0,0 +1,15 @@
+module Octane.Types.Int64LE (Int64LE(..)) where
+
+import Octane.Core
+
+newtype Int64LE = NewInt64LE
+    { getInt64LE :: Int64
+    } deriving (Show)
+
+instance Binary Int64LE where
+    get = do
+        int <- getWord64le
+        int & fromIntegral & NewInt64LE & return
+
+    put (NewInt64LE int) = do
+        int & fromIntegral & putWord64le
diff --git a/library/Octane/Types/KeyFrame.hs b/library/Octane/Types/KeyFrame.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/KeyFrame.hs
@@ -0,0 +1,27 @@
+module Octane.Types.KeyFrame (KeyFrame(..)) where
+
+import Octane.Core
+import Octane.Types.Float32LE
+import Octane.Types.Int32LE
+
+data KeyFrame = NewKeyFrame
+    { keyFrameTime :: Float32LE
+    , keyFrameFrame :: Int32LE
+    , keyFramePosition :: Int32LE
+    } deriving (Show)
+
+instance Binary KeyFrame where
+    get = do
+        time <- get
+        frame <- get
+        position <- get
+        return NewKeyFrame
+            { keyFrameTime = time
+            , keyFrameFrame = frame
+            , keyFramePosition = position
+            }
+
+    put keyFrame = do
+        keyFrame & keyFrameTime & put
+        keyFrame & keyFrameFrame & put
+        keyFrame & keyFramePosition & put
diff --git a/library/Octane/Types/List.hs b/library/Octane/Types/List.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/List.hs
@@ -0,0 +1,18 @@
+module Octane.Types.List (List(..)) where
+
+import Octane.Core
+import Octane.Types.Int32LE
+
+newtype List a = NewList
+    { getList :: [a]
+    } deriving (Show)
+
+instance (Binary a) => Binary (List a) where
+    get = do
+        (NewInt32LE size) <- get
+        elements <- replicateM (fromIntegral size) get
+        elements & NewList & return
+
+    put (NewList list) = do
+        list & length & fromIntegral & NewInt32LE & put
+        list & mapM_ put
diff --git a/library/Octane/Types/Mark.hs b/library/Octane/Types/Mark.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/Mark.hs
@@ -0,0 +1,23 @@
+module Octane.Types.Mark (Mark(..)) where
+
+import Octane.Core
+import Octane.Types.Int32LE
+import Octane.Types.PCString
+
+data Mark = NewMark
+    { markLabel :: PCString
+    , markFrame :: Int32LE
+    } deriving (Show)
+
+instance Binary Mark where
+    get = do
+        label <- get
+        frame <- get
+        return NewMark
+            { markLabel = label
+            , markFrame = frame
+            }
+
+    put mark = do
+        mark & markLabel & put
+        mark & markFrame & put
diff --git a/library/Octane/Types/Message.hs b/library/Octane/Types/Message.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/Message.hs
@@ -0,0 +1,27 @@
+module Octane.Types.Message (Message(..)) where
+
+import Octane.Core
+import Octane.Types.Int32LE
+import Octane.Types.PCString
+
+data Message = NewMessage
+    { messageFrame :: Int32LE
+    , messageName :: PCString
+    , messageContent :: PCString
+    } deriving (Show)
+
+instance Binary Message where
+    get = do
+        frame <- get
+        name <- get
+        content <- get
+        return NewMessage
+            { messageFrame = frame
+            , messageName = name
+            , messageContent = content
+            }
+
+    put message = do
+        message & messageFrame & put
+        message & messageName & put
+        message & messageContent & put
diff --git a/library/Octane/Types/ObjectMap.hs b/library/Octane/Types/ObjectMap.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/ObjectMap.hs
@@ -0,0 +1,18 @@
+module Octane.Types.ObjectMap (ObjectMap(..)) where
+
+import qualified Data.IntMap as IntMap
+import Octane.Core
+import Octane.Types.List
+import Octane.Types.PCString
+
+newtype ObjectMap = NewObjectMap
+    { getObjectMap :: IntMap PCString
+    } deriving (Show)
+
+instance Binary ObjectMap where
+    get = do
+        NewList objects <- get
+        objects & zip [0 ..] & IntMap.fromAscList & NewObjectMap & return
+
+    put (NewObjectMap objects) = do
+        objects & IntMap.elems & NewList & put
diff --git a/library/Octane/Types/PCString.hs b/library/Octane/Types/PCString.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/PCString.hs
@@ -0,0 +1,37 @@
+module Octane.Types.PCString (PCString(..)) where
+
+import qualified Data.ByteString.Char8 as BS8
+import qualified Data.Text as Text
+import Octane.Core
+import Octane.Types.Int32LE
+
+newtype PCString = NewPCString
+    { getPCString :: Text
+    } deriving (Eq, Ord, Show)
+
+instance Binary PCString where
+    get = do
+        (NewInt32LE size) <- get
+        string <- if size < 0
+            then do
+                let actualSize = 2 * negate size
+                bytes <- getByteString (fromIntegral actualSize)
+                bytes & decodeUtf16LE & return
+            else do
+                bytes <- getByteString (fromIntegral size)
+                bytes & decodeLatin1 & return
+        string & Text.dropEnd 1 & NewPCString & return
+
+    put (NewPCString string) = do
+        let cString = Text.snoc string '\NUL'
+        let size = cString & Text.length & fromIntegral
+        if Text.all isLatin1 cString
+        then do
+            size & NewInt32LE & put
+            cString & encodeLatin1 & putByteString
+        else do
+            size & negate & NewInt32LE & put
+            cString & encodeUtf16LE & putByteString
+
+encodeLatin1 :: Text -> ByteString
+encodeLatin1 text = text & Text.unpack & BS8.pack
diff --git a/library/Octane/Types/Property.hs b/library/Octane/Types/Property.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/Property.hs
@@ -0,0 +1,96 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Octane.Types.Property (Property(..)) where
+
+import Octane.Core
+import Octane.Types.Boolean
+import Octane.Types.Float32LE
+import Octane.Types.Int32LE
+import Octane.Types.Int64LE
+import Octane.Types.List
+import Octane.Types.PCString
+import Octane.Types.Table
+
+data Property
+    = ArrayProperty Int64LE (List (Table Property))
+    | BoolProperty Int64LE Boolean
+    | ByteProperty Int64LE (PCString, PCString)
+    | FloatProperty Int64LE Float32LE
+    | IntProperty Int64LE Int32LE
+    | NameProperty Int64LE PCString
+    | QWordProperty Int64LE Int64LE
+    | StrProperty Int64LE PCString
+    deriving (Show)
+
+instance Binary Property where
+    get = do
+        kind <- get
+        size <- get
+        case getPCString kind of
+            "ArrayProperty" -> do
+                value <- get
+                ArrayProperty size value & return
+            "BoolProperty" -> do
+                value <- get
+                BoolProperty size value & return
+            "ByteProperty" -> do
+                key <- get
+                value <- get
+                ByteProperty size (key, value) & return
+            "FloatProperty" -> do
+                value <- case size of
+                    NewInt64LE 4 -> get
+                    _ -> fail ("unknown FloatProperty size " ++ show size)
+                FloatProperty size value & return
+            "IntProperty" -> do
+                value <- case size of
+                    NewInt64LE 4 -> get
+                    _ -> fail ("unknown IntProperty size " ++ show size)
+                IntProperty size value & return
+            "NameProperty" -> do
+                value <- get
+                NameProperty size value & return
+            "StrProperty" -> do
+                value <- get
+                StrProperty size value & return
+            "QWordProperty" -> do
+                value <- case size of
+                    NewInt64LE 8 -> get
+                    _ -> fail ("unknown QWordProperty size " ++ show size)
+                QWordProperty size value & return
+            _ -> fail ("unknown property type " ++ show kind)
+
+    put property = case property of
+        ArrayProperty size value -> do
+            "ArrayProperty" & NewPCString & put
+            size & put
+            value & put
+        BoolProperty size value -> do
+            "BoolProperty" & NewPCString & put
+            size & put
+            value & put
+        ByteProperty size (key, value) -> do
+            "ByteProperty" & NewPCString & put
+            size & put
+            key & put
+            value & put
+        FloatProperty size value -> do
+            "FloatProperty" & NewPCString & put
+            size & put
+            value & put
+        IntProperty size value -> do
+            "IntProperty" & NewPCString & put
+            size & put
+            value & put
+        NameProperty size value -> do
+            "NameProperty" & NewPCString & put
+            size & put
+            value & put
+        QWordProperty size value -> do
+            "QWordProperty" & NewPCString & put
+            size & put
+            value & put
+        StrProperty size value -> do
+            "StrProperty" & NewPCString & put
+            size & put
+            value & put
diff --git a/library/Octane/Types/Replay.hs b/library/Octane/Types/Replay.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/Replay.hs
@@ -0,0 +1,108 @@
+module Octane.Types.Replay (Replay(..)) where
+
+import qualified Data.ByteString as BS
+import Octane.Core
+import Octane.Types.ActorMap
+import Octane.Types.CacheItem
+import Octane.Types.Int32LE
+import Octane.Types.KeyFrame
+import Octane.Types.List
+import Octane.Types.Mark
+import Octane.Types.Message
+import Octane.Types.ObjectMap
+import Octane.Types.PCString
+import Octane.Types.Property
+import Octane.Types.Table
+
+data Replay = NewReplay
+    { replaySize1 :: Int32LE
+    , replayCRC1 :: Int32LE
+    , replayVersion1 :: Int32LE
+    , replayVersion2 :: Int32LE
+    , replayLabel :: PCString
+    , replayProperties :: Table Property
+    , replaySize2 :: Int32LE
+    , replayCRC2 :: Int32LE
+    , replayEffects :: List PCString
+    , replayKeyFrames :: List KeyFrame
+    , replayFrames :: ByteString
+    , replayMessages :: List Message
+    , replayMarks :: List Mark
+    , replayPackages :: List PCString
+    , replayObjectMap :: ObjectMap
+    , replayNames :: List PCString
+    , replayActorMap :: ActorMap
+    , replayCacheItems :: List CacheItem
+    } deriving (Show)
+
+instance Binary Replay where
+    get = do
+        size1 <- get
+        crc1 <- get
+        version1 <- get
+        version2 <- get
+        label <- get
+        properties <- get
+        size2 <- get
+        crc2 <- get
+        effects <- get
+        keyFrames <- get
+        frames <- getFrameBytes
+        messages <- get
+        marks <- get
+        packages <- get
+        objectMap <- get
+        names <- get
+        actorMap <- get
+        cacheItems <- get
+        return NewReplay
+            { replaySize1 = size1
+            , replayCRC1 = crc1
+            , replayVersion1 = version1
+            , replayVersion2 = version2
+            , replayLabel = label
+            , replayProperties = properties
+            , replaySize2 = size2
+            , replayCRC2 = crc2
+            , replayEffects = effects
+            , replayKeyFrames = keyFrames
+            , replayFrames = frames
+            , replayMessages = messages
+            , replayMarks = marks
+            , replayPackages = packages
+            , replayObjectMap = objectMap
+            , replayNames = names
+            , replayActorMap = actorMap
+            , replayCacheItems = cacheItems
+            }
+
+    put replay = do
+        replay & replaySize1 & put
+        replay & replayCRC1 & put
+        replay & replayVersion1 & put
+        replay & replayVersion2 & put
+        replay & replayLabel & put
+        replay & replayProperties & put
+        replay & replaySize2 & put
+        replay & replayCRC2 & put
+        replay & replayEffects & put
+        replay & replayKeyFrames & put
+        replay & replayFrames & putFrameBytes
+        replay & replayMessages & put
+        replay & replayMarks & put
+        replay & replayPackages & put
+        replay & replayObjectMap & put
+        replay & replayNames & put
+        replay & replayActorMap & put
+        replay & replayCacheItems & put
+
+getFrameBytes :: Get ByteString
+getFrameBytes = do
+    NewInt32LE size <- get
+    frames <- getByteString (fromIntegral size)
+    return frames
+
+putFrameBytes :: ByteString -> Put
+putFrameBytes frames = do
+    frames & BS.length & fromIntegral & NewInt32LE & put
+    frames & putByteString
diff --git a/library/Octane/Types/Table.hs b/library/Octane/Types/Table.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Types/Table.hs
@@ -0,0 +1,41 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Octane.Types.Table (Table(..)) where
+
+import qualified Data.Map as Map
+import Octane.Core
+import Octane.Types.PCString
+
+newtype Table a = NewTable
+    { getTable :: Map PCString a
+    } deriving (Show)
+
+instance (Binary a) => Binary (Table a) where
+    get = do
+        row <- getRow
+        if Map.null row
+        then do
+            row & NewTable & return
+        else do
+            NewTable rows <- get
+            rows & Map.union row & NewTable & return
+
+    put (NewTable rows) = do
+        rows & Map.assocs & mapM_ putRow
+        "None" & NewPCString & put
+
+getRow :: (Binary a) => Get (Map PCString a)
+getRow = do
+    key <- get
+    if key == NewPCString "None"
+    then do
+        return Map.empty
+    else do
+        value <- get
+        return (Map.singleton key value)
+
+
+putRow :: (Binary a) => (PCString, a) -> Put
+putRow (key, value) = do
+    key & put
+    value & put
diff --git a/octane.cabal b/octane.cabal
new file mode 100644
--- /dev/null
+++ b/octane.cabal
@@ -0,0 +1,99 @@
+name: octane
+version: 0.1.0
+cabal-version: >=1.10
+build-type: Simple
+license: MIT
+license-file: LICENSE.md
+maintainer: Taylor Fausak
+homepage: https://github.com/tfausak/octane
+synopsis: A Rocket League replay parser.
+description:
+    <https://github.com/tfausak/octane#readme>
+category: Game
+extra-source-files:
+    CHANGELOG.md
+    README.md
+    stack.yaml
+
+source-repository head
+    type: git
+    location: https://github.com/tfausak/octane
+
+library
+    exposed-modules:
+        Octane
+        Octane.Main
+        Octane.Types
+        Octane.Types.Actor
+        Octane.Types.ActorMap
+        Octane.Types.Boolean
+        Octane.Types.CacheItem
+        Octane.Types.CacheProperty
+        Octane.Types.Float32LE
+        Octane.Types.Int32LE
+        Octane.Types.Int64LE
+        Octane.Types.KeyFrame
+        Octane.Types.List
+        Octane.Types.Mark
+        Octane.Types.Message
+        Octane.Types.ObjectMap
+        Octane.Types.PCString
+        Octane.Types.Property
+        Octane.Types.Replay
+        Octane.Types.Table
+    build-depends:
+        base ==4.8.*,
+        binary ==0.7.*,
+        bytestring ==0.10.*,
+        containers ==0.5.*,
+        data-binary-ieee754 ==0.4.*,
+        text ==1.2.*
+    default-language: Haskell2010
+    hs-source-dirs: library
+    other-modules:
+        Octane.Core
+    ghc-options: -Wall
+
+executable octane
+    main-is: Executable.hs
+    build-depends:
+        base -any,
+        octane -any
+    default-language: Haskell2010
+    hs-source-dirs: executable
+    ghc-options: -threaded -Wall
+
+test-suite octane-test-suite
+    type: exitcode-stdio-1.0
+    main-is: TestSuite.hs
+    build-depends:
+        base -any,
+        binary -any,
+        bytestring -any,
+        octane -any,
+        tasty ==0.11.*,
+        tasty-hspec ==1.1.*
+    default-language: Haskell2010
+    hs-source-dirs: test-suite
+    other-modules:
+        OctaneSpec
+        Octane.MainSpec
+        Octane.TypesSpec
+        Octane.Types.ActorSpec
+        Octane.Types.ActorMapSpec
+        Octane.Types.BooleanSpec
+        Octane.Types.CacheItemSpec
+        Octane.Types.CachePropertySpec
+        Octane.Types.Float32LESpec
+        Octane.Types.Int32LESpec
+        Octane.Types.Int64LESpec
+        Octane.Types.KeyFrameSpec
+        Octane.Types.ListSpec
+        Octane.Types.MarkSpec
+        Octane.Types.MessageSpec
+        Octane.Types.ObjectMapSpec
+        Octane.Types.PCStringSpec
+        Octane.Types.PropertySpec
+        Octane.Types.ReplaySpec
+        Octane.Types.TableSpec
+    ghc-options: -threaded -Wall
diff --git a/stack.yaml b/stack.yaml
new file mode 100644
--- /dev/null
+++ b/stack.yaml
@@ -0,0 +1,4 @@
+install-ghc: true
+packages: [.]
+require-stack-version: '>=1.0'
+resolver: nightly-2016-01-30
diff --git a/test-suite/Octane/MainSpec.hs b/test-suite/Octane/MainSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/MainSpec.hs
@@ -0,0 +1,7 @@
+module Octane.MainSpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "Main" $ do
+    return ()
diff --git a/test-suite/Octane/Types/ActorMapSpec.hs b/test-suite/Octane/Types/ActorMapSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/ActorMapSpec.hs
@@ -0,0 +1,7 @@
+module Octane.Types.ActorMapSpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "ActorMap" $ do
+    return ()
diff --git a/test-suite/Octane/Types/ActorSpec.hs b/test-suite/Octane/Types/ActorSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/ActorSpec.hs
@@ -0,0 +1,7 @@
+module Octane.Types.ActorSpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "Actor" $ do
+    return ()
diff --git a/test-suite/Octane/Types/BooleanSpec.hs b/test-suite/Octane/Types/BooleanSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/BooleanSpec.hs
@@ -0,0 +1,25 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Octane.Types.BooleanSpec (spec) where
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as BSL
+import qualified Octane as Octane
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "Boolean" $ do
+    it "can be decoded" $ do
+        decodeBoolean "\0" `shouldBe` Right ("", 1, Octane.NewBoolean False)
+        decodeBoolean "\1" `shouldBe` Right ("", 1, Octane.NewBoolean True)
+    it "can be encoded" $ do
+        Binary.encode (Octane.NewBoolean False) `shouldBe` "\0"
+        Binary.encode (Octane.NewBoolean True) `shouldBe` "\1"
+    it "handles extra data when decoding" $ do
+        decodeBoolean "\0extra" `shouldBe` Right ("extra", 1, Octane.NewBoolean False)
+    it "does not raise a runtime error when decoding garbage" $ do
+        decodeBoolean "garbage" `shouldBe` Left ("arbage", 1, "out of bounds")
+
+decodeBoolean :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Octane.Boolean)
+decodeBoolean = Binary.decodeOrFail
diff --git a/test-suite/Octane/Types/CacheItemSpec.hs b/test-suite/Octane/Types/CacheItemSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/CacheItemSpec.hs
@@ -0,0 +1,7 @@
+module Octane.Types.CacheItemSpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "CacheItem" $ do
+    return ()
diff --git a/test-suite/Octane/Types/CachePropertySpec.hs b/test-suite/Octane/Types/CachePropertySpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/CachePropertySpec.hs
@@ -0,0 +1,7 @@
+module Octane.Types.CachePropertySpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "CacheProperty" $ do
+    return ()
diff --git a/test-suite/Octane/Types/Float32LESpec.hs b/test-suite/Octane/Types/Float32LESpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/Float32LESpec.hs
@@ -0,0 +1,7 @@
+module Octane.Types.Float32LESpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "Float32LE" $ do
+    return ()
diff --git a/test-suite/Octane/Types/Int32LESpec.hs b/test-suite/Octane/Types/Int32LESpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/Int32LESpec.hs
@@ -0,0 +1,7 @@
+module Octane.Types.Int32LESpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "Int32LE" $ do
+    return ()
diff --git a/test-suite/Octane/Types/Int64LESpec.hs b/test-suite/Octane/Types/Int64LESpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/Int64LESpec.hs
@@ -0,0 +1,7 @@
+module Octane.Types.Int64LESpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "Int64LE" $ do
+    return ()
diff --git a/test-suite/Octane/Types/KeyFrameSpec.hs b/test-suite/Octane/Types/KeyFrameSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/KeyFrameSpec.hs
@@ -0,0 +1,7 @@
+module Octane.Types.KeyFrameSpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "KeyFrame" $ do
+    return ()
diff --git a/test-suite/Octane/Types/ListSpec.hs b/test-suite/Octane/Types/ListSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/ListSpec.hs
@@ -0,0 +1,7 @@
+module Octane.Types.ListSpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "ListSpec" $ do
+    return ()
diff --git a/test-suite/Octane/Types/MarkSpec.hs b/test-suite/Octane/Types/MarkSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/MarkSpec.hs
@@ -0,0 +1,7 @@
+module Octane.Types.MarkSpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "Mark" $ do
+    return ()
diff --git a/test-suite/Octane/Types/MessageSpec.hs b/test-suite/Octane/Types/MessageSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/MessageSpec.hs
@@ -0,0 +1,7 @@
+module Octane.Types.MessageSpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "Message" $ do
+    return ()
diff --git a/test-suite/Octane/Types/ObjectMapSpec.hs b/test-suite/Octane/Types/ObjectMapSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/ObjectMapSpec.hs
@@ -0,0 +1,7 @@
+module Octane.Types.ObjectMapSpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "ObjectMap" $ do
+    return ()
diff --git a/test-suite/Octane/Types/PCStringSpec.hs b/test-suite/Octane/Types/PCStringSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/PCStringSpec.hs
@@ -0,0 +1,7 @@
+module Octane.Types.PCStringSpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "PCString" $ do
+    return ()
diff --git a/test-suite/Octane/Types/PropertySpec.hs b/test-suite/Octane/Types/PropertySpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/PropertySpec.hs
@@ -0,0 +1,7 @@
+module Octane.Types.PropertySpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "Property" $ do
+    return ()
diff --git a/test-suite/Octane/Types/ReplaySpec.hs b/test-suite/Octane/Types/ReplaySpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/ReplaySpec.hs
@@ -0,0 +1,7 @@
+module Octane.Types.ReplaySpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "Replay" $ do
+    return ()
diff --git a/test-suite/Octane/Types/TableSpec.hs b/test-suite/Octane/Types/TableSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/Types/TableSpec.hs
@@ -0,0 +1,7 @@
+module Octane.Types.TableSpec (spec) where
+
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "Table" $ do
+    return ()
diff --git a/test-suite/Octane/TypesSpec.hs b/test-suite/Octane/TypesSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/TypesSpec.hs
@@ -0,0 +1,40 @@
+module Octane.TypesSpec (spec) where
+
+import qualified Octane.Types.ActorSpec as ActorSpec
+import qualified Octane.Types.ActorMapSpec as ActorMapSpec
+import qualified Octane.Types.BooleanSpec as BooleanSpec
+import qualified Octane.Types.CacheItemSpec as CacheItemSpec
+import qualified Octane.Types.CachePropertySpec as CachePropertySpec
+import qualified Octane.Types.Float32LESpec as Float32LESpec
+import qualified Octane.Types.Int32LESpec as Int32LESpec
+import qualified Octane.Types.Int64LESpec as Int64LESpec
+import qualified Octane.Types.KeyFrameSpec as KeyFrameSpec
+import qualified Octane.Types.ListSpec as ListSpec
+import qualified Octane.Types.MarkSpec as MarkSpec
+import qualified Octane.Types.MessageSpec as MessageSpec
+import qualified Octane.Types.ObjectMapSpec as ObjectMapSpec
+import qualified Octane.Types.PCStringSpec as PCStringSpec
+import qualified Octane.Types.PropertySpec as PropertySpec
+import qualified Octane.Types.ReplaySpec as ReplaySpec
+import qualified Octane.Types.TableSpec as TableSpec
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "Types" $ do
+    ActorSpec.spec
+    ActorMapSpec.spec
+    BooleanSpec.spec
+    CacheItemSpec.spec
+    CachePropertySpec.spec
+    Float32LESpec.spec
+    Int32LESpec.spec
+    Int64LESpec.spec
+    KeyFrameSpec.spec
+    ListSpec.spec
+    MarkSpec.spec
+    MessageSpec.spec
+    ObjectMapSpec.spec
+    PCStringSpec.spec
+    PropertySpec.spec
+    ReplaySpec.spec
+    TableSpec.spec
diff --git a/test-suite/OctaneSpec.hs b/test-suite/OctaneSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/OctaneSpec.hs
@@ -0,0 +1,10 @@
+module OctaneSpec (spec) where
+
+import qualified Octane.MainSpec as MainSpec
+import qualified Octane.TypesSpec as TypesSpec
+import Test.Tasty.Hspec
+
+spec :: Spec
+spec = describe "Octane" $ do
+    MainSpec.spec
+    TypesSpec.spec
diff --git a/test-suite/TestSuite.hs b/test-suite/TestSuite.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/TestSuite.hs
@@ -0,0 +1,11 @@
+import qualified OctaneSpec as OctaneSpec
+import qualified Test.Tasty
+import Test.Tasty.Hspec
+
+main :: IO ()
+main = do
+    test <- testSpec "octane" spec
+    Test.Tasty.defaultMain test
+
+spec :: Spec
+spec = parallel OctaneSpec.spec
