diff --git a/LICENSE.markdown b/LICENSE.markdown
--- a/LICENSE.markdown
+++ b/LICENSE.markdown
@@ -1,13 +1,13 @@
-# [The MIT License (MIT)][]
+MIT License
 
-Copyright (c) Taylor Fausak
+Copyright (c) 2018 Taylor Fausak
 
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
 
 The above copyright notice and this permission notice shall be included in all
 copies or substantial portions of the Software.
@@ -19,5 +19,3 @@
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
-
-[The MIT License (MIT)]: http://opensource.org/licenses/MIT
diff --git a/README.markdown b/README.markdown
--- a/README.markdown
+++ b/README.markdown
@@ -16,45 +16,82 @@
 
 ## Replays
 
-Rocket League saves your replays in a folder that depends on your operating system.
+Rocket League saves your replays in a folder that depends on your operating
+system.
 
-- Windows: `%UserProfile%\Documents\My Games\Rocket League\TAGame\Demos`
-- macOS: `$HOME/Library/Application Support/Rocket League/TAGame/Demos`
-- Linux: `$HOME/.local/share/Rocket League/TAGame/Demos`
+- Windows:
+  - `%UserProfile%/Documents/My Games/Rocket League/TAGame/Demos`
+  - For example: `C:/Users/Taylor/Documents/My Games/Rocket League/TAGame/Demos`
+- macOS:
+  - `$HOME/Library/Application Support/Rocket League/TAGame/Demos`
+  - For example: `/Users/taylor/Library/Application Support/Rocket League/TAGame/Demos`
+- Linux:
+  - `$HOME/.local/share/Rocket League/TAGame/Demos`
+  - For example: `/home/taylor/.local/share/Rocket League/TAGame/Demos`
 
+## Interface
+
+Rattletrap is a command line application.
+
+``` sh
+> rattletrap --help
+rattletrap version 4.0.0
+  -c             --compact                minify JSON output
+  -h             --help                   show the help
+  -i FILE|URL    --input=FILE|URL         input file or URL
+  -m MODE        --mode=MODE              decode or encode
+  -o FILE        --output=FILE            output file
+  -v             --version                show the version
+```
+
+By default Rattletrap will try to determine the appropriate mode (either decode
+or encode) based on the file extensions of the input or output. You can
+override this behavior by passing `--mode` (or `-m`) with either `decode` or
+`encode`.
+
+Input extension | Output extension | Mode
+--- | --- | ---
+`.replay` | anything | `decode` (parse)
+`.json` | anything | `encode` (generate)
+anything | `.replay` | `encode` (generate)
+anything | `.json` | `decode` (parse)
+anything | anything | `decode` (parse)
+
 ## Parse
 
 Rattletrap can parse (decode) Rocket League replays and output them as JSON.
 
 ``` sh
-> rattletrap decode http://example.com/input.replay output.json
+> rattletrap --input http://example.com/input.replay --output output.json
 # or
-> rattletrap decode input.replay > output.json
+> rattletrap -i input.replay > output.json
 # or
-> rattletrap.decode < input.replay > output.json
+> rattletrap < input.replay > output.json
 ```
 
 The input argument can either be a local path or a URL.
 
-The resulting JSON is minified, but extremely large. The output can be up to 50
-times larger than the input.
+By default the JSON is pretty-printed. To minify the JSON, pass `--compact` (or
+`-c`) to Rattletrap. Even when the JSON is minified, it's extremely large. The
+output can be up to 100 times larger than the input. For example, a 1.5 MB
+replay turns into 31 MB of minified JSON or 159 MB of pretty-printed JSON.
 
 ## Generate
 
 Rattletrap can also generate (encode) Rocket League replays from JSON files.
 
 ``` sh
-> rattletrap encode http://example.com/input.json output.replay
+> rattletrap --input http://example.com/input.json --output output.replay
 # or
-> rattletrap encode input.json > output.replay
+> rattletrap -i input.json > output.replay
 # or
-> rattletrap.encode < input.json > output.replay
+> rattletrap --mode encode < input.json > output.replay
 ```
 
 The input argument can either be a local path or a URL.
 
-If the JSON was generated by Rattletrap, the resulting replay should be
-identical to the original.
+If the JSON was generated by Rattletrap, the output replay will be bit-for-bit
+identical to the input replay.
 
 ## Modify
 
@@ -62,17 +99,17 @@
 used to modify replays.
 
 ``` sh
-> rattletrap decode < original.replay |
+> rattletrap -i input.replay |
   modify-replay-json |
-  rattletrap encode > modified.replay
+  rattletrap -o output.replay
 ```
 
 [Rattletrap]: https://github.com/tfausak/rattletrap
-[Version badge]: https://www.stackage.org/package/rattletrap/badge/nightly?label=version
+[Version badge]: https://img.shields.io/hackage/v/rattletrap.svg?label=version
 [version]: https://www.stackage.org/nightly/package/rattletrap
-[Windows build badge]: https://ci.appveyor.com/api/projects/status/github/tfausak/rattletrap?branch=master&svg=true
+[Windows build badge]: https://img.shields.io/appveyor/ci/taylorfausak/rattletrap/master.svg?logo=appveyor
 [windows build]: https://ci.appveyor.com/project/TaylorFausak/rattletrap
-[Build badge]: https://travis-ci.org/tfausak/rattletrap.svg?branch=master
+[Build badge]: https://img.shields.io/travis/tfausak/rattletrap/master.svg
 [build]: https://travis-ci.org/tfausak/rattletrap
 [Rocket League]: https://www.rocketleaguegame.com
 [the latest release]: https://github.com/tfausak/rattletrap/releases/latest
diff --git a/executables/Main.hs b/executables/Main.hs
--- a/executables/Main.hs
+++ b/executables/Main.hs
@@ -1,5 +1,5 @@
 module Main
-  ( module Rattletrap
+  ( main
   ) where
 
-import Rattletrap (main)
+import Rattletrap.Console.Main (main)
diff --git a/library/Rattletrap.hs b/library/Rattletrap.hs
--- a/library/Rattletrap.hs
+++ b/library/Rattletrap.hs
@@ -1,87 +1,140 @@
--- | Rattletrap is a tool for parsing and generating Rocket League replays. It
--- is typically used as an executable; see "Rattletrap.Main" for details about
--- that interface. Using Rattletrap from Haskell is as easy as importing this
--- module. Together with "Data.ByteString.Lazy" and "Data.Binary.Get", you can
--- decode a 'Replay' value. Then you can transform it into JSON with
--- "Data.Aeson". For example:
---
--- @
--- let file = "path-to/some.replay"
--- bytes <- 'Data.ByteString.Lazy.readFile' file
--- let replay = 'Data.Binary.Get.runGet' 'Rattletrap.Replay.getReplay' bytes
--- let json = 'Data.Aeson.encode' replay
--- 'Data.ByteString.Lazy.putStr' json
--- @
---
--- There are a few unusual aspects of Rattletrap to be aware of:
---
--- - In order to improve memory usage, everything Rattletrap does is strict.
---   Don't expect any lazy values or evaluation.
--- - Some names, like 'Text', collide with commonly-used modules. Use
---   qualified imports to avoid collisions.
--- - No instances of 'Data.Binary.Binary' are provided. Use functions like
---   'getReplay' instead. This was done to improve type inference and make
---   profiling easier. Also some types require information that could not be
---   provided by the 'Data.Binary.Binary' interface.
--- - Importing one specific module is not recommended because all JSON
---   instances are defined as orphans in "Rattletrap.Json".
 module Rattletrap
-  ( module Rattletrap.ActorMap
-  , module Rattletrap.Attribute
-  , module Rattletrap.AttributeMapping
-  , module Rattletrap.AttributeType
-  , module Rattletrap.AttributeValue
-  , module Rattletrap.Cache
-  , module Rattletrap.ClassAttributeMap
-  , module Rattletrap.ClassMapping
-  , module Rattletrap.Content
-  , module Rattletrap.Crc
-  , module Rattletrap.Data
-  , module Rattletrap.Frame
-  , module Rattletrap.Header
-  , module Rattletrap.Helper
-  , module Rattletrap.Initialization
-  , module Rattletrap.KeyFrame
-  , module Rattletrap.Main
-  , module Rattletrap.Mark
-  , module Rattletrap.Message
-  , module Rattletrap.Primitive
-  , module Rattletrap.Property
-  , module Rattletrap.PropertyValue
-  , module Rattletrap.RemoteId
-  , module Rattletrap.Replay
-  , module Rattletrap.Replication
-  , module Rattletrap.ReplicationValue
-  , module Rattletrap.Utility
-  , module Rattletrap.Version
+  ( Rattletrap.Utility.Helper.decodeReplayFile
+  , Rattletrap.Utility.Helper.encodeReplayJson
+  , Rattletrap.Utility.Helper.decodeReplayJson
+  , Rattletrap.Utility.Helper.encodeReplayFile
+  , Rattletrap.Type.Replay.Replay(..)
+  , Rattletrap.Type.Section.Section(..)
+  , Rattletrap.Type.Header.Header(..)
+  , Rattletrap.Type.Word32le.Word32le(..)
+  , Rattletrap.Type.Str.Str(..)
+  , Rattletrap.Type.Dictionary.Dictionary(..)
+  , Rattletrap.Type.Property.Property(..)
+  , Rattletrap.Type.Word64le.Word64le(..)
+  , Rattletrap.Type.PropertyValue.PropertyValue(..)
+  , Rattletrap.Type.List.List(..)
+  , Rattletrap.Type.Word8le.Word8le(..)
+  , Rattletrap.Type.Float32le.Float32le(..)
+  , Rattletrap.Type.Int32le.Int32le(..)
+  , Rattletrap.Type.Content.Content(..)
+  , Rattletrap.Type.KeyFrame.KeyFrame(..)
+  , Rattletrap.Type.Frame.Frame(..)
+  , Rattletrap.Type.Replication.Replication(..)
+  , Rattletrap.Type.CompressedWord.CompressedWord(..)
+  , Rattletrap.Type.ReplicationValue.ReplicationValue(..)
+  , Rattletrap.Type.SpawnedReplication.SpawnedReplication(..)
+  , Rattletrap.Type.Initialization.Initialization(..)
+  , Rattletrap.Type.Vector.Vector(..)
+  , Rattletrap.Type.Int8Vector.Int8Vector(..)
+  , Rattletrap.Type.Int8le.Int8le(..)
+  , Rattletrap.Type.UpdatedReplication.UpdatedReplication(..)
+  , Rattletrap.Type.Attribute.Attribute(..)
+  , Rattletrap.Type.AttributeValue.AttributeValue(..)
+  , Rattletrap.Type.AppliedDamageAttribute.AppliedDamageAttribute(..)
+  , Rattletrap.Type.BooleanAttribute.BooleanAttribute(..)
+  , Rattletrap.Type.ByteAttribute.ByteAttribute(..)
+  , Rattletrap.Type.CamSettingsAttribute.CamSettingsAttribute(..)
+  , Rattletrap.Type.ClubColorsAttribute.ClubColorsAttribute(..)
+  , Rattletrap.Type.DamageStateAttribute.DamageStateAttribute(..)
+  , Rattletrap.Type.DemolishAttribute.DemolishAttribute(..)
+  , Rattletrap.Type.EnumAttribute.EnumAttribute(..)
+  , Rattletrap.Type.ExplosionAttribute.ExplosionAttribute(..)
+  , Rattletrap.Type.ExtendedExplosionAttribute.ExtendedExplosionAttribute(..)
+  , Rattletrap.Type.FlaggedIntAttribute.FlaggedIntAttribute(..)
+  , Rattletrap.Type.FloatAttribute.FloatAttribute(..)
+  , Rattletrap.Type.GameModeAttribute.GameModeAttribute(..)
+  , Rattletrap.Type.IntAttribute.IntAttribute(..)
+  , Rattletrap.Type.LoadoutAttribute.LoadoutAttribute(..)
+  , Rattletrap.Type.LoadoutOnlineAttribute.LoadoutOnlineAttribute(..)
+  , Rattletrap.Type.ProductAttribute.ProductAttribute(..)
+  , Rattletrap.Type.LoadoutsAttribute.LoadoutsAttribute(..)
+  , Rattletrap.Type.LoadoutsOnlineAttribute.LoadoutsOnlineAttribute(..)
+  , Rattletrap.Type.LocationAttribute.LocationAttribute(..)
+  , Rattletrap.Type.MusicStingerAttribute.MusicStingerAttribute(..)
+  , Rattletrap.Type.PartyLeaderAttribute.PartyLeaderAttribute(..)
+  , Rattletrap.Type.RemoteId.RemoteId(..)
+  , Rattletrap.Type.PickupAttribute.PickupAttribute(..)
+  , Rattletrap.Type.PrivateMatchSettingsAttribute.PrivateMatchSettingsAttribute(..)
+  , Rattletrap.Type.QWordAttribute.QWordAttribute(..)
+  , Rattletrap.Type.ReservationAttribute.ReservationAttribute(..)
+  , Rattletrap.Type.UniqueIdAttribute.UniqueIdAttribute(..)
+  , Rattletrap.Type.RigidBodyStateAttribute.RigidBodyStateAttribute(..)
+  , Rattletrap.Type.CompressedWordVector.CompressedWordVector(..)
+  , Rattletrap.Type.StringAttribute.StringAttribute(..)
+  , Rattletrap.Type.TeamPaintAttribute.TeamPaintAttribute(..)
+  , Rattletrap.Type.WeldedInfoAttribute.WeldedInfoAttribute(..)
+  , Rattletrap.Type.DestroyedReplication.DestroyedReplication(..)
+  , Rattletrap.Type.Message.Message(..)
+  , Rattletrap.Type.Mark.Mark(..)
+  , Rattletrap.Type.ClassMapping.ClassMapping(..)
+  , Rattletrap.Type.Cache.Cache(..)
+  , Rattletrap.Type.AttributeMapping.AttributeMapping(..)
   ) where
 
-import Rattletrap.ActorMap
-import Rattletrap.Attribute
-import Rattletrap.AttributeMapping
-import Rattletrap.AttributeType
-import Rattletrap.AttributeValue
-import Rattletrap.Cache
-import Rattletrap.ClassAttributeMap
-import Rattletrap.ClassMapping
-import Rattletrap.Content
-import Rattletrap.Crc
-import Rattletrap.Data
-import Rattletrap.Frame
-import Rattletrap.Header
-import Rattletrap.Helper
-import Rattletrap.Initialization
-import Rattletrap.Json ()
-import Rattletrap.KeyFrame
-import Rattletrap.Main
-import Rattletrap.Mark
-import Rattletrap.Message
-import Rattletrap.Primitive
-import Rattletrap.Property
-import Rattletrap.PropertyValue
-import Rattletrap.RemoteId
-import Rattletrap.Replay
-import Rattletrap.Replication
-import Rattletrap.ReplicationValue
-import Rattletrap.Utility
-import Rattletrap.Version
+import qualified Rattletrap.Type.AppliedDamageAttribute
+import qualified Rattletrap.Type.Attribute
+import qualified Rattletrap.Type.AttributeMapping
+import qualified Rattletrap.Type.AttributeValue
+import qualified Rattletrap.Type.BooleanAttribute
+import qualified Rattletrap.Type.ByteAttribute
+import qualified Rattletrap.Type.Cache
+import qualified Rattletrap.Type.CamSettingsAttribute
+import qualified Rattletrap.Type.ClassMapping
+import qualified Rattletrap.Type.ClubColorsAttribute
+import qualified Rattletrap.Type.CompressedWord
+import qualified Rattletrap.Type.CompressedWordVector
+import qualified Rattletrap.Type.Content
+import qualified Rattletrap.Type.DamageStateAttribute
+import qualified Rattletrap.Type.DemolishAttribute
+import qualified Rattletrap.Type.DestroyedReplication
+import qualified Rattletrap.Type.Dictionary
+import qualified Rattletrap.Type.EnumAttribute
+import qualified Rattletrap.Type.ExplosionAttribute
+import qualified Rattletrap.Type.ExtendedExplosionAttribute
+import qualified Rattletrap.Type.FlaggedIntAttribute
+import qualified Rattletrap.Type.Float32le
+import qualified Rattletrap.Type.FloatAttribute
+import qualified Rattletrap.Type.Frame
+import qualified Rattletrap.Type.GameModeAttribute
+import qualified Rattletrap.Type.Header
+import qualified Rattletrap.Type.Initialization
+import qualified Rattletrap.Type.Int32le
+import qualified Rattletrap.Type.Int8Vector
+import qualified Rattletrap.Type.Int8le
+import qualified Rattletrap.Type.IntAttribute
+import qualified Rattletrap.Type.KeyFrame
+import qualified Rattletrap.Type.List
+import qualified Rattletrap.Type.LoadoutAttribute
+import qualified Rattletrap.Type.LoadoutOnlineAttribute
+import qualified Rattletrap.Type.LoadoutsAttribute
+import qualified Rattletrap.Type.LoadoutsOnlineAttribute
+import qualified Rattletrap.Type.LocationAttribute
+import qualified Rattletrap.Type.Mark
+import qualified Rattletrap.Type.Message
+import qualified Rattletrap.Type.MusicStingerAttribute
+import qualified Rattletrap.Type.PartyLeaderAttribute
+import qualified Rattletrap.Type.PickupAttribute
+import qualified Rattletrap.Type.PrivateMatchSettingsAttribute
+import qualified Rattletrap.Type.ProductAttribute
+import qualified Rattletrap.Type.Property
+import qualified Rattletrap.Type.PropertyValue
+import qualified Rattletrap.Type.QWordAttribute
+import qualified Rattletrap.Type.RemoteId
+import qualified Rattletrap.Type.Replay
+import qualified Rattletrap.Type.Replication
+import qualified Rattletrap.Type.ReplicationValue
+import qualified Rattletrap.Type.ReservationAttribute
+import qualified Rattletrap.Type.RigidBodyStateAttribute
+import qualified Rattletrap.Type.Section
+import qualified Rattletrap.Type.SpawnedReplication
+import qualified Rattletrap.Type.Str
+import qualified Rattletrap.Type.StringAttribute
+import qualified Rattletrap.Type.TeamPaintAttribute
+import qualified Rattletrap.Type.UniqueIdAttribute
+import qualified Rattletrap.Type.UpdatedReplication
+import qualified Rattletrap.Type.Vector
+import qualified Rattletrap.Type.WeldedInfoAttribute
+import qualified Rattletrap.Type.Word32le
+import qualified Rattletrap.Type.Word64le
+import qualified Rattletrap.Type.Word8le
+import qualified Rattletrap.Utility.Helper
diff --git a/library/Rattletrap/ActorMap.hs b/library/Rattletrap/ActorMap.hs
deleted file mode 100644
--- a/library/Rattletrap/ActorMap.hs
+++ /dev/null
@@ -1,21 +0,0 @@
-module Rattletrap.ActorMap where
-
-import Rattletrap.Primitive
-
-import qualified Data.Map.Strict as Map
-
-type ActorMap = Map.Map CompressedWord Word32
-
-{-# DEPRECATED
-makeActorMap "use Data.Map.Strict.empty"
- #-}
-
-makeActorMap :: ActorMap
-makeActorMap = Map.empty
-
-{-# DEPRECATED
-updateActorMap "use Data.Map.Strict.insert"
- #-}
-
-updateActorMap :: CompressedWord -> Word32 -> ActorMap -> ActorMap
-updateActorMap = Map.insert
diff --git a/library/Rattletrap/Attribute.hs b/library/Rattletrap/Attribute.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute.hs
+++ /dev/null
@@ -1,65 +0,0 @@
-module Rattletrap.Attribute where
-
-import Rattletrap.ActorMap
-import Rattletrap.AttributeValue
-import Rattletrap.ClassAttributeMap
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data Attribute = Attribute
-  { attributeId :: CompressedWord
-  , attributeName :: Text
-  -- ^ Read-only! Changing an attribute's name requires editing the class
-  -- attribute map.
-  , attributeValue :: AttributeValue
-  } deriving (Eq, Ord, Show)
-
-getAttributes
-  :: (Int, Int, Int)
-  -> ClassAttributeMap
-  -> ActorMap
-  -> CompressedWord
-  -> BinaryBit.BitGet [Attribute]
-getAttributes version classAttributeMap actorMap actorId = do
-  hasAttribute <- BinaryBit.getBool
-  if not hasAttribute
-    then pure []
-    else do
-      attribute <- getAttribute version classAttributeMap actorMap actorId
-      attributes <- getAttributes version classAttributeMap actorMap actorId
-      pure (attribute : attributes)
-
-putAttributes :: [Attribute] -> BinaryBit.BitPut ()
-putAttributes attributes = do
-  mapM_ putAttribute attributes
-  BinaryBit.putBool False
-
-getAttribute
-  :: (Int, Int, Int)
-  -> ClassAttributeMap
-  -> ActorMap
-  -> CompressedWord
-  -> BinaryBit.BitGet Attribute
-getAttribute version classAttributeMap actorMap actorId =
-  case getAttributeMap classAttributeMap actorMap actorId of
-    Nothing -> fail ("could not get attribute map for " ++ show actorId)
-    Just attributeMap -> case getAttributeIdLimit attributeMap of
-      Nothing -> fail ("could not get attribute ID limit for " ++ show actorId)
-      Just limit -> do
-        id_ <- getCompressedWord limit
-        case getAttributeName classAttributeMap attributeMap id_ of
-          Nothing -> fail ("could not get attribute name for " ++ show id_)
-          Just name -> do
-            value <- getAttributeValue
-              version
-              (classAttributeMapObjectMap classAttributeMap)
-              name
-            pure (Attribute id_ name value)
-
-putAttribute :: Attribute -> BinaryBit.BitPut ()
-putAttribute attribute = do
-  BinaryBit.putBool True
-  putCompressedWord (attributeId attribute)
-  putAttributeValue (attributeValue attribute)
diff --git a/library/Rattletrap/Attribute/AppliedDamage.hs b/library/Rattletrap/Attribute/AppliedDamage.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/AppliedDamage.hs
+++ /dev/null
@@ -1,28 +0,0 @@
-module Rattletrap.Attribute.AppliedDamage where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data AppliedDamageAttribute = AppliedDamageAttribute
-  { appliedDamageAttributeUnknown1 :: Word8
-  , appliedDamageAttributeLocation :: Vector
-  , appliedDamageAttributeUnknown3 :: Int32
-  , appliedDamageAttributeUnknown4 :: Int32
-  } deriving (Eq, Ord, Show)
-
-getAppliedDamageAttribute :: BinaryBit.BitGet AppliedDamageAttribute
-getAppliedDamageAttribute = do
-  unknown1 <- getWord8Bits
-  location <- getVector
-  unknown3 <- getInt32Bits
-  unknown4 <- getInt32Bits
-  pure (AppliedDamageAttribute unknown1 location unknown3 unknown4)
-
-putAppliedDamageAttribute :: AppliedDamageAttribute -> BinaryBit.BitPut ()
-putAppliedDamageAttribute appliedDamageAttribute = do
-  putWord8Bits (appliedDamageAttributeUnknown1 appliedDamageAttribute)
-  putVector (appliedDamageAttributeLocation appliedDamageAttribute)
-  putInt32Bits (appliedDamageAttributeUnknown3 appliedDamageAttribute)
-  putInt32Bits (appliedDamageAttributeUnknown4 appliedDamageAttribute)
diff --git a/library/Rattletrap/Attribute/Boolean.hs b/library/Rattletrap/Attribute/Boolean.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Boolean.hs
+++ /dev/null
@@ -1,17 +0,0 @@
-module Rattletrap.Attribute.Boolean where
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype BooleanAttribute = BooleanAttribute
-  { booleanAttributeValue :: Bool
-  } deriving (Eq, Ord, Show)
-
-getBooleanAttribute :: BinaryBit.BitGet BooleanAttribute
-getBooleanAttribute = do
-  value <- BinaryBit.getBool
-  pure (BooleanAttribute value)
-
-putBooleanAttribute :: BooleanAttribute -> BinaryBit.BitPut ()
-putBooleanAttribute booleanAttribute =
-  BinaryBit.putBool (booleanAttributeValue booleanAttribute)
diff --git a/library/Rattletrap/Attribute/Byte.hs b/library/Rattletrap/Attribute/Byte.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Byte.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module Rattletrap.Attribute.Byte where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype ByteAttribute = ByteAttribute
-  { byteAttributeValue :: Word8
-  } deriving (Eq, Ord, Show)
-
-getByteAttribute :: BinaryBit.BitGet ByteAttribute
-getByteAttribute = do
-  value <- getWord8Bits
-  pure (ByteAttribute value)
-
-putByteAttribute :: ByteAttribute -> BinaryBit.BitPut ()
-putByteAttribute byteAttribute =
-  putWord8Bits (byteAttributeValue byteAttribute)
diff --git a/library/Rattletrap/Attribute/CamSettings.hs b/library/Rattletrap/Attribute/CamSettings.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/CamSettings.hs
+++ /dev/null
@@ -1,52 +0,0 @@
-module Rattletrap.Attribute.CamSettings where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data CamSettingsAttribute = CamSettingsAttribute
-  { camSettingsAttributeFov :: Float32
-  , camSettingsAttributeHeight :: Float32
-  , camSettingsAttributeAngle :: Float32
-  , camSettingsAttributeDistance :: Float32
-  , camSettingsAttributeStiffness :: Float32
-  , camSettingsAttributeSwivelSpeed :: Float32
-  , camSettingsAttributeTransitionSpeed :: Maybe Float32
-  } deriving (Eq, Ord, Show)
-
-getCamSettingsAttribute :: (Int, Int, Int) -> BinaryBit.BitGet CamSettingsAttribute
-getCamSettingsAttribute version = do
-  fov <- getFloat32Bits
-  height <- getFloat32Bits
-  angle <- getFloat32Bits
-  distance <- getFloat32Bits
-  stiffness <- getFloat32Bits
-  swivelSpeed <- getFloat32Bits
-  transitionSpeed <- if version >= (868, 20, 0)
-    then do
-      x <- getFloat32Bits
-      pure (Just x)
-    else pure Nothing
-  pure
-    ( CamSettingsAttribute
-      fov
-      height
-      angle
-      distance
-      stiffness
-      swivelSpeed
-      transitionSpeed
-    )
-
-putCamSettingsAttribute :: CamSettingsAttribute -> BinaryBit.BitPut ()
-putCamSettingsAttribute camSettingsAttribute = do
-  putFloat32Bits (camSettingsAttributeFov camSettingsAttribute)
-  putFloat32Bits (camSettingsAttributeHeight camSettingsAttribute)
-  putFloat32Bits (camSettingsAttributeAngle camSettingsAttribute)
-  putFloat32Bits (camSettingsAttributeDistance camSettingsAttribute)
-  putFloat32Bits (camSettingsAttributeStiffness camSettingsAttribute)
-  putFloat32Bits (camSettingsAttributeSwivelSpeed camSettingsAttribute)
-  case camSettingsAttributeTransitionSpeed camSettingsAttribute of
-    Nothing -> pure ()
-    Just transitionSpeed -> putFloat32Bits transitionSpeed
diff --git a/library/Rattletrap/Attribute/ClubColors.hs b/library/Rattletrap/Attribute/ClubColors.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/ClubColors.hs
+++ /dev/null
@@ -1,28 +0,0 @@
-module Rattletrap.Attribute.ClubColors where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data ClubColorsAttribute = ClubColorsAttribute
-  { clubColorsAttributeBlueFlag :: Bool
-  , clubColorsAttributeBlueColor :: Word8
-  , clubColorsAttributeOrangeFlag :: Bool
-  , clubColorsAttributeOrangeColor :: Word8
-  } deriving (Eq, Ord, Show)
-
-getClubColorsAttribute :: BinaryBit.BitGet ClubColorsAttribute
-getClubColorsAttribute = do
-  blueFlag <- BinaryBit.getBool
-  blueColor <- getWord8Bits
-  orangeFlag <- BinaryBit.getBool
-  orangeColor <- getWord8Bits
-  pure (ClubColorsAttribute blueFlag blueColor orangeFlag orangeColor)
-
-putClubColorsAttribute :: ClubColorsAttribute -> BinaryBit.BitPut ()
-putClubColorsAttribute clubColorsAttribute = do
-  BinaryBit.putBool (clubColorsAttributeBlueFlag clubColorsAttribute)
-  putWord8Bits (clubColorsAttributeBlueColor clubColorsAttribute)
-  BinaryBit.putBool (clubColorsAttributeOrangeFlag clubColorsAttribute)
-  putWord8Bits (clubColorsAttributeOrangeColor clubColorsAttribute)
diff --git a/library/Rattletrap/Attribute/DamageState.hs b/library/Rattletrap/Attribute/DamageState.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/DamageState.hs
+++ /dev/null
@@ -1,35 +0,0 @@
-module Rattletrap.Attribute.DamageState where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data DamageStateAttribute = DamageStateAttribute
-  { damageStateAttributeUnknown1 :: Word8
-  , damageStateAttributeUnknown2 :: Bool
-  , damageStateAttributeUnknown3 :: Int32
-  , damageStateAttributeUnknown4 :: Vector
-  , damageStateAttributeUnknown5 :: Bool
-  , damageStateAttributeUnknown6 :: Bool
-  } deriving (Eq, Ord, Show)
-
-getDamageStateAttribute :: BinaryBit.BitGet DamageStateAttribute
-getDamageStateAttribute = do
-  unknown1 <- getWord8Bits
-  unknown2 <- BinaryBit.getBool
-  unknown3 <- getInt32Bits
-  unknown4 <- getVector
-  unknown5 <- BinaryBit.getBool
-  unknown6 <- BinaryBit.getBool
-  pure
-    (DamageStateAttribute unknown1 unknown2 unknown3 unknown4 unknown5 unknown6)
-
-putDamageStateAttribute :: DamageStateAttribute -> BinaryBit.BitPut ()
-putDamageStateAttribute damageStateAttribute = do
-  putWord8Bits (damageStateAttributeUnknown1 damageStateAttribute)
-  BinaryBit.putBool (damageStateAttributeUnknown2 damageStateAttribute)
-  putInt32Bits (damageStateAttributeUnknown3 damageStateAttribute)
-  putVector (damageStateAttributeUnknown4 damageStateAttribute)
-  BinaryBit.putBool (damageStateAttributeUnknown5 damageStateAttribute)
-  BinaryBit.putBool (damageStateAttributeUnknown6 damageStateAttribute)
diff --git a/library/Rattletrap/Attribute/Demolish.hs b/library/Rattletrap/Attribute/Demolish.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Demolish.hs
+++ /dev/null
@@ -1,42 +0,0 @@
-module Rattletrap.Attribute.Demolish where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data DemolishAttribute = DemolishAttribute
-  { demolishAttributeAttackerFlag :: Bool
-  , demolishAttributeAttackerActorId :: Word32
-  , demolishAttributeVictimFlag :: Bool
-  , demolishAttributeVictimActorId :: Word32
-  , demolishAttributeAttackerVelocity :: Vector
-  , demolishAttributeVictimVelocity :: Vector
-  } deriving (Eq, Ord, Show)
-
-getDemolishAttribute :: BinaryBit.BitGet DemolishAttribute
-getDemolishAttribute = do
-  attackerFlag <- BinaryBit.getBool
-  attackerActorId <- getWord32Bits
-  victimFlag <- BinaryBit.getBool
-  victimActorId <- getWord32Bits
-  attackerVelocity <- getVector
-  victimVelocity <- getVector
-  pure
-    ( DemolishAttribute
-      attackerFlag
-      attackerActorId
-      victimFlag
-      victimActorId
-      attackerVelocity
-      victimVelocity
-    )
-
-putDemolishAttribute :: DemolishAttribute -> BinaryBit.BitPut ()
-putDemolishAttribute demolishAttribute = do
-  BinaryBit.putBool (demolishAttributeAttackerFlag demolishAttribute)
-  putWord32Bits (demolishAttributeAttackerActorId demolishAttribute)
-  BinaryBit.putBool (demolishAttributeVictimFlag demolishAttribute)
-  putWord32Bits (demolishAttributeVictimActorId demolishAttribute)
-  putVector (demolishAttributeAttackerVelocity demolishAttribute)
-  putVector (demolishAttributeVictimVelocity demolishAttribute)
diff --git a/library/Rattletrap/Attribute/Enum.hs b/library/Rattletrap/Attribute/Enum.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Enum.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module Rattletrap.Attribute.Enum where
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Word as Word
-
-newtype EnumAttribute = EnumAttribute
-  { enumAttributeValue :: Word.Word16
-  } deriving (Eq, Ord, Show)
-
-getEnumAttribute :: BinaryBit.BitGet EnumAttribute
-getEnumAttribute = do
-  value <- BinaryBit.getWord16be 11
-  pure (EnumAttribute value)
-
-putEnumAttribute :: EnumAttribute -> BinaryBit.BitPut ()
-putEnumAttribute enumAttribute =
-  BinaryBit.putWord16be 11 (enumAttributeValue enumAttribute)
diff --git a/library/Rattletrap/Attribute/Explosion.hs b/library/Rattletrap/Attribute/Explosion.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Explosion.hs
+++ /dev/null
@@ -1,24 +0,0 @@
-module Rattletrap.Attribute.Explosion where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data ExplosionAttribute = ExplosionAttribute
-  { explosionAttributeActorId :: Int32
-  , explosionAttributeLocation :: Vector
-  } deriving (Eq, Ord, Show)
-
-getExplosionAttribute :: BinaryBit.BitGet ExplosionAttribute
-getExplosionAttribute = do
-  False <- BinaryBit.getBool
-  actorId <- getInt32Bits
-  location <- getVector
-  pure (ExplosionAttribute actorId location)
-
-putExplosionAttribute :: ExplosionAttribute -> BinaryBit.BitPut ()
-putExplosionAttribute explosionAttribute = do
-  BinaryBit.putBool False
-  putInt32Bits (explosionAttributeActorId explosionAttribute)
-  putVector (explosionAttributeLocation explosionAttribute)
diff --git a/library/Rattletrap/Attribute/ExtendedExplosion.hs b/library/Rattletrap/Attribute/ExtendedExplosion.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/ExtendedExplosion.hs
+++ /dev/null
@@ -1,32 +0,0 @@
-module Rattletrap.Attribute.ExtendedExplosion where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data ExtendedExplosionAttribute = ExtendedExplosionAttribute
-  { extendedExplosionAttributeActorId :: Int32
-  , extendedExplosionAttributeLocation :: Vector
-  , extendedExplosionAttributeUnknown1 :: Bool
-  , extendedExplosionAttributeUnknown2 :: Int32
-  } deriving (Eq, Ord, Show)
-
-getExtendedExplosionAttribute :: BinaryBit.BitGet ExtendedExplosionAttribute
-getExtendedExplosionAttribute = do
-  False <- BinaryBit.getBool
-  actorId <- getInt32Bits
-  location <- getVector
-  unknown1 <- BinaryBit.getBool
-  unknown2 <- getInt32Bits
-  pure (ExtendedExplosionAttribute actorId location unknown1 unknown2)
-
-putExtendedExplosionAttribute
-  :: ExtendedExplosionAttribute -> BinaryBit.BitPut ()
-putExtendedExplosionAttribute extendedExplosionAttribute = do
-  BinaryBit.putBool False
-  putInt32Bits (extendedExplosionAttributeActorId extendedExplosionAttribute)
-  putVector (extendedExplosionAttributeLocation extendedExplosionAttribute)
-  BinaryBit.putBool
-    (extendedExplosionAttributeUnknown1 extendedExplosionAttribute)
-  putInt32Bits (extendedExplosionAttributeUnknown2 extendedExplosionAttribute)
diff --git a/library/Rattletrap/Attribute/FlaggedInt.hs b/library/Rattletrap/Attribute/FlaggedInt.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/FlaggedInt.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-module Rattletrap.Attribute.FlaggedInt where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data FlaggedIntAttribute = FlaggedIntAttribute
-  { flaggedIntAttributeFlag :: Bool
-  , flaggedIntAttributeInt :: Int32
-  } deriving (Eq, Ord, Show)
-
-getFlaggedIntAttribute :: BinaryBit.BitGet FlaggedIntAttribute
-getFlaggedIntAttribute = do
-  flag <- BinaryBit.getBool
-  int <- getInt32Bits
-  pure (FlaggedIntAttribute flag int)
-
-putFlaggedIntAttribute :: FlaggedIntAttribute -> BinaryBit.BitPut ()
-putFlaggedIntAttribute flaggedIntAttribute = do
-  BinaryBit.putBool (flaggedIntAttributeFlag flaggedIntAttribute)
-  putInt32Bits (flaggedIntAttributeInt flaggedIntAttribute)
diff --git a/library/Rattletrap/Attribute/Float.hs b/library/Rattletrap/Attribute/Float.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Float.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module Rattletrap.Attribute.Float where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype FloatAttribute = FloatAttribute
-  { floatAttributeValue :: Float32
-  } deriving (Eq, Ord, Show)
-
-getFloatAttribute :: BinaryBit.BitGet FloatAttribute
-getFloatAttribute = do
-  value <- getFloat32Bits
-  pure (FloatAttribute value)
-
-putFloatAttribute :: FloatAttribute -> BinaryBit.BitPut ()
-putFloatAttribute floatAttribute =
-  putFloat32Bits (floatAttributeValue floatAttribute)
diff --git a/library/Rattletrap/Attribute/GameMode.hs b/library/Rattletrap/Attribute/GameMode.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/GameMode.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-module Rattletrap.Attribute.GameMode where
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Word as Word
-
-data GameModeAttribute = GameModeAttribute
-  { gameModeAttributeNumBits :: Int
-  , gameModeAttributeWord :: Word.Word8
-  } deriving (Eq, Ord, Show)
-
-getGameModeAttribute :: (Int, Int, Int) -> BinaryBit.BitGet GameModeAttribute
-getGameModeAttribute version = do
-  let numBits = if version < (868, 12, 0) then 2 else 8
-  word <- BinaryBit.getWord8 numBits
-  pure (GameModeAttribute numBits word)
-
-putGameModeAttribute :: GameModeAttribute -> BinaryBit.BitPut ()
-putGameModeAttribute gameModeAttribute = do
-  let numBits = gameModeAttributeNumBits gameModeAttribute
-  let word = gameModeAttributeWord gameModeAttribute
-  BinaryBit.putWord8 numBits word
diff --git a/library/Rattletrap/Attribute/Int.hs b/library/Rattletrap/Attribute/Int.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Int.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module Rattletrap.Attribute.Int where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype IntAttribute = IntAttribute
-  { intAttributeValue :: Int32
-  } deriving (Eq, Ord, Show)
-
-getIntAttribute :: BinaryBit.BitGet IntAttribute
-getIntAttribute = do
-  value <- getInt32Bits
-  pure (IntAttribute value)
-
-putIntAttribute :: IntAttribute -> BinaryBit.BitPut ()
-putIntAttribute intAttribute = putInt32Bits (intAttributeValue intAttribute)
diff --git a/library/Rattletrap/Attribute/Loadout.hs b/library/Rattletrap/Attribute/Loadout.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Loadout.hs
+++ /dev/null
@@ -1,83 +0,0 @@
-module Rattletrap.Attribute.Loadout where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data LoadoutAttribute = LoadoutAttribute
-  { loadoutAttributeVersion :: Word8
-  , loadoutAttributeBody :: Word32
-  , loadoutAttributeDecal :: Word32
-  , loadoutAttributeWheels :: Word32
-  , loadoutAttributeRocketTrail :: Word32
-  -- ^ Now known as "rocket boost".
-  , loadoutAttributeAntenna :: Word32
-  , loadoutAttributeTopper :: Word32
-  , loadoutAttributeUnknown1 :: Word32
-  , loadoutAttributeUnknown2 :: Maybe Word32
-  , loadoutAttributeEngineAudio :: Maybe Word32
-  , loadoutAttributeTrail :: Maybe Word32
-  , loadoutAttributeGoalExplosion :: Maybe Word32
-  , loadoutAttributeBanner :: Maybe Word32
-  } deriving (Eq, Ord, Show)
-
-getLoadoutAttribute :: BinaryBit.BitGet LoadoutAttribute
-getLoadoutAttribute = do
-  version <- getWord8Bits
-  body <- getWord32Bits
-  decal <- getWord32Bits
-  wheels <- getWord32Bits
-  rocketTrail <- getWord32Bits
-  antenna <- getWord32Bits
-  topper <- getWord32Bits
-  unknown1 <- getWord32Bits
-  unknown2 <- getOptional (version > Word8 10) getWord32Bits
-  engineAudio <- getOptional (version >= Word8 16) getWord32Bits
-  trail <- getOptional (version >= Word8 16) getWord32Bits
-  goalExplosion <- getOptional (version >= Word8 16) getWord32Bits
-  banner <- getOptional (version >= Word8 17) getWord32Bits
-  pure
-    ( LoadoutAttribute
-      version
-      body
-      decal
-      wheels
-      rocketTrail
-      antenna
-      topper
-      unknown1
-      unknown2
-      engineAudio
-      trail
-      goalExplosion
-      banner
-    )
-
-getOptional :: Bool -> BinaryBit.BitGet a -> BinaryBit.BitGet (Maybe a)
-getOptional p f = if p
-  then do
-    x <- f
-    pure (Just x)
-  else pure Nothing
-
-putLoadoutAttribute :: LoadoutAttribute -> BinaryBit.BitPut ()
-putLoadoutAttribute loadoutAttribute = do
-  putWord8Bits (loadoutAttributeVersion loadoutAttribute)
-  putWord32Bits (loadoutAttributeBody loadoutAttribute)
-  putWord32Bits (loadoutAttributeDecal loadoutAttribute)
-  putWord32Bits (loadoutAttributeWheels loadoutAttribute)
-  putWord32Bits (loadoutAttributeRocketTrail loadoutAttribute)
-  putWord32Bits (loadoutAttributeAntenna loadoutAttribute)
-  putWord32Bits (loadoutAttributeTopper loadoutAttribute)
-  putWord32Bits (loadoutAttributeUnknown1 loadoutAttribute)
-  putOptional (loadoutAttributeUnknown2 loadoutAttribute) putWord32Bits
-  putOptional (loadoutAttributeEngineAudio loadoutAttribute) putWord32Bits
-  putOptional (loadoutAttributeTrail loadoutAttribute) putWord32Bits
-  putOptional (loadoutAttributeGoalExplosion loadoutAttribute) putWord32Bits
-  putOptional (loadoutAttributeBanner loadoutAttribute) putWord32Bits
-
-putOptional :: Maybe a -> (a -> BinaryBit.BitPut ()) -> BinaryBit.BitPut ()
-putOptional m f = case m of
-  Just x -> f x
-  Nothing -> pure ()
diff --git a/library/Rattletrap/Attribute/LoadoutOnline.hs b/library/Rattletrap/Attribute/LoadoutOnline.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/LoadoutOnline.hs
+++ /dev/null
@@ -1,30 +0,0 @@
-module Rattletrap.Attribute.LoadoutOnline where
-
-import Rattletrap.Attribute.Product
-import Rattletrap.Primitive
-
-import qualified Control.Monad as Monad
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Map as Map
-
-newtype LoadoutOnlineAttribute = LoadoutOnlineAttribute
-  { loadoutAttributeValue :: [[ProductAttribute]]
-  } deriving (Eq, Ord, Show)
-
-getLoadoutOnlineAttribute
-  :: (Int, Int, Int)
-  -> Map.Map Word32 Text
-  -> BinaryBit.BitGet LoadoutOnlineAttribute
-getLoadoutOnlineAttribute version objectMap = do
-  size <- getWord8Bits
-  values <- Monad.replicateM
-    (fromIntegral (word8Value size))
-    (getProductAttributes version objectMap)
-  pure (LoadoutOnlineAttribute values)
-
-putLoadoutOnlineAttribute :: LoadoutOnlineAttribute -> BinaryBit.BitPut ()
-putLoadoutOnlineAttribute loadoutAttribute = do
-  let attributes = loadoutAttributeValue loadoutAttribute
-  putWord8Bits (Word8 (fromIntegral (length attributes)))
-  mapM_ putProductAttributes attributes
diff --git a/library/Rattletrap/Attribute/Loadouts.hs b/library/Rattletrap/Attribute/Loadouts.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Loadouts.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-module Rattletrap.Attribute.Loadouts where
-
-import Rattletrap.Attribute.Loadout
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data LoadoutsAttribute = LoadoutsAttribute
-  { loadoutsAttributeBlue :: LoadoutAttribute
-  , loadoutsAttributeOrange :: LoadoutAttribute
-  } deriving (Eq, Ord, Show)
-
-getLoadoutsAttribute :: BinaryBit.BitGet LoadoutsAttribute
-getLoadoutsAttribute = do
-  blue <- getLoadoutAttribute
-  orange <- getLoadoutAttribute
-  pure (LoadoutsAttribute blue orange)
-
-putLoadoutsAttribute :: LoadoutsAttribute -> BinaryBit.BitPut ()
-putLoadoutsAttribute loadoutsAttribute = do
-  putLoadoutAttribute (loadoutsAttributeBlue loadoutsAttribute)
-  putLoadoutAttribute (loadoutsAttributeOrange loadoutsAttribute)
diff --git a/library/Rattletrap/Attribute/LoadoutsOnline.hs b/library/Rattletrap/Attribute/LoadoutsOnline.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/LoadoutsOnline.hs
+++ /dev/null
@@ -1,35 +0,0 @@
-module Rattletrap.Attribute.LoadoutsOnline where
-
-import Rattletrap.Attribute.LoadoutOnline
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Map as Map
-
-data LoadoutsOnlineAttribute = LoadoutsOnlineAttribute
-  { loadoutsOnlineAttributeBlue :: LoadoutOnlineAttribute
-  , loadoutsOnlineAttributeOrange :: LoadoutOnlineAttribute
-  , loadoutsOnlineAttributeUnknown1 :: Bool
-  , loadoutsOnlineAttributeUnknown2 :: Bool
-  } deriving (Eq, Ord, Show)
-
-getLoadoutsOnlineAttribute
-  :: (Int, Int, Int)
-  -> Map.Map Word32 Text
-  -> BinaryBit.BitGet LoadoutsOnlineAttribute
-getLoadoutsOnlineAttribute version objectMap = do
-  blueLoadout <- getLoadoutOnlineAttribute version objectMap
-  orangeLoadout <- getLoadoutOnlineAttribute version objectMap
-  unknown1 <- BinaryBit.getBool
-  unknown2 <- BinaryBit.getBool
-  pure (LoadoutsOnlineAttribute blueLoadout orangeLoadout unknown1 unknown2)
-
-putLoadoutsOnlineAttribute :: LoadoutsOnlineAttribute -> BinaryBit.BitPut ()
-putLoadoutsOnlineAttribute loadoutsOnlineAttribute = do
-  putLoadoutOnlineAttribute
-    (loadoutsOnlineAttributeBlue loadoutsOnlineAttribute)
-  putLoadoutOnlineAttribute
-    (loadoutsOnlineAttributeOrange loadoutsOnlineAttribute)
-  BinaryBit.putBool (loadoutsOnlineAttributeUnknown1 loadoutsOnlineAttribute)
-  BinaryBit.putBool (loadoutsOnlineAttributeUnknown2 loadoutsOnlineAttribute)
diff --git a/library/Rattletrap/Attribute/Location.hs b/library/Rattletrap/Attribute/Location.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Location.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module Rattletrap.Attribute.Location where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype LocationAttribute = LocationAttribute
-  { locationAttributeValue :: Vector
-  } deriving (Eq, Ord, Show)
-
-getLocationAttribute :: BinaryBit.BitGet LocationAttribute
-getLocationAttribute = do
-  value <- getVector
-  pure (LocationAttribute value)
-
-putLocationAttribute :: LocationAttribute -> BinaryBit.BitPut ()
-putLocationAttribute locationAttribute =
-  putVector (locationAttributeValue locationAttribute)
diff --git a/library/Rattletrap/Attribute/MusicStinger.hs b/library/Rattletrap/Attribute/MusicStinger.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/MusicStinger.hs
+++ /dev/null
@@ -1,25 +0,0 @@
-module Rattletrap.Attribute.MusicStinger where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data MusicStingerAttribute = MusicStingerAttribute
-  { musicStingerAttributeFlag :: Bool
-  , musicStingerAttributeCue :: Word32
-  , musicStingerAttributeTrigger :: Word8
-  } deriving (Eq, Ord, Show)
-
-getMusicStingerAttribute :: BinaryBit.BitGet MusicStingerAttribute
-getMusicStingerAttribute = do
-  flag <- BinaryBit.getBool
-  cue <- getWord32Bits
-  trigger <- getWord8Bits
-  pure (MusicStingerAttribute flag cue trigger)
-
-putMusicStingerAttribute :: MusicStingerAttribute -> BinaryBit.BitPut ()
-putMusicStingerAttribute musicStingerAttribute = do
-  BinaryBit.putBool (musicStingerAttributeFlag musicStingerAttribute)
-  putWord32Bits (musicStingerAttributeCue musicStingerAttribute)
-  putWord8Bits (musicStingerAttributeTrigger musicStingerAttribute)
diff --git a/library/Rattletrap/Attribute/PartyLeader.hs b/library/Rattletrap/Attribute/PartyLeader.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/PartyLeader.hs
+++ /dev/null
@@ -1,32 +0,0 @@
-module Rattletrap.Attribute.PartyLeader where
-
-import Rattletrap.Primitive
-import Rattletrap.RemoteId
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data PartyLeaderAttribute = PartyLeaderAttribute
-  { partyLeaderAttributeSystemId :: Word8
-  , partyLeaderAttributeId :: Maybe (RemoteId, Word8)
-  } deriving (Eq, Ord, Show)
-
-getPartyLeaderAttribute :: (Int, Int, Int) -> BinaryBit.BitGet PartyLeaderAttribute
-getPartyLeaderAttribute version = do
-  systemId <- getWord8Bits
-  maybeRemoteAndLocalId <- if systemId == Word8 0
-    then pure Nothing
-    else do
-      remoteId <- getRemoteId version systemId
-      localId <- getWord8Bits
-      pure (Just (remoteId, localId))
-  pure (PartyLeaderAttribute systemId maybeRemoteAndLocalId)
-
-putPartyLeaderAttribute :: PartyLeaderAttribute -> BinaryBit.BitPut ()
-putPartyLeaderAttribute partyLeaderAttribute = do
-  putWord8Bits (partyLeaderAttributeSystemId partyLeaderAttribute)
-  case partyLeaderAttributeId partyLeaderAttribute of
-    Nothing -> pure ()
-    Just (remoteId, localId) -> do
-      putRemoteId remoteId
-      putWord8Bits localId
diff --git a/library/Rattletrap/Attribute/Pickup.hs b/library/Rattletrap/Attribute/Pickup.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Pickup.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module Rattletrap.Attribute.Pickup where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data PickupAttribute = PickupAttribute
-  { pickupAttributeInstigatorId :: Maybe Word32
-  , pickupAttributePickedUp :: Bool
-  } deriving (Eq, Ord, Show)
-
-getPickupAttribute :: BinaryBit.BitGet PickupAttribute
-getPickupAttribute = do
-  instigator <- BinaryBit.getBool
-  maybeInstigatorId <- if instigator
-    then do
-      instigatorId <- getWord32Bits
-      pure (Just instigatorId)
-    else pure Nothing
-  pickedUp <- BinaryBit.getBool
-  pure (PickupAttribute maybeInstigatorId pickedUp)
-
-putPickupAttribute :: PickupAttribute -> BinaryBit.BitPut ()
-putPickupAttribute pickupAttribute = do
-  case pickupAttributeInstigatorId pickupAttribute of
-    Nothing -> BinaryBit.putBool False
-    Just instigatorId -> do
-      BinaryBit.putBool True
-      putWord32Bits instigatorId
-  BinaryBit.putBool (pickupAttributePickedUp pickupAttribute)
diff --git a/library/Rattletrap/Attribute/PrivateMatchSettings.hs b/library/Rattletrap/Attribute/PrivateMatchSettings.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/PrivateMatchSettings.hs
+++ /dev/null
@@ -1,50 +0,0 @@
-module Rattletrap.Attribute.PrivateMatchSettings where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data PrivateMatchSettingsAttribute = PrivateMatchSettingsAttribute
-  { privateMatchSettingsAttributeMutators :: Text
-  , privateMatchSettingsAttributeJoinableBy :: Word32
-  , privateMatchSettingsAttributeMaxPlayers :: Word32
-  , privateMatchSettingsAttributeGameName :: Text
-  , privateMatchSettingsAttributePassword :: Text
-  , privateMatchSettingsAttributeFlag :: Bool
-  } deriving (Eq, Ord, Show)
-
-getPrivateMatchSettingsAttribute
-  :: BinaryBit.BitGet PrivateMatchSettingsAttribute
-getPrivateMatchSettingsAttribute = do
-  mutators <- getTextBits
-  joinableBy <- getWord32Bits
-  maxPlayers <- getWord32Bits
-  gameName <- getTextBits
-  password <- getTextBits
-  flag <- BinaryBit.getBool
-  pure
-    ( PrivateMatchSettingsAttribute
-      mutators
-      joinableBy
-      maxPlayers
-      gameName
-      password
-      flag
-    )
-
-putPrivateMatchSettingsAttribute
-  :: PrivateMatchSettingsAttribute -> BinaryBit.BitPut ()
-putPrivateMatchSettingsAttribute privateMatchSettingsAttribute = do
-  putTextBits
-    (privateMatchSettingsAttributeMutators privateMatchSettingsAttribute)
-  putWord32Bits
-    (privateMatchSettingsAttributeJoinableBy privateMatchSettingsAttribute)
-  putWord32Bits
-    (privateMatchSettingsAttributeMaxPlayers privateMatchSettingsAttribute)
-  putTextBits
-    (privateMatchSettingsAttributeGameName privateMatchSettingsAttribute)
-  putTextBits
-    (privateMatchSettingsAttributePassword privateMatchSettingsAttribute)
-  BinaryBit.putBool
-    (privateMatchSettingsAttributeFlag privateMatchSettingsAttribute)
diff --git a/library/Rattletrap/Attribute/Product.hs b/library/Rattletrap/Attribute/Product.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Product.hs
+++ /dev/null
@@ -1,87 +0,0 @@
-module Rattletrap.Attribute.Product where
-
-import Rattletrap.Primitive
-
-import qualified Control.Monad as Monad
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Map as Map
-import qualified Data.Word as Word
-
-data ProductAttribute = ProductAttribute
-  { productAttributeUnknown :: Bool
-  , productAttributeObjectId :: Word32
-  , productAttributeObjectName :: Maybe Text
-  -- ^ read-only
-  , productAttributeValue :: Maybe (Either CompressedWord Word.Word32)
-  } deriving (Eq, Ord, Show)
-
-getProductAttributes
-  :: (Int, Int, Int) -> Map.Map Word32 Text -> BinaryBit.BitGet [ProductAttribute]
-getProductAttributes version objectMap = do
-  size <- getWord8Bits
-  Monad.replicateM
-    (fromIntegral (word8Value size))
-    (getProductAttribute version objectMap)
-
-getProductAttribute
-  :: (Int, Int, Int) -> Map.Map Word32 Text -> BinaryBit.BitGet ProductAttribute
-getProductAttribute version objectMap = do
-  flag <- BinaryBit.getBool
-  objectId <- getWord32Bits
-  let objectName = Map.lookup objectId objectMap
-  value <- case objectName of
-    Just name -> case textToString name of
-      "TAGame.ProductAttribute_Painted_TA" -> if version >= (868, 18, 0)
-        then do
-          x <- BinaryBit.getWord32be 31
-          pure (Just (Right x))
-        else do
-          x <- getCompressedWord 13
-          pure (Just (Left x))
-      "TAGame.ProductAttribute_UserColor_TA" -> do
-        hasValue <- BinaryBit.getBool
-        value <- if hasValue
-          then do
-            x <- BinaryBit.getWord32be 31
-            pure (Just (Right x))
-          else pure Nothing
-        pure value
-      _ ->
-        fail
-          ( "unknown object name "
-          ++ show objectName
-          ++ " for ID "
-          ++ show objectId
-          )
-    Nothing -> fail ("missing object name for ID " ++ show objectId)
-  pure (ProductAttribute flag objectId objectName value)
-
-putProductAttributes :: [ProductAttribute] -> BinaryBit.BitPut ()
-putProductAttributes attributes = do
-  putWord8Bits (Word8 (fromIntegral (length attributes)))
-  mapM_ putProductAttribute attributes
-
-putProductAttribute :: ProductAttribute -> BinaryBit.BitPut ()
-putProductAttribute attribute = do
-  BinaryBit.putBool (productAttributeUnknown attribute)
-  putWord32Bits (productAttributeObjectId attribute)
-  case productAttributeObjectName attribute of
-    Just name -> case textToString name of
-      "TAGame.ProductAttribute_Painted_TA" ->
-        case productAttributeValue attribute of
-          Nothing -> pure ()
-          Just (Left x) -> putCompressedWord x
-          Just (Right x) -> BinaryBit.putWord32be 31 x
-      "TAGame.ProductAttribute_UserColor_TA" ->
-        case productAttributeValue attribute of
-          Nothing -> BinaryBit.putBool False
-          Just value -> do
-            BinaryBit.putBool True
-            case value of
-              Left x -> putCompressedWord x
-              Right x -> BinaryBit.putWord32be 31 x
-      _ ->
-        fail ("unknown object name for product attribute " ++ show attribute)
-    Nothing ->
-      fail ("missing object name for product attribute " ++ show attribute)
diff --git a/library/Rattletrap/Attribute/QWord.hs b/library/Rattletrap/Attribute/QWord.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/QWord.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module Rattletrap.Attribute.QWord where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype QWordAttribute = QWordAttribute
-  { qWordAttributeValue :: Word64
-  } deriving (Eq, Ord, Show)
-
-getQWordAttribute :: BinaryBit.BitGet QWordAttribute
-getQWordAttribute = do
-  value <- getWord64Bits
-  pure (QWordAttribute value)
-
-putQWordAttribute :: QWordAttribute -> BinaryBit.BitPut ()
-putQWordAttribute qWordAttribute =
-  putWord64Bits (qWordAttributeValue qWordAttribute)
diff --git a/library/Rattletrap/Attribute/Reservation.hs b/library/Rattletrap/Attribute/Reservation.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Reservation.hs
+++ /dev/null
@@ -1,48 +0,0 @@
-module Rattletrap.Attribute.Reservation where
-
-import Rattletrap.Attribute.UniqueId
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Word as Word
-
-data ReservationAttribute = ReservationAttribute
-  { reservationAttributeNumber :: CompressedWord
-  , reservationAttributeUniqueId :: UniqueIdAttribute
-  , reservationAttributeName :: Maybe Text
-  , reservationAttributeUnknown1 :: Bool
-  , reservationAttributeUnknown2 :: Bool
-  , reservationAttributeUnknown3 :: Maybe Word.Word8
-  } deriving (Eq, Ord, Show)
-
-getReservationAttribute :: (Int, Int, Int) -> BinaryBit.BitGet ReservationAttribute
-getReservationAttribute version = do
-  number <- getCompressedWord 7
-  uniqueId <- getUniqueIdAttribute version
-  name <- if uniqueIdAttributeSystemId uniqueId == Word8 0
-    then pure Nothing
-    else do
-      name <- getTextBits
-      pure (Just name)
-  a <- BinaryBit.getBool
-  b <- BinaryBit.getBool
-  mc <- if version < (868, 12, 0)
-    then pure Nothing
-    else do
-      c <- BinaryBit.getWord8 6
-      pure (Just c)
-  pure (ReservationAttribute number uniqueId name a b mc)
-
-putReservationAttribute :: ReservationAttribute -> BinaryBit.BitPut ()
-putReservationAttribute reservationAttribute = do
-  putCompressedWord (reservationAttributeNumber reservationAttribute)
-  putUniqueIdAttribute (reservationAttributeUniqueId reservationAttribute)
-  case reservationAttributeName reservationAttribute of
-    Nothing -> pure ()
-    Just name -> putTextBits name
-  BinaryBit.putBool (reservationAttributeUnknown1 reservationAttribute)
-  BinaryBit.putBool (reservationAttributeUnknown2 reservationAttribute)
-  case reservationAttributeUnknown3 reservationAttribute of
-    Nothing -> pure ()
-    Just c -> BinaryBit.putWord8 6 c
diff --git a/library/Rattletrap/Attribute/RigidBodyState.hs b/library/Rattletrap/Attribute/RigidBodyState.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/RigidBodyState.hs
+++ /dev/null
@@ -1,51 +0,0 @@
-module Rattletrap.Attribute.RigidBodyState where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data RigidBodyStateAttribute = RigidBodyStateAttribute
-  { rigidBodyStateAttributeSleeping :: Bool
-  , rigidBodyStateAttributeLocation :: Vector
-  , rigidBodyStateAttributeRotation :: CompressedWordVector
-  , rigidBodyStateAttributeLinearVelocity :: Maybe Vector
-  , rigidBodyStateAttributeAngularVelocity :: Maybe Vector
-  } deriving (Eq, Ord, Show)
-
-getRigidBodyStateAttribute :: BinaryBit.BitGet RigidBodyStateAttribute
-getRigidBodyStateAttribute = do
-  sleeping <- BinaryBit.getBool
-  location <- getVector
-  rotation <- getCompressedWordVector
-  linearVelocity <- if sleeping
-    then pure Nothing
-    else do
-      linearVelocity <- getVector
-      pure (Just linearVelocity)
-  angularVelocity <- if sleeping
-    then pure Nothing
-    else do
-      angularVelocity <- getVector
-      pure (Just angularVelocity)
-  pure
-    ( RigidBodyStateAttribute
-      sleeping
-      location
-      rotation
-      linearVelocity
-      angularVelocity
-    )
-
-putRigidBodyStateAttribute :: RigidBodyStateAttribute -> BinaryBit.BitPut ()
-putRigidBodyStateAttribute rigidBodyStateAttribute = do
-  BinaryBit.putBool (rigidBodyStateAttributeSleeping rigidBodyStateAttribute)
-  putVector (rigidBodyStateAttributeLocation rigidBodyStateAttribute)
-  putCompressedWordVector
-    (rigidBodyStateAttributeRotation rigidBodyStateAttribute)
-  case rigidBodyStateAttributeLinearVelocity rigidBodyStateAttribute of
-    Nothing -> pure ()
-    Just linearVelocity -> putVector linearVelocity
-  case rigidBodyStateAttributeAngularVelocity rigidBodyStateAttribute of
-    Nothing -> pure ()
-    Just angularVelocity -> putVector angularVelocity
diff --git a/library/Rattletrap/Attribute/String.hs b/library/Rattletrap/Attribute/String.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/String.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module Rattletrap.Attribute.String where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype StringAttribute = StringAttribute
-  { stringAttributeValue :: Text
-  } deriving (Eq, Ord, Show)
-
-getStringAttribute :: BinaryBit.BitGet StringAttribute
-getStringAttribute = do
-  value <- getTextBits
-  pure (StringAttribute value)
-
-putStringAttribute :: StringAttribute -> BinaryBit.BitPut ()
-putStringAttribute stringAttribute =
-  putTextBits (stringAttributeValue stringAttribute)
diff --git a/library/Rattletrap/Attribute/TeamPaint.hs b/library/Rattletrap/Attribute/TeamPaint.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/TeamPaint.hs
+++ /dev/null
@@ -1,38 +0,0 @@
-module Rattletrap.Attribute.TeamPaint where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data TeamPaintAttribute = TeamPaintAttribute
-  { teamPaintAttributeTeam :: Word8
-  , teamPaintAttributePrimaryColor :: Word8
-  , teamPaintAttributeAccentColor :: Word8
-  , teamPaintAttributePrimaryFinish :: Word32
-  , teamPaintAttributeAccentFinish :: Word32
-  } deriving (Eq, Ord, Show)
-
-getTeamPaintAttribute :: BinaryBit.BitGet TeamPaintAttribute
-getTeamPaintAttribute = do
-  team <- getWord8Bits
-  primaryColor <- getWord8Bits
-  accentColor <- getWord8Bits
-  primaryFinish <- getWord32Bits
-  accentFinish <- getWord32Bits
-  pure
-    ( TeamPaintAttribute
-      team
-      primaryColor
-      accentColor
-      primaryFinish
-      accentFinish
-    )
-
-putTeamPaintAttribute :: TeamPaintAttribute -> BinaryBit.BitPut ()
-putTeamPaintAttribute teamPaintAttribute = do
-  putWord8Bits (teamPaintAttributeTeam teamPaintAttribute)
-  putWord8Bits (teamPaintAttributePrimaryColor teamPaintAttribute)
-  putWord8Bits (teamPaintAttributeAccentColor teamPaintAttribute)
-  putWord32Bits (teamPaintAttributePrimaryFinish teamPaintAttribute)
-  putWord32Bits (teamPaintAttributeAccentFinish teamPaintAttribute)
diff --git a/library/Rattletrap/Attribute/UniqueId.hs b/library/Rattletrap/Attribute/UniqueId.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/UniqueId.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module Rattletrap.Attribute.UniqueId where
-
-import Rattletrap.Primitive
-import Rattletrap.RemoteId
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data UniqueIdAttribute = UniqueIdAttribute
-  { uniqueIdAttributeSystemId :: Word8
-  , uniqueIdAttributeRemoteId :: RemoteId
-  , uniqueIdAttributeLocalId :: Word8
-  } deriving (Eq, Ord, Show)
-
-getUniqueIdAttribute :: (Int, Int, Int) -> BinaryBit.BitGet UniqueIdAttribute
-getUniqueIdAttribute version = do
-  systemId <- getWord8Bits
-  remoteId <- getRemoteId version systemId
-  localId <- getWord8Bits
-  pure (UniqueIdAttribute systemId remoteId localId)
-
-putUniqueIdAttribute :: UniqueIdAttribute -> BinaryBit.BitPut ()
-putUniqueIdAttribute uniqueIdAttribute = do
-  putWord8Bits (uniqueIdAttributeSystemId uniqueIdAttribute)
-  putRemoteId (uniqueIdAttributeRemoteId uniqueIdAttribute)
-  putWord8Bits (uniqueIdAttributeLocalId uniqueIdAttribute)
diff --git a/library/Rattletrap/Attribute/WeldedInfo.hs b/library/Rattletrap/Attribute/WeldedInfo.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/WeldedInfo.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module Rattletrap.Attribute.WeldedInfo where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data WeldedInfoAttribute = WeldedInfoAttribute
-  { weldedInfoAttributeActive :: Bool
-  , weldedInfoAttributeActorId :: Int32
-  , weldedInfoAttributeOffset :: Vector
-  , weldedInfoAttributeMass :: Float32
-  , weldedInfoAttributeRotation :: Int8Vector
-  } deriving (Eq, Ord, Show)
-
-getWeldedInfoAttribute :: BinaryBit.BitGet WeldedInfoAttribute
-getWeldedInfoAttribute = do
-  active <- BinaryBit.getBool
-  actorId <- getInt32Bits
-  offset <- getVector
-  mass <- getFloat32Bits
-  rotation <- getInt8Vector
-  pure (WeldedInfoAttribute active actorId offset mass rotation)
-
-putWeldedInfoAttribute :: WeldedInfoAttribute -> BinaryBit.BitPut ()
-putWeldedInfoAttribute weldedInfoAttribute = do
-  BinaryBit.putBool (weldedInfoAttributeActive weldedInfoAttribute)
-  putInt32Bits (weldedInfoAttributeActorId weldedInfoAttribute)
-  putVector (weldedInfoAttributeOffset weldedInfoAttribute)
-  putFloat32Bits (weldedInfoAttributeMass weldedInfoAttribute)
-  putInt8Vector (weldedInfoAttributeRotation weldedInfoAttribute)
diff --git a/library/Rattletrap/AttributeMapping.hs b/library/Rattletrap/AttributeMapping.hs
deleted file mode 100644
--- a/library/Rattletrap/AttributeMapping.hs
+++ /dev/null
@@ -1,21 +0,0 @@
-module Rattletrap.AttributeMapping where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary as Binary
-
-data AttributeMapping = AttributeMapping
-  { attributeMappingObjectId :: Word32
-  , attributeMappingStreamId :: Word32
-  } deriving (Eq, Ord, Show)
-
-getAttributeMapping :: Binary.Get AttributeMapping
-getAttributeMapping = do
-  objectId <- getWord32
-  streamId <- getWord32
-  pure (AttributeMapping objectId streamId)
-
-putAttributeMapping :: AttributeMapping -> Binary.Put
-putAttributeMapping attributeMapping = do
-  putWord32 (attributeMappingObjectId attributeMapping)
-  putWord32 (attributeMappingStreamId attributeMapping)
diff --git a/library/Rattletrap/AttributeType.hs b/library/Rattletrap/AttributeType.hs
deleted file mode 100644
--- a/library/Rattletrap/AttributeType.hs
+++ /dev/null
@@ -1,34 +0,0 @@
-module Rattletrap.AttributeType where
-
-data AttributeType
-  = AppliedDamageAttributeType
-  | BooleanAttributeType
-  | ByteAttributeType
-  | CamSettingsAttributeType
-  | ClubColorsAttributeType
-  | DamageStateAttributeType
-  | DemolishAttributeType
-  | EnumAttributeType
-  | ExplosionAttributeType
-  | ExtendedExplosionAttributeType
-  | FlaggedIntAttributeType
-  | FloatAttributeType
-  | GameModeAttributeType
-  | IntAttributeType
-  | LoadoutAttributeType
-  | LoadoutOnlineAttributeType
-  | LoadoutsAttributeType
-  | LoadoutsOnlineAttributeType
-  | LocationAttributeType
-  | MusicStingerAttributeType
-  | PartyLeaderAttributeType
-  | PickupAttributeType
-  | PrivateMatchSettingsAttributeType
-  | QWordAttributeType
-  | ReservationAttributeType
-  | RigidBodyStateAttributeType
-  | StringAttributeType
-  | TeamPaintAttributeType
-  | UniqueIdAttributeType
-  | WeldedInfoAttributeType
-  deriving (Eq, Ord, Show)
diff --git a/library/Rattletrap/AttributeValue.hs b/library/Rattletrap/AttributeValue.hs
deleted file mode 100644
--- a/library/Rattletrap/AttributeValue.hs
+++ /dev/null
@@ -1,242 +0,0 @@
-module Rattletrap.AttributeValue
-  ( module Rattletrap.AttributeValue
-  , module Rattletrap.Attribute.AppliedDamage
-  , module Rattletrap.Attribute.Boolean
-  , module Rattletrap.Attribute.Byte
-  , module Rattletrap.Attribute.CamSettings
-  , module Rattletrap.Attribute.ClubColors
-  , module Rattletrap.Attribute.DamageState
-  , module Rattletrap.Attribute.Demolish
-  , module Rattletrap.Attribute.Enum
-  , module Rattletrap.Attribute.Explosion
-  , module Rattletrap.Attribute.ExtendedExplosion
-  , module Rattletrap.Attribute.FlaggedInt
-  , module Rattletrap.Attribute.Float
-  , module Rattletrap.Attribute.GameMode
-  , module Rattletrap.Attribute.Int
-  , module Rattletrap.Attribute.Loadout
-  , module Rattletrap.Attribute.LoadoutOnline
-  , module Rattletrap.Attribute.Loadouts
-  , module Rattletrap.Attribute.LoadoutsOnline
-  , module Rattletrap.Attribute.Location
-  , module Rattletrap.Attribute.MusicStinger
-  , module Rattletrap.Attribute.PartyLeader
-  , module Rattletrap.Attribute.Pickup
-  , module Rattletrap.Attribute.PrivateMatchSettings
-  , module Rattletrap.Attribute.Product
-  , module Rattletrap.Attribute.QWord
-  , module Rattletrap.Attribute.Reservation
-  , module Rattletrap.Attribute.RigidBodyState
-  , module Rattletrap.Attribute.String
-  , module Rattletrap.Attribute.TeamPaint
-  , module Rattletrap.Attribute.UniqueId
-  , module Rattletrap.Attribute.WeldedInfo
-  ) where
-
-import Rattletrap.Attribute.AppliedDamage
-import Rattletrap.Attribute.Boolean
-import Rattletrap.Attribute.Byte
-import Rattletrap.Attribute.CamSettings
-import Rattletrap.Attribute.ClubColors
-import Rattletrap.Attribute.DamageState
-import Rattletrap.Attribute.Demolish
-import Rattletrap.Attribute.Enum
-import Rattletrap.Attribute.Explosion
-import Rattletrap.Attribute.ExtendedExplosion
-import Rattletrap.Attribute.FlaggedInt
-import Rattletrap.Attribute.Float
-import Rattletrap.Attribute.GameMode
-import Rattletrap.Attribute.Int
-import Rattletrap.Attribute.Loadout
-import Rattletrap.Attribute.LoadoutOnline
-import Rattletrap.Attribute.Loadouts
-import Rattletrap.Attribute.LoadoutsOnline
-import Rattletrap.Attribute.Location
-import Rattletrap.Attribute.MusicStinger
-import Rattletrap.Attribute.PartyLeader
-import Rattletrap.Attribute.Pickup
-import Rattletrap.Attribute.PrivateMatchSettings
-import Rattletrap.Attribute.Product
-import Rattletrap.Attribute.QWord
-import Rattletrap.Attribute.Reservation
-import Rattletrap.Attribute.RigidBodyState
-import Rattletrap.Attribute.String
-import Rattletrap.Attribute.TeamPaint
-import Rattletrap.Attribute.UniqueId
-import Rattletrap.Attribute.WeldedInfo
-import Rattletrap.AttributeType
-import Rattletrap.Data
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Map.Strict as Map
-
-data AttributeValue
-  = AppliedDamageAttributeValue AppliedDamageAttribute
-  | BooleanAttributeValue BooleanAttribute
-  | ByteAttributeValue ByteAttribute
-  | CamSettingsAttributeValue CamSettingsAttribute
-  | ClubColorsAttributeValue ClubColorsAttribute
-  | DamageStateAttributeValue DamageStateAttribute
-  | DemolishAttributeValue DemolishAttribute
-  | EnumAttributeValue EnumAttribute
-  | ExplosionAttributeValue ExplosionAttribute
-  | ExtendedExplosionAttributeValue ExtendedExplosionAttribute
-  | FlaggedIntAttributeValue FlaggedIntAttribute
-  | FloatAttributeValue FloatAttribute
-  | GameModeAttributeValue GameModeAttribute
-  | IntAttributeValue IntAttribute
-  | LoadoutAttributeValue LoadoutAttribute
-  | LoadoutOnlineAttributeValue LoadoutOnlineAttribute
-  | LoadoutsAttributeValue LoadoutsAttribute
-  | LoadoutsOnlineAttributeValue LoadoutsOnlineAttribute
-  | LocationAttributeValue LocationAttribute
-  | MusicStingerAttributeValue MusicStingerAttribute
-  | PartyLeaderAttributeValue PartyLeaderAttribute
-  | PickupAttributeValue PickupAttribute
-  | PrivateMatchSettingsAttributeValue PrivateMatchSettingsAttribute
-  | QWordAttributeValue QWordAttribute
-  | ReservationAttributeValue ReservationAttribute
-  | RigidBodyStateAttributeValue RigidBodyStateAttribute
-  | StringAttributeValue StringAttribute
-  | TeamPaintAttributeValue TeamPaintAttribute
-  | UniqueIdAttributeValue UniqueIdAttribute
-  | WeldedInfoAttributeValue WeldedInfoAttribute
-  deriving (Eq, Ord, Show)
-
-getAttributeValue
-  :: (Int, Int, Int)
-  -> Map.Map Word32 Text
-  -> Text
-  -> BinaryBit.BitGet AttributeValue
-getAttributeValue version objectMap name =
-  case Map.lookup name attributeTypes of
-    Just constructor -> case constructor of
-      AppliedDamageAttributeType -> do
-        x <- getAppliedDamageAttribute
-        pure (AppliedDamageAttributeValue x)
-      BooleanAttributeType -> do
-        x <- getBooleanAttribute
-        pure (BooleanAttributeValue x)
-      ByteAttributeType -> do
-        x <- getByteAttribute
-        pure (ByteAttributeValue x)
-      CamSettingsAttributeType -> do
-        x <- getCamSettingsAttribute version
-        pure (CamSettingsAttributeValue x)
-      ClubColorsAttributeType -> do
-        x <- getClubColorsAttribute
-        pure (ClubColorsAttributeValue x)
-      DamageStateAttributeType -> do
-        x <- getDamageStateAttribute
-        pure (DamageStateAttributeValue x)
-      DemolishAttributeType -> do
-        x <- getDemolishAttribute
-        pure (DemolishAttributeValue x)
-      EnumAttributeType -> do
-        x <- getEnumAttribute
-        pure (EnumAttributeValue x)
-      ExplosionAttributeType -> do
-        x <- getExplosionAttribute
-        pure (ExplosionAttributeValue x)
-      ExtendedExplosionAttributeType -> do
-        x <- getExtendedExplosionAttribute
-        pure (ExtendedExplosionAttributeValue x)
-      FlaggedIntAttributeType -> do
-        x <- getFlaggedIntAttribute
-        pure (FlaggedIntAttributeValue x)
-      FloatAttributeType -> do
-        x <- getFloatAttribute
-        pure (FloatAttributeValue x)
-      GameModeAttributeType -> do
-        x <- getGameModeAttribute version
-        pure (GameModeAttributeValue x)
-      IntAttributeType -> do
-        x <- getIntAttribute
-        pure (IntAttributeValue x)
-      LoadoutAttributeType -> do
-        x <- getLoadoutAttribute
-        pure (LoadoutAttributeValue x)
-      LoadoutOnlineAttributeType -> do
-        x <- getLoadoutOnlineAttribute version objectMap
-        pure (LoadoutOnlineAttributeValue x)
-      LoadoutsAttributeType -> do
-        x <- getLoadoutsAttribute
-        pure (LoadoutsAttributeValue x)
-      LoadoutsOnlineAttributeType -> do
-        x <- getLoadoutsOnlineAttribute version objectMap
-        pure (LoadoutsOnlineAttributeValue x)
-      LocationAttributeType -> do
-        x <- getLocationAttribute
-        pure (LocationAttributeValue x)
-      MusicStingerAttributeType -> do
-        x <- getMusicStingerAttribute
-        pure (MusicStingerAttributeValue x)
-      PartyLeaderAttributeType -> do
-        x <- getPartyLeaderAttribute version
-        pure (PartyLeaderAttributeValue x)
-      PickupAttributeType -> do
-        x <- getPickupAttribute
-        pure (PickupAttributeValue x)
-      PrivateMatchSettingsAttributeType -> do
-        x <- getPrivateMatchSettingsAttribute
-        pure (PrivateMatchSettingsAttributeValue x)
-      QWordAttributeType -> do
-        x <- getQWordAttribute
-        pure (QWordAttributeValue x)
-      ReservationAttributeType -> do
-        x <- getReservationAttribute version
-        pure (ReservationAttributeValue x)
-      RigidBodyStateAttributeType -> do
-        x <- getRigidBodyStateAttribute
-        pure (RigidBodyStateAttributeValue x)
-      StringAttributeType -> do
-        x <- getStringAttribute
-        pure (StringAttributeValue x)
-      TeamPaintAttributeType -> do
-        x <- getTeamPaintAttribute
-        pure (TeamPaintAttributeValue x)
-      UniqueIdAttributeType -> do
-        x <- getUniqueIdAttribute version
-        pure (UniqueIdAttributeValue x)
-      WeldedInfoAttributeType -> do
-        x <- getWeldedInfoAttribute
-        pure (WeldedInfoAttributeValue x)
-    Nothing -> fail ("don't know how to get attribute value " ++ show name)
-
-attributeTypes :: Map.Map Text AttributeType
-attributeTypes = Map.mapKeys stringToText (Map.fromList rawAttributeTypes)
-
-putAttributeValue :: AttributeValue -> BinaryBit.BitPut ()
-putAttributeValue value = case value of
-  AppliedDamageAttributeValue x -> putAppliedDamageAttribute x
-  BooleanAttributeValue x -> putBooleanAttribute x
-  ByteAttributeValue x -> putByteAttribute x
-  CamSettingsAttributeValue x -> putCamSettingsAttribute x
-  ClubColorsAttributeValue x -> putClubColorsAttribute x
-  DamageStateAttributeValue x -> putDamageStateAttribute x
-  DemolishAttributeValue x -> putDemolishAttribute x
-  EnumAttributeValue x -> putEnumAttribute x
-  ExplosionAttributeValue x -> putExplosionAttribute x
-  ExtendedExplosionAttributeValue x -> putExtendedExplosionAttribute x
-  FlaggedIntAttributeValue x -> putFlaggedIntAttribute x
-  FloatAttributeValue x -> putFloatAttribute x
-  GameModeAttributeValue x -> putGameModeAttribute x
-  IntAttributeValue x -> putIntAttribute x
-  LoadoutAttributeValue x -> putLoadoutAttribute x
-  LoadoutOnlineAttributeValue x -> putLoadoutOnlineAttribute x
-  LoadoutsAttributeValue x -> putLoadoutsAttribute x
-  LoadoutsOnlineAttributeValue x -> putLoadoutsOnlineAttribute x
-  LocationAttributeValue x -> putLocationAttribute x
-  MusicStingerAttributeValue x -> putMusicStingerAttribute x
-  PartyLeaderAttributeValue x -> putPartyLeaderAttribute x
-  PickupAttributeValue x -> putPickupAttribute x
-  PrivateMatchSettingsAttributeValue x -> putPrivateMatchSettingsAttribute x
-  QWordAttributeValue x -> putQWordAttribute x
-  ReservationAttributeValue x -> putReservationAttribute x
-  RigidBodyStateAttributeValue x -> putRigidBodyStateAttribute x
-  StringAttributeValue x -> putStringAttribute x
-  TeamPaintAttributeValue x -> putTeamPaintAttribute x
-  UniqueIdAttributeValue x -> putUniqueIdAttribute x
-  WeldedInfoAttributeValue x -> putWeldedInfoAttribute x
diff --git a/library/Rattletrap/Cache.hs b/library/Rattletrap/Cache.hs
deleted file mode 100644
--- a/library/Rattletrap/Cache.hs
+++ /dev/null
@@ -1,28 +0,0 @@
-module Rattletrap.Cache where
-
-import Rattletrap.AttributeMapping
-import Rattletrap.Primitive
-
-import qualified Data.Binary as Binary
-
-data Cache = Cache
-  { cacheClassId :: Word32
-  , cacheParentCacheId :: Word32
-  , cacheCacheId :: Word32
-  , cacheAttributeMappings :: List AttributeMapping
-  } deriving (Eq, Ord, Show)
-
-getCache :: Binary.Get Cache
-getCache = do
-  classId <- getWord32
-  parentCacheId <- getWord32
-  cacheId <- getWord32
-  attributeMappings <- getList getAttributeMapping
-  pure (Cache classId parentCacheId cacheId attributeMappings)
-
-putCache :: Cache -> Binary.Put
-putCache cache = do
-  putWord32 (cacheClassId cache)
-  putWord32 (cacheParentCacheId cache)
-  putWord32 (cacheCacheId cache)
-  putList putAttributeMapping (cacheAttributeMappings cache)
diff --git a/library/Rattletrap/ClassAttributeMap.hs b/library/Rattletrap/ClassAttributeMap.hs
deleted file mode 100644
--- a/library/Rattletrap/ClassAttributeMap.hs
+++ /dev/null
@@ -1,300 +0,0 @@
-module Rattletrap.ClassAttributeMap where
-
-import Rattletrap.ActorMap
-import Rattletrap.AttributeMapping
-import Rattletrap.Cache
-import Rattletrap.ClassMapping
-import Rattletrap.Data
-import Rattletrap.Primitive
-
-import qualified Data.Bimap as Bimap
-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.Set as Set
-import qualified Data.Text as Text
-
--- | This data structure holds all the information about classes, objects, and
--- attributes in the replay. The class hierarchy is not fixed; it is encoded
--- in the 'Rattletrap.Content.Content'. Similarly, the attributes that belong
--- to each class are not fixed either. Converting the raw data into a usable
--- structure is tedious; see 'makeClassAttributeMap'.
-data ClassAttributeMap = ClassAttributeMap
-  { classAttributeMapObjectMap :: Map.Map Word32 Text
-  -- ^ A map from object IDs to their names.
-  , classAttributeMapObjectClassMap :: Map.Map Word32 Word32
-  -- ^ A map from object IDs to their class IDs.
-  , classAttributeMapValue :: Map.Map Word32 (Map.Map Word32 Word32)
-  -- ^ A map from class IDs to a map from attribute stream IDs to attribute
-  -- IDs.
-  , classAttributeMapNameMap :: IntMap.IntMap Text
-  } deriving (Eq, Ord, Show)
-
--- | Makes a 'ClassAttributeMap' given the necessary fields from the
--- 'Rattletrap.Content.Content'.
-makeClassAttributeMap
-  :: List Text
-  -- ^ From 'Rattletrap.Content.contentObjects'.
-  -> List ClassMapping
-  -- ^ From 'Rattletrap.Content.contentClassMappings'.
-  -> List Cache
-  -- ^ From 'Rattletrap.Content.contentCaches'.
-  -> List Text
-  -- ^ From 'Rattletrap.Content.contentNames'.
-  -> ClassAttributeMap
-makeClassAttributeMap objects classMappings caches names =
-  let
-    objectMap = makeObjectMap objects
-    classMap = makeClassMap classMappings
-    objectClassMap = makeObjectClassMap objectMap classMap
-    classCache = makeClassCache classMap caches
-    attributeMap = makeAttributeMap caches
-    classIds = map (\(_, classId, _, _) -> classId) classCache
-    parentMap = makeParentMap classCache
-    value = Map.fromList
-      ( map
-        ( \classId ->
-          let
-            ownAttributes =
-              Maybe.fromMaybe Map.empty (Map.lookup classId attributeMap)
-            parentsAttributes = case Map.lookup classId parentMap of
-              Nothing -> []
-              Just parentClassIds -> map
-                ( \parentClassId -> Maybe.fromMaybe
-                  Map.empty
-                  (Map.lookup parentClassId attributeMap)
-                )
-                parentClassIds
-            attributes = ownAttributes : parentsAttributes
-          in (classId, Map.fromList (concatMap Map.toList attributes))
-        )
-        classIds
-      )
-    nameMap = makeNameMap names
-  in ClassAttributeMap objectMap objectClassMap value nameMap
-
-makeNameMap :: List Text -> IntMap.IntMap Text
-makeNameMap names = IntMap.fromDistinctAscList (zip [0 ..] (listValue names))
-
-getName :: IntMap.IntMap Text -> Word32 -> Maybe Text
-getName nameMap nameIndex =
-  IntMap.lookup (fromIntegral (word32Value nameIndex)) nameMap
-
-makeObjectClassMap
-  :: Map.Map Word32 Text -> Bimap.Bimap Word32 Text -> Map.Map Word32 Word32
-makeObjectClassMap objectMap classMap = do
-  let objectIds = Map.keys objectMap
-  let classIds = map (getClassId objectMap classMap) objectIds
-  let rawPairs = zip objectIds classIds
-  let
-    pairs = Maybe.mapMaybe
-      ( \(objectId, maybeClassId) -> case maybeClassId of
-        Nothing -> Nothing
-        Just classId -> Just (objectId, classId)
-      )
-      rawPairs
-  Map.fromList pairs
-
-getClassId
-  :: Map.Map Word32 Text -> Bimap.Bimap Word32 Text -> Word32 -> Maybe Word32
-getClassId objectMap classMap objectId = do
-  objectName <- getObjectName objectMap objectId
-  className <- getClassName objectName
-  Bimap.lookupR className classMap
-
-makeClassCache
-  :: Bimap.Bimap Word32 Text
-  -> List Cache
-  -> [(Maybe Text, Word32, Word32, Word32)]
-makeClassCache classMap caches = map
-  ( \cache ->
-    let
-      classId = cacheClassId cache
-    in
-      ( Bimap.lookup classId classMap
-      , classId
-      , cacheCacheId cache
-      , cacheParentCacheId cache
-      )
-  )
-  (listValue caches)
-
-makeClassMap :: List ClassMapping -> Bimap.Bimap Word32 Text
-makeClassMap classMappings = Bimap.fromList
-  ( map
-    ( \classMapping ->
-      (classMappingStreamId classMapping, classMappingName classMapping)
-    )
-    (listValue classMappings)
-  )
-
-makeAttributeMap :: List Cache -> Map.Map Word32 (Map.Map Word32 Word32)
-makeAttributeMap caches = Map.fromList
-  ( map
-    ( \cache ->
-      ( cacheClassId cache
-      , Map.fromList
-        ( map
-          ( \attributeMapping ->
-            ( attributeMappingStreamId attributeMapping
-            , attributeMappingObjectId attributeMapping
-            )
-          )
-          (listValue (cacheAttributeMappings cache))
-        )
-      )
-    )
-    (listValue caches)
-  )
-
-makeShallowParentMap
-  :: [(Maybe Text, Word32, Word32, Word32)] -> Map.Map Word32 Word32
-makeShallowParentMap classCache = Map.fromList
-  ( Maybe.mapMaybe
-    ( \xs -> case xs of
-      [] -> Nothing
-      (maybeClassName, classId, _, parentCacheId):rest -> do
-        parentClassId <- getParentClass maybeClassName parentCacheId rest
-        pure (classId, parentClassId)
-    )
-    (List.tails (reverse classCache))
-  )
-
-makeParentMap
-  :: [(Maybe Text, Word32, Word32, Word32)] -> Map.Map Word32 [Word32]
-makeParentMap classCache =
-  let
-    shallowParentMap = makeShallowParentMap classCache
-  in
-    Map.mapWithKey
-      (\classId _ -> getParentClasses shallowParentMap classId)
-      shallowParentMap
-
-getParentClasses :: Map.Map Word32 Word32 -> Word32 -> [Word32]
-getParentClasses shallowParentMap classId =
-  case Map.lookup classId shallowParentMap of
-    Nothing -> []
-    Just parentClassId ->
-      parentClassId : getParentClasses shallowParentMap parentClassId
-
-getParentClass
-  :: Maybe Text
-  -> Word32
-  -> [(Maybe Text, Word32, Word32, Word32)]
-  -> Maybe Word32
-getParentClass maybeClassName parentCacheId xs = case maybeClassName of
-  Nothing -> getParentClassById parentCacheId xs
-  Just className -> getParentClassByName className parentCacheId xs
-
-getParentClassById
-  :: Word32 -> [(Maybe Text, Word32, Word32, Word32)] -> Maybe Word32
-getParentClassById parentCacheId xs =
-  case dropWhile (\(_, _, cacheId, _) -> cacheId /= parentCacheId) xs of
-    [] -> if parentCacheId == Word32 0
-      then Nothing
-      else getParentClassById (Word32 (word32Value parentCacheId - 1)) xs
-    (_, parentClassId, _, _):_ -> Just parentClassId
-
-getParentClassByName
-  :: Text -> Word32 -> [(Maybe Text, Word32, Word32, Word32)] -> Maybe Word32
-getParentClassByName className parentCacheId xs =
-  case Map.lookup className parentClasses of
-    Nothing -> getParentClassById parentCacheId xs
-    Just parentClassName -> Maybe.maybe
-      (getParentClassById parentCacheId xs)
-      Just
-      ( Maybe.listToMaybe
-        ( map
-          (\(_, parentClassId, _, _) -> parentClassId)
-          ( filter
-            (\(_, _, cacheId, _) -> cacheId <= parentCacheId)
-            ( filter
-              ( \(maybeClassName, _, _, _) ->
-                maybeClassName == Just parentClassName
-              )
-              xs
-            )
-          )
-        )
-      )
-
-parentClasses :: Map.Map Text Text
-parentClasses = Map.map
-  stringToText
-  (Map.mapKeys stringToText (Map.fromList rawParentClasses))
-
-makeObjectMap :: List Text -> Map.Map Word32 Text
-makeObjectMap objects =
-  Map.fromAscList (zip (map Word32 [0 ..]) (listValue objects))
-
-getObjectName :: Map.Map Word32 Text -> Word32 -> Maybe Text
-getObjectName objectMap objectId = Map.lookup objectId objectMap
-
-getClassName :: Text -> Maybe Text
-getClassName rawObjectName =
-  Map.lookup (normalizeObjectName rawObjectName) objectClasses
-
-normalizeObjectName :: Text -> Text
-normalizeObjectName objectName =
-  let
-    name = textValue objectName
-    crowdActor = Text.pack "TheWorld:PersistentLevel.CrowdActor_TA"
-    crowdManager = Text.pack "TheWorld:PersistentLevel.CrowdManager_TA"
-    boostPickup = Text.pack "TheWorld:PersistentLevel.VehiclePickup_Boost_TA"
-    mapScoreboard = Text.pack "TheWorld:PersistentLevel.InMapScoreboard_TA"
-    breakout = Text.pack "TheWorld:PersistentLevel.BreakOutActor_Platform_TA"
-  in if Text.isInfixOf crowdActor name
-    then Text crowdActor
-    else if Text.isInfixOf crowdManager name
-      then Text crowdManager
-      else if Text.isInfixOf boostPickup name
-        then Text boostPickup
-        else if Text.isInfixOf mapScoreboard name
-          then Text mapScoreboard
-          else if Text.isInfixOf breakout name
-            then Text breakout
-            else objectName
-
-objectClasses :: Map.Map Text Text
-objectClasses = Map.map
-  stringToText
-  (Map.mapKeys stringToText (Map.fromList rawObjectClasses))
-
-classHasLocation :: Text -> Bool
-classHasLocation className = Set.member className classesWithLocation
-
-classesWithLocation :: Set.Set Text
-classesWithLocation = Set.fromList (map stringToText rawClassesWithLocation)
-
-classHasRotation :: Text -> Bool
-classHasRotation className = Set.member className classesWithRotation
-
-classesWithRotation :: Set.Set Text
-classesWithRotation = Set.fromList (map stringToText rawClassesWithRotation)
-
-getAttributeIdLimit :: Map.Map Word32 Word32 -> Maybe Word
-getAttributeIdLimit attributeMap = do
-  ((streamId, _), _) <- Map.maxViewWithKey attributeMap
-  let limit = fromIntegral (word32Value streamId)
-  pure limit
-
-getAttributeName
-  :: ClassAttributeMap -> Map.Map Word32 Word32 -> CompressedWord -> Maybe Text
-getAttributeName classAttributeMap attributeMap streamId = do
-  let key = Word32 (fromIntegral (compressedWordValue streamId))
-  attributeId <- Map.lookup key attributeMap
-  let objectMap = classAttributeMapObjectMap classAttributeMap
-  Map.lookup attributeId objectMap
-
-getAttributeMap
-  :: ClassAttributeMap
-  -> ActorMap
-  -> CompressedWord
-  -> Maybe (Map.Map Word32 Word32)
-getAttributeMap classAttributeMap actorMap actorId = do
-  objectId <- Map.lookup actorId actorMap
-  let objectClassMap = classAttributeMapObjectClassMap classAttributeMap
-  classId <- Map.lookup objectId objectClassMap
-  let value = classAttributeMapValue classAttributeMap
-  Map.lookup classId value
diff --git a/library/Rattletrap/ClassMapping.hs b/library/Rattletrap/ClassMapping.hs
deleted file mode 100644
--- a/library/Rattletrap/ClassMapping.hs
+++ /dev/null
@@ -1,21 +0,0 @@
-module Rattletrap.ClassMapping where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary as Binary
-
-data ClassMapping = ClassMapping
-  { classMappingName :: Text
-  , classMappingStreamId :: Word32
-  } deriving (Eq, Ord, Show)
-
-getClassMapping :: Binary.Get ClassMapping
-getClassMapping = do
-  name <- getText
-  streamId <- getWord32
-  pure (ClassMapping name streamId)
-
-putClassMapping :: ClassMapping -> Binary.Put
-putClassMapping classMapping = do
-  putText (classMappingName classMapping)
-  putWord32 (classMappingStreamId classMapping)
diff --git a/library/Rattletrap/Console/Main.hs b/library/Rattletrap/Console/Main.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Console/Main.hs
@@ -0,0 +1,226 @@
+module Rattletrap.Console.Main
+  ( main
+  , rattletrap
+  ) where
+
+import qualified Control.Monad as Monad
+import qualified Data.Aeson as Json
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.Version as Version
+import qualified Network.HTTP.Client as Client
+import qualified Network.HTTP.Client.TLS as Client
+import qualified Paths_rattletrap as This
+import qualified Rattletrap
+import qualified System.Console.GetOpt as Console
+import qualified System.Environment as Environment
+import qualified System.Exit as Exit
+import qualified System.FilePath as Path
+import qualified System.IO as IO
+import qualified Text.Printf as Printf
+
+main :: IO ()
+main = do
+  name <- Environment.getProgName
+  arguments <- Environment.getArgs
+  rattletrap name arguments
+
+rattletrap :: String -> [String] -> IO ()
+rattletrap name arguments = do
+  config <- getConfig arguments
+  Monad.when (configHelp config) (printHelp name *> Exit.exitFailure)
+  Monad.when (configVersion config) (printVersion *> Exit.exitFailure)
+  input <- getInput config
+  let decode = getDecoder config
+  replay <- either fail pure (decode input)
+  let encode = getEncoder config
+  putOutput config (encode replay)
+
+getDecoder :: Config -> LazyBytes.ByteString -> Either String Rattletrap.Replay
+getDecoder config = case getMode config of
+  ModeDecode -> Rattletrap.decodeReplayFile
+  ModeEncode -> Rattletrap.decodeReplayJson
+
+getEncoder :: Config -> Rattletrap.Replay -> LazyBytes.ByteString
+getEncoder config = case getMode config of
+  ModeDecode ->
+    if configCompact config then Json.encode else Rattletrap.encodeReplayJson
+  ModeEncode -> Rattletrap.encodeReplayFile
+
+getInput :: Config -> IO LazyBytes.ByteString
+getInput config = case configInput config of
+  Nothing -> LazyBytes.getContents
+  Just fileOrUrl -> case Client.parseUrlThrow fileOrUrl of
+    Nothing -> LazyBytes.readFile fileOrUrl
+    Just request -> do
+      manager <- Client.newTlsManager
+      response <- Client.httpLbs request manager
+      pure (Client.responseBody response)
+
+putOutput :: Config -> LazyBytes.ByteString -> IO ()
+putOutput config = case configOutput config of
+  Nothing -> LazyBytes.putStr
+  Just file -> LazyBytes.writeFile file
+
+getConfig :: [String] -> IO Config
+getConfig arguments = do
+  let
+    (updates, unexpectedArguments, unknownOptions, problems) =
+      Console.getOpt' Console.Permute options arguments
+  printUnexpectedArguments unexpectedArguments
+  printUnknownOptions unknownOptions
+  printProblems problems
+  Monad.unless (null problems) Exit.exitFailure
+  either fail pure (Monad.foldM applyUpdate defaultConfig updates)
+
+type Option = Console.OptDescr Update
+
+type Update = Config -> Either String Config
+
+options :: [Option]
+options =
+  [ compactOption
+  , helpOption
+  , inputOption
+  , modeOption
+  , outputOption
+  , versionOption
+  ]
+
+compactOption :: Option
+compactOption = Console.Option
+  ['c']
+  ["compact"]
+  (Console.NoArg (\config -> pure config { configCompact = True }))
+  "minify JSON output"
+
+helpOption :: Option
+helpOption = Console.Option
+  ['h']
+  ["help"]
+  (Console.NoArg (\config -> pure config { configHelp = True }))
+  "show the help"
+
+inputOption :: Option
+inputOption = Console.Option
+  ['i']
+  ["input"]
+  ( Console.ReqArg
+    (\input config -> pure config { configInput = Just input })
+    "FILE|URL"
+  )
+  "input file or URL"
+
+modeOption :: Option
+modeOption = Console.Option
+  ['m']
+  ["mode"]
+  ( Console.ReqArg
+    ( \rawMode config -> do
+      mode <- parseMode rawMode
+      pure config { configMode = Just mode }
+    )
+    "MODE"
+  )
+  "decode or encode"
+
+outputOption :: Option
+outputOption = Console.Option
+  ['o']
+  ["output"]
+  ( Console.ReqArg
+    (\output config -> pure config { configOutput = Just output })
+    "FILE"
+  )
+  "output file"
+
+versionOption :: Option
+versionOption = Console.Option
+  ['v']
+  ["version"]
+  (Console.NoArg (\config -> pure config { configVersion = True }))
+  "show the version"
+
+applyUpdate :: Config -> Update -> Either String Config
+applyUpdate config update = update config
+
+data Config = Config
+  { configCompact :: Bool
+  , configHelp :: Bool
+  , configInput :: Maybe String
+  , configMode :: Maybe Mode
+  , configOutput :: Maybe String
+  , configVersion :: Bool
+  } deriving (Show)
+
+defaultConfig :: Config
+defaultConfig = Config
+  { configCompact = False
+  , configHelp = False
+  , configInput = Nothing
+  , configMode = Nothing
+  , configOutput = Nothing
+  , configVersion = False
+  }
+
+getMode :: Config -> Mode
+getMode config = case getExtension (configInput config) of
+  ".json" -> ModeEncode
+  ".replay" -> ModeDecode
+  _ -> case getExtension (configOutput config) of
+    ".json" -> ModeDecode
+    ".replay" -> ModeEncode
+    _ -> ModeDecode
+
+getExtension :: Maybe String -> String
+getExtension = maybe "" Path.takeExtension
+
+data Mode
+  = ModeDecode
+  | ModeEncode
+  deriving (Show)
+
+parseMode :: String -> Either String Mode
+parseMode mode = case mode of
+  "decode" -> pure ModeDecode
+  "encode" -> pure ModeEncode
+  _ -> fail (Printf.printf "invalid mode: %s" (show mode))
+
+printUnexpectedArguments :: [String] -> IO ()
+printUnexpectedArguments = mapM_ printUnexpectedArgument
+
+printUnexpectedArgument :: String -> IO ()
+printUnexpectedArgument =
+  warnLn . Printf.printf "WARNING: unexpected argument `%s'"
+
+printUnknownOptions :: [String] -> IO ()
+printUnknownOptions = mapM_ printUnknownOption
+
+printUnknownOption :: String -> IO ()
+printUnknownOption = warnLn . Printf.printf "WARNING: unknown option `%s'"
+
+printProblems :: [String] -> IO ()
+printProblems = mapM_ printProblem
+
+printProblem :: String -> IO ()
+printProblem = warn . Printf.printf "ERROR: %s"
+
+printHelp :: String -> IO ()
+printHelp = warn . help
+
+help :: String -> String
+help name = Console.usageInfo (header name) options
+
+header :: String -> String
+header name = unwords [name, "version", version]
+
+version :: String
+version = Version.showVersion This.version
+
+printVersion :: IO ()
+printVersion = warnLn version
+
+warn :: String -> IO ()
+warn = IO.hPutStr IO.stderr
+
+warnLn :: String -> IO ()
+warnLn = IO.hPutStrLn IO.stderr
diff --git a/library/Rattletrap/Content.hs b/library/Rattletrap/Content.hs
deleted file mode 100644
--- a/library/Rattletrap/Content.hs
+++ /dev/null
@@ -1,125 +0,0 @@
-module Rattletrap.Content where
-
-import Rattletrap.Cache
-import Rattletrap.ClassAttributeMap
-import Rattletrap.ClassMapping
-import Rattletrap.Frame
-import Rattletrap.KeyFrame
-import Rattletrap.Mark
-import Rattletrap.Message
-import Rattletrap.Primitive
-import Rattletrap.Utility
-
-import qualified Data.Binary as Binary
-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.Map as Map
-
--- | Contains low-level game data about a 'Rattletrap.Replay.Replay'.
-data Content = Content
-  { contentLevels :: List Text
-  -- ^ This typically only has one element, like @stadium_oob_audio_map@.
-  , contentKeyFrames :: List KeyFrame
-  -- ^ A list of which frames are key frames. Although they aren't necessary
-  -- for replay, key frames are frames that replicate every actor. They
-  -- typically happen once every 10 seconds.
-  , contentStreamSize :: Word32
-  -- ^ The size of the stream in bytes. This is only really necessary because
-  -- the stream has some arbitrary amount of padding at the end.
-  , contentFrames :: [Frame]
-  -- ^ The actual game data. This is where all the interesting information is.
-  , contentMessages :: List Message
-  -- ^ Debugging messages. In newer replays, this is always empty.
-  , contentMarks :: List Mark
-  -- ^ Tick marks shown on the scrubber when watching a replay.
-  , contentPackages :: List Text
-  -- ^ A list of @.upk@ files to load, like
-  -- @..\\..\\TAGame\\CookedPCConsole\\Stadium_P.upk@.
-  , contentObjects :: List Text
-  -- ^ Objects in the stream. Used for the
-  -- 'Rattletrap.ClassAttributeMap.ClassAttributeMap'.
-  , contentNames :: List Text
-  -- ^ It's not clear what these are used for. This list is usually not empty,
-  -- but appears unused otherwise.
-  , contentClassMappings :: List ClassMapping
-  -- ^ A mapping between classes and their ID in the stream. Used for the
-  -- 'Rattletrap.ClassAttributeMap.ClassAttributeMap'.
-  , contentCaches :: List Cache
-  -- ^ A list of classes along with their parent classes and attributes. Used
-  -- for the 'Rattletrap.ClassAttributeMap.ClassAttributeMap'.
-  } deriving (Eq, Ord, Show)
-
-getContent
-  :: (Int, Int, Int)
-  -- ^ Version numbers, usually from 'Rattletrap.Header.getVersion'.
-  -> Int
-  -- ^ The number of frames in the stream, usually from
-  -- 'Rattletrap.Header.getNumFrames'.
-  -> Word
-  -- ^ The maximum number of channels in the stream, usually from
-  -- 'Rattletrap.Header.getMaxChannels'.
-  -> Binary.Get Content
-getContent version numFrames maxChannels = do
-  levels <- getList getText
-  keyFrames <- getList getKeyFrame
-  streamSize <- getWord32
-  stream <- Binary.getLazyByteString (fromIntegral (word32Value streamSize))
-  messages <- getList getMessage
-  marks <- getList getMark
-  packages <- getList getText
-  objects <- getList getText
-  names <- getList getText
-  classMappings <- getList getClassMapping
-  caches <- getList getCache
-  let
-    classAttributeMap =
-      makeClassAttributeMap objects classMappings caches names
-  let
-    frames = Binary.runGet
-      ( BinaryBit.runBitGet
-        ( do
-          (theFrames, _) <- getFrames
-            version
-            numFrames
-            maxChannels
-            classAttributeMap
-            Map.empty
-          pure theFrames
-        )
-      )
-      (reverseBytes stream)
-  pure
-    ( Content
-      levels
-      keyFrames
-      streamSize
-      frames
-      messages
-      marks
-      packages
-      objects
-      names
-      classMappings
-      caches
-    )
-
-putContent :: Content -> Binary.Put
-putContent content = do
-  putList putText (contentLevels content)
-  putList putKeyFrame (contentKeyFrames content)
-  let streamSize = contentStreamSize content
-  putWord32 streamSize
-  let
-    stream =
-      Binary.runPut (BinaryBit.runBitPut (putFrames (contentFrames content)))
-  Binary.putLazyByteString
-    (reverseBytes (padBytes (word32Value streamSize) stream))
-  putList putMessage (contentMessages content)
-  putList putMark (contentMarks content)
-  putList putText (contentPackages content)
-  putList putText (contentObjects content)
-  putList putText (contentNames content)
-  putList putClassMapping (contentClassMappings content)
-  putList putCache (contentCaches content)
diff --git a/library/Rattletrap/Crc.hs b/library/Rattletrap/Crc.hs
deleted file mode 100644
--- a/library/Rattletrap/Crc.hs
+++ /dev/null
@@ -1,40 +0,0 @@
-module Rattletrap.Crc where
-
-import Rattletrap.Data
-
-import qualified Data.Bits as Bits
-import qualified Data.ByteString.Lazy as ByteString
-import qualified Data.Vector.Unboxed as Vector
-import qualified Data.Word as Word
-
--- | Computes the CRC32 of some bytes. This is done to ensure that the bytes
--- are valid before trying to parse them.
---
--- @
--- getCrc32 ('Data.ByteString.Lazy.pack' [0x00])
--- @
---
--- This CRC uses an initial value of @0xefcbf201@ and a polynomial of
--- @0x04c11db7@.
-getCrc32 :: ByteString.ByteString -> Word.Word32
-getCrc32 bytes = do
-  let update = crc32Update crc32Table
-  let initial = Bits.complement crc32Initial
-  let crc = ByteString.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 rawCrc32Table
diff --git a/library/Rattletrap/Data.hs b/library/Rattletrap/Data.hs
--- a/library/Rattletrap/Data.hs
+++ b/library/Rattletrap/Data.hs
@@ -1,6 +1,6 @@
 module Rattletrap.Data where
 
-import Rattletrap.AttributeType
+import Rattletrap.Type.AttributeType
 
 rawParentClasses :: [(String, String)]
 rawParentClasses =
@@ -78,7 +78,6 @@
   , "TAGame.SpecialPickup_BallCarSpring_TA"
   , "TAGame.SpecialPickup_BallFreeze_TA"
   , "TAGame.SpecialPickup_BallGravity_TA"
-  , "TAGame.SpecialPickup_BallGravity_TA"
   , "TAGame.SpecialPickup_BallLasso_TA"
   , "TAGame.SpecialPickup_BallVelcro_TA"
   , "TAGame.SpecialPickup_Batarang_TA"
@@ -156,151 +155,152 @@
 
 rawAttributeTypes :: [(String, AttributeType)]
 rawAttributeTypes =
-  [ ("Engine.Actor:bBlockActors", BooleanAttributeType)
-  , ("Engine.Actor:bCollideActors", BooleanAttributeType)
-  , ("Engine.Actor:bHidden", BooleanAttributeType)
-  , ("Engine.Actor:DrawScale", FloatAttributeType)
-  , ("Engine.Actor:Role", EnumAttributeType)
-  , ("Engine.GameReplicationInfo:bMatchIsOver", BooleanAttributeType)
-  , ("Engine.GameReplicationInfo:GameClass", FlaggedIntAttributeType)
-  , ("Engine.GameReplicationInfo:ServerName", StringAttributeType)
-  , ("Engine.Pawn:PlayerReplicationInfo", FlaggedIntAttributeType)
-  , ("Engine.PlayerReplicationInfo:bBot", BooleanAttributeType)
-  , ("Engine.PlayerReplicationInfo:bIsSpectator", BooleanAttributeType)
-  , ("Engine.PlayerReplicationInfo:bReadyToPlay", BooleanAttributeType)
-  , ("Engine.PlayerReplicationInfo:bWaitingPlayer", BooleanAttributeType)
-  , ("Engine.PlayerReplicationInfo:Ping", ByteAttributeType)
-  , ("Engine.PlayerReplicationInfo:PlayerID", IntAttributeType)
-  , ("Engine.PlayerReplicationInfo:PlayerName", StringAttributeType)
-  , ("Engine.PlayerReplicationInfo:RemoteUserData", StringAttributeType)
-  , ("Engine.PlayerReplicationInfo:Score", IntAttributeType)
-  , ("Engine.PlayerReplicationInfo:Team", FlaggedIntAttributeType)
-  , ("Engine.PlayerReplicationInfo:UniqueId", UniqueIdAttributeType)
-  , ("Engine.TeamInfo:Score", IntAttributeType)
-  , ("ProjectX.GRI_X:bGameStarted", BooleanAttributeType)
-  , ("ProjectX.GRI_X:GameServerID", QWordAttributeType)
-  , ("ProjectX.GRI_X:MatchGUID", StringAttributeType)
-  , ("ProjectX.GRI_X:ReplicatedGameMutatorIndex", IntAttributeType)
-  , ("ProjectX.GRI_X:ReplicatedGamePlaylist", IntAttributeType)
-  , ("ProjectX.GRI_X:Reservations", ReservationAttributeType)
-  , ("TAGame.Ball_Breakout_TA:AppliedDamage", AppliedDamageAttributeType)
-  , ("TAGame.Ball_Breakout_TA:DamageIndex", IntAttributeType)
-  , ("TAGame.Ball_Breakout_TA:LastTeamTouch", ByteAttributeType)
-  , ("TAGame.Ball_TA:GameEvent", FlaggedIntAttributeType)
-  , ("TAGame.Ball_TA:HitTeamNum", ByteAttributeType)
-  , ("TAGame.Ball_TA:ReplicatedAddedCarBounceScale", FloatAttributeType)
-  , ("TAGame.Ball_TA:ReplicatedBallMaxLinearSpeedScale", FloatAttributeType)
-  , ("TAGame.Ball_TA:ReplicatedBallScale", FloatAttributeType)
-  , ("TAGame.Ball_TA:ReplicatedExplosionData", ExplosionAttributeType)
-  , ("TAGame.Ball_TA:ReplicatedExplosionDataExtended", ExtendedExplosionAttributeType)
-  , ("TAGame.Ball_TA:ReplicatedWorldBounceScale", FloatAttributeType)
-  , ("TAGame.BreakOutActor_Platform_TA:DamageState", DamageStateAttributeType)
-  , ("TAGame.CameraSettingsActor_TA:bUsingBehindView", BooleanAttributeType)
-  , ("TAGame.CameraSettingsActor_TA:bUsingSecondaryCamera", BooleanAttributeType)
-  , ("TAGame.CameraSettingsActor_TA:CameraPitch", ByteAttributeType)
-  , ("TAGame.CameraSettingsActor_TA:CameraYaw", ByteAttributeType)
-  , ("TAGame.CameraSettingsActor_TA:PRI", FlaggedIntAttributeType)
-  , ("TAGame.CameraSettingsActor_TA:ProfileSettings", CamSettingsAttributeType)
-  , ("TAGame.Car_TA:AddedBallForceMultiplier", FloatAttributeType)
-  , ("TAGame.Car_TA:AddedCarForceMultiplier", FloatAttributeType)
-  , ("TAGame.Car_TA:AttachedPickup", FlaggedIntAttributeType)
-  , ("TAGame.Car_TA:ClubColors", ClubColorsAttributeType)
-  , ("TAGame.Car_TA:ReplicatedDemolish", DemolishAttributeType)
-  , ("TAGame.Car_TA:TeamPaint", TeamPaintAttributeType)
-  , ("TAGame.CarComponent_Boost_TA:bNoBoost", BooleanAttributeType)
-  , ("TAGame.CarComponent_Boost_TA:BoostModifier", FloatAttributeType)
-  , ("TAGame.CarComponent_Boost_TA:bUnlimitedBoost", BooleanAttributeType)
-  , ("TAGame.CarComponent_Boost_TA:RechargeDelay", FloatAttributeType)
-  , ("TAGame.CarComponent_Boost_TA:RechargeRate", FloatAttributeType)
-  , ("TAGame.CarComponent_Boost_TA:ReplicatedBoostAmount", ByteAttributeType)
-  , ("TAGame.CarComponent_Boost_TA:UnlimitedBoostRefCount", IntAttributeType)
-  , ("TAGame.CarComponent_Dodge_TA:DodgeTorque", LocationAttributeType)
-  , ("TAGame.CarComponent_FlipCar_TA:bFlipRight", BooleanAttributeType)
-  , ("TAGame.CarComponent_FlipCar_TA:FlipCarTime", FloatAttributeType)
-  , ("TAGame.CarComponent_TA:ReplicatedActive", ByteAttributeType)
-  , ("TAGame.CarComponent_TA:ReplicatedActivityTime", FloatAttributeType)
-  , ("TAGame.CarComponent_TA:Vehicle", FlaggedIntAttributeType)
-  , ("TAGame.CrowdActor_TA:GameEvent", FlaggedIntAttributeType)
-  , ("TAGame.CrowdActor_TA:ModifiedNoise", FloatAttributeType)
-  , ("TAGame.CrowdActor_TA:ReplicatedCountDownNumber", IntAttributeType)
-  , ("TAGame.CrowdActor_TA:ReplicatedOneShotSound", FlaggedIntAttributeType)
-  , ("TAGame.CrowdActor_TA:ReplicatedRoundCountDownNumber", IntAttributeType)
-  , ("TAGame.CrowdManager_TA:GameEvent", FlaggedIntAttributeType)
-  , ("TAGame.CrowdManager_TA:ReplicatedGlobalOneShotSound", FlaggedIntAttributeType)
-  , ("TAGame.GameEvent_Soccar_TA:bBallHasBeenHit", BooleanAttributeType)
-  , ("TAGame.GameEvent_Soccar_TA:bOverTime", BooleanAttributeType)
-  , ("TAGame.GameEvent_Soccar_TA:GameTime", IntAttributeType)
-  , ("TAGame.GameEvent_Soccar_TA:ReplicatedMusicStinger", MusicStingerAttributeType)
-  , ("TAGame.GameEvent_Soccar_TA:ReplicatedScoredOnTeam", ByteAttributeType)
-  , ("TAGame.GameEvent_Soccar_TA:RoundNum", IntAttributeType)
-  , ("TAGame.GameEvent_Soccar_TA:SecondsRemaining", IntAttributeType)
-  , ("TAGame.GameEvent_Soccar_TA:SubRulesArchetype", FlaggedIntAttributeType)
-  , ("TAGame.GameEvent_SoccarPrivate_TA:MatchSettings", PrivateMatchSettingsAttributeType)
-  , ("TAGame.GameEvent_TA:bCanVoteToForfeit", BooleanAttributeType)
-  , ("TAGame.GameEvent_TA:bHasLeaveMatchPenalty", BooleanAttributeType)
-  , ("TAGame.GameEvent_TA:BotSkill", IntAttributeType)
-  , ("TAGame.GameEvent_TA:GameMode", GameModeAttributeType)
-  , ("TAGame.GameEvent_TA:MatchTypeClass", FlaggedIntAttributeType)
-  , ("TAGame.GameEvent_TA:ReplicatedGameStateTimeRemaining", IntAttributeType)
-  , ("TAGame.GameEvent_TA:ReplicatedStateIndex", ByteAttributeType)
-  , ("TAGame.GameEvent_TA:ReplicatedStateName", IntAttributeType)
-  , ("TAGame.GameEvent_Team_TA:bForfeit", BooleanAttributeType)
-  , ("TAGame.GameEvent_Team_TA:MaxTeamSize", IntAttributeType)
-  , ("TAGame.GRI_TA:NewDedicatedServerIP", StringAttributeType)
-  , ("TAGame.PRI_TA:bIsInSplitScreen", BooleanAttributeType)
-  , ("TAGame.PRI_TA:bMatchMVP", BooleanAttributeType)
-  , ("TAGame.PRI_TA:bOnlineLoadoutSet", BooleanAttributeType)
-  , ("TAGame.PRI_TA:bOnlineLoadoutsSet", BooleanAttributeType)
-  , ("TAGame.PRI_TA:BotProductName", IntAttributeType)
-  , ("TAGame.PRI_TA:bReady", BooleanAttributeType)
-  , ("TAGame.PRI_TA:bUsingBehindView", BooleanAttributeType)
-  , ("TAGame.PRI_TA:bUsingItems", BooleanAttributeType)
-  , ("TAGame.PRI_TA:bUsingSecondaryCamera", BooleanAttributeType)
-  , ("TAGame.PRI_TA:CameraPitch", ByteAttributeType)
-  , ("TAGame.PRI_TA:CameraSettings", CamSettingsAttributeType)
-  , ("TAGame.PRI_TA:CameraYaw", ByteAttributeType)
-  , ("TAGame.PRI_TA:ClientLoadout", LoadoutAttributeType)
-  , ("TAGame.PRI_TA:ClientLoadoutOnline", LoadoutOnlineAttributeType)
-  , ("TAGame.PRI_TA:ClientLoadouts", LoadoutsAttributeType)
-  , ("TAGame.PRI_TA:ClientLoadoutsOnline", LoadoutsOnlineAttributeType)
-  , ("TAGame.PRI_TA:MatchAssists", IntAttributeType)
-  , ("TAGame.PRI_TA:MatchBreakoutDamage", IntAttributeType)
-  , ("TAGame.PRI_TA:MatchGoals", IntAttributeType)
-  , ("TAGame.PRI_TA:MatchSaves", IntAttributeType)
-  , ("TAGame.PRI_TA:MatchScore", IntAttributeType)
-  , ("TAGame.PRI_TA:MatchShots", IntAttributeType)
-  , ("TAGame.PRI_TA:MaxTimeTillItem", IntAttributeType)
-  , ("TAGame.PRI_TA:PartyLeader", PartyLeaderAttributeType)
-  , ("TAGame.PRI_TA:PawnType", ByteAttributeType)
-  , ("TAGame.PRI_TA:PersistentCamera", FlaggedIntAttributeType)
-  , ("TAGame.PRI_TA:PlayerHistoryValid", BooleanAttributeType)
-  , ("TAGame.PRI_TA:ReplicatedGameEvent", FlaggedIntAttributeType)
-  , ("TAGame.PRI_TA:SteeringSensitivity", FloatAttributeType)
-  , ("TAGame.PRI_TA:TimeTillItem", IntAttributeType)
-  , ("TAGame.PRI_TA:Title", IntAttributeType)
-  , ("TAGame.PRI_TA:TotalXP", IntAttributeType)
-  , ("TAGame.RBActor_TA:bFrozen", BooleanAttributeType)
-  , ("TAGame.RBActor_TA:bIgnoreSyncing", BooleanAttributeType)
-  , ("TAGame.RBActor_TA:bReplayActor", BooleanAttributeType)
-  , ("TAGame.RBActor_TA:ReplicatedRBState", RigidBodyStateAttributeType)
-  , ("TAGame.RBActor_TA:WeldedInfo", WeldedInfoAttributeType)
-  , ("TAGame.SpecialPickup_BallFreeze_TA:RepOrigSpeed", FloatAttributeType)
-  , ("TAGame.SpecialPickup_BallVelcro_TA:AttachTime", FloatAttributeType)
-  , ("TAGame.SpecialPickup_BallVelcro_TA:bBroken", BooleanAttributeType)
-  , ("TAGame.SpecialPickup_BallVelcro_TA:bHit", BooleanAttributeType)
-  , ("TAGame.SpecialPickup_BallVelcro_TA:BreakTime", FloatAttributeType)
-  , ("TAGame.SpecialPickup_Targeted_TA:Targeted", FlaggedIntAttributeType)
-  , ("TAGame.Team_Soccar_TA:GameScore", IntAttributeType)
-  , ("TAGame.Team_TA:ClubColors", ClubColorsAttributeType)
-  , ("TAGame.Team_TA:CustomTeamName", StringAttributeType)
-  , ("TAGame.Team_TA:GameEvent", FlaggedIntAttributeType)
-  , ("TAGame.Team_TA:LogoData", FlaggedIntAttributeType)
-  , ("TAGame.Vehicle_TA:bDriving", BooleanAttributeType)
-  , ("TAGame.Vehicle_TA:bReplicatedHandbrake", BooleanAttributeType)
-  , ("TAGame.Vehicle_TA:ReplicatedSteer", ByteAttributeType)
-  , ("TAGame.Vehicle_TA:ReplicatedThrottle", ByteAttributeType)
-  , ("TAGame.VehiclePickup_TA:bNoPickup", BooleanAttributeType)
-  , ("TAGame.VehiclePickup_TA:ReplicatedPickupData", PickupAttributeType)
+  [ ("Engine.Actor:bBlockActors", AttributeTypeBoolean)
+  , ("Engine.Actor:bCollideActors", AttributeTypeBoolean)
+  , ("Engine.Actor:bHidden", AttributeTypeBoolean)
+  , ("Engine.Actor:DrawScale", AttributeTypeFloat)
+  , ("Engine.Actor:Role", AttributeTypeEnum)
+  , ("Engine.GameReplicationInfo:bMatchIsOver", AttributeTypeBoolean)
+  , ("Engine.GameReplicationInfo:GameClass", AttributeTypeFlaggedInt)
+  , ("Engine.GameReplicationInfo:ServerName", AttributeTypeString)
+  , ("Engine.Pawn:PlayerReplicationInfo", AttributeTypeFlaggedInt)
+  , ("Engine.PlayerReplicationInfo:bBot", AttributeTypeBoolean)
+  , ("Engine.PlayerReplicationInfo:bIsSpectator", AttributeTypeBoolean)
+  , ("Engine.PlayerReplicationInfo:bReadyToPlay", AttributeTypeBoolean)
+  , ("Engine.PlayerReplicationInfo:bWaitingPlayer", AttributeTypeBoolean)
+  , ("Engine.PlayerReplicationInfo:Ping", AttributeTypeByte)
+  , ("Engine.PlayerReplicationInfo:PlayerID", AttributeTypeInt)
+  , ("Engine.PlayerReplicationInfo:PlayerName", AttributeTypeString)
+  , ("Engine.PlayerReplicationInfo:RemoteUserData", AttributeTypeString)
+  , ("Engine.PlayerReplicationInfo:Score", AttributeTypeInt)
+  , ("Engine.PlayerReplicationInfo:Team", AttributeTypeFlaggedInt)
+  , ("Engine.PlayerReplicationInfo:UniqueId", AttributeTypeUniqueId)
+  , ("Engine.TeamInfo:Score", AttributeTypeInt)
+  , ("ProjectX.GRI_X:bGameStarted", AttributeTypeBoolean)
+  , ("ProjectX.GRI_X:GameServerID", AttributeTypeQWord)
+  , ("ProjectX.GRI_X:MatchGUID", AttributeTypeString)
+  , ("ProjectX.GRI_X:ReplicatedGameMutatorIndex", AttributeTypeInt)
+  , ("ProjectX.GRI_X:ReplicatedGamePlaylist", AttributeTypeInt)
+  , ("ProjectX.GRI_X:Reservations", AttributeTypeReservation)
+  , ("TAGame.Ball_Breakout_TA:AppliedDamage", AttributeTypeAppliedDamage)
+  , ("TAGame.Ball_Breakout_TA:DamageIndex", AttributeTypeInt)
+  , ("TAGame.Ball_Breakout_TA:LastTeamTouch", AttributeTypeByte)
+  , ("TAGame.Ball_TA:GameEvent", AttributeTypeFlaggedInt)
+  , ("TAGame.Ball_TA:HitTeamNum", AttributeTypeByte)
+  , ("TAGame.Ball_TA:ReplicatedAddedCarBounceScale", AttributeTypeFloat)
+  , ("TAGame.Ball_TA:ReplicatedBallMaxLinearSpeedScale", AttributeTypeFloat)
+  , ("TAGame.Ball_TA:ReplicatedBallScale", AttributeTypeFloat)
+  , ("TAGame.Ball_TA:ReplicatedExplosionData", AttributeTypeExplosion)
+  , ("TAGame.Ball_TA:ReplicatedExplosionDataExtended", AttributeTypeExtendedExplosion)
+  , ("TAGame.Ball_TA:ReplicatedWorldBounceScale", AttributeTypeFloat)
+  , ("TAGame.BreakOutActor_Platform_TA:DamageState", AttributeTypeDamageState)
+  , ("TAGame.CameraSettingsActor_TA:bUsingBehindView", AttributeTypeBoolean)
+  , ("TAGame.CameraSettingsActor_TA:bUsingSecondaryCamera", AttributeTypeBoolean)
+  , ("TAGame.CameraSettingsActor_TA:CameraPitch", AttributeTypeByte)
+  , ("TAGame.CameraSettingsActor_TA:CameraYaw", AttributeTypeByte)
+  , ("TAGame.CameraSettingsActor_TA:PRI", AttributeTypeFlaggedInt)
+  , ("TAGame.CameraSettingsActor_TA:ProfileSettings", AttributeTypeCamSettings)
+  , ("TAGame.Car_TA:AddedBallForceMultiplier", AttributeTypeFloat)
+  , ("TAGame.Car_TA:AddedCarForceMultiplier", AttributeTypeFloat)
+  , ("TAGame.Car_TA:AttachedPickup", AttributeTypeFlaggedInt)
+  , ("TAGame.Car_TA:ClubColors", AttributeTypeClubColors)
+  , ("TAGame.Car_TA:ReplicatedDemolish", AttributeTypeDemolish)
+  , ("TAGame.Car_TA:TeamPaint", AttributeTypeTeamPaint)
+  , ("TAGame.CarComponent_Boost_TA:bNoBoost", AttributeTypeBoolean)
+  , ("TAGame.CarComponent_Boost_TA:BoostModifier", AttributeTypeFloat)
+  , ("TAGame.CarComponent_Boost_TA:bUnlimitedBoost", AttributeTypeBoolean)
+  , ("TAGame.CarComponent_Boost_TA:RechargeDelay", AttributeTypeFloat)
+  , ("TAGame.CarComponent_Boost_TA:RechargeRate", AttributeTypeFloat)
+  , ("TAGame.CarComponent_Boost_TA:ReplicatedBoostAmount", AttributeTypeByte)
+  , ("TAGame.CarComponent_Boost_TA:UnlimitedBoostRefCount", AttributeTypeInt)
+  , ("TAGame.CarComponent_Dodge_TA:DodgeTorque", AttributeTypeLocation)
+  , ("TAGame.CarComponent_FlipCar_TA:bFlipRight", AttributeTypeBoolean)
+  , ("TAGame.CarComponent_FlipCar_TA:FlipCarTime", AttributeTypeFloat)
+  , ("TAGame.CarComponent_TA:ReplicatedActive", AttributeTypeByte)
+  , ("TAGame.CarComponent_TA:ReplicatedActivityTime", AttributeTypeFloat)
+  , ("TAGame.CarComponent_TA:Vehicle", AttributeTypeFlaggedInt)
+  , ("TAGame.CrowdActor_TA:GameEvent", AttributeTypeFlaggedInt)
+  , ("TAGame.CrowdActor_TA:ModifiedNoise", AttributeTypeFloat)
+  , ("TAGame.CrowdActor_TA:ReplicatedCountDownNumber", AttributeTypeInt)
+  , ("TAGame.CrowdActor_TA:ReplicatedOneShotSound", AttributeTypeFlaggedInt)
+  , ("TAGame.CrowdActor_TA:ReplicatedRoundCountDownNumber", AttributeTypeInt)
+  , ("TAGame.CrowdManager_TA:GameEvent", AttributeTypeFlaggedInt)
+  , ("TAGame.CrowdManager_TA:ReplicatedGlobalOneShotSound", AttributeTypeFlaggedInt)
+  , ("TAGame.GameEvent_Soccar_TA:bBallHasBeenHit", AttributeTypeBoolean)
+  , ("TAGame.GameEvent_Soccar_TA:bOverTime", AttributeTypeBoolean)
+  , ("TAGame.GameEvent_Soccar_TA:GameTime", AttributeTypeInt)
+  , ("TAGame.GameEvent_Soccar_TA:ReplicatedMusicStinger", AttributeTypeMusicStinger)
+  , ("TAGame.GameEvent_Soccar_TA:ReplicatedScoredOnTeam", AttributeTypeByte)
+  , ("TAGame.GameEvent_Soccar_TA:RoundNum", AttributeTypeInt)
+  , ("TAGame.GameEvent_Soccar_TA:SecondsRemaining", AttributeTypeInt)
+  , ("TAGame.GameEvent_Soccar_TA:SubRulesArchetype", AttributeTypeFlaggedInt)
+  , ("TAGame.GameEvent_SoccarPrivate_TA:MatchSettings", AttributeTypePrivateMatchSettings)
+  , ("TAGame.GameEvent_TA:bCanVoteToForfeit", AttributeTypeBoolean)
+  , ("TAGame.GameEvent_TA:bHasLeaveMatchPenalty", AttributeTypeBoolean)
+  , ("TAGame.GameEvent_TA:BotSkill", AttributeTypeInt)
+  , ("TAGame.GameEvent_TA:GameMode", AttributeTypeGameMode)
+  , ("TAGame.GameEvent_TA:MatchTypeClass", AttributeTypeFlaggedInt)
+  , ("TAGame.GameEvent_TA:ReplicatedGameStateTimeRemaining", AttributeTypeInt)
+  , ("TAGame.GameEvent_TA:ReplicatedStateIndex", AttributeTypeByte)
+  , ("TAGame.GameEvent_TA:ReplicatedStateName", AttributeTypeInt)
+  , ("TAGame.GameEvent_Team_TA:bForfeit", AttributeTypeBoolean)
+  , ("TAGame.GameEvent_Team_TA:MaxTeamSize", AttributeTypeInt)
+  , ("TAGame.GRI_TA:NewDedicatedServerIP", AttributeTypeString)
+  , ("TAGame.PRI_TA:bIsInSplitScreen", AttributeTypeBoolean)
+  , ("TAGame.PRI_TA:bMatchMVP", AttributeTypeBoolean)
+  , ("TAGame.PRI_TA:bOnlineLoadoutSet", AttributeTypeBoolean)
+  , ("TAGame.PRI_TA:bOnlineLoadoutsSet", AttributeTypeBoolean)
+  , ("TAGame.PRI_TA:BotProductName", AttributeTypeInt)
+  , ("TAGame.PRI_TA:bReady", AttributeTypeBoolean)
+  , ("TAGame.PRI_TA:bUsingBehindView", AttributeTypeBoolean)
+  , ("TAGame.PRI_TA:bUsingItems", AttributeTypeBoolean)
+  , ("TAGame.PRI_TA:bUsingSecondaryCamera", AttributeTypeBoolean)
+  , ("TAGame.PRI_TA:CameraPitch", AttributeTypeByte)
+  , ("TAGame.PRI_TA:CameraSettings", AttributeTypeCamSettings)
+  , ("TAGame.PRI_TA:CameraYaw", AttributeTypeByte)
+  , ("TAGame.PRI_TA:ClientLoadout", AttributeTypeLoadout)
+  , ("TAGame.PRI_TA:ClientLoadoutOnline", AttributeTypeLoadoutOnline)
+  , ("TAGame.PRI_TA:ClientLoadouts", AttributeTypeLoadouts)
+  , ("TAGame.PRI_TA:ClientLoadoutsOnline", AttributeTypeLoadoutsOnline)
+  , ("TAGame.PRI_TA:MatchAssists", AttributeTypeInt)
+  , ("TAGame.PRI_TA:MatchBreakoutDamage", AttributeTypeInt)
+  , ("TAGame.PRI_TA:MatchGoals", AttributeTypeInt)
+  , ("TAGame.PRI_TA:MatchSaves", AttributeTypeInt)
+  , ("TAGame.PRI_TA:MatchScore", AttributeTypeInt)
+  , ("TAGame.PRI_TA:MatchShots", AttributeTypeInt)
+  , ("TAGame.PRI_TA:MaxTimeTillItem", AttributeTypeInt)
+  , ("TAGame.PRI_TA:PartyLeader", AttributeTypePartyLeader)
+  , ("TAGame.PRI_TA:PawnType", AttributeTypeByte)
+  , ("TAGame.PRI_TA:PersistentCamera", AttributeTypeFlaggedInt)
+  , ("TAGame.PRI_TA:PlayerHistoryKey", AttributeTypePlayerHistoryKey)
+  , ("TAGame.PRI_TA:PlayerHistoryValid", AttributeTypeBoolean)
+  , ("TAGame.PRI_TA:ReplicatedGameEvent", AttributeTypeFlaggedInt)
+  , ("TAGame.PRI_TA:SteeringSensitivity", AttributeTypeFloat)
+  , ("TAGame.PRI_TA:TimeTillItem", AttributeTypeInt)
+  , ("TAGame.PRI_TA:Title", AttributeTypeInt)
+  , ("TAGame.PRI_TA:TotalXP", AttributeTypeInt)
+  , ("TAGame.RBActor_TA:bFrozen", AttributeTypeBoolean)
+  , ("TAGame.RBActor_TA:bIgnoreSyncing", AttributeTypeBoolean)
+  , ("TAGame.RBActor_TA:bReplayActor", AttributeTypeBoolean)
+  , ("TAGame.RBActor_TA:ReplicatedRBState", AttributeTypeRigidBodyState)
+  , ("TAGame.RBActor_TA:WeldedInfo", AttributeTypeWeldedInfo)
+  , ("TAGame.SpecialPickup_BallFreeze_TA:RepOrigSpeed", AttributeTypeFloat)
+  , ("TAGame.SpecialPickup_BallVelcro_TA:AttachTime", AttributeTypeFloat)
+  , ("TAGame.SpecialPickup_BallVelcro_TA:bBroken", AttributeTypeBoolean)
+  , ("TAGame.SpecialPickup_BallVelcro_TA:bHit", AttributeTypeBoolean)
+  , ("TAGame.SpecialPickup_BallVelcro_TA:BreakTime", AttributeTypeFloat)
+  , ("TAGame.SpecialPickup_Targeted_TA:Targeted", AttributeTypeFlaggedInt)
+  , ("TAGame.Team_Soccar_TA:GameScore", AttributeTypeInt)
+  , ("TAGame.Team_TA:ClubColors", AttributeTypeClubColors)
+  , ("TAGame.Team_TA:CustomTeamName", AttributeTypeString)
+  , ("TAGame.Team_TA:GameEvent", AttributeTypeFlaggedInt)
+  , ("TAGame.Team_TA:LogoData", AttributeTypeFlaggedInt)
+  , ("TAGame.Vehicle_TA:bDriving", AttributeTypeBoolean)
+  , ("TAGame.Vehicle_TA:bReplicatedHandbrake", AttributeTypeBoolean)
+  , ("TAGame.Vehicle_TA:ReplicatedSteer", AttributeTypeByte)
+  , ("TAGame.Vehicle_TA:ReplicatedThrottle", AttributeTypeByte)
+  , ("TAGame.VehiclePickup_TA:bNoPickup", AttributeTypeBoolean)
+  , ("TAGame.VehiclePickup_TA:ReplicatedPickupData", AttributeTypePickup)
   ]
 
 rawCrc32Table :: Integral a => [a]
diff --git a/library/Rattletrap/Decode/AppliedDamageAttribute.hs b/library/Rattletrap/Decode/AppliedDamageAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/AppliedDamageAttribute.hs
@@ -0,0 +1,17 @@
+module Rattletrap.Decode.AppliedDamageAttribute
+  ( decodeAppliedDamageAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Int32le
+import Rattletrap.Decode.Vector
+import Rattletrap.Decode.Word8le
+import Rattletrap.Type.AppliedDamageAttribute
+
+decodeAppliedDamageAttributeBits :: DecodeBits AppliedDamageAttribute
+decodeAppliedDamageAttributeBits =
+  AppliedDamageAttribute
+    <$> decodeWord8leBits
+    <*> decodeVectorBits
+    <*> decodeInt32leBits
+    <*> decodeInt32leBits
diff --git a/library/Rattletrap/Decode/Attribute.hs b/library/Rattletrap/Decode/Attribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Attribute.hs
@@ -0,0 +1,74 @@
+module Rattletrap.Decode.Attribute
+  ( decodeAttributesBits
+  , decodeAttributeBits
+  ) where
+
+import Data.Semigroup ((<>))
+import Rattletrap.Decode.AttributeValue
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.CompressedWord
+import Rattletrap.Type.Attribute
+import Rattletrap.Type.ClassAttributeMap
+import Rattletrap.Type.Common
+import Rattletrap.Type.CompressedWord
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word32le
+
+decodeAttributesBits
+  :: (Int, Int, Int)
+  -> ClassAttributeMap
+  -> Map CompressedWord Word32le
+  -> CompressedWord
+  -> DecodeBits [Attribute]
+decodeAttributesBits version classes actors actor = do
+  hasAttribute <- getBool
+  if hasAttribute
+    then
+      (:)
+      <$> decodeAttributeBits version classes actors actor
+      <*> decodeAttributesBits version classes actors actor
+    else pure []
+
+decodeAttributeBits
+  :: (Int, Int, Int)
+  -> ClassAttributeMap
+  -> Map CompressedWord Word32le
+  -> CompressedWord
+  -> DecodeBits Attribute
+decodeAttributeBits version classes actors actor = do
+  attributes <- lookupAttributeMap classes actors actor
+  limit <- lookupAttributeIdLimit attributes actor
+  attribute <- decodeCompressedWordBits limit
+  name <- lookupAttributeName classes attributes attribute
+  Attribute attribute name
+    <$> decodeAttributeValueBits
+          version
+          (classAttributeMapObjectMap classes)
+          name
+
+lookupAttributeMap
+  :: ClassAttributeMap
+  -> Map CompressedWord Word32le
+  -> CompressedWord
+  -> DecodeBits (Map Word32le Word32le)
+lookupAttributeMap classes actors actor = fromMaybe
+  ("could not get attribute map for " <> show actor)
+  (getAttributeMap classes actors actor)
+
+lookupAttributeIdLimit
+  :: Map Word32le Word32le -> CompressedWord -> DecodeBits Word
+lookupAttributeIdLimit attributes actor = fromMaybe
+  ("could not get attribute ID limit for " <> show actor)
+  (getAttributeIdLimit attributes)
+
+lookupAttributeName
+  :: ClassAttributeMap
+  -> Map Word32le Word32le
+  -> CompressedWord
+  -> DecodeBits Str
+lookupAttributeName classes attributes attribute = fromMaybe
+  ("could not get attribute name for " <> show attribute)
+  (getAttributeName classes attributes attribute)
+
+fromMaybe :: String -> Maybe a -> DecodeBits a
+fromMaybe message = maybe (fail message) pure
diff --git a/library/Rattletrap/Decode/AttributeMapping.hs b/library/Rattletrap/Decode/AttributeMapping.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/AttributeMapping.hs
@@ -0,0 +1,11 @@
+module Rattletrap.Decode.AttributeMapping
+  ( decodeAttributeMapping
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Word32le
+import Rattletrap.Type.AttributeMapping
+
+decodeAttributeMapping :: Decode AttributeMapping
+decodeAttributeMapping =
+  AttributeMapping <$> decodeWord32le <*> decodeWord32le
diff --git a/library/Rattletrap/Decode/AttributeValue.hs b/library/Rattletrap/Decode/AttributeValue.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/AttributeValue.hs
@@ -0,0 +1,115 @@
+module Rattletrap.Decode.AttributeValue
+  ( decodeAttributeValueBits
+  ) where
+
+import Data.Semigroup ((<>))
+import Rattletrap.Data
+import Rattletrap.Decode.AppliedDamageAttribute
+import Rattletrap.Decode.BooleanAttribute
+import Rattletrap.Decode.ByteAttribute
+import Rattletrap.Decode.CamSettingsAttribute
+import Rattletrap.Decode.ClubColorsAttribute
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.DamageStateAttribute
+import Rattletrap.Decode.DemolishAttribute
+import Rattletrap.Decode.EnumAttribute
+import Rattletrap.Decode.ExplosionAttribute
+import Rattletrap.Decode.ExtendedExplosionAttribute
+import Rattletrap.Decode.FlaggedIntAttribute
+import Rattletrap.Decode.FloatAttribute
+import Rattletrap.Decode.GameModeAttribute
+import Rattletrap.Decode.IntAttribute
+import Rattletrap.Decode.LoadoutAttribute
+import Rattletrap.Decode.LoadoutOnlineAttribute
+import Rattletrap.Decode.LoadoutsAttribute
+import Rattletrap.Decode.LoadoutsOnlineAttribute
+import Rattletrap.Decode.LocationAttribute
+import Rattletrap.Decode.MusicStingerAttribute
+import Rattletrap.Decode.PartyLeaderAttribute
+import Rattletrap.Decode.PickupAttribute
+import Rattletrap.Decode.PlayerHistoryKeyAttribute
+import Rattletrap.Decode.PrivateMatchSettingsAttribute
+import Rattletrap.Decode.QWordAttribute
+import Rattletrap.Decode.ReservationAttribute
+import Rattletrap.Decode.RigidBodyStateAttribute
+import Rattletrap.Decode.StringAttribute
+import Rattletrap.Decode.TeamPaintAttribute
+import Rattletrap.Decode.UniqueIdAttribute
+import Rattletrap.Decode.WeldedInfoAttribute
+import Rattletrap.Type.AttributeType
+import Rattletrap.Type.AttributeValue
+import Rattletrap.Type.Common
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word32le
+
+import qualified Data.Map as Map
+
+decodeAttributeValueBits
+  :: (Int, Int, Int) -> Map Word32le Str -> Str -> DecodeBits AttributeValue
+decodeAttributeValueBits version objectMap name = do
+  constructor <- maybe
+    (fail ("don't know how to get attribute value " <> show name))
+    pure
+    (Map.lookup name attributeTypes)
+  case constructor of
+    AttributeTypeAppliedDamage ->
+      AttributeValueAppliedDamage <$> decodeAppliedDamageAttributeBits
+    AttributeTypeBoolean ->
+      AttributeValueBoolean <$> decodeBooleanAttributeBits
+    AttributeTypeByte -> AttributeValueByte <$> decodeByteAttributeBits
+    AttributeTypeCamSettings ->
+      AttributeValueCamSettings <$> decodeCamSettingsAttributeBits version
+    AttributeTypeClubColors ->
+      AttributeValueClubColors <$> decodeClubColorsAttributeBits
+    AttributeTypeDamageState ->
+      AttributeValueDamageState <$> decodeDamageStateAttributeBits
+    AttributeTypeDemolish ->
+      AttributeValueDemolish <$> decodeDemolishAttributeBits
+    AttributeTypeEnum -> AttributeValueEnum <$> decodeEnumAttributeBits
+    AttributeTypeExplosion ->
+      AttributeValueExplosion <$> decodeExplosionAttributeBits
+    AttributeTypeExtendedExplosion ->
+      AttributeValueExtendedExplosion <$> decodeExtendedExplosionAttributeBits
+    AttributeTypeFlaggedInt ->
+      AttributeValueFlaggedInt <$> decodeFlaggedIntAttributeBits
+    AttributeTypeFloat -> AttributeValueFloat <$> decodeFloatAttributeBits
+    AttributeTypeGameMode ->
+      AttributeValueGameMode <$> decodeGameModeAttributeBits version
+    AttributeTypeInt -> AttributeValueInt <$> decodeIntAttributeBits
+    AttributeTypeLoadout ->
+      AttributeValueLoadout <$> decodeLoadoutAttributeBits
+    AttributeTypeLoadoutOnline ->
+      AttributeValueLoadoutOnline
+        <$> decodeLoadoutOnlineAttributeBits version objectMap
+    AttributeTypeLoadouts ->
+      AttributeValueLoadouts <$> decodeLoadoutsAttributeBits
+    AttributeTypeLoadoutsOnline ->
+      AttributeValueLoadoutsOnline
+        <$> decodeLoadoutsOnlineAttributeBits version objectMap
+    AttributeTypeLocation ->
+      AttributeValueLocation <$> decodeLocationAttributeBits
+    AttributeTypeMusicStinger ->
+      AttributeValueMusicStinger <$> decodeMusicStingerAttributeBits
+    AttributeTypePartyLeader ->
+      AttributeValuePartyLeader <$> decodePartyLeaderAttributeBits version
+    AttributeTypePickup -> AttributeValuePickup <$> decodePickupAttributeBits
+    AttributeTypePlayerHistoryKey ->
+      AttributeValuePlayerHistoryKey <$> decodePlayerHistoryKeyAttributeBits
+    AttributeTypePrivateMatchSettings ->
+      AttributeValuePrivateMatchSettings
+        <$> decodePrivateMatchSettingsAttributeBits
+    AttributeTypeQWord -> AttributeValueQWord <$> decodeQWordAttributeBits
+    AttributeTypeReservation ->
+      AttributeValueReservation <$> decodeReservationAttributeBits version
+    AttributeTypeRigidBodyState ->
+      AttributeValueRigidBodyState <$> decodeRigidBodyStateAttributeBits
+    AttributeTypeString -> AttributeValueString <$> decodeStringAttributeBits
+    AttributeTypeTeamPaint ->
+      AttributeValueTeamPaint <$> decodeTeamPaintAttributeBits
+    AttributeTypeUniqueId ->
+      AttributeValueUniqueId <$> decodeUniqueIdAttributeBits version
+    AttributeTypeWeldedInfo ->
+      AttributeValueWeldedInfo <$> decodeWeldedInfoAttributeBits
+
+attributeTypes :: Map Str AttributeType
+attributeTypes = Map.mapKeys toStr (Map.fromList rawAttributeTypes)
diff --git a/library/Rattletrap/Decode/Bitstream.hs b/library/Rattletrap/Decode/Bitstream.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Bitstream.hs
@@ -0,0 +1,11 @@
+module Rattletrap.Decode.Bitstream
+  ( decodeBitstreamBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Type.Bitstream
+
+import qualified Control.Monad as Monad
+
+decodeBitstreamBits :: Int -> DecodeBits Bitstream
+decodeBitstreamBits n = Bitstream <$> Monad.replicateM n getBool
diff --git a/library/Rattletrap/Decode/BooleanAttribute.hs b/library/Rattletrap/Decode/BooleanAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/BooleanAttribute.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Decode.BooleanAttribute
+  ( decodeBooleanAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Type.BooleanAttribute
+
+decodeBooleanAttributeBits :: DecodeBits BooleanAttribute
+decodeBooleanAttributeBits = BooleanAttribute <$> getBool
diff --git a/library/Rattletrap/Decode/ByteAttribute.hs b/library/Rattletrap/Decode/ByteAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/ByteAttribute.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Decode.ByteAttribute
+  ( decodeByteAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Word8le
+import Rattletrap.Type.ByteAttribute
+
+decodeByteAttributeBits :: DecodeBits ByteAttribute
+decodeByteAttributeBits = ByteAttribute <$> decodeWord8leBits
diff --git a/library/Rattletrap/Decode/Cache.hs b/library/Rattletrap/Decode/Cache.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Cache.hs
@@ -0,0 +1,17 @@
+module Rattletrap.Decode.Cache
+  ( decodeCache
+  ) where
+
+import Rattletrap.Decode.AttributeMapping
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.List
+import Rattletrap.Decode.Word32le
+import Rattletrap.Type.Cache
+
+decodeCache :: Decode Cache
+decodeCache =
+  Cache
+    <$> decodeWord32le
+    <*> decodeWord32le
+    <*> decodeWord32le
+    <*> decodeList decodeAttributeMapping
diff --git a/library/Rattletrap/Decode/CamSettingsAttribute.hs b/library/Rattletrap/Decode/CamSettingsAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/CamSettingsAttribute.hs
@@ -0,0 +1,19 @@
+module Rattletrap.Decode.CamSettingsAttribute
+  ( decodeCamSettingsAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Float32le
+import Rattletrap.Type.CamSettingsAttribute
+
+decodeCamSettingsAttributeBits
+  :: (Int, Int, Int) -> DecodeBits CamSettingsAttribute
+decodeCamSettingsAttributeBits version =
+  CamSettingsAttribute
+    <$> decodeFloat32leBits
+    <*> decodeFloat32leBits
+    <*> decodeFloat32leBits
+    <*> decodeFloat32leBits
+    <*> decodeFloat32leBits
+    <*> decodeFloat32leBits
+    <*> decodeWhen (version >= (868, 20, 0)) decodeFloat32leBits
diff --git a/library/Rattletrap/Decode/ClassMapping.hs b/library/Rattletrap/Decode/ClassMapping.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/ClassMapping.hs
@@ -0,0 +1,11 @@
+module Rattletrap.Decode.ClassMapping
+  ( decodeClassMapping
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Str
+import Rattletrap.Decode.Word32le
+import Rattletrap.Type.ClassMapping
+
+decodeClassMapping :: Decode ClassMapping
+decodeClassMapping = ClassMapping <$> decodeStr <*> decodeWord32le
diff --git a/library/Rattletrap/Decode/ClubColorsAttribute.hs b/library/Rattletrap/Decode/ClubColorsAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/ClubColorsAttribute.hs
@@ -0,0 +1,15 @@
+module Rattletrap.Decode.ClubColorsAttribute
+  ( decodeClubColorsAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Word8le
+import Rattletrap.Type.ClubColorsAttribute
+
+decodeClubColorsAttributeBits :: DecodeBits ClubColorsAttribute
+decodeClubColorsAttributeBits =
+  ClubColorsAttribute
+    <$> getBool
+    <*> decodeWord8leBits
+    <*> getBool
+    <*> decodeWord8leBits
diff --git a/library/Rattletrap/Decode/Common.hs b/library/Rattletrap/Decode/Common.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Common.hs
@@ -0,0 +1,55 @@
+module Rattletrap.Decode.Common
+  ( Decode
+  , DecodeBits
+  , decodeWhen
+  , getLazyByteStringBits
+  , getWord8Bits
+  , runDecode
+  , runDecodeBits
+  , toBits
+  , Binary.getFloatle
+  , Binary.getLazyByteString
+  , Binary.getInt8
+  , Binary.getInt32le
+  , Binary.getWord8
+  , Binary.getWord32le
+  , Binary.getWord64le
+  , BinaryBits.getBool
+  , BinaryBits.getWord16be
+  , BinaryBits.getWord32be
+  ) where
+
+import qualified Control.Applicative as Applicative
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits.Get as BinaryBits
+import qualified Data.Binary.Get as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.Word as Word
+import qualified Rattletrap.Utility.Bytes as Utility
+
+type Decode = Binary.Get
+
+type DecodeBits = BinaryBits.BitGet
+
+decodeWhen
+  :: (Applicative m, Applicative.Alternative f) => Bool -> m a -> m (f a)
+decodeWhen p f = if p then fmap pure f else pure Applicative.empty
+
+getLazyByteStringBits :: Int -> DecodeBits LazyBytes.ByteString
+getLazyByteStringBits = BinaryBits.getLazyByteString
+
+getWord8Bits :: Int -> DecodeBits Word.Word8
+getWord8Bits = BinaryBits.getWord8
+
+runDecode :: Decode a -> LazyBytes.ByteString -> Either String a
+runDecode decode bytes = case Binary.runGetOrFail decode bytes of
+  Left (_, _, x) -> fail x
+  Right (_, _, x) -> pure x
+
+runDecodeBits :: DecodeBits a -> LazyBytes.ByteString -> Either String a
+runDecodeBits = runDecode . BinaryBits.runBitGet
+
+toBits :: Decode a -> Int -> DecodeBits a
+toBits decode =
+  fmap (Binary.runGet decode . Utility.reverseBytes)
+    . BinaryBits.getLazyByteString
diff --git a/library/Rattletrap/Decode/CompressedWord.hs b/library/Rattletrap/Decode/CompressedWord.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/CompressedWord.hs
@@ -0,0 +1,29 @@
+module Rattletrap.Decode.CompressedWord
+  ( decodeCompressedWordBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Type.CompressedWord
+
+import qualified Data.Bits as Bits
+
+decodeCompressedWordBits :: Word -> DecodeBits CompressedWord
+decodeCompressedWordBits limit =
+  CompressedWord limit <$> step limit (getMaxBits limit) 0 0
+
+getMaxBits :: Word -> Word
+getMaxBits x = do
+  let
+    n :: Word
+    n = max 1 (ceiling (logBase (2 :: Double) (fromIntegral (max 1 x))))
+  if x < 1024 && x == 2 ^ n then n + 1 else n
+
+step :: Word -> Word -> Word -> Word -> DecodeBits Word
+step limit maxBits position value = do
+  let x = Bits.shiftL 1 (fromIntegral position) :: Word
+  if position < maxBits && value + x <= limit
+    then do
+      bit <- getBool
+      let newValue = if bit then value + x else value
+      step limit maxBits (position + 1) newValue
+    else pure value
diff --git a/library/Rattletrap/Decode/CompressedWordVector.hs b/library/Rattletrap/Decode/CompressedWordVector.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/CompressedWordVector.hs
@@ -0,0 +1,17 @@
+module Rattletrap.Decode.CompressedWordVector
+  ( decodeCompressedWordVectorBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.CompressedWord
+import Rattletrap.Type.CompressedWordVector
+
+decodeCompressedWordVectorBits :: DecodeBits CompressedWordVector
+decodeCompressedWordVectorBits =
+  CompressedWordVector
+    <$> decodeCompressedWordBits limit
+    <*> decodeCompressedWordBits limit
+    <*> decodeCompressedWordBits limit
+
+limit :: Word
+limit = 65536
diff --git a/library/Rattletrap/Decode/Content.hs b/library/Rattletrap/Decode/Content.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Content.hs
@@ -0,0 +1,68 @@
+module Rattletrap.Decode.Content
+  ( decodeContent
+  ) where
+
+import Rattletrap.Decode.Cache
+import Rattletrap.Decode.ClassMapping
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Frame
+import Rattletrap.Decode.KeyFrame
+import Rattletrap.Decode.List
+import Rattletrap.Decode.Mark
+import Rattletrap.Decode.Message
+import Rattletrap.Decode.Str
+import Rattletrap.Decode.Word32le
+import Rattletrap.Type.ClassAttributeMap
+import Rattletrap.Type.Content
+import Rattletrap.Type.Word32le
+import Rattletrap.Utility.Bytes
+
+import qualified Control.Monad.Trans.State as State
+
+decodeContent
+  :: (Int, Int, Int)
+  -- ^ Version numbers, usually from 'Rattletrap.Header.getVersion'.
+  -> Int
+  -- ^ The number of frames in the stream, usually from
+  -- 'Rattletrap.Header.getNumFrames'.
+  -> Word
+  -- ^ The maximum number of channels in the stream, usually from
+  -- 'Rattletrap.Header.getMaxChannels'.
+  -> Decode Content
+decodeContent version numFrames maxChannels = do
+  (levels, keyFrames, streamSize) <-
+    (,,)
+    <$> decodeList decodeStr
+    <*> decodeList decodeKeyFrame
+    <*> decodeWord32le
+  (stream, messages, marks, packages, objects, names, classMappings, caches) <-
+    (,,,,,,,)
+    <$> getLazyByteString (fromIntegral (word32leValue streamSize))
+    <*> decodeList decodeMessage
+    <*> decodeList decodeMark
+    <*> decodeList decodeStr
+    <*> decodeList decodeStr
+    <*> decodeList decodeStr
+    <*> decodeList decodeClassMapping
+    <*> decodeList decodeCache
+  let
+    classAttributeMap =
+      makeClassAttributeMap objects classMappings caches names
+    bitGet = State.evalStateT
+      (decodeFramesBits version numFrames maxChannels classAttributeMap)
+      mempty
+  frames <- either fail pure (runDecodeBits bitGet (reverseBytes stream))
+  pure
+    ( Content
+      levels
+      keyFrames
+      streamSize
+      frames
+      messages
+      marks
+      packages
+      objects
+      names
+      classMappings
+      caches
+    )
diff --git a/library/Rattletrap/Decode/DamageStateAttribute.hs b/library/Rattletrap/Decode/DamageStateAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/DamageStateAttribute.hs
@@ -0,0 +1,19 @@
+module Rattletrap.Decode.DamageStateAttribute
+  ( decodeDamageStateAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Int32le
+import Rattletrap.Decode.Vector
+import Rattletrap.Decode.Word8le
+import Rattletrap.Type.DamageStateAttribute
+
+decodeDamageStateAttributeBits :: DecodeBits DamageStateAttribute
+decodeDamageStateAttributeBits =
+  DamageStateAttribute
+    <$> decodeWord8leBits
+    <*> getBool
+    <*> decodeInt32leBits
+    <*> decodeVectorBits
+    <*> getBool
+    <*> getBool
diff --git a/library/Rattletrap/Decode/DemolishAttribute.hs b/library/Rattletrap/Decode/DemolishAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/DemolishAttribute.hs
@@ -0,0 +1,18 @@
+module Rattletrap.Decode.DemolishAttribute
+  ( decodeDemolishAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Vector
+import Rattletrap.Decode.Word32le
+import Rattletrap.Type.DemolishAttribute
+
+decodeDemolishAttributeBits :: DecodeBits DemolishAttribute
+decodeDemolishAttributeBits =
+  DemolishAttribute
+    <$> getBool
+    <*> decodeWord32leBits
+    <*> getBool
+    <*> decodeWord32leBits
+    <*> decodeVectorBits
+    <*> decodeVectorBits
diff --git a/library/Rattletrap/Decode/DestroyedReplication.hs b/library/Rattletrap/Decode/DestroyedReplication.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/DestroyedReplication.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Decode.DestroyedReplication
+  ( decodeDestroyedReplicationBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Type.DestroyedReplication
+
+decodeDestroyedReplicationBits :: DecodeBits DestroyedReplication
+decodeDestroyedReplicationBits = pure DestroyedReplication
diff --git a/library/Rattletrap/Decode/Dictionary.hs b/library/Rattletrap/Decode/Dictionary.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Dictionary.hs
@@ -0,0 +1,16 @@
+module Rattletrap.Decode.Dictionary
+  ( decodeDictionary
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Str
+import Rattletrap.Type.Dictionary
+import Rattletrap.Type.Str
+
+decodeDictionary :: Decode a -> Decode (Dictionary a)
+decodeDictionary decodeValue = do
+  key <- decodeStr
+  case filter (/= '\x00') (fromStr key) of
+    "None" -> pure (DictionaryEnd key)
+    _ ->
+      DictionaryElement key <$> decodeValue <*> decodeDictionary decodeValue
diff --git a/library/Rattletrap/Decode/EnumAttribute.hs b/library/Rattletrap/Decode/EnumAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/EnumAttribute.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Decode.EnumAttribute
+  ( decodeEnumAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Type.EnumAttribute
+
+decodeEnumAttributeBits :: DecodeBits EnumAttribute
+decodeEnumAttributeBits = EnumAttribute <$> getWord16be 11
diff --git a/library/Rattletrap/Decode/ExplosionAttribute.hs b/library/Rattletrap/Decode/ExplosionAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/ExplosionAttribute.hs
@@ -0,0 +1,12 @@
+module Rattletrap.Decode.ExplosionAttribute
+  ( decodeExplosionAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Int32le
+import Rattletrap.Decode.Vector
+import Rattletrap.Type.ExplosionAttribute
+
+decodeExplosionAttributeBits :: DecodeBits ExplosionAttribute
+decodeExplosionAttributeBits =
+  ExplosionAttribute <$> getBool <*> decodeInt32leBits <*> decodeVectorBits
diff --git a/library/Rattletrap/Decode/ExtendedExplosionAttribute.hs b/library/Rattletrap/Decode/ExtendedExplosionAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/ExtendedExplosionAttribute.hs
@@ -0,0 +1,14 @@
+module Rattletrap.Decode.ExtendedExplosionAttribute
+  ( decodeExtendedExplosionAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.ExplosionAttribute
+import Rattletrap.Decode.FlaggedIntAttribute
+import Rattletrap.Type.ExtendedExplosionAttribute
+
+decodeExtendedExplosionAttributeBits :: DecodeBits ExtendedExplosionAttribute
+decodeExtendedExplosionAttributeBits =
+  ExtendedExplosionAttribute
+    <$> decodeExplosionAttributeBits
+    <*> decodeFlaggedIntAttributeBits
diff --git a/library/Rattletrap/Decode/FlaggedIntAttribute.hs b/library/Rattletrap/Decode/FlaggedIntAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/FlaggedIntAttribute.hs
@@ -0,0 +1,11 @@
+module Rattletrap.Decode.FlaggedIntAttribute
+  ( decodeFlaggedIntAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Int32le
+import Rattletrap.Type.FlaggedIntAttribute
+
+decodeFlaggedIntAttributeBits :: DecodeBits FlaggedIntAttribute
+decodeFlaggedIntAttributeBits =
+  FlaggedIntAttribute <$> getBool <*> decodeInt32leBits
diff --git a/library/Rattletrap/Decode/Float32le.hs b/library/Rattletrap/Decode/Float32le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Float32le.hs
@@ -0,0 +1,13 @@
+module Rattletrap.Decode.Float32le
+  ( decodeFloat32le
+  , decodeFloat32leBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Type.Float32le
+
+decodeFloat32le :: Decode Float32le
+decodeFloat32le = Float32le <$> getFloatle
+
+decodeFloat32leBits :: DecodeBits Float32le
+decodeFloat32leBits = toBits decodeFloat32le 4
diff --git a/library/Rattletrap/Decode/FloatAttribute.hs b/library/Rattletrap/Decode/FloatAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/FloatAttribute.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Decode.FloatAttribute
+  ( decodeFloatAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Float32le
+import Rattletrap.Type.FloatAttribute
+
+decodeFloatAttributeBits :: DecodeBits FloatAttribute
+decodeFloatAttributeBits = FloatAttribute <$> decodeFloat32leBits
diff --git a/library/Rattletrap/Decode/Frame.hs b/library/Rattletrap/Decode/Frame.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Frame.hs
@@ -0,0 +1,43 @@
+module Rattletrap.Decode.Frame
+  ( decodeFramesBits
+  , decodeFrameBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Float32le
+import Rattletrap.Decode.Replication
+import Rattletrap.Type.ClassAttributeMap
+import Rattletrap.Type.CompressedWord
+import Rattletrap.Type.Frame
+import Rattletrap.Type.Word32le
+
+import qualified Control.Monad.Trans.Class as Trans
+import qualified Control.Monad.Trans.State as State
+import qualified Data.Map as Map
+
+decodeFramesBits
+  :: (Int, Int, Int)
+  -> Int
+  -> Word
+  -> ClassAttributeMap
+  -> State.StateT
+       (Map.Map CompressedWord Word32le)
+       DecodeBits
+       [Frame]
+decodeFramesBits version count limit classes = if count <= 0
+  then pure []
+  else
+    (:)
+    <$> decodeFrameBits version limit classes
+    <*> decodeFramesBits version (count - 1) limit classes
+
+decodeFrameBits
+  :: (Int, Int, Int)
+  -> Word
+  -> ClassAttributeMap
+  -> State.StateT (Map.Map CompressedWord Word32le) DecodeBits Frame
+decodeFrameBits version limit classes =
+  Frame
+    <$> Trans.lift decodeFloat32leBits
+    <*> Trans.lift decodeFloat32leBits
+    <*> decodeReplicationsBits version limit classes
diff --git a/library/Rattletrap/Decode/GameModeAttribute.hs b/library/Rattletrap/Decode/GameModeAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/GameModeAttribute.hs
@@ -0,0 +1,14 @@
+module Rattletrap.Decode.GameModeAttribute
+  ( decodeGameModeAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Type.GameModeAttribute
+
+decodeGameModeAttributeBits :: (Int, Int, Int) -> DecodeBits GameModeAttribute
+decodeGameModeAttributeBits version =
+  GameModeAttribute <$> pure (numBits version) <*> getWord8Bits
+    (numBits version)
+
+numBits :: (Int, Int, Int) -> Int
+numBits version = if version < (868, 12, 0) then 2 else 8
diff --git a/library/Rattletrap/Decode/Header.hs b/library/Rattletrap/Decode/Header.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Header.hs
@@ -0,0 +1,21 @@
+module Rattletrap.Decode.Header
+  ( decodeHeader
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Dictionary
+import Rattletrap.Decode.Property
+import Rattletrap.Decode.Str
+import Rattletrap.Decode.Word32le
+import Rattletrap.Type.Header
+import Rattletrap.Type.Word32le
+
+decodeHeader :: Decode Header
+decodeHeader = do
+  (major, minor) <- (,) <$> decodeWord32le <*> decodeWord32le
+  Header major minor
+    <$> decodeWhen
+          (major >= Word32le 868 && minor >= Word32le 18)
+          decodeWord32le
+    <*> decodeStr
+    <*> decodeDictionary decodeProperty
diff --git a/library/Rattletrap/Decode/Initialization.hs b/library/Rattletrap/Decode/Initialization.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Initialization.hs
@@ -0,0 +1,14 @@
+module Rattletrap.Decode.Initialization
+  ( decodeInitializationBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Int8Vector
+import Rattletrap.Decode.Vector
+import Rattletrap.Type.Initialization
+
+decodeInitializationBits :: Bool -> Bool -> DecodeBits Initialization
+decodeInitializationBits hasLocation hasRotation =
+  Initialization
+    <$> decodeWhen hasLocation decodeVectorBits
+    <*> decodeWhen hasRotation decodeInt8VectorBits
diff --git a/library/Rattletrap/Decode/Int32le.hs b/library/Rattletrap/Decode/Int32le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Int32le.hs
@@ -0,0 +1,13 @@
+module Rattletrap.Decode.Int32le
+  ( decodeInt32le
+  , decodeInt32leBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Type.Int32le
+
+decodeInt32le :: Decode Int32le
+decodeInt32le = Int32le <$> getInt32le
+
+decodeInt32leBits :: DecodeBits Int32le
+decodeInt32leBits = toBits decodeInt32le 4
diff --git a/library/Rattletrap/Decode/Int8Vector.hs b/library/Rattletrap/Decode/Int8Vector.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Int8Vector.hs
@@ -0,0 +1,17 @@
+module Rattletrap.Decode.Int8Vector
+  ( decodeInt8VectorBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Int8le
+import Rattletrap.Type.Int8Vector
+import Rattletrap.Type.Int8le
+
+decodeInt8VectorBits :: DecodeBits Int8Vector
+decodeInt8VectorBits =
+  Int8Vector <$> decodeFieldBits <*> decodeFieldBits <*> decodeFieldBits
+
+decodeFieldBits :: DecodeBits (Maybe Int8le)
+decodeFieldBits = do
+  hasField <- getBool
+  decodeWhen hasField decodeInt8leBits
diff --git a/library/Rattletrap/Decode/Int8le.hs b/library/Rattletrap/Decode/Int8le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Int8le.hs
@@ -0,0 +1,13 @@
+module Rattletrap.Decode.Int8le
+  ( decodeInt8le
+  , decodeInt8leBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Type.Int8le
+
+decodeInt8le :: Decode Int8le
+decodeInt8le = Int8le <$> getInt8
+
+decodeInt8leBits :: DecodeBits Int8le
+decodeInt8leBits = toBits decodeInt8le 1
diff --git a/library/Rattletrap/Decode/IntAttribute.hs b/library/Rattletrap/Decode/IntAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/IntAttribute.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Decode.IntAttribute
+  ( decodeIntAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Int32le
+import Rattletrap.Type.IntAttribute
+
+decodeIntAttributeBits :: DecodeBits IntAttribute
+decodeIntAttributeBits = IntAttribute <$> decodeInt32leBits
diff --git a/library/Rattletrap/Decode/KeyFrame.hs b/library/Rattletrap/Decode/KeyFrame.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/KeyFrame.hs
@@ -0,0 +1,12 @@
+module Rattletrap.Decode.KeyFrame
+  ( decodeKeyFrame
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Float32le
+import Rattletrap.Decode.Word32le
+import Rattletrap.Type.KeyFrame
+
+decodeKeyFrame :: Decode KeyFrame
+decodeKeyFrame =
+  KeyFrame <$> decodeFloat32le <*> decodeWord32le <*> decodeWord32le
diff --git a/library/Rattletrap/Decode/List.hs b/library/Rattletrap/Decode/List.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/List.hs
@@ -0,0 +1,15 @@
+module Rattletrap.Decode.List
+  ( decodeList
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Word32le
+import Rattletrap.Type.List
+import Rattletrap.Type.Word32le
+
+import qualified Control.Monad as Monad
+
+decodeList :: Decode a -> Decode (List a)
+decodeList decodeElement = do
+  size <- decodeWord32le
+  List <$> Monad.replicateM (fromIntegral (word32leValue size)) decodeElement
diff --git a/library/Rattletrap/Decode/LoadoutAttribute.hs b/library/Rattletrap/Decode/LoadoutAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/LoadoutAttribute.hs
@@ -0,0 +1,26 @@
+module Rattletrap.Decode.LoadoutAttribute
+  ( decodeLoadoutAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Word32le
+import Rattletrap.Decode.Word8le
+import Rattletrap.Type.LoadoutAttribute
+import Rattletrap.Type.Word8le
+
+decodeLoadoutAttributeBits :: DecodeBits LoadoutAttribute
+decodeLoadoutAttributeBits = do
+  version <- decodeWord8leBits
+  LoadoutAttribute version
+    <$> decodeWord32leBits
+    <*> decodeWord32leBits
+    <*> decodeWord32leBits
+    <*> decodeWord32leBits
+    <*> decodeWord32leBits
+    <*> decodeWord32leBits
+    <*> decodeWord32leBits
+    <*> decodeWhen (version > Word8le 10) decodeWord32leBits
+    <*> decodeWhen (version >= Word8le 16) decodeWord32leBits
+    <*> decodeWhen (version >= Word8le 16) decodeWord32leBits
+    <*> decodeWhen (version >= Word8le 16) decodeWord32leBits
+    <*> decodeWhen (version >= Word8le 17) decodeWord32leBits
diff --git a/library/Rattletrap/Decode/LoadoutOnlineAttribute.hs b/library/Rattletrap/Decode/LoadoutOnlineAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/LoadoutOnlineAttribute.hs
@@ -0,0 +1,24 @@
+module Rattletrap.Decode.LoadoutOnlineAttribute
+  ( decodeLoadoutOnlineAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.ProductAttribute
+import Rattletrap.Decode.Word8le
+import Rattletrap.Type.LoadoutOnlineAttribute
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word32le
+import Rattletrap.Type.Word8le
+
+import qualified Control.Monad as Monad
+import qualified Data.Map as Map
+
+decodeLoadoutOnlineAttributeBits
+  :: (Int, Int, Int)
+  -> Map.Map Word32le Str
+  -> DecodeBits LoadoutOnlineAttribute
+decodeLoadoutOnlineAttributeBits version objectMap = do
+  size <- decodeWord8leBits
+  LoadoutOnlineAttribute <$> Monad.replicateM
+    (fromIntegral (word8leValue size))
+    (decodeProductAttributesBits version objectMap)
diff --git a/library/Rattletrap/Decode/LoadoutsAttribute.hs b/library/Rattletrap/Decode/LoadoutsAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/LoadoutsAttribute.hs
@@ -0,0 +1,13 @@
+module Rattletrap.Decode.LoadoutsAttribute
+  ( decodeLoadoutsAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.LoadoutAttribute
+import Rattletrap.Type.LoadoutsAttribute
+
+decodeLoadoutsAttributeBits :: DecodeBits LoadoutsAttribute
+decodeLoadoutsAttributeBits =
+  LoadoutsAttribute
+    <$> decodeLoadoutAttributeBits
+    <*> decodeLoadoutAttributeBits
diff --git a/library/Rattletrap/Decode/LoadoutsOnlineAttribute.hs b/library/Rattletrap/Decode/LoadoutsOnlineAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/LoadoutsOnlineAttribute.hs
@@ -0,0 +1,22 @@
+module Rattletrap.Decode.LoadoutsOnlineAttribute
+  ( decodeLoadoutsOnlineAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.LoadoutOnlineAttribute
+import Rattletrap.Type.LoadoutsOnlineAttribute
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word32le
+
+import qualified Data.Map as Map
+
+decodeLoadoutsOnlineAttributeBits
+  :: (Int, Int, Int)
+  -> Map.Map Word32le Str
+  -> DecodeBits LoadoutsOnlineAttribute
+decodeLoadoutsOnlineAttributeBits version objectMap =
+  LoadoutsOnlineAttribute
+    <$> decodeLoadoutOnlineAttributeBits version objectMap
+    <*> decodeLoadoutOnlineAttributeBits version objectMap
+    <*> getBool
+    <*> getBool
diff --git a/library/Rattletrap/Decode/LocationAttribute.hs b/library/Rattletrap/Decode/LocationAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/LocationAttribute.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Decode.LocationAttribute
+  ( decodeLocationAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Vector
+import Rattletrap.Type.LocationAttribute
+
+decodeLocationAttributeBits :: DecodeBits LocationAttribute
+decodeLocationAttributeBits = LocationAttribute <$> decodeVectorBits
diff --git a/library/Rattletrap/Decode/Mark.hs b/library/Rattletrap/Decode/Mark.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Mark.hs
@@ -0,0 +1,11 @@
+module Rattletrap.Decode.Mark
+  ( decodeMark
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Str
+import Rattletrap.Decode.Word32le
+import Rattletrap.Type.Mark
+
+decodeMark :: Decode Mark
+decodeMark = Mark <$> decodeStr <*> decodeWord32le
diff --git a/library/Rattletrap/Decode/Message.hs b/library/Rattletrap/Decode/Message.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Message.hs
@@ -0,0 +1,11 @@
+module Rattletrap.Decode.Message
+  ( decodeMessage
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Str
+import Rattletrap.Decode.Word32le
+import Rattletrap.Type.Message
+
+decodeMessage :: Decode Message
+decodeMessage = Message <$> decodeWord32le <*> decodeStr <*> decodeStr
diff --git a/library/Rattletrap/Decode/MusicStingerAttribute.hs b/library/Rattletrap/Decode/MusicStingerAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/MusicStingerAttribute.hs
@@ -0,0 +1,15 @@
+module Rattletrap.Decode.MusicStingerAttribute
+  ( decodeMusicStingerAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Word32le
+import Rattletrap.Decode.Word8le
+import Rattletrap.Type.MusicStingerAttribute
+
+decodeMusicStingerAttributeBits :: DecodeBits MusicStingerAttribute
+decodeMusicStingerAttributeBits =
+  MusicStingerAttribute
+    <$> getBool
+    <*> decodeWord32leBits
+    <*> decodeWord8leBits
diff --git a/library/Rattletrap/Decode/PartyLeaderAttribute.hs b/library/Rattletrap/Decode/PartyLeaderAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/PartyLeaderAttribute.hs
@@ -0,0 +1,17 @@
+module Rattletrap.Decode.PartyLeaderAttribute
+  ( decodePartyLeaderAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.RemoteId
+import Rattletrap.Decode.Word8le
+import Rattletrap.Type.PartyLeaderAttribute
+import Rattletrap.Type.Word8le
+
+decodePartyLeaderAttributeBits
+  :: (Int, Int, Int) -> DecodeBits PartyLeaderAttribute
+decodePartyLeaderAttributeBits version = do
+  systemId <- decodeWord8leBits
+  PartyLeaderAttribute systemId <$> decodeWhen
+    (systemId /= Word8le 0)
+    ((,) <$> decodeRemoteIdBits version systemId <*> decodeWord8leBits)
diff --git a/library/Rattletrap/Decode/PickupAttribute.hs b/library/Rattletrap/Decode/PickupAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/PickupAttribute.hs
@@ -0,0 +1,12 @@
+module Rattletrap.Decode.PickupAttribute
+  ( decodePickupAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Word32le
+import Rattletrap.Type.PickupAttribute
+
+decodePickupAttributeBits :: DecodeBits PickupAttribute
+decodePickupAttributeBits = do
+  instigator <- getBool
+  PickupAttribute <$> decodeWhen instigator decodeWord32leBits <*> getBool
diff --git a/library/Rattletrap/Decode/PlayerHistoryKeyAttribute.hs b/library/Rattletrap/Decode/PlayerHistoryKeyAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/PlayerHistoryKeyAttribute.hs
@@ -0,0 +1,11 @@
+module Rattletrap.Decode.PlayerHistoryKeyAttribute
+  ( decodePlayerHistoryKeyAttributeBits
+  ) where
+
+import Rattletrap.Decode.Bitstream
+import Rattletrap.Decode.Common
+import Rattletrap.Type.PlayerHistoryKeyAttribute
+
+decodePlayerHistoryKeyAttributeBits :: DecodeBits PlayerHistoryKeyAttribute
+decodePlayerHistoryKeyAttributeBits =
+  PlayerHistoryKeyAttribute <$> decodeBitstreamBits 1253
diff --git a/library/Rattletrap/Decode/PrivateMatchSettingsAttribute.hs b/library/Rattletrap/Decode/PrivateMatchSettingsAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/PrivateMatchSettingsAttribute.hs
@@ -0,0 +1,19 @@
+module Rattletrap.Decode.PrivateMatchSettingsAttribute
+  ( decodePrivateMatchSettingsAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Str
+import Rattletrap.Decode.Word32le
+import Rattletrap.Type.PrivateMatchSettingsAttribute
+
+decodePrivateMatchSettingsAttributeBits
+  :: DecodeBits PrivateMatchSettingsAttribute
+decodePrivateMatchSettingsAttributeBits =
+  PrivateMatchSettingsAttribute
+    <$> decodeStrBits
+    <*> decodeWord32leBits
+    <*> decodeWord32leBits
+    <*> decodeStrBits
+    <*> decodeStrBits
+    <*> getBool
diff --git a/library/Rattletrap/Decode/ProductAttribute.hs b/library/Rattletrap/Decode/ProductAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/ProductAttribute.hs
@@ -0,0 +1,57 @@
+module Rattletrap.Decode.ProductAttribute
+  ( decodeProductAttributesBits
+  , decodeProductAttributeBits
+  ) where
+
+import Data.Semigroup ((<>))
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.CompressedWord
+import Rattletrap.Decode.Word32le
+import Rattletrap.Decode.Word8le
+import Rattletrap.Type.Common
+import Rattletrap.Type.CompressedWord
+import Rattletrap.Type.ProductAttribute
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word32le
+import Rattletrap.Type.Word8le
+
+import qualified Control.Monad as Monad
+import qualified Data.Map as Map
+
+decodeProductAttributesBits
+  :: (Int, Int, Int) -> Map Word32le Str -> DecodeBits [ProductAttribute]
+decodeProductAttributesBits version objectMap = do
+  size <- decodeWord8leBits
+  Monad.replicateM
+    (fromIntegral (word8leValue size))
+    (decodeProductAttributeBits version objectMap)
+
+decodeProductAttributeBits
+  :: (Int, Int, Int) -> Map Word32le Str -> DecodeBits ProductAttribute
+decodeProductAttributeBits version objectMap = do
+  flag <- getBool
+  objectId <- decodeWord32leBits
+  let objectName = Map.lookup objectId objectMap
+  value <- case objectName of
+    Just name -> case fromStr name of
+      "TAGame.ProductAttribute_Painted_TA" -> Just <$> decodePainted version
+      "TAGame.ProductAttribute_UserColor_TA" -> decodeColor
+      _ ->
+        fail
+          ( "unknown object name "
+          <> show objectName
+          <> " for ID "
+          <> show objectId
+          )
+    Nothing -> fail ("missing object name for ID " <> show objectId)
+  pure (ProductAttribute flag objectId objectName value)
+
+decodePainted :: (Int, Int, Int) -> DecodeBits (Either CompressedWord Word32)
+decodePainted version = if version >= (868, 18, 0)
+  then Right <$> getWord32be 31
+  else Left <$> decodeCompressedWordBits 13
+
+decodeColor :: DecodeBits (Maybe (Either CompressedWord Word32))
+decodeColor = do
+  hasValue <- getBool
+  decodeWhen hasValue (Right <$> getWord32be 31)
diff --git a/library/Rattletrap/Decode/Property.hs b/library/Rattletrap/Decode/Property.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Property.hs
@@ -0,0 +1,14 @@
+module Rattletrap.Decode.Property
+  ( decodeProperty
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.PropertyValue
+import Rattletrap.Decode.Str
+import Rattletrap.Decode.Word64le
+import Rattletrap.Type.Property
+
+decodeProperty :: Decode Property
+decodeProperty = do
+  kind <- decodeStr
+  Property kind <$> decodeWord64le <*> decodePropertyValue decodeProperty kind
diff --git a/library/Rattletrap/Decode/PropertyValue.hs b/library/Rattletrap/Decode/PropertyValue.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/PropertyValue.hs
@@ -0,0 +1,31 @@
+module Rattletrap.Decode.PropertyValue
+  ( decodePropertyValue
+  ) where
+
+import Data.Semigroup ((<>))
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Dictionary
+import Rattletrap.Decode.Float32le
+import Rattletrap.Decode.Int32le
+import Rattletrap.Decode.List
+import Rattletrap.Decode.Str
+import Rattletrap.Decode.Word64le
+import Rattletrap.Decode.Word8le
+import Rattletrap.Type.PropertyValue
+import Rattletrap.Type.Str
+
+decodePropertyValue :: Decode a -> Str -> Decode (PropertyValue a)
+decodePropertyValue getProperty kind = case fromStr kind of
+  "ArrayProperty" ->
+    PropertyValueArray <$> decodeList (decodeDictionary getProperty)
+  "BoolProperty" -> PropertyValueBool <$> decodeWord8le
+  "ByteProperty" -> do
+    k <- decodeStr
+    PropertyValueByte k
+      <$> decodeWhen (fromStr k /= "OnlinePlatform_Steam") decodeStr
+  "FloatProperty" -> PropertyValueFloat <$> decodeFloat32le
+  "IntProperty" -> PropertyValueInt <$> decodeInt32le
+  "NameProperty" -> PropertyValueName <$> decodeStr
+  "QWordProperty" -> PropertyValueQWord <$> decodeWord64le
+  "StrProperty" -> PropertyValueStr <$> decodeStr
+  _ -> fail ("don't know how to read property value " <> show kind)
diff --git a/library/Rattletrap/Decode/QWordAttribute.hs b/library/Rattletrap/Decode/QWordAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/QWordAttribute.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Decode.QWordAttribute
+  ( decodeQWordAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Word64le
+import Rattletrap.Type.QWordAttribute
+
+decodeQWordAttributeBits :: DecodeBits QWordAttribute
+decodeQWordAttributeBits = QWordAttribute <$> decodeWord64leBits
diff --git a/library/Rattletrap/Decode/RemoteId.hs b/library/Rattletrap/Decode/RemoteId.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/RemoteId.hs
@@ -0,0 +1,38 @@
+module Rattletrap.Decode.RemoteId
+  ( decodeRemoteIdBits
+  ) where
+
+import Data.Semigroup ((<>))
+import Rattletrap.Decode.Bitstream
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Word64le
+import Rattletrap.Type.RemoteId
+import Rattletrap.Type.Word8le
+import Rattletrap.Utility.Bytes
+
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+import qualified Data.Word as Word
+
+decodeRemoteIdBits :: (Int, Int, Int) -> Word8le -> DecodeBits RemoteId
+decodeRemoteIdBits (_, _, patch) systemId = case word8leValue systemId of
+  0 -> RemoteIdSplitscreen <$> getWord32be 24
+  1 -> RemoteIdSteam <$> decodeWord64leBits
+  2 -> RemoteIdPlayStation <$> decodePsName <*> decodePsBytes patch
+  4 -> RemoteIdXbox <$> decodeWord64leBits
+  6 -> RemoteIdSwitch <$> decodeBitstreamBits 256
+  _ -> fail ("unknown system id " <> show systemId)
+
+decodePsName :: DecodeBits Text.Text
+decodePsName = fmap
+  ( Text.dropWhileEnd (== '\x00')
+  . Text.decodeLatin1
+  . LazyBytes.toStrict
+  . reverseBytes
+  )
+  (getLazyByteStringBits 16)
+
+decodePsBytes :: Int -> DecodeBits [Word.Word8]
+decodePsBytes patch =
+  LazyBytes.unpack <$> getLazyByteStringBits (if patch >= 1 then 24 else 16)
diff --git a/library/Rattletrap/Decode/Replay.hs b/library/Rattletrap/Decode/Replay.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Replay.hs
@@ -0,0 +1,48 @@
+module Rattletrap.Decode.Replay
+  ( decodeReplay
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Content
+import Rattletrap.Decode.Header
+import Rattletrap.Decode.Section
+import Rattletrap.Type.Dictionary
+import Rattletrap.Type.Header
+import Rattletrap.Type.Int32le
+import Rattletrap.Type.Property
+import Rattletrap.Type.PropertyValue
+import Rattletrap.Type.Replay
+import Rattletrap.Type.Section
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word32le
+
+decodeReplay :: Decode Replay
+decodeReplay = do
+  header <- decodeSection decodeHeader
+  Replay header <$> decodeSection
+    ( decodeContent
+      (getVersion (sectionBody header))
+      (getNumFrames (sectionBody header))
+      (getMaxChannels (sectionBody header))
+    )
+
+getVersion :: Header -> (Int, Int, Int)
+getVersion header =
+  ( fromIntegral (word32leValue (headerEngineVersion header))
+  , fromIntegral (word32leValue (headerLicenseeVersion header))
+  , maybe 0 (fromIntegral . word32leValue) (headerPatchVersion header)
+  )
+
+getNumFrames :: Header -> Int
+getNumFrames header =
+  case dictionaryLookup (toStr "NumFrames") (headerProperties header) of
+    Just (Property _ _ (PropertyValueInt numFrames)) ->
+      fromIntegral (int32leValue numFrames)
+    _ -> 0
+
+getMaxChannels :: Header -> Word
+getMaxChannels header =
+  case dictionaryLookup (toStr "MaxChannels") (headerProperties header) of
+    Just (Property _ _ (PropertyValueInt numFrames)) ->
+      fromIntegral (int32leValue numFrames)
+    _ -> 1023
diff --git a/library/Rattletrap/Decode/Replication.hs b/library/Rattletrap/Decode/Replication.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Replication.hs
@@ -0,0 +1,45 @@
+module Rattletrap.Decode.Replication
+  ( decodeReplicationsBits
+  , decodeReplicationBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.CompressedWord
+import Rattletrap.Decode.ReplicationValue
+import Rattletrap.Type.ClassAttributeMap
+import Rattletrap.Type.CompressedWord
+import Rattletrap.Type.Replication
+import Rattletrap.Type.Word32le
+
+import qualified Control.Monad.Trans.Class as Trans
+import qualified Control.Monad.Trans.State as State
+import qualified Data.Map as Map
+
+decodeReplicationsBits
+  :: (Int, Int, Int)
+  -> Word
+  -> ClassAttributeMap
+  -> State.StateT
+       (Map.Map CompressedWord Word32le)
+       DecodeBits
+       [Replication]
+decodeReplicationsBits version limit classes = do
+  hasReplication <- Trans.lift getBool
+  if hasReplication
+    then
+      (:)
+      <$> decodeReplicationBits version limit classes
+      <*> decodeReplicationsBits version limit classes
+    else pure []
+
+decodeReplicationBits
+  :: (Int, Int, Int)
+  -> Word
+  -> ClassAttributeMap
+  -> State.StateT
+       (Map.Map CompressedWord Word32le)
+       DecodeBits
+       Replication
+decodeReplicationBits version limit classes = do
+  actor <- Trans.lift (decodeCompressedWordBits limit)
+  Replication actor <$> decodeReplicationValueBits version classes actor
diff --git a/library/Rattletrap/Decode/ReplicationValue.hs b/library/Rattletrap/Decode/ReplicationValue.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/ReplicationValue.hs
@@ -0,0 +1,44 @@
+module Rattletrap.Decode.ReplicationValue
+  ( decodeReplicationValueBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.DestroyedReplication
+import Rattletrap.Decode.SpawnedReplication
+import Rattletrap.Decode.UpdatedReplication
+import Rattletrap.Type.ClassAttributeMap
+import Rattletrap.Type.CompressedWord
+import Rattletrap.Type.ReplicationValue
+import Rattletrap.Type.Word32le
+
+import qualified Control.Monad.Trans.Class as Trans
+import qualified Control.Monad.Trans.State as State
+import qualified Data.Map as Map
+
+decodeReplicationValueBits
+  :: (Int, Int, Int)
+  -> ClassAttributeMap
+  -> CompressedWord
+  -> State.StateT
+       (Map.Map CompressedWord Word32le)
+       DecodeBits
+       ReplicationValue
+decodeReplicationValueBits version classAttributeMap actorId = do
+  actorMap <- State.get
+  isOpen <- Trans.lift getBool
+  if isOpen
+    then do
+      isNew <- Trans.lift getBool
+      if isNew
+        then
+          ReplicationValueSpawned
+            <$> decodeSpawnedReplicationBits version classAttributeMap actorId
+        else ReplicationValueUpdated <$> Trans.lift
+          ( decodeUpdatedReplicationBits
+            version
+            classAttributeMap
+            actorMap
+            actorId
+          )
+    else ReplicationValueDestroyed
+      <$> Trans.lift decodeDestroyedReplicationBits
diff --git a/library/Rattletrap/Decode/ReservationAttribute.hs b/library/Rattletrap/Decode/ReservationAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/ReservationAttribute.hs
@@ -0,0 +1,24 @@
+module Rattletrap.Decode.ReservationAttribute
+  ( decodeReservationAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.CompressedWord
+import Rattletrap.Decode.Str
+import Rattletrap.Decode.UniqueIdAttribute
+import Rattletrap.Type.ReservationAttribute
+import Rattletrap.Type.UniqueIdAttribute
+import Rattletrap.Type.Word8le
+
+decodeReservationAttributeBits
+  :: (Int, Int, Int) -> DecodeBits ReservationAttribute
+decodeReservationAttributeBits version = do
+  number <- decodeCompressedWordBits 7
+  uniqueId <- decodeUniqueIdAttributeBits version
+  ReservationAttribute number uniqueId
+    <$> decodeWhen
+          (uniqueIdAttributeSystemId uniqueId /= Word8le 0)
+          decodeStrBits
+    <*> getBool
+    <*> getBool
+    <*> decodeWhen (version >= (868, 12, 0)) (getWord8Bits 6)
diff --git a/library/Rattletrap/Decode/RigidBodyStateAttribute.hs b/library/Rattletrap/Decode/RigidBodyStateAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/RigidBodyStateAttribute.hs
@@ -0,0 +1,17 @@
+module Rattletrap.Decode.RigidBodyStateAttribute
+  ( decodeRigidBodyStateAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.CompressedWordVector
+import Rattletrap.Decode.Vector
+import Rattletrap.Type.RigidBodyStateAttribute
+
+decodeRigidBodyStateAttributeBits :: DecodeBits RigidBodyStateAttribute
+decodeRigidBodyStateAttributeBits = do
+  sleeping <- getBool
+  RigidBodyStateAttribute sleeping
+    <$> decodeVectorBits
+    <*> decodeCompressedWordVectorBits
+    <*> decodeWhen (not sleeping) decodeVectorBits
+    <*> decodeWhen (not sleeping) decodeVectorBits
diff --git a/library/Rattletrap/Decode/Section.hs b/library/Rattletrap/Decode/Section.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Section.hs
@@ -0,0 +1,25 @@
+module Rattletrap.Decode.Section
+  ( decodeSection
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Word32le
+import Rattletrap.Type.Section
+import Rattletrap.Type.Word32le
+import Rattletrap.Utility.Crc
+
+import qualified Control.Monad as Monad
+
+decodeSection :: Decode a -> Decode (Section a)
+decodeSection getBody = do
+  size <- decodeWord32le
+  crc <- decodeWord32le
+  rawBody <- getLazyByteString (fromIntegral (word32leValue size))
+  let actualCrc = Word32le (getCrc32 rawBody)
+  Monad.when (actualCrc /= crc) (fail (crcMessage actualCrc crc))
+  body <- either fail pure (runDecode getBody rawBody)
+  pure (Section size crc body)
+
+crcMessage :: Word32le -> Word32le -> String
+crcMessage actual expected = unwords
+  ["actual CRC", show actual, "does not match expected CRC", show expected]
diff --git a/library/Rattletrap/Decode/SpawnedReplication.hs b/library/Rattletrap/Decode/SpawnedReplication.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/SpawnedReplication.hs
@@ -0,0 +1,69 @@
+module Rattletrap.Decode.SpawnedReplication
+  ( decodeSpawnedReplicationBits
+  ) where
+
+import Data.Semigroup ((<>))
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Initialization
+import Rattletrap.Decode.Word32le
+import Rattletrap.Type.ClassAttributeMap
+import Rattletrap.Type.CompressedWord
+import Rattletrap.Type.SpawnedReplication
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word32le
+
+import qualified Control.Monad.Trans.Class as Trans
+import qualified Control.Monad.Trans.State as State
+import qualified Data.Map as Map
+
+decodeSpawnedReplicationBits
+  :: (Int, Int, Int)
+  -> ClassAttributeMap
+  -> CompressedWord
+  -> State.StateT
+       (Map.Map CompressedWord Word32le)
+       DecodeBits
+       SpawnedReplication
+decodeSpawnedReplicationBits version classAttributeMap actorId = do
+  flag <- Trans.lift getBool
+  nameIndex <- decodeWhen
+    (version >= (868, 14, 0))
+    (Trans.lift decodeWord32leBits)
+  name <- lookupName classAttributeMap nameIndex
+  objectId <- Trans.lift decodeWord32leBits
+  State.modify (Map.insert actorId objectId)
+  objectName <- lookupObjectName classAttributeMap objectId
+  className <- lookupClassName objectName
+  let hasLocation = classHasLocation className
+  let hasRotation = classHasRotation className
+  initialization <- Trans.lift
+    (decodeInitializationBits hasLocation hasRotation)
+  pure
+    ( SpawnedReplication
+      flag
+      nameIndex
+      name
+      objectId
+      objectName
+      className
+      initialization
+    )
+
+lookupName :: Monad m => ClassAttributeMap -> Maybe Word32le -> m (Maybe Str)
+lookupName classAttributeMap maybeNameIndex = case maybeNameIndex of
+  Nothing -> pure Nothing
+  Just nameIndex ->
+    case getName (classAttributeMapNameMap classAttributeMap) nameIndex of
+      Nothing -> fail ("could not get name for index " <> show nameIndex)
+      Just name -> pure (Just name)
+
+lookupObjectName :: Monad m => ClassAttributeMap -> Word32le -> m Str
+lookupObjectName classAttributeMap objectId =
+  case getObjectName (classAttributeMapObjectMap classAttributeMap) objectId of
+    Nothing -> fail ("could not get object name for id " <> show objectId)
+    Just objectName -> pure objectName
+
+lookupClassName :: Monad m => Str -> m Str
+lookupClassName objectName = case getClassName objectName of
+  Nothing -> fail ("could not get class name for object " <> show objectName)
+  Just className -> pure className
diff --git a/library/Rattletrap/Decode/Str.hs b/library/Rattletrap/Decode/Str.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Str.hs
@@ -0,0 +1,42 @@
+module Rattletrap.Decode.Str
+  ( decodeStr
+  , decodeStrBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Int32le
+import Rattletrap.Type.Int32le
+import Rattletrap.Type.Str
+import Rattletrap.Utility.Bytes
+
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+
+decodeStr :: Decode Str
+decodeStr = do
+  rawSize <- decodeInt32le
+  bytes <- getLazyByteString (normalizeTextSize rawSize)
+  pure (Str (dropNull (getTextDecoder rawSize bytes)))
+
+decodeStrBits :: DecodeBits Str
+decodeStrBits = do
+  rawSize <- decodeInt32leBits
+  bytes <- getLazyByteStringBits (normalizeTextSize rawSize)
+  pure (Str (dropNull (getTextDecoder rawSize (reverseBytes bytes))))
+
+normalizeTextSize :: Integral a => Int32le -> a
+normalizeTextSize size = case int32leValue size of
+  0x05000000 -> 8
+  x -> if x < 0 then (-2 * fromIntegral x) else fromIntegral x
+
+getTextDecoder :: Int32le -> LazyBytes.ByteString -> Text.Text
+getTextDecoder size bytes =
+  let
+    decode =
+      if size < Int32le 0 then Text.decodeUtf16LE else Text.decodeLatin1
+  in
+    decode (LazyBytes.toStrict bytes)
+
+dropNull :: Text.Text -> Text.Text
+dropNull = Text.dropWhileEnd (== '\x00')
diff --git a/library/Rattletrap/Decode/StringAttribute.hs b/library/Rattletrap/Decode/StringAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/StringAttribute.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Decode.StringAttribute
+  ( decodeStringAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Str
+import Rattletrap.Type.StringAttribute
+
+decodeStringAttributeBits :: DecodeBits StringAttribute
+decodeStringAttributeBits = StringAttribute <$> decodeStrBits
diff --git a/library/Rattletrap/Decode/TeamPaintAttribute.hs b/library/Rattletrap/Decode/TeamPaintAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/TeamPaintAttribute.hs
@@ -0,0 +1,17 @@
+module Rattletrap.Decode.TeamPaintAttribute
+  ( decodeTeamPaintAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Word32le
+import Rattletrap.Decode.Word8le
+import Rattletrap.Type.TeamPaintAttribute
+
+decodeTeamPaintAttributeBits :: DecodeBits TeamPaintAttribute
+decodeTeamPaintAttributeBits =
+  TeamPaintAttribute
+    <$> decodeWord8leBits
+    <*> decodeWord8leBits
+    <*> decodeWord8leBits
+    <*> decodeWord32leBits
+    <*> decodeWord32leBits
diff --git a/library/Rattletrap/Decode/UniqueIdAttribute.hs b/library/Rattletrap/Decode/UniqueIdAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/UniqueIdAttribute.hs
@@ -0,0 +1,15 @@
+module Rattletrap.Decode.UniqueIdAttribute
+  ( decodeUniqueIdAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.RemoteId
+import Rattletrap.Decode.Word8le
+import Rattletrap.Type.UniqueIdAttribute
+
+decodeUniqueIdAttributeBits :: (Int, Int, Int) -> DecodeBits UniqueIdAttribute
+decodeUniqueIdAttributeBits version = do
+  systemId <- decodeWord8leBits
+  UniqueIdAttribute systemId
+    <$> decodeRemoteIdBits version systemId
+    <*> decodeWord8leBits
diff --git a/library/Rattletrap/Decode/UpdatedReplication.hs b/library/Rattletrap/Decode/UpdatedReplication.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/UpdatedReplication.hs
@@ -0,0 +1,21 @@
+module Rattletrap.Decode.UpdatedReplication
+  ( decodeUpdatedReplicationBits
+  ) where
+
+import Rattletrap.Decode.Attribute
+import Rattletrap.Decode.Common
+import Rattletrap.Type.ClassAttributeMap
+import Rattletrap.Type.CompressedWord
+import Rattletrap.Type.UpdatedReplication
+import Rattletrap.Type.Word32le
+
+import qualified Data.Map as Map
+
+decodeUpdatedReplicationBits
+  :: (Int, Int, Int)
+  -> ClassAttributeMap
+  -> Map.Map CompressedWord Word32le
+  -> CompressedWord
+  -> DecodeBits UpdatedReplication
+decodeUpdatedReplicationBits version classes actors actor =
+  UpdatedReplication <$> decodeAttributesBits version classes actors actor
diff --git a/library/Rattletrap/Decode/Vector.hs b/library/Rattletrap/Decode/Vector.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Vector.hs
@@ -0,0 +1,28 @@
+module Rattletrap.Decode.Vector
+  ( decodeVectorBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.CompressedWord
+import Rattletrap.Type.CompressedWord
+import Rattletrap.Type.Vector
+
+decodeVectorBits :: DecodeBits Vector
+decodeVectorBits = do
+  size <- decodeCompressedWordBits 19
+  let
+    limit = getLimit size
+    bias = getBias size
+  Vector bias
+    <$> fmap (fromDelta bias) (decodeCompressedWordBits limit)
+    <*> fmap (fromDelta bias) (decodeCompressedWordBits limit)
+    <*> fmap (fromDelta bias) (decodeCompressedWordBits limit)
+
+getLimit :: CompressedWord -> Word
+getLimit = (2 ^) . (+ 2) . compressedWordValue
+
+getBias :: CompressedWord -> Word
+getBias = (2 ^) . (+ 1) . compressedWordValue
+
+fromDelta :: Word -> CompressedWord -> Int
+fromDelta bias x = fromIntegral (compressedWordValue x) - fromIntegral bias
diff --git a/library/Rattletrap/Decode/WeldedInfoAttribute.hs b/library/Rattletrap/Decode/WeldedInfoAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/WeldedInfoAttribute.hs
@@ -0,0 +1,19 @@
+module Rattletrap.Decode.WeldedInfoAttribute
+  ( decodeWeldedInfoAttributeBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Float32le
+import Rattletrap.Decode.Int32le
+import Rattletrap.Decode.Int8Vector
+import Rattletrap.Decode.Vector
+import Rattletrap.Type.WeldedInfoAttribute
+
+decodeWeldedInfoAttributeBits :: DecodeBits WeldedInfoAttribute
+decodeWeldedInfoAttributeBits =
+  WeldedInfoAttribute
+    <$> getBool
+    <*> decodeInt32leBits
+    <*> decodeVectorBits
+    <*> decodeFloat32leBits
+    <*> decodeInt8VectorBits
diff --git a/library/Rattletrap/Decode/Word32le.hs b/library/Rattletrap/Decode/Word32le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Word32le.hs
@@ -0,0 +1,13 @@
+module Rattletrap.Decode.Word32le
+  ( decodeWord32le
+  , decodeWord32leBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Type.Word32le
+
+decodeWord32le :: Decode Word32le
+decodeWord32le = Word32le <$> getWord32le
+
+decodeWord32leBits :: DecodeBits Word32le
+decodeWord32leBits = toBits decodeWord32le 4
diff --git a/library/Rattletrap/Decode/Word64le.hs b/library/Rattletrap/Decode/Word64le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Word64le.hs
@@ -0,0 +1,13 @@
+module Rattletrap.Decode.Word64le
+  ( decodeWord64le
+  , decodeWord64leBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Type.Word64le
+
+decodeWord64le :: Decode Word64le
+decodeWord64le = Word64le <$> getWord64le
+
+decodeWord64leBits :: DecodeBits Word64le
+decodeWord64leBits = toBits decodeWord64le 8
diff --git a/library/Rattletrap/Decode/Word8le.hs b/library/Rattletrap/Decode/Word8le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Decode/Word8le.hs
@@ -0,0 +1,13 @@
+module Rattletrap.Decode.Word8le
+  ( decodeWord8le
+  , decodeWord8leBits
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Type.Word8le
+
+decodeWord8le :: Decode Word8le
+decodeWord8le = Word8le <$> getWord8
+
+decodeWord8leBits :: DecodeBits Word8le
+decodeWord8leBits = toBits decodeWord8le 1
diff --git a/library/Rattletrap/Encode/AppliedDamageAttribute.hs b/library/Rattletrap/Encode/AppliedDamageAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/AppliedDamageAttribute.hs
@@ -0,0 +1,17 @@
+module Rattletrap.Encode.AppliedDamageAttribute
+  ( putAppliedDamageAttribute
+  ) where
+
+import Rattletrap.Encode.Int32le
+import Rattletrap.Encode.Vector
+import Rattletrap.Encode.Word8le
+import Rattletrap.Type.AppliedDamageAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putAppliedDamageAttribute :: AppliedDamageAttribute -> BinaryBits.BitPut ()
+putAppliedDamageAttribute appliedDamageAttribute = do
+  putWord8Bits (appliedDamageAttributeUnknown1 appliedDamageAttribute)
+  putVector (appliedDamageAttributeLocation appliedDamageAttribute)
+  putInt32Bits (appliedDamageAttributeUnknown3 appliedDamageAttribute)
+  putInt32Bits (appliedDamageAttributeUnknown4 appliedDamageAttribute)
diff --git a/library/Rattletrap/Encode/Attribute.hs b/library/Rattletrap/Encode/Attribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Attribute.hs
@@ -0,0 +1,21 @@
+module Rattletrap.Encode.Attribute
+  ( putAttributes
+  , putAttribute
+  ) where
+
+import Rattletrap.Encode.AttributeValue
+import Rattletrap.Encode.CompressedWord
+import Rattletrap.Type.Attribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putAttributes :: [Attribute] -> BinaryBits.BitPut ()
+putAttributes attributes = do
+  mapM_ putAttribute attributes
+  BinaryBits.putBool False
+
+putAttribute :: Attribute -> BinaryBits.BitPut ()
+putAttribute attribute = do
+  BinaryBits.putBool True
+  putCompressedWord (attributeId attribute)
+  putAttributeValue (attributeValue attribute)
diff --git a/library/Rattletrap/Encode/AttributeMapping.hs b/library/Rattletrap/Encode/AttributeMapping.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/AttributeMapping.hs
@@ -0,0 +1,13 @@
+module Rattletrap.Encode.AttributeMapping
+  ( putAttributeMapping
+  ) where
+
+import Rattletrap.Encode.Word32le
+import Rattletrap.Type.AttributeMapping
+
+import qualified Data.Binary as Binary
+
+putAttributeMapping :: AttributeMapping -> Binary.Put
+putAttributeMapping attributeMapping = do
+  putWord32 (attributeMappingObjectId attributeMapping)
+  putWord32 (attributeMappingStreamId attributeMapping)
diff --git a/library/Rattletrap/Encode/AttributeValue.hs b/library/Rattletrap/Encode/AttributeValue.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/AttributeValue.hs
@@ -0,0 +1,72 @@
+module Rattletrap.Encode.AttributeValue
+  ( putAttributeValue
+  ) where
+
+import Rattletrap.Encode.AppliedDamageAttribute
+import Rattletrap.Encode.BooleanAttribute
+import Rattletrap.Encode.ByteAttribute
+import Rattletrap.Encode.CamSettingsAttribute
+import Rattletrap.Encode.ClubColorsAttribute
+import Rattletrap.Encode.DamageStateAttribute
+import Rattletrap.Encode.DemolishAttribute
+import Rattletrap.Encode.EnumAttribute
+import Rattletrap.Encode.ExplosionAttribute
+import Rattletrap.Encode.ExtendedExplosionAttribute
+import Rattletrap.Encode.FlaggedIntAttribute
+import Rattletrap.Encode.FloatAttribute
+import Rattletrap.Encode.GameModeAttribute
+import Rattletrap.Encode.IntAttribute
+import Rattletrap.Encode.LoadoutAttribute
+import Rattletrap.Encode.LoadoutOnlineAttribute
+import Rattletrap.Encode.LoadoutsAttribute
+import Rattletrap.Encode.LoadoutsOnlineAttribute
+import Rattletrap.Encode.LocationAttribute
+import Rattletrap.Encode.MusicStingerAttribute
+import Rattletrap.Encode.PartyLeaderAttribute
+import Rattletrap.Encode.PickupAttribute
+import Rattletrap.Encode.PlayerHistoryKeyAttribute
+import Rattletrap.Encode.PrivateMatchSettingsAttribute
+import Rattletrap.Encode.QWordAttribute
+import Rattletrap.Encode.ReservationAttribute
+import Rattletrap.Encode.RigidBodyStateAttribute
+import Rattletrap.Encode.StringAttribute
+import Rattletrap.Encode.TeamPaintAttribute
+import Rattletrap.Encode.UniqueIdAttribute
+import Rattletrap.Encode.WeldedInfoAttribute
+import Rattletrap.Type.AttributeValue
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putAttributeValue :: AttributeValue -> BinaryBits.BitPut ()
+putAttributeValue value = case value of
+  AttributeValueAppliedDamage x -> putAppliedDamageAttribute x
+  AttributeValueBoolean x -> putBooleanAttribute x
+  AttributeValueByte x -> putByteAttribute x
+  AttributeValueCamSettings x -> putCamSettingsAttribute x
+  AttributeValueClubColors x -> putClubColorsAttribute x
+  AttributeValueDamageState x -> putDamageStateAttribute x
+  AttributeValueDemolish x -> putDemolishAttribute x
+  AttributeValueEnum x -> putEnumAttribute x
+  AttributeValueExplosion x -> putExplosionAttribute x
+  AttributeValueExtendedExplosion x -> putExtendedExplosionAttribute x
+  AttributeValueFlaggedInt x -> putFlaggedIntAttribute x
+  AttributeValueFloat x -> putFloatAttribute x
+  AttributeValueGameMode x -> putGameModeAttribute x
+  AttributeValueInt x -> putIntAttribute x
+  AttributeValueLoadout x -> putLoadoutAttribute x
+  AttributeValueLoadoutOnline x -> putLoadoutOnlineAttribute x
+  AttributeValueLoadouts x -> putLoadoutsAttribute x
+  AttributeValueLoadoutsOnline x -> putLoadoutsOnlineAttribute x
+  AttributeValueLocation x -> putLocationAttribute x
+  AttributeValueMusicStinger x -> putMusicStingerAttribute x
+  AttributeValuePartyLeader x -> putPartyLeaderAttribute x
+  AttributeValuePickup x -> putPickupAttribute x
+  AttributeValuePlayerHistoryKey x -> putPlayerHistoryKeyAttribute x
+  AttributeValuePrivateMatchSettings x -> putPrivateMatchSettingsAttribute x
+  AttributeValueQWord x -> putQWordAttribute x
+  AttributeValueReservation x -> putReservationAttribute x
+  AttributeValueRigidBodyState x -> putRigidBodyStateAttribute x
+  AttributeValueString x -> putStringAttribute x
+  AttributeValueTeamPaint x -> putTeamPaintAttribute x
+  AttributeValueUniqueId x -> putUniqueIdAttribute x
+  AttributeValueWeldedInfo x -> putWeldedInfoAttribute x
diff --git a/library/Rattletrap/Encode/Bitstream.hs b/library/Rattletrap/Encode/Bitstream.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Bitstream.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Encode.Bitstream
+  ( putBitstream
+  ) where
+
+import Rattletrap.Type.Bitstream
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putBitstream :: Bitstream -> BinaryBits.BitPut ()
+putBitstream = mapM_ BinaryBits.putBool . bitstreamValue
diff --git a/library/Rattletrap/Encode/BooleanAttribute.hs b/library/Rattletrap/Encode/BooleanAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/BooleanAttribute.hs
@@ -0,0 +1,11 @@
+module Rattletrap.Encode.BooleanAttribute
+  ( putBooleanAttribute
+  ) where
+
+import Rattletrap.Type.BooleanAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putBooleanAttribute :: BooleanAttribute -> BinaryBits.BitPut ()
+putBooleanAttribute booleanAttribute =
+  BinaryBits.putBool (booleanAttributeValue booleanAttribute)
diff --git a/library/Rattletrap/Encode/ByteAttribute.hs b/library/Rattletrap/Encode/ByteAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/ByteAttribute.hs
@@ -0,0 +1,12 @@
+module Rattletrap.Encode.ByteAttribute
+  ( putByteAttribute
+  ) where
+
+import Rattletrap.Encode.Word8le
+import Rattletrap.Type.ByteAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putByteAttribute :: ByteAttribute -> BinaryBits.BitPut ()
+putByteAttribute byteAttribute =
+  putWord8Bits (byteAttributeValue byteAttribute)
diff --git a/library/Rattletrap/Encode/Cache.hs b/library/Rattletrap/Encode/Cache.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Cache.hs
@@ -0,0 +1,17 @@
+module Rattletrap.Encode.Cache
+  ( putCache
+  ) where
+
+import Rattletrap.Encode.AttributeMapping
+import Rattletrap.Encode.List
+import Rattletrap.Encode.Word32le
+import Rattletrap.Type.Cache
+
+import qualified Data.Binary as Binary
+
+putCache :: Cache -> Binary.Put
+putCache cache = do
+  putWord32 (cacheClassId cache)
+  putWord32 (cacheParentCacheId cache)
+  putWord32 (cacheCacheId cache)
+  putList putAttributeMapping (cacheAttributeMappings cache)
diff --git a/library/Rattletrap/Encode/CamSettingsAttribute.hs b/library/Rattletrap/Encode/CamSettingsAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/CamSettingsAttribute.hs
@@ -0,0 +1,20 @@
+module Rattletrap.Encode.CamSettingsAttribute
+  ( putCamSettingsAttribute
+  ) where
+
+import Rattletrap.Encode.Float32le
+import Rattletrap.Type.CamSettingsAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putCamSettingsAttribute :: CamSettingsAttribute -> BinaryBits.BitPut ()
+putCamSettingsAttribute camSettingsAttribute = do
+  putFloat32Bits (camSettingsAttributeFov camSettingsAttribute)
+  putFloat32Bits (camSettingsAttributeHeight camSettingsAttribute)
+  putFloat32Bits (camSettingsAttributeAngle camSettingsAttribute)
+  putFloat32Bits (camSettingsAttributeDistance camSettingsAttribute)
+  putFloat32Bits (camSettingsAttributeStiffness camSettingsAttribute)
+  putFloat32Bits (camSettingsAttributeSwivelSpeed camSettingsAttribute)
+  case camSettingsAttributeTransitionSpeed camSettingsAttribute of
+    Nothing -> pure ()
+    Just transitionSpeed -> putFloat32Bits transitionSpeed
diff --git a/library/Rattletrap/Encode/ClassMapping.hs b/library/Rattletrap/Encode/ClassMapping.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/ClassMapping.hs
@@ -0,0 +1,14 @@
+module Rattletrap.Encode.ClassMapping
+  ( putClassMapping
+  ) where
+
+import Rattletrap.Encode.Str
+import Rattletrap.Encode.Word32le
+import Rattletrap.Type.ClassMapping
+
+import qualified Data.Binary as Binary
+
+putClassMapping :: ClassMapping -> Binary.Put
+putClassMapping classMapping = do
+  putText (classMappingName classMapping)
+  putWord32 (classMappingStreamId classMapping)
diff --git a/library/Rattletrap/Encode/ClubColorsAttribute.hs b/library/Rattletrap/Encode/ClubColorsAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/ClubColorsAttribute.hs
@@ -0,0 +1,15 @@
+module Rattletrap.Encode.ClubColorsAttribute
+  ( putClubColorsAttribute
+  ) where
+
+import Rattletrap.Encode.Word8le
+import Rattletrap.Type.ClubColorsAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putClubColorsAttribute :: ClubColorsAttribute -> BinaryBits.BitPut ()
+putClubColorsAttribute clubColorsAttribute = do
+  BinaryBits.putBool (clubColorsAttributeBlueFlag clubColorsAttribute)
+  putWord8Bits (clubColorsAttributeBlueColor clubColorsAttribute)
+  BinaryBits.putBool (clubColorsAttributeOrangeFlag clubColorsAttribute)
+  putWord8Bits (clubColorsAttributeOrangeColor clubColorsAttribute)
diff --git a/library/Rattletrap/Encode/CompressedWord.hs b/library/Rattletrap/Encode/CompressedWord.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/CompressedWord.hs
@@ -0,0 +1,43 @@
+module Rattletrap.Encode.CompressedWord
+  ( putCompressedWord
+  ) where
+
+import Rattletrap.Type.CompressedWord
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+import qualified Data.Bits as Bits
+
+putCompressedWord :: CompressedWord -> BinaryBits.BitPut ()
+putCompressedWord compressedWord =
+  let
+    limit = compressedWordLimit compressedWord
+    value = compressedWordValue compressedWord
+    maxBits = getMaxBits limit
+  in putCompressedWordStep limit value maxBits 0 0
+
+putCompressedWordStep
+  :: Word -> Word -> Int -> Int -> Word -> BinaryBits.BitPut ()
+putCompressedWordStep limit value maxBits position soFar =
+  if position < maxBits
+    then do
+      let x = Bits.shiftL 1 position :: Word
+      if maxBits > 1 && position == maxBits - 1 && soFar + x > limit
+        then pure ()
+        else do
+          let bit = Bits.testBit value position
+          BinaryBits.putBool bit
+          let delta = if bit then x else 0
+          putCompressedWordStep
+            limit
+            value
+            maxBits
+            (position + 1)
+            (soFar + delta)
+    else pure ()
+
+getMaxBits :: Word -> Int
+getMaxBits x =
+  let
+    n :: Int
+    n = max 1 (ceiling (logBase (2 :: Double) (fromIntegral (max 1 x))))
+  in if x < 1024 && x == 2 ^ n then n + 1 else n
diff --git a/library/Rattletrap/Encode/CompressedWordVector.hs b/library/Rattletrap/Encode/CompressedWordVector.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/CompressedWordVector.hs
@@ -0,0 +1,14 @@
+module Rattletrap.Encode.CompressedWordVector
+  ( putCompressedWordVector
+  ) where
+
+import Rattletrap.Encode.CompressedWord
+import Rattletrap.Type.CompressedWordVector
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putCompressedWordVector :: CompressedWordVector -> BinaryBits.BitPut ()
+putCompressedWordVector compressedWordVector = do
+  putCompressedWord (compressedWordVectorX compressedWordVector)
+  putCompressedWord (compressedWordVectorY compressedWordVector)
+  putCompressedWord (compressedWordVectorZ compressedWordVector)
diff --git a/library/Rattletrap/Encode/Content.hs b/library/Rattletrap/Encode/Content.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Content.hs
@@ -0,0 +1,39 @@
+module Rattletrap.Encode.Content
+  ( putContent
+  ) where
+
+import Rattletrap.Encode.Cache
+import Rattletrap.Encode.ClassMapping
+import Rattletrap.Encode.Frame
+import Rattletrap.Encode.KeyFrame
+import Rattletrap.Encode.List
+import Rattletrap.Encode.Mark
+import Rattletrap.Encode.Message
+import Rattletrap.Encode.Str
+import Rattletrap.Encode.Word32le
+import Rattletrap.Type.Content
+import Rattletrap.Type.Word32le
+import Rattletrap.Utility.Bytes
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits.Put as BinaryBits
+import qualified Data.Binary.Put as Binary
+
+putContent :: Content -> Binary.Put
+putContent content = do
+  putList putText (contentLevels content)
+  putList putKeyFrame (contentKeyFrames content)
+  let streamSize = contentStreamSize content
+  putWord32 streamSize
+  let
+    stream =
+      Binary.runPut (BinaryBits.runBitPut (putFrames (contentFrames content)))
+  Binary.putLazyByteString
+    (reverseBytes (padBytes (word32leValue streamSize) stream))
+  putList putMessage (contentMessages content)
+  putList putMark (contentMarks content)
+  putList putText (contentPackages content)
+  putList putText (contentObjects content)
+  putList putText (contentNames content)
+  putList putClassMapping (contentClassMappings content)
+  putList putCache (contentCaches content)
diff --git a/library/Rattletrap/Encode/DamageStateAttribute.hs b/library/Rattletrap/Encode/DamageStateAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/DamageStateAttribute.hs
@@ -0,0 +1,19 @@
+module Rattletrap.Encode.DamageStateAttribute
+  ( putDamageStateAttribute
+  ) where
+
+import Rattletrap.Encode.Int32le
+import Rattletrap.Encode.Vector
+import Rattletrap.Encode.Word8le
+import Rattletrap.Type.DamageStateAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putDamageStateAttribute :: DamageStateAttribute -> BinaryBits.BitPut ()
+putDamageStateAttribute damageStateAttribute = do
+  putWord8Bits (damageStateAttributeUnknown1 damageStateAttribute)
+  BinaryBits.putBool (damageStateAttributeUnknown2 damageStateAttribute)
+  putInt32Bits (damageStateAttributeUnknown3 damageStateAttribute)
+  putVector (damageStateAttributeUnknown4 damageStateAttribute)
+  BinaryBits.putBool (damageStateAttributeUnknown5 damageStateAttribute)
+  BinaryBits.putBool (damageStateAttributeUnknown6 damageStateAttribute)
diff --git a/library/Rattletrap/Encode/DemolishAttribute.hs b/library/Rattletrap/Encode/DemolishAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/DemolishAttribute.hs
@@ -0,0 +1,18 @@
+module Rattletrap.Encode.DemolishAttribute
+  ( putDemolishAttribute
+  ) where
+
+import Rattletrap.Encode.Vector
+import Rattletrap.Encode.Word32le
+import Rattletrap.Type.DemolishAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putDemolishAttribute :: DemolishAttribute -> BinaryBits.BitPut ()
+putDemolishAttribute demolishAttribute = do
+  BinaryBits.putBool (demolishAttributeAttackerFlag demolishAttribute)
+  putWord32Bits (demolishAttributeAttackerActorId demolishAttribute)
+  BinaryBits.putBool (demolishAttributeVictimFlag demolishAttribute)
+  putWord32Bits (demolishAttributeVictimActorId demolishAttribute)
+  putVector (demolishAttributeAttackerVelocity demolishAttribute)
+  putVector (demolishAttributeVictimVelocity demolishAttribute)
diff --git a/library/Rattletrap/Encode/DestroyedReplication.hs b/library/Rattletrap/Encode/DestroyedReplication.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/DestroyedReplication.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Encode.DestroyedReplication
+  ( putDestroyedReplication
+  ) where
+
+import Rattletrap.Type.DestroyedReplication
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putDestroyedReplication :: DestroyedReplication -> BinaryBits.BitPut ()
+putDestroyedReplication _ = pure ()
diff --git a/library/Rattletrap/Encode/Dictionary.hs b/library/Rattletrap/Encode/Dictionary.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Dictionary.hs
@@ -0,0 +1,16 @@
+module Rattletrap.Encode.Dictionary
+  ( putDictionary
+  ) where
+
+import Rattletrap.Encode.Str
+import Rattletrap.Type.Dictionary
+
+import qualified Data.Binary as Binary
+
+putDictionary :: (a -> Binary.Put) -> Dictionary a -> Binary.Put
+putDictionary f x = case x of
+  DictionaryElement k v y -> do
+    putText k
+    f v
+    putDictionary f y
+  DictionaryEnd y -> putText y
diff --git a/library/Rattletrap/Encode/EnumAttribute.hs b/library/Rattletrap/Encode/EnumAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/EnumAttribute.hs
@@ -0,0 +1,11 @@
+module Rattletrap.Encode.EnumAttribute
+  ( putEnumAttribute
+  ) where
+
+import Rattletrap.Type.EnumAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putEnumAttribute :: EnumAttribute -> BinaryBits.BitPut ()
+putEnumAttribute enumAttribute =
+  BinaryBits.putWord16be 11 (enumAttributeValue enumAttribute)
diff --git a/library/Rattletrap/Encode/ExplosionAttribute.hs b/library/Rattletrap/Encode/ExplosionAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/ExplosionAttribute.hs
@@ -0,0 +1,15 @@
+module Rattletrap.Encode.ExplosionAttribute
+  ( putExplosionAttribute
+  ) where
+
+import Rattletrap.Encode.Int32le
+import Rattletrap.Encode.Vector
+import Rattletrap.Type.ExplosionAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putExplosionAttribute :: ExplosionAttribute -> BinaryBits.BitPut ()
+putExplosionAttribute explosionAttribute = do
+  BinaryBits.putBool False
+  putInt32Bits (explosionAttributeActorId explosionAttribute)
+  putVector (explosionAttributeLocation explosionAttribute)
diff --git a/library/Rattletrap/Encode/ExtendedExplosionAttribute.hs b/library/Rattletrap/Encode/ExtendedExplosionAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/ExtendedExplosionAttribute.hs
@@ -0,0 +1,15 @@
+module Rattletrap.Encode.ExtendedExplosionAttribute
+  ( putExtendedExplosionAttribute
+  ) where
+
+import Rattletrap.Encode.ExplosionAttribute
+import Rattletrap.Encode.FlaggedIntAttribute
+import Rattletrap.Type.ExtendedExplosionAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putExtendedExplosionAttribute
+  :: ExtendedExplosionAttribute -> BinaryBits.BitPut ()
+putExtendedExplosionAttribute x = do
+  putExplosionAttribute (extendedExplosionAttributeExplosion x)
+  putFlaggedIntAttribute (extendedExplosionAttributeUnknown x)
diff --git a/library/Rattletrap/Encode/FlaggedIntAttribute.hs b/library/Rattletrap/Encode/FlaggedIntAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/FlaggedIntAttribute.hs
@@ -0,0 +1,13 @@
+module Rattletrap.Encode.FlaggedIntAttribute
+  ( putFlaggedIntAttribute
+  ) where
+
+import Rattletrap.Encode.Int32le
+import Rattletrap.Type.FlaggedIntAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putFlaggedIntAttribute :: FlaggedIntAttribute -> BinaryBits.BitPut ()
+putFlaggedIntAttribute flaggedIntAttribute = do
+  BinaryBits.putBool (flaggedIntAttributeFlag flaggedIntAttribute)
+  putInt32Bits (flaggedIntAttributeInt flaggedIntAttribute)
diff --git a/library/Rattletrap/Encode/Float32le.hs b/library/Rattletrap/Encode/Float32le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Float32le.hs
@@ -0,0 +1,20 @@
+module Rattletrap.Encode.Float32le
+  ( putFloat32
+  , putFloat32Bits
+  ) where
+
+import Rattletrap.Type.Float32le
+import Rattletrap.Utility.Bytes
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits.Put as BinaryBits
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+
+putFloat32 :: Float32le -> Binary.Put
+putFloat32 = Binary.putFloatle . float32leValue
+
+putFloat32Bits :: Float32le -> BinaryBits.BitPut ()
+putFloat32Bits float32 = do
+  let bytes = Binary.runPut (putFloat32 float32)
+  BinaryBits.putByteString (LazyBytes.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Encode/FloatAttribute.hs b/library/Rattletrap/Encode/FloatAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/FloatAttribute.hs
@@ -0,0 +1,12 @@
+module Rattletrap.Encode.FloatAttribute
+  ( putFloatAttribute
+  ) where
+
+import Rattletrap.Encode.Float32le
+import Rattletrap.Type.FloatAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putFloatAttribute :: FloatAttribute -> BinaryBits.BitPut ()
+putFloatAttribute floatAttribute =
+  putFloat32Bits (floatAttributeValue floatAttribute)
diff --git a/library/Rattletrap/Encode/Frame.hs b/library/Rattletrap/Encode/Frame.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Frame.hs
@@ -0,0 +1,19 @@
+module Rattletrap.Encode.Frame
+  ( putFrames
+  , putFrame
+  ) where
+
+import Rattletrap.Encode.Float32le
+import Rattletrap.Encode.Replication
+import Rattletrap.Type.Frame
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putFrames :: [Frame] -> BinaryBits.BitPut ()
+putFrames = mapM_ putFrame
+
+putFrame :: Frame -> BinaryBits.BitPut ()
+putFrame frame = do
+  putFloat32Bits (frameTime frame)
+  putFloat32Bits (frameDelta frame)
+  putReplications (frameReplications frame)
diff --git a/library/Rattletrap/Encode/GameModeAttribute.hs b/library/Rattletrap/Encode/GameModeAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/GameModeAttribute.hs
@@ -0,0 +1,13 @@
+module Rattletrap.Encode.GameModeAttribute
+  ( putGameModeAttribute
+  ) where
+
+import Rattletrap.Type.GameModeAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putGameModeAttribute :: GameModeAttribute -> BinaryBits.BitPut ()
+putGameModeAttribute gameModeAttribute = do
+  let numBits = gameModeAttributeNumBits gameModeAttribute
+  let word = gameModeAttributeWord gameModeAttribute
+  BinaryBits.putWord8 numBits word
diff --git a/library/Rattletrap/Encode/Header.hs b/library/Rattletrap/Encode/Header.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Header.hs
@@ -0,0 +1,21 @@
+module Rattletrap.Encode.Header
+  ( putHeader
+  ) where
+
+import Rattletrap.Encode.Dictionary
+import Rattletrap.Encode.Property
+import Rattletrap.Encode.Str
+import Rattletrap.Encode.Word32le
+import Rattletrap.Type.Header
+
+import qualified Data.Binary as Binary
+
+putHeader :: Header -> Binary.Put
+putHeader header = do
+  putWord32 (headerEngineVersion header)
+  putWord32 (headerLicenseeVersion header)
+  case headerPatchVersion header of
+    Nothing -> pure ()
+    Just patchVersion -> putWord32 patchVersion
+  putText (headerLabel header)
+  putDictionary putProperty (headerProperties header)
diff --git a/library/Rattletrap/Encode/Initialization.hs b/library/Rattletrap/Encode/Initialization.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Initialization.hs
@@ -0,0 +1,18 @@
+module Rattletrap.Encode.Initialization
+  ( putInitialization
+  ) where
+
+import Rattletrap.Encode.Int8Vector
+import Rattletrap.Encode.Vector
+import Rattletrap.Type.Initialization
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putInitialization :: Initialization -> BinaryBits.BitPut ()
+putInitialization initialization = do
+  case initializationLocation initialization of
+    Nothing -> pure ()
+    Just location -> putVector location
+  case initializationRotation initialization of
+    Nothing -> pure ()
+    Just rotation -> putInt8Vector rotation
diff --git a/library/Rattletrap/Encode/Int32le.hs b/library/Rattletrap/Encode/Int32le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Int32le.hs
@@ -0,0 +1,20 @@
+module Rattletrap.Encode.Int32le
+  ( putInt32
+  , putInt32Bits
+  ) where
+
+import Rattletrap.Type.Int32le
+import Rattletrap.Utility.Bytes
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits.Put as BinaryBits
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+
+putInt32 :: Int32le -> Binary.Put
+putInt32 int32 = Binary.putInt32le (int32leValue int32)
+
+putInt32Bits :: Int32le -> BinaryBits.BitPut ()
+putInt32Bits int32 = do
+  let bytes = Binary.runPut (putInt32 int32)
+  BinaryBits.putByteString (LazyBytes.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Encode/Int8Vector.hs b/library/Rattletrap/Encode/Int8Vector.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Int8Vector.hs
@@ -0,0 +1,22 @@
+module Rattletrap.Encode.Int8Vector
+  ( putInt8Vector
+  ) where
+
+import Rattletrap.Encode.Int8le
+import Rattletrap.Type.Int8Vector
+import Rattletrap.Type.Int8le
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putInt8Vector :: Int8Vector -> BinaryBits.BitPut ()
+putInt8Vector int8Vector = do
+  putInt8VectorField (int8VectorX int8Vector)
+  putInt8VectorField (int8VectorY int8Vector)
+  putInt8VectorField (int8VectorZ int8Vector)
+
+putInt8VectorField :: Maybe Int8le -> BinaryBits.BitPut ()
+putInt8VectorField maybeField = case maybeField of
+  Nothing -> BinaryBits.putBool False
+  Just field -> do
+    BinaryBits.putBool True
+    putInt8Bits field
diff --git a/library/Rattletrap/Encode/Int8le.hs b/library/Rattletrap/Encode/Int8le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Int8le.hs
@@ -0,0 +1,19 @@
+module Rattletrap.Encode.Int8le
+  ( putInt8
+  , putInt8Bits
+  ) where
+
+import Rattletrap.Type.Int8le
+import Rattletrap.Utility.Bytes
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+
+putInt8 :: Int8le -> Binary.Put
+putInt8 int8 = Binary.putInt8 (int8leValue int8)
+
+putInt8Bits :: Int8le -> BinaryBits.BitPut ()
+putInt8Bits int8 = do
+  let bytes = Binary.runPut (putInt8 int8)
+  BinaryBits.putByteString (LazyBytes.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Encode/IntAttribute.hs b/library/Rattletrap/Encode/IntAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/IntAttribute.hs
@@ -0,0 +1,11 @@
+module Rattletrap.Encode.IntAttribute
+  ( putIntAttribute
+  ) where
+
+import Rattletrap.Encode.Int32le
+import Rattletrap.Type.IntAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putIntAttribute :: IntAttribute -> BinaryBits.BitPut ()
+putIntAttribute intAttribute = putInt32Bits (intAttributeValue intAttribute)
diff --git a/library/Rattletrap/Encode/KeyFrame.hs b/library/Rattletrap/Encode/KeyFrame.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/KeyFrame.hs
@@ -0,0 +1,15 @@
+module Rattletrap.Encode.KeyFrame
+  ( putKeyFrame
+  ) where
+
+import Rattletrap.Encode.Float32le
+import Rattletrap.Encode.Word32le
+import Rattletrap.Type.KeyFrame
+
+import qualified Data.Binary as Binary
+
+putKeyFrame :: KeyFrame -> Binary.Put
+putKeyFrame keyFrame = do
+  putFloat32 (keyFrameTime keyFrame)
+  putWord32 (keyFrameFrame keyFrame)
+  putWord32 (keyFramePosition keyFrame)
diff --git a/library/Rattletrap/Encode/List.hs b/library/Rattletrap/Encode/List.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/List.hs
@@ -0,0 +1,15 @@
+module Rattletrap.Encode.List
+  ( putList
+  ) where
+
+import Rattletrap.Encode.Word32le
+import Rattletrap.Type.List
+import Rattletrap.Type.Word32le
+
+import qualified Data.Binary as Binary
+
+putList :: (a -> Binary.Put) -> List a -> Binary.Put
+putList putElement list = do
+  let elements = listValue list
+  putWord32 (Word32le (fromIntegral (length elements)))
+  mapM_ putElement elements
diff --git a/library/Rattletrap/Encode/LoadoutAttribute.hs b/library/Rattletrap/Encode/LoadoutAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/LoadoutAttribute.hs
@@ -0,0 +1,30 @@
+module Rattletrap.Encode.LoadoutAttribute
+  ( putLoadoutAttribute
+  ) where
+
+import Rattletrap.Encode.Word32le
+import Rattletrap.Encode.Word8le
+import Rattletrap.Type.LoadoutAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putLoadoutAttribute :: LoadoutAttribute -> BinaryBits.BitPut ()
+putLoadoutAttribute loadoutAttribute = do
+  putWord8Bits (loadoutAttributeVersion loadoutAttribute)
+  putWord32Bits (loadoutAttributeBody loadoutAttribute)
+  putWord32Bits (loadoutAttributeDecal loadoutAttribute)
+  putWord32Bits (loadoutAttributeWheels loadoutAttribute)
+  putWord32Bits (loadoutAttributeRocketTrail loadoutAttribute)
+  putWord32Bits (loadoutAttributeAntenna loadoutAttribute)
+  putWord32Bits (loadoutAttributeTopper loadoutAttribute)
+  putWord32Bits (loadoutAttributeUnknown1 loadoutAttribute)
+  putOptional (loadoutAttributeUnknown2 loadoutAttribute) putWord32Bits
+  putOptional (loadoutAttributeEngineAudio loadoutAttribute) putWord32Bits
+  putOptional (loadoutAttributeTrail loadoutAttribute) putWord32Bits
+  putOptional (loadoutAttributeGoalExplosion loadoutAttribute) putWord32Bits
+  putOptional (loadoutAttributeBanner loadoutAttribute) putWord32Bits
+
+putOptional :: Maybe a -> (a -> BinaryBits.BitPut ()) -> BinaryBits.BitPut ()
+putOptional m f = case m of
+  Just x -> f x
+  Nothing -> pure ()
diff --git a/library/Rattletrap/Encode/LoadoutOnlineAttribute.hs b/library/Rattletrap/Encode/LoadoutOnlineAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/LoadoutOnlineAttribute.hs
@@ -0,0 +1,16 @@
+module Rattletrap.Encode.LoadoutOnlineAttribute
+  ( putLoadoutOnlineAttribute
+  ) where
+
+import Rattletrap.Encode.ProductAttribute
+import Rattletrap.Encode.Word8le
+import Rattletrap.Type.LoadoutOnlineAttribute
+import Rattletrap.Type.Word8le
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putLoadoutOnlineAttribute :: LoadoutOnlineAttribute -> BinaryBits.BitPut ()
+putLoadoutOnlineAttribute loadoutAttribute = do
+  let attributes = loadoutAttributeValue loadoutAttribute
+  putWord8Bits (Word8le (fromIntegral (length attributes)))
+  mapM_ putProductAttributes attributes
diff --git a/library/Rattletrap/Encode/LoadoutsAttribute.hs b/library/Rattletrap/Encode/LoadoutsAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/LoadoutsAttribute.hs
@@ -0,0 +1,13 @@
+module Rattletrap.Encode.LoadoutsAttribute
+  ( putLoadoutsAttribute
+  ) where
+
+import Rattletrap.Encode.LoadoutAttribute
+import Rattletrap.Type.LoadoutsAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putLoadoutsAttribute :: LoadoutsAttribute -> BinaryBits.BitPut ()
+putLoadoutsAttribute loadoutsAttribute = do
+  putLoadoutAttribute (loadoutsAttributeBlue loadoutsAttribute)
+  putLoadoutAttribute (loadoutsAttributeOrange loadoutsAttribute)
diff --git a/library/Rattletrap/Encode/LoadoutsOnlineAttribute.hs b/library/Rattletrap/Encode/LoadoutsOnlineAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/LoadoutsOnlineAttribute.hs
@@ -0,0 +1,17 @@
+module Rattletrap.Encode.LoadoutsOnlineAttribute
+  ( putLoadoutsOnlineAttribute
+  ) where
+
+import Rattletrap.Encode.LoadoutOnlineAttribute
+import Rattletrap.Type.LoadoutsOnlineAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putLoadoutsOnlineAttribute :: LoadoutsOnlineAttribute -> BinaryBits.BitPut ()
+putLoadoutsOnlineAttribute loadoutsOnlineAttribute = do
+  putLoadoutOnlineAttribute
+    (loadoutsOnlineAttributeBlue loadoutsOnlineAttribute)
+  putLoadoutOnlineAttribute
+    (loadoutsOnlineAttributeOrange loadoutsOnlineAttribute)
+  BinaryBits.putBool (loadoutsOnlineAttributeUnknown1 loadoutsOnlineAttribute)
+  BinaryBits.putBool (loadoutsOnlineAttributeUnknown2 loadoutsOnlineAttribute)
diff --git a/library/Rattletrap/Encode/LocationAttribute.hs b/library/Rattletrap/Encode/LocationAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/LocationAttribute.hs
@@ -0,0 +1,12 @@
+module Rattletrap.Encode.LocationAttribute
+  ( putLocationAttribute
+  ) where
+
+import Rattletrap.Encode.Vector
+import Rattletrap.Type.LocationAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putLocationAttribute :: LocationAttribute -> BinaryBits.BitPut ()
+putLocationAttribute locationAttribute =
+  putVector (locationAttributeValue locationAttribute)
diff --git a/library/Rattletrap/Encode/Mark.hs b/library/Rattletrap/Encode/Mark.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Mark.hs
@@ -0,0 +1,14 @@
+module Rattletrap.Encode.Mark
+  ( putMark
+  ) where
+
+import Rattletrap.Encode.Str
+import Rattletrap.Encode.Word32le
+import Rattletrap.Type.Mark
+
+import qualified Data.Binary as Binary
+
+putMark :: Mark -> Binary.Put
+putMark mark = do
+  putText (markValue mark)
+  putWord32 (markFrame mark)
diff --git a/library/Rattletrap/Encode/Message.hs b/library/Rattletrap/Encode/Message.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Message.hs
@@ -0,0 +1,15 @@
+module Rattletrap.Encode.Message
+  ( putMessage
+  ) where
+
+import Rattletrap.Encode.Str
+import Rattletrap.Encode.Word32le
+import Rattletrap.Type.Message
+
+import qualified Data.Binary as Binary
+
+putMessage :: Message -> Binary.Put
+putMessage message = do
+  putWord32 (messageFrame message)
+  putText (messageName message)
+  putText (messageValue message)
diff --git a/library/Rattletrap/Encode/MusicStingerAttribute.hs b/library/Rattletrap/Encode/MusicStingerAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/MusicStingerAttribute.hs
@@ -0,0 +1,15 @@
+module Rattletrap.Encode.MusicStingerAttribute
+  ( putMusicStingerAttribute
+  ) where
+
+import Rattletrap.Encode.Word32le
+import Rattletrap.Encode.Word8le
+import Rattletrap.Type.MusicStingerAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putMusicStingerAttribute :: MusicStingerAttribute -> BinaryBits.BitPut ()
+putMusicStingerAttribute musicStingerAttribute = do
+  BinaryBits.putBool (musicStingerAttributeFlag musicStingerAttribute)
+  putWord32Bits (musicStingerAttributeCue musicStingerAttribute)
+  putWord8Bits (musicStingerAttributeTrigger musicStingerAttribute)
diff --git a/library/Rattletrap/Encode/PartyLeaderAttribute.hs b/library/Rattletrap/Encode/PartyLeaderAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/PartyLeaderAttribute.hs
@@ -0,0 +1,18 @@
+module Rattletrap.Encode.PartyLeaderAttribute
+  ( putPartyLeaderAttribute
+  ) where
+
+import Rattletrap.Encode.RemoteId
+import Rattletrap.Encode.Word8le
+import Rattletrap.Type.PartyLeaderAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putPartyLeaderAttribute :: PartyLeaderAttribute -> BinaryBits.BitPut ()
+putPartyLeaderAttribute partyLeaderAttribute = do
+  putWord8Bits (partyLeaderAttributeSystemId partyLeaderAttribute)
+  case partyLeaderAttributeId partyLeaderAttribute of
+    Nothing -> pure ()
+    Just (remoteId, localId) -> do
+      putRemoteId remoteId
+      putWord8Bits localId
diff --git a/library/Rattletrap/Encode/PickupAttribute.hs b/library/Rattletrap/Encode/PickupAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/PickupAttribute.hs
@@ -0,0 +1,17 @@
+module Rattletrap.Encode.PickupAttribute
+  ( putPickupAttribute
+  ) where
+
+import Rattletrap.Encode.Word32le
+import Rattletrap.Type.PickupAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putPickupAttribute :: PickupAttribute -> BinaryBits.BitPut ()
+putPickupAttribute pickupAttribute = do
+  case pickupAttributeInstigatorId pickupAttribute of
+    Nothing -> BinaryBits.putBool False
+    Just instigatorId -> do
+      BinaryBits.putBool True
+      putWord32Bits instigatorId
+  BinaryBits.putBool (pickupAttributePickedUp pickupAttribute)
diff --git a/library/Rattletrap/Encode/PlayerHistoryKeyAttribute.hs b/library/Rattletrap/Encode/PlayerHistoryKeyAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/PlayerHistoryKeyAttribute.hs
@@ -0,0 +1,12 @@
+module Rattletrap.Encode.PlayerHistoryKeyAttribute
+  ( putPlayerHistoryKeyAttribute
+  ) where
+
+import Rattletrap.Encode.Bitstream
+import Rattletrap.Type.PlayerHistoryKeyAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putPlayerHistoryKeyAttribute
+  :: PlayerHistoryKeyAttribute -> BinaryBits.BitPut ()
+putPlayerHistoryKeyAttribute = putBitstream . playerHistoryKeyAttributeUnknown
diff --git a/library/Rattletrap/Encode/PrivateMatchSettingsAttribute.hs b/library/Rattletrap/Encode/PrivateMatchSettingsAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/PrivateMatchSettingsAttribute.hs
@@ -0,0 +1,25 @@
+module Rattletrap.Encode.PrivateMatchSettingsAttribute
+  ( putPrivateMatchSettingsAttribute
+  ) where
+
+import Rattletrap.Encode.Str
+import Rattletrap.Encode.Word32le
+import Rattletrap.Type.PrivateMatchSettingsAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putPrivateMatchSettingsAttribute
+  :: PrivateMatchSettingsAttribute -> BinaryBits.BitPut ()
+putPrivateMatchSettingsAttribute privateMatchSettingsAttribute = do
+  putTextBits
+    (privateMatchSettingsAttributeMutators privateMatchSettingsAttribute)
+  putWord32Bits
+    (privateMatchSettingsAttributeJoinableBy privateMatchSettingsAttribute)
+  putWord32Bits
+    (privateMatchSettingsAttributeMaxPlayers privateMatchSettingsAttribute)
+  putTextBits
+    (privateMatchSettingsAttributeGameName privateMatchSettingsAttribute)
+  putTextBits
+    (privateMatchSettingsAttributePassword privateMatchSettingsAttribute)
+  BinaryBits.putBool
+    (privateMatchSettingsAttributeFlag privateMatchSettingsAttribute)
diff --git a/library/Rattletrap/Encode/ProductAttribute.hs b/library/Rattletrap/Encode/ProductAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/ProductAttribute.hs
@@ -0,0 +1,43 @@
+module Rattletrap.Encode.ProductAttribute
+  ( putProductAttributes
+  , putProductAttribute
+  ) where
+
+import Data.Semigroup ((<>))
+import Rattletrap.Encode.CompressedWord
+import Rattletrap.Encode.Word32le
+import Rattletrap.Encode.Word8le
+import Rattletrap.Type.ProductAttribute
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word8le
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putProductAttributes :: [ProductAttribute] -> BinaryBits.BitPut ()
+putProductAttributes attributes = do
+  putWord8Bits (Word8le (fromIntegral (length attributes)))
+  mapM_ putProductAttribute attributes
+
+putProductAttribute :: ProductAttribute -> BinaryBits.BitPut ()
+putProductAttribute attribute = do
+  BinaryBits.putBool (productAttributeUnknown attribute)
+  putWord32Bits (productAttributeObjectId attribute)
+  case productAttributeObjectName attribute of
+    Just name -> case fromStr name of
+      "TAGame.ProductAttribute_Painted_TA" ->
+        case productAttributeValue attribute of
+          Nothing -> pure ()
+          Just (Left x) -> putCompressedWord x
+          Just (Right x) -> BinaryBits.putWord32be 31 x
+      "TAGame.ProductAttribute_UserColor_TA" ->
+        case productAttributeValue attribute of
+          Nothing -> BinaryBits.putBool False
+          Just value -> do
+            BinaryBits.putBool True
+            case value of
+              Left x -> putCompressedWord x
+              Right x -> BinaryBits.putWord32be 31 x
+      _ ->
+        fail ("unknown object name for product attribute " <> show attribute)
+    Nothing ->
+      fail ("missing object name for product attribute " <> show attribute)
diff --git a/library/Rattletrap/Encode/Property.hs b/library/Rattletrap/Encode/Property.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Property.hs
@@ -0,0 +1,16 @@
+module Rattletrap.Encode.Property
+  ( putProperty
+  ) where
+
+import Rattletrap.Encode.PropertyValue
+import Rattletrap.Encode.Str
+import Rattletrap.Encode.Word64le
+import Rattletrap.Type.Property
+
+import qualified Data.Binary as Binary
+
+putProperty :: Property -> Binary.Put
+putProperty property = do
+  putText (propertyKind property)
+  putWord64 (propertySize property)
+  putPropertyValue putProperty (propertyValue property)
diff --git a/library/Rattletrap/Encode/PropertyValue.hs b/library/Rattletrap/Encode/PropertyValue.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/PropertyValue.hs
@@ -0,0 +1,29 @@
+module Rattletrap.Encode.PropertyValue
+  ( putPropertyValue
+  ) where
+
+import Rattletrap.Encode.Dictionary
+import Rattletrap.Encode.Float32le
+import Rattletrap.Encode.Int32le
+import Rattletrap.Encode.List
+import Rattletrap.Encode.Str
+import Rattletrap.Encode.Word64le
+import Rattletrap.Encode.Word8le
+import Rattletrap.Type.PropertyValue
+
+import qualified Data.Binary as Binary
+
+putPropertyValue :: (a -> Binary.Put) -> PropertyValue a -> Binary.Put
+putPropertyValue putProperty value = case value of
+  PropertyValueArray list -> putList (putDictionary putProperty) list
+  PropertyValueBool word8 -> putWord8 word8
+  PropertyValueByte k mv -> do
+    putText k
+    case mv of
+      Nothing -> pure ()
+      Just v -> putText v
+  PropertyValueFloat float32 -> putFloat32 float32
+  PropertyValueInt int32 -> putInt32 int32
+  PropertyValueName text -> putText text
+  PropertyValueQWord word64 -> putWord64 word64
+  PropertyValueStr text -> putText text
diff --git a/library/Rattletrap/Encode/QWordAttribute.hs b/library/Rattletrap/Encode/QWordAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/QWordAttribute.hs
@@ -0,0 +1,12 @@
+module Rattletrap.Encode.QWordAttribute
+  ( putQWordAttribute
+  ) where
+
+import Rattletrap.Encode.Word64le
+import Rattletrap.Type.QWordAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putQWordAttribute :: QWordAttribute -> BinaryBits.BitPut ()
+putQWordAttribute qWordAttribute =
+  putWord64Bits (qWordAttributeValue qWordAttribute)
diff --git a/library/Rattletrap/Encode/RemoteId.hs b/library/Rattletrap/Encode/RemoteId.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/RemoteId.hs
@@ -0,0 +1,24 @@
+module Rattletrap.Encode.RemoteId
+  ( putRemoteId
+  ) where
+
+import Rattletrap.Encode.Bitstream
+import Rattletrap.Encode.Word64le
+import Rattletrap.Type.RemoteId
+import Rattletrap.Utility.Bytes
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+import qualified Data.ByteString.Lazy as LazyBytes
+
+putRemoteId :: RemoteId -> BinaryBits.BitPut ()
+putRemoteId remoteId = case remoteId of
+  RemoteIdPlayStation name bytes -> do
+    let
+      rawName = LazyBytes.toStrict
+        (reverseBytes (padBytes (16 :: Int) (encodeLatin1 name)))
+    BinaryBits.putByteString rawName
+    BinaryBits.putByteString (LazyBytes.toStrict (LazyBytes.pack bytes))
+  RemoteIdSplitscreen word24 -> BinaryBits.putWord32be 24 word24
+  RemoteIdSteam word64 -> putWord64Bits word64
+  RemoteIdSwitch x -> putBitstream x
+  RemoteIdXbox word64 -> putWord64Bits word64
diff --git a/library/Rattletrap/Encode/Replay.hs b/library/Rattletrap/Encode/Replay.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Replay.hs
@@ -0,0 +1,20 @@
+module Rattletrap.Encode.Replay
+  ( putReplay
+  ) where
+
+import Rattletrap.Encode.Content
+import Rattletrap.Encode.Header
+import Rattletrap.Encode.Section
+import Rattletrap.Type.Replay
+
+import qualified Data.Binary as Binary
+
+-- | Generates a raw replay. Use this with 'Data.Binary.Put.runPut'.
+--
+-- @
+-- let bytes = 'Data.Binary.Put.runPut' ('putReplay' replay)
+-- @
+putReplay :: Replay -> Binary.Put
+putReplay replay = do
+  putSection putHeader (replayHeader replay)
+  putSection putContent (replayContent replay)
diff --git a/library/Rattletrap/Encode/Replication.hs b/library/Rattletrap/Encode/Replication.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Replication.hs
@@ -0,0 +1,21 @@
+module Rattletrap.Encode.Replication
+  ( putReplications
+  , putReplication
+  ) where
+
+import Rattletrap.Encode.CompressedWord
+import Rattletrap.Encode.ReplicationValue
+import Rattletrap.Type.Replication
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putReplications :: [Replication] -> BinaryBits.BitPut ()
+putReplications replications = do
+  mapM_ putReplication replications
+  BinaryBits.putBool False
+
+putReplication :: Replication -> BinaryBits.BitPut ()
+putReplication replication = do
+  BinaryBits.putBool True
+  putCompressedWord (replicationActorId replication)
+  putReplicationValue (replicationValue replication)
diff --git a/library/Rattletrap/Encode/ReplicationValue.hs b/library/Rattletrap/Encode/ReplicationValue.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/ReplicationValue.hs
@@ -0,0 +1,24 @@
+module Rattletrap.Encode.ReplicationValue
+  ( putReplicationValue
+  ) where
+
+import Rattletrap.Encode.DestroyedReplication
+import Rattletrap.Encode.SpawnedReplication
+import Rattletrap.Encode.UpdatedReplication
+import Rattletrap.Type.ReplicationValue
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putReplicationValue :: ReplicationValue -> BinaryBits.BitPut ()
+putReplicationValue value = case value of
+  ReplicationValueSpawned x -> do
+    BinaryBits.putBool True
+    BinaryBits.putBool True
+    putSpawnedReplication x
+  ReplicationValueUpdated x -> do
+    BinaryBits.putBool True
+    BinaryBits.putBool False
+    putUpdatedReplication x
+  ReplicationValueDestroyed x -> do
+    BinaryBits.putBool False
+    putDestroyedReplication x
diff --git a/library/Rattletrap/Encode/ReservationAttribute.hs b/library/Rattletrap/Encode/ReservationAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/ReservationAttribute.hs
@@ -0,0 +1,23 @@
+module Rattletrap.Encode.ReservationAttribute
+  ( putReservationAttribute
+  ) where
+
+import Rattletrap.Encode.CompressedWord
+import Rattletrap.Encode.Str
+import Rattletrap.Encode.UniqueIdAttribute
+import Rattletrap.Type.ReservationAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putReservationAttribute :: ReservationAttribute -> BinaryBits.BitPut ()
+putReservationAttribute reservationAttribute = do
+  putCompressedWord (reservationAttributeNumber reservationAttribute)
+  putUniqueIdAttribute (reservationAttributeUniqueId reservationAttribute)
+  case reservationAttributeName reservationAttribute of
+    Nothing -> pure ()
+    Just name -> putTextBits name
+  BinaryBits.putBool (reservationAttributeUnknown1 reservationAttribute)
+  BinaryBits.putBool (reservationAttributeUnknown2 reservationAttribute)
+  case reservationAttributeUnknown3 reservationAttribute of
+    Nothing -> pure ()
+    Just c -> BinaryBits.putWord8 6 c
diff --git a/library/Rattletrap/Encode/RigidBodyStateAttribute.hs b/library/Rattletrap/Encode/RigidBodyStateAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/RigidBodyStateAttribute.hs
@@ -0,0 +1,22 @@
+module Rattletrap.Encode.RigidBodyStateAttribute
+  ( putRigidBodyStateAttribute
+  ) where
+
+import Rattletrap.Encode.CompressedWordVector
+import Rattletrap.Encode.Vector
+import Rattletrap.Type.RigidBodyStateAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putRigidBodyStateAttribute :: RigidBodyStateAttribute -> BinaryBits.BitPut ()
+putRigidBodyStateAttribute rigidBodyStateAttribute = do
+  BinaryBits.putBool (rigidBodyStateAttributeSleeping rigidBodyStateAttribute)
+  putVector (rigidBodyStateAttributeLocation rigidBodyStateAttribute)
+  putCompressedWordVector
+    (rigidBodyStateAttributeRotation rigidBodyStateAttribute)
+  case rigidBodyStateAttributeLinearVelocity rigidBodyStateAttribute of
+    Nothing -> pure ()
+    Just linearVelocity -> putVector linearVelocity
+  case rigidBodyStateAttributeAngularVelocity rigidBodyStateAttribute of
+    Nothing -> pure ()
+    Just angularVelocity -> putVector angularVelocity
diff --git a/library/Rattletrap/Encode/Section.hs b/library/Rattletrap/Encode/Section.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Section.hs
@@ -0,0 +1,27 @@
+module Rattletrap.Encode.Section
+  ( putSection
+  ) where
+
+import Rattletrap.Encode.Word32le
+import Rattletrap.Type.Section
+import Rattletrap.Type.Word32le
+import Rattletrap.Utility.Crc
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+
+-- | Given a way to put the 'sectionBody', puts a section. This will also put
+-- the size and CRC.
+--
+-- @
+-- let bytes = 'Data.Binary.Put.runPut' ('putSection' 'Rattletrap.Content.putContent' content)
+-- @
+putSection :: (a -> Binary.Put) -> Section a -> Binary.Put
+putSection putBody section = do
+  let rawBody = Binary.runPut (putBody (sectionBody section))
+  let size = LazyBytes.length rawBody
+  let crc = getCrc32 rawBody
+  putWord32 (Word32le (fromIntegral size))
+  putWord32 (Word32le crc)
+  Binary.putLazyByteString rawBody
diff --git a/library/Rattletrap/Encode/SpawnedReplication.hs b/library/Rattletrap/Encode/SpawnedReplication.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/SpawnedReplication.hs
@@ -0,0 +1,18 @@
+module Rattletrap.Encode.SpawnedReplication
+  ( putSpawnedReplication
+  ) where
+
+import Rattletrap.Encode.Initialization
+import Rattletrap.Encode.Word32le
+import Rattletrap.Type.SpawnedReplication
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putSpawnedReplication :: SpawnedReplication -> BinaryBits.BitPut ()
+putSpawnedReplication spawnedReplication = do
+  BinaryBits.putBool (spawnedReplicationFlag spawnedReplication)
+  case spawnedReplicationNameIndex spawnedReplication of
+    Nothing -> pure ()
+    Just nameIndex -> putWord32Bits nameIndex
+  putWord32Bits (spawnedReplicationObjectId spawnedReplication)
+  putInitialization (spawnedReplicationInitialization spawnedReplication)
diff --git a/library/Rattletrap/Encode/Str.hs b/library/Rattletrap/Encode/Str.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Str.hs
@@ -0,0 +1,55 @@
+module Rattletrap.Encode.Str
+  ( putText
+  , putTextBits
+  ) where
+
+import Rattletrap.Encode.Int32le
+import Rattletrap.Type.Common
+import Rattletrap.Type.Int32le
+import Rattletrap.Type.Str
+import Rattletrap.Utility.Bytes
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits.Put as BinaryBits
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.Char as Char
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+
+putText :: Str -> Binary.Put
+putText text = do
+  let size = getTextSize text
+  let encode = getTextEncoder size
+  putInt32 size
+  Binary.putLazyByteString (encode (addNull (strValue text)))
+
+putTextBits :: Str -> BinaryBits.BitPut ()
+putTextBits text = do
+  let size = getTextSize text
+  let encode = getTextEncoder size
+  putInt32Bits size
+  BinaryBits.putByteString
+    (LazyBytes.toStrict (reverseBytes (encode (addNull (strValue text)))))
+
+getTextSize :: Str -> Int32le
+getTextSize text =
+  let
+    value = strValue text
+    scale = if Text.all Char.isLatin1 value then 1 else -1 :: Int32
+    rawSize = if Text.null value
+      then 0
+      else fromIntegral (Text.length value) + 1 :: Int32
+    size = if value == Text.pack "\x00\x00\x00None"
+      then 0x05000000
+      else scale * rawSize :: Int32
+  in
+    Int32le size
+
+getTextEncoder :: Int32le -> Text.Text -> LazyBytes.ByteString
+getTextEncoder size text = if size < Int32le 0
+  then LazyBytes.fromStrict (Text.encodeUtf16LE text)
+  else encodeLatin1 text
+
+addNull :: Text.Text -> Text.Text
+addNull text = if Text.null text then text else Text.snoc text '\x00'
diff --git a/library/Rattletrap/Encode/StringAttribute.hs b/library/Rattletrap/Encode/StringAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/StringAttribute.hs
@@ -0,0 +1,12 @@
+module Rattletrap.Encode.StringAttribute
+  ( putStringAttribute
+  ) where
+
+import Rattletrap.Encode.Str
+import Rattletrap.Type.StringAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putStringAttribute :: StringAttribute -> BinaryBits.BitPut ()
+putStringAttribute stringAttribute =
+  putTextBits (stringAttributeValue stringAttribute)
diff --git a/library/Rattletrap/Encode/TeamPaintAttribute.hs b/library/Rattletrap/Encode/TeamPaintAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/TeamPaintAttribute.hs
@@ -0,0 +1,17 @@
+module Rattletrap.Encode.TeamPaintAttribute
+  ( putTeamPaintAttribute
+  ) where
+
+import Rattletrap.Encode.Word32le
+import Rattletrap.Encode.Word8le
+import Rattletrap.Type.TeamPaintAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putTeamPaintAttribute :: TeamPaintAttribute -> BinaryBits.BitPut ()
+putTeamPaintAttribute teamPaintAttribute = do
+  putWord8Bits (teamPaintAttributeTeam teamPaintAttribute)
+  putWord8Bits (teamPaintAttributePrimaryColor teamPaintAttribute)
+  putWord8Bits (teamPaintAttributeAccentColor teamPaintAttribute)
+  putWord32Bits (teamPaintAttributePrimaryFinish teamPaintAttribute)
+  putWord32Bits (teamPaintAttributeAccentFinish teamPaintAttribute)
diff --git a/library/Rattletrap/Encode/UniqueIdAttribute.hs b/library/Rattletrap/Encode/UniqueIdAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/UniqueIdAttribute.hs
@@ -0,0 +1,15 @@
+module Rattletrap.Encode.UniqueIdAttribute
+  ( putUniqueIdAttribute
+  ) where
+
+import Rattletrap.Encode.RemoteId
+import Rattletrap.Encode.Word8le
+import Rattletrap.Type.UniqueIdAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putUniqueIdAttribute :: UniqueIdAttribute -> BinaryBits.BitPut ()
+putUniqueIdAttribute uniqueIdAttribute = do
+  putWord8Bits (uniqueIdAttributeSystemId uniqueIdAttribute)
+  putRemoteId (uniqueIdAttributeRemoteId uniqueIdAttribute)
+  putWord8Bits (uniqueIdAttributeLocalId uniqueIdAttribute)
diff --git a/library/Rattletrap/Encode/UpdatedReplication.hs b/library/Rattletrap/Encode/UpdatedReplication.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/UpdatedReplication.hs
@@ -0,0 +1,12 @@
+module Rattletrap.Encode.UpdatedReplication
+  ( putUpdatedReplication
+  ) where
+
+import Rattletrap.Encode.Attribute
+import Rattletrap.Type.UpdatedReplication
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putUpdatedReplication :: UpdatedReplication -> BinaryBits.BitPut ()
+putUpdatedReplication updatedReplication =
+  putAttributes (updatedReplicationAttributes updatedReplication)
diff --git a/library/Rattletrap/Encode/Vector.hs b/library/Rattletrap/Encode/Vector.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Vector.hs
@@ -0,0 +1,26 @@
+module Rattletrap.Encode.Vector
+  ( putVector
+  ) where
+
+import Rattletrap.Encode.CompressedWord
+import Rattletrap.Type.CompressedWord
+import Rattletrap.Type.Vector
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putVector :: Vector -> BinaryBits.BitPut ()
+putVector vector = do
+  let
+    bitSize =
+      round (logBase (2 :: Float) (fromIntegral (vectorBias vector))) - 1 :: Word
+    dx =
+      fromIntegral (vectorX vector + fromIntegral (vectorBias vector)) :: Word
+    dy =
+      fromIntegral (vectorY vector + fromIntegral (vectorBias vector)) :: Word
+    dz =
+      fromIntegral (vectorZ vector + fromIntegral (vectorBias vector)) :: Word
+    limit = 2 ^ (bitSize + 2) :: Word
+  putCompressedWord (CompressedWord 19 bitSize)
+  putCompressedWord (CompressedWord limit dx)
+  putCompressedWord (CompressedWord limit dy)
+  putCompressedWord (CompressedWord limit dz)
diff --git a/library/Rattletrap/Encode/WeldedInfoAttribute.hs b/library/Rattletrap/Encode/WeldedInfoAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/WeldedInfoAttribute.hs
@@ -0,0 +1,19 @@
+module Rattletrap.Encode.WeldedInfoAttribute
+  ( putWeldedInfoAttribute
+  ) where
+
+import Rattletrap.Encode.Float32le
+import Rattletrap.Encode.Int32le
+import Rattletrap.Encode.Int8Vector
+import Rattletrap.Encode.Vector
+import Rattletrap.Type.WeldedInfoAttribute
+
+import qualified Data.Binary.Bits.Put as BinaryBits
+
+putWeldedInfoAttribute :: WeldedInfoAttribute -> BinaryBits.BitPut ()
+putWeldedInfoAttribute weldedInfoAttribute = do
+  BinaryBits.putBool (weldedInfoAttributeActive weldedInfoAttribute)
+  putInt32Bits (weldedInfoAttributeActorId weldedInfoAttribute)
+  putVector (weldedInfoAttributeOffset weldedInfoAttribute)
+  putFloat32Bits (weldedInfoAttributeMass weldedInfoAttribute)
+  putInt8Vector (weldedInfoAttributeRotation weldedInfoAttribute)
diff --git a/library/Rattletrap/Encode/Word32le.hs b/library/Rattletrap/Encode/Word32le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Word32le.hs
@@ -0,0 +1,20 @@
+module Rattletrap.Encode.Word32le
+  ( putWord32
+  , putWord32Bits
+  ) where
+
+import Rattletrap.Type.Word32le
+import Rattletrap.Utility.Bytes
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits.Put as BinaryBits
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+
+putWord32 :: Word32le -> Binary.Put
+putWord32 word32 = Binary.putWord32le (word32leValue word32)
+
+putWord32Bits :: Word32le -> BinaryBits.BitPut ()
+putWord32Bits word32 = do
+  let bytes = Binary.runPut (putWord32 word32)
+  BinaryBits.putByteString (LazyBytes.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Encode/Word64le.hs b/library/Rattletrap/Encode/Word64le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Word64le.hs
@@ -0,0 +1,20 @@
+module Rattletrap.Encode.Word64le
+  ( putWord64
+  , putWord64Bits
+  ) where
+
+import Rattletrap.Type.Word64le
+import Rattletrap.Utility.Bytes
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits.Put as BinaryBits
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+
+putWord64 :: Word64le -> Binary.Put
+putWord64 word64 = Binary.putWord64le (word64leValue word64)
+
+putWord64Bits :: Word64le -> BinaryBits.BitPut ()
+putWord64Bits word64 = do
+  let bytes = Binary.runPut (putWord64 word64)
+  BinaryBits.putByteString (LazyBytes.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Encode/Word8le.hs b/library/Rattletrap/Encode/Word8le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Encode/Word8le.hs
@@ -0,0 +1,20 @@
+module Rattletrap.Encode.Word8le
+  ( putWord8
+  , putWord8Bits
+  ) where
+
+import Rattletrap.Type.Word8le
+import Rattletrap.Utility.Bytes
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits.Put as BinaryBits
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+
+putWord8 :: Word8le -> Binary.Put
+putWord8 word8 = Binary.putWord8 (word8leValue word8)
+
+putWord8Bits :: Word8le -> BinaryBits.BitPut ()
+putWord8Bits word8 = do
+  let bytes = Binary.runPut (putWord8 word8)
+  BinaryBits.putByteString (LazyBytes.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Frame.hs b/library/Rattletrap/Frame.hs
deleted file mode 100644
--- a/library/Rattletrap/Frame.hs
+++ /dev/null
@@ -1,67 +0,0 @@
-module Rattletrap.Frame where
-
-import Rattletrap.ActorMap
-import Rattletrap.ClassAttributeMap
-import Rattletrap.Primitive
-import Rattletrap.Replication
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data Frame = Frame
-  { frameTime :: Float32
-  -- ^ Time in seconds since the beginning of the match.
-  , frameDelta :: Float32
-  -- ^ Time in seconds since the last frame. Usually about 0.03 since there
-  -- are 30 frames per second.
-  , frameReplications :: [Replication]
-  } deriving (Eq, Ord, Show)
-
-getFrames
-  :: (Int, Int, Int)
-  -> Int
-  -> Word
-  -> ClassAttributeMap
-  -> ActorMap
-  -> BinaryBit.BitGet ([Frame], ActorMap)
-getFrames version numFrames maxChannels classAttributeMap actorMap =
-  if numFrames <= 0
-    then pure ([], actorMap)
-    else do
-      (frame, newActorMap) <- getFrame
-        version
-        maxChannels
-        classAttributeMap
-        actorMap
-      (frames, newerActorMap) <- getFrames
-        version
-        (numFrames - 1)
-        maxChannels
-        classAttributeMap
-        newActorMap
-      pure (frame : frames, newerActorMap)
-
-putFrames :: [Frame] -> BinaryBit.BitPut ()
-putFrames = mapM_ putFrame
-
-getFrame
-  :: (Int, Int, Int)
-  -> Word
-  -> ClassAttributeMap
-  -> ActorMap
-  -> BinaryBit.BitGet (Frame, ActorMap)
-getFrame version maxChannels classAttributeMap actorMap = do
-  time <- getFloat32Bits
-  delta <- getFloat32Bits
-  (replications, newActorMap) <- getReplications
-    version
-    maxChannels
-    classAttributeMap
-    actorMap
-  pure (Frame time delta replications, newActorMap)
-
-putFrame :: Frame -> BinaryBit.BitPut ()
-putFrame frame = do
-  putFloat32Bits (frameTime frame)
-  putFloat32Bits (frameDelta frame)
-  putReplications (frameReplications frame)
diff --git a/library/Rattletrap/Header.hs b/library/Rattletrap/Header.hs
deleted file mode 100644
--- a/library/Rattletrap/Header.hs
+++ /dev/null
@@ -1,112 +0,0 @@
-module Rattletrap.Header where
-
-import Rattletrap.Primitive
-import Rattletrap.Property
-import Rattletrap.PropertyValue
-
-import qualified Data.Binary as Binary
-import qualified Data.Map as Map
-import qualified Data.Maybe as Maybe
-
--- | Contains high-level metadata about a 'Rattletrap.Replay.Replay'.
-data Header = Header
-  { headerEngineVersion :: Word32
-  -- ^ The "major" ("engine") version number.
-  , headerLicenseeVersion :: Word32
-  -- ^ The "minor" ("licensee") version number.
-  , headerPatchVersion :: Maybe Word32
-  -- ^ The "patch" ("net") version number.
-  , headerLabel :: Text
-  -- ^ Always @TAGame.Replay_Soccar_TA@.
-  , headerProperties :: Dictionary Property
-  -- ^ These properties determine how a replay will look in the list of
-  -- replays in-game. One element is required for the replay to show up:
-  --
-  -- - MapName: This is a 'Rattletrap.PropertyValue.NameProperty' with a
-  --   case-insensitive map identifier, like @Stadium_P@.
-  --
-  -- There are many other properties that affect how the replay looks in the
-  -- list of replays.
-  --
-  -- - Date: A 'Rattletrap.PropertyValue.StrProperty' with the format
-  --   @YYYY-mm-dd:HH-MM@. Dates are not validated, but the month must be
-  --   between 1 and 12 to show up. The hour is shown modulo 12 with AM or PM.
-  -- - MatchType: A 'Rattletrap.PropertyValue.NameProperty'. If this is not
-  --   one of the expected values, nothing will be shown next to the replay's
-  --   map. The expected values are: @Online@, @Offline@, @Private@, and
-  --   @Season@.
-  -- - NumFrames: This 'Rattletrap.PropertyValue.IntProperty' is used to
-  --   calculate the length of the match. There are 30 frames per second,
-  --   a typical 5-minute match has about 9,000 frames.
-  -- - PrimaryPlayerTeam: This is an 'Rattletrap.PropertyValue.IntProperty'.
-  --   It is either 0 (blue) or 1 (orange). Any other value is ignored. If
-  --   this would be 0, you don't have to set it at all.
-  -- - ReplayName: An optional 'Rattletrap.PropertyValue.StrProperty' with a
-  --   user-supplied name for the replay.
-  -- - Team0Score: The blue team's score as an
-  --   'Rattletrap.PropertyValue.IntProperty'. Can be omitted if the score is
-  --   0.
-  -- - Team1Score: The orange team's score as an
-  --   'Rattletrap.PropertyValue.IntProperty'. Can also be omitted if the
-  --   score is 0.
-  -- - TeamSize: An 'Rattletrap.PropertyValue.IntProperty' with the number of
-  --   players per team. This value is not validated, so you can put absurd
-  --   values like 99. To get an "unfair" team size like 1v4, you must set the
-  --   bUnfairBots 'Rattletrap.PropertyValue.BoolProperty' to @True@.
-  } deriving (Eq, Ord, Show)
-
-getHeader :: Binary.Get Header
-getHeader = do
-  engineVersion <- getWord32
-  licenseeVersion <- getWord32
-  patchVersion <- getPatchVersion engineVersion licenseeVersion
-  label <- getText
-  properties <- getDictionary getProperty
-  pure (Header engineVersion licenseeVersion patchVersion label properties)
-
-getPatchVersion :: Word32 -> Word32 -> Binary.Get (Maybe Word32)
-getPatchVersion major minor = if hasPatchVersion major minor
-  then do
-    patchVersion <- getWord32
-    pure (Just patchVersion)
-  else pure Nothing
-
-hasPatchVersion :: Word32 -> Word32 -> Bool
-hasPatchVersion major minor = major >= Word32 868 && minor >= Word32 18
-
-putHeader :: Header -> Binary.Put
-putHeader header = do
-  putWord32 (headerEngineVersion header)
-  putWord32 (headerLicenseeVersion header)
-  case headerPatchVersion header of
-    Nothing -> pure ()
-    Just patchVersion -> putWord32 patchVersion
-  putText (headerLabel header)
-  putDictionary putProperty (headerProperties header)
-
-getVersion :: Header -> (Int, Int, Int)
-getVersion header =
-  ( fromIntegral (word32Value (headerEngineVersion header))
-  , fromIntegral (word32Value (headerLicenseeVersion header))
-  , Maybe.fromMaybe 0 (fmap (\ v -> fromIntegral (word32Value v)) (headerPatchVersion header))
-  )
-
-getNumFrames :: Header -> Int
-getNumFrames header =
-  let
-    key = textValue (stringToText "NumFrames")
-    properties = dictionaryValue (headerProperties header)
-  in case Map.lookup key properties of
-    Just (Property _ _ (IntProperty numFrames)) ->
-      fromIntegral (int32Value numFrames)
-    _ -> 0
-
-getMaxChannels :: Header -> Word
-getMaxChannels header =
-  let
-    key = textValue (stringToText "MaxChannels")
-    properties = dictionaryValue (headerProperties header)
-  in case Map.lookup key properties of
-    Just (Property _ _ (IntProperty numFrames)) ->
-      fromIntegral (int32Value numFrames)
-    _ -> 1023
diff --git a/library/Rattletrap/Helper.hs b/library/Rattletrap/Helper.hs
deleted file mode 100644
--- a/library/Rattletrap/Helper.hs
+++ /dev/null
@@ -1,103 +0,0 @@
--- | This module provides helper functions for converting replays to and from
--- both their binary format and JSON.
-module Rattletrap.Helper where
-
-import Rattletrap.Json ()
-import Rattletrap.Replay
-
-import qualified Data.Aeson as Aeson
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.ByteString.Lazy as ByteString
-import qualified System.IO as IO
-
--- * Lazy byte strings
--- | Parses a raw replay.
-decodeReplay :: ByteString.ByteString -> Either String Replay
-decodeReplay contents = case Binary.runGetOrFail getReplay contents of
-  Left (_, _, message) -> fail message
-  Right (_, _, replay) -> pure replay
-
--- | Encodes a replay as JSON.
-encodeJson :: Replay -> ByteString.ByteString
-encodeJson = Aeson.encode
-
--- | Parses a JSON replay.
-decodeJson :: ByteString.ByteString -> Either String Replay
-decodeJson = Aeson.eitherDecode
-
--- | Encodes a raw replay.
-encodeReplay :: Replay -> ByteString.ByteString
-encodeReplay replay = Binary.runPut (putReplay replay)
-
--- * File paths
--- | 'decodeReplay'
-{-# DEPRECATED
-decodeReplayFile "use Data.ByteString.Lazy.readFile"
- #-}
-
-decodeReplayFile :: FilePath -> IO (Either String Replay)
-decodeReplayFile file = IO.withBinaryFile file IO.ReadMode decodeReplayHandle
-
--- | 'encodeJson'
-{-# DEPRECATED
-encodeJsonFile "use Data.ByteString.Lazy.writeFile"
- #-}
-
-encodeJsonFile :: Replay -> FilePath -> IO ()
-encodeJsonFile replay file =
-  IO.withBinaryFile file IO.WriteMode (encodeJsonHandle replay)
-
--- | 'decodeJson'
-{-# DEPRECATED
-decodeJsonFile "use Data.ByteString.Lazy.readFile"
- #-}
-
-decodeJsonFile :: FilePath -> IO (Either String Replay)
-decodeJsonFile file = IO.withBinaryFile file IO.ReadMode decodeJsonHandle
-
--- | 'encodeReplay'
-{-# DEPRECATED
-encodeReplayFile "use Data.ByteString.Lazy.writeFile"
- #-}
-
-encodeReplayFile :: Replay -> FilePath -> IO ()
-encodeReplayFile replay file =
-  IO.withBinaryFile file IO.WriteMode (encodeReplayHandle replay)
-
--- * Handles
--- | 'decodeReplay'
-{-# DEPRECATED
-decodeReplayHandle "use Data.ByteString.Lazy.hGetContents"
- #-}
-
-decodeReplayHandle :: IO.Handle -> IO (Either String Replay)
-decodeReplayHandle handle = do
-  contents <- ByteString.hGetContents handle
-  pure (decodeReplay contents)
-
--- | 'encodeJson'
-{-# DEPRECATED
-encodeJsonHandle "use Data.ByteString.Lazy.hPut"
- #-}
-
-encodeJsonHandle :: Replay -> IO.Handle -> IO ()
-encodeJsonHandle replay handle = ByteString.hPut handle (encodeJson replay)
-
--- | 'decodeJson'
-{-# DEPRECATED
-decodeJsonHandle "use Data.ByteString.Lazy.hGetContents"
- #-}
-
-decodeJsonHandle :: IO.Handle -> IO (Either String Replay)
-decodeJsonHandle handle = do
-  contents <- ByteString.hGetContents handle
-  pure (decodeJson contents)
-
--- | 'encodeReplay'
-{-# DEPRECATED
-encodeReplayHandle "use Data.ByteString.Lazy.hPut"
- #-}
-
-encodeReplayHandle :: Replay -> IO.Handle -> IO ()
-encodeReplayHandle replay handle = ByteString.hPut handle (encodeReplay replay)
diff --git a/library/Rattletrap/Initialization.hs b/library/Rattletrap/Initialization.hs
deleted file mode 100644
--- a/library/Rattletrap/Initialization.hs
+++ /dev/null
@@ -1,38 +0,0 @@
-module Rattletrap.Initialization where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data Initialization = Initialization
-  { initializationLocation :: Maybe Vector
-  -- ^ Not every class has an initial location. See
-  -- 'Rattletrap.Data.rawClassesWithLocation'.
-  , initializationRotation :: Maybe Int8Vector
-  -- ^ Only classes with location can have rotation, but not every one does.
-  -- See 'Rattletrap.Data.rawClassesWithRotation'.
-  } deriving (Eq, Ord, Show)
-
-getInitialization :: Bool -> Bool -> BinaryBit.BitGet Initialization
-getInitialization hasLocation hasRotation = do
-  location <- if hasLocation
-    then do
-      location <- getVector
-      pure (Just location)
-    else pure Nothing
-  rotation <- if hasRotation
-    then do
-      rotation <- getInt8Vector
-      pure (Just rotation)
-    else pure Nothing
-  pure (Initialization location rotation)
-
-putInitialization :: Initialization -> BinaryBit.BitPut ()
-putInitialization initialization = do
-  case initializationLocation initialization of
-    Nothing -> pure ()
-    Just location -> putVector location
-  case initializationRotation initialization of
-    Nothing -> pure ()
-    Just rotation -> putInt8Vector rotation
diff --git a/library/Rattletrap/Json.hs b/library/Rattletrap/Json.hs
deleted file mode 100644
--- a/library/Rattletrap/Json.hs
+++ /dev/null
@@ -1,112 +0,0 @@
-{-# LANGUAGE TemplateHaskell #-}
-{-# OPTIONS_GHC -fno-warn-orphans #-}
-
-module Rattletrap.Json where
-
-import Rattletrap.Attribute
-import Rattletrap.AttributeMapping
-import Rattletrap.AttributeValue
-import Rattletrap.Cache
-import Rattletrap.ClassMapping
-import Rattletrap.Content
-import Rattletrap.Frame
-import Rattletrap.Header
-import Rattletrap.Initialization
-import Rattletrap.KeyFrame
-import Rattletrap.Mark
-import Rattletrap.Message
-import Rattletrap.Primitive
-import Rattletrap.Property
-import Rattletrap.PropertyValue
-import Rattletrap.RemoteId
-import Rattletrap.Replay
-import Rattletrap.Replication
-import Rattletrap.ReplicationValue
-
-import qualified Control.Monad as Monad
-import qualified Data.Aeson.TH as Aeson
-import qualified Data.Aeson.Types as Aeson
-import qualified Language.Haskell.TH as TH
-
-$(let toSnakeCase = Aeson.camelTo2 '_'
-      dropName name = drop (length (TH.nameBase name))
-      optionsFor name =
-        Aeson.defaultOptions
-        { Aeson.constructorTagModifier = toSnakeCase
-        , Aeson.fieldLabelModifier = toSnakeCase . dropName name
-        , Aeson.omitNothingFields = True
-        , Aeson.sumEncoding = Aeson.ObjectWithSingleField
-        , Aeson.unwrapUnaryRecords = True
-        }
-      deriveJSON declarations name = do
-        newDeclarations <- Aeson.deriveJSON (optionsFor name) name
-        pure (newDeclarations ++ declarations)
-      names =
-        [ ''AppliedDamageAttribute
-        , ''Attribute
-        , ''AttributeMapping
-        , ''AttributeValue
-        , ''BooleanAttribute
-        , ''ByteAttribute
-        , ''Cache
-        , ''CamSettingsAttribute
-        , ''ClassMapping
-        , ''ClubColorsAttribute
-        , ''CompressedWord
-        , ''CompressedWordVector
-        , ''Content
-        , ''DamageStateAttribute
-        , ''DemolishAttribute
-        , ''DestroyedReplication
-        , ''Dictionary
-        , ''EnumAttribute
-        , ''ExplosionAttribute
-        , ''ExtendedExplosionAttribute
-        , ''FlaggedIntAttribute
-        , ''Float32
-        , ''FloatAttribute
-        , ''Frame
-        , ''GameModeAttribute
-        , ''Header
-        , ''Initialization
-        , ''Int32
-        , ''Int8
-        , ''Int8Vector
-        , ''IntAttribute
-        , ''KeyFrame
-        , ''List
-        , ''LoadoutAttribute
-        , ''LoadoutOnlineAttribute
-        , ''LoadoutsAttribute
-        , ''LoadoutsOnlineAttribute
-        , ''LocationAttribute
-        , ''Mark
-        , ''Message
-        , ''MusicStingerAttribute
-        , ''PartyLeaderAttribute
-        , ''PickupAttribute
-        , ''PrivateMatchSettingsAttribute
-        , ''ProductAttribute
-        , ''Property
-        , ''PropertyValue
-        , ''QWordAttribute
-        , ''RemoteId
-        , ''Replay
-        , ''Replication
-        , ''ReplicationValue
-        , ''ReservationAttribute
-        , ''RigidBodyStateAttribute
-        , ''Section
-        , ''SpawnedReplication
-        , ''StringAttribute
-        , ''TeamPaintAttribute
-        , ''Text
-        , ''UniqueIdAttribute
-        , ''UpdatedReplication
-        , ''Vector
-        , ''WeldedInfoAttribute
-        , ''Word32
-        , ''Word64
-        , ''Word8
-        ]
-  in Monad.foldM deriveJSON [] names)
diff --git a/library/Rattletrap/KeyFrame.hs b/library/Rattletrap/KeyFrame.hs
deleted file mode 100644
--- a/library/Rattletrap/KeyFrame.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-module Rattletrap.KeyFrame where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary as Binary
-
-data KeyFrame = KeyFrame
-  { keyFrameTime :: Float32
-  -- ^ When this key frame occurs, in seconds.
-  , keyFrameFrame :: Word32
-  -- ^ The frame number of this key frame, starting from 0.
-  , keyFramePosition :: Word32
-  -- ^ The bit position of this key frame in the stream.
-  } deriving (Eq, Ord, Show)
-
-getKeyFrame :: Binary.Get KeyFrame
-getKeyFrame = do
-  time <- getFloat32
-  frame <- getWord32
-  position <- getWord32
-  pure (KeyFrame time frame position)
-
-putKeyFrame :: KeyFrame -> Binary.Put
-putKeyFrame keyFrame = do
-  putFloat32 (keyFrameTime keyFrame)
-  putWord32 (keyFrameFrame keyFrame)
-  putWord32 (keyFramePosition keyFrame)
diff --git a/library/Rattletrap/Main.hs b/library/Rattletrap/Main.hs
deleted file mode 100644
--- a/library/Rattletrap/Main.hs
+++ /dev/null
@@ -1,60 +0,0 @@
-module Rattletrap.Main where
-
-import Rattletrap.Helper
-import Rattletrap.Version
-
-import qualified Data.ByteString.Lazy as ByteString
-import qualified Data.Version as Version
-import qualified Network.HTTP.Client as Client
-import qualified Network.HTTP.Client.TLS as Client
-import qualified System.Environment as Environment
-
--- | Gets command-line arguments and passes them to 'mainWithArgs'.
-main :: IO ()
-main = do
-  args <- Environment.getArgs
-  mainWithArgs args
-
--- | Runs the main command-line interface. Can be used in one of three modes:
---
--- 1. @mainWithArgs ["version"]@: Prints out the version number and exits.
---    Mostly useful for debugging.
--- 2. @mainWithArgs ["decode"]@: Parses a raw replay into JSON. By default
---    this reads from STDIN and writes to STDOUT. To read from a file, pass a
---    second argument like @mainWithArgs ["decode", "input.replay"]@. To write
---    to a file, pass a third argument like
---    @mainWithArgs ["decode", "input.replay", "output.json"]@.
--- 3. @mainWithArgs ["encode"]@: Generates a raw replay from JSON. The
---    handling of input and output is the same as decoding.
-mainWithArgs :: [String] -> IO ()
-mainWithArgs args = case args of
-  ["version"] -> putStrLn (Version.showVersion version)
-  action:files -> do
-    (getInput, putOutput) <- getIO files
-    input <- getInput
-    output <- case action of
-      "decode" -> case decodeReplay input of
-        Left message -> fail message
-        Right replay -> pure (encodeJson replay)
-      "encode" -> case decodeJson input of
-        Left message -> fail message
-        Right replay -> pure (encodeReplay replay)
-      _ -> fail ("unknown action: " ++ show action)
-    putOutput output
-  _ -> fail ("unknown arguments: " ++ show args)
-
-getIO
-  :: [FilePath] -> IO (IO ByteString.ByteString, ByteString.ByteString -> IO ())
-getIO files = case files of
-  [] -> pure (ByteString.getContents, ByteString.putStr)
-  [i] -> pure (readUrlOrFile i, ByteString.putStr)
-  [i, o] -> pure (readUrlOrFile i, ByteString.writeFile o)
-  _ -> fail ("unknown files: " ++ show files)
-
-readUrlOrFile :: FilePath -> IO ByteString.ByteString
-readUrlOrFile i = case Client.parseUrlThrow i of
-  Just request -> do
-    manager <- Client.newTlsManager
-    response <- Client.httpLbs request manager
-    pure (Client.responseBody response)
-  Nothing -> ByteString.readFile i
diff --git a/library/Rattletrap/Mark.hs b/library/Rattletrap/Mark.hs
deleted file mode 100644
--- a/library/Rattletrap/Mark.hs
+++ /dev/null
@@ -1,23 +0,0 @@
-module Rattletrap.Mark where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary as Binary
-
-data Mark = Mark
-  { markValue :: Text
-  -- ^ Which type of mark this is, like @Team0Goal@.
-  , markFrame :: Word32
-  -- ^ Which frame this mark belongs to, starting from 0.
-  } deriving (Eq, Ord, Show)
-
-getMark :: Binary.Get Mark
-getMark = do
-  value <- getText
-  frame <- getWord32
-  pure (Mark value frame)
-
-putMark :: Mark -> Binary.Put
-putMark mark = do
-  putText (markValue mark)
-  putWord32 (markFrame mark)
diff --git a/library/Rattletrap/Message.hs b/library/Rattletrap/Message.hs
deleted file mode 100644
--- a/library/Rattletrap/Message.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-module Rattletrap.Message where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary as Binary
-
-data Message = Message
-  { messageFrame :: Word32
-  -- ^ Which frame this message belongs to, starting from 0.
-  , messageName :: Text
-  -- ^ The primary player's name.
-  , messageValue :: Text
-  -- ^ The content of the message.
-  } deriving (Eq, Ord, Show)
-
-getMessage :: Binary.Get Message
-getMessage = do
-  frame <- getWord32
-  name <- getText
-  value <- getText
-  pure (Message frame name value)
-
-putMessage :: Message -> Binary.Put
-putMessage message = do
-  putWord32 (messageFrame message)
-  putText (messageName message)
-  putText (messageValue message)
diff --git a/library/Rattletrap/Primitive.hs b/library/Rattletrap/Primitive.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module Rattletrap.Primitive
-  ( module Rattletrap.Primitive.CompressedWord
-  , module Rattletrap.Primitive.CompressedWordVector
-  , module Rattletrap.Primitive.Dictionary
-  , module Rattletrap.Primitive.Float32
-  , module Rattletrap.Primitive.Int8
-  , module Rattletrap.Primitive.Int8Vector
-  , module Rattletrap.Primitive.Int32
-  , module Rattletrap.Primitive.List
-  , module Rattletrap.Primitive.Section
-  , module Rattletrap.Primitive.Text
-  , module Rattletrap.Primitive.Vector
-  , module Rattletrap.Primitive.Word8
-  , module Rattletrap.Primitive.Word32
-  , module Rattletrap.Primitive.Word64
-  ) where
-
-import Rattletrap.Primitive.CompressedWord
-import Rattletrap.Primitive.CompressedWordVector
-import Rattletrap.Primitive.Dictionary
-import Rattletrap.Primitive.Float32
-import Rattletrap.Primitive.Int32
-import Rattletrap.Primitive.Int8
-import Rattletrap.Primitive.Int8Vector
-import Rattletrap.Primitive.List
-import Rattletrap.Primitive.Section
-import Rattletrap.Primitive.Text
-import Rattletrap.Primitive.Vector
-import Rattletrap.Primitive.Word32
-import Rattletrap.Primitive.Word64
-import Rattletrap.Primitive.Word8
diff --git a/library/Rattletrap/Primitive/CompressedWord.hs b/library/Rattletrap/Primitive/CompressedWord.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/CompressedWord.hs
+++ /dev/null
@@ -1,49 +0,0 @@
-module Rattletrap.Primitive.CompressedWord where
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Bits as Bits
-
-data CompressedWord = CompressedWord
-  { compressedWordLimit :: Word
-  , compressedWordValue :: Word
-  } deriving (Eq, Ord, Show)
-
-getCompressedWord :: Word -> BinaryBit.BitGet CompressedWord
-getCompressedWord limit = do
-  value <- getCompressedWordStep limit (getMaxBits limit) 0 0
-  pure (CompressedWord limit value)
-
-putCompressedWord :: CompressedWord -> BinaryBit.BitPut ()
-putCompressedWord compressedWord = do
-  let limit = compressedWordLimit compressedWord
-  let value = compressedWordValue compressedWord
-  let maxBits = getMaxBits limit
-  let
-    go position soFar = if position < maxBits
-      then do
-        let x = Bits.shiftL 1 position
-        if maxBits > 1 && position == maxBits - 1 && soFar + x > limit
-          then pure ()
-          else do
-            let bit = Bits.testBit value position
-            BinaryBit.putBool bit
-            let delta = if bit then x else 0
-            go (position + 1) (soFar + delta)
-      else pure ()
-  go 0 0
-
-getMaxBits :: (Integral a, Integral b) => a -> b
-getMaxBits x = do
-  let n = max 1 (ceiling (logBase (2 :: Double) (fromIntegral (max 1 x))))
-  if x < 1024 && x == 2 ^ n then n + 1 else n
-
-getCompressedWordStep :: Word -> Word -> Word -> Word -> BinaryBit.BitGet Word
-getCompressedWordStep limit maxBits position value = do
-  let x = Bits.shiftL 1 (fromIntegral position)
-  if position < maxBits && value + x <= limit
-    then do
-      bit <- BinaryBit.getBool
-      let newValue = if bit then value + x else value
-      getCompressedWordStep limit maxBits (position + 1) newValue
-    else pure value
diff --git a/library/Rattletrap/Primitive/CompressedWordVector.hs b/library/Rattletrap/Primitive/CompressedWordVector.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/CompressedWordVector.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module Rattletrap.Primitive.CompressedWordVector where
-
-import Rattletrap.Primitive.CompressedWord
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data CompressedWordVector = CompressedWordVector
-  { compressedWordVectorX :: CompressedWord
-  , compressedWordVectorY :: CompressedWord
-  , compressedWordVectorZ :: CompressedWord
-  } deriving (Eq, Ord, Show)
-
-getCompressedWordVector :: BinaryBit.BitGet CompressedWordVector
-getCompressedWordVector = do
-  let limit = 65536
-  x <- getCompressedWord limit
-  y <- getCompressedWord limit
-  z <- getCompressedWord limit
-  pure (CompressedWordVector x y z)
-
-putCompressedWordVector :: CompressedWordVector -> BinaryBit.BitPut ()
-putCompressedWordVector compressedWordVector = do
-  putCompressedWord (compressedWordVectorX compressedWordVector)
-  putCompressedWord (compressedWordVectorY compressedWordVector)
-  putCompressedWord (compressedWordVectorZ compressedWordVector)
diff --git a/library/Rattletrap/Primitive/Dictionary.hs b/library/Rattletrap/Primitive/Dictionary.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Dictionary.hs
+++ /dev/null
@@ -1,60 +0,0 @@
-module Rattletrap.Primitive.Dictionary where
-
-import Rattletrap.Primitive.Text
-
-import qualified Data.Binary as Binary
-import qualified Data.Map as Map
-import qualified Data.Text as Text
-
-data Dictionary a = Dictionary
-  { dictionaryKeys :: [Text]
-  -- ^ Objects in JSON aren't ordered, so the order of the keys must be stored
-  -- separately.
-  , dictionaryLastKey :: Text
-  -- ^ The last key is usually @None@ but sometimes contains extra null bytes.
-  , dictionaryValue :: Map.Map Text.Text a
-  -- ^ Be sure to update 'dictionaryKeys' if you add, change, or remove a key
-  -- in this map.
-  } deriving (Eq, Ord, Show)
-
-getDictionary :: Binary.Get a -> Binary.Get (Dictionary a)
-getDictionary getValue = do
-  (elements, lastKey) <- getElements getValue
-  let keys = map fst elements
-  let value = Map.mapKeys textValue (Map.fromList elements)
-  pure (Dictionary keys lastKey value)
-
-getElements :: Binary.Get a -> Binary.Get ([(Text, a)], Text)
-getElements getValue = do
-  (key, maybeValue) <- getElement getValue
-  case maybeValue of
-    Nothing -> pure ([], key)
-    Just value -> do
-      let element = (key, value)
-      (elements, lastKey) <- getElements getValue
-      pure (element : elements, lastKey)
-
-getElement :: Binary.Get a -> Binary.Get (Text, Maybe a)
-getElement getValue = do
-  key <- getText
-  if isNoneKey key
-    then pure (key, Nothing)
-    else do
-      value <- getValue
-      pure (key, Just value)
-
-isNoneKey :: Text -> Bool
-isNoneKey text = filter (/= '\x00') (textToString text) == "None"
-
-putDictionary :: (a -> Binary.Put) -> Dictionary a -> Binary.Put
-putDictionary putValue dictionary = do
-  let elements = dictionaryValue dictionary
-  mapM_
-    ( \key -> do
-      putText key
-      case Map.lookup (textValue key) elements of
-        Nothing -> fail ("could not find key " ++ textToString key)
-        Just value -> putValue value
-    )
-    (dictionaryKeys dictionary)
-  putText (dictionaryLastKey dictionary)
diff --git a/library/Rattletrap/Primitive/Float32.hs b/library/Rattletrap/Primitive/Float32.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Float32.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Rattletrap.Primitive.Float32 where
-
-import Rattletrap.Utility
-
-import qualified Data.Binary as Binary
-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 ByteString
-
-newtype Float32 = Float32
-  { float32Value :: Float
-  } deriving (Eq, Ord, Show)
-
-getFloat32 :: Binary.Get Float32
-getFloat32 = do
-  float32 <- IEEE754.getFloat32le
-  pure (Float32 float32)
-
-putFloat32 :: Float32 -> Binary.Put
-putFloat32 float32 = IEEE754.putFloat32le (float32Value float32)
-
-getFloat32Bits :: BinaryBit.BitGet Float32
-getFloat32Bits = do
-  bytes <- BinaryBit.getLazyByteString 4
-  pure (Binary.runGet getFloat32 (reverseBytes bytes))
-
-putFloat32Bits :: Float32 -> BinaryBit.BitPut ()
-putFloat32Bits float32 = do
-  let bytes = Binary.runPut (putFloat32 float32)
-  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Primitive/Int32.hs b/library/Rattletrap/Primitive/Int32.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Int32.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Rattletrap.Primitive.Int32 where
-
-import Rattletrap.Utility
-
-import qualified Data.Binary as Binary
-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 ByteString
-import qualified Data.Int as Int
-
-newtype Int32 = Int32
-  { int32Value :: Int.Int32
-  } deriving (Eq, Ord, Show)
-
-getInt32 :: Binary.Get Int32
-getInt32 = do
-  int32 <- Binary.getInt32le
-  pure (Int32 int32)
-
-putInt32 :: Int32 -> Binary.Put
-putInt32 int32 = Binary.putInt32le (int32Value int32)
-
-getInt32Bits :: BinaryBit.BitGet Int32
-getInt32Bits = do
-  bytes <- BinaryBit.getLazyByteString 4
-  pure (Binary.runGet getInt32 (reverseBytes bytes))
-
-putInt32Bits :: Int32 -> BinaryBit.BitPut ()
-putInt32Bits int32 = do
-  let bytes = Binary.runPut (putInt32 int32)
-  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Primitive/Int8.hs b/library/Rattletrap/Primitive/Int8.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Int8.hs
+++ /dev/null
@@ -1,32 +0,0 @@
-module Rattletrap.Primitive.Int8 where
-
-import Rattletrap.Utility
-
-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 ByteString
-import qualified Data.Int as Int
-
-newtype Int8 = Int8
-  { int8Value :: Int.Int8
-  } deriving (Eq, Ord, Show)
-
-getInt8 :: Binary.Get Int8
-getInt8 = do
-  int8 <- Binary.getInt8
-  pure (Int8 int8)
-
-putInt8 :: Int8 -> Binary.Put
-putInt8 int8 = Binary.putInt8 (int8Value int8)
-
-getInt8Bits :: BinaryBit.BitGet Int8
-getInt8Bits = do
-  bytes <- BinaryBit.getLazyByteString 1
-  pure (Binary.runGet getInt8 (reverseBytes bytes))
-
-putInt8Bits :: Int8 -> BinaryBit.BitPut ()
-putInt8Bits int8 = do
-  let bytes = Binary.runPut (putInt8 int8)
-  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Primitive/Int8Vector.hs b/library/Rattletrap/Primitive/Int8Vector.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Int8Vector.hs
+++ /dev/null
@@ -1,41 +0,0 @@
-module Rattletrap.Primitive.Int8Vector where
-
-import Rattletrap.Primitive.Int8
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data Int8Vector = Int8Vector
-  { int8VectorX :: Maybe Int8
-  , int8VectorY :: Maybe Int8
-  , int8VectorZ :: Maybe Int8
-  } deriving (Eq, Ord, Show)
-
-getInt8Vector :: BinaryBit.BitGet Int8Vector
-getInt8Vector = do
-  x <- getInt8VectorField
-  y <- getInt8VectorField
-  z <- getInt8VectorField
-  pure (Int8Vector x y z)
-
-putInt8Vector :: Int8Vector -> BinaryBit.BitPut ()
-putInt8Vector int8Vector = do
-  putInt8VectorField (int8VectorX int8Vector)
-  putInt8VectorField (int8VectorY int8Vector)
-  putInt8VectorField (int8VectorZ int8Vector)
-
-getInt8VectorField :: BinaryBit.BitGet (Maybe Int8)
-getInt8VectorField = do
-  hasField <- BinaryBit.getBool
-  if hasField
-    then do
-      field <- getInt8Bits
-      pure (Just field)
-    else pure Nothing
-
-putInt8VectorField :: Maybe Int8 -> BinaryBit.BitPut ()
-putInt8VectorField maybeField = case maybeField of
-  Nothing -> BinaryBit.putBool False
-  Just field -> do
-    BinaryBit.putBool True
-    putInt8Bits field
diff --git a/library/Rattletrap/Primitive/List.hs b/library/Rattletrap/Primitive/List.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/List.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-module Rattletrap.Primitive.List where
-
-import Rattletrap.Primitive.Word32
-
-import qualified Control.Monad as Monad
-import qualified Data.Binary as Binary
-
-newtype List a = List
-  { listValue :: [a]
-  } deriving (Eq, Ord, Show)
-
-getList :: Binary.Get a -> Binary.Get (List a)
-getList getElement = do
-  size <- getWord32
-  elements <- Monad.replicateM (fromIntegral (word32Value size)) getElement
-  pure (List elements)
-
-putList :: (a -> Binary.Put) -> List a -> Binary.Put
-putList putElement list = do
-  let elements = listValue list
-  putWord32 (Word32 (fromIntegral (length elements)))
-  mapM_ putElement elements
diff --git a/library/Rattletrap/Primitive/Section.hs b/library/Rattletrap/Primitive/Section.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Section.hs
+++ /dev/null
@@ -1,54 +0,0 @@
-module Rattletrap.Primitive.Section where
-
-import Rattletrap.Crc
-import Rattletrap.Primitive.Word32
-
-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 ByteString
-
--- | A section is a large piece of a 'Rattletrap.Replay.Replay'. It has a
--- 32-bit size (in bytes), a 32-bit CRC (see "Rattletrap.Crc"), and then a
--- bunch of data (the body). This interface is provided so that you don't have
--- to think about the size and CRC.
-newtype Section a = Section
-  { sectionBody :: a
-  -- ^ The actual content in the section.
-  } deriving (Eq, Ord, Show)
-
--- | Given a way to get the 'sectionBody', gets a section. This will fail if
--- the section's CRC does not match.
---
--- @
--- let header = 'Data.Binary.Get.runGet' ('getSection' 'Rattletrap.Header.getHeader') bytes
--- @
-getSection :: Binary.Get a -> Binary.Get (Section a)
-getSection getBody = do
-  size <- getWord32
-  crc <- getWord32
-  rawBody <- Binary.getLazyByteString (fromIntegral (word32Value size))
-  let actualCrc = Word32 (getCrc32 rawBody)
-  Monad.when (actualCrc /= crc) (fail (crcMessage actualCrc crc))
-  let body = Binary.runGet getBody rawBody
-  pure (Section body)
-
--- | Given a way to put the 'sectionBody', puts a section. This will also put
--- the size and CRC.
---
--- @
--- let bytes = 'Data.Binary.Put.runPut' ('putSection' 'Rattletrap.Content.putContent' content)
--- @
-putSection :: (a -> Binary.Put) -> Section a -> Binary.Put
-putSection putBody section = do
-  let rawBody = Binary.runPut (putBody (sectionBody section))
-  let size = ByteString.length rawBody
-  let crc = getCrc32 rawBody
-  putWord32 (Word32 (fromIntegral size))
-  putWord32 (Word32 crc)
-  Binary.putLazyByteString rawBody
-
-crcMessage :: Word32 -> Word32 -> String
-crcMessage actual expected = unwords
-  ["actual CRC", show actual, "does not match expected CRC", show expected]
diff --git a/library/Rattletrap/Primitive/Text.hs b/library/Rattletrap/Primitive/Text.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Text.hs
+++ /dev/null
@@ -1,94 +0,0 @@
-module Rattletrap.Primitive.Text where
-
-import Rattletrap.Primitive.Int32
-import Rattletrap.Utility
-
-import qualified Data.Binary as Binary
-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 ByteString
-import qualified Data.Char as Char
-import qualified Data.Text as Text
-import qualified Data.Text.Encoding as Encoding
-
-newtype Text = Text
-  { textValue :: Text.Text
-  } deriving (Eq, Ord, Show)
-
-getText :: Binary.Get Text
-getText = do
-  rawSize <- getInt32
-  let decode = getTextDecoder rawSize
-  let size = normalizeTextSize rawSize
-  bytes <- Binary.getLazyByteString size
-  let text = dropNull (decode bytes)
-  pure (Text text)
-
-putText :: Text -> Binary.Put
-putText text = do
-  let size = getTextSize text
-  let encode = getTextEncoder size
-  putInt32 size
-  Binary.putLazyByteString (encode (addNull (textValue text)))
-
-getTextBits :: BinaryBit.BitGet Text
-getTextBits = do
-  rawSize <- getInt32Bits
-  let decode = getTextDecoder rawSize
-  let size = normalizeTextSize rawSize
-  bytes <- BinaryBit.getLazyByteString size
-  let text = dropNull (decode (reverseBytes bytes))
-  pure (Text text)
-
-putTextBits :: Text -> BinaryBit.BitPut ()
-putTextBits text = do
-  let size = getTextSize text
-  let encode = getTextEncoder size
-  putInt32Bits size
-  BinaryBit.putByteString
-    (ByteString.toStrict (reverseBytes (encode (addNull (textValue text)))))
-
-stringToText :: String -> Text
-stringToText string = Text (Text.pack string)
-
-textToString :: Text -> String
-textToString text = Text.unpack (textValue text)
-
-getTextSize :: Text -> Int32
-getTextSize text =
-  let
-    value = textValue text
-    scale = if Text.all Char.isLatin1 value then 1 else -1
-    rawSize =
-      if Text.null value then 0 else fromIntegral (Text.length value) + 1
-    size = if value == Text.pack "\x00\x00\x00None"
-      then 0x05000000
-      else scale * rawSize
-  in
-    Int32 size
-
-normalizeTextSize :: Integral a => Int32 -> a
-normalizeTextSize size = case int32Value size of
-  0x05000000 -> 8
-  x -> if x < 0 then (-2 * fromIntegral x) else fromIntegral x
-
-getTextDecoder :: Int32 -> ByteString.ByteString -> Text.Text
-getTextDecoder size bytes =
-  let
-    decode =
-      if size < Int32 0 then Encoding.decodeUtf16LE else Encoding.decodeLatin1
-  in
-    decode (ByteString.toStrict bytes)
-
-getTextEncoder :: Int32 -> Text.Text -> ByteString.ByteString
-getTextEncoder size text = if size < Int32 0
-  then ByteString.fromStrict (Encoding.encodeUtf16LE text)
-  else encodeLatin1 text
-
-dropNull :: Text.Text -> Text.Text
-dropNull = Text.dropWhileEnd (== '\x00')
-
-addNull :: Text.Text -> Text.Text
-addNull text = if Text.null text then text else Text.snoc text '\x00'
diff --git a/library/Rattletrap/Primitive/Vector.hs b/library/Rattletrap/Primitive/Vector.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Vector.hs
+++ /dev/null
@@ -1,41 +0,0 @@
-module Rattletrap.Primitive.Vector where
-
-import Rattletrap.Primitive.CompressedWord
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data Vector = Vector
-  { vectorBias :: Word
-  , vectorX :: Int
-  , vectorY :: Int
-  , vectorZ :: Int
-  } deriving (Eq, Ord, Show)
-
-getVector :: BinaryBit.BitGet Vector
-getVector = do
-  bitSize <- getCompressedWord 19
-  let limit = 2 ^ (compressedWordValue bitSize + 2)
-  dx <- getCompressedWord limit
-  dy <- getCompressedWord limit
-  dz <- getCompressedWord limit
-  let fromCompressedWord x = fromIntegral (compressedWordValue x)
-  let bias = 2 ^ (fromCompressedWord bitSize + 1 :: Word)
-  let x = fromCompressedWord dx - fromIntegral bias
-  let y = fromCompressedWord dy - fromIntegral bias
-  let z = fromCompressedWord dz - fromIntegral bias
-  pure (Vector bias x y z)
-
-putVector :: Vector -> BinaryBit.BitPut ()
-putVector vector = do
-  let
-    bitSize =
-      round (logBase (2 :: Float) (fromIntegral (vectorBias vector))) - 1
-  putCompressedWord (CompressedWord 19 bitSize)
-  let dx = fromIntegral (vectorX vector + fromIntegral (vectorBias vector))
-  let dy = fromIntegral (vectorY vector + fromIntegral (vectorBias vector))
-  let dz = fromIntegral (vectorZ vector + fromIntegral (vectorBias vector))
-  let limit = 2 ^ (bitSize + 2)
-  putCompressedWord (CompressedWord limit dx)
-  putCompressedWord (CompressedWord limit dy)
-  putCompressedWord (CompressedWord limit dz)
diff --git a/library/Rattletrap/Primitive/Word32.hs b/library/Rattletrap/Primitive/Word32.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Word32.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Rattletrap.Primitive.Word32 where
-
-import Rattletrap.Utility
-
-import qualified Data.Binary as Binary
-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 ByteString
-import qualified Data.Word as Word
-
-newtype Word32 = Word32
-  { word32Value :: Word.Word32
-  } deriving (Eq, Ord, Show)
-
-getWord32 :: Binary.Get Word32
-getWord32 = do
-  word32 <- Binary.getWord32le
-  pure (Word32 word32)
-
-putWord32 :: Word32 -> Binary.Put
-putWord32 word32 = Binary.putWord32le (word32Value word32)
-
-getWord32Bits :: BinaryBit.BitGet Word32
-getWord32Bits = do
-  bytes <- BinaryBit.getLazyByteString 4
-  pure (Binary.runGet getWord32 (reverseBytes bytes))
-
-putWord32Bits :: Word32 -> BinaryBit.BitPut ()
-putWord32Bits word32 = do
-  let bytes = Binary.runPut (putWord32 word32)
-  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Primitive/Word64.hs b/library/Rattletrap/Primitive/Word64.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Word64.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Rattletrap.Primitive.Word64 where
-
-import Rattletrap.Utility
-
-import qualified Data.Binary as Binary
-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 ByteString
-import qualified Data.Word as Word
-
-newtype Word64 = Word64
-  { word64Value :: Word.Word64
-  } deriving (Eq, Ord, Show)
-
-getWord64 :: Binary.Get Word64
-getWord64 = do
-  word64 <- Binary.getWord64le
-  pure (Word64 word64)
-
-putWord64 :: Word64 -> Binary.Put
-putWord64 word64 = Binary.putWord64le (word64Value word64)
-
-getWord64Bits :: BinaryBit.BitGet Word64
-getWord64Bits = do
-  bytes <- BinaryBit.getLazyByteString 8
-  pure (Binary.runGet getWord64 (reverseBytes bytes))
-
-putWord64Bits :: Word64 -> BinaryBit.BitPut ()
-putWord64Bits word64 = do
-  let bytes = Binary.runPut (putWord64 word64)
-  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Primitive/Word8.hs b/library/Rattletrap/Primitive/Word8.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Word8.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Rattletrap.Primitive.Word8 where
-
-import Rattletrap.Utility
-
-import qualified Data.Binary as Binary
-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 ByteString
-import qualified Data.Word as Word
-
-newtype Word8 = Word8
-  { word8Value :: Word.Word8
-  } deriving (Eq, Ord, Show)
-
-getWord8 :: Binary.Get Word8
-getWord8 = do
-  word8 <- Binary.getWord8
-  pure (Word8 word8)
-
-putWord8 :: Word8 -> Binary.Put
-putWord8 word8 = Binary.putWord8 (word8Value word8)
-
-getWord8Bits :: BinaryBit.BitGet Word8
-getWord8Bits = do
-  bytes <- BinaryBit.getLazyByteString 1
-  pure (Binary.runGet getWord8 (reverseBytes bytes))
-
-putWord8Bits :: Word8 -> BinaryBit.BitPut ()
-putWord8Bits word8 = do
-  let bytes = Binary.runPut (putWord8 word8)
-  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Property.hs b/library/Rattletrap/Property.hs
deleted file mode 100644
--- a/library/Rattletrap/Property.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module Rattletrap.Property where
-
-import Rattletrap.Primitive
-import Rattletrap.PropertyValue
-
-import qualified Data.Binary as Binary
-
-data Property = Property
-  { propertyKind :: Text
-  , propertySize :: Word64
-  -- ^ Not used.
-  , propertyValue :: PropertyValue Property
-  } deriving (Eq, Ord, Show)
-
-getProperty :: Binary.Get Property
-getProperty = do
-  kind <- getText
-  size <- getWord64
-  value <- getPropertyValue getProperty kind
-  pure (Property kind size value)
-
-putProperty :: Property -> Binary.Put
-putProperty property = do
-  putText (propertyKind property)
-  putWord64 (propertySize property)
-  putPropertyValue putProperty (propertyValue property)
diff --git a/library/Rattletrap/PropertyValue.hs b/library/Rattletrap/PropertyValue.hs
deleted file mode 100644
--- a/library/Rattletrap/PropertyValue.hs
+++ /dev/null
@@ -1,69 +0,0 @@
-module Rattletrap.PropertyValue where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary as Binary
-
-data PropertyValue a
-  = ArrayProperty (List (Dictionary a))
-  -- ^ Yes, a list of dictionaries. No, it doesn't make sense. These usually
-  -- only have one element.
-  | BoolProperty Word8
-  | ByteProperty Text
-                 (Maybe Text)
-  -- ^ This is a strange name for essentially a key-value pair.
-  | FloatProperty Float32
-  | IntProperty Int32
-  | NameProperty Text
-  -- ^ It's unclear how exactly this is different than a 'StrProperty'.
-  | QWordProperty Word64
-  | StrProperty Text
-  deriving (Eq, Ord, Show)
-
-getPropertyValue :: Binary.Get a -> Text -> Binary.Get (PropertyValue a)
-getPropertyValue getProperty kind = case textToString kind of
-  "ArrayProperty" -> do
-    list <- getList (getDictionary getProperty)
-    pure (ArrayProperty list)
-  "BoolProperty" -> do
-    word8 <- getWord8
-    pure (BoolProperty word8)
-  "ByteProperty" -> do
-    k <- getText
-    v <- if textToString k == "OnlinePlatform_Steam"
-      then pure Nothing
-      else do
-        v <- getText
-        pure (Just v)
-    pure (ByteProperty k v)
-  "FloatProperty" -> do
-    float32 <- getFloat32
-    pure (FloatProperty float32)
-  "IntProperty" -> do
-    int32 <- getInt32
-    pure (IntProperty int32)
-  "NameProperty" -> do
-    text <- getText
-    pure (NameProperty text)
-  "QWordProperty" -> do
-    word64 <- getWord64
-    pure (QWordProperty word64)
-  "StrProperty" -> do
-    text <- getText
-    pure (StrProperty text)
-  _ -> fail ("don't know how to read property value " ++ show kind)
-
-putPropertyValue :: (a -> Binary.Put) -> PropertyValue a -> Binary.Put
-putPropertyValue putProperty value = case value of
-  ArrayProperty list -> putList (putDictionary putProperty) list
-  BoolProperty word8 -> putWord8 word8
-  ByteProperty k mv -> do
-    putText k
-    case mv of
-      Nothing -> pure ()
-      Just v -> putText v
-  FloatProperty float32 -> putFloat32 float32
-  IntProperty int32 -> putInt32 int32
-  NameProperty text -> putText text
-  QWordProperty word64 -> putWord64 word64
-  StrProperty text -> putText text
diff --git a/library/Rattletrap/RemoteId.hs b/library/Rattletrap/RemoteId.hs
deleted file mode 100644
--- a/library/Rattletrap/RemoteId.hs
+++ /dev/null
@@ -1,53 +0,0 @@
-module Rattletrap.RemoteId where
-
-import Rattletrap.Primitive
-import Rattletrap.Utility
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.ByteString.Lazy as ByteString
-import qualified Data.Text as Text
-import qualified Data.Text.Encoding as Encoding
-import qualified Data.Word as Word
-
-data RemoteId
-  = PlayStationId Text.Text
-                  [Word.Word8]
-  | SplitscreenId Word.Word32
-  | SteamId Word64
-  | XboxId Word64
-  deriving (Eq, Ord, Show)
-
-getRemoteId :: (Int, Int, Int) -> Word8 -> BinaryBit.BitGet RemoteId
-getRemoteId (_, _, patchVersion) systemId = case word8Value systemId of
-  0 -> do
-    word24 <- BinaryBit.getWord32be 24
-    pure (SplitscreenId word24)
-  1 -> do
-    word64 <- getWord64Bits
-    pure (SteamId word64)
-  2 -> do
-    rawName <- BinaryBit.getLazyByteString 16
-    let
-      name = Text.dropWhileEnd
-        (== '\x00')
-        (Encoding.decodeLatin1 (ByteString.toStrict (reverseBytes rawName)))
-      numBytes = if patchVersion >= 1 then 24 else 16
-    bytes <- BinaryBit.getLazyByteString numBytes
-    pure (PlayStationId name (ByteString.unpack bytes))
-  4 -> do
-    word64 <- getWord64Bits
-    pure (XboxId word64)
-  _ -> fail ("unknown system id " ++ show systemId)
-
-putRemoteId :: RemoteId -> BinaryBit.BitPut ()
-putRemoteId remoteId = case remoteId of
-  PlayStationId name bytes -> do
-    let
-      rawName = ByteString.toStrict
-        (reverseBytes (padBytes (16 :: Int) (encodeLatin1 name)))
-    BinaryBit.putByteString rawName
-    BinaryBit.putByteString (ByteString.toStrict (ByteString.pack bytes))
-  SplitscreenId word24 -> BinaryBit.putWord32be 24 word24
-  SteamId word64 -> putWord64Bits word64
-  XboxId word64 -> putWord64Bits word64
diff --git a/library/Rattletrap/Replay.hs b/library/Rattletrap/Replay.hs
deleted file mode 100644
--- a/library/Rattletrap/Replay.hs
+++ /dev/null
@@ -1,39 +0,0 @@
-module Rattletrap.Replay where
-
-import Rattletrap.Content
-import Rattletrap.Header
-import Rattletrap.Primitive.Section
-
-import qualified Data.Binary as Binary
-
--- | A Rocket League replay.
-data Replay = Replay
-  { replayHeader :: Section Header
-  -- ^ This has most of the high-level metadata.
-  , replayContent :: Section Content
-  -- ^ This has most of the low-level game data.
-  } deriving (Eq, Ord, Show)
-
--- | Parses a raw replay. Use this with 'Data.Binary.Get.runGet'.
---
--- @
--- let replay = 'Data.Binary.Get.runGet' 'getReplay' bytes
--- @
-getReplay :: Binary.Get Replay
-getReplay = do
-  header <- getSection getHeader
-  let version = getVersion (sectionBody header)
-  let numFrames = getNumFrames (sectionBody header)
-  let maxChannels = getMaxChannels (sectionBody header)
-  content <- getSection (getContent version numFrames maxChannels)
-  pure (Replay header content)
-
--- | Generates a raw replay. Use this with 'Data.Binary.Put.runPut'.
---
--- @
--- let bytes = 'Data.Binary.Put.runPut' ('putReplay' replay)
--- @
-putReplay :: Replay -> Binary.Put
-putReplay replay = do
-  putSection putHeader (replayHeader replay)
-  putSection putContent (replayContent replay)
diff --git a/library/Rattletrap/Replication.hs b/library/Rattletrap/Replication.hs
deleted file mode 100644
--- a/library/Rattletrap/Replication.hs
+++ /dev/null
@@ -1,66 +0,0 @@
-module Rattletrap.Replication where
-
-import Rattletrap.ActorMap
-import Rattletrap.ClassAttributeMap
-import Rattletrap.Primitive
-import Rattletrap.ReplicationValue
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data Replication = Replication
-  { replicationActorId :: CompressedWord
-  , replicationValue :: ReplicationValue
-  } deriving (Eq, Ord, Show)
-
-getReplications
-  :: (Int, Int, Int)
-  -> Word
-  -> ClassAttributeMap
-  -> ActorMap
-  -> BinaryBit.BitGet ([Replication], ActorMap)
-getReplications version maxChannels classAttributeMap actorMap = do
-  maybeReplication <- getReplication
-    version
-    maxChannels
-    classAttributeMap
-    actorMap
-  case maybeReplication of
-    Nothing -> pure ([], actorMap)
-    Just (replication, newActorMap) -> do
-      (replications, newerActorMap) <- getReplications
-        version
-        maxChannels
-        classAttributeMap
-        newActorMap
-      pure (replication : replications, newerActorMap)
-
-putReplications :: [Replication] -> BinaryBit.BitPut ()
-putReplications replications = do
-  mapM_ putReplication replications
-  BinaryBit.putBool False
-
-getReplication
-  :: (Int, Int, Int)
-  -> Word
-  -> ClassAttributeMap
-  -> ActorMap
-  -> BinaryBit.BitGet (Maybe (Replication, ActorMap))
-getReplication version maxChannels classAttributeMap actorMap = do
-  hasReplication <- BinaryBit.getBool
-  if not hasReplication
-    then pure Nothing
-    else do
-      actorId <- getCompressedWord maxChannels
-      (value, newActorMap) <- getReplicationValue
-        version
-        classAttributeMap
-        actorMap
-        actorId
-      pure (Just (Replication actorId value, newActorMap))
-
-putReplication :: Replication -> BinaryBit.BitPut ()
-putReplication replication = do
-  BinaryBit.putBool True
-  putCompressedWord (replicationActorId replication)
-  putReplicationValue (replicationValue replication)
diff --git a/library/Rattletrap/Replication/Destroyed.hs b/library/Rattletrap/Replication/Destroyed.hs
deleted file mode 100644
--- a/library/Rattletrap/Replication/Destroyed.hs
+++ /dev/null
@@ -1,17 +0,0 @@
-module Rattletrap.Replication.Destroyed where
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
--- | Destroyed replications don't actually contain any extra information. All
--- you need to know is the actor's ID, which is given by the
--- 'Rattletrap.Replication.Replication'.
-data DestroyedReplication = DestroyedReplication
-  {
-  } deriving (Eq, Ord, Show)
-
-getDestroyedReplication :: BinaryBit.BitGet DestroyedReplication
-getDestroyedReplication = pure DestroyedReplication
-
-putDestroyedReplication :: DestroyedReplication -> BinaryBit.BitPut ()
-putDestroyedReplication _ = pure ()
diff --git a/library/Rattletrap/Replication/Spawned.hs b/library/Rattletrap/Replication/Spawned.hs
deleted file mode 100644
--- a/library/Rattletrap/Replication/Spawned.hs
+++ /dev/null
@@ -1,88 +0,0 @@
-module Rattletrap.Replication.Spawned where
-
-import Rattletrap.ActorMap
-import Rattletrap.ClassAttributeMap
-import Rattletrap.Initialization
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Map as Map
-
-data SpawnedReplication = SpawnedReplication
-  { spawnedReplicationFlag :: Bool
-  -- ^ Unclear what this is.
-  , spawnedReplicationNameIndex :: Maybe Word32
-  , spawnedReplicationName :: Maybe Text
-  -- ^ Read-only! Changing a replication's name requires editing the
-  -- 'spawnedReplicationNameIndex' and maybe the class attribute map.
-  , spawnedReplicationObjectId :: Word32
-  , spawnedReplicationObjectName :: Text
-  -- ^ Read-only! Changing a replication's object requires editing the class
-  -- attribute map.
-  , spawnedReplicationClassName :: Text
-  -- ^ Read-only! Changing a replication's class requires editing the class
-  -- attribute map.
-  , spawnedReplicationInitialization :: Initialization
-  } deriving (Eq, Ord, Show)
-
-getSpawnedReplication
-  :: (Int, Int, Int)
-  -> ClassAttributeMap
-  -> ActorMap
-  -> CompressedWord
-  -> BinaryBit.BitGet (SpawnedReplication, ActorMap)
-getSpawnedReplication version classAttributeMap actorMap actorId = do
-  flag <- BinaryBit.getBool
-  nameIndex <- if version < (868, 14, 0)
-    then pure Nothing
-    else do
-      nameIndex <- getWord32Bits
-      pure (Just nameIndex)
-  name <- lookupName classAttributeMap nameIndex
-  objectId <- getWord32Bits
-  let newActorMap = Map.insert actorId objectId actorMap
-  objectName <- lookupObjectName classAttributeMap objectId
-  className <- lookupClassName objectName
-  let hasLocation = classHasLocation className
-  let hasRotation = classHasRotation className
-  initialization <- getInitialization hasLocation hasRotation
-  pure
-    ( SpawnedReplication
-      flag
-      nameIndex
-      name
-      objectId
-      objectName
-      className
-      initialization
-    , newActorMap
-    )
-
-putSpawnedReplication :: SpawnedReplication -> BinaryBit.BitPut ()
-putSpawnedReplication spawnedReplication = do
-  BinaryBit.putBool (spawnedReplicationFlag spawnedReplication)
-  case spawnedReplicationNameIndex spawnedReplication of
-    Nothing -> pure ()
-    Just nameIndex -> putWord32Bits nameIndex
-  putWord32Bits (spawnedReplicationObjectId spawnedReplication)
-  putInitialization (spawnedReplicationInitialization spawnedReplication)
-
-lookupName :: Monad m => ClassAttributeMap -> Maybe Word32 -> m (Maybe Text)
-lookupName classAttributeMap maybeNameIndex = case maybeNameIndex of
-  Nothing -> pure Nothing
-  Just nameIndex ->
-    case getName (classAttributeMapNameMap classAttributeMap) nameIndex of
-      Nothing -> fail ("could not get name for index " ++ show nameIndex)
-      Just name -> pure (Just name)
-
-lookupObjectName :: Monad m => ClassAttributeMap -> Word32 -> m Text
-lookupObjectName classAttributeMap objectId =
-  case getObjectName (classAttributeMapObjectMap classAttributeMap) objectId of
-    Nothing -> fail ("could not get object name for id " ++ show objectId)
-    Just objectName -> pure objectName
-
-lookupClassName :: Monad m => Text -> m Text
-lookupClassName objectName = case getClassName objectName of
-  Nothing -> fail ("could not get class name for object " ++ show objectName)
-  Just className -> pure className
diff --git a/library/Rattletrap/Replication/Updated.hs b/library/Rattletrap/Replication/Updated.hs
deleted file mode 100644
--- a/library/Rattletrap/Replication/Updated.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-module Rattletrap.Replication.Updated where
-
-import Rattletrap.ActorMap
-import Rattletrap.Attribute
-import Rattletrap.ClassAttributeMap
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype UpdatedReplication = UpdatedReplication
-  { updatedReplicationAttributes :: [Attribute]
-  } deriving (Eq, Ord, Show)
-
-getUpdatedReplication
-  :: (Int, Int, Int)
-  -> ClassAttributeMap
-  -> ActorMap
-  -> CompressedWord
-  -> BinaryBit.BitGet UpdatedReplication
-getUpdatedReplication version classAttributeMap actorMap actorId = do
-  attributes <- getAttributes version classAttributeMap actorMap actorId
-  pure (UpdatedReplication attributes)
-
-putUpdatedReplication :: UpdatedReplication -> BinaryBit.BitPut ()
-putUpdatedReplication updatedReplication =
-  putAttributes (updatedReplicationAttributes updatedReplication)
diff --git a/library/Rattletrap/ReplicationValue.hs b/library/Rattletrap/ReplicationValue.hs
deleted file mode 100644
--- a/library/Rattletrap/ReplicationValue.hs
+++ /dev/null
@@ -1,65 +0,0 @@
-module Rattletrap.ReplicationValue
-  ( module Rattletrap.ReplicationValue
-  , module Rattletrap.Replication.Destroyed
-  , module Rattletrap.Replication.Spawned
-  , module Rattletrap.Replication.Updated
-  ) where
-
-import Rattletrap.ActorMap
-import Rattletrap.ClassAttributeMap
-import Rattletrap.Primitive
-import Rattletrap.Replication.Destroyed
-import Rattletrap.Replication.Spawned
-import Rattletrap.Replication.Updated
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data ReplicationValue
-  = SpawnedReplicationValue SpawnedReplication
-  -- ^ Creates a new actor.
-  | UpdatedReplicationValue UpdatedReplication
-  -- ^ Updates an existing actor.
-  | DestroyedReplicationValue DestroyedReplication
-  -- ^ Destroys an existing actor.
-  deriving (Eq, Ord, Show)
-
-getReplicationValue
-  :: (Int, Int, Int)
-  -> ClassAttributeMap
-  -> ActorMap
-  -> CompressedWord
-  -> BinaryBit.BitGet (ReplicationValue, ActorMap)
-getReplicationValue version classAttributeMap actorMap actorId = do
-  isOpen <- BinaryBit.getBool
-  if isOpen
-    then do
-      isNew <- BinaryBit.getBool
-      if isNew
-        then do
-          (x, newActorMap) <- getSpawnedReplication
-            version
-            classAttributeMap
-            actorMap
-            actorId
-          pure (SpawnedReplicationValue x, newActorMap)
-        else do
-          x <- getUpdatedReplication version classAttributeMap actorMap actorId
-          pure (UpdatedReplicationValue x, actorMap)
-    else do
-      x <- getDestroyedReplication
-      pure (DestroyedReplicationValue x, actorMap)
-
-putReplicationValue :: ReplicationValue -> BinaryBit.BitPut ()
-putReplicationValue value = case value of
-  SpawnedReplicationValue x -> do
-    BinaryBit.putBool True
-    BinaryBit.putBool True
-    putSpawnedReplication x
-  UpdatedReplicationValue x -> do
-    BinaryBit.putBool True
-    BinaryBit.putBool False
-    putUpdatedReplication x
-  DestroyedReplicationValue x -> do
-    BinaryBit.putBool False
-    putDestroyedReplication x
diff --git a/library/Rattletrap/Type/AppliedDamageAttribute.hs b/library/Rattletrap/Type/AppliedDamageAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/AppliedDamageAttribute.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.AppliedDamageAttribute
+  ( AppliedDamageAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Int32le
+import Rattletrap.Type.Vector
+import Rattletrap.Type.Word8le
+
+data AppliedDamageAttribute = AppliedDamageAttribute
+  { appliedDamageAttributeUnknown1 :: Word8le
+  , appliedDamageAttributeLocation :: Vector
+  , appliedDamageAttributeUnknown3 :: Int32le
+  , appliedDamageAttributeUnknown4 :: Int32le
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''AppliedDamageAttribute)
diff --git a/library/Rattletrap/Type/Attribute.hs b/library/Rattletrap/Type/Attribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Attribute.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Attribute
+  ( Attribute(..)
+  ) where
+
+import Rattletrap.Type.AttributeValue
+import Rattletrap.Type.Common
+import Rattletrap.Type.CompressedWord
+import Rattletrap.Type.Str
+
+data Attribute = Attribute
+  { attributeId :: CompressedWord
+  , attributeName :: Str
+  -- ^ Read-only! Changing an attribute's name requires editing the class
+  -- attribute map.
+  , attributeValue :: AttributeValue
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Attribute)
diff --git a/library/Rattletrap/Type/AttributeMapping.hs b/library/Rattletrap/Type/AttributeMapping.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/AttributeMapping.hs
@@ -0,0 +1,15 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.AttributeMapping
+  ( AttributeMapping(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Word32le
+
+data AttributeMapping = AttributeMapping
+  { attributeMappingObjectId :: Word32le
+  , attributeMappingStreamId :: Word32le
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''AttributeMapping)
diff --git a/library/Rattletrap/Type/AttributeType.hs b/library/Rattletrap/Type/AttributeType.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/AttributeType.hs
@@ -0,0 +1,43 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.AttributeType
+  ( AttributeType(..)
+  ) where
+
+import Rattletrap.Type.Common
+
+data AttributeType
+  = AttributeTypeAppliedDamage
+  | AttributeTypeBoolean
+  | AttributeTypeByte
+  | AttributeTypeCamSettings
+  | AttributeTypeClubColors
+  | AttributeTypeDamageState
+  | AttributeTypeDemolish
+  | AttributeTypeEnum
+  | AttributeTypeExplosion
+  | AttributeTypeExtendedExplosion
+  | AttributeTypeFlaggedInt
+  | AttributeTypeFloat
+  | AttributeTypeGameMode
+  | AttributeTypeInt
+  | AttributeTypeLoadout
+  | AttributeTypeLoadoutOnline
+  | AttributeTypeLoadouts
+  | AttributeTypeLoadoutsOnline
+  | AttributeTypeLocation
+  | AttributeTypeMusicStinger
+  | AttributeTypePartyLeader
+  | AttributeTypePickup
+  | AttributeTypePlayerHistoryKey
+  | AttributeTypePrivateMatchSettings
+  | AttributeTypeQWord
+  | AttributeTypeReservation
+  | AttributeTypeRigidBodyState
+  | AttributeTypeString
+  | AttributeTypeTeamPaint
+  | AttributeTypeUniqueId
+  | AttributeTypeWeldedInfo
+  deriving (Eq, Ord, Show)
+
+$(deriveJson ''AttributeType)
diff --git a/library/Rattletrap/Type/AttributeValue.hs b/library/Rattletrap/Type/AttributeValue.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/AttributeValue.hs
@@ -0,0 +1,74 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.AttributeValue
+  ( AttributeValue(..)
+  ) where
+
+import Rattletrap.Type.AppliedDamageAttribute
+import Rattletrap.Type.BooleanAttribute
+import Rattletrap.Type.ByteAttribute
+import Rattletrap.Type.CamSettingsAttribute
+import Rattletrap.Type.ClubColorsAttribute
+import Rattletrap.Type.Common
+import Rattletrap.Type.DamageStateAttribute
+import Rattletrap.Type.DemolishAttribute
+import Rattletrap.Type.EnumAttribute
+import Rattletrap.Type.ExplosionAttribute
+import Rattletrap.Type.ExtendedExplosionAttribute
+import Rattletrap.Type.FlaggedIntAttribute
+import Rattletrap.Type.FloatAttribute
+import Rattletrap.Type.GameModeAttribute
+import Rattletrap.Type.IntAttribute
+import Rattletrap.Type.LoadoutAttribute
+import Rattletrap.Type.LoadoutOnlineAttribute
+import Rattletrap.Type.LoadoutsAttribute
+import Rattletrap.Type.LoadoutsOnlineAttribute
+import Rattletrap.Type.LocationAttribute
+import Rattletrap.Type.MusicStingerAttribute
+import Rattletrap.Type.PartyLeaderAttribute
+import Rattletrap.Type.PickupAttribute
+import Rattletrap.Type.PlayerHistoryKeyAttribute
+import Rattletrap.Type.PrivateMatchSettingsAttribute
+import Rattletrap.Type.QWordAttribute
+import Rattletrap.Type.ReservationAttribute
+import Rattletrap.Type.RigidBodyStateAttribute
+import Rattletrap.Type.StringAttribute
+import Rattletrap.Type.TeamPaintAttribute
+import Rattletrap.Type.UniqueIdAttribute
+import Rattletrap.Type.WeldedInfoAttribute
+
+data AttributeValue
+  = AttributeValueAppliedDamage AppliedDamageAttribute
+  | AttributeValueBoolean BooleanAttribute
+  | AttributeValueByte ByteAttribute
+  | AttributeValueCamSettings CamSettingsAttribute
+  | AttributeValueClubColors ClubColorsAttribute
+  | AttributeValueDamageState DamageStateAttribute
+  | AttributeValueDemolish DemolishAttribute
+  | AttributeValueEnum EnumAttribute
+  | AttributeValueExplosion ExplosionAttribute
+  | AttributeValueExtendedExplosion ExtendedExplosionAttribute
+  | AttributeValueFlaggedInt FlaggedIntAttribute
+  | AttributeValueFloat FloatAttribute
+  | AttributeValueGameMode GameModeAttribute
+  | AttributeValueInt IntAttribute
+  | AttributeValueLoadout LoadoutAttribute
+  | AttributeValueLoadoutOnline LoadoutOnlineAttribute
+  | AttributeValueLoadouts LoadoutsAttribute
+  | AttributeValueLoadoutsOnline LoadoutsOnlineAttribute
+  | AttributeValueLocation LocationAttribute
+  | AttributeValueMusicStinger MusicStingerAttribute
+  | AttributeValuePartyLeader PartyLeaderAttribute
+  | AttributeValuePickup PickupAttribute
+  | AttributeValuePlayerHistoryKey PlayerHistoryKeyAttribute
+  | AttributeValuePrivateMatchSettings PrivateMatchSettingsAttribute
+  | AttributeValueQWord QWordAttribute
+  | AttributeValueReservation ReservationAttribute
+  | AttributeValueRigidBodyState RigidBodyStateAttribute
+  | AttributeValueString StringAttribute
+  | AttributeValueTeamPaint TeamPaintAttribute
+  | AttributeValueUniqueId UniqueIdAttribute
+  | AttributeValueWeldedInfo WeldedInfoAttribute
+  deriving (Eq, Ord, Show)
+
+$(deriveJson ''AttributeValue)
diff --git a/library/Rattletrap/Type/Bitstream.hs b/library/Rattletrap/Type/Bitstream.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Bitstream.hs
@@ -0,0 +1,28 @@
+module Rattletrap.Type.Bitstream
+  ( Bitstream(..)
+  ) where
+
+import qualified Data.Aeson as Json
+import qualified Data.Bool as Bool
+import qualified Data.Text as Text
+
+newtype Bitstream = Bitstream
+  { bitstreamValue :: [Bool]
+  } deriving (Eq, Ord, Show)
+
+instance Json.FromJSON Bitstream where
+  parseJSON =
+    Json.withText
+      "Bitstream"
+      (\text ->
+         Bitstream <$>
+         mapM
+           (\char ->
+              case char of
+                '0' -> pure False
+                '1' -> pure True
+                _ -> fail ("invalid bit: " ++ show char))
+           (Text.unpack text))
+
+instance Json.ToJSON Bitstream where
+  toJSON = Json.toJSON . fmap (Bool.bool '0' '1') . bitstreamValue
diff --git a/library/Rattletrap/Type/BooleanAttribute.hs b/library/Rattletrap/Type/BooleanAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/BooleanAttribute.hs
@@ -0,0 +1,13 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.BooleanAttribute
+  ( BooleanAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+
+newtype BooleanAttribute = BooleanAttribute
+  { booleanAttributeValue :: Bool
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''BooleanAttribute)
diff --git a/library/Rattletrap/Type/ByteAttribute.hs b/library/Rattletrap/Type/ByteAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/ByteAttribute.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.ByteAttribute
+  ( ByteAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Word8le
+
+newtype ByteAttribute = ByteAttribute
+  { byteAttributeValue :: Word8le
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''ByteAttribute)
diff --git a/library/Rattletrap/Type/Cache.hs b/library/Rattletrap/Type/Cache.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Cache.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Cache
+  ( Cache(..)
+  ) where
+
+import Rattletrap.Type.AttributeMapping
+import Rattletrap.Type.Common
+import Rattletrap.Type.List
+import Rattletrap.Type.Word32le
+
+data Cache = Cache
+  { cacheClassId :: Word32le
+  , cacheParentCacheId :: Word32le
+  , cacheCacheId :: Word32le
+  , cacheAttributeMappings :: List AttributeMapping
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Cache)
diff --git a/library/Rattletrap/Type/CamSettingsAttribute.hs b/library/Rattletrap/Type/CamSettingsAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/CamSettingsAttribute.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.CamSettingsAttribute
+  ( CamSettingsAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Float32le
+
+data CamSettingsAttribute = CamSettingsAttribute
+  { camSettingsAttributeFov :: Float32le
+  , camSettingsAttributeHeight :: Float32le
+  , camSettingsAttributeAngle :: Float32le
+  , camSettingsAttributeDistance :: Float32le
+  , camSettingsAttributeStiffness :: Float32le
+  , camSettingsAttributeSwivelSpeed :: Float32le
+  , camSettingsAttributeTransitionSpeed :: Maybe Float32le
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''CamSettingsAttribute)
diff --git a/library/Rattletrap/Type/ClassAttributeMap.hs b/library/Rattletrap/Type/ClassAttributeMap.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/ClassAttributeMap.hs
@@ -0,0 +1,326 @@
+module Rattletrap.Type.ClassAttributeMap
+  ( ClassAttributeMap(..)
+  , classHasLocation
+  , classHasRotation
+  , getAttributeIdLimit
+  , getAttributeMap
+  , getAttributeName
+  , getClassName
+  , getName
+  , getObjectName
+  , makeClassAttributeMap
+  ) where
+
+import Rattletrap.Data
+import Rattletrap.Type.AttributeMapping
+import Rattletrap.Type.Cache
+import Rattletrap.Type.ClassMapping
+import Rattletrap.Type.Common
+import Rattletrap.Type.CompressedWord
+import Rattletrap.Type.List
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word32le
+
+import qualified Data.IntMap as IntMap
+import qualified Data.List as List
+import qualified Data.Map as Map
+import qualified Data.Maybe as Maybe
+import qualified Data.Set as Set
+import qualified Data.Text as Text
+import qualified Data.Tuple as Tuple
+
+-- | This data structure holds all the information about classes, objects, and
+-- attributes in the replay. The class hierarchy is not fixed; it is encoded
+-- in the 'Rattletrap.Content.Content'. Similarly, the attributes that belong
+-- to each class are not fixed either. Converting the raw data into a usable
+-- structure is tedious; see 'makeClassAttributeMap'.
+data ClassAttributeMap = ClassAttributeMap
+  { classAttributeMapObjectMap :: Map Word32le Str
+  -- ^ A map from object IDs to their names.
+  , classAttributeMapObjectClassMap :: Map Word32le Word32le
+  -- ^ A map from object IDs to their class IDs.
+  , classAttributeMapValue :: Map Word32le (Map Word32le Word32le)
+  -- ^ A map from class IDs to a map from attribute stream IDs to attribute
+  -- IDs.
+  , classAttributeMapNameMap :: IntMap.IntMap Str
+  } deriving (Eq, Ord, Show)
+
+type Bimap l r = (Map l r, Map r l)
+
+bimap :: (Ord l, Ord r) => [(l, r)] -> Bimap l r
+bimap xs = (Map.fromList xs, Map.fromList (fmap Tuple.swap xs))
+
+lookupL :: Ord l => l -> Bimap l r -> Maybe r
+lookupL k = Map.lookup k . fst
+
+lookupR :: Ord r => r -> Bimap l r -> Maybe l
+lookupR k = Map.lookup k . snd
+
+-- | Makes a 'ClassAttributeMap' given the necessary fields from the
+-- 'Rattletrap.Content.Content'.
+makeClassAttributeMap
+  :: List Str
+  -- ^ From 'Rattletrap.Content.contentObjects'.
+  -> List ClassMapping
+  -- ^ From 'Rattletrap.Content.contentClassMappings'.
+  -> List Cache
+  -- ^ From 'Rattletrap.Content.contentCaches'.
+  -> List Str
+  -- ^ From 'Rattletrap.Content.contentNames'.
+  -> ClassAttributeMap
+makeClassAttributeMap objects classMappings caches names =
+  let
+    objectMap = makeObjectMap objects
+    classMap = makeClassMap classMappings
+    objectClassMap = makeObjectClassMap objectMap classMap
+    classCache = makeClassCache classMap caches
+    attributeMap = makeAttributeMap caches
+    classIds = fmap (\(_, classId, _, _) -> classId) classCache
+    parentMap = makeParentMap classCache
+    value = Map.fromList
+      ( fmap
+        ( \classId ->
+          let
+            ownAttributes =
+              Maybe.fromMaybe Map.empty (Map.lookup classId attributeMap)
+            parentsAttributes = case Map.lookup classId parentMap of
+              Nothing -> []
+              Just parentClassIds -> fmap
+                ( \parentClassId -> Maybe.fromMaybe
+                  Map.empty
+                  (Map.lookup parentClassId attributeMap)
+                )
+                parentClassIds
+            attributes = ownAttributes : parentsAttributes
+          in (classId, Map.fromList (concatMap Map.toList attributes))
+        )
+        classIds
+      )
+    nameMap = makeNameMap names
+  in ClassAttributeMap objectMap objectClassMap value nameMap
+
+makeNameMap :: List Str -> IntMap.IntMap Str
+makeNameMap names = IntMap.fromDistinctAscList (zip [0 ..] (listValue names))
+
+getName :: IntMap.IntMap Str -> Word32le -> Maybe Str
+getName nameMap nameIndex =
+  IntMap.lookup (fromIntegral (word32leValue nameIndex)) nameMap
+
+makeObjectClassMap
+  :: Map Word32le Str -> Bimap Word32le Str -> Map Word32le Word32le
+makeObjectClassMap objectMap classMap = do
+  let objectIds = Map.keys objectMap
+  let classIds = fmap (getClassId objectMap classMap) objectIds
+  let rawPairs = zip objectIds classIds
+  let
+    pairs = Maybe.mapMaybe
+      ( \(objectId, maybeClassId) -> case maybeClassId of
+        Nothing -> Nothing
+        Just classId -> Just (objectId, classId)
+      )
+      rawPairs
+  Map.fromList pairs
+
+getClassId
+  :: Map Word32le Str -> Bimap Word32le Str -> Word32le -> Maybe Word32le
+getClassId objectMap classMap objectId = do
+  objectName <- getObjectName objectMap objectId
+  className <- getClassName objectName
+  lookupR className classMap
+
+makeClassCache
+  :: Bimap Word32le Str
+  -> List Cache
+  -> [(Maybe Str, Word32le, Word32le, Word32le)]
+makeClassCache classMap caches = fmap
+  ( \cache ->
+    let
+      classId = cacheClassId cache
+    in
+      ( lookupL classId classMap
+      , classId
+      , cacheCacheId cache
+      , cacheParentCacheId cache
+      )
+  )
+  (listValue caches)
+
+makeClassMap :: List ClassMapping -> Bimap Word32le Str
+makeClassMap classMappings = bimap
+  ( fmap
+    ( \classMapping ->
+      (classMappingStreamId classMapping, classMappingName classMapping)
+    )
+    (listValue classMappings)
+  )
+
+makeAttributeMap :: List Cache -> Map Word32le (Map Word32le Word32le)
+makeAttributeMap caches = Map.fromList
+  ( fmap
+    ( \cache ->
+      ( cacheClassId cache
+      , Map.fromList
+        ( fmap
+          ( \attributeMapping ->
+            ( attributeMappingStreamId attributeMapping
+            , attributeMappingObjectId attributeMapping
+            )
+          )
+          (listValue (cacheAttributeMappings cache))
+        )
+      )
+    )
+    (listValue caches)
+  )
+
+makeShallowParentMap
+  :: [(Maybe Str, Word32le, Word32le, Word32le)] -> Map Word32le Word32le
+makeShallowParentMap classCache = Map.fromList
+  ( Maybe.mapMaybe
+    ( \xs -> case xs of
+      [] -> Nothing
+      (maybeClassName, classId, _, parentCacheId):rest -> do
+        parentClassId <- getParentClass maybeClassName parentCacheId rest
+        pure (classId, parentClassId)
+    )
+    (List.tails (reverse classCache))
+  )
+
+makeParentMap
+  :: [(Maybe Str, Word32le, Word32le, Word32le)] -> Map Word32le [Word32le]
+makeParentMap classCache =
+  let
+    shallowParentMap = makeShallowParentMap classCache
+  in
+    Map.mapWithKey
+      (\classId _ -> getParentClasses shallowParentMap classId)
+      shallowParentMap
+
+getParentClasses :: Map Word32le Word32le -> Word32le -> [Word32le]
+getParentClasses shallowParentMap classId =
+  case Map.lookup classId shallowParentMap of
+    Nothing -> []
+    Just parentClassId ->
+      parentClassId : getParentClasses shallowParentMap parentClassId
+
+getParentClass
+  :: Maybe Str
+  -> Word32le
+  -> [(Maybe Str, Word32le, Word32le, Word32le)]
+  -> Maybe Word32le
+getParentClass maybeClassName parentCacheId xs = case maybeClassName of
+  Nothing -> getParentClassById parentCacheId xs
+  Just className -> getParentClassByName className parentCacheId xs
+
+getParentClassById
+  :: Word32le -> [(Maybe Str, Word32le, Word32le, Word32le)] -> Maybe Word32le
+getParentClassById parentCacheId xs =
+  case dropWhile (\(_, _, cacheId, _) -> cacheId /= parentCacheId) xs of
+    [] -> if parentCacheId == Word32le 0
+      then Nothing
+      else getParentClassById (Word32le (word32leValue parentCacheId - 1)) xs
+    (_, parentClassId, _, _):_ -> Just parentClassId
+
+getParentClassByName
+  :: Str
+  -> Word32le
+  -> [(Maybe Str, Word32le, Word32le, Word32le)]
+  -> Maybe Word32le
+getParentClassByName className parentCacheId xs =
+  case Map.lookup className parentClasses of
+    Nothing -> getParentClassById parentCacheId xs
+    Just parentClassName -> Maybe.maybe
+      (getParentClassById parentCacheId xs)
+      Just
+      ( Maybe.listToMaybe
+        ( fmap
+          (\(_, parentClassId, _, _) -> parentClassId)
+          ( filter
+            (\(_, _, cacheId, _) -> cacheId <= parentCacheId)
+            ( filter
+              ( \(maybeClassName, _, _, _) ->
+                maybeClassName == Just parentClassName
+              )
+              xs
+            )
+          )
+        )
+      )
+
+parentClasses :: Map Str Str
+parentClasses =
+  Map.map toStr (Map.mapKeys toStr (Map.fromList rawParentClasses))
+
+makeObjectMap :: List Str -> Map Word32le Str
+makeObjectMap objects =
+  Map.fromAscList (zip (fmap Word32le [0 ..]) (listValue objects))
+
+getObjectName :: Map Word32le Str -> Word32le -> Maybe Str
+getObjectName objectMap objectId = Map.lookup objectId objectMap
+
+getClassName :: Str -> Maybe Str
+getClassName rawObjectName =
+  Map.lookup (normalizeObjectName rawObjectName) objectClasses
+
+normalizeObjectName :: Str -> Str
+normalizeObjectName objectName =
+  let
+    name = strValue objectName
+    crowdActor = Text.pack "TheWorld:PersistentLevel.CrowdActor_TA"
+    crowdManager = Text.pack "TheWorld:PersistentLevel.CrowdManager_TA"
+    boostPickup = Text.pack "TheWorld:PersistentLevel.VehiclePickup_Boost_TA"
+    mapScoreboard = Text.pack "TheWorld:PersistentLevel.InMapScoreboard_TA"
+    breakout = Text.pack "TheWorld:PersistentLevel.BreakOutActor_Platform_TA"
+  in
+    if Text.isInfixOf crowdActor name
+      then Str crowdActor
+      else if Text.isInfixOf crowdManager name
+        then Str crowdManager
+        else if Text.isInfixOf boostPickup name
+          then Str boostPickup
+          else if Text.isInfixOf mapScoreboard name
+            then Str mapScoreboard
+            else if Text.isInfixOf breakout name
+              then Str breakout
+              else objectName
+
+objectClasses :: Map Str Str
+objectClasses =
+  Map.map toStr (Map.mapKeys toStr (Map.fromList rawObjectClasses))
+
+classHasLocation :: Str -> Bool
+classHasLocation className = Set.member className classesWithLocation
+
+classesWithLocation :: Set.Set Str
+classesWithLocation = Set.fromList (fmap toStr rawClassesWithLocation)
+
+classHasRotation :: Str -> Bool
+classHasRotation className = Set.member className classesWithRotation
+
+classesWithRotation :: Set.Set Str
+classesWithRotation = Set.fromList (fmap toStr rawClassesWithRotation)
+
+getAttributeIdLimit :: Map Word32le Word32le -> Maybe Word
+getAttributeIdLimit attributeMap = do
+  ((streamId, _), _) <- Map.maxViewWithKey attributeMap
+  pure (fromIntegral (word32leValue streamId))
+
+getAttributeName
+  :: ClassAttributeMap -> Map Word32le Word32le -> CompressedWord -> Maybe Str
+getAttributeName classAttributeMap attributeMap streamId = do
+  let key = Word32le (fromIntegral (compressedWordValue streamId))
+  attributeId <- Map.lookup key attributeMap
+  let objectMap = classAttributeMapObjectMap classAttributeMap
+  Map.lookup attributeId objectMap
+
+getAttributeMap
+  :: ClassAttributeMap
+  -> Map CompressedWord Word32le
+  -> CompressedWord
+  -> Maybe (Map Word32le Word32le)
+getAttributeMap classAttributeMap actorMap actorId = do
+  objectId <- Map.lookup actorId actorMap
+  let objectClassMap = classAttributeMapObjectClassMap classAttributeMap
+  classId <- Map.lookup objectId objectClassMap
+  let value = classAttributeMapValue classAttributeMap
+  Map.lookup classId value
diff --git a/library/Rattletrap/Type/ClassMapping.hs b/library/Rattletrap/Type/ClassMapping.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/ClassMapping.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.ClassMapping
+  ( ClassMapping(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word32le
+
+data ClassMapping = ClassMapping
+  { classMappingName :: Str
+  , classMappingStreamId :: Word32le
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''ClassMapping)
diff --git a/library/Rattletrap/Type/ClubColorsAttribute.hs b/library/Rattletrap/Type/ClubColorsAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/ClubColorsAttribute.hs
@@ -0,0 +1,17 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.ClubColorsAttribute
+  ( ClubColorsAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Word8le
+
+data ClubColorsAttribute = ClubColorsAttribute
+  { clubColorsAttributeBlueFlag :: Bool
+  , clubColorsAttributeBlueColor :: Word8le
+  , clubColorsAttributeOrangeFlag :: Bool
+  , clubColorsAttributeOrangeColor :: Word8le
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''ClubColorsAttribute)
diff --git a/library/Rattletrap/Type/Common.hs b/library/Rattletrap/Type/Common.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Common.hs
@@ -0,0 +1,55 @@
+module Rattletrap.Type.Common
+  ( Int.Int8
+  , Int.Int32
+  , Int.Int64
+  , Map.Map
+  , Text.Text
+  , Word.Word8
+  , Word.Word16
+  , Word.Word32
+  , Word.Word64
+  , deriveJson
+  ) where
+
+import qualified Data.Aeson as Json
+import qualified Data.Aeson.TH as Json
+import qualified Data.Char as Char
+import qualified Data.Int as Int
+import qualified Data.Map as Map
+import qualified Data.Maybe as Maybe
+import qualified Data.Text as Text
+import qualified Data.Word as Word
+import qualified Language.Haskell.TH as TH
+
+deriveJson :: TH.Name -> TH.Q [TH.Dec]
+deriveJson name = Json.deriveJSON (jsonOptions (TH.nameBase name)) name
+
+jsonOptions :: String -> Json.Options
+jsonOptions prefix = Json.defaultOptions
+  { Json.constructorTagModifier = toSnakeCase . partialDropPrefix prefix
+  , Json.fieldLabelModifier = toSnakeCase
+    . partialDropPrefix (lowerFirst prefix)
+  , Json.omitNothingFields = True
+  , Json.sumEncoding = Json.ObjectWithSingleField
+  , Json.unwrapUnaryRecords = True
+  }
+
+lowerFirst :: String -> String
+lowerFirst string = case string of
+  "" -> string
+  first:rest -> Char.toLower first : rest
+
+toSnakeCase :: String -> String
+toSnakeCase = Json.camelTo2 '_'
+
+partialDropPrefix :: (Eq a, Show a) => [a] -> [a] -> [a]
+partialDropPrefix prefix list = Maybe.fromMaybe
+  (error (unwords [show prefix, "is not a prefix of", show list]))
+  (dropPrefix prefix list)
+
+dropPrefix :: Eq a => [a] -> [a] -> Maybe [a]
+dropPrefix prefix list = case prefix of
+  [] -> Just list
+  ph:pt -> case list of
+    [] -> Nothing
+    lh:lt -> if ph == lh then dropPrefix pt lt else Nothing
diff --git a/library/Rattletrap/Type/CompressedWord.hs b/library/Rattletrap/Type/CompressedWord.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/CompressedWord.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.CompressedWord
+  ( CompressedWord(..)
+  ) where
+
+import Rattletrap.Type.Common
+
+data CompressedWord = CompressedWord
+  { compressedWordLimit :: Word
+  , compressedWordValue :: Word
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''CompressedWord)
diff --git a/library/Rattletrap/Type/CompressedWordVector.hs b/library/Rattletrap/Type/CompressedWordVector.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/CompressedWordVector.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.CompressedWordVector
+  ( CompressedWordVector(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.CompressedWord
+
+data CompressedWordVector = CompressedWordVector
+  { compressedWordVectorX :: CompressedWord
+  , compressedWordVectorY :: CompressedWord
+  , compressedWordVectorZ :: CompressedWord
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''CompressedWordVector)
diff --git a/library/Rattletrap/Type/Content.hs b/library/Rattletrap/Type/Content.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Content.hs
@@ -0,0 +1,52 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Content
+  ( Content(..)
+  ) where
+
+import Rattletrap.Type.Cache
+import Rattletrap.Type.ClassMapping
+import Rattletrap.Type.Common
+import Rattletrap.Type.Frame
+import Rattletrap.Type.KeyFrame
+import Rattletrap.Type.List
+import Rattletrap.Type.Mark
+import Rattletrap.Type.Message
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word32le
+
+-- | Contains low-level game data about a 'Rattletrap.Replay.Replay'.
+data Content = Content
+  { contentLevels :: List Str
+  -- ^ This typically only has one element, like @stadium_oob_audio_map@.
+  , contentKeyFrames :: List KeyFrame
+  -- ^ A list of which frames are key frames. Although they aren't necessary
+  -- for replay, key frames are frames that replicate every actor. They
+  -- typically happen once every 10 seconds.
+  , contentStreamSize :: Word32le
+  -- ^ The size of the stream in bytes. This is only really necessary because
+  -- the stream has some arbitrary amount of padding at the end.
+  , contentFrames :: [Frame]
+  -- ^ The actual game data. This is where all the interesting information is.
+  , contentMessages :: List Message
+  -- ^ Debugging messages. In newer replays, this is always empty.
+  , contentMarks :: List Mark
+  -- ^ Tick marks shown on the scrubber when watching a replay.
+  , contentPackages :: List Str
+  -- ^ A list of @.upk@ files to load, like
+  -- @..\\..\\TAGame\\CookedPCConsole\\Stadium_P.upk@.
+  , contentObjects :: List Str
+  -- ^ Objects in the stream. Used for the
+  -- 'Rattletrap.Type.ClassAttributeMap.ClassAttributeMap'.
+  , contentNames :: List Str
+  -- ^ It's not clear what these are used for. This list is usually not empty,
+  -- but appears unused otherwise.
+  , contentClassMappings :: List ClassMapping
+  -- ^ A mapping between classes and their ID in the stream. Used for the
+  -- 'Rattletrap.Type.ClassAttributeMap.ClassAttributeMap'.
+  , contentCaches :: List Cache
+  -- ^ A list of classes along with their parent classes and attributes. Used
+  -- for the 'Rattletrap.Type.ClassAttributeMap.ClassAttributeMap'.
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Content)
diff --git a/library/Rattletrap/Type/DamageStateAttribute.hs b/library/Rattletrap/Type/DamageStateAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/DamageStateAttribute.hs
@@ -0,0 +1,21 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.DamageStateAttribute
+  ( DamageStateAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Int32le
+import Rattletrap.Type.Vector
+import Rattletrap.Type.Word8le
+
+data DamageStateAttribute = DamageStateAttribute
+  { damageStateAttributeUnknown1 :: Word8le
+  , damageStateAttributeUnknown2 :: Bool
+  , damageStateAttributeUnknown3 :: Int32le
+  , damageStateAttributeUnknown4 :: Vector
+  , damageStateAttributeUnknown5 :: Bool
+  , damageStateAttributeUnknown6 :: Bool
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''DamageStateAttribute)
diff --git a/library/Rattletrap/Type/DemolishAttribute.hs b/library/Rattletrap/Type/DemolishAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/DemolishAttribute.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.DemolishAttribute
+  ( DemolishAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Vector
+import Rattletrap.Type.Word32le
+
+data DemolishAttribute = DemolishAttribute
+  { demolishAttributeAttackerFlag :: Bool
+  , demolishAttributeAttackerActorId :: Word32le
+  , demolishAttributeVictimFlag :: Bool
+  , demolishAttributeVictimActorId :: Word32le
+  , demolishAttributeAttackerVelocity :: Vector
+  , demolishAttributeVictimVelocity :: Vector
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''DemolishAttribute)
diff --git a/library/Rattletrap/Type/DestroyedReplication.hs b/library/Rattletrap/Type/DestroyedReplication.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/DestroyedReplication.hs
@@ -0,0 +1,15 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.DestroyedReplication
+  ( DestroyedReplication(..)
+  ) where
+
+import Rattletrap.Type.Common
+
+-- | Destroyed replications don't actually contain any extra information. All
+-- you need to know is the actor's ID, which is given by the
+-- 'Rattletrap.Replication.Replication'.
+data DestroyedReplication = DestroyedReplication
+  deriving (Eq, Ord, Show)
+
+$(deriveJson ''DestroyedReplication)
diff --git a/library/Rattletrap/Type/Dictionary.hs b/library/Rattletrap/Type/Dictionary.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Dictionary.hs
@@ -0,0 +1,69 @@
+module Rattletrap.Type.Dictionary
+  ( Dictionary(..)
+  , dictionaryLookup
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Str
+
+import qualified Control.Monad as Monad
+import qualified Data.Aeson as Json
+import qualified Data.Aeson.Types as Json
+import qualified Data.Map as Map
+import qualified Data.Text as Text
+
+data Dictionary a
+  = DictionaryElement Str a (Dictionary a)
+  | DictionaryEnd Str
+  deriving (Eq, Ord, Show)
+
+instance Json.FromJSON a => Json.FromJSON (Dictionary a) where
+  parseJSON =
+    Json.withObject
+      "Dictionary"
+      (\o -> do
+         keys <- get o "keys"
+         lastKey <- get o "last_key"
+         value <- get o "value"
+         Monad.foldM
+           (\d k ->
+              case Map.lookup k value of
+                Nothing -> fail (unwords ["missing key", show k])
+                Just v -> pure (DictionaryElement (Str k) v d))
+           (DictionaryEnd lastKey)
+           (reverse keys))
+
+instance Json.ToJSON a => Json.ToJSON (Dictionary a) where
+  toJSON d =
+    Json.object
+      [ pair "keys" (dictionaryKeys d)
+      , pair "last_key" (dictionaryLastKey d)
+      , pair "value" (dictionaryValue d)
+      ]
+
+dictionaryKeys :: Dictionary a -> [Str]
+dictionaryKeys = fmap fst . toList
+
+dictionaryLastKey :: Dictionary a -> Str
+dictionaryLastKey x = case x of
+  DictionaryElement _ _ y -> dictionaryLastKey y
+  DictionaryEnd y -> y
+
+dictionaryLookup :: Str -> Dictionary a -> Maybe a
+dictionaryLookup k x = case x of
+  DictionaryElement j v y -> if k == j then Just v else dictionaryLookup k y
+  DictionaryEnd _ -> Nothing
+
+dictionaryValue :: Dictionary a -> Map Text a
+dictionaryValue = Map.mapKeys strValue . Map.fromList . toList
+
+get :: Json.FromJSON a => Json.Object -> String -> Json.Parser a
+get o k = o Json..: Text.pack k
+
+pair :: Json.ToJSON a => String -> a -> (Text, Json.Value)
+pair k v = (Text.pack k, Json.toJSON v)
+
+toList :: Dictionary a -> [(Str, a)]
+toList x = case x of
+  DictionaryElement k v y -> (k, v) : toList y
+  DictionaryEnd _ -> []
diff --git a/library/Rattletrap/Type/EnumAttribute.hs b/library/Rattletrap/Type/EnumAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/EnumAttribute.hs
@@ -0,0 +1,13 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.EnumAttribute
+  ( EnumAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+
+newtype EnumAttribute = EnumAttribute
+  { enumAttributeValue :: Word16
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''EnumAttribute)
diff --git a/library/Rattletrap/Type/ExplosionAttribute.hs b/library/Rattletrap/Type/ExplosionAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/ExplosionAttribute.hs
@@ -0,0 +1,17 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.ExplosionAttribute
+  ( ExplosionAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Int32le
+import Rattletrap.Type.Vector
+
+data ExplosionAttribute = ExplosionAttribute
+  { explosionAttributeFlag :: Bool
+  , explosionAttributeActorId :: Int32le
+  , explosionAttributeLocation :: Vector
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''ExplosionAttribute)
diff --git a/library/Rattletrap/Type/ExtendedExplosionAttribute.hs b/library/Rattletrap/Type/ExtendedExplosionAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/ExtendedExplosionAttribute.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.ExtendedExplosionAttribute
+  ( ExtendedExplosionAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.ExplosionAttribute
+import Rattletrap.Type.FlaggedIntAttribute
+
+data ExtendedExplosionAttribute = ExtendedExplosionAttribute
+  { extendedExplosionAttributeExplosion :: ExplosionAttribute
+  , extendedExplosionAttributeUnknown :: FlaggedIntAttribute
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''ExtendedExplosionAttribute)
diff --git a/library/Rattletrap/Type/FlaggedIntAttribute.hs b/library/Rattletrap/Type/FlaggedIntAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/FlaggedIntAttribute.hs
@@ -0,0 +1,15 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.FlaggedIntAttribute
+  ( FlaggedIntAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Int32le
+
+data FlaggedIntAttribute = FlaggedIntAttribute
+  { flaggedIntAttributeFlag :: Bool
+  , flaggedIntAttributeInt :: Int32le
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''FlaggedIntAttribute)
diff --git a/library/Rattletrap/Type/Float32le.hs b/library/Rattletrap/Type/Float32le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Float32le.hs
@@ -0,0 +1,13 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Float32le
+  ( Float32le(..)
+  ) where
+
+import Rattletrap.Type.Common
+
+newtype Float32le = Float32le
+  { float32leValue :: Float
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Float32le)
diff --git a/library/Rattletrap/Type/FloatAttribute.hs b/library/Rattletrap/Type/FloatAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/FloatAttribute.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.FloatAttribute
+  ( FloatAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Float32le
+
+newtype FloatAttribute = FloatAttribute
+  { floatAttributeValue :: Float32le
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''FloatAttribute)
diff --git a/library/Rattletrap/Type/Frame.hs b/library/Rattletrap/Type/Frame.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Frame.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Frame
+  ( Frame(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Float32le
+import Rattletrap.Type.Replication
+
+data Frame = Frame
+  { frameTime :: Float32le
+  -- ^ Time in seconds since the beginning of the match.
+  , frameDelta :: Float32le
+  -- ^ Time in seconds since the last frame. Usually about 0.03 since there
+  -- are 30 frames per second.
+  , frameReplications :: [Replication]
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Frame)
diff --git a/library/Rattletrap/Type/GameModeAttribute.hs b/library/Rattletrap/Type/GameModeAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/GameModeAttribute.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.GameModeAttribute
+  ( GameModeAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+
+data GameModeAttribute = GameModeAttribute
+  { gameModeAttributeNumBits :: Int
+  , gameModeAttributeWord :: Word8
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''GameModeAttribute)
diff --git a/library/Rattletrap/Type/Header.hs b/library/Rattletrap/Type/Header.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Header.hs
@@ -0,0 +1,60 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Header
+  ( Header(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Dictionary
+import Rattletrap.Type.Property
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word32le
+
+-- | Contains high-level metadata about a 'Rattletrap.Replay.Replay'.
+data Header = Header
+  { headerEngineVersion :: Word32le
+  -- ^ The "major" ("engine") version number.
+  , headerLicenseeVersion :: Word32le
+  -- ^ The "minor" ("licensee") version number.
+  , headerPatchVersion :: Maybe Word32le
+  -- ^ The "patch" ("net") version number.
+  , headerLabel :: Str
+  -- ^ Always @TAGame.Replay_Soccar_TA@.
+  , headerProperties :: Dictionary Property
+  -- ^ These properties determine how a replay will look in the list of
+  -- replays in-game. One element is required for the replay to show up:
+  --
+  -- - MapName: This is a 'Rattletrap.PropertyValue.NameProperty' with a
+  --   case-insensitive map identifier, like @Stadium_P@.
+  --
+  -- There are many other properties that affect how the replay looks in the
+  -- list of replays.
+  --
+  -- - Date: A 'Rattletrap.PropertyValue.StrProperty' with the format
+  --   @YYYY-mm-dd:HH-MM@. Dates are not validated, but the month must be
+  --   between 1 and 12 to show up. The hour is shown modulo 12 with AM or PM.
+  -- - MatchType: A 'Rattletrap.PropertyValue.NameProperty'. If this is not
+  --   one of the expected values, nothing will be shown next to the replay's
+  --   map. The expected values are: @Online@, @Offline@, @Private@, and
+  --   @Season@.
+  -- - NumFrames: This 'Rattletrap.PropertyValue.IntProperty' is used to
+  --   calculate the length of the match. There are 30 frames per second,
+  --   a typical 5-minute match has about 9,000 frames.
+  -- - PrimaryPlayerTeam: This is an 'Rattletrap.PropertyValue.IntProperty'.
+  --   It is either 0 (blue) or 1 (orange). Any other value is ignored. If
+  --   this would be 0, you don't have to set it at all.
+  -- - ReplayName: An optional 'Rattletrap.PropertyValue.StrProperty' with a
+  --   user-supplied name for the replay.
+  -- - Team0Score: The blue team's score as an
+  --   'Rattletrap.PropertyValue.IntProperty'. Can be omitted if the score is
+  --   0.
+  -- - Team1Score: The orange team's score as an
+  --   'Rattletrap.PropertyValue.IntProperty'. Can also be omitted if the
+  --   score is 0.
+  -- - TeamSize: An 'Rattletrap.PropertyValue.IntProperty' with the number of
+  --   players per team. This value is not validated, so you can put absurd
+  --   values like 99. To get an "unfair" team size like 1v4, you must set the
+  --   bUnfairBots 'Rattletrap.PropertyValue.BoolProperty' to @True@.
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Header)
diff --git a/library/Rattletrap/Type/Initialization.hs b/library/Rattletrap/Type/Initialization.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Initialization.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Initialization
+  ( Initialization(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Int8Vector
+import Rattletrap.Type.Vector
+
+data Initialization = Initialization
+  { initializationLocation :: Maybe Vector
+  -- ^ Not every class has an initial location. See
+  -- 'Rattletrap.Data.rawClassesWithLocation'.
+  , initializationRotation :: Maybe Int8Vector
+  -- ^ Only classes with location can have rotation, but not every one does.
+  -- See 'Rattletrap.Data.rawClassesWithRotation'.
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Initialization)
diff --git a/library/Rattletrap/Type/Int32le.hs b/library/Rattletrap/Type/Int32le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Int32le.hs
@@ -0,0 +1,13 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Int32le
+  ( Int32le(..)
+  ) where
+
+import Rattletrap.Type.Common
+
+newtype Int32le = Int32le
+  { int32leValue :: Int32
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Int32le)
diff --git a/library/Rattletrap/Type/Int8Vector.hs b/library/Rattletrap/Type/Int8Vector.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Int8Vector.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Int8Vector
+  ( Int8Vector(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Int8le
+
+data Int8Vector = Int8Vector
+  { int8VectorX :: Maybe Int8le
+  , int8VectorY :: Maybe Int8le
+  , int8VectorZ :: Maybe Int8le
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Int8Vector)
diff --git a/library/Rattletrap/Type/Int8le.hs b/library/Rattletrap/Type/Int8le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Int8le.hs
@@ -0,0 +1,13 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Int8le
+  ( Int8le(..)
+  ) where
+
+import Rattletrap.Type.Common
+
+newtype Int8le = Int8le
+  { int8leValue :: Int8
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Int8le)
diff --git a/library/Rattletrap/Type/IntAttribute.hs b/library/Rattletrap/Type/IntAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/IntAttribute.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.IntAttribute
+  ( IntAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Int32le
+
+newtype IntAttribute = IntAttribute
+  { intAttributeValue :: Int32le
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''IntAttribute)
diff --git a/library/Rattletrap/Type/KeyFrame.hs b/library/Rattletrap/Type/KeyFrame.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/KeyFrame.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.KeyFrame
+  ( KeyFrame(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Float32le
+import Rattletrap.Type.Word32le
+
+data KeyFrame = KeyFrame
+  { keyFrameTime :: Float32le
+  -- ^ When this key frame occurs, in seconds.
+  , keyFrameFrame :: Word32le
+  -- ^ The frame number of this key frame, starting from 0.
+  , keyFramePosition :: Word32le
+  -- ^ The bit position of this key frame in the stream.
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''KeyFrame)
diff --git a/library/Rattletrap/Type/List.hs b/library/Rattletrap/Type/List.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/List.hs
@@ -0,0 +1,13 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.List
+  ( List(..)
+  ) where
+
+import Rattletrap.Type.Common
+
+newtype List a = List
+  { listValue :: [a]
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''List)
diff --git a/library/Rattletrap/Type/LoadoutAttribute.hs b/library/Rattletrap/Type/LoadoutAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/LoadoutAttribute.hs
@@ -0,0 +1,28 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.LoadoutAttribute
+  ( LoadoutAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Word32le
+import Rattletrap.Type.Word8le
+
+data LoadoutAttribute = LoadoutAttribute
+  { loadoutAttributeVersion :: Word8le
+  , loadoutAttributeBody :: Word32le
+  , loadoutAttributeDecal :: Word32le
+  , loadoutAttributeWheels :: Word32le
+  , loadoutAttributeRocketTrail :: Word32le
+  -- ^ Now known as "rocket boost".
+  , loadoutAttributeAntenna :: Word32le
+  , loadoutAttributeTopper :: Word32le
+  , loadoutAttributeUnknown1 :: Word32le
+  , loadoutAttributeUnknown2 :: Maybe Word32le
+  , loadoutAttributeEngineAudio :: Maybe Word32le
+  , loadoutAttributeTrail :: Maybe Word32le
+  , loadoutAttributeGoalExplosion :: Maybe Word32le
+  , loadoutAttributeBanner :: Maybe Word32le
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''LoadoutAttribute)
diff --git a/library/Rattletrap/Type/LoadoutOnlineAttribute.hs b/library/Rattletrap/Type/LoadoutOnlineAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/LoadoutOnlineAttribute.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.LoadoutOnlineAttribute
+  ( LoadoutOnlineAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.ProductAttribute
+
+newtype LoadoutOnlineAttribute = LoadoutOnlineAttribute
+  { loadoutAttributeValue :: [[ProductAttribute]]
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''LoadoutOnlineAttribute)
diff --git a/library/Rattletrap/Type/LoadoutsAttribute.hs b/library/Rattletrap/Type/LoadoutsAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/LoadoutsAttribute.hs
@@ -0,0 +1,15 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.LoadoutsAttribute
+  ( LoadoutsAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.LoadoutAttribute
+
+data LoadoutsAttribute = LoadoutsAttribute
+  { loadoutsAttributeBlue :: LoadoutAttribute
+  , loadoutsAttributeOrange :: LoadoutAttribute
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''LoadoutsAttribute)
diff --git a/library/Rattletrap/Type/LoadoutsOnlineAttribute.hs b/library/Rattletrap/Type/LoadoutsOnlineAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/LoadoutsOnlineAttribute.hs
@@ -0,0 +1,17 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.LoadoutsOnlineAttribute
+  ( LoadoutsOnlineAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.LoadoutOnlineAttribute
+
+data LoadoutsOnlineAttribute = LoadoutsOnlineAttribute
+  { loadoutsOnlineAttributeBlue :: LoadoutOnlineAttribute
+  , loadoutsOnlineAttributeOrange :: LoadoutOnlineAttribute
+  , loadoutsOnlineAttributeUnknown1 :: Bool
+  , loadoutsOnlineAttributeUnknown2 :: Bool
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''LoadoutsOnlineAttribute)
diff --git a/library/Rattletrap/Type/LocationAttribute.hs b/library/Rattletrap/Type/LocationAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/LocationAttribute.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.LocationAttribute
+  ( LocationAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Vector
+
+newtype LocationAttribute = LocationAttribute
+  { locationAttributeValue :: Vector
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''LocationAttribute)
diff --git a/library/Rattletrap/Type/Mark.hs b/library/Rattletrap/Type/Mark.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Mark.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Mark
+  ( Mark(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word32le
+
+data Mark = Mark
+  { markValue :: Str
+  -- ^ Which type of mark this is, like @Team0Goal@.
+  , markFrame :: Word32le
+  -- ^ Which frame this mark belongs to, starting from 0.
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Mark)
diff --git a/library/Rattletrap/Type/Message.hs b/library/Rattletrap/Type/Message.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Message.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Message
+  ( Message(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word32le
+
+data Message = Message
+  { messageFrame :: Word32le
+  -- ^ Which frame this message belongs to, starting from 0.
+  , messageName :: Str
+  -- ^ The primary player's name.
+  , messageValue :: Str
+  -- ^ The content of the message.
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Message)
diff --git a/library/Rattletrap/Type/MusicStingerAttribute.hs b/library/Rattletrap/Type/MusicStingerAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/MusicStingerAttribute.hs
@@ -0,0 +1,17 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.MusicStingerAttribute
+  ( MusicStingerAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Word32le
+import Rattletrap.Type.Word8le
+
+data MusicStingerAttribute = MusicStingerAttribute
+  { musicStingerAttributeFlag :: Bool
+  , musicStingerAttributeCue :: Word32le
+  , musicStingerAttributeTrigger :: Word8le
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''MusicStingerAttribute)
diff --git a/library/Rattletrap/Type/PartyLeaderAttribute.hs b/library/Rattletrap/Type/PartyLeaderAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/PartyLeaderAttribute.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.PartyLeaderAttribute
+  ( PartyLeaderAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.RemoteId
+import Rattletrap.Type.Word8le
+
+data PartyLeaderAttribute = PartyLeaderAttribute
+  { partyLeaderAttributeSystemId :: Word8le
+  , partyLeaderAttributeId :: Maybe (RemoteId, Word8le)
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''PartyLeaderAttribute)
diff --git a/library/Rattletrap/Type/PickupAttribute.hs b/library/Rattletrap/Type/PickupAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/PickupAttribute.hs
@@ -0,0 +1,15 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.PickupAttribute
+  ( PickupAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Word32le
+
+data PickupAttribute = PickupAttribute
+  { pickupAttributeInstigatorId :: Maybe Word32le
+  , pickupAttributePickedUp :: Bool
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''PickupAttribute)
diff --git a/library/Rattletrap/Type/PlayerHistoryKeyAttribute.hs b/library/Rattletrap/Type/PlayerHistoryKeyAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/PlayerHistoryKeyAttribute.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.PlayerHistoryKeyAttribute
+  ( PlayerHistoryKeyAttribute(..)
+  ) where
+
+import Rattletrap.Type.Bitstream
+import Rattletrap.Type.Common
+
+newtype PlayerHistoryKeyAttribute = PlayerHistoryKeyAttribute
+  { playerHistoryKeyAttributeUnknown :: Bitstream
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''PlayerHistoryKeyAttribute)
diff --git a/library/Rattletrap/Type/PrivateMatchSettingsAttribute.hs b/library/Rattletrap/Type/PrivateMatchSettingsAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/PrivateMatchSettingsAttribute.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.PrivateMatchSettingsAttribute
+  ( PrivateMatchSettingsAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word32le
+
+data PrivateMatchSettingsAttribute = PrivateMatchSettingsAttribute
+  { privateMatchSettingsAttributeMutators :: Str
+  , privateMatchSettingsAttributeJoinableBy :: Word32le
+  , privateMatchSettingsAttributeMaxPlayers :: Word32le
+  , privateMatchSettingsAttributeGameName :: Str
+  , privateMatchSettingsAttributePassword :: Str
+  , privateMatchSettingsAttributeFlag :: Bool
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''PrivateMatchSettingsAttribute)
diff --git a/library/Rattletrap/Type/ProductAttribute.hs b/library/Rattletrap/Type/ProductAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/ProductAttribute.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.ProductAttribute
+  ( ProductAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.CompressedWord
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word32le
+
+data ProductAttribute = ProductAttribute
+  { productAttributeUnknown :: Bool
+  , productAttributeObjectId :: Word32le
+  , productAttributeObjectName :: Maybe Str
+  -- ^ read-only
+  , productAttributeValue :: Maybe (Either CompressedWord Word32)
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''ProductAttribute)
diff --git a/library/Rattletrap/Type/Property.hs b/library/Rattletrap/Type/Property.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Property.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Property
+  ( Property(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.PropertyValue
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word64le
+
+data Property = Property
+  { propertyKind :: Str
+  , propertySize :: Word64le
+  -- ^ Not used.
+  , propertyValue :: PropertyValue Property
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Property)
diff --git a/library/Rattletrap/Type/PropertyValue.hs b/library/Rattletrap/Type/PropertyValue.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/PropertyValue.hs
@@ -0,0 +1,31 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.PropertyValue
+  ( PropertyValue(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Dictionary
+import Rattletrap.Type.Float32le
+import Rattletrap.Type.Int32le
+import Rattletrap.Type.List
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word64le
+import Rattletrap.Type.Word8le
+
+data PropertyValue a
+  = PropertyValueArray (List (Dictionary a))
+  -- ^ Yes, a list of dictionaries. No, it doesn't make sense. These usually
+  -- only have one element.
+  | PropertyValueBool Word8le
+  | PropertyValueByte Str (Maybe Str)
+  -- ^ This is a strange name for essentially a key-value pair.
+  | PropertyValueFloat Float32le
+  | PropertyValueInt Int32le
+  | PropertyValueName Str
+  -- ^ It's unclear how exactly this is different than a 'StrProperty'.
+  | PropertyValueQWord Word64le
+  | PropertyValueStr Str
+  deriving (Eq, Ord, Show)
+
+$(deriveJson ''PropertyValue)
diff --git a/library/Rattletrap/Type/QWordAttribute.hs b/library/Rattletrap/Type/QWordAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/QWordAttribute.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.QWordAttribute
+  ( QWordAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Word64le
+
+newtype QWordAttribute = QWordAttribute
+  { qWordAttributeValue :: Word64le
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''QWordAttribute)
diff --git a/library/Rattletrap/Type/RemoteId.hs b/library/Rattletrap/Type/RemoteId.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/RemoteId.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.RemoteId
+  ( RemoteId(..)
+  ) where
+
+import Rattletrap.Type.Bitstream
+import Rattletrap.Type.Common
+import Rattletrap.Type.Word64le
+
+data RemoteId
+  = RemoteIdPlayStation Text [Word8]
+  | RemoteIdSplitscreen Word32
+  | RemoteIdSteam Word64le
+  | RemoteIdSwitch Bitstream
+  | RemoteIdXbox Word64le
+  deriving (Eq, Ord, Show)
+
+$(deriveJson ''RemoteId)
diff --git a/library/Rattletrap/Type/Replay.hs b/library/Rattletrap/Type/Replay.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Replay.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Replay
+  ( Replay(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Content
+import Rattletrap.Type.Header
+import Rattletrap.Type.Section
+
+-- | A Rocket League replay.
+data Replay = Replay
+  { replayHeader :: Section Header
+  -- ^ This has most of the high-level metadata.
+  , replayContent :: Section Content
+  -- ^ This has most of the low-level game data.
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Replay)
diff --git a/library/Rattletrap/Type/Replication.hs b/library/Rattletrap/Type/Replication.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Replication.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Replication
+  ( Replication(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.CompressedWord
+import Rattletrap.Type.ReplicationValue
+
+data Replication = Replication
+  { replicationActorId :: CompressedWord
+  , replicationValue :: ReplicationValue
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Replication)
diff --git a/library/Rattletrap/Type/ReplicationValue.hs b/library/Rattletrap/Type/ReplicationValue.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/ReplicationValue.hs
@@ -0,0 +1,21 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.ReplicationValue
+  ( ReplicationValue(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.DestroyedReplication
+import Rattletrap.Type.SpawnedReplication
+import Rattletrap.Type.UpdatedReplication
+
+data ReplicationValue
+  = ReplicationValueSpawned SpawnedReplication
+  -- ^ Creates a new actor.
+  | ReplicationValueUpdated UpdatedReplication
+  -- ^ Updates an existing actor.
+  | ReplicationValueDestroyed DestroyedReplication
+  -- ^ Destroys an existing actor.
+  deriving (Eq, Ord, Show)
+
+$(deriveJson ''ReplicationValue)
diff --git a/library/Rattletrap/Type/ReservationAttribute.hs b/library/Rattletrap/Type/ReservationAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/ReservationAttribute.hs
@@ -0,0 +1,21 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.ReservationAttribute
+  ( ReservationAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.CompressedWord
+import Rattletrap.Type.Str
+import Rattletrap.Type.UniqueIdAttribute
+
+data ReservationAttribute = ReservationAttribute
+  { reservationAttributeNumber :: CompressedWord
+  , reservationAttributeUniqueId :: UniqueIdAttribute
+  , reservationAttributeName :: Maybe Str
+  , reservationAttributeUnknown1 :: Bool
+  , reservationAttributeUnknown2 :: Bool
+  , reservationAttributeUnknown3 :: Maybe Word8
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''ReservationAttribute)
diff --git a/library/Rattletrap/Type/RigidBodyStateAttribute.hs b/library/Rattletrap/Type/RigidBodyStateAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/RigidBodyStateAttribute.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.RigidBodyStateAttribute
+  ( RigidBodyStateAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.CompressedWordVector
+import Rattletrap.Type.Vector
+
+data RigidBodyStateAttribute = RigidBodyStateAttribute
+  { rigidBodyStateAttributeSleeping :: Bool
+  , rigidBodyStateAttributeLocation :: Vector
+  , rigidBodyStateAttributeRotation :: CompressedWordVector
+  , rigidBodyStateAttributeLinearVelocity :: Maybe Vector
+  , rigidBodyStateAttributeAngularVelocity :: Maybe Vector
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''RigidBodyStateAttribute)
diff --git a/library/Rattletrap/Type/Section.hs b/library/Rattletrap/Type/Section.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Section.hs
@@ -0,0 +1,23 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Section
+  ( Section(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Word32le
+
+-- | A section is a large piece of a 'Rattletrap.Replay.Replay'. It has a
+-- 32-bit size (in bytes), a 32-bit CRC (see "Rattletrap.Utility.Crc"), and then a
+-- bunch of data (the body). This interface is provided so that you don't have
+-- to think about the size and CRC.
+data Section a = Section
+  { sectionSize :: Word32le
+  -- ^ read only
+  , sectionCrc :: Word32le
+  -- ^ read only
+  , sectionBody :: a
+  -- ^ The actual content in the section.
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Section)
diff --git a/library/Rattletrap/Type/SpawnedReplication.hs b/library/Rattletrap/Type/SpawnedReplication.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/SpawnedReplication.hs
@@ -0,0 +1,29 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.SpawnedReplication
+  ( SpawnedReplication(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Initialization
+import Rattletrap.Type.Str
+import Rattletrap.Type.Word32le
+
+data SpawnedReplication = SpawnedReplication
+  { spawnedReplicationFlag :: Bool
+  -- ^ Unclear what this is.
+  , spawnedReplicationNameIndex :: Maybe Word32le
+  , spawnedReplicationName :: Maybe Str
+  -- ^ Read-only! Changing a replication's name requires editing the
+  -- 'spawnedReplicationNameIndex' and maybe the class attribute map.
+  , spawnedReplicationObjectId :: Word32le
+  , spawnedReplicationObjectName :: Str
+  -- ^ Read-only! Changing a replication's object requires editing the class
+  -- attribute map.
+  , spawnedReplicationClassName :: Str
+  -- ^ Read-only! Changing a replication's class requires editing the class
+  -- attribute map.
+  , spawnedReplicationInitialization :: Initialization
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''SpawnedReplication)
diff --git a/library/Rattletrap/Type/Str.hs b/library/Rattletrap/Type/Str.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Str.hs
@@ -0,0 +1,23 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Str
+  ( Str(..)
+  , toStr
+  , fromStr
+  ) where
+
+import Rattletrap.Type.Common
+
+import qualified Data.Text as Text
+
+newtype Str = Str
+  { strValue :: Text
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Str)
+
+toStr :: String -> Str
+toStr string = Str (Text.pack string)
+
+fromStr :: Str -> String
+fromStr text = Text.unpack (strValue text)
diff --git a/library/Rattletrap/Type/StringAttribute.hs b/library/Rattletrap/Type/StringAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/StringAttribute.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.StringAttribute
+  ( StringAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Str
+
+newtype StringAttribute = StringAttribute
+  { stringAttributeValue :: Str
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''StringAttribute)
diff --git a/library/Rattletrap/Type/TeamPaintAttribute.hs b/library/Rattletrap/Type/TeamPaintAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/TeamPaintAttribute.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.TeamPaintAttribute
+  ( TeamPaintAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Word32le
+import Rattletrap.Type.Word8le
+
+data TeamPaintAttribute = TeamPaintAttribute
+  { teamPaintAttributeTeam :: Word8le
+  , teamPaintAttributePrimaryColor :: Word8le
+  , teamPaintAttributeAccentColor :: Word8le
+  , teamPaintAttributePrimaryFinish :: Word32le
+  , teamPaintAttributeAccentFinish :: Word32le
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''TeamPaintAttribute)
diff --git a/library/Rattletrap/Type/UniqueIdAttribute.hs b/library/Rattletrap/Type/UniqueIdAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/UniqueIdAttribute.hs
@@ -0,0 +1,17 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.UniqueIdAttribute
+  ( UniqueIdAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.RemoteId
+import Rattletrap.Type.Word8le
+
+data UniqueIdAttribute = UniqueIdAttribute
+  { uniqueIdAttributeSystemId :: Word8le
+  , uniqueIdAttributeRemoteId :: RemoteId
+  , uniqueIdAttributeLocalId :: Word8le
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''UniqueIdAttribute)
diff --git a/library/Rattletrap/Type/UpdatedReplication.hs b/library/Rattletrap/Type/UpdatedReplication.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/UpdatedReplication.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.UpdatedReplication
+  ( UpdatedReplication(..)
+  ) where
+
+import Rattletrap.Type.Attribute
+import Rattletrap.Type.Common
+
+newtype UpdatedReplication = UpdatedReplication
+  { updatedReplicationAttributes :: [Attribute]
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''UpdatedReplication)
diff --git a/library/Rattletrap/Type/Vector.hs b/library/Rattletrap/Type/Vector.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Vector.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Vector
+  ( Vector(..)
+  ) where
+
+import Rattletrap.Type.Common
+
+data Vector = Vector
+  { vectorBias :: Word
+  , vectorX :: Int
+  , vectorY :: Int
+  , vectorZ :: Int
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Vector)
diff --git a/library/Rattletrap/Type/WeldedInfoAttribute.hs b/library/Rattletrap/Type/WeldedInfoAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/WeldedInfoAttribute.hs
@@ -0,0 +1,21 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.WeldedInfoAttribute
+  ( WeldedInfoAttribute(..)
+  ) where
+
+import Rattletrap.Type.Common
+import Rattletrap.Type.Float32le
+import Rattletrap.Type.Int32le
+import Rattletrap.Type.Int8Vector
+import Rattletrap.Type.Vector
+
+data WeldedInfoAttribute = WeldedInfoAttribute
+  { weldedInfoAttributeActive :: Bool
+  , weldedInfoAttributeActorId :: Int32le
+  , weldedInfoAttributeOffset :: Vector
+  , weldedInfoAttributeMass :: Float32le
+  , weldedInfoAttributeRotation :: Int8Vector
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''WeldedInfoAttribute)
diff --git a/library/Rattletrap/Type/Word32le.hs b/library/Rattletrap/Type/Word32le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Word32le.hs
@@ -0,0 +1,13 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Word32le
+  ( Word32le(..)
+  ) where
+
+import Rattletrap.Type.Common
+
+newtype Word32le = Word32le
+  { word32leValue :: Word32
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Word32le)
diff --git a/library/Rattletrap/Type/Word64le.hs b/library/Rattletrap/Type/Word64le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Word64le.hs
@@ -0,0 +1,13 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Word64le
+  ( Word64le(..)
+  ) where
+
+import Rattletrap.Type.Common
+
+newtype Word64le = Word64le
+  { word64leValue :: Word64
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Word64le)
diff --git a/library/Rattletrap/Type/Word8le.hs b/library/Rattletrap/Type/Word8le.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Type/Word8le.hs
@@ -0,0 +1,13 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Rattletrap.Type.Word8le
+  ( Word8le(..)
+  ) where
+
+import Rattletrap.Type.Common
+
+newtype Word8le = Word8le
+  { word8leValue :: Word8
+  } deriving (Eq, Ord, Show)
+
+$(deriveJson ''Word8le)
diff --git a/library/Rattletrap/Utility.hs b/library/Rattletrap/Utility.hs
deleted file mode 100644
--- a/library/Rattletrap/Utility.hs
+++ /dev/null
@@ -1,30 +0,0 @@
-module Rattletrap.Utility where
-
-import qualified Data.Bits as Bits
-import qualified Data.ByteString.Lazy as ByteString
-import qualified Data.ByteString.Lazy.Char8 as ByteString8
-import qualified Data.Text as Text
-import qualified Data.Word as Word
-
-encodeLatin1 :: Text.Text -> ByteString.ByteString
-encodeLatin1 text = ByteString8.pack (Text.unpack text)
-
-padBytes :: Integral a => a -> ByteString.ByteString -> ByteString.ByteString
-padBytes size bytes = ByteString.concat
-  [ bytes
-  , ByteString.replicate (fromIntegral size - ByteString.length bytes) 0x00
-  ]
-
-reverseByte :: Word.Word8 -> Word.Word8
-reverseByte byte =
-  Bits.shiftR (byte Bits..&. Bits.bit 7) 7
-    + Bits.shiftR (byte Bits..&. Bits.bit 6) 5
-    + Bits.shiftR (byte Bits..&. Bits.bit 5) 3
-    + Bits.shiftR (byte Bits..&. Bits.bit 4) 1
-    + Bits.shiftL (byte Bits..&. Bits.bit 3) 1
-    + Bits.shiftL (byte Bits..&. Bits.bit 2) 3
-    + Bits.shiftL (byte Bits..&. Bits.bit 1) 5
-    + Bits.shiftL (byte Bits..&. Bits.bit 0) 7
-
-reverseBytes :: ByteString.ByteString -> ByteString.ByteString
-reverseBytes = ByteString.map reverseByte
diff --git a/library/Rattletrap/Utility/Bytes.hs b/library/Rattletrap/Utility/Bytes.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Utility/Bytes.hs
@@ -0,0 +1,30 @@
+module Rattletrap.Utility.Bytes where
+
+import qualified Data.Bits as Bits
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.ByteString.Lazy.Char8 as LazyBytes8
+import qualified Data.Text as Text
+import qualified Data.Word as Word
+
+encodeLatin1 :: Text.Text -> LazyBytes.ByteString
+encodeLatin1 text = LazyBytes8.pack (Text.unpack text)
+
+padBytes :: Integral a => a -> LazyBytes.ByteString -> LazyBytes.ByteString
+padBytes size bytes = LazyBytes.concat
+  [ bytes
+  , LazyBytes.replicate (fromIntegral size - LazyBytes.length bytes) 0x00
+  ]
+
+reverseByte :: Word.Word8 -> Word.Word8
+reverseByte byte =
+  Bits.shiftR (byte Bits..&. Bits.bit 7) 7
+    + Bits.shiftR (byte Bits..&. Bits.bit 6) 5
+    + Bits.shiftR (byte Bits..&. Bits.bit 5) 3
+    + Bits.shiftR (byte Bits..&. Bits.bit 4) 1
+    + Bits.shiftL (byte Bits..&. Bits.bit 3) 1
+    + Bits.shiftL (byte Bits..&. Bits.bit 2) 3
+    + Bits.shiftL (byte Bits..&. Bits.bit 1) 5
+    + Bits.shiftL (byte Bits..&. Bits.bit 0) 7
+
+reverseBytes :: LazyBytes.ByteString -> LazyBytes.ByteString
+reverseBytes = LazyBytes.map reverseByte
diff --git a/library/Rattletrap/Utility/Crc.hs b/library/Rattletrap/Utility/Crc.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Utility/Crc.hs
@@ -0,0 +1,46 @@
+module Rattletrap.Utility.Crc
+  ( getCrc32
+  ) where
+
+import Rattletrap.Data
+
+import qualified Data.Bits as Bits
+import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.IntMap as IntMap
+import qualified Data.Word as Word
+
+-- | Computes the CRC32 of some bytes. This is done to ensure that the bytes
+-- are valid before trying to parse them.
+--
+-- @
+-- getCrc32 ('Data.ByteString.Lazy.pack' [0x00])
+-- @
+--
+-- This CRC uses an initial value of @0xefcbf201@ and a polynomial of
+-- @0x04c11db7@.
+getCrc32 :: LazyBytes.ByteString -> Word.Word32
+getCrc32 bytes = do
+  let
+    update = crc32Update crc32Table
+    initial = Bits.complement crc32Initial
+    crc = LazyBytes.foldl update initial bytes
+  Bits.complement crc
+
+crc32Update
+  :: IntMap.IntMap Word.Word32 -> Word.Word32 -> Word.Word8 -> Word.Word32
+crc32Update table crc byte = do
+  let
+    toWord8 :: (Integral a) => a -> Word.Word8
+    toWord8 = fromIntegral
+    toInt :: (Integral a) => a -> Int
+    toInt = fromIntegral
+    index = toInt (Bits.xor byte (toWord8 (Bits.shiftR crc 24)))
+    left = table IntMap.! index
+    right = Bits.shiftL crc 8
+  Bits.xor left right
+
+crc32Initial :: Word.Word32
+crc32Initial = 0xefcbf201
+
+crc32Table :: IntMap.IntMap Word.Word32
+crc32Table = IntMap.fromDistinctAscList (zip [0 ..] rawCrc32Table)
diff --git a/library/Rattletrap/Utility/Helper.hs b/library/Rattletrap/Utility/Helper.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Utility/Helper.hs
@@ -0,0 +1,38 @@
+-- | This module provides helper functions for converting replays to and from
+-- both their binary format and JSON.
+module Rattletrap.Utility.Helper
+  ( decodeReplayFile
+  , encodeReplayJson
+  , decodeReplayJson
+  , encodeReplayFile
+  ) where
+
+import Rattletrap.Decode.Common
+import Rattletrap.Decode.Replay
+import Rattletrap.Encode.Replay
+import Rattletrap.Type.Replay
+
+import qualified Data.Aeson as Json
+import qualified Data.Aeson.Encode.Pretty as Json
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as LazyBytes
+
+-- | Parses a raw replay.
+decodeReplayFile :: LazyBytes.ByteString -> Either String Replay
+decodeReplayFile = runDecode decodeReplay
+
+-- | Encodes a replay as JSON.
+encodeReplayJson :: Replay -> LazyBytes.ByteString
+encodeReplayJson = Json.encodePretty' Json.defConfig
+  { Json.confCompare = compare
+  , Json.confIndent = Json.Spaces 2
+  , Json.confTrailingNewline = True
+  }
+
+-- | Parses a JSON replay.
+decodeReplayJson :: LazyBytes.ByteString -> Either String Replay
+decodeReplayJson = Json.eitherDecode
+
+-- | Encodes a raw replay.
+encodeReplayFile :: Replay -> LazyBytes.ByteString
+encodeReplayFile replay = Binary.runPut (putReplay replay)
diff --git a/library/Rattletrap/Version.hs b/library/Rattletrap/Version.hs
deleted file mode 100644
--- a/library/Rattletrap/Version.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Rattletrap.Version where
-
-import qualified Data.Version as Version
-import qualified Paths_rattletrap as This
-
-version :: Version.Version
-version = This.version
diff --git a/package.yaml b/package.yaml
--- a/package.yaml
+++ b/package.yaml
@@ -1,11 +1,10 @@
 name: rattletrap
-version: 3.1.2
+version: 4.0.0
 
 category: Game
 description: Rattletrap parses and generates Rocket League replays.
 extra-source-files:
   - CHANGELOG.markdown
-  - replays/*.replay
   - package.yaml
   - README.markdown
   - stack.yaml
@@ -16,48 +15,45 @@
 synopsis: Parse and generate Rocket League replays.
 
 dependencies:
-  - aeson >= 1.2.3 && < 1.3
-  - base >= 4.10.1 && < 4.11
-  - bimap >= 0.3.3 && < 0.4
-  - binary >= 0.8.5 && < 0.9
-  - binary-bits >= 0.5 && < 0.6
-  - bytestring >= 0.10.8 && < 0.11
-  - containers >= 0.5.10 && < 0.6
-  - data-binary-ieee754 >= 0.4.4 && < 0.5
-  - http-client >= 0.5.7 && < 0.6
-  - http-client-tls >= 0.3.5 && < 0.4
-  - template-haskell >= 2.12.0 && < 2.13
-  - text >= 1.2.2 && < 1.3
-  - vector >= 0.12.0 && < 0.13
-ghc-options: -Wall
+  aeson-pretty: == 0.8.5.*
+  aeson: == 1.2.3.*
+  base: == 4.10.1.*
+  binary-bits: == 0.5.*
+  binary: == 0.8.5.*
+  bytestring: == 0.10.8.*
+  containers: == 0.5.10.*
+  filepath: == 1.4.1.*
+  http-client-tls: == 0.3.5.*
+  http-client: == 0.5.7.*
+  template-haskell: == 2.12.*
+  text: == 1.2.2.*
+  transformers: == 0.5.2.*
+ghc-options:
+  - -Weverything
+  - -Wno-implicit-prelude
+  - -Wno-missing-import-lists
+  - -Wno-safe
+  - -Wno-unsafe
 
 library:
-  default-extensions:
-    - Strict
-  other-modules: Paths_rattletrap
   source-dirs: library
 
-executables:
-  rattletrap:
-    dependencies:
-      - rattletrap
-    ghc-options:
-      - -rtsopts
-      - -threaded
-      - -with-rtsopts=-N
-    main: Main.hs
-    source-dirs: executables
+executable:
+  dependencies: rattletrap
+  ghc-options:
+    - -rtsopts
+    - -threaded
+  main: Main.hs
+  source-dirs: executables
 
 tests:
   test:
     dependencies:
-      - filepath >= 1.4.1 && < 1.5
-      - hspec >= 2.4.4 && < 2.5
-      - rattletrap
-      - temporary >= 1.2.1 && < 1.3
+      HUnit: == 1.6.*
+      rattletrap:
+      temporary: == 1.2.1.*
     ghc-options:
       - -rtsopts
       - -threaded
-      - -with-rtsopts=-N
     main: Main.hs
     source-dirs: tests
diff --git a/rattletrap.cabal b/rattletrap.cabal
--- a/rattletrap.cabal
+++ b/rattletrap.cabal
@@ -2,10 +2,10 @@
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: ffc648678c84d8942a93a5168812a9a02db2b7471f0f42a0576f8409235cc6cd
+-- hash: db273356d10179b50de43a97e4f71076671b4d2a32bd6d23c8ee9b1cd4e20af4
 
 name:           rattletrap
-version:        3.1.2
+version:        4.0.0
 synopsis:       Parse and generate Rocket League replays.
 description:    Rattletrap parses and generates Rocket League replays.
 category:       Game
@@ -21,68 +21,6 @@
     CHANGELOG.markdown
     package.yaml
     README.markdown
-    replays/00080014003600090000036E0F65CCEB.replay
-    replays/07E925B1423653D44CB8B4B2524792C1.replay
-    replays/0AD21FEE4A512906BEB6C98136AAF49A.replay
-    replays/1205D96C4D819800927791820096CD49.replay
-    replays/160CA83E41083BFD8E6315B4BFCA0561.replay
-    replays/16D580EF479483E015207C901776F9FB.replay
-    replays/18D6738D415B70B5BE4C299588D3C141.replay
-    replays/1A126AC24CAA0DB0E98835BD960B8AF8.replay
-    replays/1AE415514DFC65DCBF8B8391AD35488D.replay
-    replays/1BC2D01444ACE577D01E988EADD4DFD0.replay
-    replays/1D1DE97D4941C86E43FE0093563DB621.replay
-    replays/1EF90FCC4F719F606A5327B3CDD782A4.replay
-    replays/1F3798E540B0C37A946561ABBB3037F9.replay
-    replays/211466D04B983F5A33CC2FA1D5928672.replay
-    replays/22660E3649FC7971E5653692473D4318.replay
-    replays/22BACD794ABE7B92E50E9CBDBD9C59CE.replay
-    replays/27B6A7B64553F0F685874584F96BAB1B.replay
-    replays/29F582C34A65EB34D358A784CBE3C189.replay
-    replays/2CFE577044E651D3FA9DBF83ECA8BCC3.replay
-    replays/338173964F9F71EBDD31058A1936CBB4.replay
-    replays/372DBFCA4BDB340E4357B6BD43032802.replay
-    replays/387F059C47C09E253C875CA990EFD9F2.replay
-    replays/3EA147DD485B8DD24810689A7A989E44.replay
-    replays/4126861E477F4A03DE2A4080374D7908.replay
-    replays/42F0D8DA4FC89AE7B80FCAB7F637A8EA.replay
-    replays/504ED825482186E771FAA9B642CE5CE4.replay
-    replays/520E1BFF468CF6C3C48D1EA85D9C7909.replay
-    replays/52AA67F94090C19D33C5009E54D31FE4.replay
-    replays/540DA764423C8FB24EB9D486D982F16F.replay
-    replays/551CA4D44FF2B86015DE44A6B5790D4C.replay
-    replays/6320E51C49066A7C210A2993C2201D5F.replay
-    replays/6688EEE34BFEB3EC3A9E3283098CC712.replay
-    replays/6B0D1614417085A7AAD82EAA30D8DABE.replay
-    replays/6D1B06D844A5BB91B81FD4B5B28F08BA.replay
-    replays/6F7CFCD24638F8A6567AB3A8B9958A90.replay
-    replays/7109EB9846D303E54B7ACBA792036213.replay
-    replays/7BF6073F4614CE0A438994B9A260DA6A.replay
-    replays/89CBA30E46FA5385BDD35DA4285D4D2E.replay
-    replays/8AE551FF406D7B82ED853B8C7BFF8CDA.replay
-    replays/9704208245D7DD851F2FB2BC7DFD9AC3.replay
-    replays/98E58A904D713F2DE202358E8573265D.replay
-    replays/A128B3AB45D5A18E3EF9CF93C9576BCE.replay
-    replays/A52F804845573D8DA65E97BF59026A43.replay
-    replays/A558B1B44124D6E021640884E8EEC2A7.replay
-    replays/A6711CE74272B2E663DCC9A200A218E3.replay
-    replays/A7F001A1417A19BFA8C90990D8F7C2FF.replay
-    replays/AFB1F46A49737E36928E1EABC6F5B7AD.replay
-    replays/B9F9B87D4A9D0A3D25D4EC91C0401DE2.replay
-    replays/C14F7E0E4D9B5E6BE9AD5D8ED56B174C.replay
-    replays/C8372B1345B1803DEF039F815DBD802D.replay
-    replays/CC4CA70D4F7A67EBAD0ED9B9923106F7.replay
-    replays/D0449F5F4AA775B86FFA7DA2B5A3204E.replay
-    replays/D428F81646A98C25902CE988AE5C14C8.replay
-    replays/D7FB197A451D69075A0C99A2F49A4053.replay
-    replays/DCB3A6B94A9DBE46FDE5EAA9B012F6C8.replay
-    replays/DE56B1B146687EA2E62E31A37A08C8EB.replay
-    replays/EAE311E84BA35B590A6FDBA6DD4F2FEB.replay
-    replays/EAE8DADA4BB2DC5422792C9B4A67392D.replay
-    replays/F299F176491554B11E34AB91CA76B2CE.replay
-    replays/F7B9E14545C7467B89A00895980FCD73.replay
-    replays/F811C1D24888015E23B598AD8628C742.replay
-    replays/FDC79DA84DD463D4BCCE6B892829AC88.replay
     stack.yaml
 
 source-repository head
@@ -92,101 +30,236 @@
 library
   hs-source-dirs:
       library
-  default-extensions: Strict
-  ghc-options: -Wall
+  ghc-options: -Weverything -Wno-implicit-prelude -Wno-missing-import-lists -Wno-safe -Wno-unsafe
   build-depends:
-      aeson >=1.2.3 && <1.3
-    , base >=4.10.1 && <4.11
-    , bimap >=0.3.3 && <0.4
-    , binary >=0.8.5 && <0.9
-    , binary-bits >=0.5 && <0.6
-    , bytestring >=0.10.8 && <0.11
-    , containers >=0.5.10 && <0.6
-    , data-binary-ieee754 >=0.4.4 && <0.5
-    , http-client >=0.5.7 && <0.6
-    , http-client-tls >=0.3.5 && <0.4
-    , template-haskell >=2.12.0 && <2.13
-    , text >=1.2.2 && <1.3
-    , vector >=0.12.0 && <0.13
+      aeson ==1.2.3.*
+    , aeson-pretty ==0.8.5.*
+    , base ==4.10.1.*
+    , binary ==0.8.5.*
+    , binary-bits ==0.5.*
+    , bytestring ==0.10.8.*
+    , containers ==0.5.10.*
+    , filepath ==1.4.1.*
+    , http-client ==0.5.7.*
+    , http-client-tls ==0.3.5.*
+    , template-haskell ==2.12.*
+    , text ==1.2.2.*
+    , transformers ==0.5.2.*
   exposed-modules:
       Rattletrap
-      Rattletrap.ActorMap
-      Rattletrap.Attribute
-      Rattletrap.Attribute.AppliedDamage
-      Rattletrap.Attribute.Boolean
-      Rattletrap.Attribute.Byte
-      Rattletrap.Attribute.CamSettings
-      Rattletrap.Attribute.ClubColors
-      Rattletrap.Attribute.DamageState
-      Rattletrap.Attribute.Demolish
-      Rattletrap.Attribute.Enum
-      Rattletrap.Attribute.Explosion
-      Rattletrap.Attribute.ExtendedExplosion
-      Rattletrap.Attribute.FlaggedInt
-      Rattletrap.Attribute.Float
-      Rattletrap.Attribute.GameMode
-      Rattletrap.Attribute.Int
-      Rattletrap.Attribute.Loadout
-      Rattletrap.Attribute.LoadoutOnline
-      Rattletrap.Attribute.Loadouts
-      Rattletrap.Attribute.LoadoutsOnline
-      Rattletrap.Attribute.Location
-      Rattletrap.Attribute.MusicStinger
-      Rattletrap.Attribute.PartyLeader
-      Rattletrap.Attribute.Pickup
-      Rattletrap.Attribute.PrivateMatchSettings
-      Rattletrap.Attribute.Product
-      Rattletrap.Attribute.QWord
-      Rattletrap.Attribute.Reservation
-      Rattletrap.Attribute.RigidBodyState
-      Rattletrap.Attribute.String
-      Rattletrap.Attribute.TeamPaint
-      Rattletrap.Attribute.UniqueId
-      Rattletrap.Attribute.WeldedInfo
-      Rattletrap.AttributeMapping
-      Rattletrap.AttributeType
-      Rattletrap.AttributeValue
-      Rattletrap.Cache
-      Rattletrap.ClassAttributeMap
-      Rattletrap.ClassMapping
-      Rattletrap.Content
-      Rattletrap.Crc
+      Rattletrap.Console.Main
       Rattletrap.Data
-      Rattletrap.Frame
-      Rattletrap.Header
-      Rattletrap.Helper
-      Rattletrap.Initialization
-      Rattletrap.Json
-      Rattletrap.KeyFrame
-      Rattletrap.Main
-      Rattletrap.Mark
-      Rattletrap.Message
-      Rattletrap.Primitive
-      Rattletrap.Primitive.CompressedWord
-      Rattletrap.Primitive.CompressedWordVector
-      Rattletrap.Primitive.Dictionary
-      Rattletrap.Primitive.Float32
-      Rattletrap.Primitive.Int32
-      Rattletrap.Primitive.Int8
-      Rattletrap.Primitive.Int8Vector
-      Rattletrap.Primitive.List
-      Rattletrap.Primitive.Section
-      Rattletrap.Primitive.Text
-      Rattletrap.Primitive.Vector
-      Rattletrap.Primitive.Word32
-      Rattletrap.Primitive.Word64
-      Rattletrap.Primitive.Word8
-      Rattletrap.Property
-      Rattletrap.PropertyValue
-      Rattletrap.RemoteId
-      Rattletrap.Replay
-      Rattletrap.Replication
-      Rattletrap.Replication.Destroyed
-      Rattletrap.Replication.Spawned
-      Rattletrap.Replication.Updated
-      Rattletrap.ReplicationValue
-      Rattletrap.Utility
-      Rattletrap.Version
+      Rattletrap.Decode.AppliedDamageAttribute
+      Rattletrap.Decode.Attribute
+      Rattletrap.Decode.AttributeMapping
+      Rattletrap.Decode.AttributeValue
+      Rattletrap.Decode.Bitstream
+      Rattletrap.Decode.BooleanAttribute
+      Rattletrap.Decode.ByteAttribute
+      Rattletrap.Decode.Cache
+      Rattletrap.Decode.CamSettingsAttribute
+      Rattletrap.Decode.ClassMapping
+      Rattletrap.Decode.ClubColorsAttribute
+      Rattletrap.Decode.Common
+      Rattletrap.Decode.CompressedWord
+      Rattletrap.Decode.CompressedWordVector
+      Rattletrap.Decode.Content
+      Rattletrap.Decode.DamageStateAttribute
+      Rattletrap.Decode.DemolishAttribute
+      Rattletrap.Decode.DestroyedReplication
+      Rattletrap.Decode.Dictionary
+      Rattletrap.Decode.EnumAttribute
+      Rattletrap.Decode.ExplosionAttribute
+      Rattletrap.Decode.ExtendedExplosionAttribute
+      Rattletrap.Decode.FlaggedIntAttribute
+      Rattletrap.Decode.Float32le
+      Rattletrap.Decode.FloatAttribute
+      Rattletrap.Decode.Frame
+      Rattletrap.Decode.GameModeAttribute
+      Rattletrap.Decode.Header
+      Rattletrap.Decode.Initialization
+      Rattletrap.Decode.Int32le
+      Rattletrap.Decode.Int8le
+      Rattletrap.Decode.Int8Vector
+      Rattletrap.Decode.IntAttribute
+      Rattletrap.Decode.KeyFrame
+      Rattletrap.Decode.List
+      Rattletrap.Decode.LoadoutAttribute
+      Rattletrap.Decode.LoadoutOnlineAttribute
+      Rattletrap.Decode.LoadoutsAttribute
+      Rattletrap.Decode.LoadoutsOnlineAttribute
+      Rattletrap.Decode.LocationAttribute
+      Rattletrap.Decode.Mark
+      Rattletrap.Decode.Message
+      Rattletrap.Decode.MusicStingerAttribute
+      Rattletrap.Decode.PartyLeaderAttribute
+      Rattletrap.Decode.PickupAttribute
+      Rattletrap.Decode.PlayerHistoryKeyAttribute
+      Rattletrap.Decode.PrivateMatchSettingsAttribute
+      Rattletrap.Decode.ProductAttribute
+      Rattletrap.Decode.Property
+      Rattletrap.Decode.PropertyValue
+      Rattletrap.Decode.QWordAttribute
+      Rattletrap.Decode.RemoteId
+      Rattletrap.Decode.Replay
+      Rattletrap.Decode.Replication
+      Rattletrap.Decode.ReplicationValue
+      Rattletrap.Decode.ReservationAttribute
+      Rattletrap.Decode.RigidBodyStateAttribute
+      Rattletrap.Decode.Section
+      Rattletrap.Decode.SpawnedReplication
+      Rattletrap.Decode.Str
+      Rattletrap.Decode.StringAttribute
+      Rattletrap.Decode.TeamPaintAttribute
+      Rattletrap.Decode.UniqueIdAttribute
+      Rattletrap.Decode.UpdatedReplication
+      Rattletrap.Decode.Vector
+      Rattletrap.Decode.WeldedInfoAttribute
+      Rattletrap.Decode.Word32le
+      Rattletrap.Decode.Word64le
+      Rattletrap.Decode.Word8le
+      Rattletrap.Encode.AppliedDamageAttribute
+      Rattletrap.Encode.Attribute
+      Rattletrap.Encode.AttributeMapping
+      Rattletrap.Encode.AttributeValue
+      Rattletrap.Encode.Bitstream
+      Rattletrap.Encode.BooleanAttribute
+      Rattletrap.Encode.ByteAttribute
+      Rattletrap.Encode.Cache
+      Rattletrap.Encode.CamSettingsAttribute
+      Rattletrap.Encode.ClassMapping
+      Rattletrap.Encode.ClubColorsAttribute
+      Rattletrap.Encode.CompressedWord
+      Rattletrap.Encode.CompressedWordVector
+      Rattletrap.Encode.Content
+      Rattletrap.Encode.DamageStateAttribute
+      Rattletrap.Encode.DemolishAttribute
+      Rattletrap.Encode.DestroyedReplication
+      Rattletrap.Encode.Dictionary
+      Rattletrap.Encode.EnumAttribute
+      Rattletrap.Encode.ExplosionAttribute
+      Rattletrap.Encode.ExtendedExplosionAttribute
+      Rattletrap.Encode.FlaggedIntAttribute
+      Rattletrap.Encode.Float32le
+      Rattletrap.Encode.FloatAttribute
+      Rattletrap.Encode.Frame
+      Rattletrap.Encode.GameModeAttribute
+      Rattletrap.Encode.Header
+      Rattletrap.Encode.Initialization
+      Rattletrap.Encode.Int32le
+      Rattletrap.Encode.Int8le
+      Rattletrap.Encode.Int8Vector
+      Rattletrap.Encode.IntAttribute
+      Rattletrap.Encode.KeyFrame
+      Rattletrap.Encode.List
+      Rattletrap.Encode.LoadoutAttribute
+      Rattletrap.Encode.LoadoutOnlineAttribute
+      Rattletrap.Encode.LoadoutsAttribute
+      Rattletrap.Encode.LoadoutsOnlineAttribute
+      Rattletrap.Encode.LocationAttribute
+      Rattletrap.Encode.Mark
+      Rattletrap.Encode.Message
+      Rattletrap.Encode.MusicStingerAttribute
+      Rattletrap.Encode.PartyLeaderAttribute
+      Rattletrap.Encode.PickupAttribute
+      Rattletrap.Encode.PlayerHistoryKeyAttribute
+      Rattletrap.Encode.PrivateMatchSettingsAttribute
+      Rattletrap.Encode.ProductAttribute
+      Rattletrap.Encode.Property
+      Rattletrap.Encode.PropertyValue
+      Rattletrap.Encode.QWordAttribute
+      Rattletrap.Encode.RemoteId
+      Rattletrap.Encode.Replay
+      Rattletrap.Encode.Replication
+      Rattletrap.Encode.ReplicationValue
+      Rattletrap.Encode.ReservationAttribute
+      Rattletrap.Encode.RigidBodyStateAttribute
+      Rattletrap.Encode.Section
+      Rattletrap.Encode.SpawnedReplication
+      Rattletrap.Encode.Str
+      Rattletrap.Encode.StringAttribute
+      Rattletrap.Encode.TeamPaintAttribute
+      Rattletrap.Encode.UniqueIdAttribute
+      Rattletrap.Encode.UpdatedReplication
+      Rattletrap.Encode.Vector
+      Rattletrap.Encode.WeldedInfoAttribute
+      Rattletrap.Encode.Word32le
+      Rattletrap.Encode.Word64le
+      Rattletrap.Encode.Word8le
+      Rattletrap.Type.AppliedDamageAttribute
+      Rattletrap.Type.Attribute
+      Rattletrap.Type.AttributeMapping
+      Rattletrap.Type.AttributeType
+      Rattletrap.Type.AttributeValue
+      Rattletrap.Type.Bitstream
+      Rattletrap.Type.BooleanAttribute
+      Rattletrap.Type.ByteAttribute
+      Rattletrap.Type.Cache
+      Rattletrap.Type.CamSettingsAttribute
+      Rattletrap.Type.ClassAttributeMap
+      Rattletrap.Type.ClassMapping
+      Rattletrap.Type.ClubColorsAttribute
+      Rattletrap.Type.Common
+      Rattletrap.Type.CompressedWord
+      Rattletrap.Type.CompressedWordVector
+      Rattletrap.Type.Content
+      Rattletrap.Type.DamageStateAttribute
+      Rattletrap.Type.DemolishAttribute
+      Rattletrap.Type.DestroyedReplication
+      Rattletrap.Type.Dictionary
+      Rattletrap.Type.EnumAttribute
+      Rattletrap.Type.ExplosionAttribute
+      Rattletrap.Type.ExtendedExplosionAttribute
+      Rattletrap.Type.FlaggedIntAttribute
+      Rattletrap.Type.Float32le
+      Rattletrap.Type.FloatAttribute
+      Rattletrap.Type.Frame
+      Rattletrap.Type.GameModeAttribute
+      Rattletrap.Type.Header
+      Rattletrap.Type.Initialization
+      Rattletrap.Type.Int32le
+      Rattletrap.Type.Int8le
+      Rattletrap.Type.Int8Vector
+      Rattletrap.Type.IntAttribute
+      Rattletrap.Type.KeyFrame
+      Rattletrap.Type.List
+      Rattletrap.Type.LoadoutAttribute
+      Rattletrap.Type.LoadoutOnlineAttribute
+      Rattletrap.Type.LoadoutsAttribute
+      Rattletrap.Type.LoadoutsOnlineAttribute
+      Rattletrap.Type.LocationAttribute
+      Rattletrap.Type.Mark
+      Rattletrap.Type.Message
+      Rattletrap.Type.MusicStingerAttribute
+      Rattletrap.Type.PartyLeaderAttribute
+      Rattletrap.Type.PickupAttribute
+      Rattletrap.Type.PlayerHistoryKeyAttribute
+      Rattletrap.Type.PrivateMatchSettingsAttribute
+      Rattletrap.Type.ProductAttribute
+      Rattletrap.Type.Property
+      Rattletrap.Type.PropertyValue
+      Rattletrap.Type.QWordAttribute
+      Rattletrap.Type.RemoteId
+      Rattletrap.Type.Replay
+      Rattletrap.Type.Replication
+      Rattletrap.Type.ReplicationValue
+      Rattletrap.Type.ReservationAttribute
+      Rattletrap.Type.RigidBodyStateAttribute
+      Rattletrap.Type.Section
+      Rattletrap.Type.SpawnedReplication
+      Rattletrap.Type.Str
+      Rattletrap.Type.StringAttribute
+      Rattletrap.Type.TeamPaintAttribute
+      Rattletrap.Type.UniqueIdAttribute
+      Rattletrap.Type.UpdatedReplication
+      Rattletrap.Type.Vector
+      Rattletrap.Type.WeldedInfoAttribute
+      Rattletrap.Type.Word32le
+      Rattletrap.Type.Word64le
+      Rattletrap.Type.Word8le
+      Rattletrap.Utility.Bytes
+      Rattletrap.Utility.Crc
+      Rattletrap.Utility.Helper
   other-modules:
       Paths_rattletrap
   default-language: Haskell2010
@@ -195,22 +268,22 @@
   main-is: Main.hs
   hs-source-dirs:
       executables
-  ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
+  ghc-options: -Weverything -Wno-implicit-prelude -Wno-missing-import-lists -Wno-safe -Wno-unsafe -rtsopts -threaded
   build-depends:
-      aeson >=1.2.3 && <1.3
-    , base >=4.10.1 && <4.11
-    , bimap >=0.3.3 && <0.4
-    , binary >=0.8.5 && <0.9
-    , binary-bits >=0.5 && <0.6
-    , bytestring >=0.10.8 && <0.11
-    , containers >=0.5.10 && <0.6
-    , data-binary-ieee754 >=0.4.4 && <0.5
-    , http-client >=0.5.7 && <0.6
-    , http-client-tls >=0.3.5 && <0.4
+      aeson ==1.2.3.*
+    , aeson-pretty ==0.8.5.*
+    , base ==4.10.1.*
+    , binary ==0.8.5.*
+    , binary-bits ==0.5.*
+    , bytestring ==0.10.8.*
+    , containers ==0.5.10.*
+    , filepath ==1.4.1.*
+    , http-client ==0.5.7.*
+    , http-client-tls ==0.3.5.*
     , rattletrap
-    , template-haskell >=2.12.0 && <2.13
-    , text >=1.2.2 && <1.3
-    , vector >=0.12.0 && <0.13
+    , template-haskell ==2.12.*
+    , text ==1.2.2.*
+    , transformers ==0.5.2.*
   other-modules:
       Paths_rattletrap
   default-language: Haskell2010
@@ -220,25 +293,24 @@
   main-is: Main.hs
   hs-source-dirs:
       tests
-  ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
+  ghc-options: -Weverything -Wno-implicit-prelude -Wno-missing-import-lists -Wno-safe -Wno-unsafe -rtsopts -threaded
   build-depends:
-      aeson >=1.2.3 && <1.3
-    , base >=4.10.1 && <4.11
-    , bimap >=0.3.3 && <0.4
-    , binary >=0.8.5 && <0.9
-    , binary-bits >=0.5 && <0.6
-    , bytestring >=0.10.8 && <0.11
-    , containers >=0.5.10 && <0.6
-    , data-binary-ieee754 >=0.4.4 && <0.5
-    , filepath >=1.4.1 && <1.5
-    , hspec >=2.4.4 && <2.5
-    , http-client >=0.5.7 && <0.6
-    , http-client-tls >=0.3.5 && <0.4
+      HUnit ==1.6.*
+    , aeson ==1.2.3.*
+    , aeson-pretty ==0.8.5.*
+    , base ==4.10.1.*
+    , binary ==0.8.5.*
+    , binary-bits ==0.5.*
+    , bytestring ==0.10.8.*
+    , containers ==0.5.10.*
+    , filepath ==1.4.1.*
+    , http-client ==0.5.7.*
+    , http-client-tls ==0.3.5.*
     , rattletrap
-    , template-haskell >=2.12.0 && <2.13
-    , temporary >=1.2.1 && <1.3
-    , text >=1.2.2 && <1.3
-    , vector >=0.12.0 && <0.13
+    , template-haskell ==2.12.*
+    , temporary ==1.2.1.*
+    , text ==1.2.2.*
+    , transformers ==0.5.2.*
   other-modules:
       Paths_rattletrap
   default-language: Haskell2010
diff --git a/replays/00080014003600090000036E0F65CCEB.replay b/replays/00080014003600090000036E0F65CCEB.replay
deleted file mode 100644
Binary files a/replays/00080014003600090000036E0F65CCEB.replay and /dev/null differ
diff --git a/replays/07E925B1423653D44CB8B4B2524792C1.replay b/replays/07E925B1423653D44CB8B4B2524792C1.replay
deleted file mode 100644
Binary files a/replays/07E925B1423653D44CB8B4B2524792C1.replay and /dev/null differ
diff --git a/replays/0AD21FEE4A512906BEB6C98136AAF49A.replay b/replays/0AD21FEE4A512906BEB6C98136AAF49A.replay
deleted file mode 100644
Binary files a/replays/0AD21FEE4A512906BEB6C98136AAF49A.replay and /dev/null differ
diff --git a/replays/1205D96C4D819800927791820096CD49.replay b/replays/1205D96C4D819800927791820096CD49.replay
deleted file mode 100644
Binary files a/replays/1205D96C4D819800927791820096CD49.replay and /dev/null differ
diff --git a/replays/160CA83E41083BFD8E6315B4BFCA0561.replay b/replays/160CA83E41083BFD8E6315B4BFCA0561.replay
deleted file mode 100644
Binary files a/replays/160CA83E41083BFD8E6315B4BFCA0561.replay and /dev/null differ
diff --git a/replays/16D580EF479483E015207C901776F9FB.replay b/replays/16D580EF479483E015207C901776F9FB.replay
deleted file mode 100644
Binary files a/replays/16D580EF479483E015207C901776F9FB.replay and /dev/null differ
diff --git a/replays/18D6738D415B70B5BE4C299588D3C141.replay b/replays/18D6738D415B70B5BE4C299588D3C141.replay
deleted file mode 100644
Binary files a/replays/18D6738D415B70B5BE4C299588D3C141.replay and /dev/null differ
diff --git a/replays/1A126AC24CAA0DB0E98835BD960B8AF8.replay b/replays/1A126AC24CAA0DB0E98835BD960B8AF8.replay
deleted file mode 100644
Binary files a/replays/1A126AC24CAA0DB0E98835BD960B8AF8.replay and /dev/null differ
diff --git a/replays/1AE415514DFC65DCBF8B8391AD35488D.replay b/replays/1AE415514DFC65DCBF8B8391AD35488D.replay
deleted file mode 100644
Binary files a/replays/1AE415514DFC65DCBF8B8391AD35488D.replay and /dev/null differ
diff --git a/replays/1BC2D01444ACE577D01E988EADD4DFD0.replay b/replays/1BC2D01444ACE577D01E988EADD4DFD0.replay
deleted file mode 100644
Binary files a/replays/1BC2D01444ACE577D01E988EADD4DFD0.replay and /dev/null differ
diff --git a/replays/1D1DE97D4941C86E43FE0093563DB621.replay b/replays/1D1DE97D4941C86E43FE0093563DB621.replay
deleted file mode 100644
Binary files a/replays/1D1DE97D4941C86E43FE0093563DB621.replay and /dev/null differ
diff --git a/replays/1EF90FCC4F719F606A5327B3CDD782A4.replay b/replays/1EF90FCC4F719F606A5327B3CDD782A4.replay
deleted file mode 100644
Binary files a/replays/1EF90FCC4F719F606A5327B3CDD782A4.replay and /dev/null differ
diff --git a/replays/1F3798E540B0C37A946561ABBB3037F9.replay b/replays/1F3798E540B0C37A946561ABBB3037F9.replay
deleted file mode 100644
Binary files a/replays/1F3798E540B0C37A946561ABBB3037F9.replay and /dev/null differ
diff --git a/replays/211466D04B983F5A33CC2FA1D5928672.replay b/replays/211466D04B983F5A33CC2FA1D5928672.replay
deleted file mode 100644
Binary files a/replays/211466D04B983F5A33CC2FA1D5928672.replay and /dev/null differ
diff --git a/replays/22660E3649FC7971E5653692473D4318.replay b/replays/22660E3649FC7971E5653692473D4318.replay
deleted file mode 100644
Binary files a/replays/22660E3649FC7971E5653692473D4318.replay and /dev/null differ
diff --git a/replays/22BACD794ABE7B92E50E9CBDBD9C59CE.replay b/replays/22BACD794ABE7B92E50E9CBDBD9C59CE.replay
deleted file mode 100644
Binary files a/replays/22BACD794ABE7B92E50E9CBDBD9C59CE.replay and /dev/null differ
diff --git a/replays/27B6A7B64553F0F685874584F96BAB1B.replay b/replays/27B6A7B64553F0F685874584F96BAB1B.replay
deleted file mode 100644
Binary files a/replays/27B6A7B64553F0F685874584F96BAB1B.replay and /dev/null differ
diff --git a/replays/29F582C34A65EB34D358A784CBE3C189.replay b/replays/29F582C34A65EB34D358A784CBE3C189.replay
deleted file mode 100644
Binary files a/replays/29F582C34A65EB34D358A784CBE3C189.replay and /dev/null differ
diff --git a/replays/2CFE577044E651D3FA9DBF83ECA8BCC3.replay b/replays/2CFE577044E651D3FA9DBF83ECA8BCC3.replay
deleted file mode 100644
Binary files a/replays/2CFE577044E651D3FA9DBF83ECA8BCC3.replay and /dev/null differ
diff --git a/replays/338173964F9F71EBDD31058A1936CBB4.replay b/replays/338173964F9F71EBDD31058A1936CBB4.replay
deleted file mode 100644
Binary files a/replays/338173964F9F71EBDD31058A1936CBB4.replay and /dev/null differ
diff --git a/replays/372DBFCA4BDB340E4357B6BD43032802.replay b/replays/372DBFCA4BDB340E4357B6BD43032802.replay
deleted file mode 100644
Binary files a/replays/372DBFCA4BDB340E4357B6BD43032802.replay and /dev/null differ
diff --git a/replays/387F059C47C09E253C875CA990EFD9F2.replay b/replays/387F059C47C09E253C875CA990EFD9F2.replay
deleted file mode 100644
Binary files a/replays/387F059C47C09E253C875CA990EFD9F2.replay and /dev/null differ
diff --git a/replays/3EA147DD485B8DD24810689A7A989E44.replay b/replays/3EA147DD485B8DD24810689A7A989E44.replay
deleted file mode 100644
Binary files a/replays/3EA147DD485B8DD24810689A7A989E44.replay and /dev/null differ
diff --git a/replays/4126861E477F4A03DE2A4080374D7908.replay b/replays/4126861E477F4A03DE2A4080374D7908.replay
deleted file mode 100644
Binary files a/replays/4126861E477F4A03DE2A4080374D7908.replay and /dev/null differ
diff --git a/replays/42F0D8DA4FC89AE7B80FCAB7F637A8EA.replay b/replays/42F0D8DA4FC89AE7B80FCAB7F637A8EA.replay
deleted file mode 100644
Binary files a/replays/42F0D8DA4FC89AE7B80FCAB7F637A8EA.replay and /dev/null differ
diff --git a/replays/504ED825482186E771FAA9B642CE5CE4.replay b/replays/504ED825482186E771FAA9B642CE5CE4.replay
deleted file mode 100644
Binary files a/replays/504ED825482186E771FAA9B642CE5CE4.replay and /dev/null differ
diff --git a/replays/520E1BFF468CF6C3C48D1EA85D9C7909.replay b/replays/520E1BFF468CF6C3C48D1EA85D9C7909.replay
deleted file mode 100644
Binary files a/replays/520E1BFF468CF6C3C48D1EA85D9C7909.replay and /dev/null differ
diff --git a/replays/52AA67F94090C19D33C5009E54D31FE4.replay b/replays/52AA67F94090C19D33C5009E54D31FE4.replay
deleted file mode 100644
Binary files a/replays/52AA67F94090C19D33C5009E54D31FE4.replay and /dev/null differ
diff --git a/replays/540DA764423C8FB24EB9D486D982F16F.replay b/replays/540DA764423C8FB24EB9D486D982F16F.replay
deleted file mode 100644
Binary files a/replays/540DA764423C8FB24EB9D486D982F16F.replay and /dev/null differ
diff --git a/replays/551CA4D44FF2B86015DE44A6B5790D4C.replay b/replays/551CA4D44FF2B86015DE44A6B5790D4C.replay
deleted file mode 100644
Binary files a/replays/551CA4D44FF2B86015DE44A6B5790D4C.replay and /dev/null differ
diff --git a/replays/6320E51C49066A7C210A2993C2201D5F.replay b/replays/6320E51C49066A7C210A2993C2201D5F.replay
deleted file mode 100644
Binary files a/replays/6320E51C49066A7C210A2993C2201D5F.replay and /dev/null differ
diff --git a/replays/6688EEE34BFEB3EC3A9E3283098CC712.replay b/replays/6688EEE34BFEB3EC3A9E3283098CC712.replay
deleted file mode 100644
Binary files a/replays/6688EEE34BFEB3EC3A9E3283098CC712.replay and /dev/null differ
diff --git a/replays/6B0D1614417085A7AAD82EAA30D8DABE.replay b/replays/6B0D1614417085A7AAD82EAA30D8DABE.replay
deleted file mode 100644
Binary files a/replays/6B0D1614417085A7AAD82EAA30D8DABE.replay and /dev/null differ
diff --git a/replays/6D1B06D844A5BB91B81FD4B5B28F08BA.replay b/replays/6D1B06D844A5BB91B81FD4B5B28F08BA.replay
deleted file mode 100644
Binary files a/replays/6D1B06D844A5BB91B81FD4B5B28F08BA.replay and /dev/null differ
diff --git a/replays/6F7CFCD24638F8A6567AB3A8B9958A90.replay b/replays/6F7CFCD24638F8A6567AB3A8B9958A90.replay
deleted file mode 100644
Binary files a/replays/6F7CFCD24638F8A6567AB3A8B9958A90.replay and /dev/null differ
diff --git a/replays/7109EB9846D303E54B7ACBA792036213.replay b/replays/7109EB9846D303E54B7ACBA792036213.replay
deleted file mode 100644
Binary files a/replays/7109EB9846D303E54B7ACBA792036213.replay and /dev/null differ
diff --git a/replays/7BF6073F4614CE0A438994B9A260DA6A.replay b/replays/7BF6073F4614CE0A438994B9A260DA6A.replay
deleted file mode 100644
Binary files a/replays/7BF6073F4614CE0A438994B9A260DA6A.replay and /dev/null differ
diff --git a/replays/89CBA30E46FA5385BDD35DA4285D4D2E.replay b/replays/89CBA30E46FA5385BDD35DA4285D4D2E.replay
deleted file mode 100644
Binary files a/replays/89CBA30E46FA5385BDD35DA4285D4D2E.replay and /dev/null differ
diff --git a/replays/8AE551FF406D7B82ED853B8C7BFF8CDA.replay b/replays/8AE551FF406D7B82ED853B8C7BFF8CDA.replay
deleted file mode 100644
Binary files a/replays/8AE551FF406D7B82ED853B8C7BFF8CDA.replay and /dev/null differ
diff --git a/replays/9704208245D7DD851F2FB2BC7DFD9AC3.replay b/replays/9704208245D7DD851F2FB2BC7DFD9AC3.replay
deleted file mode 100644
Binary files a/replays/9704208245D7DD851F2FB2BC7DFD9AC3.replay and /dev/null differ
diff --git a/replays/98E58A904D713F2DE202358E8573265D.replay b/replays/98E58A904D713F2DE202358E8573265D.replay
deleted file mode 100644
Binary files a/replays/98E58A904D713F2DE202358E8573265D.replay and /dev/null differ
diff --git a/replays/A128B3AB45D5A18E3EF9CF93C9576BCE.replay b/replays/A128B3AB45D5A18E3EF9CF93C9576BCE.replay
deleted file mode 100644
Binary files a/replays/A128B3AB45D5A18E3EF9CF93C9576BCE.replay and /dev/null differ
diff --git a/replays/A52F804845573D8DA65E97BF59026A43.replay b/replays/A52F804845573D8DA65E97BF59026A43.replay
deleted file mode 100644
Binary files a/replays/A52F804845573D8DA65E97BF59026A43.replay and /dev/null differ
diff --git a/replays/A558B1B44124D6E021640884E8EEC2A7.replay b/replays/A558B1B44124D6E021640884E8EEC2A7.replay
deleted file mode 100644
Binary files a/replays/A558B1B44124D6E021640884E8EEC2A7.replay and /dev/null differ
diff --git a/replays/A6711CE74272B2E663DCC9A200A218E3.replay b/replays/A6711CE74272B2E663DCC9A200A218E3.replay
deleted file mode 100644
Binary files a/replays/A6711CE74272B2E663DCC9A200A218E3.replay and /dev/null differ
diff --git a/replays/A7F001A1417A19BFA8C90990D8F7C2FF.replay b/replays/A7F001A1417A19BFA8C90990D8F7C2FF.replay
deleted file mode 100644
Binary files a/replays/A7F001A1417A19BFA8C90990D8F7C2FF.replay and /dev/null differ
diff --git a/replays/AFB1F46A49737E36928E1EABC6F5B7AD.replay b/replays/AFB1F46A49737E36928E1EABC6F5B7AD.replay
deleted file mode 100644
Binary files a/replays/AFB1F46A49737E36928E1EABC6F5B7AD.replay and /dev/null differ
diff --git a/replays/B9F9B87D4A9D0A3D25D4EC91C0401DE2.replay b/replays/B9F9B87D4A9D0A3D25D4EC91C0401DE2.replay
deleted file mode 100644
Binary files a/replays/B9F9B87D4A9D0A3D25D4EC91C0401DE2.replay and /dev/null differ
diff --git a/replays/C14F7E0E4D9B5E6BE9AD5D8ED56B174C.replay b/replays/C14F7E0E4D9B5E6BE9AD5D8ED56B174C.replay
deleted file mode 100644
Binary files a/replays/C14F7E0E4D9B5E6BE9AD5D8ED56B174C.replay and /dev/null differ
diff --git a/replays/C8372B1345B1803DEF039F815DBD802D.replay b/replays/C8372B1345B1803DEF039F815DBD802D.replay
deleted file mode 100644
Binary files a/replays/C8372B1345B1803DEF039F815DBD802D.replay and /dev/null differ
diff --git a/replays/CC4CA70D4F7A67EBAD0ED9B9923106F7.replay b/replays/CC4CA70D4F7A67EBAD0ED9B9923106F7.replay
deleted file mode 100644
Binary files a/replays/CC4CA70D4F7A67EBAD0ED9B9923106F7.replay and /dev/null differ
diff --git a/replays/D0449F5F4AA775B86FFA7DA2B5A3204E.replay b/replays/D0449F5F4AA775B86FFA7DA2B5A3204E.replay
deleted file mode 100644
Binary files a/replays/D0449F5F4AA775B86FFA7DA2B5A3204E.replay and /dev/null differ
diff --git a/replays/D428F81646A98C25902CE988AE5C14C8.replay b/replays/D428F81646A98C25902CE988AE5C14C8.replay
deleted file mode 100644
Binary files a/replays/D428F81646A98C25902CE988AE5C14C8.replay and /dev/null differ
diff --git a/replays/D7FB197A451D69075A0C99A2F49A4053.replay b/replays/D7FB197A451D69075A0C99A2F49A4053.replay
deleted file mode 100644
Binary files a/replays/D7FB197A451D69075A0C99A2F49A4053.replay and /dev/null differ
diff --git a/replays/DCB3A6B94A9DBE46FDE5EAA9B012F6C8.replay b/replays/DCB3A6B94A9DBE46FDE5EAA9B012F6C8.replay
deleted file mode 100644
Binary files a/replays/DCB3A6B94A9DBE46FDE5EAA9B012F6C8.replay and /dev/null differ
diff --git a/replays/DE56B1B146687EA2E62E31A37A08C8EB.replay b/replays/DE56B1B146687EA2E62E31A37A08C8EB.replay
deleted file mode 100644
Binary files a/replays/DE56B1B146687EA2E62E31A37A08C8EB.replay and /dev/null differ
diff --git a/replays/EAE311E84BA35B590A6FDBA6DD4F2FEB.replay b/replays/EAE311E84BA35B590A6FDBA6DD4F2FEB.replay
deleted file mode 100644
Binary files a/replays/EAE311E84BA35B590A6FDBA6DD4F2FEB.replay and /dev/null differ
diff --git a/replays/EAE8DADA4BB2DC5422792C9B4A67392D.replay b/replays/EAE8DADA4BB2DC5422792C9B4A67392D.replay
deleted file mode 100644
Binary files a/replays/EAE8DADA4BB2DC5422792C9B4A67392D.replay and /dev/null differ
diff --git a/replays/F299F176491554B11E34AB91CA76B2CE.replay b/replays/F299F176491554B11E34AB91CA76B2CE.replay
deleted file mode 100644
Binary files a/replays/F299F176491554B11E34AB91CA76B2CE.replay and /dev/null differ
diff --git a/replays/F7B9E14545C7467B89A00895980FCD73.replay b/replays/F7B9E14545C7467B89A00895980FCD73.replay
deleted file mode 100644
Binary files a/replays/F7B9E14545C7467B89A00895980FCD73.replay and /dev/null differ
diff --git a/replays/F811C1D24888015E23B598AD8628C742.replay b/replays/F811C1D24888015E23B598AD8628C742.replay
deleted file mode 100644
Binary files a/replays/F811C1D24888015E23B598AD8628C742.replay and /dev/null differ
diff --git a/replays/FDC79DA84DD463D4BCCE6B892829AC88.replay b/replays/FDC79DA84DD463D4BCCE6B892829AC88.replay
deleted file mode 100644
Binary files a/replays/FDC79DA84DD463D4BCCE6B892829AC88.replay and /dev/null differ
diff --git a/stack.yaml b/stack.yaml
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,5 +1,1 @@
-resolver: nightly-2017-11-25
-
-packages:
-  - .
-  - tools
+resolver: lts-10.0
diff --git a/tests/Main.hs b/tests/Main.hs
--- a/tests/Main.hs
+++ b/tests/Main.hs
@@ -1,106 +1,107 @@
-import qualified Control.Monad as Monad
-import qualified Data.ByteString.Lazy as ByteString
-import qualified Rattletrap
-import qualified System.FilePath as FilePath
-import qualified System.IO.Temp as Temp
-import qualified Test.Hspec as Hspec
+import Control.Exception (displayException, try)
+import Control.Monad (unless)
+import Data.ByteString.Lazy (readFile)
+import Prelude hiding (readFile)
+import Rattletrap.Console.Main (rattletrap)
+import System.FilePath (addExtension, joinPath)
+import System.IO.Temp (withSystemTempDirectory)
+import Text.Printf (printf)
 
-main :: IO ()
-main = Hspec.hspec spec
+import qualified Test.HUnit as Test
 
-spec :: Hspec.Spec
-spec = Hspec.describe "Rattletrap" (mapM_ (uncurry itCanRoundTrip) replays)
+main :: IO Test.Counts
+main = withSystemTempDirectory "rattletrap-" (Test.runTestTT . toTests)
 
-itCanRoundTrip :: String -> String -> Hspec.Spec
-itCanRoundTrip uuid description =
-  Hspec.it (specName uuid description) (specBody uuid)
+toTests :: FilePath -> Test.Test
+toTests directory = Test.TestList (fmap (toTest directory) replays)
 
-specName :: String -> String -> String
-specName uuid description = unwords [take 4 uuid, description]
+toTest :: FilePath -> (String, String) -> Test.Test
+toTest directory (uuid, name) = Test.TestLabel
+  (toLabel uuid name)
+  (Test.TestCase (toAssertion directory uuid))
 
-specBody :: String -> IO ()
-specBody uuid = do
-  let inputFile = pathToReplay uuid
-  input <- ByteString.readFile inputFile
-  Temp.withSystemTempDirectory
-    "replay-"
-    ( \directory -> do
-      let jsonFile = FilePath.combine directory "replay.json"
-      Rattletrap.mainWithArgs ["decode", inputFile, jsonFile]
-      let outputFile = FilePath.combine directory "output.replay"
-      Rattletrap.mainWithArgs ["encode", jsonFile, outputFile]
-      output <- ByteString.readFile outputFile
-      Monad.unless
-        (output == input)
-        (Hspec.expectationFailure "output does not match input")
-    )
+toLabel :: String -> String -> String
+toLabel = printf "%s: %s"
 
-pathToReplay :: String -> FilePath
-pathToReplay uuid =
-  FilePath.joinPath ["replays", FilePath.addExtension uuid ".replay"]
+toAssertion :: FilePath -> String -> Test.Assertion
+toAssertion directory uuid = do
+  let inputFile = joinPath ["replays", addExtension uuid ".replay"]
+  result <- try (readFile inputFile)
+  case result of
+    Left problem -> putStrLn (displayException (problem :: IOError))
+    Right input -> do
+      let jsonFile = joinPath [directory, addExtension uuid ".json"]
+      rattletrap "" ["--compact", "--input", inputFile, "--output", jsonFile]
+      let outputFile = joinPath [directory, addExtension uuid ".replay"]
+      rattletrap "" ["--input", jsonFile, "--output", outputFile]
+      output <- readFile outputFile
+      unless
+        (output == input)
+        (Test.assertFailure "output does not match input")
 
 replays :: [(String, String)]
 replays =
-  [ ("00080014003600090000036E0F65CCEB", "a flip time")
-  , ("07E925B1423653D44CB8B4B2524792C1", "a game mode before Neo Tokyo")
-  , ("0AD21FEE4A512906BEB6C98136AAF49A", "some Latin-1 text")
-  , ("1205D96C4D819800927791820096CD49", "rumble mode")
-  , ("160CA83E41083BFD8E6315B4BFCA0561", "a dedicated server IP")
-  , ("16D580EF479483E015207C901776F9FB", "new property types")
-  , ("18D6738D415B70B5BE4C299588D3C141", "an online loadout attribute")
-  , ("1A126AC24CAA0DB0E98835BD960B8AF8", "overtime")
-  , ("1AE415514DFC65DCBF8B8391AD35488D", "a game time")
-  , ("1BC2D01444ACE577D01E988EADD4DFD0", "no padding after the frames")
-  , ("1D1DE97D4941C86E43FE0093563DB621", "a camera pitch")
-  , ("1EF90FCC4F719F606A5327B3CDD782A4", "a private hoops match")
-  , ("1F3798E540B0C37A946561ABBB3037F9", "splitscreen players")
-  , ("211466D04B983F5A33CC2FA1D5928672", "a match save")
-  , ("22660E3649FC7971E5653692473D4318", "dropshot")
-  , ("22BACD794ABE7B92E50E9CBDBD9C59CE", "a vote to forfeit")
-  , ("27B6A7B64553F0F685874584F96BAB1B", "some UTF-16 text")
-  , ("29F582C34A65EB34D358A784CBE3C189", "frames")
-  , ("2CFE577044E651D3FA9DBF83ECA8BCC3", "a new playstation id")
-  , ("338173964F9F71EBDD31058A1936CBB4", "patch 1.37")
-  , ("372DBFCA4BDB340E4357B6BD43032802", "a camera yaw attribute")
-  , ("387F059C47C09E253C875CA990EFD9F2", "a frozen attribute")
-  , ("3EA147DD485B8DD24810689A7A989E44", "a custom team name")
-  , ("4126861E477F4A03DE2A4080374D7908", "a game mode after Neo Tokyo")
-  , ("42F0D8DA4FC89AE7B80FCAB7F637A8EA", "reservations after Neo Tokyo")
-  , ("504ED825482186E771FAA9B642CE5CE4", "some messages")
-  , ("520E1BFF468CF6C3C48D1EA85D9C7909", "no pickup attribute")
-  , ("52AA67F94090C19D33C5009E54D31FE4", "a match-ending attribute")
-  , ("540DA764423C8FB24EB9D486D982F16F", "a demolish attribute")
-  , ("551CA4D44FF2B86015DE44A6B5790D4C", "private match settings")
-  , ("6320E51C49066A7C210A2993C2201D5F", "a forfeit attribute")
-  , ("6688EEE34BFEB3EC3A9E3283098CC712", "a malformed byte property")
-  , ("6B0D1614417085A7AAD82EAA30D8DABE", "patch 1.37")
-  , ("6D1B06D844A5BB91B81FD4B5B28F08BA", "a flip right")
-  , ("6F7CFCD24638F8A6567AB3A8B9958A90", "a map with numbers")
-  , ("7109EB9846D303E54B7ACBA792036213", "a boost modifier")
-  , ("7BF6073F4614CE0A438994B9A260DA6A", "an online loadouts attribute")
-  , ("89CBA30E46FA5385BDD35DA4285D4D2E", "remote user data")
-  , ("8AE551FF406D7B82ED853B8C7BFF8CDA", "new painted items")
-  , ("9704208245D7DD851F2FB2BC7DFD9AC3", "a batarang")
-  , ("98E58A904D713F2DE202358E8573265D", "a player using behind view")
-  , ("A128B3AB45D5A18E3EF9CF93C9576BCE", "a round count down")
-  , ("A52F804845573D8DA65E97BF59026A43", "some more mutators")
-  , ("A558B1B44124D6E021640884E8EEC2A7", "extended explosion data")
-  , ("A6711CE74272B2E663DCC9A200A218E3", "a waiting player")
-  , ("A7F001A1417A19BFA8C90990D8F7C2FF", "a ready attribute")
-  , ("AFB1F46A49737E36928E1EABC6F5B7AD", "patch 1.37")
-  , ("B9F9B87D4A9D0A3D25D4EC91C0401DE2", "a party leader")
-  , ("C14F7E0E4D9B5E6BE9AD5D8ED56B174C", "some mutators")
-  , ("C8372B1345B1803DEF039F815DBD802D", "a spectator")
-  , ("CC4CA70D4F7A67EBAD0ED9B9923106F7", "after Starbase ARC")
-  , ("D0449F5F4AA775B86FFA7DA2B5A3204E", "hoops mutators")
-  , ("D428F81646A98C25902CE988AE5C14C8", "a private hockey match")
-  , ("D7FB197A451D69075A0C99A2F49A4053", "an explosion attribute")
-  , ("DCB3A6B94A9DBE46FDE5EAA9B012F6C8", "a pawn type attribute")
-  , ("DE56B1B146687EA2E62E31A37A08C8EB", "a problematic product attribute")
-  , ("EAE311E84BA35B590A6FDBA6DD4F2FEB", "an actor/object ID collision")
-  , ("EAE8DADA4BB2DC5422792C9B4A67392D", "custom team colors")
-  , ("F299F176491554B11E34AB91CA76B2CE", "a location attribute")
-  , ("F7B9E14545C7467B89A00895980FCD73", "a hockey game event")
-  , ("F811C1D24888015E23B598AD8628C742", "no frames")
-  , ("FDC79DA84DD463D4BCCE6B892829AC88", "an MVP")
+  [ ("0008", "a flip time")
+  , ("000b", "nintendo switch")
+  , ("07e9", "a game mode before Neo Tokyo")
+  , ("0ad2", "some Latin-1 text")
+  , ("1205", "rumble mode")
+  , ("160c", "a dedicated server IP")
+  , ("16d5", "new property types")
+  , ("18d6", "an online loadout attribute")
+  , ("1a12", "overtime")
+  , ("1ae4", "a game time")
+  , ("1bc2", "no padding after the frames")
+  , ("1d1d", "a camera pitch")
+  , ("1ef9", "a private hoops match")
+  , ("1f37", "splitscreen players")
+  , ("2114", "a match save")
+  , ("2266", "dropshot")
+  , ("22ba", "a vote to forfeit")
+  , ("27b6", "some UTF-16 text")
+  , ("29f5", "frames")
+  , ("2cfe", "a new playstation id")
+  , ("3381", "patch 1.37")
+  , ("372d", "a camera yaw attribute")
+  , ("387f", "a frozen attribute")
+  , ("3ea1", "a custom team name")
+  , ("4126", "a game mode after Neo Tokyo")
+  , ("42f0", "reservations after Neo Tokyo")
+  , ("504e", "some messages")
+  , ("520e", "no pickup attribute")
+  , ("52aa", "a match-ending attribute")
+  , ("540d", "a demolish attribute")
+  , ("551c", "private match settings")
+  , ("6320", "a forfeit attribute")
+  , ("6688", "a malformed byte property")
+  , ("6b0d", "patch 1.37")
+  , ("6d1b", "a flip right")
+  , ("6f7c", "a map with numbers")
+  , ("7109", "a boost modifier")
+  , ("7bf6", "an online loadouts attribute")
+  , ("89cb", "remote user data")
+  , ("8ae5", "new painted items")
+  , ("9704", "a batarang")
+  , ("98e5", "a player using behind view")
+  , ("a128", "a round count down")
+  , ("a52f", "some more mutators")
+  , ("a558", "extended explosion data")
+  , ("a671", "a waiting player")
+  , ("a7f0", "a ready attribute")
+  , ("afb1", "patch 1.37")
+  , ("b9f9", "a party leader")
+  , ("c14f", "some mutators")
+  , ("c837", "a spectator")
+  , ("cc4c", "after Starbase ARC")
+  , ("d044", "hoops mutators")
+  , ("d428", "a private hockey match")
+  , ("d7fb", "an explosion attribute")
+  , ("dcb3", "a pawn type attribute")
+  , ("de56", "a problematic product attribute")
+  , ("eae3", "an actor/object ID collision")
+  , ("eae8", "custom team colors")
+  , ("f299", "a location attribute")
+  , ("f7b9", "a hockey game event")
+  , ("f811", "no frames")
+  , ("fdc7", "an MVP")
   ]
