diff --git a/Setup.hs b/Setup.hs
--- a/Setup.hs
+++ b/Setup.hs
@@ -1,4 +1,5 @@
 import qualified Distribution.Simple
 
+
 main :: IO ()
 main = Distribution.Simple.defaultMain
diff --git a/benchmark/Benchmark.hs b/benchmark/Benchmark.hs
deleted file mode 100644
--- a/benchmark/Benchmark.hs
+++ /dev/null
@@ -1,5 +0,0 @@
-import qualified Criterion.Main
-import qualified OctaneBench
-
-main :: IO ()
-main = Criterion.Main.defaultMain [OctaneBench.benchmarks]
diff --git a/benchmark/Main.hs b/benchmark/Main.hs
new file mode 100644
--- /dev/null
+++ b/benchmark/Main.hs
@@ -0,0 +1,14 @@
+module Main (main) where
+
+import qualified Criterion.Main as Criterion
+import qualified OctaneBench
+
+
+main :: IO ()
+main = Criterion.defaultMain bench
+
+
+bench :: [Criterion.Benchmark]
+bench =
+    [ OctaneBench.bench
+    ]
diff --git a/benchmark/Octane/MainBench.hs b/benchmark/Octane/MainBench.hs
deleted file mode 100644
--- a/benchmark/Octane/MainBench.hs
+++ /dev/null
@@ -1,8 +0,0 @@
-module Octane.MainBench (benchmarks) where
-
-import Criterion
-
-benchmarks :: Benchmark
-benchmarks = bgroup "Main"
-    [
-    ]
diff --git a/benchmark/Octane/Type/ActorBench.hs b/benchmark/Octane/Type/ActorBench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/ActorBench.hs
+++ /dev/null
@@ -1,23 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.ActorBench (benchmarks) where
-
-import Criterion
-import Octane
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-benchmarks :: Benchmark
-benchmarks = bgroup "Actor"
-    [ bench "decode basic" (nf decodeActor "\
-        \\1\0\0\0\0\
-        \\0\0\0\0")
-    , bench "encode basic" (nf Binary.encode (Actor
-        (Text "")
-        (Int32 0)))
-    ]
-
-decodeActor :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Actor)
-decodeActor = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/CacheItemBench.hs b/benchmark/Octane/Type/CacheItemBench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/CacheItemBench.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.CacheItemBench (benchmarks) where
-
-import Criterion
-import Octane
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-benchmarks :: Benchmark
-benchmarks = bgroup "CacheItem"
-    [ bench "decode basic" (nf decodeCacheItem "\
-        \\0\0\0\0\
-        \\0\0\0\0\
-        \\0\0\0\0\
-        \\0\0\0\0")
-    , bench "encode basic" (nf Binary.encode (CacheItem
-        (Int32 0)
-        (Int32 0)
-        (Int32 0)
-        (List [])))
-    ]
-
-decodeCacheItem :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, CacheItem)
-decodeCacheItem = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/CachePropertyBench.hs b/benchmark/Octane/Type/CachePropertyBench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/CachePropertyBench.hs
+++ /dev/null
@@ -1,23 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.CachePropertyBench (benchmarks) where
-
-import Criterion
-import Octane
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-benchmarks :: Benchmark
-benchmarks = bgroup "CacheProperty"
-    [ bench "decode basic" (nf decodeCacheProperty "\
-        \\0\0\0\0\
-        \\0\0\0\0")
-    , bench "encode basic" (nf Binary.encode (CacheProperty
-        (Int32 0)
-        (Int32 0)))
-    ]
-
-decodeCacheProperty :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, CacheProperty)
-decodeCacheProperty = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/KeyFrameBench.hs b/benchmark/Octane/Type/KeyFrameBench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/KeyFrameBench.hs
+++ /dev/null
@@ -1,25 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.KeyFrameBench (benchmarks) where
-
-import Criterion
-import Octane
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-benchmarks :: Benchmark
-benchmarks = bgroup "KeyFrame"
-    [ bench "decode basic" (nf decodeKeyFrame "\
-        \\0\0\0\0\
-        \\0\0\0\0\
-        \\0\0\0\0")
-    , bench "encode basic" (nf Binary.encode (KeyFrame
-        (Float32 0.0)
-        (Int32 0)
-        (Int32 0)))
-    ]
-
-decodeKeyFrame :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, KeyFrame)
-decodeKeyFrame = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/MarkBench.hs b/benchmark/Octane/Type/MarkBench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/MarkBench.hs
+++ /dev/null
@@ -1,23 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.MarkBench (benchmarks) where
-
-import Criterion
-import Octane
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-benchmarks :: Benchmark
-benchmarks = bgroup "Mark"
-    [ bench "decode basic" (nf decodeMark "\
-        \\1\0\0\0\0\
-        \\0\0\0\0")
-    , bench "encode basic" (nf Binary.encode (Mark
-        (Text "")
-        (Int32 0)))
-    ]
-
-decodeMark :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Mark)
-decodeMark = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/MessageBench.hs b/benchmark/Octane/Type/MessageBench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/MessageBench.hs
+++ /dev/null
@@ -1,25 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.MessageBench (benchmarks) where
-
-import Criterion
-import Octane
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-benchmarks :: Benchmark
-benchmarks = bgroup "Message"
-    [ bench "decode basic" (nf decodeMessage "\
-        \\0\0\0\0\
-        \\1\0\0\0\0\
-        \\1\0\0\0\0")
-    , bench "encode basic" (nf Binary.encode (Message
-        (Int32 0)
-        (Text "")
-        (Text "")))
-    ]
-
-decodeMessage :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Message)
-decodeMessage = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/Primitive/BooleanBench.hs b/benchmark/Octane/Type/Primitive/BooleanBench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/Primitive/BooleanBench.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.BooleanBench (benchmarks) where
-
-import Criterion
-import Octane
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-benchmarks :: Benchmark
-benchmarks = bgroup "Boolean"
-    [ bench "decode basic" (nf decodeBoolean "\0")
-    , bench "encode basic" (nf Binary.encode (Boolean False))
-    ]
-
-decodeBoolean :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Boolean)
-decodeBoolean = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/Primitive/DictionaryBench.hs b/benchmark/Octane/Type/Primitive/DictionaryBench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/Primitive/DictionaryBench.hs
+++ /dev/null
@@ -1,23 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.DictionaryBench (benchmarks) where
-
-import Criterion
-import Octane
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-import qualified Data.Map as Map
-
-benchmarks :: Benchmark
-benchmarks = bgroup "Dictionary"
-    [ bench "decode basic" (nf decodeBooleanDictionary "\5\0\0\0None\0")
-    , bench "encode basic" (nf Binary.encode (Dictionary Map.empty :: Dictionary Boolean))
-    ]
-
-decodeDictionary :: (Binary.Binary a) => BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Dictionary a)
-decodeDictionary = Binary.decodeOrFail
-
-decodeBooleanDictionary :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Dictionary Boolean)
-decodeBooleanDictionary = decodeDictionary
diff --git a/benchmark/Octane/Type/Primitive/Float32Bench.hs b/benchmark/Octane/Type/Primitive/Float32Bench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/Primitive/Float32Bench.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.Float32Bench (benchmarks) where
-
-import Criterion
-import Octane
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-benchmarks :: Benchmark
-benchmarks = bgroup "Float32"
-    [ bench "decode basic" (nf decodeFloat32 "\0\0\0\0")
-    , bench "encode basic" (nf Binary.encode (Float32 0.0))
-    ]
-
-decodeFloat32 :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Float32)
-decodeFloat32 = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/Primitive/Int32Bench.hs b/benchmark/Octane/Type/Primitive/Int32Bench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/Primitive/Int32Bench.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.Int32Bench (benchmarks) where
-
-import Criterion
-import Octane
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-benchmarks :: Benchmark
-benchmarks = bgroup "Int32"
-    [ bench "decode basic" (nf decodeInt32 "\0\0\0\0")
-    , bench "encode basic" (nf Binary.encode (Int32 0))
-    ]
-
-decodeInt32 :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Int32)
-decodeInt32 = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/Primitive/Int64Bench.hs b/benchmark/Octane/Type/Primitive/Int64Bench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/Primitive/Int64Bench.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.Int64Bench (benchmarks) where
-
-import Criterion
-import Octane
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-benchmarks :: Benchmark
-benchmarks = bgroup "Int64"
-    [ bench "decode basic" (nf decodeInt64 "\0\0\0\0")
-    , bench "encode basic" (nf Binary.encode (Int64 0))
-    ]
-
-decodeInt64 :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Int64)
-decodeInt64 = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/Primitive/ListBench.hs b/benchmark/Octane/Type/Primitive/ListBench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/Primitive/ListBench.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.ListBench (benchmarks) where
-
-import Criterion
-import Octane
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-benchmarks :: Benchmark
-benchmarks = bgroup "List"
-    [ bench "decode basic" (nf decodeBooleanList "\0\0\0\0")
-    , bench "encode basic" (nf Binary.encode (List [] :: List Boolean))
-    ]
-
-decodeList :: (Binary.Binary a) => BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, List a)
-decodeList = Binary.decodeOrFail
-
-decodeBooleanList :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, List Boolean)
-decodeBooleanList = decodeList
diff --git a/benchmark/Octane/Type/Primitive/StreamBench.hs b/benchmark/Octane/Type/Primitive/StreamBench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/Primitive/StreamBench.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.StreamBench (benchmarks) where
-
-import Criterion
-import Octane
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-benchmarks :: Benchmark
-benchmarks = bgroup "Stream"
-    [ bench "decode basic" (nf decodeStream "\0\0\0\0")
-    , bench "encode basic" (nf Binary.encode (Stream ""))
-    ]
-
-decodeStream :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Stream)
-decodeStream = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/Primitive/TextBench.hs b/benchmark/Octane/Type/Primitive/TextBench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/Primitive/TextBench.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.TextBench (benchmarks) where
-
-import Criterion
-import Octane
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-benchmarks :: Benchmark
-benchmarks = bgroup "Text"
-    [ bench "decode basic" (nf decodeText "\1\0\0\0\0")
-    , bench "encode basic" (nf Binary.encode (Text ""))
-    ]
-
-decodeText :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Text)
-decodeText = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/PrimitiveBench.hs b/benchmark/Octane/Type/PrimitiveBench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/PrimitiveBench.hs
+++ /dev/null
@@ -1,24 +0,0 @@
-module Octane.Type.PrimitiveBench (benchmarks) where
-
-import Criterion
-
-import qualified Octane.Type.Primitive.BooleanBench
-import qualified Octane.Type.Primitive.DictionaryBench
-import qualified Octane.Type.Primitive.Float32Bench
-import qualified Octane.Type.Primitive.ListBench
-import qualified Octane.Type.Primitive.TextBench
-import qualified Octane.Type.Primitive.StreamBench
-import qualified Octane.Type.Primitive.Int32Bench
-import qualified Octane.Type.Primitive.Int64Bench
-
-benchmarks :: Benchmark
-benchmarks = bgroup "Primitive"
-    [ Octane.Type.Primitive.BooleanBench.benchmarks
-    , Octane.Type.Primitive.DictionaryBench.benchmarks
-    , Octane.Type.Primitive.Float32Bench.benchmarks
-    , Octane.Type.Primitive.ListBench.benchmarks
-    , Octane.Type.Primitive.TextBench.benchmarks
-    , Octane.Type.Primitive.StreamBench.benchmarks
-    , Octane.Type.Primitive.Int32Bench.benchmarks
-    , Octane.Type.Primitive.Int64Bench.benchmarks
-    ]
diff --git a/benchmark/Octane/Type/PropertyBench.hs b/benchmark/Octane/Type/PropertyBench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/PropertyBench.hs
+++ /dev/null
@@ -1,90 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.PropertyBench (benchmarks) where
-
-import Criterion
-import Octane
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-benchmarks :: Benchmark
-benchmarks = bgroup "Property"
-    [ bgroup "Array"
-        [ bench "decode basic" (nf decodeProperty "\
-            \\14\0\0\0ArrayProperty\0\
-            \\0\0\0\0\0\0\0\0\
-            \\0\0\0\0")
-        , bench "encode basic" (nf Binary.encode (ArrayProperty
-            (Int64 0)
-            (List [])))
-        ]
-    , bgroup "Bool"
-        [ bench "decode basic" (nf decodeProperty "\
-            \\13\0\0\0BoolProperty\0\
-            \\0\0\0\0\0\0\0\0\
-            \\0")
-        , bench "encode basic" (nf Binary.encode (BoolProperty
-            (Int64 0)
-            (Boolean False)))
-        ]
-    , bgroup "Byte"
-        [ bench "decode basic" (nf decodeProperty "\
-            \\13\0\0\0ByteProperty\0\
-            \\0\0\0\0\0\0\0\0\
-            \\1\0\0\0\0\
-            \\1\0\0\0\0")
-        , bench "encode basic" (nf Binary.encode (ByteProperty
-            (Int64 0)
-            (Text "", Text "")))
-        ]
-    , bgroup "Float"
-        [ bench "decode basic" (nf decodeProperty "\
-            \\14\0\0\0FloatProperty\0\
-            \\4\0\0\0\0\0\0\0\
-            \\0\0\0\0")
-        , bench "encode basic" (nf Binary.encode (FloatProperty
-            (Int64 4)
-            (Float32 0.0)))
-        ]
-    , bgroup "Int"
-        [ bench "decode basic" (nf decodeProperty "\
-            \\12\0\0\0IntProperty\0\
-            \\4\0\0\0\0\0\0\0\
-            \\0\0\0\0")
-        , bench "encode basic" (nf Binary.encode (IntProperty
-            (Int64 4)
-            (Int32 0)))
-        ]
-    , bgroup "Name"
-        [ bench "decode basic" (nf decodeProperty "\
-            \\13\0\0\0NameProperty\0\
-            \\0\0\0\0\0\0\0\0\
-            \\1\0\0\0\0")
-        , bench "encode basic" (nf Binary.encode (NameProperty
-            (Int64 0)
-            (Text "")))
-        ]
-    , bgroup "QWord"
-        [ bench "decode basic" (nf decodeProperty "\
-            \\14\0\0\0QWordProperty\0\
-            \\8\0\0\0\0\0\0\0\
-            \\0\0\0\0\0\0\0\0")
-        , bench "encode basic" (nf Binary.encode (QWordProperty
-            (Int64 8)
-            (Int64 0)))
-        ]
-    , bgroup "String"
-        [ bench "decode basic" (nf decodeProperty "\
-            \\12\0\0\0StrProperty\0\
-            \\0\0\0\0\0\0\0\0\
-            \\1\0\0\0\0")
-        , bench "encode basic" (nf Binary.encode (StrProperty
-            (Int64 0)
-            (Text "")))
-        ]
-    ]
-
-decodeProperty :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Property)
-decodeProperty = Binary.decodeOrFail
diff --git a/benchmark/Octane/Type/ReplayBench.hs b/benchmark/Octane/Type/ReplayBench.hs
deleted file mode 100644
--- a/benchmark/Octane/Type/ReplayBench.hs
+++ /dev/null
@@ -1,56 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.ReplayBench (benchmarks) where
-
-import Criterion
-import Octane
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-import qualified Data.Map as Map
-
-benchmarks :: Benchmark
-benchmarks = bgroup "Replay"
-    [ bench "decode basic" (nf decodeReplay "\
-        \\0\0\0\0\
-        \\0\0\0\0\
-        \\0\0\0\0\
-        \\0\0\0\0\
-        \\1\0\0\0\0\
-        \\5\0\0\0None\0\
-        \\0\0\0\0\
-        \\0\0\0\0\
-        \\0\0\0\0\
-        \\0\0\0\0\
-        \\0\0\0\0\
-        \\0\0\0\0\
-        \\0\0\0\0\
-        \\0\0\0\0\
-        \\0\0\0\0\
-        \\0\0\0\0\
-        \\0\0\0\0\
-        \\0\0\0\0")
-    , bench "encode basic" (nf Binary.encode (Replay
-        (Word32 0)
-        (Word32 0)
-        (Int32 0)
-        (Int32 0)
-        (Text "")
-        (Dictionary Map.empty)
-        (Word32 0)
-        (Word32 0)
-        (List [])
-        (List [])
-        (Stream "")
-        (List [])
-        (List [])
-        (List [])
-        (List [])
-        (List [])
-        (List [])
-        (List [])))
-    ]
-
-decodeReplay :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Replay)
-decodeReplay = Binary.decodeOrFail
diff --git a/benchmark/Octane/TypeBench.hs b/benchmark/Octane/TypeBench.hs
deleted file mode 100644
--- a/benchmark/Octane/TypeBench.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module Octane.TypeBench (benchmarks) where
-
-import Criterion
-
-import qualified Octane.Type.ActorBench
-import qualified Octane.Type.CacheItemBench
-import qualified Octane.Type.CachePropertyBench
-import qualified Octane.Type.KeyFrameBench
-import qualified Octane.Type.MarkBench
-import qualified Octane.Type.MessageBench
-import qualified Octane.Type.PrimitiveBench
-import qualified Octane.Type.PropertyBench
-import qualified Octane.Type.ReplayBench
-
-benchmarks :: Benchmark
-benchmarks = bgroup "Type"
-    [ Octane.Type.ActorBench.benchmarks
-    , Octane.Type.CacheItemBench.benchmarks
-    , Octane.Type.CachePropertyBench.benchmarks
-    , Octane.Type.KeyFrameBench.benchmarks
-    , Octane.Type.MarkBench.benchmarks
-    , Octane.Type.MessageBench.benchmarks
-    , Octane.Type.PrimitiveBench.benchmarks
-    , Octane.Type.PropertyBench.benchmarks
-    , Octane.Type.ReplayBench.benchmarks
-    ]
diff --git a/benchmark/OctaneBench.hs b/benchmark/OctaneBench.hs
--- a/benchmark/OctaneBench.hs
+++ b/benchmark/OctaneBench.hs
@@ -1,12 +1,9 @@
-module OctaneBench (benchmarks) where
+module OctaneBench (bench) where
 
-import Criterion
+import qualified Criterion
 
-import qualified Octane.MainBench
-import qualified Octane.TypeBench
 
-benchmarks :: Benchmark
-benchmarks = bgroup "Octane"
-    [ Octane.MainBench.benchmarks
-    , Octane.TypeBench.benchmarks
+bench :: Criterion.Benchmark
+bench = Criterion.bgroup "Octane"
+    [
     ]
diff --git a/executable/Executable.hs b/executable/Executable.hs
deleted file mode 100644
--- a/executable/Executable.hs
+++ /dev/null
@@ -1,3 +0,0 @@
-module Main (module Octane) where
-
-import Octane (main)
diff --git a/executable/Main.hs b/executable/Main.hs
new file mode 100644
--- /dev/null
+++ b/executable/Main.hs
@@ -0,0 +1,3 @@
+module Main (module Octane) where
+
+import Octane (main)
diff --git a/library/Octane.hs b/library/Octane.hs
--- a/library/Octane.hs
+++ b/library/Octane.hs
@@ -1,15 +1,11 @@
 module Octane
     ( module Octane.Data
-    , module Octane.FullReplay
     , module Octane.Main
-    , module Octane.Parser
     , module Octane.Type
-    , module Octane.Version
+    , module Octane.Utility
     ) where
 
 import Octane.Data
-import Octane.FullReplay
 import Octane.Main
-import Octane.Parser
 import Octane.Type
-import Octane.Version
+import Octane.Utility
diff --git a/library/Octane/Analyzer.hs b/library/Octane/Analyzer.hs
deleted file mode 100644
--- a/library/Octane/Analyzer.hs
+++ /dev/null
@@ -1,233 +0,0 @@
-module Octane.Analyzer where
-
-import Data.Function ((&))
-
-import qualified Data.Binary as Binary
-import qualified Data.Map.Strict as Map
-import qualified Data.Maybe as Maybe
-import qualified Data.Set as Set
-import qualified Data.Text as Text
-import qualified Octane.Parser as Parser
-import qualified Octane.Type as Type
-import qualified System.Environment as Environment
-
-type Point = (Int, Int, Int)
-type Points = [Point]
-type Frames = [Parser.Frame]
-type ActorId = Int
-
--- { car actor id => player actor id }
-getCarActorIds :: Frames -> Map.Map ActorId ActorId
-getCarActorIds frames = frames
-    & concatMap Parser.frameReplications
-    & filter (\ replication -> replication
-        & Parser.replicationClassName
-        & flip Set.member carClassNames)
-    & Maybe.mapMaybe (\ replication -> let
-        carActorId = replication
-            & Parser.replicationActorId
-        maybeProperty = replication
-            & Parser.replicationProperties
-            & Map.lookup playerReplicationInfoPropertyName
-        in case maybeProperty of
-            Nothing -> Nothing
-            Just property -> case property of
-                Parser.PFlaggedInt _ playerActorId -> Just (carActorId, Type.fromInt32 playerActorId)
-                _ -> Nothing)
-    & Map.fromList
-
-getBallActorIds :: Frames -> Set.Set ActorId
-getBallActorIds frames = frames
-    & concatMap Parser.frameReplications
-    & filter (\ replication -> replication
-        & Parser.replicationClassName
-        & (== ballClassName))
-    & map (\ replication -> replication
-        & Parser.replicationActorId)
-    & Set.fromList
-
--- { player actor id => player name }
-getPlayerActorIds :: Frames -> Map.Map ActorId Text.Text
-getPlayerActorIds frames = frames
-    & concatMap Parser.frameReplications
-    & filter (\ replication -> replication
-        & Parser.replicationClassName
-        & (== playerClassName))
-    & Maybe.mapMaybe (\ replication -> let
-        actorId = replication
-            & Parser.replicationActorId
-        maybeProperty = replication
-            & Parser.replicationProperties
-            & Map.lookup playerNamePropertyName
-        in case maybeProperty of
-            Nothing -> Nothing
-            Just property -> case property of
-                Parser.PString name -> Just (actorId, Type.unpackText name)
-                _ -> Nothing)
-    & Map.fromList
-
-getRigidBodyStatesForActorId :: Frames -> ActorId -> [(Parser.Time, Point)]
-getRigidBodyStatesForActorId frames actorId = frames
-    & concatMap (\ frame -> let
-        time = Parser.frameTime frame
-        in frame
-            & Parser.frameReplications
-            & filter (\ replication -> replication
-                & Parser.replicationActorId
-                & (== actorId))
-            & map Parser.replicationProperties
-            & Maybe.mapMaybe (\ properties -> properties
-                & Map.lookup rbsPropertyName)
-            & Maybe.mapMaybe (\ property -> case property of
-                Parser.PRigidBodyState _ location _ _ _ -> Just location
-                _ -> Nothing)
-            & map (\ (Parser.Vector x y z) -> (x, y, z))
-            & map (\ point -> (time, point)))
-
-getBallRigidBodyStates :: Frames -> [[(Parser.Time, Point)]]
-getBallRigidBodyStates frames = frames
-    & getBallActorIds
-    & Set.toAscList
-    & map (\ actorId -> actorId
-        & getRigidBodyStatesForActorId frames)
-
-getBallLocations :: Frames -> Points
-getBallLocations frames = frames
-    & getBallRigidBodyStates
-    & concat
-    & map snd
-
-carClassNames :: Set.Set Text.Text
-carClassNames =
-    [ "TAGame.Car_Season_TA"
-    , "TAGame.Car_TA"
-    ] & map Text.pack & Set.fromList
-
-ballClassName :: Text.Text
-ballClassName = Text.pack "TAGame.Ball_TA"
-
-playerClassName :: Text.Text
-playerClassName = Text.pack "TAGame.PRI_TA"
-
-playerReplicationInfoPropertyName :: Text.Text
-playerReplicationInfoPropertyName = Text.pack "Engine.Pawn:PlayerReplicationInfo"
-
-rbsPropertyName :: Text.Text
-rbsPropertyName = Text.pack "TAGame.RBActor_TA:ReplicatedRBState"
-
-playerNamePropertyName :: Text.Text
-playerNamePropertyName = Text.pack "Engine.PlayerReplicationInfo:PlayerName"
-
-getDistance :: Point -> Point -> Float
-getDistance (x1, y1, z1) (x2, y2, z2) = let
-    dx = fromIntegral (x2 - x1)
-    dy = fromIntegral (y2 - y1)
-    dz = fromIntegral (z2 - z1)
-    in sqrt (dx ** 2 + dy ** 2 + dz ** 2)
-
-getBallDistances :: Points -> [Float]
-getBallDistances points = points
-    & zip (drop 1 points)
-    & map (\ (p2, p1) -> getDistance p1 p2)
-
-getBallDistance :: Points -> Float
-getBallDistance points = points
-    & getBallDistances
-    & sum
-
-getBallSpeeds :: Points -> [Float]
-getBallSpeeds points = points
-    & getBallDistances
-    & map (\ distance -> distance / 0.04 {- the average delta -})
-
-getHisto :: (a -> Int) -> Int -> Int -> [a] -> (Int, Int, Int)
-getHisto fromPoint bottom top points = let
-    range = abs (top - bottom)
-    third = quot range 3
-    values = map fromPoint points
-    in foldr
-        ((\ value (low, mid, high) ->
-            if value > top - third then (low, mid, high + 1)
-            else if value < bottom + third then (low + 1, mid, high)
-            else (low, mid + 1, high)))
-        (0, 0, 0)
-        values
-
-getXHisto :: Points -> (Int, Int, Int) -- not sure of the direction... left to right?
-getXHisto points = getHisto (\ (x, _, _) -> x) (-4500) 4500 points
-
-getYHisto :: Points -> (Int, Int, Int) -- not sure of the direction... orange to blue?
-getYHisto points = getHisto (\ (_, y, _) -> y) (-5200) 5200 points
-
-getZHisto :: Points -> (Int, Int, Int) -- bottom to top
-getZHisto points = getHisto (\ (_, _, z) -> z) 0 2000 points
-
-getSpeedHisto :: [Float] -> (Int, Int, Int) -- slow to fast
-getSpeedHisto speeds = getHisto round 0 2000 speeds
-
-analyze :: FilePath -> IO ()
-analyze file = do
-    replay <- Binary.decodeFile file
-    let frames = Parser.parseFrames replay
-
-    putStr "Number of frames: "
-    print (length frames)
-
-    let ballLocations = getBallLocations frames
-    putStr "Number of ball replications: "
-    print (length ballLocations) -- 7702
-
-    let ballDistance = getBallDistance ballLocations
-    putStr "Total distance traveled by the ball: "
-    print ballDistance -- 323759.94
-
-    let (xs, ys, zs) = unzip3 ballLocations
-    let minX = minimum xs
-    let maxX = maximum xs
-    putStr "(Minimum X value, maximum X value): "
-    print (minX, maxX) -- (-4004,4003) [wasteland (-4451,4458)]
-
-    let minY = minimum ys
-    let maxY = maximum ys
-    putStr "(Minimum Y value, maximum Y value): "
-    print (minY, maxY) -- (-5215,5214)
-
-    let minZ = minimum zs
-    let maxZ = maximum zs
-    putStr "(Minimum Z value, maximum Z value): "
-    print (minZ, maxZ) -- (88,1943)
-
-    let xHisto = getXHisto ballLocations
-    putStr "(Left, middle, right): "
-    print xHisto -- (2241,2237,3224)
-
-    let yHisto = getYHisto ballLocations
-    putStr "(Orange, middle, blue): "
-    print yHisto -- (3731,1504,2467)
-
-    let zHisto = getZHisto ballLocations
-    putStr "(Bottom, middle, top): "
-    print zHisto -- (6967,507,228)
-
-    let ballSpeeds = getBallSpeeds ballLocations
-    let minSpeed = minimum ballSpeeds
-    let maxSpeed = maximum ballSpeeds
-    putStr "(Minimum speed, maximum speed): "
-    print (minSpeed, maxSpeed) -- (0.0,130894.555)
-
-    let speedHisto = getSpeedHisto ballSpeeds
-    putStr "(Slow, medium, fast): "
-    print speedHisto -- (2250,3630,1821)
-
-    let playerActorIds = getPlayerActorIds frames
-    putStrLn "Players:"
-    playerActorIds & Map.toList & mapM_ print
-
-    let carActorIds = getCarActorIds frames
-    putStrLn "Cars:"
-    carActorIds & Map.toList & mapM_ print
-
-main :: IO ()
-main = do
-    args <- Environment.getArgs
-    mapM_ analyze args
diff --git a/library/Octane/Data.hs b/library/Octane/Data.hs
--- a/library/Octane/Data.hs
+++ b/library/Octane/Data.hs
@@ -1,1294 +1,27 @@
--- | This module contains largely static data about Rocket League itself, such
--- as which classes objects belong to.
-module Octane.Data where
-
-import Data.Function ((&))
-import Data.Monoid ((<>))
-
-import qualified Data.Bimap as Bimap
-import qualified Data.Map.Strict as Map
-import qualified Data.Set as Set
-import qualified Data.Text as Text
-
--- * Classes
-
--- | A map from class names to a set of object names that are that class. This
--- is typically not useful by itself, as it is used to build the
--- 'objectToClass' map.
-classToObjects :: Map.Map Text.Text (Set.Set Text.Text)
-classToObjects = let
-    normal =
-        [ ( "TAGame.Ball_TA",
-            [ "Archetypes.Ball.Ball_Default"
-            , "Archetypes.Ball.Ball_Basketball"
-            , "Archetypes.Ball.Ball_Puck"
-            , "Archetypes.Ball.CubeBall"
-            ])
-        , ( "TAGame.CameraSettingsActor_TA",
-            [ "TAGame.CameraSettingsActor_TA:PRI"
-            , "TAGame.Default__CameraSettingsActor_TA"
-            ])
-        , ( "TAGame.CarComponent_Boost_TA",
-            [ "Archetypes.CarComponents.CarComponent_Boost"
-            ])
-        , ( "TAGame.CarComponent_Dodge_TA",
-            [ "Archetypes.CarComponents.CarComponent_Dodge"
-            ])
-        , ( "TAGame.CarComponent_DoubleJump_TA",
-            [ "Archetypes.CarComponents.CarComponent_DoubleJump"
-            ])
-        , ( "TAGame.CarComponent_FlipCar_TA",
-            [ "Archetypes.CarComponents.CarComponent_FlipCar"
-            ])
-        , ( "TAGame.CarComponent_Jump_TA",
-            [ "Archetypes.CarComponents.CarComponent_Jump"
-            ])
-        , ( "TAGame.Car_TA",
-            [ "Archetypes.Car.Car_Default"
-            , "Archetypes.GameEvent.GameEvent_Season:CarArchetype"
-            ])
-        , ( "TAGame.GameEvent_Season_TA",
-            [ "Archetypes.GameEvent.GameEvent_Season"
-            ])
-        , ( "TAGame.GameEvent_SoccarPrivate_TA",
-            [ "Archetypes.GameEvent.GameEvent_BasketballPrivate"
-            , "Archetypes.GameEvent.GameEvent_HockeyPrivate"
-            , "Archetypes.GameEvent.GameEvent_SoccarPrivate"
-            ])
-        , ( "TAGame.GameEvent_SoccarSplitscreen_TA",
-            [ "Archetypes.GameEvent.GameEvent_BasketballSplitscreen"
-            , "Archetypes.GameEvent.GameEvent_HockeySplitscreen"
-            , "Archetypes.GameEvent.GameEvent_SoccarSplitscreen"
-            ])
-        , ( "TAGame.GameEvent_Soccar_TA",
-            [ "Archetypes.GameEvent.GameEvent_Soccar"
-            , "Archetypes.GameEvent.GameEvent_Basketball"
-            ])
-        , ( "TAGame.GRI_TA",
-            [ "GameInfo_Basketball.GameInfo.GameInfo_Basketball:GameReplicationInfoArchetype"
-            , "Gameinfo_Hockey.GameInfo.Gameinfo_Hockey:GameReplicationInfoArchetype"
-            , "GameInfo_Season.GameInfo.GameInfo_Season:GameReplicationInfoArchetype"
-            , "GameInfo_Soccar.GameInfo.GameInfo_Soccar:GameReplicationInfoArchetype"
-            ])
-        , ( "TAGame.PRI_TA",
-            [ "TAGame.Default__PRI_TA"
-            ])
-        , ( "TAGame.Team_Soccar_TA",
-            [ "Archetypes.Teams.Team"
-            ])
-        , ( "TAGame.VoteActor_TA",
-            [ "TAGame.Default__VoteActor_TA"
-            ])
-        ] & map (\ (klass, objects) ->
-            ( Text.pack klass
-            , objects & map Text.pack & Set.fromList
-            ))
-    special =
-        [ ("TAGame.CrowdActor_TA", ".TheWorld:PersistentLevel.CrowdActor_TA_")
-        , ("TAGame.CrowdManager_TA", ".TheWorld:PersistentLevel.CrowdManager_TA_")
-        , ("TAGame.VehiclePickup_Boost_TA", ".TheWorld:PersistentLevel.VehiclePickup_Boost_TA_")
-        ] & map (\ (klass, suffix) ->
-            ( Text.pack klass
-            , levels & Set.map (\ level -> level <> Text.pack suffix)
-            ))
-    in Map.fromList (normal ++ special)
-
--- | A set of classes that have an initial location vector.
-locationClasses :: Set.Set Text.Text
-locationClasses =
-    [ "TAGame.Ball_TA"
-    , "TAGame.CameraSettingsActor_TA"
-    , "TAGame.CarComponent_Boost_TA"
-    , "TAGame.CarComponent_Dodge_TA"
-    , "TAGame.CarComponent_DoubleJump_TA"
-    , "TAGame.CarComponent_FlipCar_TA"
-    , "TAGame.CarComponent_Jump_TA"
-    , "TAGame.Car_Season_TA"
-    , "TAGame.Car_TA"
-    , "TAGame.GRI_TA"
-    , "TAGame.GameEvent_Season_TA"
-    , "TAGame.GameEvent_SoccarPrivate_TA"
-    , "TAGame.GameEvent_SoccarSplitscreen_TA"
-    , "TAGame.GameEvent_Soccar_TA"
-    , "TAGame.PRI_TA"
-    , "TAGame.Team_Soccar_TA"
-    ] & map Text.pack & Set.fromList
-
--- | A set of classes that have an initial rotation vector.
-rotationClasses :: Set.Set Text.Text
-rotationClasses =
-    [ "TAGame.Ball_TA"
-    , "TAGame.Car_Season_TA"
-    , "TAGame.Car_TA"
-    ] & map Text.pack & Set.fromList
-
--- * Levels
-
--- | A set of level names. This is typically not useful by itself, as it is
--- used to build the 'classToObjects' map. If you are going to use it, be
--- aware that many of the levels are duplicated with strange capitalization.
--- For example, this set contains both @Wasteland_P@ and @Wasteland_p@.
-levels :: Set.Set Text.Text
-levels =
-    [ "EuroStadium_Rainy_P"
-    , "HoopsStadium_P"
-    , "Park_Night_P"
-    , "Park_Rainy_P"
-    , "Stadium_p"
-    , "TrainStation_Night_P"
-    , "TrainStation_P"
-    , "Trainstation_Night_P"
-    , "UtopiaStadium_Dusk_P"
-    , "UtopiaStadium_Dusk_p"
-    , "UtopiaStadium_P"
-    , "Utopiastadium_p"
-    , "Wasteland_P"
-    , "Wasteland_p"
-    , "eurostad_oob_audio_map"
-    , "eurostadium_p"
-    , "eurostadium_rainy_audio"
-    , "hoopsstadium_sfx"
-    , "labs_cosmic_p"
-    , "labs_doublegoal_p"
-    , "labs_underpass_p"
-    , "labs_utopia_p"
-    , "park_night_sfx"
-    , "park_p"
-    , "park_rainy_sfx"
-    , "park_sfx"
-    , "stadium_oob_audio_map"
-    , "stadium_p"
-    , "stadium_winter_p"
-    , "trainstation_p"
-    , "utopiastadium_p"
-    , "utopiastadium_sfx"
-    , "wasteland_sfx"
-    ] & map Text.pack & Set.fromList
-
--- * Objects
-
--- | A map from object names to their class names. Note that any trailing
--- numbers have been stripped from the object names. So
--- @Archetypes.Teams.Team0@ is in this map as @Archetypes.Teams.Team@.
-objectToClass :: Map.Map Text.Text Text.Text
-objectToClass = Map.foldrWithKey
-    (\ klass objects m -> objects
-        & Set.map (\ object -> (object, klass))
-        & Set.toList
-        & Map.fromList
-        & Map.union m)
-    Map.empty
-    classToObjects
-
--- * Properties
-
--- | A set of properties that are booleans.
-booleanProperties :: Set.Set Text.Text
-booleanProperties =
-    [ "Engine.Actor:bBlockActors"
-    , "Engine.Actor:bCollideActors"
-    , "Engine.Actor:bCollideWorld"
-    , "Engine.Actor:bHardAttach"
-    , "Engine.Actor:bHidden"
-    , "Engine.Actor:bNetOwner"
-    , "Engine.Actor:bProjTarget"
-    , "Engine.Actor:bTearOff"
-    , "Engine.GameReplicationInfo:bMatchIsOver"
-    , "Engine.Pawn:bCanSwatTurn"
-    , "Engine.Pawn:bRootMotionFromInterpCurve"
-    , "Engine.Pawn:bSimulateGravity"
-    , "Engine.PlayerReplicationInfo:bAdmin"
-    , "Engine.PlayerReplicationInfo:bBot"
-    , "Engine.PlayerReplicationInfo:bFromPreviousLevel"
-    , "Engine.PlayerReplicationInfo:bIsSpectator"
-    , "Engine.PlayerReplicationInfo:bOnlySpectator"
-    , "Engine.PlayerReplicationInfo:bOutOfLives"
-    , "Engine.PlayerReplicationInfo:bReadyToPlay"
-    , "Engine.PlayerReplicationInfo:bWaitingPlayer"
-    , "ProjectX.GRI_X:bGameStarted"
-    , "TAGame.CameraSettingsActor_TA:bUsingBehindView"
-    , "TAGame.CameraSettingsActor_TA:bUsingSecondaryCamera"
-    , "TAGame.CarComponent_Boost_TA:bNoBoost"
-    , "TAGame.CarComponent_Boost_TA:bUnlimitedBoost"
-    , "TAGame.CarComponent_FlipCar_TA:bFlipRight"
-    , "TAGame.GameEvent_Soccar_TA:bBallHasBeenHit"
-    , "TAGame.GameEvent_Soccar_TA:bOverTime"
-    , "TAGame.GameEvent_TA:bHasLeaveMatchPenalty"
-    , "TAGame.GameEvent_Team_TA:bDisableMutingOtherTeam"
-    , "TAGame.PRI_TA:bIsInSplitScreen"
-    , "TAGame.PRI_TA:bMatchMVP"
-    , "TAGame.PRI_TA:bOnlineLoadoutSet"
-    , "TAGame.PRI_TA:bReady"
-    , "TAGame.PRI_TA:bUsingBehindView"
-    , "TAGame.PRI_TA:bUsingFreecam"
-    , "TAGame.PRI_TA:bUsingSecondaryCamera"
-    , "TAGame.PRI_TA:bVoteToForfeitDisabled"
-    , "TAGame.RBActor_TA:bFrozen"
-    , "TAGame.RBActor_TA:bReplayActor"
-    , "TAGame.Vehicle_TA:bDriving"
-    , "TAGame.Vehicle_TA:bReplicatedHandbrake"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are bytes.
-byteProperties :: Set.Set Text.Text
-byteProperties =
-    [ "Engine.PlayerReplicationInfo:Ping"
-    , "TAGame.Ball_TA:HitTeamNum"
-    , "TAGame.CameraSettingsActor_TA:CameraPitch"
-    , "TAGame.CameraSettingsActor_TA:CameraYaw"
-    , "TAGame.CarComponent_Boost_TA:ReplicatedBoostAmount"
-    , "TAGame.CarComponent_TA:ReplicatedActive"
-    , "TAGame.GameEvent_Soccar_TA:ReplicatedScoredOnTeam"
-    , "TAGame.GameEvent_TA:ReplicatedStateIndex"
-    , "TAGame.PRI_TA:CameraPitch"
-    , "TAGame.PRI_TA:CameraYaw"
-    , "TAGame.Vehicle_TA:ReplicatedSteer"
-    , "TAGame.Vehicle_TA:ReplicatedThrottle"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are camera settings.
-camSettingsProperties :: Set.Set Text.Text
-camSettingsProperties =
-    [ "TAGame.CameraSettingsActor_TA:ProfileSettings"
-    , "TAGame.PRI_TA:CameraSettings"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are demolitions.
-demolishProperties :: Set.Set Text.Text
-demolishProperties =
-    [ "TAGame.Car_TA:ReplicatedDemolish"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are enumerations.
-enumProperties :: Set.Set Text.Text
-enumProperties =
-    [ "Engine.Actor:Role"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are explosions.
-explosionProperties :: Set.Set Text.Text
-explosionProperties =
-    [ "TAGame.Ball_TA:ReplicatedExplosionData"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are flagged integers.
-flaggedIntProperties :: Set.Set Text.Text
-flaggedIntProperties =
-    [ "Engine.Actor:Owner"
-    , "Engine.Actor:ReplicatedCollisionType"
-    , "Engine.GameReplicationInfo:GameClass"
-    , "Engine.Pawn:PlayerReplicationInfo"
-    , "Engine.PlayerReplicationInfo:Team"
-    , "Engine.TeamInfo:TeamIndex"
-    , "TAGame.Ball_TA:GameEvent"
-    , "TAGame.CameraSettingsActor_TA:PRI"
-    , "TAGame.CarComponent_TA:Vehicle"
-    , "TAGame.CrowdActor_TA:GameEvent"
-    , "TAGame.CrowdActor_TA:ReplicatedOneShotSound"
-    , "TAGame.CrowdManager_TA:GameEvent"
-    , "TAGame.CrowdManager_TA:ReplicatedGlobalOneShotSound"
-    , "TAGame.PRI_TA:PersistentCamera"
-    , "TAGame.PRI_TA:ReplicatedGameEvent"
-    , "TAGame.Team_TA:GameEvent"
-    , "TAGame.Team_TA:LogoData"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are floats.
-floatProperties :: Set.Set Text.Text
-floatProperties =
-    [ "Engine.Actor:DrawScale"
-    , "TAGame.Ball_TA:ReplicatedAddedCarBounceScale"
-    , "TAGame.Ball_TA:ReplicatedBallMaxLinearSpeedScale"
-    , "TAGame.Ball_TA:ReplicatedBallScale"
-    , "TAGame.Ball_TA:ReplicatedWorldBounceScale"
-    , "TAGame.CarComponent_Boost_TA:BoostModifier"
-    , "TAGame.CarComponent_Boost_TA:RechargeDelay"
-    , "TAGame.CarComponent_Boost_TA:RechargeRate"
-    , "TAGame.CarComponent_FlipCar_TA:FlipCarTime"
-    , "TAGame.CrowdActor_TA:ModifiedNoise"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are game modes.
-gameModeProperties :: Set.Set Text.Text
-gameModeProperties =
-    [ "TAGame.GameEvent_TA:GameMode"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are integers.
-intProperties :: Set.Set Text.Text
-intProperties =
-    [ "Engine.PlayerReplicationInfo:PlayerID"
-    , "Engine.PlayerReplicationInfo:Score"
-    , "Engine.TeamInfo:Score"
-    , "ProjectX.GRI_X:ReplicatedGameMutatorIndex"
-    , "ProjectX.GRI_X:ReplicatedGamePlaylist"
-    , "TAGame.CrowdActor_TA:ReplicatedCountDownNumber"
-    , "TAGame.GameEvent_Soccar_TA:RoundNum"
-    , "TAGame.GameEvent_Soccar_TA:SecondsRemaining"
-    , "TAGame.GameEvent_TA:BotSkill"
-    , "TAGame.GameEvent_TA:ReplicatedGameStateTimeRemaining"
-    , "TAGame.GameEvent_TA:ReplicatedStateName"
-    , "TAGame.GameEvent_Team_TA:MaxTeamSize"
-    , "TAGame.PRI_TA:MatchAssists"
-    , "TAGame.PRI_TA:MatchGoals"
-    , "TAGame.PRI_TA:MatchSaves"
-    , "TAGame.PRI_TA:MatchScore"
-    , "TAGame.PRI_TA:MatchShots"
-    , "TAGame.PRI_TA:Title"
-    , "TAGame.PRI_TA:TotalXP"
-    , "TAGame.Team_Soccar_TA:GameScore"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are online loadouts.
-loadoutOnlineProperties :: Set.Set Text.Text
-loadoutOnlineProperties =
-    [ "TAGame.PRI_TA:ClientLoadoutOnline"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are loadouts.
-loadoutProperties :: Set.Set Text.Text
-loadoutProperties =
-    [ "TAGame.PRI_TA:ClientLoadout"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are locations.
-locationProperties :: Set.Set Text.Text
-locationProperties =
-    [ "Engine.Actor:RelativeLocation"
-    , "TAGame.CarComponent_Dodge_TA:DodgeTorque"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are music stingers.
-musicStingerProperties :: Set.Set Text.Text
-musicStingerProperties =
-    [ "TAGame.GameEvent_Soccar_TA:ReplicatedMusicStinger"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are pickups.
-pickupProperties :: Set.Set Text.Text
-pickupProperties =
-    [ "TAGame.VehiclePickup_TA:ReplicatedPickupData"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are private match settings.
-privateMatchSettingsProperties :: Set.Set Text.Text
-privateMatchSettingsProperties =
-    [ "TAGame.GameEvent_SoccarPrivate_TA:MatchSettings"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are Q words, whatever those are.
-qWordProperties :: Set.Set Text.Text
-qWordProperties =
-    [ "ProjectX.GRI_X:GameServerID"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are relation rotations.
-relativeRotationProperties :: Set.Set Text.Text
-relativeRotationProperties =
-    [ "Engine.Actor:RelativeRotation"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are reservations.
-reservationProperties :: Set.Set Text.Text
-reservationProperties =
-    [ "ProjectX.GRI_X:Reservations"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are rigid body states.
-rigidBodyStateProperties :: Set.Set Text.Text
-rigidBodyStateProperties =
-    [ "TAGame.RBActor_TA:ReplicatedRBState"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are strings.
-stringProperties :: Set.Set Text.Text
-stringProperties =
-    [ "Engine.GameReplicationInfo:ServerName"
-    , "Engine.PlayerReplicationInfo:PlayerName"
-    , "Engine.PlayerReplicationInfo:RemoteUserData"
-    , "TAGame.GRI_TA:NewDedicatedServerIP"
-    , "TAGame.Team_TA:CustomTeamName"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are team paints.
-teamPaintProperties :: Set.Set Text.Text
-teamPaintProperties =
-    [ "TAGame.Car_TA:TeamPaint"
-    ] & map Text.pack & Set.fromList
-
--- | A set of properties that are unique IDs.
-uniqueIdProperties :: Set.Set Text.Text
-uniqueIdProperties =
-    [ "Engine.PlayerReplicationInfo:UniqueId"
-    -- , "TAGame.PRI_TA:PartyLeader"
-    ] & map Text.pack & Set.fromList
-
--- * Garage
-
--- | A one-to-one mapping between body IDs and their names.
-bodies :: Bimap.Bimap Int Text.Text
-bodies =
-    [ ("Armadillo", 625) -- GearsCar
-    , ("Backfire", 21)
-    , ("Batmobile", 803) -- Darkcar
-    , ("Breakout", 22) -- Force
-    , ("DeLorean Time Machine", 597) -- GreyCar
-    , ("Dominus", 403) -- MuscleCar
-    , ("Gizmo", 26) -- Spark
-    , ("Grog", 607) -- WastelandTruck
-    , ("Hogsticker", 723) -- Warthog
-    , ("Hotshot", 29) -- Torment
-    , ("Merc", 30) -- Vanquish
-    , ("Octane", 23)
-    , ("Paladin", 24) -- Orion
-    , ("Ripper", 600) -- Interceptor
-    , ("Road Hog", 25) -- Rhino
-    , ("Scarab", 404)
-    , ("Sweet Tooth", 27)
-    , ("Takumi", 402) -- Import
-    , ("Venom", 31)
-    , ("X-Devil", 28) -- Torch
-    , ("Zippy", 523)
-    ] & map (\ (v, k) -> (k, Text.pack v)) & Bimap.fromList
-
--- | A one-to-one mapping between decal IDs and their names.
-decals :: Bimap.Bimap Int Text.Text
-decals =
-    [ ( "Backfire",
-        [ ("Flames", 288)
-        , ("Lightning", 289)
-        , ("Skulls", 290)
-        , ("Stars", 291)
-        , ("Stripes", 292)
-        , ("Tech", 293)
-        , ("Wings", 294)
-        ])
-    , ( "Breakout",
-        [ ("Flames", 295)
-        , ("Lightning", 296)
-        , ("Skulls", 297)
-        , ("Stars", 298)
-        , ("Stripes", 299)
-        , ("Tech", 300)
-        , ("Wings", 301)
-        ])
-    , ( "Dominus",
-        [ ("Flames", 504)
-        , ("Scorpions", 506) -- Skorpion
-        , ("Skulls", 507)
-        , ("Stripes", 508)
-        , ("Tats", 509) -- Tribal
-        , ("Wings", 510)
-        ])
-    , ( "Gizmo",
-        [ ("Flames", 323)
-        , ("Lightning", 324)
-        , ("Skulls", 325)
-        , ("Stars", 326)
-        , ("Stripes", 327)
-        , ("Tech", 328)
-        , ("Wings", 329)
-        ])
-    , ( "Grog",
-        [ ("Bomber", 674)
-        , ("Cyclops", 694)
-        , ("Lepus", 675) -- Bunny
-        , ("Stripes", 687)
-        , ("Tagged", 695)
-        , ("Tribal", 696)
-        ])
-    , ( "Hotshot",
-        [ ("Flames", 337)
-        , ("Lightning", 338)
-        , ("Skulls", 339)
-        , ("Stars", 340)
-        , ("Stripes", 341)
-        , ("Tech", 342)
-        , ("Wings", 343)
-        ])
-    , ( "Merc",
-        [ ("Dots", 344)
-        , ("Flames", 345)
-        , ("Lightning", 346)
-        , ("Skulls", 347)
-        , ("Stars", 348)
-        , ("Stripes", 349)
-        , ("Wings", 350)
-        ])
-    , ( "Octane",
-        [ ("Flames", 302)
-        , ("Lightning", 303)
-        , ("Skulls", 304)
-        , ("Stars", 305)
-        , ("Stripes", 306)
-        , ("Tech", 307)
-        , ("Wings", 308)
-        ])
-    , ( "Paladin",
-        [ ("Flames", 309)
-        , ("Lightning", 310)
-        , ("Skulls", 311)
-        , ("Stars", 312)
-        , ("Stripes", 313)
-        , ("Tech", 314)
-        , ("Wings", 315)
-        ])
-    , ( "Ripper",
-        [ ("Bomber", 701) -- Sandgirl
-        , ("Flames", 669)
-        , ("Ockie", 700) -- Getkraken
-        , ("Shot Fox", 670) -- Kitsune
-        , ("Spikes", 671)
-        , ("Tribal", 672)
-        ])
-    , ( "Road Hog",
-        [ ("Flames", 316)
-        , ("Lightning", 317)
-        , ("Skulls", 318)
-        , ("Stars", 319)
-        , ("Stripes", 320)
-        , ("Tech", 321) -- TechSplat
-        , ("Wings", 322)
-        ])
-    , ( "Scarab",
-        [ ("Bomani", 585) -- Clouds
-        , ("Derby Girl", 592) -- RollerDerby
-        , ("Flames", 591)
-        , ("Hearts", 586)
-        , ("Tiger", 587)
-        , ("Tribal", 588)
-        ])
-    , ( "Takumi",
-        [ ("Chaser", 503) -- Wisp
-        , ("Copycat", 497) -- Gato
-        , ("Crazy-8", 502) -- Technologic
-        , ("Gaki", 499) -- Ohai
-        , ("Reiko", 498) -- Nitrous
-        , ("Stripes", 501)
-        ])
-    , ( "Venom",
-        [ ("Dots", 351)
-        , ("Flames", 352)
-        , ("Lightning", 353)
-        , ("Skulls", 354)
-        , ("Stripes", 355)
-        , ("Tagged", 356)
-        , ("Wings", 357)
-        ])
-    , ( "X-Devil",
-        [ ("Flames", 330)
-        , ("Lightning", 331)
-        , ("Skulls", 332)
-        , ("Stars", 333)
-        , ("Stripes", 334)
-        , ("Tech", 335)
-        , ("Wings", 336)
-        ])
-    , ( "Zippy",
-        [ ("Caboodle", 589) -- HeckTec
-        , ("Callous", 590) -- MeanMachine
-        , ("Flames", 583)
-        , ("Hearts", 581)
-        , ("Leopard", 584)
-        , ("Tiger", 582)
-        ])
-    ]
-        & map (\ (body, values) -> values &
-            map (\ (v, k) -> (k, v ++ " [" ++ body ++ "]")))
-        & concat
-        & map (\ (k, v) -> (k, Text.pack v))
-        & Bimap.fromList
-        & Bimap.insert 0 (Text.pack "None")
-
--- | A one-to-one mapping between wheels IDs and their names.
-wheels :: Bimap.Bimap Int Text.Text
-wheels =
-    [ ("(Alpha Reward) Goldstone", 358) -- AlphaRim
-    , ("Alchemist", 374) -- Revolution
-    , ("Almas", 364) -- Crypt
-    , ("Armadillo", 618)
-    , ("Batmobile", 874) -- Darkcar
-    , ("Bender", 360) -- Ballistic
-    , ("Carriage", 549)
-    , ("Cog", 716) -- TODO: What is this called in game?
-    , ("Cristiano", 386) -- SoccerBall
-    , ("DeLorean Time Machine", 609) -- GreyCar
-    , ("Dieci", 363) -- Caliber
-    , ("Falco", 382) -- Vulcan
-    , ("Foreman", 372) -- OffRoad
-    , ("Grog", 613) -- WastelandTruck
-    , ("Hogsticker", 719) -- Warthog
-    , ("Invader", 380) -- Triad
-    , ("Lowrider", 369)
-    , ("Lucci", 361) -- Bling
-    , ("Mountaineer", 375) -- SnowTire
-    , ("Neptune", 370) -- Mob
-    , ("Octavian", 379) -- Tarantula
-    , ("OEM", 376) -- Star
-    , ("Rat Rod", 366) -- Dynamo
-    , ("Ripper", 690) -- Spiked
-    , ("Scarab", 540) -- Scarab
-    , ("Servergate", 519) -- SkullxBones
-    , ("Spinner", 388) -- Spinner
-    , ("Spyder", 371) -- Ninja
-    , ("Stallion", 365) -- DeepDish
-    , ("Stern", 368) -- Hydra
-    , ("Sunburt", 383) -- Wynd
-    , ("Sweet Tooth", 378)
-    , ("Tempest", 377) -- Storm
-    , ("Tomahawk", 362) -- Brink
-    , ("Trahere", 367) -- Forge
-    , ("Tunica", 359) -- Atlantis
-    , ("Veloce", 373) -- Pedigree
-    , ("Vortex", 381)
-    , ("Zippy", 518) -- SingleSpoke
-    ] & map (\ (v, k) -> (k, Text.pack v)) & Bimap.fromList
-
--- | A one-to-one mapping between rocket trail IDs and their names.
-rocketTrails :: Bimap.Bimap Int Text.Text
-rocketTrails =
-    [ ("(Alpha Reward) Gold Rush", 32) -- AlphaReward
-    , ("Accelerato", 524) -- MusicalNotes
-    , ("Batmobile", 868) -- DarkCar
-    , ("Battle-Stars", 545)
-    , ("Bubbles", 33)
-    , ("Burnout", 387)
-    , ("Candy Corn", 578)
-    , ("Confetti", 34)
-    , ("Datastream", 35) -- Digital
-    , ("Flamethrower Blue", 37)
-    , ("Flamethrower Green", 38)
-    , ("Flamethrower Pink", 39)
-    , ("Flamethrower Purple", 40)
-    , ("Flamethrower Red", 41)
-    , ("Flamethrower", 36)
-    , ("Flowers", 42)
-    , ("Grass", 43)
-    , ("Hydro", 69) -- Water
-    , ("Ion Blue", 45) -- LightTrail
-    , ("Ion Green", 46)
-    , ("Ion Pink", 47)
-    , ("Ion Purple", 48)
-    , ("Ion Red", 49)
-    , ("Ion Yellow", 50)
-    , ("Money", 51)
-    , ("Nitrous", 384)
-    , ("Nuts & Bolts", 544)
-    , ("OutaTime", 626) -- GreyCar
-    , ("Plasma", 52)
-    , ("Portal - Conversion Gel", 651)
-    , ("Portal - Propulsion Gel", 652)
-    , ("Portal - Reuplsion Gel", 653)
-    , ("Rainbow", 59)
-    , ("Sacred", 44) -- HolyLight
-    , ("Sandstorm", 664)
-    , ("Slime", 60)
-    , ("Snowflakes", 61)
-    , ("Sparkles", 62)
-    , ("Standard Blue", 64)
-    , ("Standard Pink", 65)
-    , ("Standard Purple", 66)
-    , ("Standard Red", 63)
-    , ("Standard Yellow", 67)
-    , ("Sweet Tooth", 68)
-    , ("Thermal Blue", 54)
-    , ("Thermal Green", 55)
-    , ("Thermal Pink", 56)
-    , ("Thermal Purple", 57)
-    , ("Thermal Yellow", 58)
-    , ("Thermal", 53) -- Propulsion
-    , ("Xmas", 635) -- Presents
-    ] & map (\ (v, k) -> (k, Text.pack v)) & Bimap.fromList
-
--- | A one-to-one mapping between antenna IDs and their names.
-antennas :: Bimap.Bimap Int Text.Text
-antennas =
-    [ ("None", 0)
-    , ("(Beta Reward) Gold Nugget", 6)
-    , ("8-Ball", 1)
-    , ("Alien", 753)
-    , ("Balloon Dog", 752)
-    , ("Batman", 804)
-    , ("Blue Chequered Flag", 284)
-    , ("Bomb Pole", 614)
-    , ("Calavera", 574) -- DOTDSkull
-    , ("Camo Flag", 285)
-    , ("Candle", 757)
-    , ("Candy Cane", 649)
-    , ("Chick Magnet", 392)
-    , ("Cupcake", 393)
-    , ("Dave's Bread", 391)
-    , ("deadmau5", 781) -- Mau5
-    , ("Disco Ball", 800)
-    , ("Disconnect", 394)
-    , ("Dollar Sign", 3)
-    , ("Donut", 395)
-    , ("Foam Finger", 398)
-    , ("Fuzzy Brute", 593) -- FurballFrakenstein
-    , ("Fuzzy Vamp", 595) -- FurballVampire
-    , ("Genie Lamp", 798)
-    , ("Gingerbread Man", 5)
-    , ("Heart", 7)
-    , ("Holiday Gift", 650) -- ChristmasGift
-    , ("Horseshoe", 8)
-    , ("Hula Girl", 762)
-    , ("Jolly Roger", 217) -- Pirate
-    , ("Lightning Bolt", 9)
-    , ("Moai", 522)
-    , ("Parrot", 750)
-    , ("Peace", 11)
-    , ("Piñata", 400)
-    , ("Planetoid", 12) -- Saturn
-    , ("Radioactive", 615) -- RadiationSymbol
-    , ("Rainbow", 490)
-    , ("Reddit Snoo", 629)
-    , ("Retro Ball - Urban", 575)
-    , ("Retro Ball - Utopia", 576)
-    , ("Retro Ball - Wasteland", 577)
-    , ("Rocket", 755)
-    , ("Rose", 785)
-    , ("Rubber Duckie", 779)
-    , ("Safety Flag", 286) -- OrangeNylon
-    , ("Skull", 13)
-    , ("Smiley", 14)
-    , ("Snowman", 15)
-    , ("Soccer Ball", 16)
-    , ("Star", 17)
-    , ("Sunflower", 786)
-    , ("Superman", 805)
-    , ("Sweet Tooth", 18)
-    , ("Tennis Ball", 19)
-    , ("The Game Awards - Statue", 655) -- VGA_Statue
-    , ("UFO", 20)
-    , ("Venus Flytrap", 788)
-    , ("Waffle", 401)
-    , ("White Flag", 491)
-    , ("Wonder Woman", 806)
-    -- TODO: What are these called in game?
-    , ("Community", 527)
-    , ("ESL_Champ", 537)
-    , ("Eyeball", 396)
-    , ("Moderator", 528)
-    , ("PanicButton", 829)
-    , ("Psyonix", 287)
-    , ("TourneyChamp", 539)
-    , ("Translator", 530)
-    -- Community Flags
-    , ("9GAG", 799)
-    , ("AbleGamers", 814)
-    , ("Achievement Hunter", 610)
-    , ("Angry Army", 548) -- AgryJoe
-    , ("Anne Munition", 531)
-    , ("Day[9]TV", 654)
-    , ("Destructoid", 724)
-    , ("EMD1", 532) -- EatMyDiction
-    , ("ESL", 536)
-    , ("Funhaus", 611)
-    , ("Gamespot", 725)
-    , ("Gassy Mexican", 511)
-    , ("Gfinity", 732)
-    , ("GoldGlove", 728)
-    , ("IGN", 802)
-    , ("itmeJP", 533)
-    , ("LIRIK", 513)
-    , ("lolRenaynay", 628)
-    , ("MLG", 538)
-    , ("NeoGAF", 616)
-    , ("NVIDIA", 808)
-    , ("Operation Sports", 726)
-    , ("PC Gamer", 667)
-    , ("Polaris", 608)
-    , ("Razer", 573)
-    , ("Reddit", 630)
-    , ("RL Garage", 645)
-    , ("Rocket Beans TV", 869)
-    , ("Rooster Teeth", 612)
-    , ("Saudi Gamer", 534)
-    , ("Serious Gaming", 572)
-    , ("Something Awful", 699)
-    , ("Spooky1611", 514)
-    , ("Team Fat", 220)
-    , ("Team PowA", 515)
-    , ("The Attack", 535)
-    , ("The Game Awards - Logo", 693) -- VGX
-    , ("Twitch", 221)
-    , ("YouTube Gaming", 516)
-    -- Country Flags
-    , ("Afghanistan", 407)
-    , ("Albania", 408)
-    , ("Algeria", 124)
-    , ("American Samoa", 409)
-    , ("Andorra", 830)
-    , ("Angola", 410)
-    , ("Anguilla", 831)
-    , ("Antigua & Barbuda", 832)
-    , ("Argentina", 125)
-    , ("Armenia", 411)
-    , ("Aruba", 833)
-    , ("Australia", 126)
-    , ("Austria", 127)
-    , ("Azerbaijan", 412)
-    , ("Bahamas", 834)
-    , ("Bahrain", 550)
-    , ("Bangladesh", 413)
-    , ("Barbados", 835)
-    , ("Belarus", 414)
-    , ("Belgium", 128)
-    , ("Belize", 551)
-    , ("Benin", 415)
-    , ("Bermuda", 836)
-    , ("Bhutan", 552)
-    , ("Bolivia", 416)
-    , ("Bosnia and Herzegovina", 129)
-    , ("Botswana", 553)
-    , ("Brazil", 130)
-    , ("British Virgin Islands", 837)
-    , ("Bulgaria", 131)
-    , ("Burkina Faso", 417)
-    , ("Burma", 418)
-    , ("Burundi", 419)
-    , ("Cambodia", 420)
-    , ("Cameroon", 132)
-    , ("Canada", 133)
-    , ("Cape Verde Islands", 134)
-    , ("Cayman Islands", 838)
-    , ("Central African Republic", 540)
-    , ("Chad", 422)
-    , ("Chile", 135)
-    , ("China", 136)
-    , ("Chinese Taipei", 198)
-    , ("Colombia", 137)
-    , ("Comoros", 554)
-    , ("Congo", 138)
-    , ("Congo DR", 423)
-    , ("Cook Islands", 839)
-    , ("Costa Rica", 139)
-    , ("Croatia", 140)
-    , ("Cuba", 424)
-    , ("Curacao", 840)
-    , ("Cyprus", 141)
-    , ("Czech Republic", 142)
-    , ("Côte d'Ivoire", 165)
-    , ("Denmark", 143)
-    , ("Djibouti", 555)
-    , ("Dominica", 841)
-    , ("Dominican Republic", 425)
-    , ("East Timor", 556)
-    , ("Ecuador", 144)
-    , ("Egypt", 426)
-    , ("El Salvador", 145)
-    , ("England", 146)
-    , ("Equatorial Guinea", 147)
-    , ("Eritrea", 427)
-    , ("Estonia", 428)
-    , ("Ethiopia", 429)
-    , ("Falkland Islands", 842)
-    , ("Faroe Islands", 843)
-    , ("Fiji", 557)
-    , ("Finland", 148)
-    , ("France", 149)
-    , ("Gabon", 430)
-    , ("Gambia", 558)
-    , ("Georgia", 431)
-    , ("Germany", 150)
-    , ("Ghana", 151)
-    , ("Gibraltar", 844)
-    , ("Greece", 152)
-    , ("Grenada", 863)
-    , ("Guam", 432)
-    , ("Guatemala", 153)
-    , ("Guinea", 154)
-    , ("Guinea Bissau", 559)
-    , ("Guyana", 560)
-    , ("Haiti", 433)
-    , ("Honduras", 155)
-    , ("Hong Kong", 156)
-    , ("Hungary", 157)
-    , ("Iceland", 158)
-    , ("India", 159)
-    , ("Indonesia", 160)
-    , ("Iran", 161)
-    , ("Iraq", 434)
-    , ("Ireland", 162)
-    , ("Isle Of Man", 845)
-    , ("Israel", 163)
-    , ("Italy", 164)
-    , ("Jamaica", 166)
-    , ("Japan", 167)
-    , ("Jordan", 435)
-    , ("Kazakhstan", 436)
-    , ("Kenya", 168)
-    , ("Kiribati", 846)
-    , ("Kosovo", 561)
-    , ("Kuwait", 437)
-    , ("Kyrgyzstan", 438)
-    , ("Laos", 439)
-    , ("Latvia", 440)
-    , ("Lebanon", 441)
-    , ("Lesotho", 562)
-    , ("Liberia", 442)
-    , ("Libya", 443)
-    , ("Lithuania", 444)
-    , ("Luxembourg", 169)
-    , ("Macau", 563)
-    , ("Macedonia", 445)
-    , ("Madagascar", 446)
-    , ("Malawi", 447)
-    , ("Malaysia", 170)
-    , ("Maldives", 847)
-    , ("Mali", 448)
-    , ("Malta", 171)
-    , ("Marshall Islands", 848)
-    , ("Mauritania", 449)
-    , ("Mauritius", 564)
-    , ("Mexico", 172)
-    , ("Micronesia", 849)
-    , ("Moldova", 450)
-    , ("Mongolia", 451)
-    , ("Montenegro", 452)
-    , ("Montserrat", 864)
-    , ("Morocco", 453)
-    , ("Mozambique", 454)
-    , ("Namibia", 457)
-    , ("Nepal", 458)
-    , ("Netherlands", 173)
-    , ("New Caledonia", 850)
-    , ("New Zealand", 174)
-    , ("Nicaragua", 175)
-    , ("Nigeria", 176)
-    , ("North Korea", 460)
-    , ("North Mariana Islands", 456)
-    , ("Northern Ireland", 177)
-    , ("Norway", 178)
-    , ("Oman", 461)
-    , ("Palestine", 462)
-    , ("Palau", 851)
-    , ("Palestine", 462)
-    , ("Panama", 180)
-    , ("Papua New Guinea", 463)
-    , ("Paraguay", 464)
-    , ("Peru", 181)
-    , ("Philippines", 182)
-    , ("Poland", 183)
-    , ("Portugal", 184)
-    , ("Puerto Rico", 465)
-    , ("Puntland", 565)
-    , ("Qatar", 466)
-    , ("Republic of Niger", 459)
-    , ("Romania", 185)
-    , ("Russia", 186)
-    , ("Rwanda", 467)
-    , ("Réunion", 566)
-    , ("Saint Kitts & Nevis", 852)
-    , ("Saint Lucia", 853)
-    , ("Saint Vincent", 854)
-    , ("Samoa", 855)
-    , ("San Marino", 856)
-    , ("Sao Tome", 857)
-    , ("Saudi Arabia", 468)
-    , ("Scotland", 187)
-    , ("Senegal", 188)
-    , ("Serbia", 189)
-    , ("Seychelles", 858)
-    , ("Sierra Leone", 469)
-    , ("Singapore", 190)
-    , ("Slovakia", 191)
-    , ("Slovenia", 192)
-    , ("Solomon Islands", 567)
-    , ("Somalia", 470)
-    , ("Somaliland", 568)
-    , ("South Africa", 193)
-    , ("South Korea", 194)
-    , ("South Sudan", 471)
-    , ("Spain", 195)
-    , ("Sri Lanka", 389)
-    , ("Sudan", 472)
-    , ("Suriname", 569)
-    , ("Swaziland", 570)
-    , ("Sweden", 196)
-    , ("Switzerland", 197)
-    , ("Syria", 473)
-    , ("Tahiti", 859)
-    , ("Tajikistan", 474)
-    , ("Tanzania", 475)
-    , ("Thailand", 199)
-    , ("Togo", 476)
-    , ("Tonga", 730)
-    , ("Trinidad and Tobago", 477)
-    , ("Tunisia", 200)
-    , ("Turkey", 201)
-    , ("Turkmenistan", 478)
-    , ("Turks & Caicos", 860)
-    , ("Uganda", 479)
-    , ("Ukraine", 204)
-    , ("United Arab Emirates", 202)
-    , ("United Kingdom", 203)
-    , ("United States", 206)
-    , ("Uruguay", 502)
-    , ("US Virgin Islands", 480)
-    , ("Uzbekistan", 481)
-    , ("Vanuatu", 861)
-    , ("Vatican City", 862)
-    , ("Venezuela", 207)
-    , ("Vietnam", 208)
-    , ("Wales", 209)
-    , ("Western Sahara", 571)
-    , ("Yemen", 210)
-    , ("Zambia", 482)
-    , ("Zimbabwe", 483)
-    -- NBA Flags
-    , ("NBA", 1263)
-    , ("Atlanta Hawks", 1245)
-    , ("Boston Celtics", 1246)
-    , ("Brooklyn Nets", 1247)
-    , ("Charlotte Hornets", 1248)
-    , ("Chicago Bulls", 1249)
-    , ("Cleveland Cavaliers", 1250)
-    , ("Dallas Mavericks", 1251)
-    , ("Denver Nuggets", 1252)
-    , ("Detroit Pistons", 1253)
-    , ("Golden State Warriors", 1254)
-    , ("Houston Rockets", 1255)
-    , ("Indian Pacers", 1256)
-    , ("Los Angeles Clippers", 1257)
-    , ("Los Angeles Lakers", 1258)
-    , ("Memphis Grizzlies", 1259)
-    , ("Miami Heat", 1260)
-    , ("Milwaukee Bucks", 1261)
-    , ("Minnesota Timberwolves", 1262)
-    , ("New Orleans Pelicans", 1264)
-    , ("New Tork Knicks", 1265)
-    , ("Oklahoma City Thunder", 1266)
-    , ("Orlando Magic", 1267)
-    , ("Philadelphia 76ers", 1268)
-    , ("Phoenix Suns", 1269)
-    , ("Portland Trail Blazers", 1270)
-    , ("Sacramento Kings", 1271)
-    , ("San Antonio Spurs", 1272)
-    , ("Toronto Raptors", 1273)
-    , ("Utah Jazz", 1274)
-    , ("Washington Wizards", 1275)
-    -- Video Games
-    , ("Blacklight", 385)
-    , ("Blacklight: Retribution", 211)
-    , ("Chivalry - Agatha Knights", 525)
-    , ("Chivalry - Mason Order", 526)
-    , ("Edge Of Space", 213)
-    , ("Euro Truck Simulator Rig", 813) -- ETS2
-    , ("Fallout - Vault Boy", 673)
-    , ("Fenix Rage", 216)
-    , ("Goat Simulator - G2", 927)
-    , ("Goat Simulator - Goatenna", 867)
-    , ("Oddworld - Abe", 678)
-    , ("Oddworld - Molluck", 679)
-    , ("Oddworld - Necrum", 632)
-    , ("Oddworld - RuptureFarms", 633)
-    , ("Portal - Aperture Laboratories", 656)
-    , ("Portal - Cake Sticker", 657)
-    , ("Portal - Companion Cube", 681)
-    , ("Portal - PotatOS", 698)
-    , ("Portal - Wheatley", 686) -- Portal_PC
-    , ("Shadowgate", 218)
-    , ("Strike Vector EX", 219)
-    , ("Unreal", 222)
-    , ("Unreal Frag Center", 485)
-    , ("Unreal Tournament", 489) -- UTBlue
-    , ("Unreal Tournament (Classic)", 487) -- UT
-    , ("Unreal Tournament - Blue", 484) -- Epic_BlueFlag
-    , ("Unreal Tournament - Flak Shell", 397)
-    , ("Unreal Tournament - Red", 486) -- Epic_RedFlag
-    , ("Unreal Tournament 2004", 488)
-    , ("Warframe", 223)
-    , ("Warframe - Chroma", 682)
-    , ("Warframe - Excalibur", 684)
-    , ("Warframe - Loki", 683)
-    , ("Witcher Medallion", 749)
-    , ("Worms W.M.D", 1008)
-    , ("Worms W.M.D. Grenade", 1000)
-    ] & map (\ (v, k) -> (k, Text.pack v)) & Bimap.fromList
-
--- | A one-to-one mapping between topper IDs and their names.
-toppers :: Bimap.Bimap Int Text.Text
-toppers =
-    [ ("None", 0)
-    , ("(Alpha Reward) Gold Cap", 224) -- AlphaHat
-    , ("Antlers", 774)
-    , ("Beret", 775)
-    , ("Biker Cap", 776)
-    , ("Birthday Cake", 789)
-    , ("Blitzen", 640) -- ReindeerAntlers
-    , ("Bobby Helmet", 227) -- BPH
-    , ("Boombox", 521)
-    , ("Bowler", 745) -- GentlemanHat
-    , ("Brodie Helmet", 743) -- DoughboyHelmet
-    , ("Brünnhilde", 241) -- Viking_Helmet
-    , ("Captain's Hat", 741)
-    , ("Cavalier", 580) -- Tricorn
-    , ("Chainsaw", 795)
-    , ("Chef's Hat", 756)
-    , ("Cherry Top", 237) -- PoliceSiren
-    , ("Christmas Tree", 639) -- RegularChristmasTree
-    , ("Cockroach", 769)
-    , ("Cow Skull", 685)
-    , ("deadmau5", 796)
-    , ("Deerstalker", 792)
-    , ("Derby", 790)
-    , ("Devil Horns", 229)
-    , ("Fez", 230)
-    , ("Fire Helmet", 231)
-    , ("Foam Hat", 390) -- TourneyWinner
-    , ("Fruit Hat", 744)
-    , ("Graduation Cap", 778)
-    , ("Halo", 232)
-    , ("Hard Hat", 233)
-    , ("Homburg", 742) -- DetectiveHat
-    , ("Locomotive", 547) -- TrainConductor
-    , ("Mariachi Hat", 234)
-    , ("Mohawk", 638)
-    , ("Mouse Trap", 782) -- RatTrap
-    , ("Paper Boat", 770)
-    , ("Pirate's Hat", 235)
-    , ("Pixelated Shades", 494) -- DWI
-    , ("Pizza Topper", 236)
-    , ("Plunger", 772)
-    , ("Police Hat", 746)
-    , ("Portal - Cake", 661)
-    , ("Propellerhead", 225) -- Beanie
-    , ("Pumpkin", 546) -- CatOLantern
-    , ("Rasta", 771)
-    , ("Rhino Horns", 780)
-    , ("Royal Crown", 228)
-    , ("Sad Sapling", 641) -- SadChristmasTree
-    , ("Santa", 642)
-    , ("Season 1 - Bronze", 705)
-    , ("Season 1 - Gold", 706)
-    , ("Season 1 - Platinum", 707)
-    , ("Season 1 - Silver", 708)
-    , ("Shark Fin", 579)
-    , ("Shuriken", 773)
-    , ("Sombrero", 238)
-    , ("Taxi Topper", 239)
-    , ("Tiara", 495)
-    , ("Top Hat", 240)
-    , ("Traffic Cone", 747)
-    , ("Unicorn", 748) -- Unihorn
-    , ("Witch's Hat", 242)
-    , ("Wizard Hat", 243)
-    , ("Work Boot", 740)
-    , ("Worms W.M.D.", 1028)
-    ] & map (\ (v, k) -> (k, Text.pack v)) & Bimap.fromList
-
--- | A one-to-one mapping between paint finish IDs and their names.
-finishes :: Bimap.Bimap Int Text.Text
-finishes =
-    [ ("Brushed Metal", 266)
-    , ("Camo", 541)
-    , ("Carbon Fiber", 272) -- GlossyCarbonFiber
-    , ("Corroded Metal", 268) -- Corroded
-    , ("DeLorean Time Machine", 623) -- GreyCar
-    , ("Glossy", 270) -- Default
-    , ("Matte", 273)
-    , ("Metallic", 274)
-    , ("Metallic Pearl", 275)
-    , ("Pearlescent", 276)
-    , ("Semigloss", 277) -- Plastic
-    , ("Sun-Damaged", 542) -- Cracked
-    , ("Toon Glossy", 279)
-    , ("Toon Matte", 281)
-    , ("Toon Wood", 280)
-    , ("Wood", 283)
-    ] & map (\ (v, k) -> (k, Text.pack v)) & Bimap.fromList
-
--- * Misc
-
--- | A one-to-one mapping between bot IDs and their names.
-bots :: Bimap.Bimap Int Text.Text
-bots =
-    [ ("Astro", 70)
-    , ("Bane", 71)
-    , ("Beast", 72)
-    , ("Blade", 73)
-    , ("C-Block", 74)
-    , ("Captain", 75)
-    , ("Caveman", 76)
-    , ("Cougar", 77)
-    , ("Devil", 78)
-    , ("Diesel", 79)
-    , ("Dotty", 80)
-    , ("Dude", 81)
-    , ("Duke", 82)
-    , ("Dynamite", 83)
-    , ("Flame", 84)
-    , ("Flash", 85)
-    , ("Fossil", 86)
-    , ("Fury", 87)
-    , ("Hawk", 88)
-    , ("Hunter", 89)
-    , ("Imp", 90)
-    , ("Jet", 91)
-    , ("JM", 92)
-    , ("Kidd", 93)
-    , ("Laser", 94)
-    , ("Lightning", 95)
-    , ("Lucky", 96)
-    , ("Middy", 97)
-    , ("Mountain", 98)
-    , ("Nitro", 99)
-    , ("Outlaw", 100)
-    , ("Phoenix", 101)
-    , ("Raja", 102)
-    , ("Razor", 103)
-    , ("Retro", 104)
-    , ("Roundhouse", 105)
-    , ("Sabre", 106)
-    , ("Samson", 107)
-    , ("Savage", 108)
-    , ("Shadow", 109)
-    , ("Shield", 110)
-    , ("Siren", 111)
-    , ("Sky", 112)
-    , ("Squall", 113)
-    , ("Tank", 114)
-    , ("Thunder", 115)
-    , ("Titan", 116)
-    , ("Turbo", 117)
-    , ("Tusk", 118)
-    , ("Viper", 119)
-    , ("Warrior", 120)
-    , ("Wolf", 121)
-    , ("Yuri", 122)
-    , ("Zap", 123)
-    ] & map (\ (v, k) -> (k, Text.pack v)) & Bimap.fromList
-
--- | A one-to-one mapping between logo IDs and their names.
-logos :: Bimap.Bimap Int Text.Text
-logos =
-    [ ("Barracudas", 244)
-    , ("Blacklight", 245)
-    , ("Bombers", 246)
-    , ("Cyclones", 247)
-    , ("Destroyers", 248)
-    , ("Dragons", 249)
-    , ("Express", 250)
-    , ("Guardians", 251)
-    , ("Knights", 252)
-    , ("Kodiaks", 253)
-    , ("Mammoths", 254)
-    , ("Monarchs", 255)
-    , ("Phantoms", 256)
-    , ("Pharaohs", 257)
-    , ("Pioneers", 258)
-    , ("Ravagers", 259)
-    , ("Rebels", 260)
-    , ("Rovers", 261)
-    , ("Scorpions", 262)
-    , ("Skyhawks", 263)
-    , ("Wolves", 264)
-    ] & map (\ (v, k) -> (k, Text.pack v)) & Bimap.fromList
-
-gameModes :: Bimap.Bimap Int Text.Text
-gameModes =
-    [ ("Soccar", 0)
-    , ("Hockey", 1)
-    , ("Hoops", 2)
-    ] & map (\ (v, k) -> (k, Text.pack v)) & Bimap.fromList
+module Octane.Data
+    ( module Octane.Data.Antennas
+    , module Octane.Data.Bodies
+    , module Octane.Data.Bots
+    , module Octane.Data.Classes
+    , module Octane.Data.Decals
+    , module Octane.Data.Finishes
+    , module Octane.Data.GameModes
+    , module Octane.Data.Logos
+    , module Octane.Data.Properties
+    , module Octane.Data.RocketTrails
+    , module Octane.Data.Toppers
+    , module Octane.Data.Wheels
+    ) where
+
+import Octane.Data.Antennas
+import Octane.Data.Bodies
+import Octane.Data.Bots
+import Octane.Data.Classes
+import Octane.Data.Decals
+import Octane.Data.Finishes
+import Octane.Data.GameModes
+import Octane.Data.Logos
+import Octane.Data.Properties
+import Octane.Data.RocketTrails
+import Octane.Data.Toppers
+import Octane.Data.Wheels
diff --git a/library/Octane/Data/Antennas.hs b/library/Octane/Data/Antennas.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Data/Antennas.hs
@@ -0,0 +1,435 @@
+module Octane.Data.Antennas (antennas) where
+
+import Data.Function ((&))
+
+import qualified Data.Bimap as Bimap
+import qualified Data.Text as StrictText
+
+
+-- | A one-to-one mapping between antenna IDs and their names.
+antennas :: Bimap.Bimap Int StrictText.Text
+antennas =
+    [ ("None", 0)
+    , ("(Beta Reward) Gold Nugget", 6)
+    , ("8-Ball", 1)
+    , ("Alien", 753)
+    , ("Balloon Dog", 752)
+    , ("Batman", 804)
+    , ("Blue Chequered Flag", 284)
+    , ("Bomb Pole", 614)
+    , ("Calavera", 574) -- DOTDSkull
+    , ("Camo Flag", 285)
+    , ("Candle", 757)
+    , ("Candy Cane", 649)
+    , ("Chick Magnet", 392)
+    , ("Cupcake", 393)
+    , ("Dave's Bread", 391)
+    , ("deadmau5", 781) -- Mau5
+    , ("Disco Ball", 800)
+    , ("Disconnect", 394)
+    , ("Dollar Sign", 3)
+    , ("Donut", 395)
+    , ("Foam Finger", 398)
+    , ("Fuzzy Brute", 593) -- FurballFrakenstein
+    , ("Fuzzy Vamp", 595) -- FurballVampire
+    , ("Genie Lamp", 798)
+    , ("Gingerbread Man", 5)
+    , ("Heart", 7)
+    , ("Holiday Gift", 650) -- ChristmasGift
+    , ("Horseshoe", 8)
+    , ("Hula Girl", 762)
+    , ("Jolly Roger", 217) -- Pirate
+    , ("Lightning Bolt", 9)
+    , ("Moai", 522)
+    , ("Parrot", 750)
+    , ("Peace", 11)
+    , ("Piñata", 400)
+    , ("Planetoid", 12) -- Saturn
+    , ("Radioactive", 615) -- RadiationSymbol
+    , ("Rainbow", 490)
+    , ("Reddit Snoo", 629)
+    , ("Retro Ball - Urban", 575)
+    , ("Retro Ball - Utopia", 576)
+    , ("Retro Ball - Wasteland", 577)
+    , ("Rocket", 755)
+    , ("Rose", 785)
+    , ("Rubber Duckie", 779)
+    , ("Safety Flag", 286) -- OrangeNylon
+    , ("Skull", 13)
+    , ("Smiley", 14)
+    , ("Snowman", 15)
+    , ("Soccer Ball", 16)
+    , ("Star", 17)
+    , ("Sunflower", 786)
+    , ("Superman", 805)
+    , ("Sweet Tooth", 18)
+    , ("Tennis Ball", 19)
+    , ("The Game Awards - Statue", 655) -- VGA_Statue
+    , ("UFO", 20)
+    , ("Venus Flytrap", 788)
+    , ("Waffle", 401)
+    , ("White Flag", 491)
+    , ("Wonder Woman", 806)
+    -- TODO: What are these called in game?
+    , ("Community", 527)
+    , ("ESL_Champ", 537)
+    , ("Eyeball", 396)
+    , ("Moderator", 528)
+    , ("PanicButton", 829)
+    , ("Psyonix", 287)
+    , ("TourneyChamp", 539)
+    , ("Translator", 530)
+    ]
+        ++ communityFlags
+        ++ countryFlags
+        ++ videoGames
+        ++ nbaFlags
+        & map (\ (v, k) -> (k, StrictText.pack v)) & Bimap.fromList
+
+
+communityFlags :: [(String, Int)]
+communityFlags =
+    [ ("9GAG", 799)
+    , ("AbleGamers", 814)
+    , ("Achievement Hunter", 610)
+    , ("Angry Army", 548) -- AgryJoe
+    , ("Anne Munition", 531)
+    , ("Day[9]TV", 654)
+    , ("Destructoid", 724)
+    , ("EMD1", 532) -- EatMyDiction
+    , ("ESL", 536)
+    , ("Funhaus", 611)
+    , ("Gamespot", 725)
+    , ("Gassy Mexican", 511)
+    , ("Gfinity", 732)
+    , ("GoldGlove", 728)
+    , ("IGN", 802)
+    , ("itmeJP", 533)
+    , ("LIRIK", 513)
+    , ("lolRenaynay", 628)
+    , ("MLG", 538)
+    , ("NeoGAF", 616)
+    , ("NVIDIA", 808)
+    , ("Operation Sports", 726)
+    , ("PC Gamer", 667)
+    , ("Polaris", 608)
+    , ("Razer", 573)
+    , ("Reddit", 630)
+    , ("RL Garage", 645)
+    , ("Rocket Beans TV", 869)
+    , ("Rooster Teeth", 612)
+    , ("Saudi Gamer", 534)
+    , ("Serious Gaming", 572)
+    , ("Something Awful", 699)
+    , ("Spooky1611", 514)
+    , ("Team Fat", 220)
+    , ("Team PowA", 515)
+    , ("The Attack", 535)
+    , ("The Game Awards - Logo", 693) -- VGX
+    , ("Twitch", 221)
+    , ("YouTube Gaming", 516)
+    ]
+
+
+countryFlags :: [(String, Int)]
+countryFlags =
+    [ ("Afghanistan", 407)
+    , ("Albania", 408)
+    , ("Algeria", 124)
+    , ("American Samoa", 409)
+    , ("Andorra", 830)
+    , ("Angola", 410)
+    , ("Anguilla", 831)
+    , ("Antigua & Barbuda", 832)
+    , ("Argentina", 125)
+    , ("Armenia", 411)
+    , ("Aruba", 833)
+    , ("Australia", 126)
+    , ("Austria", 127)
+    , ("Azerbaijan", 412)
+    , ("Bahamas", 834)
+    , ("Bahrain", 550)
+    , ("Bangladesh", 413)
+    , ("Barbados", 835)
+    , ("Belarus", 414)
+    , ("Belgium", 128)
+    , ("Belize", 551)
+    , ("Benin", 415)
+    , ("Bermuda", 836)
+    , ("Bhutan", 552)
+    , ("Bolivia", 416)
+    , ("Bosnia and Herzegovina", 129)
+    , ("Botswana", 553)
+    , ("Brazil", 130)
+    , ("British Virgin Islands", 837)
+    , ("Bulgaria", 131)
+    , ("Burkina Faso", 417)
+    , ("Burma", 418)
+    , ("Burundi", 419)
+    , ("Cambodia", 420)
+    , ("Cameroon", 132)
+    , ("Canada", 133)
+    , ("Cape Verde Islands", 134)
+    , ("Cayman Islands", 838)
+    , ("Central African Republic", 540)
+    , ("Chad", 422)
+    , ("Chile", 135)
+    , ("China", 136)
+    , ("Chinese Taipei", 198)
+    , ("Colombia", 137)
+    , ("Comoros", 554)
+    , ("Congo", 138)
+    , ("Congo DR", 423)
+    , ("Cook Islands", 839)
+    , ("Costa Rica", 139)
+    , ("Croatia", 140)
+    , ("Cuba", 424)
+    , ("Curacao", 840)
+    , ("Cyprus", 141)
+    , ("Czech Republic", 142)
+    , ("Côte d'Ivoire", 165)
+    , ("Denmark", 143)
+    , ("Djibouti", 555)
+    , ("Dominica", 841)
+    , ("Dominican Republic", 425)
+    , ("East Timor", 556)
+    , ("Ecuador", 144)
+    , ("Egypt", 426)
+    , ("El Salvador", 145)
+    , ("England", 146)
+    , ("Equatorial Guinea", 147)
+    , ("Eritrea", 427)
+    , ("Estonia", 428)
+    , ("Ethiopia", 429)
+    , ("Falkland Islands", 842)
+    , ("Faroe Islands", 843)
+    , ("Fiji", 557)
+    , ("Finland", 148)
+    , ("France", 149)
+    , ("Gabon", 430)
+    , ("Gambia", 558)
+    , ("Georgia", 431)
+    , ("Germany", 150)
+    , ("Ghana", 151)
+    , ("Gibraltar", 844)
+    , ("Greece", 152)
+    , ("Grenada", 863)
+    , ("Guam", 432)
+    , ("Guatemala", 153)
+    , ("Guinea", 154)
+    , ("Guinea Bissau", 559)
+    , ("Guyana", 560)
+    , ("Haiti", 433)
+    , ("Honduras", 155)
+    , ("Hong Kong", 156)
+    , ("Hungary", 157)
+    , ("Iceland", 158)
+    , ("India", 159)
+    , ("Indonesia", 160)
+    , ("Iran", 161)
+    , ("Iraq", 434)
+    , ("Ireland", 162)
+    , ("Isle Of Man", 845)
+    , ("Israel", 163)
+    , ("Italy", 164)
+    , ("Jamaica", 166)
+    , ("Japan", 167)
+    , ("Jordan", 435)
+    , ("Kazakhstan", 436)
+    , ("Kenya", 168)
+    , ("Kiribati", 846)
+    , ("Kosovo", 561)
+    , ("Kuwait", 437)
+    , ("Kyrgyzstan", 438)
+    , ("Laos", 439)
+    , ("Latvia", 440)
+    , ("Lebanon", 441)
+    , ("Lesotho", 562)
+    , ("Liberia", 442)
+    , ("Libya", 443)
+    , ("Lithuania", 444)
+    , ("Luxembourg", 169)
+    , ("Macau", 563)
+    , ("Macedonia", 445)
+    , ("Madagascar", 446)
+    , ("Malawi", 447)
+    , ("Malaysia", 170)
+    , ("Maldives", 847)
+    , ("Mali", 448)
+    , ("Malta", 171)
+    , ("Marshall Islands", 848)
+    , ("Mauritania", 449)
+    , ("Mauritius", 564)
+    , ("Mexico", 172)
+    , ("Micronesia", 849)
+    , ("Moldova", 450)
+    , ("Mongolia", 451)
+    , ("Montenegro", 452)
+    , ("Montserrat", 864)
+    , ("Morocco", 453)
+    , ("Mozambique", 454)
+    , ("Namibia", 457)
+    , ("Nepal", 458)
+    , ("Netherlands", 173)
+    , ("New Caledonia", 850)
+    , ("New Zealand", 174)
+    , ("Nicaragua", 175)
+    , ("Nigeria", 176)
+    , ("North Korea", 460)
+    , ("North Mariana Islands", 456)
+    , ("Northern Ireland", 177)
+    , ("Norway", 178)
+    , ("Oman", 461)
+    , ("Palestine", 462)
+    , ("Palau", 851)
+    , ("Palestine", 462)
+    , ("Panama", 180)
+    , ("Papua New Guinea", 463)
+    , ("Paraguay", 464)
+    , ("Peru", 181)
+    , ("Philippines", 182)
+    , ("Poland", 183)
+    , ("Portugal", 184)
+    , ("Puerto Rico", 465)
+    , ("Puntland", 565)
+    , ("Qatar", 466)
+    , ("Republic of Niger", 459)
+    , ("Romania", 185)
+    , ("Russia", 186)
+    , ("Rwanda", 467)
+    , ("Réunion", 566)
+    , ("Saint Kitts & Nevis", 852)
+    , ("Saint Lucia", 853)
+    , ("Saint Vincent", 854)
+    , ("Samoa", 855)
+    , ("San Marino", 856)
+    , ("Sao Tome", 857)
+    , ("Saudi Arabia", 468)
+    , ("Scotland", 187)
+    , ("Senegal", 188)
+    , ("Serbia", 189)
+    , ("Seychelles", 858)
+    , ("Sierra Leone", 469)
+    , ("Singapore", 190)
+    , ("Slovakia", 191)
+    , ("Slovenia", 192)
+    , ("Solomon Islands", 567)
+    , ("Somalia", 470)
+    , ("Somaliland", 568)
+    , ("South Africa", 193)
+    , ("South Korea", 194)
+    , ("South Sudan", 471)
+    , ("Spain", 195)
+    , ("Sri Lanka", 389)
+    , ("Sudan", 472)
+    , ("Suriname", 569)
+    , ("Swaziland", 570)
+    , ("Sweden", 196)
+    , ("Switzerland", 197)
+    , ("Syria", 473)
+    , ("Tahiti", 859)
+    , ("Tajikistan", 474)
+    , ("Tanzania", 475)
+    , ("Thailand", 199)
+    , ("Togo", 476)
+    , ("Tonga", 730)
+    , ("Trinidad and Tobago", 477)
+    , ("Tunisia", 200)
+    , ("Turkey", 201)
+    , ("Turkmenistan", 478)
+    , ("Turks & Caicos", 860)
+    , ("Uganda", 479)
+    , ("Ukraine", 204)
+    , ("United Arab Emirates", 202)
+    , ("United Kingdom", 203)
+    , ("United States", 206)
+    , ("Uruguay", 502)
+    , ("US Virgin Islands", 480)
+    , ("Uzbekistan", 481)
+    , ("Vanuatu", 861)
+    , ("Vatican City", 862)
+    , ("Venezuela", 207)
+    , ("Vietnam", 208)
+    , ("Wales", 209)
+    , ("Western Sahara", 571)
+    , ("Yemen", 210)
+    , ("Zambia", 482)
+    , ("Zimbabwe", 483)
+    ]
+
+
+nbaFlags :: [(String, Int)]
+nbaFlags =
+    [ ("NBA", 1263)
+    , ("Atlanta Hawks", 1245)
+    , ("Boston Celtics", 1246)
+    , ("Brooklyn Nets", 1247)
+    , ("Charlotte Hornets", 1248)
+    , ("Chicago Bulls", 1249)
+    , ("Cleveland Cavaliers", 1250)
+    , ("Dallas Mavericks", 1251)
+    , ("Denver Nuggets", 1252)
+    , ("Detroit Pistons", 1253)
+    , ("Golden State Warriors", 1254)
+    , ("Houston Rockets", 1255)
+    , ("Indian Pacers", 1256)
+    , ("Los Angeles Clippers", 1257)
+    , ("Los Angeles Lakers", 1258)
+    , ("Memphis Grizzlies", 1259)
+    , ("Miami Heat", 1260)
+    , ("Milwaukee Bucks", 1261)
+    , ("Minnesota Timberwolves", 1262)
+    , ("New Orleans Pelicans", 1264)
+    , ("New Tork Knicks", 1265)
+    , ("Oklahoma City Thunder", 1266)
+    , ("Orlando Magic", 1267)
+    , ("Philadelphia 76ers", 1268)
+    , ("Phoenix Suns", 1269)
+    , ("Portland Trail Blazers", 1270)
+    , ("Sacramento Kings", 1271)
+    , ("San Antonio Spurs", 1272)
+    , ("Toronto Raptors", 1273)
+    , ("Utah Jazz", 1274)
+    , ("Washington Wizards", 1275)
+    ]
+
+
+videoGames :: [(String, Int)]
+videoGames =
+    [ ("Blacklight", 385)
+    , ("Blacklight: Retribution", 211)
+    , ("Chivalry - Agatha Knights", 525)
+    , ("Chivalry - Mason Order", 526)
+    , ("Edge Of Space", 213)
+    , ("Euro Truck Simulator Rig", 813) -- ETS2
+    , ("Fallout - Vault Boy", 673)
+    , ("Fenix Rage", 216)
+    , ("Goat Simulator - G2", 927)
+    , ("Goat Simulator - Goatenna", 867)
+    , ("Oddworld - Abe", 678)
+    , ("Oddworld - Molluck", 679)
+    , ("Oddworld - Necrum", 632)
+    , ("Oddworld - RuptureFarms", 633)
+    , ("Portal - Aperture Laboratories", 656)
+    , ("Portal - Cake Sticker", 657)
+    , ("Portal - Companion Cube", 681)
+    , ("Portal - PotatOS", 698)
+    , ("Portal - Wheatley", 686) -- Portal_PC
+    , ("Shadowgate", 218)
+    , ("Strike Vector EX", 219)
+    , ("Unreal", 222)
+    , ("Unreal Frag Center", 485)
+    , ("Unreal Tournament", 489) -- UTBlue
+    , ("Unreal Tournament (Classic)", 487) -- UT
+    , ("Unreal Tournament - Blue", 484) -- Epic_BlueFlag
+    , ("Unreal Tournament - Flak Shell", 397)
+    , ("Unreal Tournament - Red", 486) -- Epic_RedFlag
+    , ("Unreal Tournament 2004", 488)
+    , ("Warframe", 223)
+    , ("Warframe - Chroma", 682)
+    , ("Warframe - Excalibur", 684)
+    , ("Warframe - Loki", 683)
+    , ("Witcher Medallion", 749)
+    , ("Worms W.M.D", 1008)
+    , ("Worms W.M.D. Grenade", 1000)
+    ]
diff --git a/library/Octane/Data/Bodies.hs b/library/Octane/Data/Bodies.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Data/Bodies.hs
@@ -0,0 +1,33 @@
+module Octane.Data.Bodies (bodies) where
+
+import Data.Function ((&))
+
+import qualified Data.Bimap as Bimap
+import qualified Data.Text as StrictText
+
+
+-- | A one-to-one mapping between body IDs and their names.
+bodies :: Bimap.Bimap Int StrictText.Text
+bodies =
+    [ ("Armadillo", 625) -- GearsCar
+    , ("Backfire", 21)
+    , ("Batmobile", 803) -- Darkcar
+    , ("Breakout", 22) -- Force
+    , ("DeLorean Time Machine", 597) -- GreyCar
+    , ("Dominus", 403) -- MuscleCar
+    , ("Gizmo", 26) -- Spark
+    , ("Grog", 607) -- WastelandTruck
+    , ("Hogsticker", 723) -- Warthog
+    , ("Hotshot", 29) -- Torment
+    , ("Merc", 30) -- Vanquish
+    , ("Octane", 23)
+    , ("Paladin", 24) -- Orion
+    , ("Ripper", 600) -- Interceptor
+    , ("Road Hog", 25) -- Rhino
+    , ("Scarab", 404)
+    , ("Sweet Tooth", 27)
+    , ("Takumi", 402) -- Import
+    , ("Venom", 31)
+    , ("X-Devil", 28) -- Torch
+    , ("Zippy", 523)
+    ] & map (\ (v, k) -> (k, StrictText.pack v)) & Bimap.fromList
diff --git a/library/Octane/Data/Bots.hs b/library/Octane/Data/Bots.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Data/Bots.hs
@@ -0,0 +1,66 @@
+module Octane.Data.Bots (bots) where
+
+import Data.Function ((&))
+
+import qualified Data.Bimap as Bimap
+import qualified Data.Text as StrictText
+
+
+-- | A one-to-one mapping between bot IDs and their names.
+bots :: Bimap.Bimap Int StrictText.Text
+bots =
+    [ ("Astro", 70)
+    , ("Bane", 71)
+    , ("Beast", 72)
+    , ("Blade", 73)
+    , ("C-Block", 74)
+    , ("Captain", 75)
+    , ("Caveman", 76)
+    , ("Cougar", 77)
+    , ("Devil", 78)
+    , ("Diesel", 79)
+    , ("Dotty", 80)
+    , ("Dude", 81)
+    , ("Duke", 82)
+    , ("Dynamite", 83)
+    , ("Flame", 84)
+    , ("Flash", 85)
+    , ("Fossil", 86)
+    , ("Fury", 87)
+    , ("Hawk", 88)
+    , ("Hunter", 89)
+    , ("Imp", 90)
+    , ("Jet", 91)
+    , ("JM", 92)
+    , ("Kidd", 93)
+    , ("Laser", 94)
+    , ("Lightning", 95)
+    , ("Lucky", 96)
+    , ("Middy", 97)
+    , ("Mountain", 98)
+    , ("Nitro", 99)
+    , ("Outlaw", 100)
+    , ("Phoenix", 101)
+    , ("Raja", 102)
+    , ("Razor", 103)
+    , ("Retro", 104)
+    , ("Roundhouse", 105)
+    , ("Sabre", 106)
+    , ("Samson", 107)
+    , ("Savage", 108)
+    , ("Shadow", 109)
+    , ("Shield", 110)
+    , ("Siren", 111)
+    , ("Sky", 112)
+    , ("Squall", 113)
+    , ("Tank", 114)
+    , ("Thunder", 115)
+    , ("Titan", 116)
+    , ("Turbo", 117)
+    , ("Tusk", 118)
+    , ("Viper", 119)
+    , ("Warrior", 120)
+    , ("Wolf", 121)
+    , ("Yuri", 122)
+    , ("Zap", 123)
+    ] & map (\ (v, k) -> (k, StrictText.pack v)) & Bimap.fromList
diff --git a/library/Octane/Data/Classes.hs b/library/Octane/Data/Classes.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Data/Classes.hs
@@ -0,0 +1,174 @@
+module Octane.Data.Classes
+    ( objectToClass
+    , locationClasses
+    , rotationClasses
+    ) where
+
+import Data.Function ((&))
+import Data.Monoid ((<>))
+
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Text as StrictText
+
+
+-- | A map from object names to their class names. Note that any trailing
+-- numbers have been stripped from the object names. So
+-- @Archetypes.Teams.Team0@ is in this map as @Archetypes.Teams.Team@.
+objectToClass :: Map.Map StrictText.Text StrictText.Text
+objectToClass = Map.foldrWithKey
+    (\ klass objects m -> objects
+        & Set.map (\ object -> (object, klass))
+        & Set.toList
+        & Map.fromList
+        & Map.union m)
+    Map.empty
+    classToObjects
+
+
+-- | A set of classes that have an initial location vector.
+locationClasses :: Set.Set StrictText.Text
+locationClasses =
+    [ "TAGame.Ball_TA"
+    , "TAGame.CameraSettingsActor_TA"
+    , "TAGame.CarComponent_Boost_TA"
+    , "TAGame.CarComponent_Dodge_TA"
+    , "TAGame.CarComponent_DoubleJump_TA"
+    , "TAGame.CarComponent_FlipCar_TA"
+    , "TAGame.CarComponent_Jump_TA"
+    , "TAGame.Car_Season_TA"
+    , "TAGame.Car_TA"
+    , "TAGame.GRI_TA"
+    , "TAGame.GameEvent_Season_TA"
+    , "TAGame.GameEvent_SoccarPrivate_TA"
+    , "TAGame.GameEvent_SoccarSplitscreen_TA"
+    , "TAGame.GameEvent_Soccar_TA"
+    , "TAGame.PRI_TA"
+    , "TAGame.Team_Soccar_TA"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of classes that have an initial rotation vector.
+rotationClasses :: Set.Set StrictText.Text
+rotationClasses =
+    [ "TAGame.Ball_TA"
+    , "TAGame.Car_Season_TA"
+    , "TAGame.Car_TA"
+    ] & map StrictText.pack & Set.fromList
+
+
+classToObjects :: Map.Map StrictText.Text (Set.Set StrictText.Text)
+classToObjects = let
+    normal =
+        [ ( "TAGame.Ball_TA",
+            [ "Archetypes.Ball.Ball_Default"
+            , "Archetypes.Ball.Ball_Basketball"
+            , "Archetypes.Ball.Ball_Puck"
+            , "Archetypes.Ball.CubeBall"
+            ])
+        , ( "TAGame.CameraSettingsActor_TA",
+            [ "TAGame.CameraSettingsActor_TA:PRI"
+            , "TAGame.Default__CameraSettingsActor_TA"
+            ])
+        , ( "TAGame.CarComponent_Boost_TA",
+            [ "Archetypes.CarComponents.CarComponent_Boost"
+            ])
+        , ( "TAGame.CarComponent_Dodge_TA",
+            [ "Archetypes.CarComponents.CarComponent_Dodge"
+            ])
+        , ( "TAGame.CarComponent_DoubleJump_TA",
+            [ "Archetypes.CarComponents.CarComponent_DoubleJump"
+            ])
+        , ( "TAGame.CarComponent_FlipCar_TA",
+            [ "Archetypes.CarComponents.CarComponent_FlipCar"
+            ])
+        , ( "TAGame.CarComponent_Jump_TA",
+            [ "Archetypes.CarComponents.CarComponent_Jump"
+            ])
+        , ( "TAGame.Car_TA",
+            [ "Archetypes.Car.Car_Default"
+            , "Archetypes.GameEvent.GameEvent_Season:CarArchetype"
+            ])
+        , ( "TAGame.GameEvent_Season_TA",
+            [ "Archetypes.GameEvent.GameEvent_Season"
+            ])
+        , ( "TAGame.GameEvent_SoccarPrivate_TA",
+            [ "Archetypes.GameEvent.GameEvent_BasketballPrivate"
+            , "Archetypes.GameEvent.GameEvent_HockeyPrivate"
+            , "Archetypes.GameEvent.GameEvent_SoccarPrivate"
+            ])
+        , ( "TAGame.GameEvent_SoccarSplitscreen_TA",
+            [ "Archetypes.GameEvent.GameEvent_BasketballSplitscreen"
+            , "Archetypes.GameEvent.GameEvent_HockeySplitscreen"
+            , "Archetypes.GameEvent.GameEvent_SoccarSplitscreen"
+            ])
+        , ( "TAGame.GameEvent_Soccar_TA",
+            [ "Archetypes.GameEvent.GameEvent_Soccar"
+            , "Archetypes.GameEvent.GameEvent_Basketball"
+            ])
+        , ( "TAGame.GRI_TA",
+            [ "GameInfo_Basketball.GameInfo.GameInfo_Basketball:GameReplicationInfoArchetype"
+            , "Gameinfo_Hockey.GameInfo.Gameinfo_Hockey:GameReplicationInfoArchetype"
+            , "GameInfo_Season.GameInfo.GameInfo_Season:GameReplicationInfoArchetype"
+            , "GameInfo_Soccar.GameInfo.GameInfo_Soccar:GameReplicationInfoArchetype"
+            ])
+        , ( "TAGame.PRI_TA",
+            [ "TAGame.Default__PRI_TA"
+            ])
+        , ( "TAGame.Team_Soccar_TA",
+            [ "Archetypes.Teams.Team"
+            ])
+        , ( "TAGame.VoteActor_TA",
+            [ "TAGame.Default__VoteActor_TA"
+            ])
+        ] & map (\ (klass, objects) ->
+            ( StrictText.pack klass
+            , objects & map StrictText.pack & Set.fromList
+            ))
+    special =
+        [ ("TAGame.CrowdActor_TA", ".TheWorld:PersistentLevel.CrowdActor_TA_")
+        , ("TAGame.CrowdManager_TA", ".TheWorld:PersistentLevel.CrowdManager_TA_")
+        , ("TAGame.VehiclePickup_Boost_TA", ".TheWorld:PersistentLevel.VehiclePickup_Boost_TA_")
+        ] & map (\ (klass, suffix) ->
+            ( StrictText.pack klass
+            , levels & Set.map (\ level -> level <> StrictText.pack suffix)
+            ))
+    in Map.fromList (normal ++ special)
+
+
+levels :: Set.Set StrictText.Text
+levels =
+    [ "EuroStadium_Rainy_P"
+    , "HoopsStadium_P"
+    , "Park_Night_P"
+    , "Park_Rainy_P"
+    , "Stadium_p"
+    , "TrainStation_Night_P"
+    , "TrainStation_P"
+    , "Trainstation_Night_P"
+    , "UtopiaStadium_Dusk_P"
+    , "UtopiaStadium_Dusk_p"
+    , "UtopiaStadium_P"
+    , "Utopiastadium_p"
+    , "Wasteland_P"
+    , "Wasteland_p"
+    , "eurostad_oob_audio_map"
+    , "eurostadium_p"
+    , "eurostadium_rainy_audio"
+    , "hoopsstadium_sfx"
+    , "labs_cosmic_p"
+    , "labs_doublegoal_p"
+    , "labs_underpass_p"
+    , "labs_utopia_p"
+    , "park_night_sfx"
+    , "park_p"
+    , "park_rainy_sfx"
+    , "park_sfx"
+    , "stadium_oob_audio_map"
+    , "stadium_p"
+    , "stadium_winter_p"
+    , "trainstation_p"
+    , "utopiastadium_p"
+    , "utopiastadium_sfx"
+    , "wasteland_sfx"
+    ] & map StrictText.pack & Set.fromList
diff --git a/library/Octane/Data/Decals.hs b/library/Octane/Data/Decals.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Data/Decals.hs
@@ -0,0 +1,220 @@
+module Octane.Data.Decals (decals) where
+
+import Data.Function ((&))
+
+import qualified Data.Bimap as Bimap
+import qualified Data.Text as StrictText
+
+
+-- | A one-to-one mapping between decal IDs and their names.
+decals :: Bimap.Bimap Int StrictText.Text
+decals =
+    [ ("Backfire", backfireDecals)
+    , ("Breakout", breakoutDecals)
+    , ("Dominus", dominusDecals)
+    , ("Gizmo", gizmoDecals)
+    , ("Grog", grogDecals)
+    , ("Hotshot", hotshotDecals)
+    , ("Merc", mercDecals)
+    , ("Octane", octaneDecals)
+    , ("Paladin", paladinDecals)
+    , ("Ripper", ripperDecals)
+    , ("Road Hog", roadHogDecals)
+    , ("Scarab", scarabDecals)
+    , ("Takumi", takumiDecals)
+    , ("Venom", venomDecals)
+    , ("X-Devil", xDevilDecals)
+    , ("Zippy", zippyDecals)
+    ]
+        & map (\ (body, values) -> values &
+            map (\ (v, k) -> (k, v ++ " [" ++ body ++ "]")))
+        & concat
+        & map (\ (k, v) -> (k, StrictText.pack v))
+        & Bimap.fromList
+        & Bimap.insert 0 (StrictText.pack "None")
+
+
+backfireDecals :: [(String, Int)]
+backfireDecals =
+    [ ("Flames", 288)
+    , ("Lightning", 289)
+    , ("Skulls", 290)
+    , ("Stars", 291)
+    , ("Stripes", 292)
+    , ("Tech", 293)
+    , ("Wings", 294)
+    ]
+
+
+breakoutDecals :: [(String, Int)]
+breakoutDecals =
+    [ ("Flames", 295)
+    , ("Lightning", 296)
+    , ("Skulls", 297)
+    , ("Stars", 298)
+    , ("Stripes", 299)
+    , ("Tech", 300)
+    , ("Wings", 301)
+    ]
+
+
+dominusDecals :: [(String, Int)]
+dominusDecals =
+    [ ("Flames", 504)
+    , ("Scorpions", 506) -- Skorpion
+    , ("Skulls", 507)
+    , ("Stripes", 508)
+    , ("Tats", 509) -- Tribal
+    , ("Wings", 510)
+    ]
+
+
+gizmoDecals :: [(String, Int)]
+gizmoDecals =
+    [ ("Flames", 323)
+    , ("Lightning", 324)
+    , ("Skulls", 325)
+    , ("Stars", 326)
+    , ("Stripes", 327)
+    , ("Tech", 328)
+    , ("Wings", 329)
+    ]
+
+
+grogDecals :: [(String, Int)]
+grogDecals =
+    [ ("Bomber", 674)
+    , ("Cyclops", 694)
+    , ("Lepus", 675) -- Bunny
+    , ("Stripes", 687)
+    , ("Tagged", 695)
+    , ("Tribal", 696)
+    ]
+
+
+hotshotDecals :: [(String, Int)]
+hotshotDecals =
+    [ ("Flames", 337)
+    , ("Lightning", 338)
+    , ("Skulls", 339)
+    , ("Stars", 340)
+    , ("Stripes", 341)
+    , ("Tech", 342)
+    , ("Wings", 343)
+    ]
+
+
+mercDecals :: [(String, Int)]
+mercDecals =
+    [ ("Dots", 344)
+    , ("Flames", 345)
+    , ("Lightning", 346)
+    , ("Skulls", 347)
+    , ("Stars", 348)
+    , ("Stripes", 349)
+    , ("Wings", 350)
+    ]
+
+
+octaneDecals :: [(String, Int)]
+octaneDecals =
+    [ ("Flames", 302)
+    , ("Lightning", 303)
+    , ("Skulls", 304)
+    , ("Stars", 305)
+    , ("Stripes", 306)
+    , ("Tech", 307)
+    , ("Wings", 308)
+    ]
+
+
+paladinDecals :: [(String, Int)]
+paladinDecals =
+    [ ("Flames", 309)
+    , ("Lightning", 310)
+    , ("Skulls", 311)
+    , ("Stars", 312)
+    , ("Stripes", 313)
+    , ("Tech", 314)
+    , ("Wings", 315)
+    ]
+
+
+ripperDecals :: [(String, Int)]
+ripperDecals =
+    [ ("Bomber", 701) -- Sandgirl
+    , ("Flames", 669)
+    , ("Ockie", 700) -- Getkraken
+    , ("Shot Fox", 670) -- Kitsune
+    , ("Spikes", 671)
+    , ("Tribal", 672)
+    ]
+
+
+roadHogDecals :: [(String, Int)]
+roadHogDecals =
+    [ ("Flames", 316)
+    , ("Lightning", 317)
+    , ("Skulls", 318)
+    , ("Stars", 319)
+    , ("Stripes", 320)
+    , ("Tech", 321) -- TechSplat
+    , ("Wings", 322)
+    ]
+
+
+scarabDecals :: [(String, Int)]
+scarabDecals =
+    [ ("Bomani", 585) -- Clouds
+    , ("Derby Girl", 592) -- RollerDerby
+    , ("Flames", 591)
+    , ("Hearts", 586)
+    , ("Tiger", 587)
+    , ("Tribal", 588)
+    ]
+
+
+takumiDecals :: [(String, Int)]
+takumiDecals =
+    [ ("Chaser", 503) -- Wisp
+    , ("Copycat", 497) -- Gato
+    , ("Crazy-8", 502) -- Technologic
+    , ("Gaki", 499) -- Ohai
+    , ("Reiko", 498) -- Nitrous
+    , ("Stripes", 501)
+    ]
+
+
+venomDecals :: [(String, Int)]
+venomDecals =
+    [ ("Dots", 351)
+    , ("Flames", 352)
+    , ("Lightning", 353)
+    , ("Skulls", 354)
+    , ("Stripes", 355)
+    , ("Tagged", 356)
+    , ("Wings", 357)
+    ]
+
+
+xDevilDecals :: [(String, Int)]
+xDevilDecals =
+    [ ("Flames", 330)
+    , ("Lightning", 331)
+    , ("Skulls", 332)
+    , ("Stars", 333)
+    , ("Stripes", 334)
+    , ("Tech", 335)
+    , ("Wings", 336)
+    ]
+
+
+zippyDecals :: [(String, Int)]
+zippyDecals =
+    [ ("Caboodle", 589) -- HeckTec
+    , ("Callous", 590) -- MeanMachine
+    , ("Flames", 583)
+    , ("Hearts", 581)
+    , ("Leopard", 584)
+    , ("Tiger", 582)
+    ]
diff --git a/library/Octane/Data/Finishes.hs b/library/Octane/Data/Finishes.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Data/Finishes.hs
@@ -0,0 +1,28 @@
+module Octane.Data.Finishes (finishes) where
+
+import Data.Function ((&))
+
+import qualified Data.Bimap as Bimap
+import qualified Data.Text as StrictText
+
+
+-- | A one-to-one mapping between paint finish IDs and their names.
+finishes :: Bimap.Bimap Int StrictText.Text
+finishes =
+    [ ("Brushed Metal", 266)
+    , ("Camo", 541)
+    , ("Carbon Fiber", 272) -- GlossyCarbonFiber
+    , ("Corroded Metal", 268) -- Corroded
+    , ("DeLorean Time Machine", 623) -- GreyCar
+    , ("Glossy", 270) -- Default
+    , ("Matte", 273)
+    , ("Metallic", 274)
+    , ("Metallic Pearl", 275)
+    , ("Pearlescent", 276)
+    , ("Semigloss", 277) -- Plastic
+    , ("Sun-Damaged", 542) -- Cracked
+    , ("Toon Glossy", 279)
+    , ("Toon Matte", 281)
+    , ("Toon Wood", 280)
+    , ("Wood", 283)
+    ] & map (\ (v, k) -> (k, StrictText.pack v)) & Bimap.fromList
diff --git a/library/Octane/Data/GameModes.hs b/library/Octane/Data/GameModes.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Data/GameModes.hs
@@ -0,0 +1,15 @@
+module Octane.Data.GameModes (gameModes) where
+
+import Data.Function ((&))
+
+import qualified Data.Bimap as Bimap
+import qualified Data.Text as StrictText
+
+
+-- | A one-to-one mapping between game mode IDs and their names.
+gameModes :: Bimap.Bimap Int StrictText.Text
+gameModes =
+    [ ("Soccar", 0)
+    , ("Hockey", 1)
+    , ("Hoops", 2)
+    ] & map (\ (v, k) -> (k, StrictText.pack v)) & Bimap.fromList
diff --git a/library/Octane/Data/Logos.hs b/library/Octane/Data/Logos.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Data/Logos.hs
@@ -0,0 +1,33 @@
+module Octane.Data.Logos (logos) where
+
+import Data.Function ((&))
+
+import qualified Data.Bimap as Bimap
+import qualified Data.Text as StrictText
+
+
+-- | A one-to-one mapping between logo IDs and their names.
+logos :: Bimap.Bimap Int StrictText.Text
+logos =
+    [ ("Barracudas", 244)
+    , ("Blacklight", 245)
+    , ("Bombers", 246)
+    , ("Cyclones", 247)
+    , ("Destroyers", 248)
+    , ("Dragons", 249)
+    , ("Express", 250)
+    , ("Guardians", 251)
+    , ("Knights", 252)
+    , ("Kodiaks", 253)
+    , ("Mammoths", 254)
+    , ("Monarchs", 255)
+    , ("Phantoms", 256)
+    , ("Pharaohs", 257)
+    , ("Pioneers", 258)
+    , ("Ravagers", 259)
+    , ("Rebels", 260)
+    , ("Rovers", 261)
+    , ("Scorpions", 262)
+    , ("Skyhawks", 263)
+    , ("Wolves", 264)
+    ] & map (\ (v, k) -> (k, StrictText.pack v)) & Bimap.fromList
diff --git a/library/Octane/Data/Properties.hs b/library/Octane/Data/Properties.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Data/Properties.hs
@@ -0,0 +1,294 @@
+module Octane.Data.Properties
+    ( booleanProperties
+    , byteProperties
+    , camSettingsProperties
+    , demolishProperties
+    , enumProperties
+    , explosionProperties
+    , flaggedIntProperties
+    , floatProperties
+    , gameModeProperties
+    , intProperties
+    , loadoutOnlineProperties
+    , loadoutProperties
+    , locationProperties
+    , musicStingerProperties
+    , pickupProperties
+    , privateMatchSettingsProperties
+    , qWordProperties
+    , relativeRotationProperties
+    , reservationProperties
+    , rigidBodyStateProperties
+    , stringProperties
+    , teamPaintProperties
+    , uniqueIdProperties
+    ) where
+
+import Data.Function ((&))
+
+import qualified Data.Set as Set
+import qualified Data.Text as StrictText
+
+
+-- | A set of properties that are booleans.
+booleanProperties :: Set.Set StrictText.Text
+booleanProperties =
+    [ "Engine.Actor:bBlockActors"
+    , "Engine.Actor:bCollideActors"
+    , "Engine.Actor:bCollideWorld"
+    , "Engine.Actor:bHardAttach"
+    , "Engine.Actor:bHidden"
+    , "Engine.Actor:bNetOwner"
+    , "Engine.Actor:bProjTarget"
+    , "Engine.Actor:bTearOff"
+    , "Engine.GameReplicationInfo:bMatchIsOver"
+    , "Engine.Pawn:bCanSwatTurn"
+    , "Engine.Pawn:bRootMotionFromInterpCurve"
+    , "Engine.Pawn:bSimulateGravity"
+    , "Engine.PlayerReplicationInfo:bAdmin"
+    , "Engine.PlayerReplicationInfo:bBot"
+    , "Engine.PlayerReplicationInfo:bFromPreviousLevel"
+    , "Engine.PlayerReplicationInfo:bIsSpectator"
+    , "Engine.PlayerReplicationInfo:bOnlySpectator"
+    , "Engine.PlayerReplicationInfo:bOutOfLives"
+    , "Engine.PlayerReplicationInfo:bReadyToPlay"
+    , "Engine.PlayerReplicationInfo:bWaitingPlayer"
+    , "ProjectX.GRI_X:bGameStarted"
+    , "TAGame.CameraSettingsActor_TA:bUsingBehindView"
+    , "TAGame.CameraSettingsActor_TA:bUsingSecondaryCamera"
+    , "TAGame.CarComponent_Boost_TA:bNoBoost"
+    , "TAGame.CarComponent_Boost_TA:bUnlimitedBoost"
+    , "TAGame.CarComponent_FlipCar_TA:bFlipRight"
+    , "TAGame.GameEvent_Soccar_TA:bBallHasBeenHit"
+    , "TAGame.GameEvent_Soccar_TA:bOverTime"
+    , "TAGame.GameEvent_TA:bHasLeaveMatchPenalty"
+    , "TAGame.GameEvent_Team_TA:bDisableMutingOtherTeam"
+    , "TAGame.PRI_TA:bIsInSplitScreen"
+    , "TAGame.PRI_TA:bMatchMVP"
+    , "TAGame.PRI_TA:bOnlineLoadoutSet"
+    , "TAGame.PRI_TA:bReady"
+    , "TAGame.PRI_TA:bUsingBehindView"
+    , "TAGame.PRI_TA:bUsingFreecam"
+    , "TAGame.PRI_TA:bUsingSecondaryCamera"
+    , "TAGame.PRI_TA:bVoteToForfeitDisabled"
+    , "TAGame.RBActor_TA:bFrozen"
+    , "TAGame.RBActor_TA:bReplayActor"
+    , "TAGame.Vehicle_TA:bDriving"
+    , "TAGame.Vehicle_TA:bReplicatedHandbrake"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are bytes.
+byteProperties :: Set.Set StrictText.Text
+byteProperties =
+    [ "Engine.PlayerReplicationInfo:Ping"
+    , "TAGame.Ball_TA:HitTeamNum"
+    , "TAGame.CameraSettingsActor_TA:CameraPitch"
+    , "TAGame.CameraSettingsActor_TA:CameraYaw"
+    , "TAGame.CarComponent_Boost_TA:ReplicatedBoostAmount"
+    , "TAGame.CarComponent_TA:ReplicatedActive"
+    , "TAGame.GameEvent_Soccar_TA:ReplicatedScoredOnTeam"
+    , "TAGame.GameEvent_TA:ReplicatedStateIndex"
+    , "TAGame.PRI_TA:CameraPitch"
+    , "TAGame.PRI_TA:CameraYaw"
+    , "TAGame.Vehicle_TA:ReplicatedSteer"
+    , "TAGame.Vehicle_TA:ReplicatedThrottle"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are camera settings.
+camSettingsProperties :: Set.Set StrictText.Text
+camSettingsProperties =
+    [ "TAGame.CameraSettingsActor_TA:ProfileSettings"
+    , "TAGame.PRI_TA:CameraSettings"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are demolitions.
+demolishProperties :: Set.Set StrictText.Text
+demolishProperties =
+    [ "TAGame.Car_TA:ReplicatedDemolish"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are enumerations.
+enumProperties :: Set.Set StrictText.Text
+enumProperties =
+    [ "Engine.Actor:Role"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are explosions.
+explosionProperties :: Set.Set StrictText.Text
+explosionProperties =
+    [ "TAGame.Ball_TA:ReplicatedExplosionData"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are flagged integers.
+flaggedIntProperties :: Set.Set StrictText.Text
+flaggedIntProperties =
+    [ "Engine.Actor:Owner"
+    , "Engine.Actor:ReplicatedCollisionType"
+    , "Engine.GameReplicationInfo:GameClass"
+    , "Engine.Pawn:PlayerReplicationInfo"
+    , "Engine.PlayerReplicationInfo:Team"
+    , "Engine.TeamInfo:TeamIndex"
+    , "TAGame.Ball_TA:GameEvent"
+    , "TAGame.CameraSettingsActor_TA:PRI"
+    , "TAGame.CarComponent_TA:Vehicle"
+    , "TAGame.CrowdActor_TA:GameEvent"
+    , "TAGame.CrowdActor_TA:ReplicatedOneShotSound"
+    , "TAGame.CrowdManager_TA:GameEvent"
+    , "TAGame.CrowdManager_TA:ReplicatedGlobalOneShotSound"
+    , "TAGame.PRI_TA:PersistentCamera"
+    , "TAGame.PRI_TA:ReplicatedGameEvent"
+    , "TAGame.Team_TA:GameEvent"
+    , "TAGame.Team_TA:LogoData"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are floats.
+floatProperties :: Set.Set StrictText.Text
+floatProperties =
+    [ "Engine.Actor:DrawScale"
+    , "TAGame.Ball_TA:ReplicatedAddedCarBounceScale"
+    , "TAGame.Ball_TA:ReplicatedBallMaxLinearSpeedScale"
+    , "TAGame.Ball_TA:ReplicatedBallScale"
+    , "TAGame.Ball_TA:ReplicatedWorldBounceScale"
+    , "TAGame.CarComponent_Boost_TA:BoostModifier"
+    , "TAGame.CarComponent_Boost_TA:RechargeDelay"
+    , "TAGame.CarComponent_Boost_TA:RechargeRate"
+    , "TAGame.CarComponent_FlipCar_TA:FlipCarTime"
+    , "TAGame.CrowdActor_TA:ModifiedNoise"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are game modes.
+gameModeProperties :: Set.Set StrictText.Text
+gameModeProperties =
+    [ "TAGame.GameEvent_TA:GameMode"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are integers.
+intProperties :: Set.Set StrictText.Text
+intProperties =
+    [ "Engine.PlayerReplicationInfo:PlayerID"
+    , "Engine.PlayerReplicationInfo:Score"
+    , "Engine.TeamInfo:Score"
+    , "ProjectX.GRI_X:ReplicatedGameMutatorIndex"
+    , "ProjectX.GRI_X:ReplicatedGamePlaylist"
+    , "TAGame.CrowdActor_TA:ReplicatedCountDownNumber"
+    , "TAGame.GameEvent_Soccar_TA:RoundNum"
+    , "TAGame.GameEvent_Soccar_TA:SecondsRemaining"
+    , "TAGame.GameEvent_TA:BotSkill"
+    , "TAGame.GameEvent_TA:ReplicatedGameStateTimeRemaining"
+    , "TAGame.GameEvent_TA:ReplicatedStateName"
+    , "TAGame.GameEvent_Team_TA:MaxTeamSize"
+    , "TAGame.PRI_TA:MatchAssists"
+    , "TAGame.PRI_TA:MatchGoals"
+    , "TAGame.PRI_TA:MatchSaves"
+    , "TAGame.PRI_TA:MatchScore"
+    , "TAGame.PRI_TA:MatchShots"
+    , "TAGame.PRI_TA:Title"
+    , "TAGame.PRI_TA:TotalXP"
+    , "TAGame.Team_Soccar_TA:GameScore"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are online loadouts.
+loadoutOnlineProperties :: Set.Set StrictText.Text
+loadoutOnlineProperties =
+    [ "TAGame.PRI_TA:ClientLoadoutOnline"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are loadouts.
+loadoutProperties :: Set.Set StrictText.Text
+loadoutProperties =
+    [ "TAGame.PRI_TA:ClientLoadout"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are locations.
+locationProperties :: Set.Set StrictText.Text
+locationProperties =
+    [ "Engine.Actor:RelativeLocation"
+    , "TAGame.CarComponent_Dodge_TA:DodgeTorque"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are music stingers.
+musicStingerProperties :: Set.Set StrictText.Text
+musicStingerProperties =
+    [ "TAGame.GameEvent_Soccar_TA:ReplicatedMusicStinger"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are pickups.
+pickupProperties :: Set.Set StrictText.Text
+pickupProperties =
+    [ "TAGame.VehiclePickup_TA:ReplicatedPickupData"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are private match settings.
+privateMatchSettingsProperties :: Set.Set StrictText.Text
+privateMatchSettingsProperties =
+    [ "TAGame.GameEvent_SoccarPrivate_TA:MatchSettings"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are qwords.
+qWordProperties :: Set.Set StrictText.Text
+qWordProperties =
+    [ "ProjectX.GRI_X:GameServerID"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are relation rotations.
+relativeRotationProperties :: Set.Set StrictText.Text
+relativeRotationProperties =
+    [ "Engine.Actor:RelativeRotation"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are reservations.
+reservationProperties :: Set.Set StrictText.Text
+reservationProperties =
+    [ "ProjectX.GRI_X:Reservations"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are rigid body states.
+rigidBodyStateProperties :: Set.Set StrictText.Text
+rigidBodyStateProperties =
+    [ "TAGame.RBActor_TA:ReplicatedRBState"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are strings.
+stringProperties :: Set.Set StrictText.Text
+stringProperties =
+    [ "Engine.GameReplicationInfo:ServerName"
+    , "Engine.PlayerReplicationInfo:PlayerName"
+    , "Engine.PlayerReplicationInfo:RemoteUserData"
+    , "TAGame.GRI_TA:NewDedicatedServerIP"
+    , "TAGame.Team_TA:CustomTeamName"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are team paints.
+teamPaintProperties :: Set.Set StrictText.Text
+teamPaintProperties =
+    [ "TAGame.Car_TA:TeamPaint"
+    ] & map StrictText.pack & Set.fromList
+
+
+-- | A set of properties that are unique IDs.
+uniqueIdProperties :: Set.Set StrictText.Text
+uniqueIdProperties =
+    [ "Engine.PlayerReplicationInfo:UniqueId"
+    -- , "TAGame.PRI_TA:PartyLeader"
+    ] & map StrictText.pack & Set.fromList
diff --git a/library/Octane/Data/RocketTrails.hs b/library/Octane/Data/RocketTrails.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Data/RocketTrails.hs
@@ -0,0 +1,63 @@
+module Octane.Data.RocketTrails (rocketTrails) where
+
+import Data.Function ((&))
+
+import qualified Data.Bimap as Bimap
+import qualified Data.Text as StrictText
+
+
+-- | A one-to-one mapping between rocket trail IDs and their names.
+rocketTrails :: Bimap.Bimap Int StrictText.Text
+rocketTrails =
+    [ ("(Alpha Reward) Gold Rush", 32) -- AlphaReward
+    , ("Accelerato", 524) -- MusicalNotes
+    , ("Batmobile", 868) -- DarkCar
+    , ("Battle-Stars", 545)
+    , ("Bubbles", 33)
+    , ("Burnout", 387)
+    , ("Candy Corn", 578)
+    , ("Confetti", 34)
+    , ("Datastream", 35) -- Digital
+    , ("Flamethrower Blue", 37)
+    , ("Flamethrower Green", 38)
+    , ("Flamethrower Pink", 39)
+    , ("Flamethrower Purple", 40)
+    , ("Flamethrower Red", 41)
+    , ("Flamethrower", 36)
+    , ("Flowers", 42)
+    , ("Grass", 43)
+    , ("Hydro", 69) -- Water
+    , ("Ion Blue", 45) -- LightTrail
+    , ("Ion Green", 46)
+    , ("Ion Pink", 47)
+    , ("Ion Purple", 48)
+    , ("Ion Red", 49)
+    , ("Ion Yellow", 50)
+    , ("Money", 51)
+    , ("Nitrous", 384)
+    , ("Nuts & Bolts", 544)
+    , ("OutaTime", 626) -- GreyCar
+    , ("Plasma", 52)
+    , ("Portal - Conversion Gel", 651)
+    , ("Portal - Propulsion Gel", 652)
+    , ("Portal - Reuplsion Gel", 653)
+    , ("Rainbow", 59)
+    , ("Sacred", 44) -- HolyLight
+    , ("Sandstorm", 664)
+    , ("Slime", 60)
+    , ("Snowflakes", 61)
+    , ("Sparkles", 62)
+    , ("Standard Blue", 64)
+    , ("Standard Pink", 65)
+    , ("Standard Purple", 66)
+    , ("Standard Red", 63)
+    , ("Standard Yellow", 67)
+    , ("Sweet Tooth", 68)
+    , ("Thermal Blue", 54)
+    , ("Thermal Green", 55)
+    , ("Thermal Pink", 56)
+    , ("Thermal Purple", 57)
+    , ("Thermal Yellow", 58)
+    , ("Thermal", 53) -- Propulsion
+    , ("Xmas", 635) -- Presents
+    ] & map (\ (v, k) -> (k, StrictText.pack v)) & Bimap.fromList
diff --git a/library/Octane/Data/Toppers.hs b/library/Octane/Data/Toppers.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Data/Toppers.hs
@@ -0,0 +1,78 @@
+module Octane.Data.Toppers (toppers) where
+
+import Data.Function ((&))
+
+import qualified Data.Bimap as Bimap
+import qualified Data.Text as StrictText
+
+
+-- | A one-to-one mapping between topper IDs and their names.
+toppers :: Bimap.Bimap Int StrictText.Text
+toppers =
+    [ ("None", 0)
+    , ("(Alpha Reward) Gold Cap", 224) -- AlphaHat
+    , ("Antlers", 774)
+    , ("Beret", 775)
+    , ("Biker Cap", 776)
+    , ("Birthday Cake", 789)
+    , ("Blitzen", 640) -- ReindeerAntlers
+    , ("Bobby Helmet", 227) -- BPH
+    , ("Boombox", 521)
+    , ("Bowler", 745) -- GentlemanHat
+    , ("Brodie Helmet", 743) -- DoughboyHelmet
+    , ("Brünnhilde", 241) -- Viking_Helmet
+    , ("Captain's Hat", 741)
+    , ("Cavalier", 580) -- Tricorn
+    , ("Chainsaw", 795)
+    , ("Chef's Hat", 756)
+    , ("Cherry Top", 237) -- PoliceSiren
+    , ("Christmas Tree", 639) -- RegularChristmasTree
+    , ("Cockroach", 769)
+    , ("Cow Skull", 685)
+    , ("deadmau5", 796)
+    , ("Deerstalker", 792)
+    , ("Derby", 790)
+    , ("Devil Horns", 229)
+    , ("Fez", 230)
+    , ("Fire Helmet", 231)
+    , ("Foam Hat", 390) -- TourneyWinner
+    , ("Fruit Hat", 744)
+    , ("Graduation Cap", 778)
+    , ("Halo", 232)
+    , ("Hard Hat", 233)
+    , ("Homburg", 742) -- DetectiveHat
+    , ("Locomotive", 547) -- TrainConductor
+    , ("Mariachi Hat", 234)
+    , ("Mohawk", 638)
+    , ("Mouse Trap", 782) -- RatTrap
+    , ("Paper Boat", 770)
+    , ("Pirate's Hat", 235)
+    , ("Pixelated Shades", 494) -- DWI
+    , ("Pizza Topper", 236)
+    , ("Plunger", 772)
+    , ("Police Hat", 746)
+    , ("Portal - Cake", 661)
+    , ("Propellerhead", 225) -- Beanie
+    , ("Pumpkin", 546) -- CatOLantern
+    , ("Rasta", 771)
+    , ("Rhino Horns", 780)
+    , ("Royal Crown", 228)
+    , ("Sad Sapling", 641) -- SadChristmasTree
+    , ("Santa", 642)
+    , ("Season 1 - Bronze", 705)
+    , ("Season 1 - Gold", 706)
+    , ("Season 1 - Platinum", 707)
+    , ("Season 1 - Silver", 708)
+    , ("Shark Fin", 579)
+    , ("Shuriken", 773)
+    , ("Sombrero", 238)
+    , ("Taxi Topper", 239)
+    , ("Tiara", 495)
+    , ("Top Hat", 240)
+    , ("Traffic Cone", 747)
+    , ("Unicorn", 748) -- Unihorn
+    , ("Witch's Hat", 242)
+    , ("Wizard Hat", 243)
+    , ("Work Boot", 740)
+    , ("Worms W.M.D.", 1028)
+    ] & map (\ (v, k) -> (k, StrictText.pack v)) & Bimap.fromList
diff --git a/library/Octane/Data/Wheels.hs b/library/Octane/Data/Wheels.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Data/Wheels.hs
@@ -0,0 +1,51 @@
+module Octane.Data.Wheels (wheels) where
+
+import Data.Function ((&))
+
+import qualified Data.Bimap as Bimap
+import qualified Data.Text as StrictText
+
+
+-- | A one-to-one mapping between wheels IDs and their names.
+wheels :: Bimap.Bimap Int StrictText.Text
+wheels =
+    [ ("(Alpha Reward) Goldstone", 358) -- AlphaRim
+    , ("Alchemist", 374) -- Revolution
+    , ("Almas", 364) -- Crypt
+    , ("Armadillo", 618)
+    , ("Batmobile", 874) -- Darkcar
+    , ("Bender", 360) -- Ballistic
+    , ("Carriage", 549)
+    , ("Cog", 716) -- TODO: What is this called in game?
+    , ("Cristiano", 386) -- SoccerBall
+    , ("DeLorean Time Machine", 609) -- GreyCar
+    , ("Dieci", 363) -- Caliber
+    , ("Falco", 382) -- Vulcan
+    , ("Foreman", 372) -- OffRoad
+    , ("Grog", 613) -- WastelandTruck
+    , ("Hogsticker", 719) -- Warthog
+    , ("Invader", 380) -- Triad
+    , ("Lowrider", 369)
+    , ("Lucci", 361) -- Bling
+    , ("Mountaineer", 375) -- SnowTire
+    , ("Neptune", 370) -- Mob
+    , ("Octavian", 379) -- Tarantula
+    , ("OEM", 376) -- Star
+    , ("Rat Rod", 366) -- Dynamo
+    , ("Ripper", 690) -- Spiked
+    , ("Scarab", 540) -- Scarab
+    , ("Servergate", 519) -- SkullxBones
+    , ("Spinner", 388) -- Spinner
+    , ("Spyder", 371) -- Ninja
+    , ("Stallion", 365) -- DeepDish
+    , ("Stern", 368) -- Hydra
+    , ("Sunburt", 383) -- Wynd
+    , ("Sweet Tooth", 378)
+    , ("Tempest", 377) -- Storm
+    , ("Tomahawk", 362) -- Brink
+    , ("Trahere", 367) -- Forge
+    , ("Tunica", 359) -- Atlantis
+    , ("Veloce", 373) -- Pedigree
+    , ("Vortex", 381)
+    , ("Zippy", 518) -- SingleSpoke
+    ] & map (\ (v, k) -> (k, StrictText.pack v)) & Bimap.fromList
diff --git a/library/Octane/FullReplay.hs b/library/Octane/FullReplay.hs
deleted file mode 100644
--- a/library/Octane/FullReplay.hs
+++ /dev/null
@@ -1,483 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.FullReplay
-    ( FullReplay(..)
-    , parseReplay
-    , parseReplayFile
-    , unsafeParseReplay
-    , unsafeParseReplayFile
-    ) where
-
-import Data.Aeson ((.=))
-import Data.Function ((&))
-import Data.Monoid ((<>))
-import Prelude ((==), (/=), (&&))
-
-import qualified Control.DeepSeq as DeepSeq
-import qualified Control.Monad as Monad
-import qualified Data.Aeson as Aeson
-import qualified Data.Bimap as Bimap
-import qualified Data.Binary as Binary
-import qualified Data.ByteString.Lazy as ByteString
-import qualified Data.Foldable as Foldable
-import qualified Data.IntMap.Strict as IntMap
-import qualified Data.Map.Strict as Map
-import qualified Data.Text as Text
-import qualified Data.Version as Version
-import qualified GHC.Generics as Generics
-import qualified Octane.Data as Data
-import qualified Octane.Parser as Parser
-import qualified Octane.Parser.Garage as Garage
-import qualified Octane.Type as Type
-import qualified Prelude
-
-
-newtype FullReplay = FullReplay
-    { unpackFullReplay :: (Type.Replay, [Parser.Frame])
-    } deriving (Generics.Generic, Prelude.Show)
-
-instance DeepSeq.NFData FullReplay
-
-instance Aeson.ToJSON FullReplay where
-    toJSON fullReplay = do
-        Aeson.object
-            [ "Version" .= getVersion fullReplay
-            , "Metadata" .= getMetadata fullReplay
-            , "Levels" .= getLevels fullReplay
-            , "Messages" .= getMessages fullReplay
-            , "TickMarks" .= getTickMarks fullReplay
-            , "Frames" .= getFrames fullReplay
-            ]
-
-
-newFullReplay :: Type.Replay -> [Parser.Frame] -> FullReplay
-newFullReplay replay frames = FullReplay (replay, frames)
-
-
-getVersion :: FullReplay -> Prelude.String
-getVersion fullReplay =
-    [ fullReplay
-        & unpackFullReplay
-        & Prelude.fst
-        & Type.replayVersion1
-        & Type.unpackInt32
-        & Prelude.fromIntegral
-    , fullReplay
-        & unpackFullReplay
-        & Prelude.fst
-        & Type.replayVersion2
-        & Type.unpackInt32
-        & Prelude.fromIntegral
-    ] & Version.makeVersion & Version.showVersion
-
-
-getMetadata :: FullReplay -> Map.Map Text.Text Aeson.Value
-getMetadata fullReplay = fullReplay
-    & unpackFullReplay
-    & Prelude.fst
-    & Type.replayProperties
-    & Type.unpackDictionary
-    & Map.mapKeys Type.unpackText
-    & Map.map Aeson.toJSON
-
-
-getLevels :: FullReplay -> [Text.Text]
-getLevels fullReplay = fullReplay
-    & unpackFullReplay
-    & Prelude.fst
-    & Type.replayLevels
-    & Type.unpackList
-    & Prelude.map Type.unpackText
-
-
-getMessages :: FullReplay -> Map.Map Text.Text Type.Text
-getMessages fullReplay = fullReplay
-    & unpackFullReplay
-    & Prelude.fst
-    & Type.replayMessages
-    & Type.unpackList
-    & Prelude.map (\ message ->
-        ( message
-            & Type.messageFrame
-            & Type.unpackInt32
-            & Prelude.show
-            & Text.pack
-        , message & Type.messageContent
-        ))
-    & Map.fromList
-
-
-getTickMarks :: FullReplay -> Map.Map Text.Text Type.Text
-getTickMarks fullReplay = fullReplay
-    & unpackFullReplay
-    & Prelude.fst
-    & Type.replayMarks
-    & Type.unpackList
-    & Prelude.map (\ mark ->
-        ( mark
-            & Type.markFrame
-            & Type.unpackInt32
-            & Prelude.show
-            & Text.pack
-        , mark & Type.markLabel
-        ))
-    & Map.fromList
-
-
-getFrames :: FullReplay -> [Map.Map Text.Text Aeson.Value]
-getFrames fullReplay = fullReplay
-    & unpackFullReplay
-    & Prelude.snd
-    & Foldable.foldl'
-        (\ (state, frames) frame -> let
-            newState = updateState frame state
-            minimalFrame = getDelta state frame
-            in (newState, minimalFrame : frames))
-        (initialState, [])
-    & Prelude.snd
-    & Prelude.reverse
-    & Prelude.map (\ frame -> Map.fromList
-        [ ("IsKeyFrame", frame & Parser.frameIsKeyFrame & Aeson.toJSON)
-        , ("Number", frame & Parser.frameNumber & Aeson.toJSON)
-        , ("Time", frame & Parser.frameTime & Aeson.toJSON)
-        , ("Delta", frame & Parser.frameDelta & Aeson.toJSON)
-        , ("Spawned", getSpawnedActors frame)
-        , ("Updated", getUpdatedActors frame)
-        , ("Destroyed", getDestroyedActors frame)
-        ])
-
-
--- { actor id => (alive?, { property name => property value } ) }
-type State = IntMap.IntMap (Prelude.Bool, Map.Map Text.Text Parser.PropValue)
-
-
-initialState :: State
-initialState = IntMap.empty
-
-
-updateState :: Parser.Frame -> State -> State
-updateState frame state1 = let
-    spawned = frame
-        & Parser.frameReplications
-        & Prelude.filter (\ replication -> replication
-            & Parser.replicationState
-            & (== Parser.RSOpening))
-        & Prelude.map Parser.replicationActorId
-    state2 = spawned
-        & Prelude.foldr
-            (IntMap.alter (\ maybeValue -> Prelude.Just (case maybeValue of
-                Prelude.Nothing -> (Prelude.True, Map.empty)
-                Prelude.Just (_, properties) -> (Prelude.True, properties))))
-            state1
-
-    destroyed = frame
-        & Parser.frameReplications
-        & Prelude.filter (\ replication -> replication
-            & Parser.replicationState
-            & (== Parser.RSClosing))
-        & Prelude.map Parser.replicationActorId
-    state3 = destroyed
-        & Prelude.foldr
-            (IntMap.alter (\ maybeValue -> Prelude.Just (case maybeValue of
-                Prelude.Nothing -> (Prelude.False, Map.empty)
-                Prelude.Just (_, properties) -> (Prelude.False, properties))))
-            state2
-
-    updated = frame
-        & Parser.frameReplications
-        & Prelude.filter (\ replication -> replication
-            & Parser.replicationState
-            & (== Parser.RSExisting))
-    state4 = updated
-        & Prelude.foldr
-            (\ replication -> IntMap.alter
-                (\ maybeValue -> Prelude.Just (case maybeValue of
-                    Prelude.Nothing ->
-                        (Prelude.True, Parser.replicationProperties replication)
-                    Prelude.Just (alive, properties) ->
-                        ( alive
-                        , Map.union
-                            (Parser.replicationProperties replication)
-                            properties
-                        )))
-                (Parser.replicationActorId replication))
-            state3
-
-    in state4
-
-
-getDelta :: State -> Parser.Frame -> Parser.Frame
-getDelta state frame = let
-    newReplications = frame
-        & Parser.frameReplications
-        -- Remove replications that aren't actually new.
-        & reject (\ replication -> let
-            isOpening = Parser.replicationState replication == Parser.RSOpening
-            actorId = Parser.replicationActorId replication
-            currentState = IntMap.lookup actorId state
-            isAlive = Prelude.fmap Prelude.fst currentState
-            wasAlreadyAlive = isAlive == Prelude.Just Prelude.True
-            in isOpening && wasAlreadyAlive)
-        -- Remove properties that haven't changed.
-        & Prelude.map (\ replication ->
-            if Parser.replicationState replication == Parser.RSExisting
-            then let
-                actorId = Parser.replicationActorId replication
-                currentState = IntMap.findWithDefault
-                    (Prelude.True, Map.empty) actorId state
-                currentProperties = Prelude.snd currentState
-                newProperties = Parser.replicationProperties replication
-                changes = newProperties
-                    & Map.filterWithKey (\ name newValue -> let
-                        oldValue = Map.lookup name currentProperties
-                        in Prelude.Just newValue /= oldValue)
-                in replication { Parser.replicationProperties = changes }
-            else replication)
-    in frame { Parser.frameReplications = newReplications }
-
-
-reject :: (a -> Prelude.Bool) -> [a] -> [a]
-reject p xs = Prelude.filter (\ x -> Prelude.not (p x)) xs
-
-
-getSpawnedActors :: Parser.Frame -> Aeson.Value
-getSpawnedActors frame = frame
-    & Parser.frameReplications
-    & Prelude.filter (\ replication -> replication
-        & Parser.replicationState
-        & (== Parser.RSOpening))
-    & Prelude.map (\ replication ->
-        ( replication
-            & Parser.replicationActorId
-            & Prelude.show
-            & Text.pack
-        , Aeson.object
-            [ ("Name", replication
-                & Parser.replicationObjectName
-                & Aeson.toJSON)
-            , ("Class", replication
-                & Parser.replicationClassName
-                & Aeson.toJSON)
-            , ("Position", replication
-                & Parser.replicationInitialization
-                & Prelude.fmap Parser.classInitLocation
-                & Monad.join
-                & Aeson.toJSON)
-            , ("Rotation", replication
-                & Parser.replicationInitialization
-                & Prelude.fmap Parser.classInitRotation
-                & Monad.join
-                & Aeson.toJSON)
-            ]
-        ))
-    & Aeson.object
-
-
-getUpdatedActors :: Parser.Frame -> Aeson.Value
-getUpdatedActors frame = frame
-    & Parser.frameReplications
-    & Prelude.filter (\ replication -> replication
-        & Parser.replicationState
-        & (== Parser.RSExisting))
-    & Prelude.map (\ replication ->
-        ( replication
-            & Parser.replicationActorId
-            & Prelude.show
-            & Text.pack
-        , replication
-            & Parser.replicationProperties
-            & Map.map (\ property -> Aeson.object
-                [ ("Type", getPropertyName property)
-                , ("Value", getPropertyValue property)
-                ])
-        ))
-    & reject (\ (_, properties) -> Map.null properties)
-    & Prelude.map (\ (actorId, properties) ->
-        (actorId, Aeson.toJSON properties))
-    & Aeson.object
-
-
-getPropertyName :: Parser.PropValue -> Aeson.Value
-getPropertyName property = case property of
-    Parser.PBoolean _ -> "Boolean"
-    Parser.PByte _ -> "Byte"
-    Parser.PCamSettings _ _ _ _ _ _ -> "CameraSettings"
-    Parser.PDemolish _ _ _ _ _ _ -> "Demolition"
-    Parser.PEnum _ _ -> "Enum"
-    Parser.PExplosion _ _ _ -> "Explosion"
-    Parser.PFlaggedInt _ _ -> "FlaggedInt"
-    Parser.PFloat _ -> "Float"
-    Parser.PGameMode _ -> "GameMode"
-    Parser.PInt _ -> "Int"
-    Parser.PLoadout _ _ _ _ _ _ _ _ _ -> "Loadout"
-    Parser.PLoadoutOnline _ _ _ _ -> "OnlineLoadout"
-    Parser.PLocation _ -> "Position"
-    Parser.PMusicStinger _ _ _ -> "MusicStinger"
-    Parser.PPickup _ _ _ -> "Pickup"
-    Parser.PPrivateMatchSettings _ _ _ _ _ _ -> "PrivateMatchSettings"
-    Parser.PQWord _ _ -> "QWord"
-    Parser.PRelativeRotation _ -> "RelativeRotation"
-    Parser.PReservation _ _ _ _ _ _ _ -> "Reservation"
-    Parser.PRigidBodyState _ _ _ _ _ -> "RigidBodyState"
-    Parser.PString _ -> "String"
-    Parser.PTeamPaint _ _ _ _ _ -> "Paint"
-    Parser.PUniqueId _ _ _ -> "UniqueId"
-
-
-getPropertyValue :: Parser.PropValue -> Aeson.Value
-getPropertyValue property = case property of
-    Parser.PBoolean x -> Aeson.toJSON x
-    Parser.PByte x -> Aeson.toJSON x
-    Parser.PCamSettings fov height angle distance stiffness swivelSpeed -> Aeson.object
-        [ ("FOV", Aeson.toJSON fov)
-        , ("Height", Aeson.toJSON height)
-        , ("Angle", Aeson.toJSON angle)
-        , ("Distance", Aeson.toJSON distance)
-        , ("Stiffness", Aeson.toJSON stiffness)
-        , ("SwivelSpeed", Aeson.toJSON swivelSpeed)
-        ]
-    Parser.PDemolish a b c d e f -> Aeson.toJSON (a, b, c, d, e, f)
-    Parser.PEnum x y -> Aeson.toJSON (x, y)
-    Parser.PExplosion a b c -> Aeson.toJSON (a, b, c)
-    Parser.PFlaggedInt x y -> Aeson.toJSON (x, y)
-    Parser.PFloat x -> Aeson.toJSON x
-    Parser.PGameMode gameMode -> Aeson.object
-        [ ("Id", Aeson.toJSON gameMode)
-        , ("Name", Data.gameModes
-            & Bimap.lookup (Prelude.fromIntegral gameMode)
-            & (\ x -> x :: Prelude.Maybe Text.Text)
-            & Aeson.toJSON)
-        ]
-    Parser.PInt x -> Aeson.toJSON x
-    Parser.PLoadout version body decal wheels rocketTrail antenna topper x y -> Aeson.object
-        [ ("Version", Aeson.toJSON version)
-        , ("Body", Aeson.object
-            [ ("Id", Aeson.toJSON body)
-            , ("Name", body & Garage.getBody & Aeson.toJSON)
-            ])
-        , ("Decal", Aeson.object
-            [ ("Id", Aeson.toJSON decal)
-            , ("Name", decal & Garage.getDecal & Aeson.toJSON)
-            ])
-        , ("Wheels", Aeson.object
-            [ ("Id", Aeson.toJSON wheels)
-            , ("Name", wheels & Garage.getWheels & Aeson.toJSON)
-            ])
-        , ("RocketTrail", Aeson.object
-            [ ("Id", Aeson.toJSON rocketTrail)
-            , ("Name", rocketTrail & Garage.getRocketTrail & Aeson.toJSON)
-            ])
-        , ("Antenna", Aeson.object
-            [ ("Id", Aeson.toJSON antenna)
-            , ("Name", antenna & Garage.getAntenna & Aeson.toJSON)
-            ])
-        , ("Topper", Aeson.object
-            [ ("Id", Aeson.toJSON topper)
-            , ("Name", topper & Garage.getTopper & Aeson.toJSON)
-            ])
-        , ("Unknown1", Aeson.toJSON x)
-        , ("Unknown2", Aeson.toJSON y)
-        ]
-    Parser.PLoadoutOnline a b c d -> Aeson.toJSON (a, b, c, d)
-    Parser.PLocation x -> Aeson.toJSON x
-    Parser.PMusicStinger a b c -> Aeson.toJSON (a, b, c)
-    Parser.PPickup a b c -> Aeson.toJSON (a, b, c)
-    Parser.PPrivateMatchSettings mutators joinableBy maxPlayers name password x -> Aeson.object
-        [ ("Mutators", Aeson.toJSON mutators)
-        , ("JoinableBy", Aeson.toJSON joinableBy)
-        , ("MaxPlayers", Aeson.toJSON maxPlayers)
-        , ("Name", Aeson.toJSON name)
-        , ("Password", Aeson.toJSON password)
-        , ("Unknown", Aeson.toJSON x)
-        ]
-    Parser.PQWord a b -> Aeson.toJSON (a, b)
-    Parser.PRelativeRotation x -> Aeson.toJSON x
-    Parser.PReservation number systemId remoteId localId name x y -> Aeson.object
-        [ ("Number", Aeson.toJSON number)
-        , ("SystemId", Aeson.toJSON systemId)
-        , ("RemoteId", Aeson.toJSON remoteId)
-        , ("LocalId", Aeson.toJSON localId)
-        , ("Name", Aeson.toJSON name)
-        , ("Unknown1", Aeson.toJSON x)
-        , ("Unknown2", Aeson.toJSON y)
-        ]
-    Parser.PRigidBodyState sleeping position rotation linear angular -> Aeson.object
-        [ ("Sleeping", Aeson.toJSON sleeping)
-        , ("Position", Aeson.toJSON position)
-        , ("Rotation", Aeson.toJSON rotation)
-        , ("LinearVelocity", Aeson.toJSON linear)
-        , ("AngularVelocity", Aeson.toJSON angular)
-        ]
-    Parser.PString x -> Aeson.toJSON x
-    Parser.PTeamPaint team color1 color2 finish1 finish2 -> Aeson.object
-        [ ("Team", Aeson.toJSON team)
-        , ("PrimaryColor", Aeson.toJSON color1)
-        , ("AccentColor", Aeson.toJSON color2)
-        , ("PrimaryFinish", Aeson.object
-            [ ("Id", Aeson.toJSON finish1)
-            , ("Name", finish1 & Garage.getFinish & Aeson.toJSON)
-            ])
-        , ("AccentFinish", Aeson.object
-            [ ("Id", Aeson.toJSON finish2)
-            , ("Name", finish2 & Garage.getFinish & Aeson.toJSON)
-            ])
-        ]
-    Parser.PUniqueId systemId remoteId localId -> Aeson.object
-        [ ("System", case systemId of
-            0 -> "Local"
-            1 -> "Steam"
-            2 -> "PlayStation"
-            4 -> "Xbox"
-            _ -> Aeson.String ("Unknown system " <> Text.pack (Prelude.show systemId)))
-        , ("Remote", case remoteId of
-            Parser.SplitscreenId x -> Aeson.toJSON x
-            Parser.SteamId x -> Aeson.toJSON x
-            Parser.PlayStationId x -> Aeson.toJSON x
-            Parser.XboxId x -> Aeson.toJSON x)
-        , ("Local", Aeson.toJSON localId)
-        ]
-
-getDestroyedActors :: Parser.Frame -> Aeson.Value
-getDestroyedActors frame = frame
-    & Parser.frameReplications
-    & Prelude.filter (\ replication -> replication
-        & Parser.replicationState
-        & (== Parser.RSClosing))
-    & Prelude.map Parser.replicationActorId
-    & Aeson.toJSON
-
-
-parseReplay :: ByteString.ByteString -> Prelude.Either Text.Text FullReplay
-parseReplay bytes = do
-    case Binary.decodeOrFail bytes of
-        Prelude.Left (_, _, message) -> do
-            Prelude.Left (Text.pack message)
-        Prelude.Right (_, _, replay) -> do
-            let frames = Parser.parseFrames replay
-            Prelude.Right (newFullReplay replay frames)
-
-
-parseReplayFile :: Prelude.FilePath -> Prelude.IO (Prelude.Either Text.Text FullReplay)
-parseReplayFile file = do
-    result <- Binary.decodeFileOrFail file
-    case result of
-        Prelude.Left (_, message) -> do
-            Prelude.pure (Prelude.Left (Text.pack message))
-        Prelude.Right replay -> do
-            let frames = Parser.parseFrames replay
-            Prelude.pure (Prelude.Right (newFullReplay replay frames))
-
-
-unsafeParseReplay :: ByteString.ByteString -> FullReplay
-unsafeParseReplay bytes = do
-    let replay = Binary.decode bytes
-    let frames = Parser.parseFrames replay
-    newFullReplay replay frames
-
-
-unsafeParseReplayFile :: Prelude.FilePath -> Prelude.IO FullReplay
-unsafeParseReplayFile file = do
-    replay <- Binary.decodeFile file
-    let frames = Parser.parseFrames replay
-    Prelude.pure (newFullReplay replay frames)
diff --git a/library/Octane/Json.hs b/library/Octane/Json.hs
deleted file mode 100644
--- a/library/Octane/Json.hs
+++ /dev/null
@@ -1,9 +0,0 @@
-module Octane.Json where
-
-import qualified Data.Aeson as Aeson
-import qualified Data.Aeson.Types as Aeson
-
-toJsonOptions :: String -> Aeson.Options
-toJsonOptions name = Aeson.defaultOptions
-    { Aeson.fieldLabelModifier = Aeson.camelTo2 '_' . drop (length name)
-    }
diff --git a/library/Octane/Main.hs b/library/Octane/Main.hs
--- a/library/Octane/Main.hs
+++ b/library/Octane/Main.hs
@@ -1,16 +1,77 @@
 module Octane.Main (main) where
 
-import qualified Control.DeepSeq as DeepSeq
 import qualified Data.Aeson as Aeson
-import qualified Data.ByteString.Lazy.Char8 as ByteString
-import qualified Octane.FullReplay as FullReplay
-import qualified Prelude
+import qualified Data.Binary as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Network.HTTP.Client as Client
+import qualified Network.HTTP.Client.TLS as TLS
+import qualified Octane.Type.Replay as Replay
 import qualified System.Environment as Environment
 
 
-main :: Prelude.IO ()
+-- | Octane's command-line entrypoint. It has three modes:
+--
+-- 1.  If no arguments are given, it will read from 'System.IO.stdin' and write
+--     to a JSON object to 'System.IO.stdout'.
+--
+--     > octane < a.replay > replay.json
+--
+-- 2.  If one argument is given, it will assume that argument is a path to a
+--     replay. Both local and remote (HTTP or HTTPS) paths are supported.
+--     Octane will read from the path and write a JSON object to
+--     'System.IO.stdout'.
+--
+--     > octane a.replay > replay.json
+--
+--     > octane https://media.rocketleaguereplays.com/uploads/replay_files/8D0940554D285C3F45109F85C79396A2.replay > replay.json
+--
+-- 3.  If multiple arguments are given, it will assume that those arguments are
+--     paths to replays, read from them, and write a JSON array of objects to
+--     'System.IO.stdout'.
+--
+--     > octane first.replay second.replay > replays.json
+main :: IO ()
 main = do
-    [file] <- Environment.getArgs
-    fullReplay <- FullReplay.unsafeParseReplayFile file
-    DeepSeq.deepseq fullReplay (Prelude.pure ())
-    ByteString.putStrLn (Aeson.encode fullReplay)
+    manager <- Client.newManager TLS.tlsManagerSettings
+    TLS.setGlobalManager manager
+
+    args <- Environment.getArgs
+    case args of
+        [] -> main0
+        [x] -> main1 x
+        xs -> main2 xs
+
+
+main0 :: IO ()
+main0 = do
+    LazyBytes.interact (\ input -> do
+        let replay = Binary.decode input
+        Aeson.encode (replay :: Replay.Replay))
+
+
+main1 :: String -> IO ()
+main1 x = do
+    replay <- decode x
+    let output = Aeson.encode replay
+    LazyBytes.putStr output
+
+
+main2 :: [String] -> IO ()
+main2 xs = do
+    replays <- mapM decode xs
+    let output = Aeson.encode replays
+    LazyBytes.putStr output
+
+
+decode :: String -> IO Replay.Replay
+decode x = do
+    case Client.parseUrl x of
+        Nothing -> do
+            let file = x
+            Binary.decodeFile file
+        Just request -> do
+            manager <- TLS.getGlobalManager
+            response <- Client.httpLbs request manager
+            let input = Client.responseBody response
+            let replay = Binary.decode input
+            pure replay
diff --git a/library/Octane/Parser.hs b/library/Octane/Parser.hs
deleted file mode 100644
--- a/library/Octane/Parser.hs
+++ /dev/null
@@ -1,823 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-
-module Octane.Parser where
-
-import Data.Function ((&))
-
-import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
-import qualified Data.Binary.Bits as BinaryBit
-import qualified Data.Binary.Bits.Get as Bits
-import qualified Data.Binary.Get as Binary
-import qualified Data.Bits as Bits
-import qualified Data.ByteString as BS
-import qualified Data.ByteString.Lazy as BSL
-import qualified Data.IntMap.Strict as IntMap
-import qualified Data.Map.Strict as Map
-import qualified Data.Set as Set
-import qualified Data.Text as Text
-import qualified Data.Word as Word
-import qualified GHC.Generics as Generics
-import qualified Octane.Data as Data
-import qualified Octane.Json as Json
-import qualified Octane.Parser.ClassPropertyMap as CPM
-import qualified Octane.Type as Type
-import qualified Octane.Utility as Utility
-import qualified Text.Printf as Printf
-
-parseFrames :: Type.Replay -> [Frame]
-parseFrames replay = let
-    numFrames = replay
-        & Type.replayProperties
-        & Type.unpackDictionary
-        & Map.lookup ("NumFrames" & Text.pack & Type.Text)
-        & (\ property -> case property of
-            Just (Type.IntProperty _ x) -> x & Type.unpackInt32 & fromIntegral
-            _ -> 0)
-    get = replay & extractContext & getFrames 0 numFrames & Bits.runBitGet
-    stream = replay & Type.replayStream & Type.unpackStream & BSL.fromStrict
-    (_context, frames) = Binary.runGet get stream
-    in frames
-
-getFrames :: Int -> Int -> Context -> Bits.BitGet (Context, [Frame])
-getFrames number numFrames context = do
-    if number >= numFrames
-    then return (context, [])
-    else do
-        isEmpty <- Bits.isEmpty
-        if isEmpty
-        then return (context, [])
-        else do
-            maybeFrame <- getMaybeFrame context number
-            case maybeFrame of
-                Nothing -> return (context, [])
-                Just (newContext, frame) -> do
-                    (newerContext, frames) <- getFrames (number + 1) numFrames newContext
-                    return (newerContext, (frame : frames))
-
-getMaybeFrame :: Context -> Int -> Bits.BitGet (Maybe (Context, Frame))
-getMaybeFrame context number = do
-    time <- getFloat32
-    delta <- getFloat32
-    if time == 0 && delta == 0
-    then return Nothing
-    else if time < 0.001 || delta < 0.001
-    then fail ("parsing previous frame probably failed. time: " ++ show time ++ ", delta: " ++ show delta)
-    else do
-        (newContext, frame) <- getFrame context number time delta
-        return (Just (newContext, frame))
-
-getFrame :: Context -> Int -> Time -> Delta -> Bits.BitGet (Context, Frame)
-getFrame context number time delta = do
-    (newContext, replications) <- getReplications context
-    let frame =
-            Frame
-            { frameNumber = number
-            , frameIsKeyFrame = context & contextKeyFrames & Set.member number
-            , frameTime = time
-            , frameDelta = delta
-            , frameReplications = replications
-            }
-    (newContext, frame) & DeepSeq.force & return
-
-getReplications :: Context -> Bits.BitGet (Context, [Replication])
-getReplications context = do
-    maybeReplication <- getMaybeReplication context
-    case maybeReplication of
-        Nothing -> return (context, [])
-        Just (newContext, replication) -> do
-            (newerContext, replications) <- getReplications newContext
-            return (newerContext, replication : replications)
-
-getMaybeReplication :: Context -> Bits.BitGet (Maybe (Context, Replication))
-getMaybeReplication context = do
-    hasReplication <- getBool
-    if Type.unpackBoolean hasReplication
-        then do
-            (newContext,replication) <- getReplication context
-            return (Just (newContext, replication))
-        else return Nothing
-
-getReplication :: Context -> Bits.BitGet (Context, Replication)
-getReplication context = do
-    actorId <- getActorId
-    isOpen <- getBool
-    let go =
-            if Type.unpackBoolean isOpen
-                then getOpenReplication
-                else getClosedReplication
-    go context actorId
-
-getOpenReplication :: Context
-                   -> ActorId
-                   -> Bits.BitGet (Context, Replication)
-getOpenReplication context actorId = do
-    isNew <- getBool
-    let go =
-            if Type.unpackBoolean isNew
-                then getNewReplication
-                else getExistingReplication
-    go context actorId
-
-getNewReplication :: Context
-                  -> ActorId
-                  -> Bits.BitGet (Context, Replication)
-getNewReplication context actorId = do
-    unknownFlag <- getBool
-    if Type.unpackBoolean unknownFlag
-        then fail "the unknown flag in a new replication is true! what does it mean?"
-        else pure ()
-    objectId <- getInt32
-    objectName <- case context & contextObjectMap & IntMap.lookup (Type.fromInt32 objectId) of
-        Nothing -> fail ("could not find object name for id " ++ show objectId)
-        Just x -> pure x
-    (classId, className) <- case CPM.getClass (contextObjectMap context) Data.objectToClass (contextClassMap context) (Type.fromInt32 objectId) of
-        Nothing -> fail ("could not find class for object id " ++ show objectId)
-        Just x -> pure x
-    classInit <- getClassInit className
-    let thing = Thing
-            { thingFlag = unknownFlag
-            , thingObjectId = objectId
-            , thingObjectName = objectName
-            , thingClassId = classId
-            , thingClassName = className
-            , thingClassInit = classInit
-            }
-    let things = contextThings context
-    let newThings = IntMap.insert actorId thing things
-    let newContext = context { contextThings = newThings }
-    return
-        ( newContext
-        , Replication
-          { replicationActorId = actorId
-          , replicationObjectName = objectName
-          , replicationClassName = className
-          , replicationState = RSOpening
-          , replicationInitialization = Just classInit
-          , replicationProperties = Map.empty
-          })
-
-getExistingReplication :: Context
-                       -> ActorId
-                       -> Bits.BitGet (Context, Replication)
-getExistingReplication context actorId = do
-    thing <- case context & contextThings & IntMap.lookup actorId of
-        Nothing -> fail ("could not find thing for actor id " ++ show actorId)
-        Just x -> pure x
-    props <- getProps context thing
-    return (context, Replication
-        { replicationActorId = actorId
-        , replicationObjectName = thingObjectName thing
-        , replicationClassName = thingClassName thing
-        , replicationState = RSExisting
-        , replicationInitialization = Nothing
-        , replicationProperties = props
-        })
-
-getClosedReplication :: Context
-                     -> ActorId
-                     -> Bits.BitGet (Context, Replication)
-getClosedReplication context actorId = do
-    thing <- case context & contextThings & IntMap.lookup actorId of
-        Nothing -> fail ("could not find thing for actor id " ++ show actorId)
-        Just x -> pure x
-    let newThings = context & contextThings & IntMap.delete actorId
-    let newContext = context { contextThings = newThings }
-    return
-        ( newContext
-        , Replication
-          { replicationActorId = actorId
-          , replicationObjectName = thingObjectName thing
-          , replicationClassName = thingClassName thing
-          , replicationState = RSClosing
-          , replicationInitialization = Nothing
-          , replicationProperties = Map.empty
-          })
-
-getProps :: Context -> Thing -> Bits.BitGet (Map.Map Text.Text PropValue)
-getProps context thing = do
-    maybeProp <- getMaybeProp context thing
-    case maybeProp of
-        Nothing -> return Map.empty
-        Just prop -> do
-            let k = propName prop
-            let v = propValue prop
-            let m = Map.singleton k v
-            props <- getProps context thing
-            return (Map.union m props)
-
-getMaybeProp :: Context -> Thing -> Bits.BitGet (Maybe Prop)
-getMaybeProp context thing = do
-    hasProp <- getBool
-    if Type.unpackBoolean hasProp
-    then do
-        prop <- getProp context thing
-        return (Just prop)
-    else return Nothing
-
-getProp :: Context -> Thing -> Bits.BitGet Prop
-getProp context thing = do
-    let classId = thing & thingClassId
-    props <- case context & contextClassPropertyMap & IntMap.lookup classId of
-        Nothing -> fail ("could not find property map for class id " ++ show classId)
-        Just x -> pure x
-    let maxId = props & IntMap.keys & (0 :) & maximum
-    pid <- getInt maxId
-    name <- case props & IntMap.lookup pid of
-        Nothing -> fail ("could not find property name for property id " ++ show pid)
-        Just x -> pure x
-    value <- getPropValue name
-    return (Prop { propName = name, propValue = value })
-
---
-
-getPropValue :: Text.Text -> Bits.BitGet PropValue
-getPropValue name = case Map.lookup name propertyNameToGet of
-    Nothing -> fail ("don't know how to read property " ++ show name)
-    Just get -> get
-
-propertyNameToGet :: Map.Map Text.Text (Bits.BitGet PropValue)
-propertyNameToGet =
-    [ (Data.booleanProperties, getBooleanProperty)
-    , (Data.byteProperties, getByteProperty)
-    , (Data.camSettingsProperties, getCamSettingsProperty)
-    , (Data.demolishProperties, getDemolishProperty)
-    , (Data.enumProperties, getEnumProperty)
-    , (Data.explosionProperties, getExplosionProperty)
-    , (Data.flaggedIntProperties, getFlaggedIntProperty)
-    , (Data.floatProperties, getFloatProperty)
-    , (Data.gameModeProperties, getGameModeProperty)
-    , (Data.intProperties, getIntProperty)
-    , (Data.loadoutOnlineProperties, getLoadoutOnlineProperty)
-    , (Data.loadoutProperties, getLoadoutProperty)
-    , (Data.locationProperties, getLocationProperty)
-    , (Data.musicStingerProperties, getMusicStingerProperty)
-    , (Data.pickupProperties, getPickupProperty)
-    , (Data.privateMatchSettingsProperties, getPrivateMatchSettingsProperty)
-    , (Data.qWordProperties, getQWordProperty)
-    , (Data.relativeRotationProperties, getRelativeRotationProperty)
-    , (Data.reservationProperties, getReservationProperty)
-    , (Data.rigidBodyStateProperties, getRigidBodyStateProperty)
-    , (Data.stringProperties, getStringProperty)
-    , (Data.teamPaintProperties, getTeamPaintProperty)
-    , (Data.uniqueIdProperties, getUniqueIdProperty)
-    , (Set.fromList [Text.pack "TAGame.PRI_TA:PartyLeader"], getPartyLeaderProperty)
-    ]
-        & concatMap (\ (ks, v) -> ks & Set.toList & map (\ k -> (k, v)))
-        & Map.fromList
-
-getBooleanProperty :: Bits.BitGet PropValue
-getBooleanProperty = do
-    bool <- getBool
-    return (PBoolean bool)
-
-getByteProperty :: Bits.BitGet PropValue
-getByteProperty = do
-    word <- getWord8
-    return (PByte word)
-
-getCamSettingsProperty :: Bits.BitGet PropValue
-getCamSettingsProperty = do
-    fov <- getFloat32
-    height <- getFloat32
-    angle <- getFloat32
-    distance <- getFloat32
-    stiffness <- getFloat32
-    swivelSpeed <- getFloat32
-    return (PCamSettings fov height angle distance stiffness swivelSpeed)
-
-getDemolishProperty :: Bits.BitGet PropValue
-getDemolishProperty = do
-    atkFlag <- getBool
-    atk <- getInt32
-    vicFlag <- getBool
-    vic <- getInt32
-    vec1 <- getVector
-    vec2 <- getVector
-    return (PDemolish atkFlag atk vicFlag vic vec1 vec2)
-
-getEnumProperty :: Bits.BitGet PropValue
-getEnumProperty = do
-    x <- Bits.getWord16be 10
-    y <- if x == 1023
-        then getBool
-        else fail ("unexpected enum value " ++ show x)
-    return (PEnum x y)
-
-getExplosionProperty :: Bits.BitGet PropValue
-getExplosionProperty = do
-    noGoal <- getBool
-    a <- if Type.unpackBoolean noGoal
-        then return Nothing
-        else fmap Just getInt32
-    b <- getVector
-    return (PExplosion noGoal a b)
-
-getFlaggedIntProperty :: Bits.BitGet PropValue
-getFlaggedIntProperty = do
-    flag <- getBool
-    int <- getInt32
-    return (PFlaggedInt flag int)
-
-getFloatProperty :: Bits.BitGet PropValue
-getFloatProperty = do
-    float <- getFloat32
-    return (PFloat float)
-
-getGameModeProperty :: Bits.BitGet PropValue
-getGameModeProperty = do
-    x <- Bits.getWord8 2
-    return (PGameMode x)
-
-getIntProperty :: Bits.BitGet PropValue
-getIntProperty = do
-    int <- getInt32
-    return (PInt int)
-
-getLoadoutOnlineProperty :: Bits.BitGet PropValue
-getLoadoutOnlineProperty = do
-    version <- getInt32
-    x <- getInt32
-    y <- getInt32
-    z <- if version >= 12
-        then do
-            value <- getInt8
-            return (Just value)
-        else return Nothing
-    return (PLoadoutOnline version x y z)
-
-getLoadoutProperty :: Bits.BitGet PropValue
-getLoadoutProperty = do
-    version <- getInt8
-    body <- getInt32
-    decal <- getInt32
-    wheels <- getInt32
-    rocketTrail <- getInt32
-    antenna <- getInt32
-    topper <- getInt32
-    g <- getInt32
-    h <- if version > 10
-        then do
-            value <- getInt32
-            return (Just value)
-        else return Nothing
-    return (PLoadout version body decal wheels rocketTrail antenna topper g h)
-
-getLocationProperty :: Bits.BitGet PropValue
-getLocationProperty = do
-    vector <- getVector
-    return (PLocation vector)
-
-getMusicStingerProperty :: Bits.BitGet PropValue
-getMusicStingerProperty = do
-    flag <- getBool
-    cue <- getInt32
-    trigger <- getInt8
-    return (PMusicStinger flag cue trigger)
-
-getPickupProperty :: Bits.BitGet PropValue
-getPickupProperty = do
-    instigator <- getBool
-    instigatorId <- if Type.unpackBoolean instigator
-        then fmap Just getInt32
-        else return Nothing
-    pickedUp <- getBool
-    return (PPickup instigator instigatorId pickedUp)
-
-getPrivateMatchSettingsProperty :: Bits.BitGet PropValue
-getPrivateMatchSettingsProperty = do
-    mutators <- getText
-    joinableBy <- getInt32
-    maxPlayers <- getInt32
-    gameName <- getText
-    password <- getText
-    flag <- getBool
-    return (PPrivateMatchSettings mutators joinableBy maxPlayers gameName password flag)
-
-getQWordProperty :: Bits.BitGet PropValue
-getQWordProperty = do
-    x <- getInt32
-    y <- getInt32
-    return (PQWord x y)
-
-getRelativeRotationProperty :: Bits.BitGet PropValue
-getRelativeRotationProperty = do
-    vector <- getFloatVector
-    return (PRelativeRotation vector)
-
-getReservationProperty :: Bits.BitGet PropValue
-getReservationProperty = do
-    -- I think this is the connection order. The first player to connect
-    -- gets number 0, and it goes up from there. The maximum is 7, which
-    -- would be a full 4x4 game.
-    number <- getInt7
-    (systemId, remoteId, localId) <- getUniqueId
-    playerName <- if systemId == 0 then return Nothing else do
-        string <- getText
-        return (Just string)
-    -- No idea what these two flags are. Might be for bots?
-    a <- getBool
-    b <- getBool
-    return (PReservation number systemId remoteId localId playerName a b)
-
-getRigidBodyStateProperty :: Bits.BitGet PropValue
-getRigidBodyStateProperty = do
-    flag <- getBool
-    position <- getVector
-    rotation <- getFloatVector
-    x <- if Type.unpackBoolean flag
-        then return Nothing
-        else fmap Just getVector
-    y <- if Type.unpackBoolean flag
-        then return Nothing
-        else fmap Just getVector
-    return (PRigidBodyState flag position rotation x y)
-
-getStringProperty :: Bits.BitGet PropValue
-getStringProperty = do
-    string <- getText
-    return (PString string)
-
-getTeamPaintProperty :: Bits.BitGet PropValue
-getTeamPaintProperty = do
-    team <- getInt8
-    primaryColor <- getInt8
-    accentColor <- getInt8
-    primaryFinish <- getInt32
-    accentFinish <- getInt32
-    return (PTeamPaint team primaryColor accentColor primaryFinish accentFinish)
-
-getUniqueIdProperty :: Bits.BitGet PropValue
-getUniqueIdProperty = do
-    (systemId, remoteId, localId) <- getUniqueId
-    return (PUniqueId systemId remoteId localId)
-
--- | Even though this is just a unique ID property, it must be handled
--- specially because it sometimes doesn't have the remote or local IDs.
-getPartyLeaderProperty :: Bits.BitGet PropValue
-getPartyLeaderProperty = do
-    systemId <- getSystemId
-    (remoteId, localId) <- if systemId == 0
-        then return (SplitscreenId Nothing, Nothing)
-        else do
-            remoteId <- getRemoteId systemId
-            localId <- getLocalId
-            return (remoteId, localId)
-    return (PUniqueId systemId remoteId localId)
-
---
-
-getFloat32 :: Bits.BitGet Type.Float32
-getFloat32 = BinaryBit.getBits unimportant
-
-getText :: Bits.BitGet Type.Text
-getText = BinaryBit.getBits unimportant
-
-getUniqueId :: Bits.BitGet (SystemId, RemoteId, LocalId)
-getUniqueId = do
-    systemId <- getSystemId
-    remoteId <- getRemoteId systemId
-    localId <- getLocalId
-    return (systemId, remoteId, localId)
-
-getSystemId :: Bits.BitGet SystemId
-getSystemId = getWord8
-
-getRemoteId :: SystemId -> Bits.BitGet RemoteId
-getRemoteId systemId = case systemId of
-    0 -> do
-        remoteId <- Bits.getByteString 3
-        if BS.all (\ byte -> byte == 0) remoteId
-            then 0 & Just & SplitscreenId & return
-            else fail ("unexpected splitscreen id " ++ show remoteId)
-    1 -> do
-        bytes <- Bits.getByteString 8
-        let remoteId = Binary.runGet
-                Binary.getWord64le
-                (bytes & BS.map Utility.reverseBits & BSL.fromStrict)
-        remoteId & SteamId & return
-    2 -> do
-        bytes <- Bits.getByteString 32
-        let remoteId = bytes
-                & BS.map Utility.reverseBits
-                & BS.unpack
-                & concatMap (\ b -> Printf.printf "%02x" b)
-                & Text.pack
-        remoteId & PlayStationId & return
-    4 -> do
-        bytes <- Bits.getByteString 8
-        let remoteId = Binary.runGet
-                Binary.getWord64le
-                (bytes & BS.map Utility.reverseBits & BSL.fromStrict)
-        remoteId & XboxId & return
-    _ -> fail ("unknown system id " ++ show systemId)
-
-getLocalId :: Bits.BitGet LocalId
-getLocalId = fmap Just getWord8
-
-type SystemId = Type.Word8
-
--- This is the number associated with a splitscreen player. So the first player
--- is 0, the second is 1, and so on.
--- - 0 "Someone"
--- - 1 "Someone (1)"
-type LocalId = Maybe Type.Word8
-
-data RemoteId
-    = SteamId !Word.Word64
-    | PlayStationId !Text.Text
-    | SplitscreenId !(Maybe Int)
-    | XboxId !Word.Word64
-    deriving (Eq, Generics.Generic, Show)
-
-instance DeepSeq.NFData RemoteId
-instance Aeson.ToJSON RemoteId
-
-data Prop = Prop
-    { propName :: !Text.Text
-    , propValue :: !PropValue
-    } deriving (Eq, Generics.Generic, Show)
-
-instance DeepSeq.NFData Prop
-instance Aeson.ToJSON Prop where
-    toJSON = Aeson.genericToJSON (Json.toJsonOptions "Prop")
-
-data PropValue
-    = PBoolean !Type.Boolean
-    | PByte !Type.Word8
-    | PCamSettings !Type.Float32 !Type.Float32 !Type.Float32 !Type.Float32 !Type.Float32 !Type.Float32
-    | PDemolish !Type.Boolean !Type.Int32 !Type.Boolean !Type.Int32 !(Vector Int) !(Vector Int)
-    | PEnum !Word.Word16 !Type.Boolean
-    | PExplosion !Type.Boolean !(Maybe Type.Int32) !(Vector Int)
-    | PFlaggedInt !Type.Boolean !Type.Int32
-    | PFloat !Type.Float32
-    | PGameMode !Word.Word8
-    | PInt !Type.Int32
-    | PLoadout !Type.Int8 !Type.Int32 !Type.Int32 !Type.Int32 !Type.Int32 !Type.Int32 !Type.Int32 !Type.Int32 !(Maybe Type.Int32)
-    | PLoadoutOnline !Type.Int32 !Type.Int32 !Type.Int32 !(Maybe Type.Int8)
-    | PLocation !(Vector Int)
-    | PMusicStinger !Type.Boolean !Type.Int32 !Type.Int8
-    | PPickup !Type.Boolean !(Maybe Type.Int32) !Type.Boolean
-    | PPrivateMatchSettings !Type.Text !Type.Int32 !Type.Int32 !Type.Text !Type.Text !Type.Boolean
-    | PQWord !Type.Int32 !Type.Int32
-    | PRelativeRotation !(Vector Float)
-    | PReservation !Int !SystemId !RemoteId !LocalId !(Maybe Type.Text) !Type.Boolean !Type.Boolean
-    | PRigidBodyState !Type.Boolean !(Vector Int) !(Vector Float) !(Maybe (Vector Int)) !(Maybe (Vector Int))
-    | PString !Type.Text
-    | PTeamPaint !Type.Int8 !Type.Int8 !Type.Int8 !Type.Int32 !Type.Int32
-    | PUniqueId !SystemId !RemoteId !LocalId
-    deriving (Eq, Generics.Generic, Show)
-
-instance DeepSeq.NFData PropValue
-instance Aeson.ToJSON PropValue where
-    toJSON = Aeson.genericToJSON (Json.toJsonOptions "PropValue")
-
--- | A frame in the net stream. Each frame has the time since the beginning of
--- the match, the time since the last frame, and a list of replications.
-data Frame = Frame
-    { frameNumber :: !Int
-    , frameIsKeyFrame :: !Bool
-    , frameTime :: !Time
-    , frameDelta :: !Delta
-    , frameReplications :: ![Replication]
-    } deriving (Eq,Generics.Generic,Show)
-
-instance DeepSeq.NFData Frame
-instance Aeson.ToJSON Frame where
-    toJSON = Aeson.genericToJSON (Json.toJsonOptions "Frame")
-
-data ReplicationState
-    = RSOpening
-    | RSExisting
-    | RSClosing
-    deriving (Eq, Generics.Generic, Show)
-
-instance DeepSeq.NFData ReplicationState
-instance Aeson.ToJSON ReplicationState where
-    toJSON rs = Aeson.toJSON (case rs of
-        RSOpening -> "opening"
-        RSExisting -> "existing"
-        RSClosing -> "closing")
-
--- | Replication information about an actor in the net stream.
-data Replication = Replication
-    { replicationActorId :: !Int
-    , replicationObjectName :: !Text.Text
-    , replicationClassName :: !Text.Text
-    , replicationState :: !ReplicationState
-    , replicationInitialization :: !(Maybe ClassInit)
-    , replicationProperties :: !(Map.Map Text.Text PropValue)
-    } deriving (Eq,Generics.Generic,Show)
-
-instance DeepSeq.NFData Replication
-instance Aeson.ToJSON Replication where
-    toJSON = Aeson.genericToJSON (Json.toJsonOptions "Replication")
-
-data Thing = Thing
-    { thingFlag :: !Type.Boolean
-    , thingObjectId :: !Type.Int32
-    , thingObjectName :: !Text.Text
-    , thingClassId :: !Int
-    , thingClassName :: !Text.Text
-    , thingClassInit :: !ClassInit
-    } deriving (Eq, Generics.Generic, Show)
-
-instance DeepSeq.NFData Thing
-
-type Time = Type.Float32
-
-type Delta = Type.Float32
-
-type ActorId = Int
-
-data Vector a = Vector
-    { vectorX :: !a
-    , vectorY :: !a
-    , vectorZ :: !a
-    } deriving (Eq, Generics.Generic, Show)
-
-instance (DeepSeq.NFData a) => DeepSeq.NFData (Vector a)
-instance (Aeson.ToJSON a) => Aeson.ToJSON (Vector a) where
-    toJSON vector = Aeson.toJSON
-        [ vectorX vector
-        , vectorY vector
-        , vectorZ vector
-        ]
-
-data ClassInit = ClassInit
-    { classInitLocation :: !(Maybe (Vector Int))
-    , classInitRotation :: !(Maybe (Vector Type.Int8))
-    } deriving (Eq, Generics.Generic, Show)
-
-instance DeepSeq.NFData ClassInit
-instance Aeson.ToJSON ClassInit where
-    toJSON = Aeson.genericToJSON (Json.toJsonOptions "ClassInit")
-
--- { class stream id => { property stream id => name } }
-type ClassPropertyMap = IntMap.IntMap (IntMap.IntMap Text.Text)
-
--- { stream id => object name }
-type ObjectMap = IntMap.IntMap Text.Text
-
--- { class name => class id }
-type ClassMap = Map.Map Text.Text Int
-
-data Context = Context
-    { contextObjectMap :: !ObjectMap
-    , contextClassPropertyMap :: !ClassPropertyMap
-    , contextThings :: !(IntMap.IntMap Thing)
-    , contextClassMap :: !ClassMap
-    , contextKeyFrames :: !(Set.Set Int)
-    } deriving (Eq, Generics.Generic, Show)
-
-instance DeepSeq.NFData Context
-
-extractContext :: Type.Replay -> Context
-extractContext replay =
-    Context
-    { contextObjectMap = CPM.getPropertyMap replay
-    , contextClassPropertyMap = CPM.getClassPropertyMap replay
-    , contextThings = IntMap.empty
-    , contextClassMap = CPM.getActorMap replay
-    , contextKeyFrames = replay
-        & Type.replayKeyFrames
-        & Type.unpackList
-        & map Type.keyFrameFrame
-        & map Type.unpackInt32
-        & map fromIntegral
-        & Set.fromList
-    }
-
-getVector :: Bits.BitGet (Vector Int)
-getVector = do
-    numBits <- getNumVectorBits
-    let bias = Bits.shiftL 1 (numBits + 1)
-    let maxBits = numBits + 2
-    let maxValue = 2 ^ maxBits
-    dx <- getInt maxValue
-    dy <- getInt maxValue
-    dz <- getInt maxValue
-    return
-        Vector
-        { vectorX = dx - bias
-        , vectorY = dy - bias
-        , vectorZ = dz - bias
-        }
-
-getVectorBytewise
-    :: Bits.BitGet (Vector Type.Int8)
-getVectorBytewise = do
-    hasX <- getBool
-    x <- if Type.unpackBoolean hasX then getInt8 else return 0
-    hasY <- getBool
-    y <- if Type.unpackBoolean hasY then getInt8 else return 0
-    hasZ <- getBool
-    z <- if Type.unpackBoolean hasZ then getInt8 else return 0
-    return
-        Vector
-        { vectorX = x
-        , vectorY = y
-        , vectorZ = z
-        }
-
-getFloatVector :: Bits.BitGet (Vector Float)
-getFloatVector = do
-    let maxValue = 1
-    let numBits = 16
-    x <- getFloat maxValue numBits
-    y <- getFloat maxValue numBits
-    z <- getFloat maxValue numBits
-    return Vector { vectorX = x, vectorY = y, vectorZ = z }
-
-getFloat :: Int -> Int -> Bits.BitGet Float
-getFloat maxValue numBits = do
-    let maxBitValue = (Bits.shiftL 1 (numBits - 1)) - 1
-    let bias = Bits.shiftL 1 (numBits - 1)
-    let serIntMax = Bits.shiftL 1 numBits
-    delta <- getInt serIntMax
-    let unscaledValue = delta - bias
-    if maxValue > maxBitValue
-    then do
-        let invScale = fromIntegral maxValue / fromIntegral maxBitValue
-        return (fromIntegral unscaledValue * invScale)
-    else do
-        let scale = fromIntegral maxBitValue / fromIntegral maxValue
-        let invScale = 1.0 / scale
-        return (fromIntegral unscaledValue * invScale)
-
-getClassInit :: Text.Text -> Bits.BitGet ClassInit
-getClassInit className = do
-    location <-
-        if Set.member className Data.locationClasses
-            then do
-                vector <- getVector
-                return (Just vector)
-            else return Nothing
-    rotation <-
-        if Set.member className Data.rotationClasses
-            then do
-                vector <- getVectorBytewise
-                return (Just vector)
-            else return Nothing
-    return
-        ClassInit
-        { classInitLocation = location
-        , classInitRotation = rotation
-        }
-
-bitSize
-    :: (Integral a)
-    => a -> a
-bitSize x = x & fromIntegral & logBase (2 :: Double) & ceiling
-
--- Reads an integer bitwise. The bits of the integer are backwards, so the
--- least significant bit is first. The argument is the maximum value this
--- integer can have. Bits will be read until the next bit would be greater than
--- the maximum value, or the number of bits necessary to reach the maximum
--- value has been reached, whichever comes first.
---
--- For example, if the maximum value is 4 and "11" has been read already,
--- nothing more will be read because another "1" would put the value over the
--- maximum.
-getInt
-    :: Int -> Bits.BitGet Int
-getInt maxValue = do
-    let maxBits = bitSize maxValue
-        go i value = do
-            let x = Bits.shiftL 1 i
-            if i < maxBits && value + x <= maxValue
-                then do
-                    bit <- getBool
-                    let newValue =
-                            if Type.unpackBoolean bit
-                                then value + x
-                                else value
-                    go (i + 1) newValue
-                else return value
-    go 0 0
-
-getInt32 :: Bits.BitGet Type.Int32
-getInt32 = BinaryBit.getBits unimportant
-
-getInt8 :: Bits.BitGet Type.Int8
-getInt8 = BinaryBit.getBits unimportant
-
-getWord8 :: Bits.BitGet Type.Word8
-getWord8 = BinaryBit.getBits unimportant
-
-getActorId :: Bits.BitGet Int
-getActorId = getInt 1024
-
-getNumVectorBits :: Bits.BitGet Int
-getNumVectorBits = getInt 19
-
-getInt7 :: Bits.BitGet Int
-getInt7 = getInt 7
-
-getBool :: Bits.BitGet Type.Boolean
-getBool = BinaryBit.getBits unimportant
-
--- | The 'getBits' function from "Data.Binary.Bits" requires a size parameter.
--- None of Octane's instances use it.
-unimportant :: Int
-unimportant = 0
diff --git a/library/Octane/Parser/ClassPropertyMap.hs b/library/Octane/Parser/ClassPropertyMap.hs
deleted file mode 100644
--- a/library/Octane/Parser/ClassPropertyMap.hs
+++ /dev/null
@@ -1,167 +0,0 @@
--- | This module is responsible for building the class property map, which maps
--- class IDs to a map of property IDs to property names. This map is the
--- cornerstone of the replay stream parser.
-module Octane.Parser.ClassPropertyMap where
-
-import Data.Function ((&))
-
-import qualified Data.Char as Char
-import qualified Data.IntMap.Strict as IntMap
-import qualified Data.List as List
-import qualified Data.Map.Strict as Map
-import qualified Data.Maybe as Maybe
-import qualified Data.Text as Text
-import qualified Octane.Type as Type
-
--- | The class property map is a map from class IDs in the stream to a map from
--- property IDs in the stream to property names.
-getClassPropertyMap :: Type.Replay -> IntMap.IntMap (IntMap.IntMap Text.Text)
-getClassPropertyMap replay = let
-    basicClassPropertyMap = getBasicClassPropertyMap replay
-    classMap = getClassMap replay
-    in replay
-        & getClassIds
-        & map (\ classId -> let
-            ownProperties = case IntMap.lookup classId basicClassPropertyMap of
-                Nothing -> IntMap.empty
-                Just x -> x
-            parentProperties = case IntMap.lookup classId classMap of
-                Nothing -> IntMap.empty
-                Just parentClassIds -> parentClassIds
-                    & map (\ parentClassId ->
-                        case IntMap.lookup parentClassId basicClassPropertyMap of
-                            Nothing -> IntMap.empty
-                            Just x -> x)
-                    & IntMap.unions
-            properties = IntMap.union ownProperties parentProperties
-            in (classId, properties))
-        & IntMap.fromList
-
--- | The class cache is a list of 3-tuples where the first element is a class
--- ID, the second is its cache ID, and the third is its parent's cache ID.
-getClassCache :: Type.Replay -> [(Int, Int, Int)]
-getClassCache replay = replay
-    & Type.replayCacheItems
-    & Type.unpackList
-    & map (\ x ->
-        ( x & Type.cacheItemClassId & Type.unpackInt32 & fromIntegral
-        , x & Type.cacheItemCacheId & Type.unpackInt32 & fromIntegral
-        , x & Type.cacheItemParentCacheId & Type.unpackInt32 & fromIntegral
-        ))
-
--- | The class IDs in a replay. Comes from the class cache.
-getClassIds :: Type.Replay -> [Int]
-getClassIds replay = replay
-    & getClassCache
-    & map (\ (x, _, _) -> x)
-
--- | Gets the parent class ID for the given parent cache ID. This is necessary
--- because there is not always a class with the given cache ID in the cache.
--- When that happens, the parent cache ID is decremented and tried again.
-getParentClassId :: Int -> [(Int, Int, Int)] -> Maybe Int
-getParentClassId parentCacheId xs =
-    case dropWhile (\ (_, cacheId, _) -> cacheId /= parentCacheId) xs of
-        [] -> if parentCacheId <= 0
-            then Nothing
-            else getParentClassId (parentCacheId - 1) xs
-        (parentClassId, _, _) : _ -> Just parentClassId
-
--- | The basic class map is a naive mapping from class ID to its parent class
--- ID. It's naive because it only maps the class ID to its immediate parent.
--- It does not chase the inheritance all the way down.
-getBasicClassMap :: Type.Replay -> IntMap.IntMap Int
-getBasicClassMap replay = replay
-    & getClassCache
-    & reverse
-    & List.tails
-    & Maybe.mapMaybe (\ xs -> case xs of
-        [] -> Nothing
-        (classId, _, parentCacheId) : ys -> do
-            parentClassId <- getParentClassId parentCacheId ys
-            return (classId, parentClassId))
-    & IntMap.fromList
-
--- | Given a naive mapping from class ID to its parent class ID, return all of
--- the parent IDs for a given class.
-getParentClassIds :: Int -> IntMap.IntMap Int -> [Int]
-getParentClassIds classId basicClassMap =
-    case IntMap.lookup classId basicClassMap of
-        Nothing -> []
-        Just parentClassId -> parentClassId : getParentClassIds parentClassId basicClassMap
-
--- | The class map is a mapping from a class ID to all of its parent class IDs.
-getClassMap :: Type.Replay -> IntMap.IntMap [Int]
-getClassMap replay = let
-    basicClassMap = getBasicClassMap replay
-    in replay
-        & getClassIds
-        & map (\ classId ->
-            ( classId
-            , getParentClassIds classId basicClassMap
-            ))
-        & IntMap.fromList
-
--- | The property map is a mapping from property IDs to property names.
-getPropertyMap :: Type.Replay -> IntMap.IntMap Text.Text
-getPropertyMap replay = replay
-    & Type.replayObjects
-    & Type.unpackList
-    & map Type.unpackText
-    & zip [0 ..]
-    & IntMap.fromList
-
--- | The basic class property map is a naive mapping from class IDs to a
--- mapping from property IDs to property names. It's naive because it does
--- not include the properties from the class's parents.
-getBasicClassPropertyMap :: Type.Replay -> IntMap.IntMap (IntMap.IntMap Text.Text)
-getBasicClassPropertyMap replay = let
-    propertyMap = getPropertyMap replay
-    in replay
-        & Type.replayCacheItems
-        & Type.unpackList
-        & map (\ x -> let
-            classId = x & Type.cacheItemClassId & Type.unpackInt32 & fromIntegral
-            properties = x
-                & Type.cacheItemCacheProperties
-                & Type.unpackList
-                & Maybe.mapMaybe (\ y -> let
-                    streamId = y & Type.cachePropertyStreamId & Type.unpackInt32 & fromIntegral
-                    propertyId = y & Type.cachePropertyObjectId & Type.unpackInt32 & fromIntegral
-                    in case IntMap.lookup propertyId propertyMap of
-                        Nothing -> Nothing
-                        Just name -> Just (streamId, name))
-                & IntMap.fromList
-            in (classId, properties))
-        & IntMap.fromList
-
--- | The actor map is a mapping from class names to their IDs.
-getActorMap :: Type.Replay -> Map.Map Text.Text Int
-getActorMap replay = replay
-    & Type.replayActors
-    & Type.unpackList
-    & map (\ x -> let
-        className = x & Type.actorName & Type.unpackText
-        classId = x & Type.actorStreamId & Type.unpackInt32 & fromIntegral
-        in (className, classId))
-    & Map.fromList
-
--- | Gets the class ID and name for a given property ID.
-getClass
-    :: IntMap.IntMap Text.Text -- ^ Property ID to property name
-    -> Map.Map Text.Text Text.Text -- ^ Property name to class name
-    -> Map.Map Text.Text Int -- ^ Class name to class ID
-    -> Int -- ^ property ID
-    -> Maybe (Int, Text.Text) -- ^ Maybe class ID and class name
-getClass propertyIdsToNames propertyNamesToClassNames classNamesToIds propertyId =
-    case IntMap.lookup propertyId propertyIdsToNames of
-        Nothing -> Nothing
-        Just rawPropertyName -> let
-            -- There are a large number of properties that end in numbers that
-            -- should all be treated the same. Instead of explicitly mapping
-            -- each of them, we can remove the numbers and treat them the same.
-            propertyName = rawPropertyName & Text.dropWhileEnd Char.isDigit
-            in case Map.lookup propertyName propertyNamesToClassNames of
-                Nothing -> Nothing
-                Just className -> case Map.lookup className classNamesToIds of
-                    Nothing -> Nothing
-                    Just classId -> Just (classId, className)
diff --git a/library/Octane/Parser/Garage.hs b/library/Octane/Parser/Garage.hs
deleted file mode 100644
--- a/library/Octane/Parser/Garage.hs
+++ /dev/null
@@ -1,43 +0,0 @@
-module Octane.Parser.Garage where
-
-import qualified Data.Bimap as Bimap
-import qualified Data.Text as Text
-import qualified Octane.Data as Data
-import qualified Octane.Type as Type
-
-
-type Body = Text.Text
-type Decal = Text.Text
-type Wheels = Text.Text
-type RocketTrail = Text.Text
-type Antenna = Text.Text
-type Topper = Text.Text
-type Finish = Text.Text
-
-
-getBody :: Type.Int32 -> Maybe Body
-getBody bodyId = Bimap.lookup (Type.fromInt32 bodyId) Data.bodies
-
-
-getDecal :: Type.Int32 -> Maybe Decal
-getDecal decalId = Bimap.lookup (Type.fromInt32 decalId) Data.decals
-
-
-getWheels :: Type.Int32 -> Maybe Wheels
-getWheels wheelsId = Bimap.lookup (Type.fromInt32 wheelsId) Data.wheels
-
-
-getRocketTrail :: Type.Int32 -> Maybe RocketTrail
-getRocketTrail rocketTrailId = Bimap.lookup (Type.fromInt32 rocketTrailId) Data.rocketTrails
-
-
-getAntenna :: Type.Int32 -> Maybe Antenna
-getAntenna antennaId = Bimap.lookup (Type.fromInt32 antennaId) Data.antennas
-
-
-getTopper :: Type.Int32 -> Maybe Topper
-getTopper topperId = Bimap.lookup (Type.fromInt32 topperId) Data.toppers
-
-
-getFinish :: Type.Int32 -> Maybe Finish
-getFinish finishId = Bimap.lookup (Type.fromInt32 finishId) Data.finishes
diff --git a/library/Octane/Type.hs b/library/Octane/Type.hs
--- a/library/Octane/Type.hs
+++ b/library/Octane/Type.hs
@@ -1,1 +1,63 @@
-{-# OPTIONS_GHC -F -pgmF autoexporter #-}
+module Octane.Type
+    ( module Octane.Type.Boolean
+    , module Octane.Type.CacheItem
+    , module Octane.Type.CacheProperty
+    , module Octane.Type.ClassItem
+    , module Octane.Type.Dictionary
+    , module Octane.Type.Float32
+    , module Octane.Type.Frame
+    , module Octane.Type.Initialization
+    , module Octane.Type.Int32
+    , module Octane.Type.Int8
+    , module Octane.Type.KeyFrame
+    , module Octane.Type.List
+    , module Octane.Type.Mark
+    , module Octane.Type.Message
+    , module Octane.Type.OptimizedReplay
+    , module Octane.Type.Property
+    , module Octane.Type.RawReplay
+    , module Octane.Type.RemoteId
+    , module Octane.Type.Replay
+    , module Octane.Type.ReplayWithFrames
+    , module Octane.Type.ReplayWithoutFrames
+    , module Octane.Type.Replication
+    , module Octane.Type.State
+    , module Octane.Type.Stream
+    , module Octane.Type.Text
+    , module Octane.Type.Value
+    , module Octane.Type.Word16
+    , module Octane.Type.Word32
+    , module Octane.Type.Word64
+    , module Octane.Type.Word8
+    ) where
+
+import Octane.Type.Boolean
+import Octane.Type.CacheItem
+import Octane.Type.CacheProperty
+import Octane.Type.ClassItem
+import Octane.Type.Dictionary
+import Octane.Type.Float32
+import Octane.Type.Frame
+import Octane.Type.Initialization
+import Octane.Type.Int32
+import Octane.Type.Int8
+import Octane.Type.KeyFrame
+import Octane.Type.List
+import Octane.Type.Mark
+import Octane.Type.Message
+import Octane.Type.OptimizedReplay
+import Octane.Type.Property
+import Octane.Type.RawReplay
+import Octane.Type.RemoteId
+import Octane.Type.Replay
+import Octane.Type.ReplayWithFrames
+import Octane.Type.ReplayWithoutFrames
+import Octane.Type.Replication
+import Octane.Type.State
+import Octane.Type.Stream
+import Octane.Type.Text
+import Octane.Type.Value
+import Octane.Type.Word16
+import Octane.Type.Word32
+import Octane.Type.Word64
+import Octane.Type.Word8
diff --git a/library/Octane/Type/Actor.hs b/library/Octane/Type/Actor.hs
deleted file mode 100644
--- a/library/Octane/Type/Actor.hs
+++ /dev/null
@@ -1,30 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-
-module Octane.Type.Actor (Actor(..)) where
-
-import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
-import qualified Data.Binary as Binary
-import Data.Function ((&))
-import qualified GHC.Generics as Generics
-import qualified Octane.Json as Json
-import qualified Octane.Type.Primitive.Text as Text
-import qualified Octane.Type.Primitive.Int32 as Int32
-
--- | A class (like @Core.Object@) and it's associated ID in the net stream
--- (like 0).
-data Actor = Actor
-    { actorName :: !Text.Text
-    , actorStreamId :: !Int32.Int32
-    } deriving (Eq,Generics.Generic,Show)
-
-instance Binary.Binary Actor where
-    get = Actor <$> Binary.get <*> Binary.get
-    put actor = do
-        actor & actorName & Binary.put
-        actor & actorStreamId & Binary.put
-
-instance DeepSeq.NFData Actor
-
-instance Aeson.ToJSON Actor where
-    toJSON = Aeson.genericToJSON (Json.toJsonOptions "Actor")
diff --git a/library/Octane/Type/Boolean.hs b/library/Octane/Type/Boolean.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/Boolean.hs
@@ -0,0 +1,52 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+
+module Octane.Type.Boolean (Boolean(..)) where
+
+import Data.Function ((&))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Aeson as Aeson
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits as BinaryBit
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified GHC.Generics as Generics
+
+
+-- | A boolean value.
+newtype Boolean = Boolean
+    { unpack :: Bool
+    } deriving (Eq, Generics.Generic, Show)
+
+-- | Boolean values are stored in the last bit of a byte. Decoding will fail if
+-- the byte is anything other than @0b00000000@ or @0b00000001@.
+instance Binary.Binary Boolean where
+    get = do
+        value <- Binary.getWord8
+        case value of
+            0 -> pure (Boolean False)
+            1 -> pure (Boolean True)
+            _ -> fail ("Unexpected Boolean value " ++ show value)
+
+    put boolean = boolean
+        & unpack
+        & fromEnum
+        & fromIntegral
+        & Binary.putWord8
+
+instance BinaryBit.BinaryBit Boolean where
+    getBits _ = do
+        value <- BinaryBit.getBool
+        value & Boolean & pure
+
+    putBits _ boolean = boolean
+        & unpack
+        & BinaryBit.putBool
+
+instance DeepSeq.NFData Boolean where
+
+instance Aeson.ToJSON Boolean where
+    toJSON boolean = boolean
+        & unpack
+        & Aeson.toJSON
diff --git a/library/Octane/Type/CacheItem.hs b/library/Octane/Type/CacheItem.hs
--- a/library/Octane/Type/CacheItem.hs
+++ b/library/Octane/Type/CacheItem.hs
@@ -1,34 +1,33 @@
 {-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE StrictData #-}
 
 module Octane.Type.CacheItem (CacheItem(..)) where
 
+import Data.Function ((&))
+
 import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
 import qualified Data.Binary as Binary
-import Data.Function ((&))
 import qualified GHC.Generics as Generics
-import qualified Octane.Json as Json
 import qualified Octane.Type.CacheProperty as CacheProperty
-import qualified Octane.Type.Primitive.List as List
-import qualified Octane.Type.Primitive.Int32 as Int32
+import qualified Octane.Type.List as List
+import qualified Octane.Type.Word32 as Word32
 
+
 -- | An item in the class net cache map.
 data CacheItem = CacheItem
-    { cacheItemClassId :: !Int32.Int32
-    , cacheItemParentCacheId :: !Int32.Int32
-    , cacheItemCacheId :: !Int32.Int32
-    , cacheItemCacheProperties :: !(List.List CacheProperty.CacheProperty)
-    } deriving (Eq,Generics.Generic,Show)
+    { classId :: Word32.Word32
+    , parentCacheId :: Word32.Word32
+    , cacheId :: Word32.Word32
+    , properties :: List.List CacheProperty.CacheProperty
+    } deriving (Eq, Generics.Generic, Show)
 
 instance Binary.Binary CacheItem where
     get = CacheItem <$> Binary.get <*> Binary.get <*> Binary.get <*> Binary.get
     put cacheItem = do
-        cacheItem & cacheItemClassId & Binary.put
-        cacheItem & cacheItemParentCacheId & Binary.put
-        cacheItem & cacheItemCacheId & Binary.put
-        cacheItem & cacheItemCacheProperties & Binary.put
-
-instance DeepSeq.NFData CacheItem
+        cacheItem & classId & Binary.put
+        cacheItem & parentCacheId & Binary.put
+        cacheItem & cacheId & Binary.put
+        cacheItem & properties & Binary.put
 
-instance Aeson.ToJSON CacheItem where
-    toJSON = Aeson.genericToJSON (Json.toJsonOptions "CacheItem")
+instance DeepSeq.NFData CacheItem where
diff --git a/library/Octane/Type/CacheProperty.hs b/library/Octane/Type/CacheProperty.hs
--- a/library/Octane/Type/CacheProperty.hs
+++ b/library/Octane/Type/CacheProperty.hs
@@ -1,28 +1,27 @@
 {-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE StrictData #-}
 
 module Octane.Type.CacheProperty (CacheProperty(..)) where
 
+import Data.Function ((&))
+
 import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
 import qualified Data.Binary as Binary
-import Data.Function ((&))
 import qualified GHC.Generics as Generics
-import qualified Octane.Json as Json
-import qualified Octane.Type.Primitive.Int32 as Int32
+import qualified Octane.Type.Word32 as Word32
 
+
 -- | A property on an item in the class net cache map.
 data CacheProperty = CacheProperty
-    { cachePropertyObjectId :: !Int32.Int32
-    , cachePropertyStreamId :: !Int32.Int32
-    } deriving (Eq,Generics.Generic,Show)
+    { objectId :: Word32.Word32
+    , streamId :: Word32.Word32
+    } deriving (Eq, Generics.Generic, Show)
 
 instance Binary.Binary CacheProperty where
     get = CacheProperty <$> Binary.get <*> Binary.get
     put cacheProperty = do
-        cacheProperty & cachePropertyObjectId & Binary.put
-        cacheProperty & cachePropertyStreamId & Binary.put
-
-instance DeepSeq.NFData CacheProperty
+        cacheProperty & objectId & Binary.put
+        cacheProperty & streamId & Binary.put
 
-instance Aeson.ToJSON CacheProperty where
-    toJSON = Aeson.genericToJSON (Json.toJsonOptions "CacheProperty")
+instance DeepSeq.NFData CacheProperty where
diff --git a/library/Octane/Type/ClassItem.hs b/library/Octane/Type/ClassItem.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/ClassItem.hs
@@ -0,0 +1,29 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE StrictData #-}
+
+module Octane.Type.ClassItem (ClassItem(..)) where
+
+import Data.Function ((&))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Binary as Binary
+import qualified GHC.Generics as Generics
+import qualified Octane.Type.Text as Text
+import qualified Octane.Type.Word32 as Word32
+
+
+-- | A class (like @Core.Object@) and it's associated ID in the net stream
+-- (like 0).
+data ClassItem = ClassItem
+    { name :: Text.Text
+    , streamId :: Word32.Word32
+    } deriving (Eq,Generics.Generic,Show)
+
+instance Binary.Binary ClassItem where
+    get = ClassItem <$> Binary.get <*> Binary.get
+    put classItem = do
+        classItem & name & Binary.put
+        classItem & streamId & Binary.put
+
+instance DeepSeq.NFData ClassItem where
diff --git a/library/Octane/Type/Dictionary.hs b/library/Octane/Type/Dictionary.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/Dictionary.hs
@@ -0,0 +1,75 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies #-}
+
+module Octane.Type.Dictionary (Dictionary(..)) where
+
+import Data.Function ((&))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Aeson as Aeson
+import qualified Data.Binary as Binary
+import qualified Data.Map.Strict as Map
+import qualified GHC.Exts as Exts
+import qualified GHC.Generics as Generics
+import qualified Octane.Type.Text as Text
+
+
+-- | A dictionary that maps text to values.
+newtype Dictionary a = Dictionary
+    { unpack :: (Map.Map Text.Text a)
+    } deriving (Eq, Generics.Generic)
+
+-- | Reads elements are stored with the key first, then the value. The
+-- dictionary ends when a key is @"None"@.
+instance (Binary.Binary a) => Binary.Binary (Dictionary a) where
+    get = do
+        element <- getElement
+        if Map.null element
+        then element & Dictionary & pure
+        else do
+            Dictionary elements <- Binary.get
+            elements & Map.union element & Dictionary & pure
+
+    put dictionary = do
+        dictionary & unpack & Map.assocs & mapM_ putElement
+        noneKey & Binary.put
+
+instance Exts.IsList (Dictionary a) where
+    type Item (Dictionary a) = (Text.Text, a)
+
+    fromList items = Dictionary (Map.fromList items)
+
+    toList dictionary = Map.toList (unpack dictionary)
+
+instance (DeepSeq.NFData a) => DeepSeq.NFData (Dictionary a) where
+
+instance (Show a) => Show (Dictionary a) where
+    show dictionary = show (unpack dictionary)
+
+instance (Aeson.ToJSON a) => Aeson.ToJSON (Dictionary a) where
+    toJSON dictionary = dictionary
+        & unpack
+        & Map.mapKeys Text.unpack
+        & Aeson.toJSON
+
+
+getElement :: (Binary.Binary a) => Binary.Get (Map.Map Text.Text a)
+getElement = do
+    key <- Binary.get
+    if key == noneKey
+    then pure Map.empty
+    else do
+        value <- Binary.get
+        value & Map.singleton key & pure
+
+
+putElement :: (Binary.Binary a) => (Text.Text, a) -> Binary.Put
+putElement (key, value) = do
+    Binary.put key
+    Binary.put value
+
+
+noneKey :: Text.Text
+noneKey = "None"
diff --git a/library/Octane/Type/Float32.hs b/library/Octane/Type/Float32.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/Float32.hs
@@ -0,0 +1,62 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+module Octane.Type.Float32 (Float32(..)) where
+
+import Data.Function ((&))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Aeson as Aeson
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits as BinaryBit
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.IEEE754 as IEEE754
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified GHC.Generics as Generics
+import qualified Octane.Utility.Endian as Endian
+
+
+-- | A 32-bit float.
+newtype Float32 = Float32
+    { unpack :: Float
+    } deriving (Eq, Fractional, Generics.Generic, Num, Ord)
+
+-- | Stored little-endian.
+instance Binary.Binary Float32 where
+    get = do
+        value <- IEEE754.getFloat32le
+        value & Float32 & pure
+
+    put float32 = float32
+        & unpack
+        & IEEE754.putFloat32le
+
+instance BinaryBit.BinaryBit Float32 where
+    getBits _ = do
+        bytes <- BinaryBit.getByteString 4
+        bytes
+            & LazyBytes.fromStrict
+            & Endian.reverseBitsInBytes
+            & Binary.runGet Binary.get
+            & pure
+
+    putBits _ float32 = float32
+        & Binary.put
+        & Binary.runPut
+        & Endian.reverseBitsInBytes
+        & LazyBytes.toStrict
+        & BinaryBit.putByteString
+
+instance DeepSeq.NFData Float32
+
+instance Show Float32 where
+    show float32 = show (unpack float32)
+
+instance Aeson.ToJSON Float32 where
+    toJSON float32 = float32
+        & unpack
+        & Aeson.toJSON
diff --git a/library/Octane/Type/Frame.hs b/library/Octane/Type/Frame.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/Frame.hs
@@ -0,0 +1,289 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE StrictData #-}
+
+module Octane.Type.Frame (Frame(..)) where
+
+import Data.Aeson ((.=))
+import Data.Function ((&))
+import Data.Monoid ((<>))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Aeson as Aeson
+import qualified Data.Bimap as Bimap
+import qualified Data.Map.Strict as Map
+import qualified Data.Text as StrictText
+import qualified GHC.Generics as Generics
+import qualified Octane.Data as Data
+import qualified Octane.Type.Float32 as Float32
+import qualified Octane.Type.Initialization as Initialization
+import qualified Octane.Type.RemoteId as RemoteId
+import qualified Octane.Type.Replication as Replication
+import qualified Octane.Type.State as State
+import qualified Octane.Type.Value as Value
+import qualified Octane.Type.Word32 as Word32
+import qualified Octane.Type.Word8 as Word8
+
+
+data Frame = Frame
+    { number :: Word
+    , isKeyFrame :: Bool
+    , time :: Float32.Float32
+    , delta :: Float32.Float32
+    , replications :: [Replication.Replication]
+    } deriving (Eq, Generics.Generic, Show)
+
+instance DeepSeq.NFData Frame where
+
+instance Aeson.ToJSON Frame where
+    toJSON frame = Aeson.object
+        [ "Number" .= number frame
+        , "IsKeyFrame" .= isKeyFrame frame
+        , "Time" .= time frame
+        , "Delta" .= delta frame
+        , "Spawned" .= (frame & replications & getSpawned)
+        , "Updated" .= (frame & replications & getUpdated)
+        , "Destroyed" .= (frame & replications & getDestroyed)
+        ]
+
+
+newtype Spawned = Spawned [Replication.Replication]
+
+instance Aeson.ToJSON Spawned where
+    toJSON (Spawned xs) = xs
+        & map (\ x -> do
+            let k = x & Replication.actorId & show & StrictText.pack
+            let v = Aeson.object
+                    [ "Name" .= Replication.objectName x
+                    , "Class" .= Replication.className x
+                    , "Position" .= (x & Replication.initialization & fmap Initialization.location)
+                    , "Rotation" .= (x & Replication.initialization & fmap Initialization.rotation)
+                    ]
+            (k, v))
+        & Map.fromList
+        & Aeson.toJSON
+
+getSpawned :: [Replication.Replication] -> Spawned
+getSpawned xs = xs
+    & filter (\ x -> x
+        & Replication.state
+        & (== State.SOpening))
+    & Spawned
+
+
+newtype Updated = Updated [Replication.Replication]
+
+instance Aeson.ToJSON Updated where
+    toJSON (Updated xs) = xs
+        & map (\ x -> do
+            let k = x
+                    & Replication.actorId
+                    & show
+                    & StrictText.pack
+            let v = x
+                    & Replication.properties
+                    & Map.map (\ value -> Aeson.object
+                        [ "Type" .= getType value
+                        , "Value" .= getValue value
+                        ])
+            (k, v))
+        & Map.fromList
+        & Aeson.toJSON
+
+
+getUpdated :: [Replication.Replication] -> Updated
+getUpdated xs = xs
+    & filter (\ x -> x
+        & Replication.state
+        & (== State.SExisting))
+    & filter (\ x -> x
+        & Replication.properties
+        & null
+        & not)
+    & Updated
+
+
+newtype Destroyed = Destroyed [Replication.Replication]
+
+instance Aeson.ToJSON Destroyed where
+    toJSON (Destroyed xs) = xs
+        & map Replication.actorId
+        & Aeson.toJSON
+
+getDestroyed :: [Replication.Replication] -> Destroyed
+getDestroyed xs = xs
+    & filter (\ x -> x
+        & Replication.state
+        & (== State.SClosing))
+    & Destroyed
+
+
+getType :: Value.Value -> StrictText.Text
+getType value = case value of
+    Value.VBoolean _ -> "Boolean"
+    Value.VByte _ -> "Byte"
+    Value.VCamSettings _ _ _ _ _ _ -> "CameraSettings"
+    Value.VDemolish _ _ _ _ _ _ -> "Demolition"
+    Value.VEnum _ _ -> "Enum"
+    Value.VExplosion _ _ _ -> "Explosion"
+    Value.VFlaggedInt _ _ -> "FlaggedInt"
+    Value.VFloat _ -> "Float"
+    Value.VGameMode _ -> "GameMode"
+    Value.VInt _ -> "Int"
+    Value.VLoadout _ _ _ _ _ _ _ _ _ -> "Loadout"
+    Value.VLoadoutOnline _ _ _ _ -> "OnlineLoadout"
+    Value.VLocation _ -> "Position"
+    Value.VMusicStinger _ _ _ -> "MusicStinger"
+    Value.VPickup _ _ _ -> "Pickup"
+    Value.VPrivateMatchSettings _ _ _ _ _ _ -> "PrivateMatchSettings"
+    Value.VQWord _ -> "QWord"
+    Value.VRelativeRotation _ -> "RelativeRotation"
+    Value.VReservation _ _ _ _ _ _ _ -> "Reservation"
+    Value.VRigidBodyState _ _ _ _ _ -> "RigidBodyState"
+    Value.VString _ -> "String"
+    Value.VTeamPaint _ _ _ _ _ -> "Paint"
+    Value.VUniqueId _ _ _ -> "UniqueId"
+
+
+getValue :: Value.Value -> Aeson.Value
+getValue value = case value of
+    Value.VBoolean x -> Aeson.toJSON x
+    Value.VByte x -> Aeson.toJSON x
+    Value.VCamSettings fov height angle distance stiffness swivelSpeed -> Aeson.object
+        [ ("FOV", Aeson.toJSON fov)
+        , ("Height", Aeson.toJSON height)
+        , ("Angle", Aeson.toJSON angle)
+        , ("Distance", Aeson.toJSON distance)
+        , ("Stiffness", Aeson.toJSON stiffness)
+        , ("SwivelSpeed", Aeson.toJSON swivelSpeed)
+        ]
+    Value.VDemolish a b c d e f -> Aeson.toJSON (a, b, c, d, e, f)
+    Value.VEnum x y -> Aeson.toJSON (x, y)
+    Value.VExplosion a b c -> Aeson.toJSON (a, b, c)
+    Value.VFlaggedInt x y -> Aeson.toJSON (x, y)
+    Value.VFloat x -> Aeson.toJSON x
+    Value.VGameMode gameMode -> Aeson.object
+        [ ("Id", Aeson.toJSON gameMode)
+        , ("Name", gameMode & getGameMode & Aeson.toJSON)
+        ]
+    Value.VInt x -> Aeson.toJSON x
+    Value.VLoadout version body decal wheels rocketTrail antenna topper x y -> Aeson.object
+        [ ("Version", Aeson.toJSON version)
+        , ("Body", Aeson.object
+            [ ("Id", Aeson.toJSON body)
+            , ("Name", body & getBody & Aeson.toJSON)
+            ])
+        , ("Decal", Aeson.object
+            [ ("Id", Aeson.toJSON decal)
+            , ("Name", decal & getDecal & Aeson.toJSON)
+            ])
+        , ("Wheels", Aeson.object
+            [ ("Id", Aeson.toJSON wheels)
+            , ("Name", wheels & getWheels & Aeson.toJSON)
+            ])
+        , ("RocketTrail", Aeson.object
+            [ ("Id", Aeson.toJSON rocketTrail)
+            , ("Name", rocketTrail & getRocketTrail & Aeson.toJSON)
+            ])
+        , ("Antenna", Aeson.object
+            [ ("Id", Aeson.toJSON antenna)
+            , ("Name", antenna & getAntenna & Aeson.toJSON)
+            ])
+        , ("Topper", Aeson.object
+            [ ("Id", Aeson.toJSON topper)
+            , ("Name", topper & getTopper & Aeson.toJSON)
+            ])
+        , ("Unknown1", Aeson.toJSON x)
+        , ("Unknown2", Aeson.toJSON y)
+        ]
+    Value.VLoadoutOnline a b c d -> Aeson.toJSON (a, b, c, d)
+    Value.VLocation x -> Aeson.toJSON x
+    Value.VMusicStinger a b c -> Aeson.toJSON (a, b, c)
+    Value.VPickup a b c -> Aeson.toJSON (a, b, c)
+    Value.VPrivateMatchSettings mutators joinableBy maxPlayers name password x -> Aeson.object
+        [ ("Mutators", Aeson.toJSON mutators)
+        , ("JoinableBy", Aeson.toJSON joinableBy)
+        , ("MaxPlayers", Aeson.toJSON maxPlayers)
+        , ("Name", Aeson.toJSON name)
+        , ("Password", Aeson.toJSON password)
+        , ("Unknown", Aeson.toJSON x)
+        ]
+    Value.VQWord x -> Aeson.toJSON x
+    Value.VRelativeRotation x -> Aeson.toJSON x
+    Value.VReservation num systemId remoteId localId name x y -> Aeson.object
+        [ ("Number", Aeson.toJSON num)
+        , ("SystemId", Aeson.toJSON systemId)
+        , ("RemoteId", Aeson.toJSON remoteId)
+        , ("LocalId", Aeson.toJSON localId)
+        , ("Name", Aeson.toJSON name)
+        , ("Unknown1", Aeson.toJSON x)
+        , ("Unknown2", Aeson.toJSON y)
+        ]
+    Value.VRigidBodyState sleeping position rotation linear angular -> Aeson.object
+        [ ("Sleeping", Aeson.toJSON sleeping)
+        , ("Position", Aeson.toJSON position)
+        , ("Rotation", Aeson.toJSON rotation)
+        , ("LinearVelocity", Aeson.toJSON linear)
+        , ("AngularVelocity", Aeson.toJSON angular)
+        ]
+    Value.VString x -> Aeson.toJSON x
+    Value.VTeamPaint team color1 color2 finish1 finish2 -> Aeson.object
+        [ ("Team", Aeson.toJSON team)
+        , ("PrimaryColor", Aeson.toJSON color1)
+        , ("AccentColor", Aeson.toJSON color2)
+        , ("PrimaryFinish", Aeson.object
+            [ ("Id", Aeson.toJSON finish1)
+            , ("Name", finish1 & getFinish & Aeson.toJSON)
+            ])
+        , ("AccentFinish", Aeson.object
+            [ ("Id", Aeson.toJSON finish2)
+            , ("Name", finish2 & getFinish & Aeson.toJSON)
+            ])
+        ]
+    Value.VUniqueId systemId remoteId localId -> Aeson.object
+        [ ("System", case systemId of
+            0 -> "Local"
+            1 -> "Steam"
+            2 -> "PlayStation"
+            4 -> "Xbox"
+            _ -> Aeson.String ("Unknown system " <> StrictText.pack (show systemId)))
+        , ("Remote", case remoteId of
+            RemoteId.SplitscreenId x -> Aeson.toJSON x
+            RemoteId.SteamId x -> Aeson.toJSON x
+            RemoteId.PlayStationId x -> Aeson.toJSON x
+            RemoteId.XboxId x -> Aeson.toJSON x)
+        , ("Local", Aeson.toJSON localId)
+        ]
+
+
+getGameMode :: Word8.Word8 -> Maybe StrictText.Text
+getGameMode x = Bimap.lookup (Word8.fromWord8 x) Data.gameModes
+
+
+getBody :: Word32.Word32 -> Maybe StrictText.Text
+getBody x = Bimap.lookup (Word32.fromWord32 x) Data.bodies
+
+
+getDecal :: Word32.Word32 -> Maybe StrictText.Text
+getDecal x = Bimap.lookup (Word32.fromWord32 x) Data.decals
+
+
+getWheels :: Word32.Word32 -> Maybe StrictText.Text
+getWheels x = Bimap.lookup (Word32.fromWord32 x) Data.wheels
+
+
+getRocketTrail :: Word32.Word32 -> Maybe StrictText.Text
+getRocketTrail x = Bimap.lookup (Word32.fromWord32 x) Data.rocketTrails
+
+
+getAntenna :: Word32.Word32 -> Maybe StrictText.Text
+getAntenna x = Bimap.lookup (Word32.fromWord32 x) Data.antennas
+
+
+getTopper :: Word32.Word32 -> Maybe StrictText.Text
+getTopper x = Bimap.lookup (Word32.fromWord32 x) Data.toppers
+
+
+getFinish :: Word32.Word32 -> Maybe StrictText.Text
+getFinish x = Bimap.lookup (Word32.fromWord32 x) Data.finishes
diff --git a/library/Octane/Type/Initialization.hs b/library/Octane/Type/Initialization.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/Initialization.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE StrictData #-}
+
+module Octane.Type.Initialization (Initialization(..)) where
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified GHC.Generics as Generics
+import qualified Octane.Type.Int8 as Int8
+import qualified Octane.Type.Vector as Vector
+
+
+data Initialization = Initialization
+    { location :: Maybe (Vector.Vector Int)
+    , rotation :: Maybe (Vector.Vector Int8.Int8)
+    } deriving (Eq, Generics.Generic, Show)
+
+instance DeepSeq.NFData Initialization where
diff --git a/library/Octane/Type/Int32.hs b/library/Octane/Type/Int32.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/Int32.hs
@@ -0,0 +1,79 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+module Octane.Type.Int32 (Int32(..), fromInt32, toInt32) where
+
+import Data.Function ((&))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Aeson as Aeson
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits as BinaryBit
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.Int as Int
+import qualified GHC.Generics as Generics
+import qualified Octane.Utility.Endian as Endian
+
+
+-- | A 32-bit signed integer.
+newtype Int32 = Int32
+    { unpack :: Int.Int32
+    } deriving (Eq, Generics.Generic, Num, Ord)
+
+-- | Stored in little-endian byte order.
+instance Binary.Binary Int32 where
+    get = do
+        value <- Binary.getInt32le
+        pure (Int32 value)
+
+    put int32 = do
+        let value = unpack int32
+        Binary.putInt32le value
+
+instance BinaryBit.BinaryBit Int32 where
+    getBits _ = do
+        bytes <- BinaryBit.getByteString 4
+        bytes
+            & LazyBytes.fromStrict
+            & Endian.reverseBitsInBytes
+            & Binary.runGet Binary.get
+            & pure
+
+    putBits _ int32 = int32
+        & Binary.put
+        & Binary.runPut
+        & Endian.reverseBitsInBytes
+        & LazyBytes.toStrict
+        & BinaryBit.putByteString
+
+instance DeepSeq.NFData Int32 where
+
+-- | Shown as @1234@.
+instance Show Int32 where
+    show int32 = show (unpack int32)
+
+instance Aeson.ToJSON Int32 where
+    toJSON int32 = int32
+        & unpack
+        & Aeson.toJSON
+
+
+-- | Converts a 'Int32' into any 'Integral' value.
+--
+-- >>> fromInt32 1 :: Int.Int32
+-- 1
+fromInt32 :: (Integral a) => Int32 -> a
+fromInt32 int32 = fromIntegral (unpack int32)
+
+
+-- | Converts any 'Integral' value into a 'Int32'.
+--
+-- >>> toInt32 (1 :: Int.Int32)
+-- 1
+toInt32 :: (Integral a) => a -> Int32
+toInt32 value = Int32 (fromIntegral value)
diff --git a/library/Octane/Type/Int8.hs b/library/Octane/Type/Int8.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/Int8.hs
@@ -0,0 +1,78 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+module Octane.Type.Int8 (Int8(..), fromInt8, toInt8) where
+
+import Data.Function ((&))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Aeson as Aeson
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits as BinaryBit
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.Int as Int
+import qualified GHC.Generics as Generics
+import qualified Octane.Utility.Endian as Endian
+
+
+-- | A 8-bit signed integer.
+newtype Int8 = Int8
+    { unpack :: Int.Int8
+    } deriving (Eq, Generics.Generic, Num, Ord)
+
+instance Binary.Binary Int8 where
+    get = do
+        value <- Binary.getInt8
+        pure (Int8 value)
+
+    put int8 = do
+        let value = unpack int8
+        Binary.putInt8 value
+
+instance BinaryBit.BinaryBit Int8 where
+    getBits _ = do
+        bytes <- BinaryBit.getByteString 1
+        bytes
+            & LazyBytes.fromStrict
+            & Endian.reverseBitsInBytes
+            & Binary.runGet Binary.get
+            & pure
+
+    putBits _ int8 = int8
+        & Binary.put
+        & Binary.runPut
+        & Endian.reverseBitsInBytes
+        & LazyBytes.toStrict
+        & BinaryBit.putByteString
+
+instance DeepSeq.NFData Int8 where
+
+-- | Shown as @1234@.
+instance Show Int8 where
+    show int8 = show (unpack int8)
+
+instance Aeson.ToJSON Int8 where
+    toJSON int8 = int8
+        & unpack
+        & Aeson.toJSON
+
+
+-- | Converts a 'Int8' into any 'Integral' value.
+--
+-- >>> fromInt8 1 :: Int.Int8
+-- 1
+fromInt8 :: (Integral a) => Int8 -> a
+fromInt8 int8 = fromIntegral (unpack int8)
+
+
+-- | Converts any 'Integral' value into a 'Int8'.
+--
+-- >>> toInt8 (1 :: Int.Int8)
+-- 1
+toInt8 :: (Integral a) => a -> Int8
+toInt8 value = Int8 (fromIntegral value)
diff --git a/library/Octane/Type/KeyFrame.hs b/library/Octane/Type/KeyFrame.hs
--- a/library/Octane/Type/KeyFrame.hs
+++ b/library/Octane/Type/KeyFrame.hs
@@ -1,33 +1,29 @@
 {-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE StrictData #-}
 
 module Octane.Type.KeyFrame (KeyFrame(..)) where
 
+import Data.Function ((&))
+
 import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
 import qualified Data.Binary as Binary
-import Data.Function ((&))
 import qualified GHC.Generics as Generics
-import qualified Octane.Json as Json
-import qualified Octane.Type.Primitive.Float32 as Float32
-import qualified Octane.Type.Primitive.Int32 as Int32
+import qualified Octane.Type.Float32 as Float32
+import qualified Octane.Type.Word32 as Word32
 
--- | A key frame. Each key frame has the time since the beginning of the match,
--- the frame it corresponds to, and that frame's bit position in the network
--- stream.
+
 data KeyFrame = KeyFrame
-    { keyFrameTime :: !Float32.Float32
-    , keyFrameFrame :: !Int32.Int32
-    , keyFramePosition :: !Int32.Int32
-    } deriving (Eq,Generics.Generic,Show)
+    { time :: Float32.Float32
+    , frame :: Word32.Word32
+    , position :: Word32.Word32
+    } deriving (Eq, Generics.Generic, Show)
 
 instance Binary.Binary KeyFrame where
     get = KeyFrame <$> Binary.get <*> Binary.get <*> Binary.get
     put keyFrame = do
-        keyFrame & keyFrameTime & Binary.put
-        keyFrame & keyFrameFrame & Binary.put
-        keyFrame & keyFramePosition & Binary.put
-
-instance DeepSeq.NFData KeyFrame
+        keyFrame & time & Binary.put
+        keyFrame & frame & Binary.put
+        keyFrame & position & Binary.put
 
-instance Aeson.ToJSON KeyFrame where
-    toJSON = Aeson.genericToJSON (Json.toJsonOptions "KeyFrame")
+instance DeepSeq.NFData KeyFrame where
diff --git a/library/Octane/Type/List.hs b/library/Octane/Type/List.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/List.hs
@@ -0,0 +1,37 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+
+module Octane.Type.List (List(..)) where
+
+import Data.Function ((&))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Control.Monad as Monad
+import qualified Data.Aeson as Aeson
+import qualified Data.Binary as Binary
+import qualified GHC.Generics as Generics
+import qualified Octane.Type.Word32 as Word32
+
+
+-- | A list of valeus.
+newtype List a = List
+    { unpack :: [a]
+    } deriving (Eq, Generics.Generic, Ord, Show)
+
+-- | Bytewise lists are length-prefixed.
+instance (Binary.Binary a) => Binary.Binary (List a) where
+    get = do
+        size <- Binary.get
+        elements <- Monad.replicateM (Word32.fromWord32 size) Binary.get
+        elements & List & return
+
+    put list = do
+        list & unpack & length & fromIntegral & Word32.Word32 & Binary.put
+        list & unpack & mapM_ Binary.put
+
+instance (DeepSeq.NFData a) => DeepSeq.NFData (List a) where
+
+instance (Aeson.ToJSON a) => Aeson.ToJSON (List a) where
+    toJSON list = list
+        & unpack
+        & Aeson.toJSON
diff --git a/library/Octane/Type/Mark.hs b/library/Octane/Type/Mark.hs
--- a/library/Octane/Type/Mark.hs
+++ b/library/Octane/Type/Mark.hs
@@ -1,29 +1,27 @@
 {-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE StrictData #-}
 
 module Octane.Type.Mark (Mark(..)) where
 
+import Data.Function ((&))
+
 import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
 import qualified Data.Binary as Binary
-import Data.Function ((&))
 import qualified GHC.Generics as Generics
-import qualified Octane.Json as Json
-import qualified Octane.Type.Primitive.Text as Text
-import qualified Octane.Type.Primitive.Int32 as Int32
+import qualified Octane.Type.Text as Text
+import qualified Octane.Type.Word32 as Word32
 
 -- | A tick mark on the replay. Both goals and saves make tick marks.
 data Mark = Mark
-    { markLabel :: !Text.Text
-    , markFrame :: !Int32.Int32
+    { label :: Text.Text
+    , frame :: Word32.Word32
     } deriving (Eq,Generics.Generic,Show)
 
 instance Binary.Binary Mark where
     get = Mark <$> Binary.get <*> Binary.get
     put mark = do
-        mark & markLabel & Binary.put
-        mark & markFrame & Binary.put
+        mark & label & Binary.put
+        mark & frame & Binary.put
 
 instance DeepSeq.NFData Mark
-
-instance Aeson.ToJSON Mark where
-    toJSON = Aeson.genericToJSON (Json.toJsonOptions "Mark")
diff --git a/library/Octane/Type/Message.hs b/library/Octane/Type/Message.hs
--- a/library/Octane/Type/Message.hs
+++ b/library/Octane/Type/Message.hs
@@ -1,31 +1,29 @@
 {-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE StrictData #-}
 
 module Octane.Type.Message (Message(..)) where
 
+import Data.Function ((&))
+
 import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
 import qualified Data.Binary as Binary
-import Data.Function ((&))
 import qualified GHC.Generics as Generics
-import qualified Octane.Json as Json
-import qualified Octane.Type.Primitive.Text as Text
-import qualified Octane.Type.Primitive.Int32 as Int32
+import qualified Octane.Type.Text as Text
+import qualified Octane.Type.Word32 as Word32
 
 -- | A debugging message. Replays do not have any of these anymore.
 data Message = Message
-    { messageFrame :: !Int32.Int32
-    , messageName :: !Text.Text
-    , messageContent :: !Text.Text
-    } deriving (Eq,Generics.Generic,Show)
+    { frame :: Word32.Word32
+    , name :: Text.Text
+    , content :: Text.Text
+    } deriving (Eq, Generics.Generic, Show)
 
 instance Binary.Binary Message where
     get = Message <$> Binary.get <*> Binary.get <*> Binary.get
     put message = do
-        message & messageFrame & Binary.put
-        message & messageName & Binary.put
-        message & messageContent & Binary.put
-
-instance DeepSeq.NFData Message
+        message & frame & Binary.put
+        message & name & Binary.put
+        message & content & Binary.put
 
-instance Aeson.ToJSON Message where
-    toJSON = Aeson.genericToJSON (Json.toJsonOptions "Message")
+instance DeepSeq.NFData Message where
diff --git a/library/Octane/Type/OptimizedReplay.hs b/library/Octane/Type/OptimizedReplay.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/OptimizedReplay.hs
@@ -0,0 +1,94 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE StrictData #-}
+
+module Octane.Type.OptimizedReplay (OptimizedReplay(..), fromReplayWithFrames, toReplayWithFrames) where
+
+import Data.Function ((&))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Binary as Binary
+import qualified GHC.Generics as Generics
+import qualified Octane.Type.CacheItem as CacheItem
+import qualified Octane.Type.ClassItem as ClassItem
+import qualified Octane.Type.Dictionary as Dictionary
+import qualified Octane.Type.Frame as Frame
+import qualified Octane.Type.KeyFrame as KeyFrame
+import qualified Octane.Type.List as List
+import qualified Octane.Type.Mark as Mark
+import qualified Octane.Type.Message as Message
+import qualified Octane.Type.Property as Property
+import qualified Octane.Type.ReplayWithFrames as ReplayWithFrames
+import qualified Octane.Type.Text as Text
+import qualified Octane.Type.Word32 as Word32
+import qualified Octane.Utility.Optimizer as Optimizer
+
+
+data OptimizedReplay = OptimizedReplay
+    { version1 :: Word32.Word32
+    , version2 :: Word32.Word32
+    , label :: Text.Text
+    , properties :: Dictionary.Dictionary Property.Property
+    , levels :: List.List Text.Text
+    , keyFrames :: List.List KeyFrame.KeyFrame
+    , frames :: [Frame.Frame]
+    , messages :: List.List Message.Message
+    , marks :: List.List Mark.Mark
+    , packages :: List.List Text.Text
+    , objects :: List.List Text.Text
+    , names :: List.List Text.Text
+    , classes :: List.List ClassItem.ClassItem
+    , cache :: List.List CacheItem.CacheItem
+    } deriving (Eq, Generics.Generic, Show)
+
+instance Binary.Binary OptimizedReplay where
+    get = do
+        replayWithFrames <- Binary.get
+        fromReplayWithFrames replayWithFrames
+
+    put replay = do
+        replayWithFrames <- toReplayWithFrames replay
+        Binary.put replayWithFrames
+
+instance DeepSeq.NFData OptimizedReplay where
+
+
+fromReplayWithFrames :: (Monad m) => ReplayWithFrames.ReplayWithFrames -> m OptimizedReplay
+fromReplayWithFrames replayWithFrames = do
+    pure OptimizedReplay
+        { version1 = replayWithFrames & ReplayWithFrames.version1
+        , version2 = replayWithFrames & ReplayWithFrames.version2
+        , label = replayWithFrames & ReplayWithFrames.label
+        , properties = replayWithFrames & ReplayWithFrames.properties
+        , levels = replayWithFrames & ReplayWithFrames.levels
+        , keyFrames = replayWithFrames & ReplayWithFrames.keyFrames
+        , frames = replayWithFrames & ReplayWithFrames.frames & Optimizer.optimizeFrames
+        , messages = replayWithFrames & ReplayWithFrames.messages
+        , marks = replayWithFrames & ReplayWithFrames.marks
+        , packages = replayWithFrames & ReplayWithFrames.packages
+        , objects = replayWithFrames & ReplayWithFrames.objects
+        , names = replayWithFrames & ReplayWithFrames.names
+        , classes = replayWithFrames & ReplayWithFrames.classes
+        , cache = replayWithFrames & ReplayWithFrames.cache
+        }
+
+
+toReplayWithFrames :: (Monad m) => OptimizedReplay -> m ReplayWithFrames.ReplayWithFrames
+toReplayWithFrames optimizedReplay = do
+    pure ReplayWithFrames.ReplayWithFrames
+        { ReplayWithFrames.version1 = optimizedReplay & version1
+        , ReplayWithFrames.version2 = optimizedReplay & version2
+        , ReplayWithFrames.label = optimizedReplay & label
+        , ReplayWithFrames.properties = optimizedReplay & properties
+        , ReplayWithFrames.levels = optimizedReplay & levels
+        , ReplayWithFrames.keyFrames = optimizedReplay & keyFrames
+        , ReplayWithFrames.frames = optimizedReplay & frames
+        , ReplayWithFrames.messages = optimizedReplay & messages
+        , ReplayWithFrames.marks = optimizedReplay & marks
+        , ReplayWithFrames.packages = optimizedReplay & packages
+        , ReplayWithFrames.objects = optimizedReplay & objects
+        , ReplayWithFrames.names = optimizedReplay & names
+        , ReplayWithFrames.classes = optimizedReplay & classes
+        , ReplayWithFrames.cache = optimizedReplay & cache
+        }
diff --git a/library/Octane/Type/Primitive.hs b/library/Octane/Type/Primitive.hs
deleted file mode 100644
--- a/library/Octane/Type/Primitive.hs
+++ /dev/null
@@ -1,1 +0,0 @@
-{-# OPTIONS_GHC -F -pgmF autoexporter #-}
diff --git a/library/Octane/Type/Primitive/Boolean.hs b/library/Octane/Type/Primitive/Boolean.hs
deleted file mode 100644
--- a/library/Octane/Type/Primitive/Boolean.hs
+++ /dev/null
@@ -1,58 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-
-module Octane.Type.Primitive.Boolean (Boolean(..)) where
-
-import Data.Function ((&))
-
-import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
-import qualified Data.Aeson.Types as Aeson
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Bits as BinaryBit
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified GHC.Generics as Generics
-
-
--- | A boolean value.
-newtype Boolean = Boolean
-    { unpackBoolean :: Bool
-    } deriving (Eq, Generics.Generic, Show)
-
--- | Boolean values are stored in the last bit of a byte. Decoding will fail if
--- the byte is anything other than @0b00000000@ or @0b00000001@.
-instance Binary.Binary Boolean where
-    get = do
-        value <- Binary.getWord8
-        case value of
-            0 -> pure (Boolean False)
-            1 -> pure (Boolean True)
-            _ -> fail ("Unexpected Boolean value " ++ show value)
-
-    put boolean = boolean
-        & unpackBoolean
-        & fromEnum
-        & fromIntegral
-        & Binary.putWord8
-
--- | Boolean values are stored as a single bit.
-instance BinaryBit.BinaryBit Boolean where
-    getBits _ = do
-        value <- BinaryBit.getBool
-        pure (Boolean value)
-
-    putBits _ boolean = boolean
-        & unpackBoolean
-        & BinaryBit.putBool
-
-instance Aeson.FromJSON Boolean where
-    parseJSON json = case json of
-        Aeson.Bool value -> pure (Boolean value)
-        _ -> Aeson.typeMismatch "Boolean" json
-
-instance DeepSeq.NFData Boolean where
-
-instance Aeson.ToJSON Boolean where
-    toJSON boolean = boolean
-        & unpackBoolean
-        & Aeson.toJSON
diff --git a/library/Octane/Type/Primitive/Dictionary.hs b/library/Octane/Type/Primitive/Dictionary.hs
deleted file mode 100644
--- a/library/Octane/Type/Primitive/Dictionary.hs
+++ /dev/null
@@ -1,59 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.Dictionary (Dictionary(..)) where
-
-import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
-import qualified Data.Binary as Binary
-import Data.Function ((&))
-import qualified Data.Map.Strict as Map
-import qualified GHC.Generics as Generics
-import qualified Octane.Type.Primitive.Text as Text
-
--- | A dictionary that maps strings to values. The dictionary is terminated by
--- the key "None".
-newtype Dictionary a = Dictionary
-    { unpackDictionary :: (Map.Map Text.Text a)
-    } deriving (Eq,Generics.Generic,Show)
-
-instance (Binary.Binary a) => Binary.Binary (Dictionary a) where
-    get = do
-        element <- getElement
-        if Map.null element
-            then do
-                element & Dictionary & return
-            else do
-                Dictionary elements <- Binary.get
-                elements & Map.union element & Dictionary & return
-    put dictionary = do
-        dictionary & unpackDictionary & Map.assocs & mapM_ putElement
-        noneKey & Binary.put
-
-instance (DeepSeq.NFData a) => DeepSeq.NFData (Dictionary a)
-
-instance (Aeson.ToJSON a) => Aeson.ToJSON (Dictionary a) where
-    toJSON dictionary =
-        dictionary & unpackDictionary & Map.mapKeys Text.unpackText & Aeson.toJSON
-
-getElement
-    :: (Binary.Binary a)
-    => Binary.Get (Map.Map Text.Text a)
-getElement = do
-    key <- Binary.get
-    if key == noneKey
-        then do
-            return Map.empty
-        else do
-            value <- Binary.get
-            value & Map.singleton key & return
-
-putElement
-    :: (Binary.Binary a)
-    => (Text.Text, a) -> Binary.Put
-putElement (key,value) = do
-    Binary.put key
-    Binary.put value
-
-noneKey :: Text.Text
-noneKey = "None"
diff --git a/library/Octane/Type/Primitive/Float32.hs b/library/Octane/Type/Primitive/Float32.hs
deleted file mode 100644
--- a/library/Octane/Type/Primitive/Float32.hs
+++ /dev/null
@@ -1,65 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-
-module Octane.Type.Primitive.Float32 (Float32(..)) where
-
-import Data.Function ((&))
-
-import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
-import qualified Data.Aeson.Types as Aeson
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Bits as BinaryBit
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.IEEE754 as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.ByteString.Lazy as LazyBytes
-import qualified Data.Scientific as Scientific
-import qualified GHC.Generics as Generics
-import qualified Octane.Utility as Utility
-
-
--- | A 32-bit little-endian float.
-newtype Float32 = Float32
-    { unpackFloat32 :: Float
-    } deriving (Eq, Fractional, Generics.Generic, Num, Ord, Show)
-
-instance Binary.Binary Float32 where
-    get = do
-        value <- Binary.getFloat32le
-        value & Float32 & pure
-
-    put float32 = float32
-        & unpackFloat32
-        & Binary.putFloat32le
-
-instance BinaryBit.BinaryBit Float32 where
-    getBits _ = do
-        bytes <- BinaryBit.getByteString 4
-        let value = Binary.runGet
-                Binary.getFloat32le
-                (bytes & LazyBytes.fromStrict & Utility.reverseBitsInBytes)
-        value & Float32 & pure
-
-    putBits _ float32 = float32
-        & unpackFloat32
-        & Binary.putFloat32le
-        & Binary.runPut
-        & LazyBytes.toStrict
-        & BinaryBit.putByteString
-
-instance Aeson.FromJSON Float32 where
-    parseJSON json = case json of
-        Aeson.Number number -> case Scientific.toBoundedRealFloat number of
-            Right float -> pure (Float32 float)
-            _ -> Aeson.typeMismatch "Float32" json
-        _ -> Aeson.typeMismatch "Float32" json
-
-instance DeepSeq.NFData Float32
-
-instance Aeson.ToJSON Float32 where
-    toJSON float32 = float32
-        & unpackFloat32
-        & Aeson.toJSON
diff --git a/library/Octane/Type/Primitive/Int32.hs b/library/Octane/Type/Primitive/Int32.hs
deleted file mode 100644
--- a/library/Octane/Type/Primitive/Int32.hs
+++ /dev/null
@@ -1,71 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-
-module Octane.Type.Primitive.Int32 (Int32(..), fromInt32) where
-
-import Data.Function ((&))
-
-import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
-import qualified Data.Aeson.Types as Aeson
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Bits as BinaryBit
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.ByteString.Lazy as LazyBytes
-import qualified Data.Int as Int
-import qualified Data.Scientific as Scientific
-import qualified GHC.Generics as Generics
-import qualified Octane.Utility as Utility
-
-
--- | A 32-bit little-endian integer.
-newtype Int32 = Int32
-    { unpackInt32 :: Int.Int32
-    } deriving (Eq, Generics.Generic, Num, Ord, Show)
-
-instance Binary.Binary Int32 where
-    get = do
-        value <- Binary.getWord32le
-        value & fromIntegral & Int32 & pure
-
-    put int32 = int32
-        & unpackInt32
-        & fromIntegral
-        & Binary.putWord32le
-
-instance BinaryBit.BinaryBit Int32 where
-    getBits _ = do
-        bytes <- BinaryBit.getByteString 4
-        let value = Binary.runGet
-                Binary.getWord32le
-                (bytes & LazyBytes.fromStrict & Utility.reverseBitsInBytes)
-        value & fromIntegral & Int32 & pure
-
-    putBits _ int32 = int32
-        & unpackInt32
-        & fromIntegral
-        & Binary.putWord32le
-        & Binary.runPut
-        & LazyBytes.toStrict
-        & BinaryBit.putByteString
-
-instance Aeson.FromJSON Int32 where
-    parseJSON json = case json of
-        Aeson.Number number -> case Scientific.toBoundedInteger number of
-            Just integer -> pure (Int32 integer)
-            _ -> Aeson.typeMismatch "Int32" json
-        _ -> Aeson.typeMismatch "Int32" json
-
-instance DeepSeq.NFData Int32 where
-
-instance Aeson.ToJSON Int32 where
-    toJSON int32 = int32
-        & unpackInt32
-        & Aeson.toJSON
-
-
-fromInt32 :: (Integral a) => Int32 -> a
-fromInt32 int32 = int32 & unpackInt32 & fromIntegral
diff --git a/library/Octane/Type/Primitive/Int64.hs b/library/Octane/Type/Primitive/Int64.hs
deleted file mode 100644
--- a/library/Octane/Type/Primitive/Int64.hs
+++ /dev/null
@@ -1,45 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-
-module Octane.Type.Primitive.Int64 (Int64(..)) where
-
-import Data.Function ((&))
-
-import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
-import qualified Data.Aeson.Types as Aeson
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.Int as Int
-import qualified Data.Scientific as Scientific
-import qualified GHC.Generics as Generics
-
-
--- | A 64-bit little-endian integer.
-newtype Int64 = Int64
-    { unpackInt64 :: Int.Int64
-    } deriving (Eq, Generics.Generic, Show)
-
-instance Binary.Binary Int64 where
-    get = do
-        value <- Binary.getWord64le
-        value & fromIntegral & Int64 & return
-
-    put word64 = word64
-        & unpackInt64
-        & fromIntegral
-        & Binary.putWord64le
-
-instance Aeson.FromJSON Int64 where
-    parseJSON json = case json of
-        Aeson.Number number -> case Scientific.toBoundedInteger number of
-            Just integer -> pure (Int64 integer)
-            _ -> Aeson.typeMismatch "Int64" json
-        _ -> Aeson.typeMismatch "Int64" json
-
-instance DeepSeq.NFData Int64
-
-instance Aeson.ToJSON Int64 where
-    toJSON int64 = int64
-        & unpackInt64
-        & Aeson.toJSON
diff --git a/library/Octane/Type/Primitive/Int8.hs b/library/Octane/Type/Primitive/Int8.hs
deleted file mode 100644
--- a/library/Octane/Type/Primitive/Int8.hs
+++ /dev/null
@@ -1,61 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-
-module Octane.Type.Primitive.Int8 (Int8(..), fromInt8) where
-
-import Data.Function ((&))
-
-import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
-import qualified Data.Aeson.Types as Aeson
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Bits as BinaryBit
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.ByteString.Lazy as LazyBytes
-import qualified Data.Int as Int
-import qualified Data.Scientific as Scientific
-import qualified GHC.Generics as Generics
-import qualified Octane.Utility as Utility
-
-
--- | An 8-bit little-endian integer.
-newtype Int8 = Int8
-    { unpackInt8 :: Int.Int8
-    } deriving (Eq, Generics.Generic, Num, Ord, Show)
-
-instance BinaryBit.BinaryBit Int8 where
-    getBits _ = do
-        bytes <- BinaryBit.getByteString 1
-        let value = Binary.runGet
-                Binary.getWord8
-                (bytes & LazyBytes.fromStrict & Utility.reverseBitsInBytes)
-        value & fromIntegral & Int8 & pure
-
-    putBits _ int8 = int8
-        & unpackInt8
-        & fromIntegral
-        & Binary.putWord8
-        & Binary.runPut
-        & LazyBytes.toStrict
-        & BinaryBit.putByteString
-
-instance Aeson.FromJSON Int8 where
-    parseJSON json = case json of
-        Aeson.Number number -> case Scientific.toBoundedInteger number of
-            Just integer -> pure (Int8 integer)
-            _ -> Aeson.typeMismatch "Int8" json
-        _ -> Aeson.typeMismatch "Int8" json
-
-instance DeepSeq.NFData Int8 where
-
-instance Aeson.ToJSON Int8 where
-    toJSON int8 = int8
-        & unpackInt8
-        & Aeson.toJSON
-
-
-fromInt8 :: (Integral a) => Int8 -> a
-fromInt8 int8 = int8 & unpackInt8 & fromIntegral
diff --git a/library/Octane/Type/Primitive/List.hs b/library/Octane/Type/Primitive/List.hs
deleted file mode 100644
--- a/library/Octane/Type/Primitive/List.hs
+++ /dev/null
@@ -1,63 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-
-module Octane.Type.Primitive.List (List(..)) where
-
-import Data.Function ((&))
-
-import qualified Control.DeepSeq as DeepSeq
-import qualified Control.Monad as Monad
-import qualified Data.Aeson as Aeson
-import qualified Data.Aeson.Types as Aeson
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Bits as BinaryBit
-import qualified Data.Vector as Vector
-import qualified GHC.Generics as Generics
-import qualified Octane.Type.Primitive.Int32 as Int32
-import qualified Octane.Type.Primitive.Boolean as Boolean
-
-
--- | A list of valeus.
-newtype List a = List
-    { unpackList :: [a]
-    } deriving (Eq, Generics.Generic, Show)
-
--- | Bytewise lists are length-prefixed.
-instance (Binary.Binary a) => Binary.Binary (List a) where
-    get = do
-        (Int32.Int32 size) <- Binary.get
-        elements <- Monad.replicateM (fromIntegral size) Binary.get
-        elements & List & return
-
-    put list = do
-        list & unpackList & length & fromIntegral & Int32.Int32 & Binary.put
-        list & unpackList & mapM_ Binary.put
-
--- | Bitwise lists use a bit to signify if there is another element.
-instance (BinaryBit.BinaryBit a) => BinaryBit.BinaryBit (List a) where
-    getBits _ = do
-        (Boolean.Boolean hasMore) <- BinaryBit.getBits 0
-        if hasMore
-            then do
-                x <- BinaryBit.getBits 0
-                (List xs) <- BinaryBit.getBits 0
-                pure (List (x : xs))
-            else pure (List [])
-
-    putBits _ list = case unpackList list of
-        [] -> BinaryBit.putBits 0 (Boolean.Boolean False)
-        x : xs -> do
-            BinaryBit.putBits 0 (Boolean.Boolean True)
-            BinaryBit.putBits 0 x
-            BinaryBit.putBits 0 (List xs)
-
-instance (Aeson.FromJSON a) => Aeson.FromJSON (List a) where
-    parseJSON json = case json of
-        Aeson.Array array -> do
-            values <- Vector.mapM Aeson.parseJSON array
-            values & Vector.toList & List & pure
-        _ -> Aeson.typeMismatch "List" json
-
-instance (DeepSeq.NFData a) => DeepSeq.NFData (List a)
-
-instance (Aeson.ToJSON a) => Aeson.ToJSON (List a) where
-    toJSON list = list & unpackList & Aeson.toJSON
diff --git a/library/Octane/Type/Primitive/Stream.hs b/library/Octane/Type/Primitive/Stream.hs
deleted file mode 100644
--- a/library/Octane/Type/Primitive/Stream.hs
+++ /dev/null
@@ -1,41 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-
-module Octane.Type.Primitive.Stream (Stream(..)) where
-
-import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.ByteString as BS
-import Data.Function ((&))
-import qualified GHC.Generics as Generics
-import qualified Octane.Type.Primitive.Int32 as Int32
-import qualified Octane.Utility as Utility
-
--- | A length-prefixed stream of bits. The length is given in bytes. Each byte
--- is reversed such that 0b01234567 is actually 0b76543210.
-newtype Stream = Stream
-    { unpackStream :: BS.ByteString
-    } deriving (Eq,Generics.Generic,Show)
-
-instance Binary.Binary Stream where
-    get = do
-        Int32.Int32 size <- Binary.get
-        content <- size & fromIntegral & Binary.getByteString
-        content & BS.map Utility.reverseBits & Stream & return
-    put stream = do
-        let content = unpackStream stream
-        content & BS.length & fromIntegral & Int32.Int32 & Binary.put
-        content & BS.map Utility.reverseBits & Binary.putByteString
-
-instance DeepSeq.NFData Stream
-
-instance Aeson.ToJSON Stream where
-    toJSON stream =
-        let size = stream & unpackStream & BS.length
-            bytes =
-                if size == 1
-                    then "byte"
-                    else "bytes"
-        in Aeson.toJSON (unwords ["Stream:", show size, bytes, "..."])
diff --git a/library/Octane/Type/Primitive/Text.hs b/library/Octane/Type/Primitive/Text.hs
deleted file mode 100644
--- a/library/Octane/Type/Primitive/Text.hs
+++ /dev/null
@@ -1,111 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.Text (Text(..)) where
-
-import Data.Function ((&))
-
-import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
-import qualified Data.Aeson.Types as Aeson
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Bits as BinaryBit
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.ByteString.Char8 as Bytes
-import qualified Data.Char as Char
-import qualified Data.String as String
-import qualified Data.Text as Text
-import qualified Data.Text.Encoding as Encoding
-import qualified GHC.Generics as Generics
-import qualified Octane.Type.Primitive.Int32 as Int32
-import qualified Octane.Utility as Utility
-
-
--- | A length-prefixed null-terminated string.
-newtype Text = Text
-    { unpackText :: Text.Text
-    } deriving (Eq, Generics.Generic, Ord, Show)
-
-instance Binary.Binary Text where
-    get = getText Binary.get Binary.getByteString
-
-    put text = putText
-        Binary.put
-        Binary.putByteString
-        id
-        text
-
-instance BinaryBit.BinaryBit Text where
-    getBits _ = getText (BinaryBit.getBits 32) (BinaryBit.getByteString)
-
-    putBits _ text = putText
-        (BinaryBit.putBits 32)
-        BinaryBit.putByteString
-        Utility.reverseBitsInBytes'
-        text
-
-instance String.IsString Text where
-    fromString string = string
-        & Text.pack
-        & Text
-
-instance Aeson.FromJSON Text where
-    parseJSON json = case json of
-        Aeson.String text -> text & Text & pure
-        _ -> Aeson.typeMismatch "Text" json
-
-instance DeepSeq.NFData Text
-
-instance Aeson.ToJSON Text where
-    toJSON text = text
-        & unpackText
-        & Aeson.toJSON
-
-
-getText :: (Monad m) => (m Int32.Int32) -> (Int -> m Bytes.ByteString) -> m Text
-getText getInt getBytes = do
-    (Int32.Int32 rawSize) <- getInt
-    (size, decode) <-
-        -- In some tiny percentage of replays, this nonsensical string size
-        -- shows up. As far as I can tell the next 3 bytes are always null. And
-        -- the actual string is "None", which is 5 bytes including the null
-        -- terminator.
-        --
-        -- These annoying replays come from around 2015-10-25 to 2015-11-01.
-        if rawSize == 0x05000000
-        then do
-            bytes <- getBytes 3
-            if Bytes.all (== '\0') bytes
-                then pure (5, Encoding.decodeLatin1)
-                else fail ("Unexpected Text bytes " ++ show bytes ++ " after size " ++ show rawSize)
-        else if rawSize < 0
-        then pure (-2 * fromIntegral rawSize, Encoding.decodeUtf16LE)
-        else pure (fromIntegral rawSize, Encoding.decodeLatin1)
-    bytes <- getBytes size
-    let rawText = decode bytes
-    case Text.splitAt (Text.length rawText - 1) rawText of
-        (text, "") -> text & Text & pure
-        (text, "\0") -> text & Text & pure
-        _ -> fail ("Unexpected Text value " ++ show rawText)
-
-
-putText :: (Monad m) => (Int32.Int32 -> m ()) -> (Bytes.ByteString -> m ()) -> (Bytes.ByteString -> Bytes.ByteString) -> Text -> m ()
-putText putInt putBytes convertBytes text = do
-    let fullText = text & unpackText & flip Text.snoc '\NUL'
-    let size = fullText & Text.length & fromIntegral
-    if Text.all Char.isLatin1 fullText
-        then do
-            size & Int32.Int32 & putInt
-            fullText & encodeLatin1 & convertBytes & putBytes
-        else do
-            size & negate & Int32.Int32 & putInt
-            fullText & Encoding.encodeUtf16LE & convertBytes & putBytes
-
-
-encodeLatin1 :: Text.Text -> Bytes.ByteString
-encodeLatin1 text = text
-    & Text.unpack
-    & Bytes.pack
diff --git a/library/Octane/Type/Primitive/Word32.hs b/library/Octane/Type/Primitive/Word32.hs
deleted file mode 100644
--- a/library/Octane/Type/Primitive/Word32.hs
+++ /dev/null
@@ -1,49 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-
-module Octane.Type.Primitive.Word32 (Word32(..), fromWord32) where
-
-import Data.Function ((&))
-
-import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
-import qualified Data.Aeson.Types as Aeson
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.Scientific as Scientific
-import qualified Data.Word as Word
-import qualified GHC.Generics as Generics
-
-
--- | A 32-bit little-endian word.
-newtype Word32 = Word32
-    { unpackWord32 :: Word.Word32
-    } deriving (Eq, Generics.Generic, Num, Ord, Show)
-
-instance Binary.Binary Word32 where
-    get = do
-        value <- Binary.getWord32le
-        value & Word32 & pure
-
-    put word32 = word32
-        & unpackWord32
-        & Binary.putWord32le
-
-instance Aeson.FromJSON Word32 where
-    parseJSON json = case json of
-        Aeson.Number number -> case Scientific.toBoundedInteger number of
-            Just integer -> pure (Word32 integer)
-            _ -> Aeson.typeMismatch "Word32" json
-        _ -> Aeson.typeMismatch "Word32" json
-
-instance DeepSeq.NFData Word32 where
-
-instance Aeson.ToJSON Word32 where
-    toJSON word32 = word32
-        & unpackWord32
-        & Aeson.toJSON
-
-
-fromWord32 :: (Integral a) => Word32 -> a
-fromWord32 word32 = word32 & unpackWord32 & fromIntegral
diff --git a/library/Octane/Type/Primitive/Word8.hs b/library/Octane/Type/Primitive/Word8.hs
deleted file mode 100644
--- a/library/Octane/Type/Primitive/Word8.hs
+++ /dev/null
@@ -1,60 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-
-module Octane.Type.Primitive.Word8 (Word8(..), fromWord8) where
-
-import Data.Function ((&))
-
-import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson as Aeson
-import qualified Data.Aeson.Types as Aeson
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Bits as BinaryBit
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.ByteString.Lazy as LazyBytes
-import qualified Data.Word as Word
-import qualified Data.Scientific as Scientific
-import qualified GHC.Generics as Generics
-import qualified Octane.Utility as Utility
-
-
--- | An 8-bit little-endian word.
-newtype Word8 = Word8
-    { unpackWord8 :: Word.Word8
-    } deriving (Eq, Generics.Generic, Num, Ord, Show)
-
-instance BinaryBit.BinaryBit Word8 where
-    getBits _ = do
-        bytes <- BinaryBit.getByteString 1
-        let value = Binary.runGet
-                Binary.getWord8
-                (bytes & LazyBytes.fromStrict & Utility.reverseBitsInBytes)
-        value & Word8 & pure
-
-    putBits _ word8 = word8
-        & unpackWord8
-        & Binary.putWord8
-        & Binary.runPut
-        & LazyBytes.toStrict
-        & BinaryBit.putByteString
-
-instance Aeson.FromJSON Word8 where
-    parseJSON json = case json of
-        Aeson.Number number -> case Scientific.toBoundedInteger number of
-            Just integer -> pure (Word8 integer)
-            _ -> Aeson.typeMismatch "Word8" json
-        _ -> Aeson.typeMismatch "Word8" json
-
-instance DeepSeq.NFData Word8 where
-
-instance Aeson.ToJSON Word8 where
-    toJSON word8 = word8
-        & unpackWord8
-        & Aeson.toJSON
-
-
-fromWord8 :: (Integral a) => Word8 -> a
-fromWord8 word8 = word8 & unpackWord8 & fromIntegral
diff --git a/library/Octane/Type/Property.hs b/library/Octane/Type/Property.hs
--- a/library/Octane/Type/Property.hs
+++ b/library/Octane/Type/Property.hs
@@ -1,42 +1,46 @@
 {-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE StrictData #-}
 
 module Octane.Type.Property (Property(..)) where
 
+import Data.Function ((&))
+
 import qualified Control.DeepSeq as DeepSeq
-import qualified Data.Aeson.Types as Aeson
+import qualified Data.Aeson as Aeson
 import qualified Data.Binary as Binary
-import Data.Function ((&))
 import qualified GHC.Generics as Generics
-import qualified Octane.Type.Primitive.Boolean as Boolean
-import qualified Octane.Type.Primitive.Dictionary as Dictionary
-import qualified Octane.Type.Primitive.Float32 as Float32
-import qualified Octane.Type.Primitive.List as List
-import qualified Octane.Type.Primitive.Text as Text
-import qualified Octane.Type.Primitive.Int32 as Int32
-import qualified Octane.Type.Primitive.Int64 as Int64
+import qualified Octane.Type.Boolean as Boolean
+import qualified Octane.Type.Dictionary as Dictionary
+import qualified Octane.Type.Float32 as Float32
+import qualified Octane.Type.Int32 as Int32
+import qualified Octane.Type.List as List
+import qualified Octane.Type.Text as Text
+import qualified Octane.Type.Word64 as Word64
 
+
 -- | A metadata property. All properties have a size, but only some actually
 -- use it. The value stored in the property can be an array, a boolean, and
 -- so on.
 data Property
-    = ArrayProperty !Int64.Int64
-                    !(List.List (Dictionary.Dictionary Property))
-    | BoolProperty !Int64.Int64
-                   !Boolean.Boolean
-    | ByteProperty !Int64.Int64
-                   !(Text.Text, Text.Text)
-    | FloatProperty !Int64.Int64
-                    !Float32.Float32
-    | IntProperty !Int64.Int64
-                  !Int32.Int32
-    | NameProperty !Int64.Int64
-                   !Text.Text
-    | QWordProperty !Int64.Int64
-                    !Int64.Int64
-    | StrProperty !Int64.Int64
-                  !Text.Text
-    deriving (Eq,Generics.Generic,Show)
+    = ArrayProperty Word64.Word64
+                    (List.List (Dictionary.Dictionary Property))
+    | BoolProperty Word64.Word64
+                   Boolean.Boolean
+    | ByteProperty Word64.Word64
+                   (Text.Text, Text.Text)
+    | FloatProperty Word64.Word64
+                    Float32.Float32
+    | IntProperty Word64.Word64
+                  Int32.Int32
+    | NameProperty Word64.Word64
+                   Text.Text
+    | QWordProperty Word64.Word64
+                    Word64.Word64
+    | StrProperty Word64.Word64
+                  Text.Text
+    deriving (Eq, Generics.Generic, Show)
 
 instance Binary.Binary Property where
     get = do
@@ -60,17 +64,17 @@
                         (key, value) & ByteProperty size & return
             _ | kind == floatProperty -> do
                 size <- Binary.get
-                value <-
-                    case Int64.unpackInt64 size of
-                        4 -> Binary.get
-                        x -> fail ("unknown FloatProperty size " ++ show x)
+                value <- case size of
+                    4 -> Binary.get
+                    (Word64.Word64 x) ->
+                        fail ("unknown FloatProperty size " ++ show x)
                 value & FloatProperty size & return
             _ | kind == intProperty -> do
                 size <- Binary.get
-                value <-
-                    case Int64.unpackInt64 size of
-                        4 -> Binary.get
-                        x -> fail ("unknown IntProperty size " ++ show x)
+                value <- case size of
+                    4 -> Binary.get
+                    (Word64.Word64 x) ->
+                        fail ("unknown IntProperty size " ++ show x)
                 value & IntProperty size & return
             _ | kind == nameProperty -> do
                 size <- Binary.get
@@ -78,16 +82,16 @@
                 value & NameProperty size & return
             _ | kind == qWordProperty -> do
                 size <- Binary.get
-                value <-
-                    case Int64.unpackInt64 size of
-                        8 -> Binary.get
-                        x -> fail ("unknown QWordProperty size " ++ show x)
+                value <- case size of
+                    8 -> Binary.get
+                    (Word64.Word64 x) ->
+                        fail ("unknown QWordProperty size " ++ show x)
                 value & QWordProperty size & return
             _ | kind == strProperty -> do
                 size <- Binary.get
                 value <- Binary.get
                 value & StrProperty size & return
-            _ -> fail ("unknown property type " ++ show (Text.unpackText kind))
+            _ -> fail ("unknown property type " ++ show (Text.unpack kind))
     put property =
         case property of
             ArrayProperty size value -> do
@@ -124,19 +128,20 @@
                 Binary.put size
                 Binary.put value
 
-instance DeepSeq.NFData Property
+instance DeepSeq.NFData Property where
 
+-- TODO: This encoding is lossy.
 instance Aeson.ToJSON Property where
-    toJSON property =
-        case property of
-            ArrayProperty _ x -> Aeson.toJSON x
-            BoolProperty _ x -> Aeson.toJSON x
-            ByteProperty _ (_,x) -> Aeson.toJSON x
-            FloatProperty _ x -> Aeson.toJSON x
-            IntProperty _ x -> Aeson.toJSON x
-            NameProperty _ x -> Aeson.toJSON x
-            QWordProperty _ x -> Aeson.toJSON x
-            StrProperty _ x -> Aeson.toJSON x
+    toJSON property = case property of
+        ArrayProperty _size x -> Aeson.toJSON x
+        BoolProperty _size x -> Aeson.toJSON x
+        ByteProperty _size (_key, value) -> Aeson.toJSON value
+        FloatProperty _size x -> Aeson.toJSON x
+        IntProperty _size x -> Aeson.toJSON x
+        NameProperty _size x -> Aeson.toJSON x
+        QWordProperty _size x -> Aeson.toJSON x
+        StrProperty _size x -> Aeson.toJSON x
+
 
 arrayProperty :: Text.Text
 arrayProperty = "ArrayProperty"
diff --git a/library/Octane/Type/RawReplay.hs b/library/Octane/Type/RawReplay.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/RawReplay.hs
@@ -0,0 +1,94 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE StrictData #-}
+
+module Octane.Type.RawReplay (RawReplay(..), newRawReplay) where
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Control.Monad as Monad
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified GHC.Generics as Generics
+import qualified Octane.Utility.CRC as CRC
+import qualified Octane.Type.Word32 as Word32
+import qualified Text.Printf as Printf
+
+
+data RawReplay = RawReplay
+    { headerSize :: Word32.Word32
+    -- ^ The byte size of the first section.
+    , headerCRC :: Word32.Word32
+    -- ^ The CRC of the first section.
+    , header :: LazyBytes.ByteString
+    -- ^ The first section.
+    , contentSize :: Word32.Word32
+    -- ^ The byte size of the second section.
+    , contentCRC :: Word32.Word32
+    -- ^ The CRC of the second section.
+    , content :: LazyBytes.ByteString
+    -- ^ The second section.
+    , footer :: LazyBytes.ByteString
+    -- ^ Arbitrary data after the second section. In replays generated by
+    -- Rocket League, this is always empty. However it is not technically
+    -- invalid to put something here.
+    } deriving (Eq, Generics.Generic, Show)
+
+-- | Decoding will fail if the CRCs don't match, but it is possible to encode
+-- invalid replays. That means @decode (encode rawReplay)@ can fail.
+instance Binary.Binary RawReplay where
+    get = do
+        headerSize <- Binary.get
+        headerCRC <- Binary.get
+        header <- Binary.getLazyByteString (Word32.fromWord32 headerSize)
+        checkCRC headerCRC header
+
+        contentSize <- Binary.get
+        contentCRC <- Binary.get
+        content <- Binary.getLazyByteString (Word32.fromWord32 contentSize)
+        checkCRC contentCRC content
+
+        footer <- Binary.getRemainingLazyByteString
+
+        pure RawReplay { .. }
+
+    put replay = do
+        Binary.put (headerSize replay)
+        Binary.put (headerCRC replay)
+        Binary.putLazyByteString (header replay)
+
+        Binary.put (contentSize replay)
+        Binary.put (contentCRC replay)
+        Binary.putLazyByteString (content replay)
+
+        Binary.putLazyByteString (footer replay)
+
+instance DeepSeq.NFData RawReplay where
+
+
+newRawReplay
+    :: LazyBytes.ByteString -- ^ The 'header'.
+    -> LazyBytes.ByteString -- ^ The 'content'.
+    -> LazyBytes.ByteString -- ^ The 'footer'.
+    -> RawReplay
+newRawReplay header content footer = do
+    let headerSize = Word32.toWord32 (LazyBytes.length header)
+    let headerCRC = Word32.Word32 (CRC.crc32 header)
+
+    let contentSize = Word32.toWord32 (LazyBytes.length content)
+    let contentCRC = Word32.Word32 (CRC.crc32 content)
+
+    RawReplay { .. }
+
+
+checkCRC :: (Monad m) => Word32.Word32 -> LazyBytes.ByteString -> m ()
+checkCRC (Word32.Word32 expected) bytes = do
+    let actual = CRC.crc32 bytes
+    Monad.when (actual /= expected) (do
+        let message = Printf.printf
+                "CRC 0x%08x does not equal expected value 0x%08x"
+                actual
+                expected
+        fail message)
diff --git a/library/Octane/Type/RemoteId.hs b/library/Octane/Type/RemoteId.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/RemoteId.hs
@@ -0,0 +1,23 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE StrictData #-}
+
+module Octane.Type.RemoteId (RemoteId(..)) where
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Aeson as Aeson
+import qualified GHC.Generics as Generics
+import qualified Octane.Type.Text as Text
+import qualified Octane.Type.Word64 as Word64
+
+
+data RemoteId
+    = SteamId Word64.Word64
+    | PlayStationId Text.Text
+    | SplitscreenId (Maybe Int)
+    | XboxId Word64.Word64
+    deriving (Eq, Generics.Generic, Show)
+
+instance DeepSeq.NFData RemoteId where
+
+instance Aeson.ToJSON RemoteId where
diff --git a/library/Octane/Type/Replay.hs b/library/Octane/Type/Replay.hs
--- a/library/Octane/Type/Replay.hs
+++ b/library/Octane/Type/Replay.hs
@@ -1,163 +1,144 @@
 {-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE StrictData #-}
 
-module Octane.Type.Replay (Replay(..)) where
+module Octane.Type.Replay (Replay(..), fromOptimizedReplay, toOptimizedReplay) where
 
+import Data.Aeson ((.=))
 import Data.Function ((&))
 
 import qualified Control.DeepSeq as DeepSeq
-import qualified Control.Monad as Monad
 import qualified Data.Aeson as Aeson
 import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.Map as Map
+import qualified Data.Text as StrictText
+import qualified Data.Version as Version
 import qualified GHC.Generics as Generics
-import qualified Octane.Json as Json
-import qualified Octane.Type.Actor as Actor
-import qualified Octane.Type.CacheItem as CacheItem
-import qualified Octane.Type.KeyFrame as KeyFrame
+import qualified Octane.Type.Dictionary as Dictionary
+import qualified Octane.Type.Frame as Frame
+import qualified Octane.Type.List as List
 import qualified Octane.Type.Mark as Mark
 import qualified Octane.Type.Message as Message
-import qualified Octane.Type.Primitive.Dictionary as Dictionary
-import qualified Octane.Type.Primitive.Int32 as Int32
-import qualified Octane.Type.Primitive.List as List
-import qualified Octane.Type.Primitive.Stream as Stream
-import qualified Octane.Type.Primitive.Text as Text
-import qualified Octane.Type.Primitive.Word32 as Word32
+import qualified Octane.Type.OptimizedReplay as OptimizedReplay
 import qualified Octane.Type.Property as Property
-import qualified Octane.Utility as Utility
-import qualified Text.Printf as Printf
+import qualified Octane.Type.Text as Text
+import qualified Octane.Type.Word32 as Word32
 
--- | An entire replay. All of the metadata has been parsed, but the actual net
--- stream has not.
+
 data Replay = Replay
-    -- Number of bytes in the first section.
-    { replaySize1 :: !Word32.Word32
-    -- CRC to check the first section.
-    , replayCRC1 :: !Word32.Word32
-    -- Major replay version number.
-    , replayVersion1 :: !Int32.Int32
-    -- Minor replay version number.
-    , replayVersion2 :: !Int32.Int32
-    -- Label, which is always "TAGame.Replay_Soccar_TA".
-    , replayLabel :: !Text.Text
-    -- High-level metadata about the replay.
-    , replayProperties :: !(Dictionary.Dictionary Property.Property)
-    -- Number of bytes in the last section.
-    , replaySize2 :: !Word32.Word32
-    -- CRC to check the last section.
-    , replayCRC2 :: !Word32.Word32
-    -- Array of strings for all of the levels that need to be loaded (array
-    -- length followed by each string)
-    , replayLevels :: !(List.List Text.Text)
-    -- Array of Keyframe information used for timeline scrubbing (array
-    -- length followed by each keyframe struct) (Time, Frame, File Position)
-    , replayKeyFrames :: !(List.List KeyFrame.KeyFrame)
-    -- Array of bytes that is the bulk of the data. This is the raw network
-    -- stream. (array length followed by a bunch of bytes)
-    , replayStream :: !Stream.Stream
-    -- Array of debugging logs (strings). This reminds me that I should
-    -- probably turn these off to make the replays smaller. (array length
-    -- followed by each string)
-    , replayMessages :: !(List.List Message.Message)
-    -- Array of information used to display the Tick marks in the replay (goal
-    -- scores). (array length followed by each tick struct) (Type, Frame)
-    , replayMarks :: !(List.List Mark.Mark)
-    -- Array of strings of replicated Packages
-    , replayPackages :: !(List.List Text.Text)
-    -- Array of strings for the Object table. Whenever a persistent object gets
-    -- referenced in the network stream its path gets added to this array. Then
-    -- its index in this array is used in the network stream.
-    , replayObjects :: !(List.List Text.Text)
-    -- Array of strings for the Name table. "Names" are commonly used strings
-    -- that get assigned an integer for use in the network stream.
-    , replayNames :: !(List.List Text.Text)
-    -- Map of string, integer pairs for the Class Index Map. Whenever a class
-    -- is used in the network stream it is given an integer id by this map.
-    , replayActors :: !(List.List Actor.Actor)
-    -- "Class Net Cache Map" maps each replicated property in a class to an
-    -- integer id used in the network stream.
-    , replayCacheItems :: !(List.List CacheItem.CacheItem)
-    } deriving (Eq,Generics.Generic,Show)
+    { version :: Version.Version
+    , metadata :: Map.Map StrictText.Text Property.Property
+    , levels :: [StrictText.Text]
+    , messages :: Map.Map StrictText.Text StrictText.Text
+    , tickMarks :: Map.Map StrictText.Text StrictText.Text
+    , frames :: [Frame.Frame]
+    } deriving (Eq, Generics.Generic, Show)
 
 instance Binary.Binary Replay where
     get = do
-        size1 <- Binary.get
-        crc1 <- Binary.get
-        data1 <- Binary.getLazyByteString (Word32.fromWord32 size1)
-        let actualCRC1 = Utility.crc32 data1
-        let expectedCRC1 = Word32.fromWord32 crc1
-        Monad.when (actualCRC1 /= expectedCRC1) (fail (Printf.printf
-            "First CRC 0x%08x does not match expected value 0x%08x"
-            actualCRC1 expectedCRC1))
+        optimizedReplay <- Binary.get
+        fromOptimizedReplay optimizedReplay
 
-        size2 <- Binary.get
-        crc2 <- Binary.get
-        data2 <- Binary.getLazyByteString (Word32.fromWord32 size2)
-        let actualCRC2 = Utility.crc32 data2
-        let expectedCRC2 = Word32.fromWord32 crc2
-        Monad.when (actualCRC2 /= expectedCRC2) (fail (Printf.printf
-            "Second CRC 0x%08x does not match expected value 0x%08x"
-            actualCRC2 expectedCRC2))
+    put replay = do
+        optimizedReplay <- toOptimizedReplay replay
+        Binary.put optimizedReplay
 
-        pure (flip Binary.runGet (LazyBytes.append data1 data2) (do
-            version1 <- Binary.get
-            version2 <- Binary.get
-            label <- Binary.get
-            properties <- Binary.get
+instance DeepSeq.NFData Replay where
 
-            levels <- Binary.get
-            keyFrames <- Binary.get
-            stream <- Binary.get
-            messages <- Binary.get
-            marks <- Binary.get
-            packages <- Binary.get
-            objects <- Binary.get
-            names <- Binary.get
-            actors <- Binary.get
-            cacheItems <- Binary.get
+instance Aeson.ToJSON Replay where
+    toJSON replay = Aeson.object
+        [ "Version" .= version replay
+        , "Metadata" .= metadata replay
+        , "Levels" .= levels replay
+        , "Messages" .= messages replay
+        , "TickMarks" .= tickMarks replay
+        , "Frames" .= frames replay
+        ]
 
-            pure Replay
-                { replaySize1 = size1
-                , replayCRC1 = crc1
-                , replayVersion1 = version1
-                , replayVersion2 = version2
-                , replayLabel = label
-                , replayProperties = properties
-                , replaySize2 = size2
-                , replayCRC2 = crc2
-                , replayLevels = levels
-                , replayKeyFrames = keyFrames
-                , replayStream = stream
-                , replayMessages = messages
-                , replayMarks = marks
-                , replayPackages = packages
-                , replayObjects = objects
-                , replayNames = names
-                , replayActors = actors
-                , replayCacheItems = cacheItems
-                }))
 
-    put replay = do
-        replay & replaySize1 & Binary.put
-        replay & replayCRC1 & Binary.put
-        replay & replayVersion1 & Binary.put
-        replay & replayVersion2 & Binary.put
-        replay & replayLabel & Binary.put
-        replay & replayProperties & Binary.put
-        replay & replaySize2 & Binary.put
-        replay & replayCRC2 & Binary.put
-        replay & replayLevels & Binary.put
-        replay & replayKeyFrames & Binary.put
-        replay & replayStream & Binary.put
-        replay & replayMessages & Binary.put
-        replay & replayMarks & Binary.put
-        replay & replayPackages & Binary.put
-        replay & replayObjects & Binary.put
-        replay & replayNames & Binary.put
-        replay & replayActors & Binary.put
-        replay & replayCacheItems & Binary.put
+fromOptimizedReplay :: (Monad m) => OptimizedReplay.OptimizedReplay -> m Replay
+fromOptimizedReplay optimizedReplay = do
+    pure Replay
+        { version =
+            [ OptimizedReplay.version1 optimizedReplay
+            , OptimizedReplay.version2 optimizedReplay
+            ] & map Word32.fromWord32 & Version.makeVersion
+        , metadata = optimizedReplay
+            & OptimizedReplay.properties
+            & Dictionary.unpack
+            & Map.mapKeys Text.unpack
+        , levels = optimizedReplay
+            & OptimizedReplay.levels
+            & List.unpack
+            & map Text.unpack
+        , messages = optimizedReplay
+            & OptimizedReplay.messages
+            & List.unpack
+            & map (\ message -> do
+                let key = message
+                        & Message.frame
+                        & Word32.unpack
+                        & show
+                        & StrictText.pack
+                let value = message
+                        & Message.content
+                        & Text.unpack
+                (key, value))
+            & Map.fromList
+        , tickMarks = optimizedReplay
+            & OptimizedReplay.marks
+            & List.unpack
+            & map (\ mark -> do
+                let key = mark
+                        & Mark.frame
+                        & Word32.unpack
+                        & show
+                        & StrictText.pack
+                let value = mark
+                        & Mark.label
+                        & Text.unpack
+                (key, value))
+            & Map.fromList
+        , frames = optimizedReplay
+            & OptimizedReplay.frames
+        }
 
-instance DeepSeq.NFData Replay
 
-instance Aeson.ToJSON Replay where
-    toJSON = Aeson.genericToJSON (Json.toJsonOptions "Replay")
+toOptimizedReplay :: (Monad m) => Replay -> m OptimizedReplay.OptimizedReplay
+toOptimizedReplay replay = do
+    let [version1, version2] = replay
+            & version
+            & Version.versionBranch
+            & map Word32.toWord32
+
+    pure OptimizedReplay.OptimizedReplay
+        { OptimizedReplay.version1 = version1
+        , OptimizedReplay.version2 = version2
+        , OptimizedReplay.label = "TAGame.Replay_Soccar_TA"
+        , OptimizedReplay.properties = replay & metadata & Map.mapKeys Text.Text & Dictionary.Dictionary
+        , OptimizedReplay.levels = replay & levels & map Text.Text & List.List
+        , OptimizedReplay.keyFrames = List.List [] -- TODO
+        , OptimizedReplay.frames = replay & frames
+        , OptimizedReplay.messages = replay
+            & messages
+            & Map.toList
+            & map (\ (key, value) -> do
+                let frame = key & StrictText.unpack & read & Word32.Word32
+                let content = value & Text.Text
+                Message.Message frame "" content)
+            & List.List
+        , OptimizedReplay.marks = replay
+            & tickMarks
+            & Map.toList
+            & map (\ (key, value) -> do
+                let label = value & Text.Text
+                let frame = key & StrictText.unpack & read & Word32.Word32
+                Mark.Mark label frame)
+            & List.List
+        , OptimizedReplay.packages = List.List [] -- TODO
+        , OptimizedReplay.objects = List.List [] -- TODO
+        , OptimizedReplay.names = List.List [] -- TODO
+        , OptimizedReplay.classes = List.List [] -- TODO
+        , OptimizedReplay.cache = List.List [] -- TODO
+        }
diff --git a/library/Octane/Type/ReplayWithFrames.hs b/library/Octane/Type/ReplayWithFrames.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/ReplayWithFrames.hs
@@ -0,0 +1,95 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE StrictData #-}
+
+module Octane.Type.ReplayWithFrames (ReplayWithFrames(..), fromReplayWithoutFrames, toReplayWithoutFrames) where
+
+import Data.Function ((&))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Binary as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified GHC.Generics as Generics
+import qualified Octane.Type.CacheItem as CacheItem
+import qualified Octane.Type.ClassItem as ClassItem
+import qualified Octane.Type.Dictionary as Dictionary
+import qualified Octane.Type.Frame as Frame
+import qualified Octane.Type.KeyFrame as KeyFrame
+import qualified Octane.Type.List as List
+import qualified Octane.Type.Mark as Mark
+import qualified Octane.Type.Message as Message
+import qualified Octane.Type.Property as Property
+import qualified Octane.Type.ReplayWithoutFrames as ReplayWithoutFrames
+import qualified Octane.Type.Stream as Stream
+import qualified Octane.Type.Text as Text
+import qualified Octane.Type.Word32 as Word32
+import qualified Octane.Utility.Parser as Parser
+
+
+data ReplayWithFrames = ReplayWithFrames
+    { version1 :: Word32.Word32
+    , version2 :: Word32.Word32
+    , label :: Text.Text
+    , properties :: Dictionary.Dictionary Property.Property
+    , levels :: List.List Text.Text
+    , keyFrames :: List.List KeyFrame.KeyFrame
+    , frames :: [Frame.Frame]
+    , messages :: List.List Message.Message
+    , marks :: List.List Mark.Mark
+    , packages :: List.List Text.Text
+    , objects :: List.List Text.Text
+    , names :: List.List Text.Text
+    , classes :: List.List ClassItem.ClassItem
+    , cache :: List.List CacheItem.CacheItem
+    } deriving (Eq, Generics.Generic, Show)
+
+instance Binary.Binary ReplayWithFrames where
+    get = do
+        replayWithoutFrames <- Binary.get
+        fromReplayWithoutFrames replayWithoutFrames
+
+    put replay = do
+        replayWithoutFrames <- toReplayWithoutFrames replay
+        Binary.put replayWithoutFrames
+
+instance DeepSeq.NFData ReplayWithFrames where
+
+
+fromReplayWithoutFrames :: (Monad m) => ReplayWithoutFrames.ReplayWithoutFrames -> m ReplayWithFrames
+fromReplayWithoutFrames replayWithoutFrames = do
+    pure ReplayWithFrames
+        { version1 = replayWithoutFrames & ReplayWithoutFrames.version1
+        , version2 = replayWithoutFrames & ReplayWithoutFrames.version2
+        , label = replayWithoutFrames & ReplayWithoutFrames.label
+        , properties = replayWithoutFrames & ReplayWithoutFrames.properties
+        , levels = replayWithoutFrames & ReplayWithoutFrames.levels
+        , keyFrames = replayWithoutFrames & ReplayWithoutFrames.keyFrames
+        , frames = replayWithoutFrames & Parser.parseFrames
+        , messages = replayWithoutFrames & ReplayWithoutFrames.messages
+        , marks = replayWithoutFrames & ReplayWithoutFrames.marks
+        , packages = replayWithoutFrames & ReplayWithoutFrames.packages
+        , objects = replayWithoutFrames & ReplayWithoutFrames.objects
+        , names = replayWithoutFrames & ReplayWithoutFrames.names
+        , classes = replayWithoutFrames & ReplayWithoutFrames.classes
+        , cache = replayWithoutFrames & ReplayWithoutFrames.cache
+        }
+
+
+toReplayWithoutFrames :: (Monad m) => ReplayWithFrames -> m ReplayWithoutFrames.ReplayWithoutFrames
+toReplayWithoutFrames replayWithFrames = do
+    pure ReplayWithoutFrames.ReplayWithoutFrames
+        { ReplayWithoutFrames.version1 = replayWithFrames & version1
+        , ReplayWithoutFrames.version2 = replayWithFrames & version2
+        , ReplayWithoutFrames.label = replayWithFrames & label
+        , ReplayWithoutFrames.properties = replayWithFrames & properties
+        , ReplayWithoutFrames.levels = replayWithFrames & levels
+        , ReplayWithoutFrames.keyFrames = replayWithFrames & keyFrames
+        , ReplayWithoutFrames.stream = Stream.Stream LazyBytes.empty -- TODO
+        , ReplayWithoutFrames.messages = replayWithFrames & messages
+        , ReplayWithoutFrames.marks = replayWithFrames & marks
+        , ReplayWithoutFrames.packages = replayWithFrames & packages
+        , ReplayWithoutFrames.objects = replayWithFrames & objects
+        , ReplayWithoutFrames.names = replayWithFrames & names
+        , ReplayWithoutFrames.classes = replayWithFrames & classes
+        , ReplayWithoutFrames.cache = replayWithFrames & cache
+        }
diff --git a/library/Octane/Type/ReplayWithoutFrames.hs b/library/Octane/Type/ReplayWithoutFrames.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/ReplayWithoutFrames.hs
@@ -0,0 +1,106 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE StrictData #-}
+
+module Octane.Type.ReplayWithoutFrames (ReplayWithoutFrames(..), fromRawReplay, toRawReplay) where
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified GHC.Generics as Generics
+import qualified Octane.Type.CacheItem as CacheItem
+import qualified Octane.Type.ClassItem as ClassItem
+import qualified Octane.Type.Dictionary as Dictionary
+import qualified Octane.Type.KeyFrame as KeyFrame
+import qualified Octane.Type.List as List
+import qualified Octane.Type.Mark as Mark
+import qualified Octane.Type.Message as Message
+import qualified Octane.Type.Property as Property
+import qualified Octane.Type.RawReplay as RawReplay
+import qualified Octane.Type.Stream as Stream
+import qualified Octane.Type.Text as Text
+import qualified Octane.Type.Word32 as Word32
+
+
+data ReplayWithoutFrames = ReplayWithoutFrames
+    { version1 :: Word32.Word32
+    , version2 :: Word32.Word32
+    , label :: Text.Text
+    , properties :: Dictionary.Dictionary Property.Property
+    , levels :: List.List Text.Text
+    , keyFrames :: List.List KeyFrame.KeyFrame
+    , stream :: Stream.Stream
+    , messages :: List.List Message.Message
+    , marks :: List.List Mark.Mark
+    , packages :: List.List Text.Text
+    , objects :: List.List Text.Text
+    , names :: List.List Text.Text
+    , classes :: List.List ClassItem.ClassItem
+    , cache :: List.List CacheItem.CacheItem
+    } deriving (Eq, Generics.Generic, Show)
+
+instance Binary.Binary ReplayWithoutFrames where
+    get = do
+        rawReplay <- Binary.get
+        fromRawReplay rawReplay
+
+    put replayWithoutFrames = do
+        rawReplay <- toRawReplay replayWithoutFrames
+        Binary.put rawReplay
+
+instance DeepSeq.NFData ReplayWithoutFrames where
+
+
+fromRawReplay :: (Monad m) => RawReplay.RawReplay -> m ReplayWithoutFrames
+fromRawReplay rawReplay = do
+    let header = RawReplay.header rawReplay
+    let content = RawReplay.content rawReplay
+
+    let get = do
+            version1 <- Binary.get
+            version2 <- Binary.get
+            label <- Binary.get
+            properties <- Binary.get
+            levels <- Binary.get
+            keyFrames <- Binary.get
+            stream <- Binary.get
+            messages <- Binary.get
+            marks <- Binary.get
+            packages <- Binary.get
+            objects <- Binary.get
+            names <- Binary.get
+            classes <- Binary.get
+            cache <- Binary.get
+
+            pure ReplayWithoutFrames { .. }
+    let bytes = LazyBytes.append header content
+
+    pure (Binary.runGet get bytes)
+
+
+toRawReplay :: (Monad m) => ReplayWithoutFrames -> m RawReplay.RawReplay
+toRawReplay replay = do
+    let header = Binary.runPut (do
+            Binary.put (version1 replay)
+            Binary.put (version2 replay)
+            Binary.put (label replay)
+            Binary.put (properties replay))
+
+    let content = Binary.runPut (do
+            Binary.put (levels replay)
+            Binary.put (keyFrames replay)
+            Binary.put (stream replay)
+            Binary.put (messages replay)
+            Binary.put (marks replay)
+            Binary.put (packages replay)
+            Binary.put (objects replay)
+            Binary.put (names replay)
+            Binary.put (classes replay)
+            Binary.put (cache replay))
+
+    let footer = LazyBytes.empty
+
+    pure (RawReplay.newRawReplay header content footer)
diff --git a/library/Octane/Type/Replication.hs b/library/Octane/Type/Replication.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/Replication.hs
@@ -0,0 +1,25 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE StrictData #-}
+
+module Octane.Type.Replication (Replication(..)) where
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Map.Strict as Map
+import qualified Data.Text as StrictText
+import qualified GHC.Generics as Generics
+import qualified Octane.Type.Initialization as Initialization
+import qualified Octane.Type.State as State
+import qualified Octane.Type.Value as Value
+
+
+data Replication = Replication
+    { actorId :: Word
+    , objectName :: StrictText.Text
+    , className :: StrictText.Text
+    , state :: State.State
+    , initialization :: Maybe Initialization.Initialization
+    , properties :: Map.Map StrictText.Text Value.Value
+    } deriving (Eq, Generics.Generic, Show)
+
+instance DeepSeq.NFData Replication where
diff --git a/library/Octane/Type/State.hs b/library/Octane/Type/State.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/State.hs
@@ -0,0 +1,15 @@
+{-# LANGUAGE DeriveGeneric #-}
+
+module Octane.Type.State (State(..)) where
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified GHC.Generics as Generics
+
+
+data State
+    = SOpening
+    | SExisting
+    | SClosing
+    deriving (Eq, Generics.Generic, Show)
+
+instance DeepSeq.NFData State where
diff --git a/library/Octane/Type/Stream.hs b/library/Octane/Type/Stream.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/Stream.hs
@@ -0,0 +1,34 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+
+module Octane.Type.Stream (Stream(..)) where
+
+import Data.Function ((&))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified GHC.Generics as Generics
+import qualified Octane.Type.Word32 as Word32
+import qualified Octane.Utility.Endian as Endian
+
+
+-- | A length-prefixed stream of bits. The length is given in bytes. Each byte
+-- is reversed such that @0b01234567@ is actually @0b76543210@.
+newtype Stream = Stream
+    { unpack :: LazyBytes.ByteString
+    } deriving (Eq, Generics.Generic, Show)
+
+instance Binary.Binary Stream where
+    get = do
+        size <- Binary.get
+        content <- size & Word32.fromWord32 & Binary.getLazyByteString
+        content & Endian.reverseBitsInBytes & Stream & return
+    put stream = do
+        let content = unpack stream
+        content & LazyBytes.length & Word32.toWord32 & Binary.put
+        content & Endian.reverseBitsInBytes & Binary.putLazyByteString
+
+instance DeepSeq.NFData Stream where
diff --git a/library/Octane/Type/Text.hs b/library/Octane/Type/Text.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/Text.hs
@@ -0,0 +1,114 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Octane.Type.Text (Text(..)) where
+
+import Data.Function ((&))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Aeson as Aeson
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits as BinaryBit
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Char8 as StrictBytes
+import qualified Data.Char as Char
+import qualified Data.String as String
+import qualified Data.Text as StrictText
+import qualified Data.Text.Encoding as Encoding
+import qualified GHC.Generics as Generics
+import qualified Octane.Type.Int32 as Int32
+import qualified Octane.Utility.Endian as Endian
+
+
+-- | A thin wrapper around 'StrictText.Text'.
+newtype Text = Text
+    { unpack :: StrictText.Text
+    } deriving (Eq, Generics.Generic, Ord)
+
+-- | Text is both length-prefixed and null-terminated.
+instance Binary.Binary Text where
+    get = getText
+        Binary.get
+        Binary.getByteString
+        id
+
+    put text = putText
+        Binary.put
+        Binary.putByteString
+        id
+        text
+
+instance BinaryBit.BinaryBit Text where
+    getBits _ = getText
+        (BinaryBit.getBits 32)
+        BinaryBit.getByteString
+        Endian.reverseBitsInBytes'
+
+    putBits _ text = putText
+        (BinaryBit.putBits 32)
+        BinaryBit.putByteString
+        Endian.reverseBitsInBytes'
+        text
+
+instance String.IsString Text where
+    fromString string = Text (StrictText.pack string)
+
+instance DeepSeq.NFData Text where
+
+instance Show Text where
+    show text = StrictText.unpack (unpack text)
+
+instance Aeson.ToJSON Text where
+    toJSON text = text
+        & unpack
+        & Aeson.toJSON
+
+
+getText :: (Monad m) => (m Int32.Int32) -> (Int -> m StrictBytes.ByteString) -> (StrictBytes.ByteString -> StrictBytes.ByteString) -> m Text
+getText getInt getBytes convertBytes = do
+    (Int32.Int32 rawSize) <- getInt
+    (size, decode) <-
+        -- In some tiny percentage of replays, this nonsensical string size
+        -- shows up. As far as I can tell the next 3 bytes are always null. And
+        -- the actual string is "None", which is 5 bytes including the null
+        -- terminator.
+        --
+        -- These annoying replays come from around 2015-10-25 to 2015-11-01.
+        if rawSize == 0x05000000
+        then do
+            bytes <- getBytes 3
+            if StrictBytes.all (== '\0') bytes
+                then pure (5, Encoding.decodeLatin1)
+                else fail ("Unexpected Text bytes " ++ show bytes ++ " after size " ++ show rawSize)
+        else if rawSize < 0
+        then pure (-2 * fromIntegral rawSize, Encoding.decodeUtf16LE)
+        else pure (fromIntegral rawSize, Encoding.decodeLatin1)
+    bytes <- getBytes size
+    let rawText = bytes & convertBytes & decode
+    case StrictText.splitAt (StrictText.length rawText - 1) rawText of
+        (text, "") -> text & Text & pure
+        (text, "\0") -> text & Text & pure
+        _ -> fail ("Unexpected Text value " ++ show rawText)
+
+
+putText :: (Monad m) => (Int32.Int32 -> m ()) -> (StrictBytes.ByteString -> m ()) -> (StrictBytes.ByteString -> StrictBytes.ByteString) -> Text -> m ()
+putText putInt putBytes convertBytes text = do
+    let fullText = text & unpack & flip StrictText.snoc '\NUL'
+    let size = fullText & StrictText.length & fromIntegral
+    if StrictText.all Char.isLatin1 fullText
+    then do
+        size & Int32.Int32 & putInt
+        fullText & encodeLatin1 & convertBytes & putBytes
+    else do
+        size & negate & Int32.Int32 & putInt
+        fullText & Encoding.encodeUtf16LE & convertBytes & putBytes
+
+
+encodeLatin1 :: StrictText.Text -> StrictBytes.ByteString
+encodeLatin1 text = text
+    & StrictText.unpack
+    & StrictBytes.pack
diff --git a/library/Octane/Type/Value.hs b/library/Octane/Type/Value.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/Value.hs
@@ -0,0 +1,120 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE StrictData #-}
+
+module Octane.Type.Value (Value(..)) where
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified GHC.Generics as Generics
+import qualified Octane.Type.Boolean as Boolean
+import qualified Octane.Type.Float32 as Float32
+import qualified Octane.Type.Int32 as Int32
+import qualified Octane.Type.RemoteId as RemoteId
+import qualified Octane.Type.Text as Text
+import qualified Octane.Type.Vector as Vector
+import qualified Octane.Type.Word16 as Word16
+import qualified Octane.Type.Word32 as Word32
+import qualified Octane.Type.Word64 as Word64
+import qualified Octane.Type.Word8 as Word8
+
+
+data Value
+    = VBoolean
+        Boolean.Boolean
+    | VByte
+        Word8.Word8
+    | VCamSettings
+        Float32.Float32
+        Float32.Float32
+        Float32.Float32
+        Float32.Float32
+        Float32.Float32
+        Float32.Float32
+    | VDemolish
+        Boolean.Boolean
+        Word32.Word32
+        Boolean.Boolean
+        Word32.Word32
+        (Vector.Vector Int)
+        (Vector.Vector Int)
+    | VEnum
+        Word16.Word16
+        Boolean.Boolean
+    | VExplosion
+        Boolean.Boolean
+        (Maybe Int32.Int32)
+        (Vector.Vector Int)
+    | VFlaggedInt
+        Boolean.Boolean
+        Int32.Int32
+    | VFloat
+        Float32.Float32
+    | VGameMode
+        Word8.Word8
+    | VInt
+        Int32.Int32
+    | VLoadout
+        Word8.Word8
+        Word32.Word32
+        Word32.Word32
+        Word32.Word32
+        Word32.Word32
+        Word32.Word32
+        Word32.Word32
+        Word32.Word32
+        (Maybe Word32.Word32)
+    | VLoadoutOnline
+        Word32.Word32
+        Word32.Word32
+        Word32.Word32
+        (Maybe Word8.Word8)
+    | VLocation
+        (Vector.Vector Int)
+    | VMusicStinger
+        Boolean.Boolean
+        Word32.Word32
+        Word8.Word8
+    | VPickup
+        Boolean.Boolean
+        (Maybe Word32.Word32)
+        Boolean.Boolean
+    | VPrivateMatchSettings
+        Text.Text
+        Word32.Word32
+        Word32.Word32
+        Text.Text
+        Text.Text
+        Boolean.Boolean
+    | VQWord
+        Word64.Word64
+    | VRelativeRotation
+        (Vector.Vector Float)
+    | VReservation
+        Int
+        Word8.Word8
+        RemoteId.RemoteId
+        (Maybe Word8.Word8)
+        (Maybe Text.Text)
+        Boolean.Boolean
+        Boolean.Boolean
+    | VRigidBodyState
+        Boolean.Boolean
+        (Vector.Vector Int)
+        (Vector.Vector Float)
+        (Maybe (Vector.Vector Int))
+        (Maybe (Vector.Vector Int))
+    | VString
+        Text.Text
+    | VTeamPaint
+        Word8.Word8
+        Word8.Word8
+        Word8.Word8
+        Word32.Word32
+        Word32.Word32
+    | VUniqueId
+        Word8.Word8
+        RemoteId.RemoteId
+        (Maybe Word8.Word8)
+    deriving (Eq, Generics.Generic, Show)
+
+instance DeepSeq.NFData Value where
diff --git a/library/Octane/Type/Vector.hs b/library/Octane/Type/Vector.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/Vector.hs
@@ -0,0 +1,25 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE StrictData #-}
+
+module Octane.Type.Vector (Vector(..)) where
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Aeson as Aeson
+import qualified GHC.Generics as Generics
+
+
+data Vector a = Vector
+    { x :: a
+    , y :: a
+    , z :: a
+    } deriving (Eq, Generics.Generic, Show)
+
+instance (DeepSeq.NFData a) => DeepSeq.NFData (Vector a) where
+
+instance (Aeson.ToJSON a) => Aeson.ToJSON (Vector a) where
+    toJSON vector = Aeson.toJSON
+        [ x vector
+        , y vector
+        , z vector
+        ]
diff --git a/library/Octane/Type/Word16.hs b/library/Octane/Type/Word16.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/Word16.hs
@@ -0,0 +1,65 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+module Octane.Type.Word16 (Word16(..), fromWord16, toWord16) where
+
+import Data.Function ((&))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Aeson as Aeson
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.Word as Word
+import qualified GHC.Generics as Generics
+import qualified Text.Printf as Printf
+
+
+-- | A 16-bit unsigned integer.
+newtype Word16 = Word16
+    { unpack :: Word.Word16
+    } deriving (Eq, Generics.Generic, Num, Ord)
+
+-- | Stored in little-endian byte order.
+instance Binary.Binary Word16 where
+    get = do
+        value <- Binary.getWord16le
+        pure (Word16 value)
+
+    put word16 = do
+        let value = unpack word16
+        Binary.putWord16le value
+
+instance DeepSeq.NFData Word16 where
+
+-- | Shown as @0x0102@.
+instance Show Word16 where
+    show word16 = Printf.printf "0x%04x" (unpack word16)
+
+instance Aeson.ToJSON Word16 where
+    toJSON word16 = word16
+        & unpack
+        & Aeson.toJSON
+
+
+-- | Converts a 'Word16' into any 'Integral' value.
+--
+-- >>> fromWord16 0x0001 :: Word.Word16
+-- 1
+--
+-- >>> fromWord16 0xffff :: Data.Int.Int16
+-- -1
+fromWord16 :: (Integral a) => Word16 -> a
+fromWord16 word16 = fromIntegral (unpack word16)
+
+
+-- | Converts any 'Integral' value into a 'Word16'.
+--
+-- >>> toWord16 (1 :: Word.Word16)
+-- 0x0001
+--
+-- >>> toWord16 (-1 :: Data.Int.Int16)
+-- 0xffff
+toWord16 :: (Integral a) => a -> Word16
+toWord16 value = Word16 (fromIntegral value)
diff --git a/library/Octane/Type/Word32.hs b/library/Octane/Type/Word32.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/Word32.hs
@@ -0,0 +1,86 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+module Octane.Type.Word32 (Word32(..), fromWord32, toWord32) where
+
+import Data.Function ((&))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Aeson as Aeson
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits as BinaryBit
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.Word as Word
+import qualified GHC.Generics as Generics
+import qualified Octane.Utility.Endian as Endian
+import qualified Text.Printf as Printf
+
+
+-- | A 32-bit unsigned integer.
+newtype Word32 = Word32
+    { unpack :: Word.Word32
+    } deriving (Eq, Generics.Generic, Num, Ord)
+
+-- | Stored in little-endian byte order.
+instance Binary.Binary Word32 where
+    get = do
+        value <- Binary.getWord32le
+        pure (Word32 value)
+
+    put word32 = do
+        let value = unpack word32
+        Binary.putWord32le value
+
+instance BinaryBit.BinaryBit Word32 where
+    getBits _ = do
+        bytes <- BinaryBit.getByteString 4
+        bytes
+            & LazyBytes.fromStrict
+            & Endian.reverseBitsInBytes
+            & Binary.runGet Binary.get
+            & pure
+
+    putBits _ word32 = word32
+        & Binary.put
+        & Binary.runPut
+        & Endian.reverseBitsInBytes
+        & LazyBytes.toStrict
+        & BinaryBit.putByteString
+
+instance DeepSeq.NFData Word32 where
+
+-- | Shown as @0x01020304@.
+instance Show Word32 where
+    show word32 = Printf.printf "0x%08x" (unpack word32)
+
+instance Aeson.ToJSON Word32 where
+    toJSON word32 = word32
+        & unpack
+        & Aeson.toJSON
+
+
+-- | Converts a 'Word32' into any 'Integral' value.
+--
+-- >>> fromWord32 0x00000001 :: Word.Word32
+-- 1
+--
+-- >>> fromWord32 0xffffffff :: Data.Int.Int32
+-- -1
+fromWord32 :: (Integral a) => Word32 -> a
+fromWord32 word32 = fromIntegral (unpack word32)
+
+
+-- | Converts any 'Integral' value into a 'Word32'.
+--
+-- >>> toWord32 (1 :: Word.Word32)
+-- 0x00000001
+--
+-- >>> toWord32 (-1 :: Data.Int.Int32)
+-- 0xffffffff
+toWord32 :: (Integral a) => a -> Word32
+toWord32 value = Word32 (fromIntegral value)
diff --git a/library/Octane/Type/Word64.hs b/library/Octane/Type/Word64.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/Word64.hs
@@ -0,0 +1,86 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+module Octane.Type.Word64 (Word64(..), fromWord64, toWord64) where
+
+import Data.Function ((&))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Aeson as Aeson
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits as BinaryBit
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.Word as Word
+import qualified GHC.Generics as Generics
+import qualified Octane.Utility.Endian as Endian
+import qualified Text.Printf as Printf
+
+
+-- | A 64-bit unsigned integer.
+newtype Word64 = Word64
+    { unpack :: Word.Word64
+    } deriving (Eq, Generics.Generic, Num, Ord)
+
+-- | Stored in little-endian byte order.
+instance Binary.Binary Word64 where
+    get = do
+        value <- Binary.getWord64le
+        pure (Word64 value)
+
+    put word64 = do
+        let value = unpack word64
+        Binary.putWord64le value
+
+instance BinaryBit.BinaryBit Word64 where
+    getBits _ = do
+        bytes <- BinaryBit.getByteString 8
+        bytes
+            & LazyBytes.fromStrict
+            & Endian.reverseBitsInBytes
+            & Binary.runGet Binary.get
+            & pure
+
+    putBits _ word64 = word64
+        & Binary.put
+        & Binary.runPut
+        & Endian.reverseBitsInBytes
+        & LazyBytes.toStrict
+        & BinaryBit.putByteString
+
+instance DeepSeq.NFData Word64 where
+
+-- | Shown as @0x01020304@.
+instance Show Word64 where
+    show word64 = Printf.printf "0x%016x" (unpack word64)
+
+instance Aeson.ToJSON Word64 where
+    toJSON word64 = word64
+        & unpack
+        & Aeson.toJSON
+
+
+-- | Converts a 'Word64' into any 'Integral' value.
+--
+-- >>> fromWord64 0x0000000000000001 :: Word.Word64
+-- 1
+--
+-- >>> fromWord64 0xffffffffffffffff :: Data.Int.Int64
+-- -1
+fromWord64 :: (Integral a) => Word64 -> a
+fromWord64 word64 = fromIntegral (unpack word64)
+
+
+-- | Converts any 'Integral' value into a 'Word64'.
+--
+-- >>> toWord64 (1 :: Word.Word64)
+-- 0x0000000000000001
+--
+-- >>> toWord64 (-1 :: Data.Int.Int64)
+-- 0xffffffffffffffff
+toWord64 :: (Integral a) => a -> Word64
+toWord64 value = Word64 (fromIntegral value)
diff --git a/library/Octane/Type/Word8.hs b/library/Octane/Type/Word8.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Type/Word8.hs
@@ -0,0 +1,85 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+module Octane.Type.Word8 (Word8(..), fromWord8, toWord8) where
+
+import Data.Function ((&))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Aeson as Aeson
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits as BinaryBit
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.Word as Word
+import qualified GHC.Generics as Generics
+import qualified Octane.Utility.Endian as Endian
+import qualified Text.Printf as Printf
+
+
+-- | A 8-bit unsigned integer.
+newtype Word8 = Word8
+    { unpack :: Word.Word8
+    } deriving (Eq, Generics.Generic, Num, Ord)
+
+instance Binary.Binary Word8 where
+    get = do
+        value <- Binary.getWord8
+        pure (Word8 value)
+
+    put word8 = do
+        let value = unpack word8
+        Binary.putWord8 value
+
+instance BinaryBit.BinaryBit Word8 where
+    getBits _ = do
+        bytes <- BinaryBit.getByteString 1
+        bytes
+            & LazyBytes.fromStrict
+            & Endian.reverseBitsInBytes
+            & Binary.runGet Binary.get
+            & pure
+
+    putBits _ word8 = word8
+        & Binary.put
+        & Binary.runPut
+        & Endian.reverseBitsInBytes
+        & LazyBytes.toStrict
+        & BinaryBit.putByteString
+
+instance DeepSeq.NFData Word8 where
+
+-- | Shown as @0x01@.
+instance Show Word8 where
+    show word8 = Printf.printf "0x%02x" (unpack word8)
+
+instance Aeson.ToJSON Word8 where
+    toJSON word8 = word8
+        & unpack
+        & Aeson.toJSON
+
+
+-- | Converts a 'Word8' into any 'Integral' value.
+--
+-- >>> fromWord8 0x01 :: Word.Word8
+-- 1
+--
+-- >>> fromWord8 0xff :: Data.Int.Int8
+-- -1
+fromWord8 :: (Integral a) => Word8 -> a
+fromWord8 word8 = fromIntegral (unpack word8)
+
+
+-- | Converts any 'Integral' value into a 'Word8'.
+--
+-- >>> toWord8 (1 :: Word.Word8)
+-- 0x01
+--
+-- >>> toWord8 (-1 :: Data.Int.Int8)
+-- 0xff
+toWord8 :: (Integral a) => a -> Word8
+toWord8 value = Word8 (fromIntegral value)
diff --git a/library/Octane/Utility.hs b/library/Octane/Utility.hs
--- a/library/Octane/Utility.hs
+++ b/library/Octane/Utility.hs
@@ -1,116 +1,11 @@
-{-# LANGUAGE BinaryLiterals #-}
-
-module Octane.Utility where
-
-import qualified Data.Bits as Bits
-import qualified Data.ByteString as StrictBytes
-import qualified Data.ByteString.Lazy as LazyBytes
-import qualified Data.Vector.Unboxed as Vector
-import qualified Data.Word as Word
-
-
--- | Computes the CRC32 of some bytes. Note that this is a non-standard CRC32.
--- It probably only works for Rocket League.
-crc32 :: LazyBytes.ByteString -> Word.Word32
-crc32 bytes = Bits.xor 0xffffffff (LazyBytes.foldl
-    (\ crc byte -> Bits.xor
-        (crc32Table Vector.! fromIntegral
-            (Bits.xor byte (fromIntegral (Bits.shiftR crc 24))))
-        (Bits.shiftL crc 8))
-    0x10340dfe
-    bytes)
-
-
--- | The lookup table of values for 'crc32'. Computed from the polynomial
--- @0x04c11db7@.
-crc32Table :: Vector.Vector Word.Word32
-crc32Table = Vector.fromList
-    [
-        0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9,
-        0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005,
-        0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61,
-        0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd,
-        0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9,
-        0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75,
-        0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011,
-        0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd,
-        0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039,
-        0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5,
-        0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81,
-        0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d,
-        0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49,
-        0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95,
-        0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1,
-        0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d,
-        0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae,
-        0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072,
-        0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16,
-        0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca,
-        0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde,
-        0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02,
-        0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066,
-        0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba,
-        0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e,
-        0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692,
-        0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6,
-        0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a,
-        0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e,
-        0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2,
-        0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686,
-        0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a,
-        0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637,
-        0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb,
-        0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f,
-        0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53,
-        0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47,
-        0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b,
-        0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff,
-        0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623,
-        0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7,
-        0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b,
-        0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f,
-        0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3,
-        0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7,
-        0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b,
-        0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f,
-        0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3,
-        0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640,
-        0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c,
-        0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8,
-        0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24,
-        0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30,
-        0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec,
-        0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088,
-        0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654,
-        0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0,
-        0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c,
-        0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18,
-        0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4,
-        0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0,
-        0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c,
-        0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668,
-        0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4
-    ]
-
-
--- | Reverses all the bits in each lazy byte.
-reverseBitsInBytes :: LazyBytes.ByteString -> LazyBytes.ByteString
-reverseBitsInBytes bytes = LazyBytes.map reverseBits bytes
-
-
--- | Reverses all the bits in each strict byte.
-reverseBitsInBytes' :: StrictBytes.ByteString -> StrictBytes.ByteString
-reverseBitsInBytes' bytes = StrictBytes.map reverseBits bytes
-
+module Octane.Utility
+    ( module Octane.Utility.ClassPropertyMap
+    , module Octane.Utility.CRC
+    , module Octane.Utility.Endian
+    , module Octane.Utility.Optimizer
+    ) where
 
--- | Reverses the bits in a byte.
-reverseBits :: Word.Word8 -> Word.Word8
-reverseBits byte
-    = Bits.shiftR (byte Bits..&. 0b10000000) 7
-    + Bits.shiftR (byte Bits..&. 0b01000000) 5
-    + Bits.shiftR (byte Bits..&. 0b00100000) 3
-    + Bits.shiftR (byte Bits..&. 0b00010000) 1
-    + Bits.shiftL (byte Bits..&. 0b00001000) 1
-    + Bits.shiftL (byte Bits..&. 0b00000100) 3
-    + Bits.shiftL (byte Bits..&. 0b00000010) 5
-    + Bits.shiftL (byte Bits..&. 0b00000001) 7
+import Octane.Utility.ClassPropertyMap
+import Octane.Utility.CRC
+import Octane.Utility.Endian
+import Octane.Utility.Optimizer
diff --git a/library/Octane/Utility/CRC.hs b/library/Octane/Utility/CRC.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Utility/CRC.hs
@@ -0,0 +1,106 @@
+module Octane.Utility.CRC (crc32) where
+
+import qualified Data.Bits as Bits
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.Vector.Unboxed as Vector
+import qualified Data.Word as Word
+
+
+-- | Computes the CRC32 of some bytes. Note that this is a non-standard CRC32.
+-- It probably only works for Rocket League.
+--
+-- >>> crc32 LazyBytes.empty -- ""
+-- 4023120385
+--
+-- >>> crc32 (LazyBytes.pack [49 .. 57]) -- "123456789"
+-- 3690624627
+crc32 :: LazyBytes.ByteString -> Word.Word32
+crc32 bytes = do
+    let update = crc32Update crc32Table
+    let initial = Bits.complement crc32Initial
+    let crc = LazyBytes.foldl update initial bytes
+    Bits.complement crc
+
+
+crc32Update :: Vector.Vector Word.Word32 -> Word.Word32 -> Word.Word8 -> Word.Word32
+crc32Update table crc byte = do
+    let toWord8 = fromIntegral :: (Integral a) => a -> Word.Word8
+    let toInt = fromIntegral :: (Integral a) => a -> Int
+    let index = toInt (Bits.xor byte (toWord8 (Bits.shiftR crc 24)))
+    let left = Vector.unsafeIndex table index
+    let right = Bits.shiftL crc 8
+    Bits.xor left right
+
+
+crc32Initial :: Word.Word32
+crc32Initial = 0xefcbf201
+
+
+crc32Table :: Vector.Vector Word.Word32
+crc32Table = Vector.fromList
+    [
+        0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9,
+        0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005,
+        0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61,
+        0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd,
+        0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9,
+        0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75,
+        0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011,
+        0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd,
+        0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039,
+        0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5,
+        0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81,
+        0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d,
+        0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49,
+        0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95,
+        0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1,
+        0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d,
+        0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae,
+        0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072,
+        0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16,
+        0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca,
+        0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde,
+        0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02,
+        0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066,
+        0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba,
+        0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e,
+        0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692,
+        0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6,
+        0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a,
+        0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e,
+        0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2,
+        0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686,
+        0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a,
+        0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637,
+        0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb,
+        0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f,
+        0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53,
+        0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47,
+        0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b,
+        0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff,
+        0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623,
+        0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7,
+        0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b,
+        0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f,
+        0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3,
+        0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7,
+        0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b,
+        0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f,
+        0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3,
+        0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640,
+        0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c,
+        0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8,
+        0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24,
+        0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30,
+        0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec,
+        0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088,
+        0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654,
+        0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0,
+        0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c,
+        0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18,
+        0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4,
+        0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0,
+        0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c,
+        0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668,
+        0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4
+    ]
diff --git a/library/Octane/Utility/ClassPropertyMap.hs b/library/Octane/Utility/ClassPropertyMap.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Utility/ClassPropertyMap.hs
@@ -0,0 +1,174 @@
+-- | This module is responsible for building the class property map, which maps
+-- class IDs to a map of property IDs to property names. This map is the
+-- cornerstone of the replay stream parser.
+module Octane.Utility.ClassPropertyMap where
+
+import Data.Function ((&))
+
+import qualified Data.Char as Char
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.List as List
+import qualified Data.Map.Strict as Map
+import qualified Data.Maybe as Maybe
+import qualified Data.Text as StrictText
+import qualified Octane.Type.CacheItem as CacheItem
+import qualified Octane.Type.CacheProperty as CacheProperty
+import qualified Octane.Type.ClassItem as ClassItem
+import qualified Octane.Type.List as List
+import qualified Octane.Type.ReplayWithoutFrames as ReplayWithoutFrames
+import qualified Octane.Type.Text as Text
+import qualified Octane.Type.Word32 as Word32
+
+
+-- | The class property map is a map from class IDs in the stream to a map from
+-- property IDs in the stream to property names.
+getClassPropertyMap :: ReplayWithoutFrames.ReplayWithoutFrames -> IntMap.IntMap (IntMap.IntMap StrictText.Text)
+getClassPropertyMap replay = let
+    basicClassPropertyMap = getBasicClassPropertyMap replay
+    classMap = getClassMap replay
+    in replay
+        & getClassIds
+        & map (\ classId -> let
+            ownProperties = case IntMap.lookup classId basicClassPropertyMap of
+                Nothing -> IntMap.empty
+                Just x -> x
+            parentProperties = case IntMap.lookup classId classMap of
+                Nothing -> IntMap.empty
+                Just parentClassIds -> parentClassIds
+                    & map (\ parentClassId ->
+                        case IntMap.lookup parentClassId basicClassPropertyMap of
+                            Nothing -> IntMap.empty
+                            Just x -> x)
+                    & IntMap.unions
+            properties = IntMap.union ownProperties parentProperties
+            in (classId, properties))
+        & IntMap.fromList
+
+-- | The class cache is a list of 3-tuples where the first element is a class
+-- ID, the second is its cache ID, and the third is its parent's cache ID.
+getClassCache :: ReplayWithoutFrames.ReplayWithoutFrames -> [(Int, Int, Int)]
+getClassCache replay = replay
+    & ReplayWithoutFrames.cache
+    & List.unpack
+    & map (\ x ->
+        ( x & CacheItem.classId & Word32.fromWord32
+        , x & CacheItem.cacheId & Word32.fromWord32
+        , x & CacheItem.parentCacheId & Word32.fromWord32
+        ))
+
+-- | The class IDs in a replay. Comes from the class cache.
+getClassIds :: ReplayWithoutFrames.ReplayWithoutFrames -> [Int]
+getClassIds replay = replay
+    & getClassCache
+    & map (\ (x, _, _) -> x)
+
+-- | Gets the parent class ID for the given parent cache ID. This is necessary
+-- because there is not always a class with the given cache ID in the cache.
+-- When that happens, the parent cache ID is decremented and tried again.
+getParentClassId :: Int -> [(Int, Int, Int)] -> Maybe Int
+getParentClassId parentCacheId xs =
+    case dropWhile (\ (_, cacheId, _) -> cacheId /= parentCacheId) xs of
+        [] -> if parentCacheId <= 0
+            then Nothing
+            else getParentClassId (parentCacheId - 1) xs
+        (parentClassId, _, _) : _ -> Just parentClassId
+
+-- | The basic class map is a naive mapping from class ID to its parent class
+-- ID. It's naive because it only maps the class ID to its immediate parent.
+-- It does not chase the inheritance all the way down.
+getBasicClassMap :: ReplayWithoutFrames.ReplayWithoutFrames -> IntMap.IntMap Int
+getBasicClassMap replay = replay
+    & getClassCache
+    & reverse
+    & List.tails
+    & Maybe.mapMaybe (\ xs -> case xs of
+        [] -> Nothing
+        (classId, _, parentCacheId) : ys -> do
+            parentClassId <- getParentClassId parentCacheId ys
+            return (classId, parentClassId))
+    & IntMap.fromList
+
+-- | Given a naive mapping from class ID to its parent class ID, return all of
+-- the parent IDs for a given class.
+getParentClassIds :: Int -> IntMap.IntMap Int -> [Int]
+getParentClassIds classId basicClassMap =
+    case IntMap.lookup classId basicClassMap of
+        Nothing -> []
+        Just parentClassId -> parentClassId : getParentClassIds parentClassId basicClassMap
+
+-- | The class map is a mapping from a class ID to all of its parent class IDs.
+getClassMap :: ReplayWithoutFrames.ReplayWithoutFrames -> IntMap.IntMap [Int]
+getClassMap replay = let
+    basicClassMap = getBasicClassMap replay
+    in replay
+        & getClassIds
+        & map (\ classId ->
+            ( classId
+            , getParentClassIds classId basicClassMap
+            ))
+        & IntMap.fromList
+
+-- | The property map is a mapping from property IDs to property names.
+getPropertyMap :: ReplayWithoutFrames.ReplayWithoutFrames -> IntMap.IntMap StrictText.Text
+getPropertyMap replay = replay
+    & ReplayWithoutFrames.objects
+    & List.unpack
+    & map Text.unpack
+    & zip [0 ..]
+    & IntMap.fromList
+
+-- | The basic class property map is a naive mapping from class IDs to a
+-- mapping from property IDs to property names. It's naive because it does
+-- not include the properties from the class's parents.
+getBasicClassPropertyMap :: ReplayWithoutFrames.ReplayWithoutFrames -> IntMap.IntMap (IntMap.IntMap StrictText.Text)
+getBasicClassPropertyMap replay = let
+    propertyMap = getPropertyMap replay
+    in replay
+        & ReplayWithoutFrames.cache
+        & List.unpack
+        & map (\ x -> let
+            classId = x & CacheItem.classId & Word32.fromWord32
+            properties = x
+                & CacheItem.properties
+                & List.unpack
+                & Maybe.mapMaybe (\ y -> let
+                    streamId = y & CacheProperty.streamId & Word32.fromWord32
+                    propertyId = y & CacheProperty.objectId & Word32.fromWord32
+                    in case IntMap.lookup propertyId propertyMap of
+                        Nothing -> Nothing
+                        Just name -> Just (streamId, name))
+                & IntMap.fromList
+            in (classId, properties))
+        & IntMap.fromList
+
+-- | The actor map is a mapping from class names to their IDs.
+getActorMap :: ReplayWithoutFrames.ReplayWithoutFrames -> Map.Map StrictText.Text Int
+getActorMap replay = replay
+    & ReplayWithoutFrames.classes
+    & List.unpack
+    & map (\ x -> let
+        className = x & ClassItem.name & Text.unpack
+        classId = x & ClassItem.streamId & Word32.fromWord32
+        in (className, classId))
+    & Map.fromList
+
+-- | Gets the class ID and name for a given property ID.
+getClass
+    :: IntMap.IntMap StrictText.Text -- ^ Property ID to property name
+    -> Map.Map StrictText.Text StrictText.Text -- ^ Property name to class name
+    -> Map.Map StrictText.Text Int -- ^ Class name to class ID
+    -> Int -- ^ property ID
+    -> Maybe (Int, StrictText.Text) -- ^ Maybe class ID and class name
+getClass propertyIdsToNames propertyNamesToClassNames classNamesToIds propertyId =
+    case IntMap.lookup propertyId propertyIdsToNames of
+        Nothing -> Nothing
+        Just rawPropertyName -> let
+            -- There are a large number of properties that end in numbers that
+            -- should all be treated the same. Instead of explicitly mapping
+            -- each of them, we can remove the numbers and treat them the same.
+            propertyName = rawPropertyName & StrictText.dropWhileEnd Char.isDigit
+            in case Map.lookup propertyName propertyNamesToClassNames of
+                Nothing -> Nothing
+                Just className -> case Map.lookup className classNamesToIds of
+                    Nothing -> Nothing
+                    Just classId -> Just (classId, className)
diff --git a/library/Octane/Utility/Endian.hs b/library/Octane/Utility/Endian.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Utility/Endian.hs
@@ -0,0 +1,31 @@
+{-# LANGUAGE BinaryLiterals #-}
+
+module Octane.Utility.Endian where
+
+import qualified Data.Bits as Bits
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.ByteString as StrictBytes
+import qualified Data.Word as Word
+
+
+-- | Reverses all the bits in each lazy byte.
+reverseBitsInBytes :: LazyBytes.ByteString -> LazyBytes.ByteString
+reverseBitsInBytes bytes = LazyBytes.map reverseBits bytes
+
+
+-- | Reverses all the bits in each strict byte.
+reverseBitsInBytes' :: StrictBytes.ByteString -> StrictBytes.ByteString
+reverseBitsInBytes' bytes = StrictBytes.map reverseBits bytes
+
+
+-- | Reverses the bits in a byte.
+reverseBits :: Word.Word8 -> Word.Word8
+reverseBits byte
+    = Bits.shiftR (byte Bits..&. 0b10000000) 7
+    + Bits.shiftR (byte Bits..&. 0b01000000) 5
+    + Bits.shiftR (byte Bits..&. 0b00100000) 3
+    + Bits.shiftR (byte Bits..&. 0b00010000) 1
+    + Bits.shiftL (byte Bits..&. 0b00001000) 1
+    + Bits.shiftL (byte Bits..&. 0b00000100) 3
+    + Bits.shiftL (byte Bits..&. 0b00000010) 5
+    + Bits.shiftL (byte Bits..&. 0b00000001) 7
diff --git a/library/Octane/Utility/Optimizer.hs b/library/Octane/Utility/Optimizer.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Utility/Optimizer.hs
@@ -0,0 +1,121 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Octane.Utility.Optimizer (optimizeFrames) where
+
+import Data.Function ((&))
+
+import qualified Data.Foldable as Foldable
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.Map.Strict as Map
+import qualified Data.Text as StrictText
+import qualified Octane.Type.Frame as Frame
+import qualified Octane.Type.Replication as Replication
+import qualified Octane.Type.State as State
+import qualified Octane.Type.Value as Value
+
+
+optimizeFrames :: [Frame.Frame] -> [Frame.Frame]
+optimizeFrames frames = frames
+    & Foldable.foldl'
+        (\ (state, fs) f -> let
+            newState = updateState f state
+            minimalFrame = getDelta state f
+            in (newState, minimalFrame : fs))
+        (initialState, [])
+    & snd
+    & reverse
+
+
+-- { actor id => (alive?, { property name => property value } ) }
+type State = IntMap.IntMap (Bool, Map.Map StrictText.Text Value.Value)
+
+
+initialState :: State
+initialState = IntMap.empty
+
+
+updateState :: Frame.Frame -> State -> State
+updateState frame state1 = let
+    spawned = frame
+        & Frame.replications
+        & filter (\ replication -> replication
+            & Replication.state
+            & (== State.SOpening))
+        & map Replication.actorId
+        & map fromIntegral
+    state2 = spawned
+        & foldr
+            (IntMap.alter (\ maybeValue -> Just (case maybeValue of
+                Nothing -> (True, Map.empty)
+                Just (_, properties) -> (True, properties))))
+            state1
+
+    destroyed = frame
+        & Frame.replications
+        & filter (\ replication -> replication
+            & Replication.state
+            & (== State.SClosing))
+        & map Replication.actorId
+        & map fromIntegral
+    state3 = destroyed
+        & foldr
+            (IntMap.alter (\ maybeValue -> Just (case maybeValue of
+                Nothing -> (False, Map.empty)
+                Just (_, properties) -> (False, properties))))
+            state2
+
+    updated = frame
+        & Frame.replications
+        & filter (\ replication -> replication
+            & Replication.state
+            & (== State.SExisting))
+    state4 = updated
+        & foldr
+            (\ replication -> IntMap.alter
+                (\ maybeValue -> Just (case maybeValue of
+                    Nothing ->
+                        (True, Replication.properties replication)
+                    Just (alive, properties) ->
+                        ( alive
+                        , Map.union
+                            (Replication.properties replication)
+                            properties
+                        )))
+                (replication & Replication.actorId & fromIntegral))
+            state3
+
+    in state4
+
+
+getDelta :: State -> Frame.Frame -> Frame.Frame
+getDelta state frame = let
+    newReplications = frame
+        & Frame.replications
+        -- Remove replications that aren't actually new.
+        & reject (\ replication -> let
+            isOpening = Replication.state replication == State.SOpening
+            actorId = Replication.actorId replication
+            currentState = IntMap.lookup (fromIntegral actorId) state
+            isAlive = fmap fst currentState
+            wasAlreadyAlive = isAlive == Just True
+            in isOpening && wasAlreadyAlive)
+        -- Remove properties that haven't changed.
+        & map (\ replication ->
+            if Replication.state replication == State.SExisting
+            then let
+                actorId = Replication.actorId replication
+                currentState = IntMap.findWithDefault
+                    (True, Map.empty) (fromIntegral actorId) state
+                currentProperties = snd currentState
+                newProperties = Replication.properties replication
+                changes = newProperties
+                    & Map.filterWithKey (\ name newValue -> let
+                        oldValue = Map.lookup name currentProperties
+                        in Just newValue /= oldValue)
+                in replication { Replication.properties = changes }
+            else replication)
+    in frame { Frame.replications = newReplications }
+
+
+reject :: (a -> Bool) -> [a] -> [a]
+reject p xs = filter (\ x -> not (p x)) xs
diff --git a/library/Octane/Utility/Parser.hs b/library/Octane/Utility/Parser.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Utility/Parser.hs
@@ -0,0 +1,721 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE StrictData #-}
+
+module Octane.Utility.Parser where
+
+import Data.Function ((&))
+
+import qualified Control.DeepSeq as DeepSeq
+import qualified Data.Binary.Bits as BinaryBit
+import qualified Data.Binary.Bits.Get as Bits
+import qualified Data.Binary.Get as Binary
+import qualified Data.Bits as Bits
+import qualified Data.ByteString as StrictBytes
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Text as StrictText
+import qualified GHC.Generics as Generics
+import qualified Octane.Data as Data
+import qualified Octane.Type.Boolean as Boolean
+import qualified Octane.Type.Dictionary as Dictionary
+import qualified Octane.Type.Float32 as Float32
+import qualified Octane.Type.Frame as Frame
+import qualified Octane.Type.Initialization as Initialization
+import qualified Octane.Type.Int32 as Int32
+import qualified Octane.Type.Int8 as Int8
+import qualified Octane.Type.KeyFrame as KeyFrame
+import qualified Octane.Type.List as List
+import qualified Octane.Type.Property as Property
+import qualified Octane.Type.RemoteId as RemoteId
+import qualified Octane.Type.ReplayWithoutFrames as ReplayWithoutFrames
+import qualified Octane.Type.Replication as Replication
+import qualified Octane.Type.State as State
+import qualified Octane.Type.Stream as Stream
+import qualified Octane.Type.Text as Text
+import qualified Octane.Type.Value as Value
+import qualified Octane.Type.Vector as Vector
+import qualified Octane.Type.Word16 as Word16
+import qualified Octane.Type.Word32 as Word32
+import qualified Octane.Type.Word64 as Word64
+import qualified Octane.Type.Word8 as Word8
+import qualified Octane.Utility.ClassPropertyMap as CPM
+import qualified Octane.Utility.Endian as Endian
+import qualified Text.Printf as Printf
+
+
+parseFrames :: ReplayWithoutFrames.ReplayWithoutFrames -> [Frame.Frame]
+parseFrames replay = let
+    numFrames = replay
+        & ReplayWithoutFrames.properties
+        & Dictionary.unpack
+        & Map.lookup ("NumFrames" & StrictText.pack & Text.Text)
+        & (\ property -> case property of
+            Just (Property.IntProperty _ x) -> x & Int32.unpack & fromIntegral
+            _ -> 0)
+    get = replay & extractContext & getFrames 0 numFrames & Bits.runBitGet
+    stream = replay & ReplayWithoutFrames.stream & Stream.unpack
+    (_context, frames) = Binary.runGet get stream
+    in frames
+
+getFrames :: Word -> Int -> Context -> Bits.BitGet (Context, [Frame.Frame])
+getFrames number numFrames context = do
+    if fromIntegral number >= numFrames
+    then return (context, [])
+    else do
+        isEmpty <- Bits.isEmpty
+        if isEmpty
+        then return (context, [])
+        else do
+            maybeFrame <- getMaybeFrame context number
+            case maybeFrame of
+                Nothing -> return (context, [])
+                Just (newContext, frame) -> do
+                    (newerContext, frames) <- getFrames (number + 1) numFrames newContext
+                    return (newerContext, (frame : frames))
+
+getMaybeFrame :: Context -> Word -> Bits.BitGet (Maybe (Context, Frame.Frame))
+getMaybeFrame context number = do
+    time <- getFloat32
+    delta <- getFloat32
+    if time == 0 && delta == 0
+    then return Nothing
+    else if time < 0.001 || delta < 0.001
+    then fail ("parsing previous frame probably failed. time: " ++ show time ++ ", delta: " ++ show delta)
+    else do
+        (newContext, frame) <- getFrame context number time delta
+        return (Just (newContext, frame))
+
+getFrame :: Context -> Word -> Float32.Float32 -> Float32.Float32 -> Bits.BitGet (Context, Frame.Frame)
+getFrame context number time delta = do
+    (newContext, replications) <- getReplications context
+    let frame =
+            Frame.Frame
+            { Frame.number = number
+            , Frame.isKeyFrame = context & contextKeyFrames & Set.member number
+            , Frame.time = time
+            , Frame.delta = delta
+            , Frame.replications = replications
+            }
+    (newContext, frame) & DeepSeq.force & return
+
+getReplications :: Context -> Bits.BitGet (Context, [Replication.Replication])
+getReplications context = do
+    maybeReplication <- getMaybeReplication context
+    case maybeReplication of
+        Nothing -> return (context, [])
+        Just (newContext, replication) -> do
+            (newerContext, replications) <- getReplications newContext
+            return (newerContext, replication : replications)
+
+getMaybeReplication :: Context -> Bits.BitGet (Maybe (Context, Replication.Replication))
+getMaybeReplication context = do
+    hasReplication <- getBool
+    if Boolean.unpack hasReplication
+        then do
+            (newContext,replication) <- getReplication context
+            return (Just (newContext, replication))
+        else return Nothing
+
+getReplication :: Context -> Bits.BitGet (Context, Replication.Replication)
+getReplication context = do
+    actorId <- getActorId
+    isOpen <- getBool
+    let go =
+            if Boolean.unpack isOpen
+                then getOpenReplication
+                else getClosedReplication
+    go context actorId
+
+getOpenReplication :: Context
+                   -> Int
+                   -> Bits.BitGet (Context, Replication.Replication)
+getOpenReplication context actorId = do
+    isNew <- getBool
+    let go =
+            if Boolean.unpack isNew
+                then getNewReplication
+                else getExistingReplication
+    go context actorId
+
+getNewReplication :: Context
+                  -> Int
+                  -> Bits.BitGet (Context, Replication.Replication)
+getNewReplication context actorId = do
+    unknownFlag <- getBool
+    if Boolean.unpack unknownFlag
+        then fail "the unknown flag in a new replication is true! what does it mean?"
+        else pure ()
+    objectId <- getInt32
+    objectName <- case context & contextObjectMap & IntMap.lookup (Int32.fromInt32 objectId) of
+        Nothing -> fail ("could not find object name for id " ++ show objectId)
+        Just x -> pure x
+    (classId, className) <- case CPM.getClass (contextObjectMap context) Data.objectToClass (contextClassMap context) (Int32.fromInt32 objectId) of
+        Nothing -> fail ("could not find class for object id " ++ show objectId)
+        Just x -> pure x
+    classInit <- getInitialization className
+    let thing = Thing
+            { thingFlag = unknownFlag
+            , thingObjectId = objectId
+            , thingObjectName = objectName
+            , thingClassId = classId
+            , thingClassName = className
+            , thingInitialization = classInit
+            }
+    let things = contextThings context
+    let newThings = IntMap.insert actorId thing things
+    let newContext = context { contextThings = newThings }
+    return
+        ( newContext
+        , Replication.Replication
+          { Replication.actorId = fromIntegral actorId
+          , Replication.objectName = objectName
+          , Replication.className = className
+          , Replication.state = State.SOpening
+          , Replication.initialization = Just classInit
+          , Replication.properties = Map.empty
+          })
+
+getExistingReplication :: Context
+                       -> Int
+                       -> Bits.BitGet (Context, Replication.Replication)
+getExistingReplication context actorId = do
+    thing <- case context & contextThings & IntMap.lookup actorId of
+        Nothing -> fail ("could not find thing for actor id " ++ show actorId)
+        Just x -> pure x
+    props <- getProps context thing
+    return (context, Replication.Replication
+        { Replication.actorId = fromIntegral actorId
+        , Replication.objectName = thingObjectName thing
+        , Replication.className = thingClassName thing
+        , Replication.state = State.SExisting
+        , Replication.initialization = Nothing
+        , Replication.properties = props
+        })
+
+getClosedReplication :: Context
+                     -> Int
+                     -> Bits.BitGet (Context, Replication.Replication)
+getClosedReplication context actorId = do
+    thing <- case context & contextThings & IntMap.lookup actorId of
+        Nothing -> fail ("could not find thing for actor id " ++ show actorId)
+        Just x -> pure x
+    let newThings = context & contextThings & IntMap.delete actorId
+    let newContext = context { contextThings = newThings }
+    return
+        ( newContext
+        , Replication.Replication
+          { Replication.actorId = fromIntegral actorId
+          , Replication.objectName = thingObjectName thing
+          , Replication.className = thingClassName thing
+          , Replication.state = State.SClosing
+          , Replication.initialization = Nothing
+          , Replication.properties = Map.empty
+          })
+
+getProps :: Context -> Thing -> Bits.BitGet (Map.Map StrictText.Text Value.Value)
+getProps context thing = do
+    maybeProp <- getMaybeProp context thing
+    case maybeProp of
+        Nothing -> return Map.empty
+        Just (k, v) -> do
+            let m = Map.singleton k v
+            props <- getProps context thing
+            return (Map.union m props)
+
+getMaybeProp :: Context -> Thing -> Bits.BitGet (Maybe (StrictText.Text, Value.Value))
+getMaybeProp context thing = do
+    hasProp <- getBool
+    if Boolean.unpack hasProp
+    then do
+        prop <- getProp context thing
+        return (Just prop)
+    else return Nothing
+
+getProp :: Context -> Thing -> Bits.BitGet (StrictText.Text, Value.Value)
+getProp context thing = do
+    let classId = thing & thingClassId
+    props <- case context & contextClassPropertyMap & IntMap.lookup classId of
+        Nothing -> fail ("could not find property map for class id " ++ show classId)
+        Just x -> pure x
+    let maxId = props & IntMap.keys & (0 :) & maximum
+    pid <- getInt maxId
+    name <- case props & IntMap.lookup pid of
+        Nothing -> fail ("could not find property name for property id " ++ show pid)
+        Just x -> pure x
+    value <- getPropValue name
+    return (name, value)
+
+--
+
+getPropValue :: StrictText.Text -> Bits.BitGet Value.Value
+getPropValue name = case Map.lookup name propertyNameToGet of
+    Nothing -> fail ("don't know how to read property " ++ show name)
+    Just get -> get
+
+propertyNameToGet :: Map.Map StrictText.Text (Bits.BitGet Value.Value)
+propertyNameToGet =
+    [ (Data.booleanProperties, getBooleanProperty)
+    , (Data.byteProperties, getByteProperty)
+    , (Data.camSettingsProperties, getCamSettingsProperty)
+    , (Data.demolishProperties, getDemolishProperty)
+    , (Data.enumProperties, getEnumProperty)
+    , (Data.explosionProperties, getExplosionProperty)
+    , (Data.flaggedIntProperties, getFlaggedIntProperty)
+    , (Data.floatProperties, getFloatProperty)
+    , (Data.gameModeProperties, getGameModeProperty)
+    , (Data.intProperties, getIntProperty)
+    , (Data.loadoutOnlineProperties, getLoadoutOnlineProperty)
+    , (Data.loadoutProperties, getLoadoutProperty)
+    , (Data.locationProperties, getLocationProperty)
+    , (Data.musicStingerProperties, getMusicStingerProperty)
+    , (Data.pickupProperties, getPickupProperty)
+    , (Data.privateMatchSettingsProperties, getPrivateMatchSettingsProperty)
+    , (Data.qWordProperties, getQWordProperty)
+    , (Data.relativeRotationProperties, getRelativeRotationProperty)
+    , (Data.reservationProperties, getReservationProperty)
+    , (Data.rigidBodyStateProperties, getRigidBodyStateProperty)
+    , (Data.stringProperties, getStringProperty)
+    , (Data.teamPaintProperties, getTeamPaintProperty)
+    , (Data.uniqueIdProperties, getUniqueIdProperty)
+    , (Set.fromList [StrictText.pack "TAGame.PRI_TA:PartyLeader"], getPartyLeaderProperty)
+    ]
+        & concatMap (\ (ks, v) -> ks & Set.toList & map (\ k -> (k, v)))
+        & Map.fromList
+
+getBooleanProperty :: Bits.BitGet Value.Value
+getBooleanProperty = do
+    bool <- getBool
+    return (Value.VBoolean bool)
+
+getByteProperty :: Bits.BitGet Value.Value
+getByteProperty = do
+    word <- getWord8
+    return (Value.VByte word)
+
+getCamSettingsProperty :: Bits.BitGet Value.Value
+getCamSettingsProperty = do
+    fov <- getFloat32
+    height <- getFloat32
+    angle <- getFloat32
+    distance <- getFloat32
+    stiffness <- getFloat32
+    swivelSpeed <- getFloat32
+    return (Value.VCamSettings fov height angle distance stiffness swivelSpeed)
+
+getDemolishProperty :: Bits.BitGet Value.Value
+getDemolishProperty = do
+    atkFlag <- getBool
+    atk <- getWord32
+    vicFlag <- getBool
+    vic <- getWord32
+    vec1 <- getVector
+    vec2 <- getVector
+    return (Value.VDemolish atkFlag atk vicFlag vic vec1 vec2)
+
+getEnumProperty :: Bits.BitGet Value.Value
+getEnumProperty = do
+    x <- Bits.getWord16be 10
+    y <- if x == 1023
+        then getBool
+        else fail ("unexpected enum value " ++ show x)
+    return (Value.VEnum (Word16.toWord16 x) y)
+
+getExplosionProperty :: Bits.BitGet Value.Value
+getExplosionProperty = do
+    noGoal <- getBool
+    a <- if Boolean.unpack noGoal
+        then return Nothing
+        else fmap Just getInt32
+    b <- getVector
+    return (Value.VExplosion noGoal a b)
+
+getFlaggedIntProperty :: Bits.BitGet Value.Value
+getFlaggedIntProperty = do
+    flag <- getBool
+    int <- getInt32
+    return (Value.VFlaggedInt flag int)
+
+getFloatProperty :: Bits.BitGet Value.Value
+getFloatProperty = do
+    float <- getFloat32
+    return (Value.VFloat float)
+
+getGameModeProperty :: Bits.BitGet Value.Value
+getGameModeProperty = do
+    x <- Bits.getWord8 2
+    return (Value.VGameMode (Word8.toWord8 x))
+
+getIntProperty :: Bits.BitGet Value.Value
+getIntProperty = do
+    int <- getInt32
+    return (Value.VInt int)
+
+getLoadoutOnlineProperty :: Bits.BitGet Value.Value
+getLoadoutOnlineProperty = do
+    version <- getWord32
+    x <- getWord32
+    y <- getWord32
+    z <- if version >= 12
+        then do
+            value <- getWord8
+            return (Just value)
+        else return Nothing
+    return (Value.VLoadoutOnline version x y z)
+
+getLoadoutProperty :: Bits.BitGet Value.Value
+getLoadoutProperty = do
+    version <- getWord8
+    body <- getWord32
+    decal <- getWord32
+    wheels <- getWord32
+    rocketTrail <- getWord32
+    antenna <- getWord32
+    topper <- getWord32
+    g <- getWord32
+    h <- if version > 10
+        then do
+            value <- getWord32
+            return (Just value)
+        else return Nothing
+    return (Value.VLoadout version body decal wheels rocketTrail antenna topper g h)
+
+getLocationProperty :: Bits.BitGet Value.Value
+getLocationProperty = do
+    vector <- getVector
+    return (Value.VLocation vector)
+
+getMusicStingerProperty :: Bits.BitGet Value.Value
+getMusicStingerProperty = do
+    flag <- getBool
+    cue <- getWord32
+    trigger <- getWord8
+    return (Value.VMusicStinger flag cue trigger)
+
+getPickupProperty :: Bits.BitGet Value.Value
+getPickupProperty = do
+    instigator <- getBool
+    instigatorId <- if Boolean.unpack instigator
+        then fmap Just getWord32
+        else return Nothing
+    pickedUp <- getBool
+    return (Value.VPickup instigator instigatorId pickedUp)
+
+getPrivateMatchSettingsProperty :: Bits.BitGet Value.Value
+getPrivateMatchSettingsProperty = do
+    mutators <- getText
+    joinableBy <- getWord32
+    maxPlayers <- getWord32
+    gameName <- getText
+    password <- getText
+    flag <- getBool
+    return (Value.VPrivateMatchSettings mutators joinableBy maxPlayers gameName password flag)
+
+getQWordProperty :: Bits.BitGet Value.Value
+getQWordProperty = do
+    qword <- getWord64
+    return (Value.VQWord qword)
+
+getRelativeRotationProperty :: Bits.BitGet Value.Value
+getRelativeRotationProperty = do
+    vector <- getFloatVector
+    return (Value.VRelativeRotation vector)
+
+getReservationProperty :: Bits.BitGet Value.Value
+getReservationProperty = do
+    -- I think this is the connection order. The first player to connect
+    -- gets number 0, and it goes up from there. The maximum is 7, which
+    -- would be a full 4x4 game.
+    number <- getInt7
+    (systemId, remoteId, localId) <- getUniqueId
+    playerName <- if systemId == 0 then return Nothing else do
+        string <- getText
+        return (Just string)
+    -- No idea what these two flags are. Might be for bots?
+    a <- getBool
+    b <- getBool
+    return (Value.VReservation number systemId remoteId localId playerName a b)
+
+getRigidBodyStateProperty :: Bits.BitGet Value.Value
+getRigidBodyStateProperty = do
+    flag <- getBool
+    position <- getVector
+    rotation <- getFloatVector
+    x <- if Boolean.unpack flag
+        then return Nothing
+        else fmap Just getVector
+    y <- if Boolean.unpack flag
+        then return Nothing
+        else fmap Just getVector
+    return (Value.VRigidBodyState flag position rotation x y)
+
+getStringProperty :: Bits.BitGet Value.Value
+getStringProperty = do
+    string <- getText
+    return (Value.VString string)
+
+getTeamPaintProperty :: Bits.BitGet Value.Value
+getTeamPaintProperty = do
+    team <- getWord8
+    primaryColor <- getWord8
+    accentColor <- getWord8
+    primaryFinish <- getWord32
+    accentFinish <- getWord32
+    return (Value.VTeamPaint team primaryColor accentColor primaryFinish accentFinish)
+
+getUniqueIdProperty :: Bits.BitGet Value.Value
+getUniqueIdProperty = do
+    (systemId, remoteId, localId) <- getUniqueId
+    return (Value.VUniqueId systemId remoteId localId)
+
+-- | Even though this is just a unique ID property, it must be handled
+-- specially because it sometimes doesn't have the remote or local IDs.
+getPartyLeaderProperty :: Bits.BitGet Value.Value
+getPartyLeaderProperty = do
+    systemId <- getSystemId
+    (remoteId, localId) <- if systemId == 0
+        then return (RemoteId.SplitscreenId Nothing, Nothing)
+        else do
+            remoteId <- getRemoteId systemId
+            localId <- getLocalId
+            return (remoteId, localId)
+    return (Value.VUniqueId systemId remoteId localId)
+
+--
+
+getFloat32 :: Bits.BitGet Float32.Float32
+getFloat32 = BinaryBit.getBits unimportant
+
+getText :: Bits.BitGet Text.Text
+getText = BinaryBit.getBits unimportant
+
+getUniqueId :: Bits.BitGet (Word8.Word8, RemoteId.RemoteId, Maybe Word8.Word8)
+getUniqueId = do
+    systemId <- getSystemId
+    remoteId <- getRemoteId systemId
+    localId <- getLocalId
+    return (systemId, remoteId, localId)
+
+getSystemId :: Bits.BitGet Word8.Word8
+getSystemId = getWord8
+
+getRemoteId :: Word8.Word8 -> Bits.BitGet RemoteId.RemoteId
+getRemoteId systemId = case systemId of
+    0 -> do
+        remoteId <- Bits.getByteString 3
+        if StrictBytes.all (\ byte -> byte == 0) remoteId
+            then 0 & Just & RemoteId.SplitscreenId & return
+            else fail ("unexpected splitscreen id " ++ show remoteId)
+    1 -> do
+        bytes <- Bits.getByteString 8
+        let remoteId = Binary.runGet
+                Binary.getWord64le
+                (bytes & LazyBytes.fromStrict & Endian.reverseBitsInBytes)
+        remoteId & Word64.toWord64 & RemoteId.SteamId & return
+    2 -> do
+        bytes <- Bits.getByteString 32
+        let remoteId = bytes
+                & LazyBytes.fromStrict
+                & Endian.reverseBitsInBytes
+                & LazyBytes.unpack
+                & concatMap (\ b -> Printf.printf "%02x" b)
+                & StrictText.pack
+                & Text.Text
+        remoteId & RemoteId.PlayStationId & return
+    4 -> do
+        bytes <- Bits.getByteString 8
+        let remoteId = Binary.runGet
+                Binary.getWord64le
+                (bytes & LazyBytes.fromStrict & Endian.reverseBitsInBytes)
+        remoteId & Word64.toWord64 & RemoteId.XboxId & return
+    _ -> fail ("unknown system id " ++ show systemId)
+
+getLocalId :: Bits.BitGet (Maybe Word8.Word8)
+getLocalId = fmap Just getWord8
+
+
+data Thing = Thing
+    { thingFlag :: Boolean.Boolean
+    , thingObjectId :: Int32.Int32
+    , thingObjectName :: StrictText.Text
+    , thingClassId :: Int
+    , thingClassName :: StrictText.Text
+    , thingInitialization :: Initialization.Initialization
+    } deriving (Eq, Generics.Generic, Show)
+
+instance DeepSeq.NFData Thing
+
+-- { class stream id => { property stream id => name } }
+type ClassPropertyMap = IntMap.IntMap (IntMap.IntMap StrictText.Text)
+
+-- { stream id => object name }
+type ObjectMap = IntMap.IntMap StrictText.Text
+
+-- { class name => class id }
+type ClassMap = Map.Map StrictText.Text Int
+
+data Context = Context
+    { contextObjectMap :: ObjectMap
+    , contextClassPropertyMap :: ClassPropertyMap
+    , contextThings :: (IntMap.IntMap Thing)
+    , contextClassMap :: ClassMap
+    , contextKeyFrames :: (Set.Set Word)
+    } deriving (Eq, Generics.Generic, Show)
+
+instance DeepSeq.NFData Context
+
+extractContext :: ReplayWithoutFrames.ReplayWithoutFrames -> Context
+extractContext replay =
+    Context
+    { contextObjectMap = CPM.getPropertyMap replay
+    , contextClassPropertyMap = CPM.getClassPropertyMap replay
+    , contextThings = IntMap.empty
+    , contextClassMap = CPM.getActorMap replay
+    , contextKeyFrames = replay
+        & ReplayWithoutFrames.keyFrames
+        & List.unpack
+        & map KeyFrame.frame
+        & map Word32.fromWord32
+        & Set.fromList
+    }
+
+getVector :: Bits.BitGet (Vector.Vector Int)
+getVector = do
+    numBits <- getNumVectorBits
+    let bias = Bits.shiftL 1 (numBits + 1)
+    let maxBits = numBits + 2
+    let maxValue = 2 ^ maxBits
+    dx <- getInt maxValue
+    dy <- getInt maxValue
+    dz <- getInt maxValue
+    return
+        Vector.Vector
+        { Vector.x = dx - bias
+        , Vector.y = dy - bias
+        , Vector.z = dz - bias
+        }
+
+getVectorBytewise
+    :: Bits.BitGet (Vector.Vector Int8.Int8)
+getVectorBytewise = do
+    hasX <- getBool
+    x <- if Boolean.unpack hasX then getInt8 else return 0
+    hasY <- getBool
+    y <- if Boolean.unpack hasY then getInt8 else return 0
+    hasZ <- getBool
+    z <- if Boolean.unpack hasZ then getInt8 else return 0
+    return
+        Vector.Vector
+        { Vector.x = x
+        , Vector.y = y
+        , Vector.z = z
+        }
+
+getFloatVector :: Bits.BitGet (Vector.Vector Float)
+getFloatVector = do
+    let maxValue = 1
+    let numBits = 16
+    x <- getFloat maxValue numBits
+    y <- getFloat maxValue numBits
+    z <- getFloat maxValue numBits
+    return Vector.Vector { Vector.x = x, Vector.y = y, Vector.z = z }
+
+getFloat :: Int -> Int -> Bits.BitGet Float
+getFloat maxValue numBits = do
+    let maxBitValue = (Bits.shiftL 1 (numBits - 1)) - 1
+    let bias = Bits.shiftL 1 (numBits - 1)
+    let serIntMax = Bits.shiftL 1 numBits
+    delta <- getInt serIntMax
+    let unscaledValue = delta - bias
+    if maxValue > maxBitValue
+    then do
+        let invScale = fromIntegral maxValue / fromIntegral maxBitValue
+        return (fromIntegral unscaledValue * invScale)
+    else do
+        let scale = fromIntegral maxBitValue / fromIntegral maxValue
+        let invScale = 1.0 / scale
+        return (fromIntegral unscaledValue * invScale)
+
+getInitialization :: StrictText.Text -> Bits.BitGet Initialization.Initialization
+getInitialization className = do
+    location <-
+        if Set.member className Data.locationClasses
+            then do
+                vector <- getVector
+                return (Just vector)
+            else return Nothing
+    rotation <-
+        if Set.member className Data.rotationClasses
+            then do
+                vector <- getVectorBytewise
+                return (Just vector)
+            else return Nothing
+    return
+        Initialization.Initialization
+        { Initialization.location = location
+        , Initialization.rotation = rotation
+        }
+
+bitSize
+    :: (Integral a)
+    => a -> a
+bitSize x = x & fromIntegral & logBase (2 :: Double) & ceiling
+
+-- Reads an integer bitwise. The bits of the integer are backwards, so the
+-- least significant bit is first. The argument is the maximum value this
+-- integer can have. Bits will be read until the next bit would be greater than
+-- the maximum value, or the number of bits necessary to reach the maximum
+-- value has been reached, whichever comes first.
+--
+-- For example, if the maximum value is 4 and "11" has been read already,
+-- nothing more will be read because another "1" would put the value over the
+-- maximum.
+getInt
+    :: Int -> Bits.BitGet Int
+getInt maxValue = do
+    let maxBits = bitSize maxValue
+        go i value = do
+            let x = Bits.shiftL 1 i
+            if i < maxBits && value + x <= maxValue
+                then do
+                    bit <- getBool
+                    let newValue =
+                            if Boolean.unpack bit
+                                then value + x
+                                else value
+                    go (i + 1) newValue
+                else return value
+    go 0 0
+
+getInt32 :: Bits.BitGet Int32.Int32
+getInt32 = BinaryBit.getBits unimportant
+
+getInt8 :: Bits.BitGet Int8.Int8
+getInt8 = BinaryBit.getBits unimportant
+
+getWord64 :: Bits.BitGet Word64.Word64
+getWord64 = BinaryBit.getBits unimportant
+
+getWord32 :: Bits.BitGet Word32.Word32
+getWord32 = BinaryBit.getBits unimportant
+
+getWord8 :: Bits.BitGet Word8.Word8
+getWord8 = BinaryBit.getBits unimportant
+
+getActorId :: Bits.BitGet Int
+getActorId = getInt 1024
+
+getNumVectorBits :: Bits.BitGet Int
+getNumVectorBits = getInt 19
+
+getInt7 :: Bits.BitGet Int
+getInt7 = getInt 7
+
+getBool :: Bits.BitGet Boolean.Boolean
+getBool = BinaryBit.getBits unimportant
+
+-- | The 'getBits' function from "Data.Binary.Bits" requires a size parameter.
+-- None of Octane's instances use it.
+unimportant :: Int
+unimportant = 0
diff --git a/library/Octane/Version.hs b/library/Octane/Version.hs
deleted file mode 100644
--- a/library/Octane/Version.hs
+++ /dev/null
@@ -1,8 +0,0 @@
-module Octane.Version (version) where
-
-import qualified Data.Version as Version
-import qualified Paths_octane as This
-
--- | The current version number.
-version :: Version.Version
-version = This.version
diff --git a/octane.cabal b/octane.cabal
--- a/octane.cabal
+++ b/octane.cabal
@@ -3,7 +3,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           octane
-version:        0.10.0
+version:        0.11.0
 synopsis:       Parse Rocket League replays.
 description:    Octane parses Rocket League replays.
 category:       Game
@@ -31,7 +31,6 @@
   ghc-options: -Wall
   build-depends:
       aeson ==0.11.*
-    , autoexporter ==0.2.*
     , base ==4.9.*
     , bimap ==0.3.*
     , binary ==0.8.*
@@ -40,48 +39,69 @@
     , containers ==0.5.*
     , data-binary-ieee754 ==0.4.*
     , deepseq ==1.4.*
-    , scientific ==0.3.*
+    , http-client ==0.4.*
+    , http-client-tls ==0.2.*
     , text ==1.2.*
+    , unordered-containers ==0.2.*
     , vector ==0.11.*
   exposed-modules:
       Octane
-      Octane.Analyzer
       Octane.Data
-      Octane.FullReplay
-      Octane.Json
+      Octane.Data.Antennas
+      Octane.Data.Bodies
+      Octane.Data.Bots
+      Octane.Data.Classes
+      Octane.Data.Decals
+      Octane.Data.Finishes
+      Octane.Data.GameModes
+      Octane.Data.Logos
+      Octane.Data.Properties
+      Octane.Data.RocketTrails
+      Octane.Data.Toppers
+      Octane.Data.Wheels
       Octane.Main
-      Octane.Parser
-      Octane.Parser.ClassPropertyMap
-      Octane.Parser.Garage
       Octane.Type
-      Octane.Type.Actor
+      Octane.Type.Boolean
       Octane.Type.CacheItem
       Octane.Type.CacheProperty
+      Octane.Type.ClassItem
+      Octane.Type.Dictionary
+      Octane.Type.Float32
+      Octane.Type.Frame
+      Octane.Type.Initialization
+      Octane.Type.Int32
+      Octane.Type.Int8
       Octane.Type.KeyFrame
+      Octane.Type.List
       Octane.Type.Mark
       Octane.Type.Message
-      Octane.Type.Primitive
-      Octane.Type.Primitive.Boolean
-      Octane.Type.Primitive.Dictionary
-      Octane.Type.Primitive.Float32
-      Octane.Type.Primitive.Int32
-      Octane.Type.Primitive.Int64
-      Octane.Type.Primitive.Int8
-      Octane.Type.Primitive.List
-      Octane.Type.Primitive.Stream
-      Octane.Type.Primitive.Text
-      Octane.Type.Primitive.Word32
-      Octane.Type.Primitive.Word8
+      Octane.Type.OptimizedReplay
       Octane.Type.Property
+      Octane.Type.RawReplay
+      Octane.Type.RemoteId
       Octane.Type.Replay
+      Octane.Type.ReplayWithFrames
+      Octane.Type.ReplayWithoutFrames
+      Octane.Type.Replication
+      Octane.Type.State
+      Octane.Type.Stream
+      Octane.Type.Text
+      Octane.Type.Value
+      Octane.Type.Vector
+      Octane.Type.Word16
+      Octane.Type.Word32
+      Octane.Type.Word64
+      Octane.Type.Word8
       Octane.Utility
-      Octane.Version
-  other-modules:
-      Paths_octane
+      Octane.Utility.ClassPropertyMap
+      Octane.Utility.CRC
+      Octane.Utility.Endian
+      Octane.Utility.Optimizer
+      Octane.Utility.Parser
   default-language: Haskell2010
 
 executable octane
-  main-is: Executable.hs
+  main-is: Main.hs
   hs-source-dirs:
       executable
   ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
@@ -90,76 +110,47 @@
     , octane
   default-language: Haskell2010
 
+test-suite octane-doctest
+  type: exitcode-stdio-1.0
+  main-is: DocTest.hs
+  hs-source-dirs:
+      test-suite
+  ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
+  build-depends:
+      base
+    , octane
+    , doctest ==0.11.*
+  other-modules:
+      Main
+      OctaneSpec
+  default-language: Haskell2010
+
 test-suite octane-test-suite
   type: exitcode-stdio-1.0
-  main-is: TestSuite.hs
+  main-is: Main.hs
   hs-source-dirs:
       test-suite
   ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
   build-depends:
       base
-    , binary
-    , bytestring
-    , containers
     , octane
     , tasty ==0.11.*
     , tasty-hspec ==1.1.*
   other-modules:
-      Octane.MainSpec
-      Octane.Type.ActorSpec
-      Octane.Type.CacheItemSpec
-      Octane.Type.CachePropertySpec
-      Octane.Type.KeyFrameSpec
-      Octane.Type.MarkSpec
-      Octane.Type.MessageSpec
-      Octane.Type.Primitive.BooleanSpec
-      Octane.Type.Primitive.DictionarySpec
-      Octane.Type.Primitive.Float32Spec
-      Octane.Type.Primitive.Int32Spec
-      Octane.Type.Primitive.Int64Spec
-      Octane.Type.Primitive.ListSpec
-      Octane.Type.Primitive.StreamSpec
-      Octane.Type.Primitive.TextSpec
-      Octane.Type.PrimitiveSpec
-      Octane.Type.PropertySpec
-      Octane.Type.ReplaySpec
-      Octane.TypeSpec
-      Octane.VersionSpec
+      DocTest
       OctaneSpec
   default-language: Haskell2010
 
 benchmark octane-benchmark
   type: exitcode-stdio-1.0
-  main-is: Benchmark.hs
+  main-is: Main.hs
   hs-source-dirs:
       benchmark
   ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
   build-depends:
       base
-    , binary
-    , bytestring
-    , containers
     , criterion ==1.1.*
     , octane
   other-modules:
-      Octane.MainBench
-      Octane.Type.ActorBench
-      Octane.Type.CacheItemBench
-      Octane.Type.CachePropertyBench
-      Octane.Type.KeyFrameBench
-      Octane.Type.MarkBench
-      Octane.Type.MessageBench
-      Octane.Type.Primitive.BooleanBench
-      Octane.Type.Primitive.DictionaryBench
-      Octane.Type.Primitive.Float32Bench
-      Octane.Type.Primitive.Int32Bench
-      Octane.Type.Primitive.Int64Bench
-      Octane.Type.Primitive.ListBench
-      Octane.Type.Primitive.StreamBench
-      Octane.Type.Primitive.TextBench
-      Octane.Type.PrimitiveBench
-      Octane.Type.PropertyBench
-      Octane.Type.ReplayBench
-      Octane.TypeBench
       OctaneBench
   default-language: Haskell2010
diff --git a/package.yaml b/package.yaml
--- a/package.yaml
+++ b/package.yaml
@@ -2,16 +2,13 @@
   octane-benchmark:
     dependencies:
     - base
-    - binary
-    - bytestring
-    - containers
     - criterion ==1.1.*
     - octane
     ghc-options:
     - -rtsopts
     - -threaded
     - -with-rtsopts=-N
-    main: Benchmark.hs
+    main: Main.hs
     source-dirs: benchmark
 category: Game
 description: Octane parses Rocket League replays.
@@ -24,7 +21,7 @@
     - -rtsopts
     - -threaded
     - -with-rtsopts=-N
-    main: Executable.hs
+    main: Main.hs
     source-dirs: executable
 extra-source-files:
 - CHANGELOG.markdown
@@ -36,7 +33,6 @@
 library:
   dependencies:
   - aeson ==0.11.*
-  - autoexporter ==0.2.*
   - base ==4.9.*
   - bimap ==0.3.*
   - binary ==0.8.*
@@ -45,10 +41,11 @@
   - containers ==0.5.*
   - data-binary-ieee754 ==0.4.*
   - deepseq ==1.4.*
-  - scientific ==0.3.*
+  - http-client ==0.4.*
+  - http-client-tls ==0.2.*
   - text ==1.2.*
+  - unordered-containers ==0.2.*
   - vector ==0.11.*
-  other-modules: Paths_octane
   source-dirs: library
 license: MIT
 license-file: LICENSE.markdown
@@ -56,12 +53,20 @@
 name: octane
 synopsis: Parse Rocket League replays.
 tests:
+  octane-doctest:
+    dependencies:
+    - base
+    - octane
+    - doctest ==0.11.*
+    ghc-options:
+    - -rtsopts
+    - -threaded
+    - -with-rtsopts=-N
+    main: DocTest.hs
+    source-dirs: test-suite
   octane-test-suite:
     dependencies:
     - base
-    - binary
-    - bytestring
-    - containers
     - octane
     - tasty ==0.11.*
     - tasty-hspec ==1.1.*
@@ -69,6 +74,6 @@
     - -rtsopts
     - -threaded
     - -with-rtsopts=-N
-    main: TestSuite.hs
+    main: Main.hs
     source-dirs: test-suite
-version: '0.10.0'
+version: '0.11.0'
diff --git a/test-suite/DocTest.hs b/test-suite/DocTest.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/DocTest.hs
@@ -0,0 +1,7 @@
+module Main (main) where
+
+import qualified Test.DocTest as DocTest
+
+
+main :: IO ()
+main = DocTest.doctest ["library"]
diff --git a/test-suite/Main.hs b/test-suite/Main.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Main.hs
@@ -0,0 +1,14 @@
+module Main (main) where
+
+import qualified OctaneSpec
+import qualified Test.Tasty as Tasty
+import qualified Test.Tasty.Hspec as Hspec
+
+main :: IO ()
+main = do
+    test <- Hspec.testSpec "octane" spec
+    Tasty.defaultMain test
+
+
+spec :: Hspec.Spec
+spec = Hspec.parallel OctaneSpec.spec
diff --git a/test-suite/Octane/MainSpec.hs b/test-suite/Octane/MainSpec.hs
deleted file mode 100644
--- a/test-suite/Octane/MainSpec.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Octane.MainSpec (spec) where
-
-import Test.Tasty.Hspec
-
-spec :: Spec
-spec = describe "Main" $ do
-    return ()
diff --git a/test-suite/Octane/Type/ActorSpec.hs b/test-suite/Octane/Type/ActorSpec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/ActorSpec.hs
+++ /dev/null
@@ -1,46 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.ActorSpec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-spec :: Spec
-spec = describe "Actor" $ do
-    it "can be decoded" $ do
-        shouldBe
-            (decodeActor "\
-                \\1\0\0\0\0\
-                \\0\0\0\0")
-            (Right ("", 9, Actor
-                (Text "")
-                (Int32 0)))
-        shouldBe
-            (decodeActor "\
-                \\2\0\0\0a\0\
-                \\2\0\0\0")
-            (Right ("", 10, Actor
-                (Text "a")
-                (Int32 2)))
-    it "can be encoded" $ do
-        shouldBe
-            (Binary.encode (Actor
-                (Text "")
-                (Int32 0)))
-            "\
-                \\1\0\0\0\0\
-                \\0\0\0\0"
-        shouldBe
-            (Binary.encode (Actor
-                (Text "a")
-                (Int32 2)))
-            "\
-                \\2\0\0\0a\0\
-                \\2\0\0\0"
-
-decodeActor :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Actor)
-decodeActor = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/CacheItemSpec.hs b/test-suite/Octane/Type/CacheItemSpec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/CacheItemSpec.hs
+++ /dev/null
@@ -1,64 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.CacheItemSpec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-spec :: Spec
-spec = describe "CacheItem" $ do
-    it "can be decoded" $ do
-        shouldBe
-            (decodeCacheItem "\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0")
-            (Right ("", 16, CacheItem
-                (Int32 0)
-                (Int32 0)
-                (Int32 0)
-                (List [])))
-        shouldBe
-            (decodeCacheItem "\
-                \\1\0\0\0\
-                \\2\0\0\0\
-                \\3\0\0\0\
-                \\1\0\0\0\
-                \\4\0\0\0\5\0\0\0")
-            (Right ("", 24, CacheItem
-                (Int32 1)
-                (Int32 2)
-                (Int32 3)
-                (List [CacheProperty (Int32 4) (Int32 5)])))
-    it "can be encoded" $ do
-        shouldBe
-            (Binary.encode (CacheItem
-                (Int32 0)
-                (Int32 0)
-                (Int32 0)
-                (List [])))
-            "\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0"
-        shouldBe
-            (Binary.encode (CacheItem
-                (Int32 1)
-                (Int32 2)
-                (Int32 3)
-                (List [CacheProperty (Int32 4) (Int32 5)])))
-            "\
-                \\1\0\0\0\
-                \\2\0\0\0\
-                \\3\0\0\0\
-                \\1\0\0\0\
-                \\4\0\0\0\5\0\0\0"
-
-decodeCacheItem :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, CacheItem)
-decodeCacheItem = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/CachePropertySpec.hs b/test-suite/Octane/Type/CachePropertySpec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/CachePropertySpec.hs
+++ /dev/null
@@ -1,46 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.CachePropertySpec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-spec :: Spec
-spec = describe "CacheProperty" $ do
-    it "can be decoded" $ do
-        shouldBe
-            (decodeCacheProperty "\
-                \\0\0\0\0\
-                \\0\0\0\0")
-            (Right ("", 8, CacheProperty
-                (Int32 0)
-                (Int32 0)))
-        shouldBe
-            (decodeCacheProperty "\
-                \\1\0\0\0\
-                \\2\0\0\0")
-            (Right ("", 8, CacheProperty
-                (Int32 1)
-                (Int32 2)))
-    it "can be encoded" $ do
-        shouldBe
-            (Binary.encode (CacheProperty
-                (Int32 0)
-                (Int32 0)))
-            "\
-                \\0\0\0\0\
-                \\0\0\0\0"
-        shouldBe
-            (Binary.encode (CacheProperty
-                (Int32 1)
-                (Int32 2)))
-            "\
-                \\1\0\0\0\
-                \\2\0\0\0"
-
-decodeCacheProperty :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, CacheProperty)
-decodeCacheProperty = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/KeyFrameSpec.hs b/test-suite/Octane/Type/KeyFrameSpec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/KeyFrameSpec.hs
+++ /dev/null
@@ -1,54 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.KeyFrameSpec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-spec :: Spec
-spec = describe "KeyFrame" $ do
-    it "can be decoded" $ do
-        shouldBe
-            (decodeKeyFrame "\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0")
-            (Right ("", 12, KeyFrame
-                (Float32 0.0)
-                (Int32 0)
-                (Int32 0)))
-        shouldBe
-            (decodeKeyFrame "\
-                \\0\0\128\63\
-                \\2\0\0\0\
-                \\3\0\0\0")
-            (Right ("", 12, KeyFrame
-                (Float32 1.0)
-                (Int32 2)
-                (Int32 3)))
-    it "can be encoded" $ do
-        shouldBe
-            (Binary.encode (KeyFrame
-                (Float32 0.0)
-                (Int32 0)
-                (Int32 0)))
-            "\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0"
-        shouldBe
-            (Binary.encode (KeyFrame
-                (Float32 1.0)
-                (Int32 2)
-                (Int32 3)))
-            "\
-                \\0\0\128\63\
-                \\2\0\0\0\
-                \\3\0\0\0"
-
-decodeKeyFrame :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, KeyFrame)
-decodeKeyFrame = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/MarkSpec.hs b/test-suite/Octane/Type/MarkSpec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/MarkSpec.hs
+++ /dev/null
@@ -1,46 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.MarkSpec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-spec :: Spec
-spec = describe "Mark" $ do
-    it "can be decoded" $ do
-        shouldBe
-            (decodeMark "\
-                \\1\0\0\0\0\
-                \\0\0\0\0")
-            (Right ("", 9, Mark
-                (Text "")
-                (Int32 0)))
-        shouldBe
-            (decodeMark "\
-                \\2\0\0\0a\0\
-                \\1\0\0\0")
-            (Right ("", 10, Mark
-                (Text "a")
-                (Int32 1)))
-    it "can be encoded" $ do
-        shouldBe
-            (Binary.encode (Mark
-                (Text "")
-                (Int32 0)))
-            "\
-                \\1\0\0\0\0\
-                \\0\0\0\0"
-        shouldBe
-            (Binary.encode (Mark
-                (Text "a")
-                (Int32 1)))
-            "\
-                \\2\0\0\0a\0\
-                \\1\0\0\0"
-
-decodeMark :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Mark)
-decodeMark = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/MessageSpec.hs b/test-suite/Octane/Type/MessageSpec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/MessageSpec.hs
+++ /dev/null
@@ -1,48 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.MessageSpec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-spec :: Spec
-spec = describe "Message" $ do
-    it "can be decoded" $ do
-        shouldBe
-            (decodeMessage "\
-                \\0\0\0\0\
-                \\1\0\0\0\0\
-                \\1\0\0\0\0")
-            (Right ("", 14, Message
-                (Int32 0)
-                (Text "")
-                (Text "")))
-        shouldBe
-            (decodeMessage "\
-                \\1\0\0\0\
-                \\2\0\0\0a\0\
-                \\2\0\0\0b\0")
-            (Right ("", 16, Message
-                (Int32 1)
-                (Text "a")
-                (Text "b")))
-    it "can be encoded" $ do
-        shouldBe
-            (Binary.encode (Message (Int32 0) (Text "") (Text "")))
-            "\
-                \\0\0\0\0\
-                \\1\0\0\0\0\
-                \\1\0\0\0\0"
-        shouldBe
-            (Binary.encode (Message (Int32 1) (Text "a") (Text "b")))
-            "\
-                \\1\0\0\0\
-                \\2\0\0\0a\0\
-                \\2\0\0\0b\0"
-
-decodeMessage :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Message)
-decodeMessage = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/Primitive/BooleanSpec.hs b/test-suite/Octane/Type/Primitive/BooleanSpec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/Primitive/BooleanSpec.hs
+++ /dev/null
@@ -1,34 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.BooleanSpec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-spec :: Spec
-spec = describe "Boolean" $ do
-    it "can be decoded" $ do
-        shouldBe
-            (decodeBoolean "\0")
-            (Right ("", 1, Boolean False))
-        shouldBe
-            (decodeBoolean "\1")
-            (Right ("", 1, Boolean True))
-    it "can be encoded" $ do
-        shouldBe
-            (Binary.encode (Boolean False))
-            "\0"
-        shouldBe
-            (Binary.encode (Boolean True))
-            "\1"
-    it "does not raise a runtime error when decoding garbage" $ do
-        shouldBe
-            (decodeBoolean "garbage")
-            (Left ("arbage", 1, "Unexpected Boolean value 103"))
-
-decodeBoolean :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Boolean)
-decodeBoolean = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/Primitive/DictionarySpec.hs b/test-suite/Octane/Type/Primitive/DictionarySpec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/Primitive/DictionarySpec.hs
+++ /dev/null
@@ -1,37 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.DictionarySpec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-import qualified Data.Map as Map
-
-spec :: Spec
-spec = describe "Dictionary" $ do
-    it "can be decoded" $ do
-        shouldBe
-            (decodeDictionary "\5\0\0\0None\0")
-            (Right ("", 9, Dictionary Map.empty :: Dictionary Boolean))
-        shouldBe
-            (decodeDictionary "\6\0\0\0Hello\0\6\0\0\0World\0\5\0\0\0None\0")
-            (Right ("", 29, Dictionary (Map.singleton (Text "Hello") (Text "World"))))
-        shouldBe
-            (decodeDictionary "\7\0\0\0falsey\0\0\7\0\0\0truthy\0\1\5\0\0\0None\0")
-            (Right ("", 33, Dictionary (Map.fromList [(Text "truthy", Boolean True), (Text "falsey", Boolean False)])))
-    it "can be encoded" $ do
-        shouldBe
-            (Binary.encode (Dictionary Map.empty :: Dictionary Boolean))
-            "\5\0\0\0None\0"
-        shouldBe
-            (Binary.encode (Dictionary (Map.singleton (Text "Hello") (Text "World"))))
-            "\6\0\0\0Hello\0\6\0\0\0World\0\5\0\0\0None\0"
-        shouldBe
-            (Binary.encode (Dictionary (Map.fromList [(Text "truthy", Boolean True), (Text "falsey", Boolean False)])))
-            "\7\0\0\0falsey\0\0\7\0\0\0truthy\0\1\5\0\0\0None\0"
-
-decodeDictionary :: (Binary.Binary a) => BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Dictionary a)
-decodeDictionary = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/Primitive/Float32Spec.hs b/test-suite/Octane/Type/Primitive/Float32Spec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/Primitive/Float32Spec.hs
+++ /dev/null
@@ -1,30 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.Float32Spec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-spec :: Spec
-spec = describe "Float32" $ do
-    it "can be decoded" $ do
-        shouldBe
-            (decodeFloat32 "\0\0\0\0")
-            (Right ("", 4, Float32 0.0))
-        shouldBe
-            (decodeFloat32 "\0\0\128\63")
-            (Right ("", 4, Float32 1.0))
-    it "can be encoded" $ do
-        shouldBe
-            (Binary.encode (Float32 0.0))
-            "\0\0\0\0"
-        shouldBe
-            (Binary.encode (Float32 1.0))
-            "\0\0\128\63"
-
-decodeFloat32 :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Float32)
-decodeFloat32 = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/Primitive/Int32Spec.hs b/test-suite/Octane/Type/Primitive/Int32Spec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/Primitive/Int32Spec.hs
+++ /dev/null
@@ -1,30 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.Int32Spec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-spec :: Spec
-spec = describe "Int32" $ do
-    it "can be decoded" $ do
-        shouldBe
-            (decodeInt32 "\0\0\0\0")
-            (Right ("", 4, Int32 0))
-        shouldBe
-            (decodeInt32 "\1\0\0\0")
-            (Right ("", 4, Int32 1))
-    it "can be encoded" $ do
-        shouldBe
-            (Binary.encode (Int32 0))
-            "\0\0\0\0"
-        shouldBe
-            (Binary.encode (Int32 1))
-            "\1\0\0\0"
-
-decodeInt32 :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Int32)
-decodeInt32 = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/Primitive/Int64Spec.hs b/test-suite/Octane/Type/Primitive/Int64Spec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/Primitive/Int64Spec.hs
+++ /dev/null
@@ -1,30 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.Int64Spec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-spec :: Spec
-spec = describe "Int64" $ do
-    it "can be decoded" $ do
-        shouldBe
-            (decodeInt64 "\0\0\0\0\0\0\0\0")
-            (Right ("", 8, Int64 0))
-        shouldBe
-            (decodeInt64 "\1\0\0\0\0\0\0\0")
-            (Right ("", 8, Int64 1))
-    it "can be encoded" $ do
-        shouldBe
-            (Binary.encode (Int64 0))
-            "\0\0\0\0\0\0\0\0"
-        shouldBe
-            (Binary.encode (Int64 1))
-            "\1\0\0\0\0\0\0\0"
-
-decodeInt64 :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Int64)
-decodeInt64 = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/Primitive/ListSpec.hs b/test-suite/Octane/Type/Primitive/ListSpec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/Primitive/ListSpec.hs
+++ /dev/null
@@ -1,36 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.ListSpec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-spec :: Spec
-spec = describe "List" $ do
-    it "can be decoded" $ do
-        shouldBe
-            (decodeList "\0\0\0\0")
-            (Right ("", 4, List [] :: List Boolean))
-        shouldBe
-            (decodeList "\1\0\0\0\0\0\0\0")
-            (Right ("", 8, List [Int32 0]))
-        shouldBe
-            (decodeList "\2\0\0\0\0\1")
-            (Right ("", 6, List [Boolean False, Boolean True]))
-    it "can be encoded" $ do
-        shouldBe
-            (Binary.encode (List [] :: List Boolean))
-            "\0\0\0\0"
-        shouldBe
-            (Binary.encode (List [Int32 0]))
-            "\1\0\0\0\0\0\0\0"
-        shouldBe
-            (Binary.encode (List [Boolean False, Boolean True]))
-            "\2\0\0\0\0\1"
-
-decodeList :: (Binary.Binary a) => BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, List a)
-decodeList = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/Primitive/StreamSpec.hs b/test-suite/Octane/Type/Primitive/StreamSpec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/Primitive/StreamSpec.hs
+++ /dev/null
@@ -1,32 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.StreamSpec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-spec :: Spec
-spec = describe "Stream" $ do
-    it "can be decoded" $ do
-        shouldBe
-            (decodeStream "\0\0\0\0")
-            (Right ("", 4, Stream ""))
-    it "reverses the bits within each byte when decoding" $ do
-        shouldBe
-            (decodeStream "\2\0\0\0\128\64")
-            (Right ("", 6, Stream "\1\2"))
-    it "can be encoded" $ do
-        shouldBe
-            (Binary.encode (Stream ""))
-            "\0\0\0\0"
-    it "reverses the bits within each byte when encoding" $ do
-        shouldBe
-            (Binary.encode (Stream "\1\2"))
-            "\2\0\0\0\128\64"
-
-decodeStream :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Stream)
-decodeStream = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/Primitive/TextSpec.hs b/test-suite/Octane/Type/Primitive/TextSpec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/Primitive/TextSpec.hs
+++ /dev/null
@@ -1,40 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.Primitive.TextSpec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-
-spec :: Spec
-spec = describe "Text" $ do
-    it "can be decoded" $ do
-        shouldBe
-            (decodeText "\1\0\0\0\0")
-            (Right ("", 5, Text ""))
-        shouldBe
-            (decodeText "\6\0\0\0ascii\0")
-            (Right ("", 10, Text "ascii"))
-        shouldBe
-            (decodeText "\254\255\255\255\24\35\0\0")
-            (Right ("", 8, Text "\8984"))
-    it "can be encoded" $ do
-        shouldBe
-            (Binary.encode (Text ""))
-            "\1\0\0\0\0"
-        shouldBe
-            (Binary.encode (Text "ascii"))
-            "\6\0\0\0ascii\0"
-        shouldBe
-            (Binary.encode (Text "\8984"))
-            "\254\255\255\255\24\35\0\0"
-    it "decodes strings of length 0" $ do
-        shouldBe
-            (decodeText "\0\0\0\0")
-            (Right ("", 4, ""))
-
-decodeText :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Text)
-decodeText = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/PrimitiveSpec.hs b/test-suite/Octane/Type/PrimitiveSpec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/PrimitiveSpec.hs
+++ /dev/null
@@ -1,23 +0,0 @@
-module Octane.Type.PrimitiveSpec (spec) where
-
-import Test.Tasty.Hspec
-
-import qualified Octane.Type.Primitive.BooleanSpec
-import qualified Octane.Type.Primitive.DictionarySpec
-import qualified Octane.Type.Primitive.Float32Spec
-import qualified Octane.Type.Primitive.ListSpec
-import qualified Octane.Type.Primitive.TextSpec
-import qualified Octane.Type.Primitive.StreamSpec
-import qualified Octane.Type.Primitive.Int32Spec
-import qualified Octane.Type.Primitive.Int64Spec
-
-spec :: Spec
-spec = describe "Primitive" $ do
-    Octane.Type.Primitive.BooleanSpec.spec
-    Octane.Type.Primitive.DictionarySpec.spec
-    Octane.Type.Primitive.Float32Spec.spec
-    Octane.Type.Primitive.ListSpec.spec
-    Octane.Type.Primitive.TextSpec.spec
-    Octane.Type.Primitive.StreamSpec.spec
-    Octane.Type.Primitive.Int32Spec.spec
-    Octane.Type.Primitive.Int64Spec.spec
diff --git a/test-suite/Octane/Type/PropertySpec.hs b/test-suite/Octane/Type/PropertySpec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/PropertySpec.hs
+++ /dev/null
@@ -1,316 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.PropertySpec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-import qualified Data.Map as Map
-
-spec :: Spec
-spec = describe "Property" $ do
-    describe "Array" $ do
-        it "can be decoded" $ do
-            shouldBe
-                (decodeProperty "\
-                    \\14\0\0\0ArrayProperty\0\
-                    \\0\0\0\0\0\0\0\0\
-                    \\0\0\0\0")
-                (Right ("", 30, ArrayProperty
-                    (Int64 0)
-                    (List [])))
-            shouldBe
-                (decodeProperty "\
-                    \\14\0\0\0ArrayProperty\0\
-                    \\1\0\0\0\0\0\0\0\1\0\0\0\2\0\0\0a\0\13\0\0\0BoolProperty\0\2\0\0\0\0\0\0\0\1\5\0\0\0None\0")
-                (Right ("", 71, ArrayProperty
-                    (Int64 1)
-                    (List [Dictionary (Map.singleton (Text "a") (BoolProperty (Int64 2) (Boolean True)))])))
-        it "can be encoded" $ do
-            shouldBe
-                (Binary.encode (ArrayProperty
-                    (Int64 0)
-                    (List [])))
-                "\
-                    \\14\0\0\0ArrayProperty\0\
-                    \\0\0\0\0\0\0\0\0\
-                    \\0\0\0\0"
-            shouldBe
-                (Binary.encode (ArrayProperty
-                    (Int64 1)
-                    (List [Dictionary (Map.singleton (Text "a") (BoolProperty (Int64 2) (Boolean True)))])))
-                "\
-                    \\14\0\0\0ArrayProperty\0\
-                    \\1\0\0\0\0\0\0\0\1\0\0\0\2\0\0\0a\0\13\0\0\0BoolProperty\0\2\0\0\0\0\0\0\0\1\5\0\0\0None\0"
-    describe "Bool" $ do
-        it "can be decoded" $ do
-            shouldBe
-                (decodeProperty "\
-                    \\13\0\0\0BoolProperty\0\
-                    \\0\0\0\0\0\0\0\0\
-                    \\0")
-                (Right ("", 26, BoolProperty
-                    (Int64 0)
-                    (Boolean False)))
-            shouldBe
-                (decodeProperty "\
-                    \\13\0\0\0BoolProperty\0\
-                    \\1\0\0\0\0\0\0\0\
-                    \\1")
-                (Right ("", 26, BoolProperty
-                    (Int64 1)
-                    (Boolean True)))
-        it "can be encoded" $ do
-            shouldBe
-                (Binary.encode (BoolProperty
-                    (Int64 0)
-                    (Boolean False)))
-                "\
-                    \\13\0\0\0BoolProperty\0\
-                    \\0\0\0\0\0\0\0\0\
-                    \\0"
-            shouldBe
-                (Binary.encode (BoolProperty
-                    (Int64 1)
-                    (Boolean True)))
-                "\
-                    \\13\0\0\0BoolProperty\0\
-                    \\1\0\0\0\0\0\0\0\
-                    \\1"
-    describe "Byte" $ do
-        it "can be decoded" $ do
-            shouldBe
-                (decodeProperty "\
-                    \\13\0\0\0ByteProperty\0\
-                    \\0\0\0\0\0\0\0\0\
-                    \\1\0\0\0\0\1\0\0\0\0")
-                (Right ("", 35, ByteProperty
-                    (Int64 0)
-                    (Text "", Text "")))
-            shouldBe
-                (decodeProperty "\
-                    \\13\0\0\0ByteProperty\0\
-                    \\1\0\0\0\0\0\0\0\
-                    \\2\0\0\0a\0\2\0\0\0b\0")
-                (Right ("", 37, ByteProperty
-                    (Int64 1)
-                    (Text "a", Text "b")))
-        it "can be encoded" $ do
-            shouldBe
-                (Binary.encode (ByteProperty
-                    (Int64 0)
-                    (Text "", Text "")))
-                "\
-                    \\13\0\0\0ByteProperty\0\
-                    \\0\0\0\0\0\0\0\0\
-                    \\1\0\0\0\0\1\0\0\0\0"
-            shouldBe
-                (Binary.encode (ByteProperty
-                    (Int64 1)
-                    (Text "a", Text "b")))
-                "\
-                    \\13\0\0\0ByteProperty\0\
-                    \\1\0\0\0\0\0\0\0\
-                    \\2\0\0\0a\0\2\0\0\0b\0"
-    describe "Float" $ do
-        it "can be decoded" $ do
-            shouldBe
-                (decodeProperty "\
-                    \\14\0\0\0FloatProperty\0\
-                    \\4\0\0\0\0\0\0\0\
-                    \\0\0\0\0")
-                (Right ("", 30, FloatProperty
-                    (Int64 4)
-                    (Float32 0.0)))
-            shouldBe
-                (decodeProperty "\
-                    \\14\0\0\0FloatProperty\0\
-                    \\4\0\0\0\0\0\0\0\
-                    \\0\0\128\63")
-                (Right ("", 30, FloatProperty
-                    (Int64 4)
-                    (Float32 1.0)))
-        it "can be encoded" $ do
-            shouldBe
-                (Binary.encode (FloatProperty
-                    (Int64 4)
-                    (Float32 0.0)))
-                "\
-                    \\14\0\0\0FloatProperty\0\
-                    \\4\0\0\0\0\0\0\0\
-                    \\0\0\0\0"
-            shouldBe
-                (Binary.encode (FloatProperty
-                    (Int64 4)
-                    (Float32 1.0)))
-                "\
-                    \\14\0\0\0FloatProperty\0\
-                    \\4\0\0\0\0\0\0\0\
-                    \\0\0\128\63"
-        it "does not raise a runtime error when decoding garbage" $ do
-            shouldBe
-                (decodeProperty "\
-                    \\14\0\0\0FloatProperty\0\
-                    \\0\0\0\0\0\0\0\0")
-                (Left ("", 26, "unknown FloatProperty size 0"))
-    describe "Int" $ do
-        it "can be decoded" $ do
-            shouldBe
-                (decodeProperty "\
-                    \\12\0\0\0IntProperty\0\
-                    \\4\0\0\0\0\0\0\0\
-                    \\0\0\0\0")
-                (Right ("", 28, IntProperty
-                    (Int64 4)
-                    (Int32 0)))
-            shouldBe
-                (decodeProperty "\
-                    \\12\0\0\0IntProperty\0\
-                    \\4\0\0\0\0\0\0\0\
-                    \\1\0\0\0")
-                (Right ("", 28, IntProperty
-                    (Int64 4)
-                    (Int32 1)))
-        it "can be encoded" $ do
-            shouldBe
-                (Binary.encode (IntProperty
-                    (Int64 4)
-                    (Int32 0)))
-                "\
-                    \\12\0\0\0IntProperty\0\
-                    \\4\0\0\0\0\0\0\0\
-                    \\0\0\0\0"
-            shouldBe
-                (Binary.encode (IntProperty
-                    (Int64 4)
-                    (Int32 1)))
-                "\
-                    \\12\0\0\0IntProperty\0\
-                    \\4\0\0\0\0\0\0\0\
-                    \\1\0\0\0"
-        it "does not raise a runtime error when decoding garbage" $ do
-            shouldBe
-                (decodeProperty "\
-                    \\12\0\0\0IntProperty\0\
-                    \\0\0\0\0\0\0\0\0")
-                (Left ("", 24, "unknown IntProperty size 0"))
-    describe "Name" $ do
-        it "can be decoded" $ do
-            shouldBe
-                (decodeProperty "\
-                    \\13\0\0\0NameProperty\0\
-                    \\0\0\0\0\0\0\0\0\
-                    \\1\0\0\0\0")
-                (Right ("", 30, NameProperty
-                    (Int64 0)
-                    (Text "")))
-            shouldBe
-                (decodeProperty "\
-                    \\13\0\0\0NameProperty\0\
-                    \\1\0\0\0\0\0\0\0\
-                    \\2\0\0\0a\0")
-                (Right ("", 31, NameProperty
-                    (Int64 1)
-                    (Text "a")))
-        it "can be encoded" $ do
-            shouldBe
-                (Binary.encode (NameProperty
-                    (Int64 0)
-                    (Text "")))
-                "\
-                    \\13\0\0\0NameProperty\0\
-                    \\0\0\0\0\0\0\0\0\
-                    \\1\0\0\0\0"
-            shouldBe
-                (Binary.encode (NameProperty
-                    (Int64 1)
-                    (Text "a")))
-                "\
-                    \\13\0\0\0NameProperty\0\
-                    \\1\0\0\0\0\0\0\0\
-                    \\2\0\0\0a\0"
-    describe "QWord" $ do
-        it "can be decoded" $ do
-            shouldBe
-                (decodeProperty "\
-                    \\14\0\0\0QWordProperty\0\
-                    \\8\0\0\0\0\0\0\0\
-                    \\0\0\0\0\0\0\0\0")
-                (Right ("", 34, QWordProperty
-                    (Int64 8)
-                    (Int64 0)))
-            shouldBe
-                (decodeProperty "\
-                    \\14\0\0\0QWordProperty\0\
-                    \\8\0\0\0\0\0\0\0\
-                    \\2\0\0\0\0\0\0\0")
-                (Right ("", 34, QWordProperty
-                    (Int64 8)
-                    (Int64 2)))
-        it "can be encoded" $ do
-            shouldBe
-                (Binary.encode (QWordProperty
-                    (Int64 8)
-                    (Int64 0)))
-                "\
-                    \\14\0\0\0QWordProperty\0\
-                    \\8\0\0\0\0\0\0\0\
-                    \\0\0\0\0\0\0\0\0"
-            shouldBe
-                (Binary.encode (QWordProperty
-                    (Int64 8)
-                    (Int64 2)))
-                "\
-                    \\14\0\0\0QWordProperty\0\
-                    \\8\0\0\0\0\0\0\0\
-                    \\2\0\0\0\0\0\0\0"
-        it "does not raise a runtime error when decoding garbage" $ do
-            shouldBe
-                (decodeProperty "\14\0\0\0QWordProperty\0\0\0\0\0\0\0\0\0")
-                (Left ("", 26, "unknown QWordProperty size 0"))
-    describe "Str" $ do
-        it "can be decoded" $ do
-            shouldBe
-                (decodeProperty "\
-                    \\12\0\0\0StrProperty\0\
-                    \\0\0\0\0\0\0\0\0\
-                    \\1\0\0\0\0")
-                (Right ("", 29, StrProperty
-                    (Int64 0)
-                    (Text "")))
-            shouldBe
-                (decodeProperty "\
-                    \\12\0\0\0StrProperty\0\
-                    \\1\0\0\0\0\0\0\0\
-                    \\2\0\0\0a\0")
-                (Right ("", 30, StrProperty
-                    (Int64 1)
-                    (Text "a")))
-        it "can be encoded" $ do
-            shouldBe
-                (Binary.encode (StrProperty
-                    (Int64 0)
-                    (Text "")))
-                "\
-                    \\12\0\0\0StrProperty\0\
-                    \\0\0\0\0\0\0\0\0\
-                    \\1\0\0\0\0"
-            shouldBe
-                (Binary.encode (StrProperty
-                    (Int64 1)
-                    (Text "a")))
-                "\
-                    \\12\0\0\0StrProperty\0\
-                    \\1\0\0\0\0\0\0\0\
-                    \\2\0\0\0a\0"
-    it "does not raise a runtime error when decoding garbage" $ do
-        shouldBe
-            (decodeProperty "\
-                \\14\0\0\0OtherProperty\0")
-            (Left ("", 18, "unknown property type \"OtherProperty\""))
-
-decodeProperty :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Property)
-decodeProperty = Binary.decodeOrFail
diff --git a/test-suite/Octane/Type/ReplaySpec.hs b/test-suite/Octane/Type/ReplaySpec.hs
deleted file mode 100644
--- a/test-suite/Octane/Type/ReplaySpec.hs
+++ /dev/null
@@ -1,97 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Octane.Type.ReplaySpec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.ByteString.Lazy as BSL
-import qualified Data.Map as Map
-
-spec :: Spec
-spec = describe "Replay" $ do
-    it "can be decoded" $ do
-        shouldBe
-            (decodeReplay "\
-                \\x16\x00\x00\x00\
-                \\xc5\x30\xa7\xe2\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\1\0\0\0\0\
-                \\5\0\0\0None\0\
-                \\x28\x00\x00\x00\
-                \\xf5\x9a\x89\x13\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0")
-            (Right ("", 78, Replay
-                (Word32 22)
-                (Word32 3802607813)
-                (Int32 0)
-                (Int32 0)
-                (Text "")
-                (Dictionary Map.empty)
-                (Word32 40)
-                (Word32 327785205)
-                (List [])
-                (List [])
-                (Stream "")
-                (List [])
-                (List [])
-                (List [])
-                (List [])
-                (List [])
-                (List [])
-                (List [])))
-    it "can be encoded" $ do
-        shouldBe
-            (Binary.encode (Replay
-                (Word32 0)
-                (Word32 0)
-                (Int32 0)
-                (Int32 0)
-                (Text "")
-                (Dictionary Map.empty)
-                (Word32 0)
-                (Word32 0)
-                (List [])
-                (List [])
-                (Stream "")
-                (List [])
-                (List [])
-                (List [])
-                (List [])
-                (List [])
-                (List [])
-                (List [])))
-            "\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\1\0\0\0\0\
-                \\5\0\0\0None\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0\
-                \\0\0\0\0"
-
-decodeReplay :: BSL.ByteString -> Either (BSL.ByteString, Binary.ByteOffset, String) (BSL.ByteString, Binary.ByteOffset, Replay)
-decodeReplay = Binary.decodeOrFail
diff --git a/test-suite/Octane/TypeSpec.hs b/test-suite/Octane/TypeSpec.hs
deleted file mode 100644
--- a/test-suite/Octane/TypeSpec.hs
+++ /dev/null
@@ -1,25 +0,0 @@
-module Octane.TypeSpec (spec) where
-
-import Test.Tasty.Hspec
-
-import qualified Octane.Type.ActorSpec
-import qualified Octane.Type.CacheItemSpec
-import qualified Octane.Type.CachePropertySpec
-import qualified Octane.Type.KeyFrameSpec
-import qualified Octane.Type.MarkSpec
-import qualified Octane.Type.MessageSpec
-import qualified Octane.Type.PrimitiveSpec
-import qualified Octane.Type.PropertySpec
-import qualified Octane.Type.ReplaySpec
-
-spec :: Spec
-spec = describe "Type" $ do
-    Octane.Type.ActorSpec.spec
-    Octane.Type.CacheItemSpec.spec
-    Octane.Type.CachePropertySpec.spec
-    Octane.Type.KeyFrameSpec.spec
-    Octane.Type.MarkSpec.spec
-    Octane.Type.MessageSpec.spec
-    Octane.Type.PrimitiveSpec.spec
-    Octane.Type.PropertySpec.spec
-    Octane.Type.ReplaySpec.spec
diff --git a/test-suite/Octane/VersionSpec.hs b/test-suite/Octane/VersionSpec.hs
deleted file mode 100644
--- a/test-suite/Octane/VersionSpec.hs
+++ /dev/null
@@ -1,11 +0,0 @@
-module Octane.VersionSpec (spec) where
-
-import Octane
-import Test.Tasty.Hspec
-
-import qualified Data.Version as Version
-
-spec :: Spec
-spec = describe "Version" $ do
-    it "has some parts" $ do
-        shouldBe (null (Version.versionBranch version)) False
diff --git a/test-suite/OctaneSpec.hs b/test-suite/OctaneSpec.hs
--- a/test-suite/OctaneSpec.hs
+++ b/test-suite/OctaneSpec.hs
@@ -1,13 +1,8 @@
 module OctaneSpec (spec) where
 
-import Test.Tasty.Hspec
+import qualified Test.Tasty.Hspec as Hspec
 
-import qualified Octane.MainSpec
-import qualified Octane.TypeSpec
-import qualified Octane.VersionSpec
 
-spec :: Spec
-spec = describe "Octane" $ do
-    Octane.MainSpec.spec
-    Octane.TypeSpec.spec
-    Octane.VersionSpec.spec
+spec :: Hspec.Spec
+spec = Hspec.describe "Octane" (do
+    pure ())
diff --git a/test-suite/TestSuite.hs b/test-suite/TestSuite.hs
deleted file mode 100644
--- a/test-suite/TestSuite.hs
+++ /dev/null
@@ -1,12 +0,0 @@
-import Test.Tasty.Hspec
-
-import qualified OctaneSpec
-import qualified Test.Tasty
-
-main :: IO ()
-main = do
-    test <- testSpec "octane" spec
-    Test.Tasty.defaultMain test
-
-spec :: Spec
-spec = parallel OctaneSpec.spec
