diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown
deleted file mode 100644
--- a/CHANGELOG.markdown
+++ /dev/null
@@ -1,7 +0,0 @@
-# Change log
-
-Rattletrap uses [Semantic Versioning][].
-The change log is available through the [releases on GitHub][].
-
-[Semantic Versioning]: http://semver.org/spec/v2.0.0.html
-[releases on GitHub]: https://github.com/tfausak/rattletrap/releases
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+# Change log
+
+Rattletrap uses [Semantic Versioning][].
+The change log is available through the [releases on GitHub][].
+
+[Semantic Versioning]: http://semver.org/spec/v2.0.0.html
+[releases on GitHub]: https://github.com/tfausak/rattletrap/releases
diff --git a/LICENSE.markdown b/LICENSE.markdown
deleted file mode 100644
--- a/LICENSE.markdown
+++ /dev/null
@@ -1,23 +0,0 @@
-# [The MIT License (MIT)][]
-
-Copyright (c) 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:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-[The MIT License (MIT)]: http://opensource.org/licenses/MIT
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 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:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.markdown b/README.markdown
deleted file mode 100644
--- a/README.markdown
+++ /dev/null
@@ -1,79 +0,0 @@
-# [Rattletrap][]
-
-[![Version badge][]][version]
-[![Windows build badge][]][windows build]
-[![Build badge][]][build]
-
-Rattletrap parses and generates [Rocket League][] replays.
-
-## Install
-
-Get Rattletrap by downloading and unpacking [the latest release][] for your
-platform.
-
-To build Rattletrap from source, install [Stack][]. Then run
-`stack --resolver nightly install rattletrap`.
-
-## Replays
-
-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`
-
-## Parse
-
-Rattletrap can parse (decode) Rocket League replays and output them as JSON.
-
-``` sh
-> rattletrap decode http://example.com/input.replay output.json
-# or
-> rattletrap decode input.replay > output.json
-# or
-> rattletrap.decode < 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.
-
-## Generate
-
-Rattletrap can also generate (encode) Rocket League replays from JSON files.
-
-``` sh
-> rattletrap encode http://example.com/input.json output.replay
-# or
-> rattletrap encode input.json > output.replay
-# or
-> rattletrap.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.
-
-## Modify
-
-By inserting another program between parsing and generating, Rattletrap can be
-used to modify replays.
-
-``` sh
-> rattletrap decode < original.replay |
-  modify-replay-json |
-  rattletrap encode > modified.replay
-```
-
-[Rattletrap]: https://github.com/tfausak/rattletrap
-[Version badge]: https://www.stackage.org/package/rattletrap/badge/nightly?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]: https://ci.appveyor.com/project/TaylorFausak/rattletrap
-[Build badge]: https://travis-ci.org/tfausak/rattletrap.svg?branch=master
-[build]: https://travis-ci.org/tfausak/rattletrap
-[Rocket League]: https://www.rocketleaguegame.com
-[the latest release]: https://github.com/tfausak/rattletrap/releases/latest
-[Stack]: https://docs.haskellstack.org/en/stable/README/
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,135 @@
+# [Rattletrap](https://github.com/tfausak/rattletrap)
+
+[![CI](https://github.com/tfausak/rattletrap/actions/workflows/ci.yml/badge.svg)](https://github.com/tfausak/rattletrap/actions/workflows/ci.yml)
+[![Hackage](https://badgen.net/hackage/v/rattletrap)](https://hackage.haskell.org/package/rattletrap)
+
+:warning: This package is not maintained anymore.
+
+Rattletrap parses and generates [Rocket League][] replays. Parsing replays can
+be used to analyze data in order to collect high-level statistics like players
+and points, or low-level details like positions and cameras. Generating replays
+can be used to modify replays in order to force everyone into the same car or
+change the map a game was played on.
+
+Rattletrap supports every version of Rocket League up to [2.46][], which was
+released on 2024-12-03. If a replay can be played by the Rocket League client,
+it can be parsed by Rattletrap. (If not, that's a bug. Please report it!)
+
+## Install
+
+Rattletrap is a command-line application. You should only use it if you're
+comfortable running things in terminals or command prompts. Otherwise consider
+using another tool like [Ball Chasing][].
+
+The best way to get Rattletrap is by downloading [the latest release][] for
+your platform.
+
+Rattletrap is written in Haskell. If you'd like to use a program written in
+a different language, consider one of the following:
+
+- <https://github.com/jjbott/RocketLeagueReplayParser> (C#)
+- <https://github.com/nickbabcock/rrrocket> (Rust)
+- <https://github.com/Bakkes/CPPRP> (C++)
+- <https://github.com/Drogebot/RocketRP> (C#)
+
+## Replays
+
+Rocket League saves your replays in a folder that depends on your operating
+system.
+
+- 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 12.0.0
+  -c           --compact         minify JSON output
+  -f           --fast            only encode or decode the header
+  -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
+               --schema          output the schema
+               --skip-crc        skip the CRC
+  -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 --input http://example.com/input.replay --output output.json
+# or
+$ rattletrap -i input.replay -o output.json
+# or
+$ rattletrap < input.replay > output.json
+```
+
+The input argument can either be a local path or a URL.
+
+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 --input http://example.com/input.json --output output.replay
+# or
+$ rattletrap -i input.json -o output.replay
+# or
+$ 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 output replay will be bit-for-bit
+identical to the input replay.
+
+## Modify
+
+By inserting another program between parsing and generating, Rattletrap can be
+used to modify replays.
+
+``` sh
+$ rattletrap -i input.replay |
+  modify-replay-json |
+  rattletrap -o output.replay
+```
+
+[Rattletrap]: https://github.com/tfausak/rattletrap
+[Rocket League]: https://www.rocketleague.com
+[2.46]: https://www.rocketleague.com/en/news/patch-notes-v2-46
+[Ball Chasing]: https://ballchasing.com
+[the latest release]: https://github.com/tfausak/rattletrap/releases/latest
diff --git a/Setup.hs b/Setup.hs
deleted file mode 100644
--- a/Setup.hs
+++ /dev/null
@@ -1,4 +0,0 @@
-import qualified Distribution.Simple as Cabal
-
-main :: IO ()
-main = Cabal.defaultMain
diff --git a/executables/Main.hs b/executables/Main.hs
deleted file mode 100644
--- a/executables/Main.hs
+++ /dev/null
@@ -1,5 +0,0 @@
-module Main
-  ( module Rattletrap
-  ) where
-
-import Rattletrap (main)
diff --git a/library/Rattletrap.hs b/library/Rattletrap.hs
deleted file mode 100644
--- a/library/Rattletrap.hs
+++ /dev/null
@@ -1,87 +0,0 @@
--- | 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
-  ) 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
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/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
deleted file mode 100644
--- a/library/Rattletrap/Data.hs
+++ /dev/null
@@ -1,372 +0,0 @@
-module Rattletrap.Data where
-
-import Rattletrap.AttributeType
-
-rawParentClasses :: [(String, String)]
-rawParentClasses =
-  [ ("Engine.Actor", "Core.Object")
-  , ("Engine.GameReplicationInfo", "Engine.ReplicationInfo")
-  , ("Engine.Info", "Engine.Actor")
-  , ("Engine.Pawn", "Engine.Actor")
-  , ("Engine.PlayerReplicationInfo", "Engine.ReplicationInfo")
-  , ("Engine.ReplicationInfo", "Engine.Info")
-  , ("Engine.TeamInfo", "Engine.ReplicationInfo")
-  , ("ProjectX.GRI_X", "Engine.GameReplicationInfo")
-  , ("ProjectX.Pawn_X", "Engine.Pawn")
-  , ("ProjectX.PRI_X", "Engine.PlayerReplicationInfo")
-  , ("TAGame.Ball_TA", "TAGame.RBActor_TA")
-  , ("TAGame.CameraSettingsActor_TA", "Engine.ReplicationInfo")
-  , ("TAGame.Car_Season_TA", "TAGame.PRI_TA")
-  , ("TAGame.Car_TA", "TAGame.Vehicle_TA")
-  , ("TAGame.CarComponent_Boost_TA", "TAGame.CarComponent_TA")
-  , ("TAGame.CarComponent_Dodge_TA", "TAGame.CarComponent_TA")
-  , ("TAGame.CarComponent_DoubleJump_TA", "TAGame.CarComponent_TA")
-  , ("TAGame.CarComponent_FlipCar_TA", "TAGame.CarComponent_TA")
-  , ("TAGame.CarComponent_Jump_TA", "TAGame.CarComponent_TA")
-  , ("TAGame.CarComponent_TA", "Engine.ReplicationInfo")
-  , ("TAGame.CrowdActor_TA", "Engine.ReplicationInfo")
-  , ("TAGame.CrowdManager_TA", "Engine.ReplicationInfo")
-  , ("TAGame.GameEvent_Season_TA", "TAGame.GameEvent_Soccar_TA")
-  , ("TAGame.GameEvent_Soccar_TA", "TAGame.GameEvent_Team_TA")
-  , ("TAGame.GameEvent_SoccarPrivate_TA", "TAGame.GameEvent_Soccar_TA")
-  , ("TAGame.GameEvent_SoccarSplitscreen_TA", "TAGame.GameEvent_SoccarPrivate_TA")
-  , ("TAGame.GameEvent_TA", "Engine.ReplicationInfo")
-  , ("TAGame.GameEvent_Team_TA", "TAGame.GameEvent_TA")
-  , ("TAGame.GRI_TA", "ProjectX.GRI_X")
-  , ("TAGame.InMapScoreboard_TA", "Engine.Actor")
-  , ("TAGame.PRI_TA", "ProjectX.PRI_X")
-  , ("TAGame.RBActor_TA", "ProjectX.Pawn_X")
-  , ("TAGame.SpecialPickup_BallCarSpring_TA", "TAGame.SpecialPickup_Spring_TA")
-  , ("TAGame.SpecialPickup_BallFreeze_TA", "TAGame.SpecialPickup_Targeted_TA")
-  , ("TAGame.SpecialPickup_BallGravity_TA", "TAGame.SpecialPickup_TA")
-  , ("TAGame.SpecialPickup_BallLasso_TA", "TAGame.SpecialPickup_GrapplingHook_TA")
-  , ("TAGame.SpecialPickup_BallVelcro_TA", "TAGame.SpecialPickup_TA")
-  , ("TAGame.SpecialPickup_Batarang_TA", "TAGame.SpecialPickup_BallLasso_TA")
-  , ("TAGame.SpecialPickup_BoostOverride_TA", "TAGame.SpecialPickup_Targeted_TA")
-  , ("TAGame.SpecialPickup_GrapplingHook_TA", "TAGame.SpecialPickup_Targeted_TA")
-  , ("TAGame.SpecialPickup_HitForce_TA", "TAGame.SpecialPickup_TA")
-  , ("TAGame.SpecialPickup_Spring_TA", "TAGame.SpecialPickup_Targeted_TA")
-  , ("TAGame.SpecialPickup_Swapper_TA", "TAGame.SpecialPickup_Targeted_TA")
-  , ("TAGame.SpecialPickup_TA", "TAGame.CarComponent_TA")
-  , ("TAGame.SpecialPickup_Targeted_TA", "TAGame.SpecialPickup_TA")
-  , ("TAGame.SpecialPickup_Tornado_TA", "TAGame.SpecialPickup_TA")
-  , ("TAGame.Team_Soccar_TA", "TAGame.Team_TA")
-  , ("TAGame.Team_TA", "Engine.TeamInfo")
-  , ("TAGame.Vehicle_TA", "TAGame.RBActor_TA")
-  , ("TAGame.VehiclePickup_Boost_TA", "TAGame.VehiclePickup_TA")
-  , ("TAGame.VehiclePickup_TA", "Engine.ReplicationInfo")
-  ]
-
-rawClassesWithLocation :: [String]
-rawClassesWithLocation =
-  [ "TAGame.Ball_Breakout_TA"
-  , "TAGame.Ball_TA"
-  , "TAGame.CameraSettingsActor_TA"
-  , "TAGame.Car_Season_TA"
-  , "TAGame.Car_TA"
-  , "TAGame.CarComponent_Boost_TA"
-  , "TAGame.CarComponent_Dodge_TA"
-  , "TAGame.CarComponent_DoubleJump_TA"
-  , "TAGame.CarComponent_FlipCar_TA"
-  , "TAGame.CarComponent_Jump_TA"
-  , "TAGame.GameEvent_Season_TA"
-  , "TAGame.GameEvent_Soccar_TA"
-  , "TAGame.GameEvent_SoccarPrivate_TA"
-  , "TAGame.GameEvent_SoccarSplitscreen_TA"
-  , "TAGame.GRI_TA"
-  , "TAGame.PRI_TA"
-  , "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"
-  , "TAGame.SpecialPickup_BoostOverride_TA"
-  , "TAGame.SpecialPickup_GrapplingHook_TA"
-  , "TAGame.SpecialPickup_HitForce_TA"
-  , "TAGame.SpecialPickup_Swapper_TA"
-  , "TAGame.SpecialPickup_Tornado_TA"
-  , "TAGame.Team_Soccar_TA"
-  ]
-
-rawClassesWithRotation :: [String]
-rawClassesWithRotation =
-  [ "TAGame.Ball_Breakout_TA"
-  , "TAGame.Ball_TA"
-  , "TAGame.Car_Season_TA"
-  , "TAGame.Car_TA"
-  ]
-
-rawObjectClasses :: [(String, String)]
-rawObjectClasses =
-  [ ("Archetypes.Ball.Ball_BasketBall_Mutator", "TAGame.Ball_TA")
-  , ("Archetypes.Ball.Ball_Basketball", "TAGame.Ball_TA")
-  , ("Archetypes.Ball.Ball_Breakout", "TAGame.Ball_Breakout_TA")
-  , ("Archetypes.Ball.Ball_Default", "TAGame.Ball_TA")
-  , ("Archetypes.Ball.Ball_Puck", "TAGame.Ball_TA")
-  , ("Archetypes.Ball.CubeBall", "TAGame.Ball_TA")
-  , ("Archetypes.Car.Car_Default", "TAGame.Car_TA")
-  , ("Archetypes.CarComponents.CarComponent_Boost", "TAGame.CarComponent_Boost_TA")
-  , ("Archetypes.CarComponents.CarComponent_Dodge", "TAGame.CarComponent_Dodge_TA")
-  , ("Archetypes.CarComponents.CarComponent_DoubleJump", "TAGame.CarComponent_DoubleJump_TA")
-  , ("Archetypes.CarComponents.CarComponent_FlipCar", "TAGame.CarComponent_FlipCar_TA")
-  , ("Archetypes.CarComponents.CarComponent_Jump", "TAGame.CarComponent_Jump_TA")
-  , ("Archetypes.GameEvent.GameEvent_Basketball", "TAGame.GameEvent_Soccar_TA")
-  , ("Archetypes.GameEvent.GameEvent_BasketballPrivate", "TAGame.GameEvent_SoccarPrivate_TA")
-  , ("Archetypes.GameEvent.GameEvent_BasketballSplitscreen", "TAGame.GameEvent_SoccarSplitscreen_TA")
-  , ("Archetypes.GameEvent.GameEvent_Breakout", "TAGame.GameEvent_Soccar_TA")
-  , ("Archetypes.GameEvent.GameEvent_Hockey", "TAGame.GameEvent_Soccar_TA")
-  , ("Archetypes.GameEvent.GameEvent_HockeyPrivate", "TAGame.GameEvent_SoccarPrivate_TA")
-  , ("Archetypes.GameEvent.GameEvent_HockeySplitscreen", "TAGame.GameEvent_SoccarSplitscreen_TA")
-  , ("Archetypes.GameEvent.GameEvent_Items", "TAGame.GameEvent_Soccar_TA")
-  , ("Archetypes.GameEvent.GameEvent_Season:CarArchetype", "TAGame.Car_TA")
-  , ("Archetypes.GameEvent.GameEvent_Season", "TAGame.GameEvent_Season_TA")
-  , ("Archetypes.GameEvent.GameEvent_Soccar", "TAGame.GameEvent_Soccar_TA")
-  , ("Archetypes.GameEvent.GameEvent_SoccarPrivate", "TAGame.GameEvent_SoccarPrivate_TA")
-  , ("Archetypes.GameEvent.GameEvent_SoccarSplitscreen", "TAGame.GameEvent_SoccarSplitscreen_TA")
-  , ("Archetypes.SpecialPickups.SpecialPickup_BallFreeze", "TAGame.SpecialPickup_BallFreeze_TA")
-  , ("Archetypes.SpecialPickups.SpecialPickup_BallGrapplingHook", "TAGame.SpecialPickup_GrapplingHook_TA")
-  , ("Archetypes.SpecialPickups.SpecialPickup_BallLasso", "TAGame.SpecialPickup_BallLasso_TA")
-  , ("Archetypes.SpecialPickups.SpecialPickup_BallSpring", "TAGame.SpecialPickup_BallCarSpring_TA")
-  , ("Archetypes.SpecialPickups.SpecialPickup_BallVelcro", "TAGame.SpecialPickup_BallVelcro_TA")
-  , ("Archetypes.SpecialPickups.SpecialPickup_Batarang", "TAGame.SpecialPickup_Batarang_TA")
-  , ("Archetypes.SpecialPickups.SpecialPickup_BoostOverride", "TAGame.SpecialPickup_BoostOverride_TA")
-  , ("Archetypes.SpecialPickups.SpecialPickup_CarSpring", "TAGame.SpecialPickup_BallCarSpring_TA")
-  , ("Archetypes.SpecialPickups.SpecialPickup_GravityWell", "TAGame.SpecialPickup_BallGravity_TA")
-  , ("Archetypes.SpecialPickups.SpecialPickup_StrongHit", "TAGame.SpecialPickup_HitForce_TA")
-  , ("Archetypes.SpecialPickups.SpecialPickup_Swapper", "TAGame.SpecialPickup_Swapper_TA")
-  , ("Archetypes.SpecialPickups.SpecialPickup_Tornado", "TAGame.SpecialPickup_Tornado_TA")
-  , ("Archetypes.Teams.Team0", "TAGame.Team_Soccar_TA")
-  , ("Archetypes.Teams.Team1", "TAGame.Team_Soccar_TA")
-  , ("GameInfo_Basketball.GameInfo.GameInfo_Basketball:GameReplicationInfoArchetype", "TAGame.GRI_TA")
-  , ("GameInfo_Breakout.GameInfo.GameInfo_Breakout:GameReplicationInfoArchetype", "TAGame.GRI_TA")
-  , ("Gameinfo_Hockey.GameInfo.Gameinfo_Hockey:GameReplicationInfoArchetype", "TAGame.GRI_TA")
-  , ("GameInfo_Items.GameInfo.GameInfo_Items:GameReplicationInfoArchetype", "TAGame.GRI_TA")
-  , ("GameInfo_Season.GameInfo.GameInfo_Season:GameReplicationInfoArchetype", "TAGame.GRI_TA")
-  , ("GameInfo_Soccar.GameInfo.GameInfo_Soccar:GameReplicationInfoArchetype", "TAGame.GRI_TA")
-  , ("TAGame.Default__CameraSettingsActor_TA", "TAGame.CameraSettingsActor_TA")
-  , ("TAGame.Default__PRI_TA", "TAGame.PRI_TA")
-  , ("TheWorld:PersistentLevel.BreakOutActor_Platform_TA", "TAGame.BreakOutActor_Platform_TA")
-  , ("TheWorld:PersistentLevel.CrowdActor_TA", "TAGame.CrowdActor_TA")
-  , ("TheWorld:PersistentLevel.CrowdManager_TA", "TAGame.CrowdManager_TA")
-  , ("TheWorld:PersistentLevel.InMapScoreboard_TA", "TAGame.InMapScoreboard_TA")
-  , ("TheWorld:PersistentLevel.VehiclePickup_Boost_TA", "TAGame.VehiclePickup_Boost_TA")
-  ]
-
-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)
-  ]
-
-rawCrc32Table :: Integral a => [a]
-rawCrc32Table =
-  [ 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9
-  , 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005
-  , 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61
-  , 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd
-  , 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9
-  , 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75
-  , 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011
-  , 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd
-  , 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039
-  , 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5
-  , 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81
-  , 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d
-  , 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49
-  , 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95
-  , 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1
-  , 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d
-  , 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae
-  , 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072
-  , 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16
-  , 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca
-  , 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde
-  , 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02
-  , 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066
-  , 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba
-  , 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e
-  , 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692
-  , 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6
-  , 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a
-  , 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e
-  , 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2
-  , 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686
-  , 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a
-  , 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637
-  , 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb
-  , 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f
-  , 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53
-  , 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47
-  , 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b
-  , 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff
-  , 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623
-  , 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7
-  , 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b
-  , 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f
-  , 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3
-  , 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7
-  , 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b
-  , 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f
-  , 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3
-  , 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640
-  , 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c
-  , 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8
-  , 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24
-  , 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30
-  , 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec
-  , 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088
-  , 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654
-  , 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0
-  , 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c
-  , 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18
-  , 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4
-  , 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0
-  , 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c
-  , 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668
-  , 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4
-  ]
diff --git a/library/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/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/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
deleted file mode 100644
--- a/package.yaml
+++ /dev/null
@@ -1,63 +0,0 @@
-name: rattletrap
-version: 3.1.2
-
-category: Game
-description: Rattletrap parses and generates Rocket League replays.
-extra-source-files:
-  - CHANGELOG.markdown
-  - replays/*.replay
-  - package.yaml
-  - README.markdown
-  - stack.yaml
-github: tfausak/rattletrap
-license-file: LICENSE.markdown
-license: MIT
-maintainer: Taylor Fausak
-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
-
-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
-
-tests:
-  test:
-    dependencies:
-      - filepath >= 1.4.1 && < 1.5
-      - hspec >= 2.4.4 && < 2.5
-      - rattletrap
-      - temporary >= 1.2.1 && < 1.3
-    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
@@ -1,244 +1,233 @@
--- This file has been generated from package.yaml by hpack version 0.20.0.
---
--- see: https://github.com/sol/hpack
---
--- hash: ffc648678c84d8942a93a5168812a9a02db2b7471f0f42a0576f8409235cc6cd
+cabal-version: 2.2
+name: rattletrap
+version: 14.1.4
+synopsis: Parse and generate Rocket League replays.
+description:
+  Warning: This package is not maintained anymore.
+  .
+  Rattletrap parses and generates Rocket League replays.
 
-name:           rattletrap
-version:        3.1.2
-synopsis:       Parse and generate Rocket League replays.
-description:    Rattletrap parses and generates Rocket League replays.
-category:       Game
-homepage:       https://github.com/tfausak/rattletrap#readme
-bug-reports:    https://github.com/tfausak/rattletrap/issues
-maintainer:     Taylor Fausak
-license:        MIT
-license-file:   LICENSE.markdown
-build-type:     Simple
-cabal-version:  >= 1.10
+build-type: Simple
+category: Game
+extra-doc-files:
+  CHANGELOG.md
+  README.md
 
-extra-source-files:
-    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
+license-file: LICENSE.txt
+license: MIT
+maintainer: Taylor Fausak
 
 source-repository head
   type: git
   location: https://github.com/tfausak/rattletrap
 
+flag pedantic
+  default: False
+  manual: True
+
+common library
+  build-depends: base ^>=4.19.0.0 || ^>=4.20.0.0 || ^>=4.21.0.0
+  default-extensions: NamedFieldPuns
+  default-language: Haskell2010
+  ghc-options:
+    -Weverything
+    -Wno-all-missed-specialisations
+    -Wno-implicit-prelude
+    -Wno-missed-specialisations
+    -Wno-missing-deriving-strategies
+    -Wno-missing-export-lists
+    -Wno-missing-exported-signatures
+    -Wno-missing-import-lists
+    -Wno-missing-kind-signatures
+    -Wno-missing-role-annotations
+    -Wno-missing-safe-haskell-mode
+    -Wno-prepositive-qualified-module
+    -Wno-safe
+    -Wno-unsafe
+
+  if flag(pedantic)
+    ghc-options: -Werror
+
+common executable
+  import: library
+  build-depends: rattletrap
+  ghc-options:
+    -rtsopts
+    -threaded
+
 library
-  hs-source-dirs:
-      library
-  default-extensions: Strict
-  ghc-options: -Wall
+  import: library
+  autogen-modules: Paths_rattletrap
   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 ^>=2.1.2.1 || ^>=2.2.2.0,
+    aeson-pretty ^>=0.8.10,
+    array ^>=0.5.5.0,
+    bytestring ^>=0.11.4.0 || ^>=0.12.0.2,
+    containers ^>=0.6.7 || ^>=0.7,
+    filepath ^>=1.4.100.1 || ^>=1.5.2.0,
+    http-client ^>=0.7.17,
+    http-client-tls ^>=0.3.6.3,
+    text ^>=2.0.2 || ^>=2.1,
+
+  exposed-modules: Paths_rattletrap
+  -- cabal-gild: discover src/lib
   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.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
-  other-modules:
-      Paths_rattletrap
-  default-language: Haskell2010
+    Rattletrap
+    Rattletrap.BitBuilder
+    Rattletrap.BitGet
+    Rattletrap.BitPut
+    Rattletrap.BitString
+    Rattletrap.ByteGet
+    Rattletrap.BytePut
+    Rattletrap.Console.Config
+    Rattletrap.Console.Flag
+    Rattletrap.Console.Main
+    Rattletrap.Console.Mode
+    Rattletrap.Console.Option
+    Rattletrap.Data
+    Rattletrap.Exception.CrcMismatch
+    Rattletrap.Exception.Empty
+    Rattletrap.Exception.Fail
+    Rattletrap.Exception.InvalidComponent
+    Rattletrap.Exception.InvalidJson
+    Rattletrap.Exception.MissingAttributeLimit
+    Rattletrap.Exception.MissingAttributeName
+    Rattletrap.Exception.MissingClassName
+    Rattletrap.Exception.MissingObjectName
+    Rattletrap.Exception.MissingProductName
+    Rattletrap.Exception.NotEnoughInput
+    Rattletrap.Exception.UnknownActor
+    Rattletrap.Exception.UnknownAttribute
+    Rattletrap.Exception.UnknownName
+    Rattletrap.Exception.UnknownProduct
+    Rattletrap.Exception.UnknownProperty
+    Rattletrap.Exception.UnknownSystemId
+    Rattletrap.Get
+    Rattletrap.Schema
+    Rattletrap.Type.Attribute
+    Rattletrap.Type.Attribute.AppliedDamage
+    Rattletrap.Type.Attribute.Boolean
+    Rattletrap.Type.Attribute.Boost
+    Rattletrap.Type.Attribute.Byte
+    Rattletrap.Type.Attribute.CamSettings
+    Rattletrap.Type.Attribute.ClubColors
+    Rattletrap.Type.Attribute.CustomDemolish
+    Rattletrap.Type.Attribute.CustomDemolishExtended
+    Rattletrap.Type.Attribute.DamageState
+    Rattletrap.Type.Attribute.Demolish
+    Rattletrap.Type.Attribute.Enum
+    Rattletrap.Type.Attribute.Explosion
+    Rattletrap.Type.Attribute.ExtendedExplosion
+    Rattletrap.Type.Attribute.FlaggedByte
+    Rattletrap.Type.Attribute.FlaggedInt
+    Rattletrap.Type.Attribute.Float
+    Rattletrap.Type.Attribute.GameMode
+    Rattletrap.Type.Attribute.GameServer
+    Rattletrap.Type.Attribute.Int
+    Rattletrap.Type.Attribute.Int64
+    Rattletrap.Type.Attribute.Loadout
+    Rattletrap.Type.Attribute.LoadoutOnline
+    Rattletrap.Type.Attribute.Loadouts
+    Rattletrap.Type.Attribute.LoadoutsOnline
+    Rattletrap.Type.Attribute.Location
+    Rattletrap.Type.Attribute.MusicStinger
+    Rattletrap.Type.Attribute.PartyLeader
+    Rattletrap.Type.Attribute.Pickup
+    Rattletrap.Type.Attribute.PickupInfo
+    Rattletrap.Type.Attribute.PickupNew
+    Rattletrap.Type.Attribute.PlayerHistoryKey
+    Rattletrap.Type.Attribute.PrivateMatchSettings
+    Rattletrap.Type.Attribute.Product
+    Rattletrap.Type.Attribute.ProductValue
+    Rattletrap.Type.Attribute.QWord
+    Rattletrap.Type.Attribute.RepStatTitle
+    Rattletrap.Type.Attribute.Reservation
+    Rattletrap.Type.Attribute.RigidBodyState
+    Rattletrap.Type.Attribute.Rotation
+    Rattletrap.Type.Attribute.StatEvent
+    Rattletrap.Type.Attribute.String
+    Rattletrap.Type.Attribute.TeamPaint
+    Rattletrap.Type.Attribute.Title
+    Rattletrap.Type.Attribute.UniqueId
+    Rattletrap.Type.Attribute.WeldedInfo
+    Rattletrap.Type.AttributeMapping
+    Rattletrap.Type.AttributeType
+    Rattletrap.Type.AttributeValue
+    Rattletrap.Type.Cache
+    Rattletrap.Type.ClassAttributeMap
+    Rattletrap.Type.ClassMapping
+    Rattletrap.Type.CompressedWord
+    Rattletrap.Type.CompressedWordVector
+    Rattletrap.Type.Content
+    Rattletrap.Type.Dictionary
+    Rattletrap.Type.F32
+    Rattletrap.Type.Frame
+    Rattletrap.Type.Header
+    Rattletrap.Type.I32
+    Rattletrap.Type.I64
+    Rattletrap.Type.I8
+    Rattletrap.Type.Initialization
+    Rattletrap.Type.Int8Vector
+    Rattletrap.Type.Keyframe
+    Rattletrap.Type.List
+    Rattletrap.Type.Mark
+    Rattletrap.Type.Message
+    Rattletrap.Type.ObjectTarget
+    Rattletrap.Type.Property
+    Rattletrap.Type.Property.Array
+    Rattletrap.Type.Property.Bool
+    Rattletrap.Type.Property.Byte
+    Rattletrap.Type.Property.Float
+    Rattletrap.Type.Property.Int
+    Rattletrap.Type.Property.Name
+    Rattletrap.Type.Property.QWord
+    Rattletrap.Type.Property.Str
+    Rattletrap.Type.Property.Struct
+    Rattletrap.Type.PropertyValue
+    Rattletrap.Type.Quaternion
+    Rattletrap.Type.RemoteId
+    Rattletrap.Type.RemoteId.Epic
+    Rattletrap.Type.RemoteId.PlayStation
+    Rattletrap.Type.RemoteId.PsyNet
+    Rattletrap.Type.RemoteId.QQ
+    Rattletrap.Type.RemoteId.Splitscreen
+    Rattletrap.Type.RemoteId.Steam
+    Rattletrap.Type.RemoteId.Switch
+    Rattletrap.Type.RemoteId.Xbox
+    Rattletrap.Type.Replay
+    Rattletrap.Type.Replication
+    Rattletrap.Type.Replication.Destroyed
+    Rattletrap.Type.Replication.Spawned
+    Rattletrap.Type.Replication.Updated
+    Rattletrap.Type.ReplicationValue
+    Rattletrap.Type.Rotation
+    Rattletrap.Type.Section
+    Rattletrap.Type.Str
+    Rattletrap.Type.U32
+    Rattletrap.Type.U64
+    Rattletrap.Type.U8
+    Rattletrap.Type.Vector
+    Rattletrap.Type.Version
+    Rattletrap.Utility.Bytes
+    Rattletrap.Utility.Crc
+    Rattletrap.Utility.Helper
+    Rattletrap.Utility.Json
+    Rattletrap.Utility.Monad
+    Rattletrap.Version
 
+  hs-source-dirs: src/lib
+
 executable rattletrap
+  import: executable
+  hs-source-dirs: src/exe
   main-is: Main.hs
-  hs-source-dirs:
-      executables
-  ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
+
+test-suite rattletrap-test-suite
+  import: executable
   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
-    , rattletrap
-    , template-haskell >=2.12.0 && <2.13
-    , text >=1.2.2 && <1.3
-    , vector >=0.12.0 && <0.13
-  other-modules:
-      Paths_rattletrap
-  default-language: Haskell2010
+    bytestring,
+    filepath,
 
-test-suite test
-  type: exitcode-stdio-1.0
+  hs-source-dirs: src/test
   main-is: Main.hs
-  hs-source-dirs:
-      tests
-  ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
-  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
-    , 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
-  other-modules:
-      Paths_rattletrap
-  default-language: Haskell2010
+  type: exitcode-stdio-1.0
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/src/exe/Main.hs b/src/exe/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/exe/Main.hs
@@ -0,0 +1,4 @@
+import qualified Rattletrap
+
+main :: IO ()
+main = Rattletrap.main
diff --git a/src/lib/Rattletrap.hs b/src/lib/Rattletrap.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap.hs
@@ -0,0 +1,12 @@
+module Rattletrap
+  ( Rattletrap.Console.Main.main,
+    Rattletrap.Console.Main.rattletrap,
+    Rattletrap.Utility.Helper.decodeReplayFile,
+    Rattletrap.Utility.Helper.encodeReplayJson,
+    Rattletrap.Utility.Helper.decodeReplayJson,
+    Rattletrap.Utility.Helper.encodeReplayFile,
+  )
+where
+
+import qualified Rattletrap.Console.Main
+import qualified Rattletrap.Utility.Helper
diff --git a/src/lib/Rattletrap/BitBuilder.hs b/src/lib/Rattletrap/BitBuilder.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/BitBuilder.hs
@@ -0,0 +1,33 @@
+{- hlint ignore "Avoid restricted extensions" -}
+{-# LANGUAGE BangPatterns #-}
+
+module Rattletrap.BitBuilder where
+
+import qualified Data.Bits as Bits
+import qualified Data.ByteString.Builder as Builder
+import qualified Data.Word as Word
+
+data BitBuilder = BitBuilder
+  { buffer :: Word.Word8,
+    builder :: Builder.Builder,
+    offset :: Int
+  }
+
+empty :: BitBuilder
+empty = BitBuilder {buffer = 0x00, builder = mempty, offset = 0}
+
+push :: Bool -> BitBuilder -> BitBuilder
+push b x =
+  let !newBuffer = if b then Bits.setBit (buffer x) (offset x) else buffer x
+   in if offset x == 7
+        then
+          BitBuilder
+            { buffer = 0x00,
+              builder = builder x <> Builder.word8 newBuffer,
+              offset = 0
+            }
+        else x {buffer = newBuffer, offset = offset x + 1}
+
+toBuilder :: BitBuilder -> Builder.Builder
+toBuilder x =
+  if offset x == 0 then builder x else builder x <> Builder.word8 (buffer x)
diff --git a/src/lib/Rattletrap/BitGet.hs b/src/lib/Rattletrap/BitGet.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/BitGet.hs
@@ -0,0 +1,52 @@
+module Rattletrap.BitGet where
+
+import qualified Control.Exception as Exception
+import qualified Control.Monad as Monad
+import qualified Data.Bits as Bits
+import qualified Data.ByteString as ByteString
+import qualified Data.Functor.Identity as Identity
+import qualified Rattletrap.BitString as BitString
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.Exception.NotEnoughInput as NotEnoughInput
+import qualified Rattletrap.Get as Get
+
+type BitGet = Get.Get BitString.BitString Identity.Identity
+
+toByteGet :: BitGet a -> ByteGet.ByteGet a
+toByteGet g = do
+  s1 <- Get.get
+  case Identity.runIdentity . Get.run g $ BitString.fromByteString s1 of
+    Left (ls, e) -> Get.labels ls $ ByteGet.throw e
+    Right (s2, x) -> do
+      Get.put $ BitString.byteString s2
+      pure x
+
+fromByteGet :: ByteGet.ByteGet a -> Int -> BitGet a
+fromByteGet f n = do
+  x <- byteString n
+  Get.embed f x
+
+bits :: (Bits.Bits a) => Int -> BitGet a
+bits n = do
+  let f :: (Bits.Bits a) => Bool -> a -> a
+      f bit x = let y = Bits.shiftL x 1 in if bit then Bits.setBit y 0 else y
+  xs <- Monad.replicateM n bool
+  pure $ foldr f Bits.zeroBits xs
+
+bool :: BitGet Bool
+bool = do
+  s1 <- Get.get
+  case BitString.pop s1 of
+    Nothing -> throw NotEnoughInput.NotEnoughInput
+    Just (x, s2) -> do
+      Get.put s2
+      pure x
+
+byteString :: Int -> BitGet ByteString.ByteString
+byteString n = fmap ByteString.pack . Monad.replicateM n $ bits 8
+
+throw :: (Exception.Exception e) => e -> BitGet a
+throw = Get.throw
+
+label :: String -> BitGet a -> BitGet a
+label = Get.label
diff --git a/src/lib/Rattletrap/BitPut.hs b/src/lib/Rattletrap/BitPut.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/BitPut.hs
@@ -0,0 +1,32 @@
+module Rattletrap.BitPut where
+
+import qualified Data.Bits as Bits
+import qualified Data.ByteString as ByteString
+import qualified Rattletrap.BitBuilder as BitBuilder
+import qualified Rattletrap.BytePut as BytePut
+
+newtype BitPut = BitPut (BitBuilder.BitBuilder -> BitBuilder.BitBuilder)
+
+instance Semigroup BitPut where
+  f1 <> f2 = BitPut $ run f2 . run f1
+
+instance Monoid BitPut where
+  mempty = BitPut id
+
+run :: BitPut -> BitBuilder.BitBuilder -> BitBuilder.BitBuilder
+run (BitPut f) = f
+
+toBytePut :: BitPut -> BytePut.BytePut
+toBytePut b = BitBuilder.toBuilder $ run b BitBuilder.empty
+
+fromBytePut :: BytePut.BytePut -> BitPut
+fromBytePut = byteString . BytePut.toByteString
+
+bits :: (Bits.Bits a) => Int -> a -> BitPut
+bits n x = foldMap (bool . Bits.testBit x) [0 .. n - 1]
+
+bool :: Bool -> BitPut
+bool = BitPut . BitBuilder.push
+
+byteString :: ByteString.ByteString -> BitPut
+byteString = foldMap (bits 8) . ByteString.unpack
diff --git a/src/lib/Rattletrap/BitString.hs b/src/lib/Rattletrap/BitString.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/BitString.hs
@@ -0,0 +1,23 @@
+module Rattletrap.BitString where
+
+import qualified Data.Bits as Bits
+import qualified Data.ByteString as ByteString
+
+data BitString = BitString
+  { byteString :: ByteString.ByteString,
+    offset :: Int
+  }
+  deriving (Eq, Show)
+
+fromByteString :: ByteString.ByteString -> BitString
+fromByteString byteString = BitString {byteString, offset = 0}
+
+pop :: BitString -> Maybe (Bool, BitString)
+pop old = do
+  (word, byteString) <- ByteString.uncons $ byteString old
+  let bit = Bits.testBit word $ offset old
+      new =
+        if offset old == 7
+          then fromByteString byteString
+          else old {offset = offset old + 1}
+  pure (bit, new)
diff --git a/src/lib/Rattletrap/ByteGet.hs b/src/lib/Rattletrap/ByteGet.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/ByteGet.hs
@@ -0,0 +1,82 @@
+module Rattletrap.ByteGet where
+
+import qualified Control.Exception as Exception
+import qualified Data.Bits as Bits
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Lazy as LazyByteString
+import qualified Data.Functor.Identity as Identity
+import qualified Data.Int as Int
+import qualified Data.Word as Word
+import qualified GHC.Float as Float
+import qualified Rattletrap.Exception.NotEnoughInput as NotEnoughInput
+import qualified Rattletrap.Get as Get
+
+type ByteGet = Get.Get ByteString.ByteString Identity.Identity
+
+run ::
+  ByteGet a ->
+  ByteString.ByteString ->
+  Either ([String], Exception.SomeException) a
+run g = fmap snd . Identity.runIdentity . Get.run g
+
+byteString :: Int -> ByteGet ByteString.ByteString
+byteString n = do
+  s1 <- Get.get
+  let (x, s2) = ByteString.splitAt n s1
+  if ByteString.length x == n
+    then do
+      Get.put s2
+      pure x
+    else throw NotEnoughInput.NotEnoughInput
+
+float :: ByteGet Float
+float = fmap Float.castWord32ToFloat word32
+
+int8 :: ByteGet Int.Int8
+int8 = fmap fromIntegral word8
+
+int32 :: ByteGet Int.Int32
+int32 = fmap fromIntegral word32
+
+int64 :: ByteGet Int.Int64
+int64 = fmap fromIntegral word64
+
+remaining :: ByteGet LazyByteString.ByteString
+remaining = do
+  x <- Get.get
+  Get.put ByteString.empty
+  pure $ LazyByteString.fromStrict x
+
+word8 :: ByteGet Word.Word8
+word8 = fmap ByteString.head $ byteString 1
+
+word32 :: ByteGet Word.Word32
+word32 = do
+  x <- byteString 4
+  pure $
+    Bits.shiftL (fromIntegral $ ByteString.index x 0) 0
+      + Bits.shiftL (fromIntegral $ ByteString.index x 1) 8
+      + Bits.shiftL (fromIntegral $ ByteString.index x 2) 16
+      + Bits.shiftL (fromIntegral $ ByteString.index x 3) 24
+
+word64 :: ByteGet Word.Word64
+word64 = do
+  x <- byteString 8
+  pure $
+    Bits.shiftL (fromIntegral $ ByteString.index x 0) 0
+      + Bits.shiftL (fromIntegral $ ByteString.index x 1) 8
+      + Bits.shiftL (fromIntegral $ ByteString.index x 2) 16
+      + Bits.shiftL (fromIntegral $ ByteString.index x 3) 24
+      + Bits.shiftL (fromIntegral $ ByteString.index x 4) 32
+      + Bits.shiftL (fromIntegral $ ByteString.index x 5) 40
+      + Bits.shiftL (fromIntegral $ ByteString.index x 6) 48
+      + Bits.shiftL (fromIntegral $ ByteString.index x 7) 56
+
+throw :: (Exception.Exception e) => e -> ByteGet a
+throw = Get.throw
+
+embed :: ByteGet a -> ByteString.ByteString -> ByteGet a
+embed = Get.embed
+
+label :: String -> ByteGet a -> ByteGet a
+label = Get.label
diff --git a/src/lib/Rattletrap/BytePut.hs b/src/lib/Rattletrap/BytePut.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/BytePut.hs
@@ -0,0 +1,39 @@
+module Rattletrap.BytePut where
+
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Builder as Builder
+import qualified Data.ByteString.Lazy as LazyByteString
+import qualified Data.Int as Int
+import qualified Data.Word as Word
+
+type BytePut = Builder.Builder
+
+toByteString :: BytePut -> ByteString.ByteString
+toByteString = LazyByteString.toStrict . toLazyByteString
+
+toLazyByteString :: BytePut -> LazyByteString.ByteString
+toLazyByteString = Builder.toLazyByteString
+
+byteString :: ByteString.ByteString -> BytePut
+byteString = Builder.byteString
+
+float :: Float -> BytePut
+float = Builder.floatLE
+
+int8 :: Int.Int8 -> BytePut
+int8 = Builder.int8
+
+int32 :: Int.Int32 -> BytePut
+int32 = Builder.int32LE
+
+int64 :: Int.Int64 -> BytePut
+int64 = Builder.int64LE
+
+word8 :: Word.Word8 -> BytePut
+word8 = Builder.word8
+
+word32 :: Word.Word32 -> BytePut
+word32 = Builder.word32LE
+
+word64 :: Word.Word64 -> BytePut
+word64 = Builder.word64LE
diff --git a/src/lib/Rattletrap/Console/Config.hs b/src/lib/Rattletrap/Console/Config.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Console/Config.hs
@@ -0,0 +1,57 @@
+module Rattletrap.Console.Config where
+
+import qualified Rattletrap.Console.Flag as Flag
+import qualified Rattletrap.Console.Mode as Mode
+import qualified System.FilePath as FilePath
+
+data Config = Config
+  { compact :: Bool,
+    fast :: Bool,
+    help :: Bool,
+    input :: Maybe String,
+    mode :: Maybe Mode.Mode,
+    output :: Maybe String,
+    schema :: Bool,
+    skipCrc :: Bool,
+    version :: Bool
+  }
+  deriving (Eq, Show)
+
+initial :: Config
+initial =
+  Config
+    { compact = False,
+      fast = False,
+      help = False,
+      input = Nothing,
+      mode = Nothing,
+      output = Nothing,
+      schema = False,
+      skipCrc = False,
+      version = False
+    }
+
+applyFlag :: Config -> Flag.Flag -> Either String Config
+applyFlag config flag = case flag of
+  Flag.Compact -> Right config {compact = True}
+  Flag.Fast -> Right config {fast = True}
+  Flag.Help -> Right config {help = True}
+  Flag.Input x -> Right config {input = Just x}
+  Flag.Mode x -> do
+    y <- Mode.fromString x
+    Right config {mode = Just y}
+  Flag.Output x -> Right config {output = Just x}
+  Flag.Schema -> Right config {schema = True}
+  Flag.SkipCrc -> Right config {skipCrc = True}
+  Flag.Version -> Right config {version = True}
+
+getMode :: Config -> Mode.Mode
+getMode config =
+  let i = fmap FilePath.takeExtension $ input config
+      o = fmap FilePath.takeExtension $ output config
+   in case (i, o) of
+        (Just ".json", _) -> Mode.Encode
+        (Just ".replay", _) -> Mode.Decode
+        (_, Just ".json") -> Mode.Decode
+        (_, Just ".replay") -> Mode.Encode
+        _ -> Mode.Decode
diff --git a/src/lib/Rattletrap/Console/Flag.hs b/src/lib/Rattletrap/Console/Flag.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Console/Flag.hs
@@ -0,0 +1,13 @@
+module Rattletrap.Console.Flag where
+
+data Flag
+  = Compact
+  | Fast
+  | Help
+  | Input FilePath
+  | Mode String
+  | Output FilePath
+  | Schema
+  | SkipCrc
+  | Version
+  deriving (Eq, Show)
diff --git a/src/lib/Rattletrap/Console/Main.hs b/src/lib/Rattletrap/Console/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Console/Main.hs
@@ -0,0 +1,329 @@
+module Rattletrap.Console.Main where
+
+import qualified Control.Exception as Exception
+import qualified Control.Monad as Monad
+import qualified Data.Bool as Bool
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Lazy as LazyByteString
+import qualified Data.List as List
+import qualified Data.Text as Text
+import qualified Network.HTTP.Client as Client
+import qualified Network.HTTP.Client.TLS as Client
+import qualified Rattletrap.Console.Config as Config
+import qualified Rattletrap.Console.Mode as Mode
+import qualified Rattletrap.Console.Option as Option
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Attribute as Attribute
+import qualified Rattletrap.Type.Attribute.AppliedDamage as Attribute.AppliedDamage
+import qualified Rattletrap.Type.Attribute.Boolean as Attribute.Boolean
+import qualified Rattletrap.Type.Attribute.Boost as Attribute.Boost
+import qualified Rattletrap.Type.Attribute.Byte as Attribute.Byte
+import qualified Rattletrap.Type.Attribute.CamSettings as Attribute.CamSettings
+import qualified Rattletrap.Type.Attribute.ClubColors as Attribute.ClubColors
+import qualified Rattletrap.Type.Attribute.CustomDemolish as Attribute.CustomDemolish
+import qualified Rattletrap.Type.Attribute.CustomDemolishExtended as Attribute.CustomDemolishExtended
+import qualified Rattletrap.Type.Attribute.DamageState as Attribute.DamageState
+import qualified Rattletrap.Type.Attribute.Demolish as Attribute.Demolish
+import qualified Rattletrap.Type.Attribute.Enum as Attribute.Enum
+import qualified Rattletrap.Type.Attribute.Explosion as Attribute.Explosion
+import qualified Rattletrap.Type.Attribute.ExtendedExplosion as Attribute.ExtendedExplosion
+import qualified Rattletrap.Type.Attribute.FlaggedByte as Attribute.FlaggedByte
+import qualified Rattletrap.Type.Attribute.FlaggedInt as Attribute.FlaggedInt
+import qualified Rattletrap.Type.Attribute.Float as Attribute.Float
+import qualified Rattletrap.Type.Attribute.GameMode as Attribute.GameMode
+import qualified Rattletrap.Type.Attribute.GameServer as Attribute.GameServer
+import qualified Rattletrap.Type.Attribute.Int as Attribute.Int
+import qualified Rattletrap.Type.Attribute.Int64 as Attribute.Int64
+import qualified Rattletrap.Type.Attribute.Loadout as Attribute.Loadout
+import qualified Rattletrap.Type.Attribute.LoadoutOnline as Attribute.LoadoutOnline
+import qualified Rattletrap.Type.Attribute.Loadouts as Attribute.Loadouts
+import qualified Rattletrap.Type.Attribute.LoadoutsOnline as Attribute.LoadoutsOnline
+import qualified Rattletrap.Type.Attribute.Location as Attribute.Location
+import qualified Rattletrap.Type.Attribute.MusicStinger as Attribute.MusicStinger
+import qualified Rattletrap.Type.Attribute.PartyLeader as Attribute.PartyLeader
+import qualified Rattletrap.Type.Attribute.Pickup as Attribute.Pickup
+import qualified Rattletrap.Type.Attribute.PickupInfo as Attribute.PickupInfo
+import qualified Rattletrap.Type.Attribute.PickupNew as Attribute.PickupNew
+import qualified Rattletrap.Type.Attribute.PlayerHistoryKey as Attribute.PlayerHistoryKey
+import qualified Rattletrap.Type.Attribute.PrivateMatchSettings as Attribute.PrivateMatchSettings
+import qualified Rattletrap.Type.Attribute.Product as Attribute.Product
+import qualified Rattletrap.Type.Attribute.ProductValue as Attribute.ProductValue
+import qualified Rattletrap.Type.Attribute.QWord as Attribute.QWord
+import qualified Rattletrap.Type.Attribute.RepStatTitle as Attribute.RepStatTitle
+import qualified Rattletrap.Type.Attribute.Reservation as Attribute.Reservation
+import qualified Rattletrap.Type.Attribute.RigidBodyState as Attribute.RigidBodyState
+import qualified Rattletrap.Type.Attribute.Rotation as Attribute.Rotation
+import qualified Rattletrap.Type.Attribute.StatEvent as Attribute.StatEvent
+import qualified Rattletrap.Type.Attribute.String as Attribute.String
+import qualified Rattletrap.Type.Attribute.TeamPaint as Attribute.TeamPaint
+import qualified Rattletrap.Type.Attribute.Title as Attribute.Title
+import qualified Rattletrap.Type.Attribute.UniqueId as Attribute.UniqueId
+import qualified Rattletrap.Type.Attribute.WeldedInfo as Attribute.WeldedInfo
+import qualified Rattletrap.Type.AttributeMapping as AttributeMapping
+import qualified Rattletrap.Type.AttributeValue as AttributeValue
+import qualified Rattletrap.Type.Cache as Cache
+import qualified Rattletrap.Type.ClassMapping as ClassMapping
+import qualified Rattletrap.Type.CompressedWord as CompressedWord
+import qualified Rattletrap.Type.CompressedWordVector as CompressedWordVector
+import qualified Rattletrap.Type.Content as Content
+import qualified Rattletrap.Type.Dictionary as Dictionary
+import qualified Rattletrap.Type.F32 as F32
+import qualified Rattletrap.Type.Frame as Frame
+import qualified Rattletrap.Type.Header as Header
+import qualified Rattletrap.Type.I32 as I32
+import qualified Rattletrap.Type.I64 as I64
+import qualified Rattletrap.Type.I8 as I8
+import qualified Rattletrap.Type.Initialization as Initialization
+import qualified Rattletrap.Type.Int8Vector as Int8Vector
+import qualified Rattletrap.Type.Keyframe as Keyframe
+import qualified Rattletrap.Type.List as RList
+import qualified Rattletrap.Type.Mark as Mark
+import qualified Rattletrap.Type.Message as Message
+import qualified Rattletrap.Type.ObjectTarget as ObjectTarget
+import qualified Rattletrap.Type.Property as Property
+import qualified Rattletrap.Type.Property.Array as Property.Array
+import qualified Rattletrap.Type.Property.Byte as Property.Byte
+import qualified Rattletrap.Type.Property.Struct as Property.Struct
+import qualified Rattletrap.Type.PropertyValue as PropertyValue
+import qualified Rattletrap.Type.Quaternion as Quaternion
+import qualified Rattletrap.Type.RemoteId as RemoteId
+import qualified Rattletrap.Type.RemoteId.PlayStation as RemoteId.PlayStation
+import qualified Rattletrap.Type.RemoteId.PsyNet as RemoteId.PsyNet
+import qualified Rattletrap.Type.RemoteId.Switch as RemoteId.Switch
+import qualified Rattletrap.Type.Replay as Replay
+import qualified Rattletrap.Type.Replication as Replication
+import qualified Rattletrap.Type.Replication.Destroyed as Replication.Destroyed
+import qualified Rattletrap.Type.Replication.Spawned as Replication.Spawned
+import qualified Rattletrap.Type.Replication.Updated as Replication.Updated
+import qualified Rattletrap.Type.ReplicationValue as ReplicationValue
+import qualified Rattletrap.Type.Rotation as Rotation
+import qualified Rattletrap.Type.Section as Section
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.U64 as U64
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Type.Vector as Vector
+import qualified Rattletrap.Utility.Helper as Rattletrap
+import qualified Rattletrap.Utility.Json as Json
+import qualified Rattletrap.Version as Version
+import qualified System.Console.GetOpt as Console
+import qualified System.Environment as Environment
+import qualified System.Exit as Exit
+import qualified System.FilePath as FilePath
+import qualified System.IO as IO
+
+main :: IO ()
+main = do
+  name <- Environment.getProgName
+  arguments <- Environment.getArgs
+  rattletrap name arguments
+
+rattletrap :: String -> [String] -> IO ()
+rattletrap name arguments = do
+  config <- getConfig arguments
+  if Config.help config
+    then helpMain name
+    else
+      if Config.version config
+        then versionMain
+        else
+          if Config.schema config
+            then schemaMain config
+            else defaultMain name config
+
+helpMain :: String -> IO ()
+helpMain name = do
+  putStr $
+    Console.usageInfo
+      (unwords [name, "version", Version.string])
+      Option.all
+
+versionMain :: IO ()
+versionMain = do
+  putStrLn Version.string
+
+schemaMain :: Config.Config -> IO ()
+schemaMain config = putOutput config $ encodeJson config schema
+
+defaultMain :: String -> Config.Config -> IO ()
+defaultMain name config = do
+  input <- getInput name config
+  let decode = getDecoder config
+  replay <- case decode input of
+    Left (ls, e) -> do
+      IO.hPutStr IO.stderr $
+        unlines
+          [ "ERROR: " <> Exception.displayException e,
+            "-- Context: " <> List.intercalate ", " ls,
+            "-- You are using Rattletrap version " <> Version.string,
+            "-- " <> show config,
+            "-- Please report this problem at https://github.com/tfausak/rattletrap/issues/new"
+          ]
+      Exit.exitFailure
+    Right x -> pure x
+  let encode = getEncoder config
+  putOutput config (encode replay)
+
+schema :: Json.Value
+schema =
+  let contentSchema = Content.schema $ RList.schema Frame.schema
+   in Json.object
+        [ Json.pair "$schema" "http://json-schema.org/draft-07/schema",
+          Json.pair "$id" Replay.schemaUrl,
+          Json.pair "$ref" "#/definitions/replay",
+          Json.pair "definitions" . Json.object $
+            fmap
+              (\s -> Json.pair (Text.unpack $ Schema.name s) $ Schema.json s)
+              [ Attribute.schema,
+                Attribute.AppliedDamage.schema,
+                Attribute.Boolean.schema,
+                Attribute.Boost.schema,
+                Attribute.Byte.schema,
+                Attribute.CamSettings.schema,
+                Attribute.ClubColors.schema,
+                Attribute.CustomDemolish.schema,
+                Attribute.CustomDemolishExtended.schema,
+                Attribute.DamageState.schema,
+                Attribute.Demolish.schema,
+                Attribute.Enum.schema,
+                Attribute.Explosion.schema,
+                Attribute.ExtendedExplosion.schema,
+                Attribute.FlaggedByte.schema,
+                Attribute.FlaggedInt.schema,
+                Attribute.Float.schema,
+                Attribute.GameMode.schema,
+                Attribute.GameServer.schema,
+                Attribute.Int.schema,
+                Attribute.Int64.schema,
+                Attribute.Loadout.schema,
+                Attribute.LoadoutOnline.schema,
+                Attribute.Loadouts.schema,
+                Attribute.LoadoutsOnline.schema,
+                Attribute.Location.schema,
+                Attribute.MusicStinger.schema,
+                Attribute.PartyLeader.schema,
+                Attribute.Pickup.schema,
+                Attribute.PickupInfo.schema,
+                Attribute.PickupNew.schema,
+                Attribute.PlayerHistoryKey.schema,
+                Attribute.PrivateMatchSettings.schema,
+                Attribute.Product.schema,
+                Attribute.ProductValue.schema,
+                Attribute.QWord.schema,
+                Attribute.RepStatTitle.schema,
+                Attribute.Reservation.schema,
+                Attribute.RigidBodyState.schema,
+                Attribute.Rotation.schema,
+                Attribute.StatEvent.schema,
+                Attribute.String.schema,
+                Attribute.TeamPaint.schema,
+                Attribute.Title.schema,
+                Attribute.UniqueId.schema,
+                Attribute.WeldedInfo.schema,
+                AttributeMapping.schema,
+                AttributeValue.schema,
+                Cache.schema,
+                ClassMapping.schema,
+                CompressedWord.schema,
+                CompressedWordVector.schema,
+                contentSchema,
+                Dictionary.elementSchema Property.schema,
+                Dictionary.schema Property.schema,
+                F32.schema,
+                Frame.schema,
+                Header.schema,
+                I32.schema,
+                I64.schema,
+                I8.schema,
+                Initialization.schema,
+                Int8Vector.schema,
+                Keyframe.schema,
+                RList.schema Attribute.Product.schema,
+                Mark.schema,
+                Message.schema,
+                ObjectTarget.schema,
+                Property.schema,
+                Property.Array.schema Property.schema,
+                Property.Byte.schema,
+                Property.Struct.schema Property.schema,
+                PropertyValue.schema Property.schema,
+                Quaternion.schema,
+                RemoteId.schema,
+                RemoteId.PlayStation.schema,
+                RemoteId.PsyNet.schema,
+                RemoteId.Switch.schema,
+                Replay.schema (Section.schema Header.schema)
+                  . Section.schema
+                  $ contentSchema,
+                Replication.Destroyed.schema,
+                Replication.schema,
+                Replication.Spawned.schema,
+                Replication.Updated.schema,
+                ReplicationValue.schema,
+                Rotation.schema,
+                Schema.boolean,
+                Schema.integer,
+                Schema.null,
+                Schema.number,
+                Schema.string,
+                Section.schema contentSchema,
+                Section.schema Header.schema,
+                Str.schema,
+                U32.schema,
+                U64.schema,
+                U8.schema,
+                Vector.schema
+              ]
+        ]
+
+getDecoder ::
+  Config.Config ->
+  ByteString.ByteString ->
+  Either ([String], Exception.SomeException) Replay.Replay
+getDecoder config = case Config.getMode config of
+  Mode.Decode ->
+    Rattletrap.decodeReplayFile (Config.fast config) (Config.skipCrc config)
+  Mode.Encode -> Rattletrap.decodeReplayJson
+
+getEncoder :: Config.Config -> Replay.Replay -> LazyByteString.ByteString
+getEncoder config = case Config.getMode config of
+  Mode.Decode -> encodeJson config
+  Mode.Encode -> Rattletrap.encodeReplayFile $ Config.fast config
+
+getInput :: String -> Config.Config -> IO ByteString.ByteString
+getInput name config = case Config.input config of
+  Nothing -> do
+    isTerminalDevice <- IO.hIsTerminalDevice IO.stdin
+    Monad.when isTerminalDevice . IO.hPutStr IO.stderr $
+      unlines
+        [ "-- You did not supply any input, so Rattletrap will read from STDIN.",
+          "-- If that is unexpected, try running: "
+            <> FilePath.combine "." name
+            <> " --help"
+        ]
+    ByteString.getContents
+  Just fileOrUrl -> case Client.parseUrlThrow fileOrUrl of
+    Nothing -> ByteString.readFile fileOrUrl
+    Just request -> do
+      manager <- Client.newTlsManager
+      response <- Client.httpLbs request manager
+      pure (LazyByteString.toStrict (Client.responseBody response))
+
+putOutput :: Config.Config -> LazyByteString.ByteString -> IO ()
+putOutput =
+  maybe LazyByteString.putStr LazyByteString.writeFile . Config.output
+
+encodeJson :: (Json.ToJSON a) => Config.Config -> a -> LazyByteString.ByteString
+encodeJson = Bool.bool Json.encodePretty Json.encode . Config.compact
+
+getConfig :: [String] -> IO Config.Config
+getConfig arguments = do
+  let (flags, unexpectedArguments, unknownOptions, problems) =
+        Console.getOpt' Console.Permute Option.all arguments
+  Monad.forM_ unexpectedArguments $ \x ->
+    IO.hPutStrLn IO.stderr $ "WARNING: unexpected argument `" <> x <> "'"
+  Monad.forM_ unknownOptions $
+    \x -> IO.hPutStrLn IO.stderr $ "WARNING: unknown option `" <> x <> "'"
+  Monad.forM_ problems $ \x -> IO.hPutStr IO.stderr $ "ERROR: " <> x
+  Monad.unless (null problems) Exit.exitFailure
+  either fail pure $ Monad.foldM Config.applyFlag Config.initial flags
diff --git a/src/lib/Rattletrap/Console/Mode.hs b/src/lib/Rattletrap/Console/Mode.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Console/Mode.hs
@@ -0,0 +1,12 @@
+module Rattletrap.Console.Mode where
+
+data Mode
+  = Decode
+  | Encode
+  deriving (Eq, Show)
+
+fromString :: String -> Either String Mode
+fromString string = case string of
+  "decode" -> Right Decode
+  "encode" -> Right Encode
+  _ -> Left $ "invalid mode: " <> show string
diff --git a/src/lib/Rattletrap/Console/Option.hs b/src/lib/Rattletrap/Console/Option.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Console/Option.hs
@@ -0,0 +1,68 @@
+module Rattletrap.Console.Option where
+
+import qualified Rattletrap.Console.Flag as Flag
+import qualified System.Console.GetOpt as Console
+
+type Option = Console.OptDescr Flag.Flag
+
+all :: [Option]
+all = [compact, fast, help, input, mode, output, schema, skipCrc, version]
+
+compact :: Option
+compact =
+  Console.Option
+    ['c']
+    ["compact"]
+    (Console.NoArg Flag.Compact)
+    "minify JSON output"
+
+fast :: Option
+fast =
+  Console.Option
+    ['f']
+    ["fast"]
+    (Console.NoArg Flag.Fast)
+    "only encode or decode the header"
+
+help :: Option
+help = Console.Option ['h'] ["help"] (Console.NoArg Flag.Help) "show the help"
+
+input :: Option
+input =
+  Console.Option
+    ['i']
+    ["input"]
+    (Console.ReqArg Flag.Input "FILE|URL")
+    "input file or URL"
+
+mode :: Option
+mode =
+  Console.Option
+    ['m']
+    ["mode"]
+    (Console.ReqArg Flag.Mode "MODE")
+    "decode or encode"
+
+output :: Option
+output =
+  Console.Option
+    ['o']
+    ["output"]
+    (Console.ReqArg Flag.Output "FILE")
+    "output file"
+
+schema :: Option
+schema =
+  Console.Option [] ["schema"] (Console.NoArg Flag.Schema) "output the schema"
+
+skipCrc :: Option
+skipCrc =
+  Console.Option [] ["skip-crc"] (Console.NoArg Flag.SkipCrc) "skip the CRC"
+
+version :: Option
+version =
+  Console.Option
+    ['v']
+    ["version"]
+    (Console.NoArg Flag.Version)
+    "show the version"
diff --git a/src/lib/Rattletrap/Data.hs b/src/lib/Rattletrap/Data.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Data.hs
@@ -0,0 +1,469 @@
+-- brittany --columns 120
+module Rattletrap.Data where
+
+import qualified Data.Bifunctor as Bifunctor
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+import qualified Data.Text as Text
+import qualified Rattletrap.Type.AttributeType as AttributeType
+
+parentClasses :: Map.Map Text.Text Text.Text
+parentClasses =
+  Map.fromList $
+    fmap
+      (Bifunctor.bimap Text.pack Text.pack)
+      [ ("Engine.Actor", "Core.Object"),
+        ("Engine.GameReplicationInfo", "Engine.ReplicationInfo"),
+        ("Engine.Info", "Engine.Actor"),
+        ("Engine.Pawn", "Engine.Actor"),
+        ("Engine.PlayerReplicationInfo", "Engine.ReplicationInfo"),
+        ("Engine.ReplicationInfo", "Engine.Info"),
+        ("Engine.TeamInfo", "Engine.ReplicationInfo"),
+        ("ProjectX.GRI_X", "Engine.GameReplicationInfo"),
+        ("ProjectX.Pawn_X", "Engine.Pawn"),
+        ("ProjectX.PRI_X", "Engine.PlayerReplicationInfo"),
+        ("TAGame.Ball_God_TA", "TAGame.Ball_TA"),
+        ("TAGame.Ball_Haunted_TA", "TAGame.Ball_TA"),
+        ("TAGame.Ball_TA", "TAGame.RBActor_TA"),
+        ("TAGame.CameraSettingsActor_TA", "Engine.ReplicationInfo"),
+        ("TAGame.Car_Season_TA", "TAGame.PRI_TA"),
+        ("TAGame.Car_TA", "TAGame.Vehicle_TA"),
+        ("TAGame.CarComponent_Boost_TA", "TAGame.CarComponent_TA"),
+        ("TAGame.CarComponent_Dodge_TA", "TAGame.CarComponent_TA"),
+        ("TAGame.CarComponent_DoubleJump_TA", "TAGame.CarComponent_TA"),
+        ("TAGame.CarComponent_FlipCar_TA", "TAGame.CarComponent_TA"),
+        ("TAGame.CarComponent_Jump_TA", "TAGame.CarComponent_TA"),
+        ("TAGame.CarComponent_TA", "Engine.ReplicationInfo"),
+        ("TAGame.CrowdActor_TA", "Engine.ReplicationInfo"),
+        ("TAGame.CrowdManager_TA", "Engine.ReplicationInfo"),
+        ("TAGame.GameEvent_Football_TA", "TAGame.GameEvent_Soccar_TA"),
+        ("TAGame.GameEvent_GodBall_TA", "TAGame.GameEvent_Soccar_TA"),
+        ("TAGame.GameEvent_Season_TA", "TAGame.GameEvent_Soccar_TA"),
+        ("TAGame.GameEvent_Soccar_TA", "TAGame.GameEvent_Team_TA"),
+        ("TAGame.GameEvent_SoccarPrivate_TA", "TAGame.GameEvent_Soccar_TA"),
+        ("TAGame.GameEvent_SoccarSplitscreen_TA", "TAGame.GameEvent_SoccarPrivate_TA"),
+        ("TAGame.GameEvent_TA", "Engine.ReplicationInfo"),
+        ("TAGame.GameEvent_Team_TA", "TAGame.GameEvent_TA"),
+        ("TAGame.GRI_TA", "ProjectX.GRI_X"),
+        ("TAGame.HauntedBallTrapTrigger_TA", "Engine.Actor"),
+        ("TAGame.InMapScoreboard_TA", "Engine.Actor"),
+        ("TAGame.PRI_TA", "ProjectX.PRI_X"),
+        ("TAGame.RBActor_TA", "ProjectX.Pawn_X"),
+        ("TAGame.SpecialPickup_BallCarSpring_TA", "TAGame.SpecialPickup_Spring_TA"),
+        ("TAGame.SpecialPickup_BallFreeze_TA", "TAGame.SpecialPickup_Targeted_TA"),
+        ("TAGame.SpecialPickup_BallGravity_TA", "TAGame.SpecialPickup_TA"),
+        ("TAGame.SpecialPickup_BallLasso_TA", "TAGame.SpecialPickup_GrapplingHook_TA"),
+        ("TAGame.SpecialPickup_BallVelcro_TA", "TAGame.SpecialPickup_TA"),
+        ("TAGame.SpecialPickup_Batarang_TA", "TAGame.SpecialPickup_BallLasso_TA"),
+        ("TAGame.SpecialPickup_BoostOverride_TA", "TAGame.SpecialPickup_Targeted_TA"),
+        ("TAGame.SpecialPickup_Football_TA", "TAGame.SpecialPickup_TA"),
+        ("TAGame.SpecialPickup_GrapplingHook_TA", "TAGame.SpecialPickup_Targeted_TA"),
+        ("TAGame.SpecialPickup_HauntedBallBeam_TA", "TAGame.SpecialPickup_TA"),
+        ("TAGame.SpecialPickup_HitForce_TA", "TAGame.SpecialPickup_TA"),
+        ("TAGame.SpecialPickup_Rugby_TA", "TAGame.SpecialPickup_TA"),
+        ("TAGame.SpecialPickup_Spring_TA", "TAGame.SpecialPickup_Targeted_TA"),
+        ("TAGame.SpecialPickup_Swapper_TA", "TAGame.SpecialPickup_Targeted_TA"),
+        ("TAGame.SpecialPickup_TA", "TAGame.CarComponent_TA"),
+        ("TAGame.SpecialPickup_Targeted_TA", "TAGame.SpecialPickup_TA"),
+        ("TAGame.SpecialPickup_Tornado_TA", "TAGame.SpecialPickup_TA"),
+        ("TAGame.Team_Soccar_TA", "TAGame.Team_TA"),
+        ("TAGame.Team_TA", "Engine.TeamInfo"),
+        ("TAGame.Vehicle_TA", "TAGame.RBActor_TA"),
+        ("TAGame.VehiclePickup_Boost_TA", "TAGame.VehiclePickup_TA"),
+        ("TAGame.VehiclePickup_TA", "Engine.ReplicationInfo")
+      ]
+
+classesWithLocation :: Set.Set Text.Text
+classesWithLocation =
+  Set.fromList $
+    fmap
+      Text.pack
+      [ "Archetypes.Ball.Ball_BasketBall_Mutator",
+        "Archetypes.Ball.Ball_Basketball",
+        "Archetypes.Ball.Ball_BasketBall",
+        "Archetypes.Ball.Ball_Breakout",
+        "Archetypes.Ball.Ball_Default",
+        "Archetypes.Ball.Ball_Puck",
+        "Archetypes.Ball.Ball_Trajectory",
+        "Archetypes.Ball.CubeBall",
+        "Archetypes.Car.Car_Default",
+        "Archetypes.GameEvent.GameEvent_Season:CarArchetype",
+        "Archetypes.SpecialPickups.SpecialPickup_Rugby",
+        "ProjectX.NetModeReplicator",
+        "TAGame.Ball_Breakout_TA",
+        "TAGame.Ball_God_TA",
+        "TAGame.Ball_Haunted_TA",
+        "TAGame.Ball_TA",
+        "TAGame.CameraSettingsActor_TA",
+        "TAGame.Cannon_TA",
+        "TAGame.Car_Season_TA",
+        "TAGame.Car_TA",
+        "TAGame.CarComponent_Boost_TA",
+        "TAGame.CarComponent_Dodge_TA",
+        "TAGame.CarComponent_DoubleJump_TA",
+        "TAGame.CarComponent_FlipCar_TA",
+        "TAGame.CarComponent_Jump_TA",
+        "TAGame.Default__CameraSettingsActor_TA",
+        "TAGame.Default__PRI_TA",
+        "TAGame.GameEvent_Football_TA",
+        "TAGame.GameEvent_FTE_TA",
+        "TAGame.GameEvent_GodBall_TA",
+        "TAGame.GameEvent_Season_TA",
+        "TAGame.GameEvent_Soccar_TA",
+        "TAGame.GameEvent_SoccarPrivate_TA",
+        "TAGame.GameEvent_SoccarSplitscreen_TA",
+        "TAGame.GameEvent_Tutorial_TA",
+        "TAGame.GRI_TA",
+        "TAGame.MaxTimeWarningData_TA",
+        "TAGame.PickupTimer_TA",
+        "TAGame.PRI_TA",
+        "TAGame.RumblePickups_TA",
+        "TAGame.SpecialPickup_BallCarSpring_TA",
+        "TAGame.SpecialPickup_BallFreeze_TA",
+        "TAGame.SpecialPickup_BallGravity_TA",
+        "TAGame.SpecialPickup_BallLasso_TA",
+        "TAGame.SpecialPickup_BallVelcro_TA",
+        "TAGame.SpecialPickup_Batarang_TA",
+        "TAGame.SpecialPickup_BoostOverride_TA",
+        "TAGame.SpecialPickup_Football_TA",
+        "TAGame.SpecialPickup_GrapplingHook_TA",
+        "TAGame.SpecialPickup_HauntedBallBeam_TA",
+        "TAGame.SpecialPickup_HitForce_TA",
+        "TAGame.SpecialPickup_Rugby_TA",
+        "TAGame.SpecialPickup_Swapper_TA",
+        "TAGame.SpecialPickup_Tornado_TA",
+        "TAGame.Team_Soccar_TA",
+        "TheWorld:PersistentLevel.BreakOutActor_Platform_TA",
+        "TheWorld:PersistentLevel.CrowdActor_TA",
+        "TheWorld:PersistentLevel.CrowdManager_TA",
+        "TheWorld:PersistentLevel.InMapScoreboard_TA",
+        "TheWorld:PersistentLevel.VehiclePickup_Boost_TA"
+      ]
+
+classesWithRotation :: Set.Set Text.Text
+classesWithRotation =
+  Set.fromList $
+    fmap
+      Text.pack
+      [ "Archetypes.Ball.Ball_BasketBall_Mutator",
+        "Archetypes.Ball.Ball_Basketball",
+        "Archetypes.Ball.Ball_BasketBall",
+        "Archetypes.Ball.Ball_Breakout",
+        "Archetypes.Ball.Ball_Default",
+        "Archetypes.Ball.Ball_Puck",
+        "Archetypes.Ball.Ball_Trajectory",
+        "Archetypes.Ball.CubeBall",
+        "Archetypes.Car.Car_Default",
+        "Archetypes.GameEvent.GameEvent_Season:CarArchetype",
+        "Archetypes.SpecialPickups.SpecialPickup_Rugby",
+        "TAGame.Ball_Breakout_TA",
+        "TAGame.Ball_God_TA",
+        "TAGame.Ball_Haunted_TA",
+        "TAGame.Ball_TA",
+        "TAGame.Car_Season_TA",
+        "TAGame.Car_TA"
+      ]
+
+objectClasses :: Map.Map Text.Text Text.Text
+objectClasses =
+  Map.fromList $
+    fmap
+      (Bifunctor.bimap Text.pack Text.pack)
+      [ ("Archetypes.Ball.Ball_Anniversary", "TAGame.Ball_TA"),
+        ("Archetypes.Ball.Ball_BasketBall_Mutator", "TAGame.Ball_TA"),
+        ("Archetypes.Ball.Ball_Basketball", "TAGame.Ball_TA"),
+        ("Archetypes.Ball.Ball_BasketBall", "TAGame.Ball_TA"),
+        ("Archetypes.Ball.Ball_Beachball", "TAGame.Ball_TA"),
+        ("Archetypes.Ball.Ball_Breakout", "TAGame.Ball_Breakout_TA"),
+        ("Archetypes.Ball.Ball_Default", "TAGame.Ball_TA"),
+        ("Archetypes.Ball.Ball_Football", "TAGame.Ball_TA"),
+        ("Archetypes.Ball.Ball_God", "TAGame.Ball_God_TA"),
+        ("Archetypes.Ball.Ball_Haunted", "TAGame.Ball_Haunted_TA"),
+        ("Archetypes.Ball.Ball_Puck", "TAGame.Ball_TA"),
+        ("Archetypes.Ball.Ball_Training", "TAGame.Ball_TA"),
+        ("Archetypes.Ball.Ball_Trajectory", "TAGame.Ball_TA"),
+        ("Archetypes.Ball.CubeBall", "TAGame.Ball_TA"),
+        ("Archetypes.Car.Car_Default", "TAGame.Car_TA"),
+        ("Archetypes.Car.Car_PostGameLobby", "TAGame.Car_TA"),
+        ("Archetypes.CarComponents.CarComponent_Boost", "TAGame.CarComponent_Boost_TA"),
+        ("Archetypes.CarComponents.CarComponent_Dodge", "TAGame.CarComponent_Dodge_TA"),
+        ("Archetypes.CarComponents.CarComponent_DoubleJump", "TAGame.CarComponent_DoubleJump_TA"),
+        ("Archetypes.CarComponents.CarComponent_FlipCar", "TAGame.CarComponent_FlipCar_TA"),
+        ("Archetypes.CarComponents.CarComponent_Jump", "TAGame.CarComponent_Jump_TA"),
+        ("Archetypes.GameEvent.GameEvent_Basketball", "TAGame.GameEvent_Soccar_TA"),
+        ("Archetypes.GameEvent.GameEvent_BasketballPrivate", "TAGame.GameEvent_SoccarPrivate_TA"),
+        ("Archetypes.GameEvent.GameEvent_BasketballSplitscreen", "TAGame.GameEvent_SoccarSplitscreen_TA"),
+        ("Archetypes.GameEvent.GameEvent_Breakout", "TAGame.GameEvent_Soccar_TA"),
+        ("Archetypes.GameEvent.GameEvent_FTE_Part1_Prime", "TAGame.GameEvent_FTE_TA"),
+        ("Archetypes.GameEvent.GameEvent_Hockey", "TAGame.GameEvent_Soccar_TA"),
+        ("Archetypes.GameEvent.GameEvent_HockeyPrivate", "TAGame.GameEvent_SoccarPrivate_TA"),
+        ("Archetypes.GameEvent.GameEvent_HockeySplitscreen", "TAGame.GameEvent_SoccarSplitscreen_TA"),
+        ("Archetypes.GameEvent.GameEvent_Items", "TAGame.GameEvent_Soccar_TA"),
+        ("Archetypes.GameEvent.GameEvent_Season:CarArchetype", "TAGame.Car_TA"),
+        ("Archetypes.GameEvent.GameEvent_Season", "TAGame.GameEvent_Season_TA"),
+        ("Archetypes.GameEvent.GameEvent_Soccar", "TAGame.GameEvent_Soccar_TA"),
+        ("Archetypes.GameEvent.GameEvent_SoccarLan", "TAGame.GameEvent_Soccar_TA"),
+        ("Archetypes.GameEvent.GameEvent_SoccarPrivate", "TAGame.GameEvent_SoccarPrivate_TA"),
+        ("Archetypes.GameEvent.GameEvent_SoccarSplitscreen", "TAGame.GameEvent_SoccarSplitscreen_TA"),
+        ("Archetypes.SpecialPickups.SpecialPickup_BallFreeze", "TAGame.SpecialPickup_BallFreeze_TA"),
+        ("Archetypes.SpecialPickups.SpecialPickup_BallGrapplingHook", "TAGame.SpecialPickup_GrapplingHook_TA"),
+        ("Archetypes.SpecialPickups.SpecialPickup_BallLasso", "TAGame.SpecialPickup_BallLasso_TA"),
+        ("Archetypes.SpecialPickups.SpecialPickup_BallSpring", "TAGame.SpecialPickup_BallCarSpring_TA"),
+        ("Archetypes.SpecialPickups.SpecialPickup_BallVelcro", "TAGame.SpecialPickup_BallVelcro_TA"),
+        ("Archetypes.SpecialPickups.SpecialPickup_Batarang", "TAGame.SpecialPickup_Batarang_TA"),
+        ("Archetypes.SpecialPickups.SpecialPickup_BoostOverride", "TAGame.SpecialPickup_BoostOverride_TA"),
+        ("Archetypes.SpecialPickups.SpecialPickup_CarSpring", "TAGame.SpecialPickup_BallCarSpring_TA"),
+        ("Archetypes.SpecialPickups.SpecialPickup_Football", "TAGame.SpecialPickup_Football_TA"),
+        ("Archetypes.SpecialPickups.SpecialPickup_GravityWell", "TAGame.SpecialPickup_BallGravity_TA"),
+        ("Archetypes.SpecialPickups.SpecialPickup_HauntedBallBeam", "TAGame.SpecialPickup_HauntedBallBeam_TA"),
+        ("Archetypes.SpecialPickups.SpecialPickup_Rugby", "TAGame.SpecialPickup_Rugby_TA"),
+        ("Archetypes.SpecialPickups.SpecialPickup_StrongHit", "TAGame.SpecialPickup_HitForce_TA"),
+        ("Archetypes.SpecialPickups.SpecialPickup_Swapper", "TAGame.SpecialPickup_Swapper_TA"),
+        ("Archetypes.SpecialPickups.SpecialPickup_Tornado", "TAGame.SpecialPickup_Tornado_TA"),
+        ("Archetypes.Teams.Team0", "TAGame.Team_Soccar_TA"),
+        ("Archetypes.Teams.Team1", "TAGame.Team_Soccar_TA"),
+        ("Archetypes.Tutorial.Cannon", "TAGame.Cannon_TA"),
+        ("GameInfo_Basketball.GameInfo.GameInfo_Basketball:GameReplicationInfoArchetype", "TAGame.GRI_TA"),
+        ("GameInfo_Breakout.GameInfo.GameInfo_Breakout:GameReplicationInfoArchetype", "TAGame.GRI_TA"),
+        ("GameInfo_FootBall.GameInfo.GameInfo_FootBall:Archetype", "TAGame.GameEvent_Football_TA"),
+        ("GameInfo_FootBall.GameInfo.GameInfo_FootBall:GameReplicationInfoArchetype", "TAGame.GRI_TA"),
+        ("GameInfo_FTE.GameInfo.GameInfo_FTE:GameReplicationInfoArchetype", "TAGame.GRI_TA"),
+        ("gameinfo_godball.GameInfo.gameinfo_godball:Archetype", "TAGame.GameEvent_GodBall_TA"),
+        ("GameInfo_GodBall.GameInfo.GameInfo_GodBall:Archetype", "TAGame.GameEvent_GodBall_TA"),
+        ("gameinfo_godball.GameInfo.gameinfo_godball:GameReplicationInfoArchetype", "TAGame.GRI_TA"),
+        ("GameInfo_GodBall.GameInfo.GameInfo_GodBall:GameReplicationInfoArchetype", "TAGame.GRI_TA"),
+        ("Gameinfo_Hockey.GameInfo.Gameinfo_Hockey:GameReplicationInfoArchetype", "TAGame.GRI_TA"),
+        ("GameInfo_Items.GameInfo.GameInfo_Items:GameReplicationInfoArchetype", "TAGame.GRI_TA"),
+        ("GameInfo_Season.GameInfo.GameInfo_Season:GameReplicationInfoArchetype", "TAGame.GRI_TA"),
+        ("GameInfo_Soccar.GameInfo.GameInfo_Soccar:GameReplicationInfoArchetype", "TAGame.GRI_TA"),
+        ("GameInfo_Tutorial.GameEvent.GameEvent_Tutorial_Aerial", "TAGame.GameEvent_Tutorial_TA"),
+        ("GameInfo_Tutorial.GameInfo.GameInfo_Tutorial:GameReplicationInfoArchetype", "TAGame.GRI_TA"),
+        ("Haunted_TrainStation_P.TheWorld:PersistentLevel.HauntedBallTrapTrigger_TA_0", "TAGame.HauntedBallTrapTrigger_TA"),
+        ("Haunted_TrainStation_P.TheWorld:PersistentLevel.HauntedBallTrapTrigger_TA_1", "TAGame.HauntedBallTrapTrigger_TA"),
+        ("ProjectX.Default__NetModeReplicator_X", "ProjectX.NetModeReplicator"),
+        ("TAGame.Default__CameraSettingsActor_TA", "TAGame.CameraSettingsActor_TA"),
+        ("TAGame.Default__MaxTimeWarningData_TA", "TAGame.MaxTimeWarningData_TA"),
+        ("TAGame.Default__PickupTimer_TA", "TAGame.PickupTimer_TA"),
+        ("TAGame.Default__PRI_TA", "TAGame.PRI_TA"),
+        ("TAGame.Default__RumblePickups_TA", "TAGame.RumblePickups_TA"),
+        ("TheWorld:PersistentLevel.BreakOutActor_Platform_TA", "TAGame.BreakOutActor_Platform_TA"),
+        ("TheWorld:PersistentLevel.CrowdActor_TA", "TAGame.CrowdActor_TA"),
+        ("TheWorld:PersistentLevel.CrowdManager_TA", "TAGame.CrowdManager_TA"),
+        ("TheWorld:PersistentLevel.InMapScoreboard_TA", "TAGame.InMapScoreboard_TA"),
+        ("TheWorld:PersistentLevel.VehiclePickup_Boost_TA", "TAGame.VehiclePickup_Boost_TA")
+      ]
+
+attributeTypes :: Map.Map Text.Text AttributeType.AttributeType
+attributeTypes =
+  Map.fromList $
+    fmap
+      (Bifunctor.first Text.pack)
+      [ ("Engine.Actor:bBlockActors", AttributeType.Boolean),
+        ("Engine.Actor:bCollideActors", AttributeType.Boolean),
+        ("Engine.Actor:bHidden", AttributeType.Boolean),
+        ("Engine.Actor:bTearOff", AttributeType.Boolean),
+        ("Engine.Actor:DrawScale", AttributeType.Float),
+        ("Engine.Actor:RemoteRole", AttributeType.Enum),
+        ("Engine.Actor:Role", AttributeType.Enum),
+        ("Engine.Actor:Rotation", AttributeType.Rotation),
+        ("Engine.GameReplicationInfo:bMatchIsOver", AttributeType.Boolean),
+        ("Engine.GameReplicationInfo:GameClass", AttributeType.FlaggedInt),
+        ("Engine.GameReplicationInfo:ServerName", AttributeType.String),
+        ("Engine.Pawn:HealthMax", AttributeType.Int),
+        ("Engine.Pawn:PlayerReplicationInfo", AttributeType.FlaggedInt),
+        ("Engine.PlayerReplicationInfo:bBot", AttributeType.Boolean),
+        ("Engine.PlayerReplicationInfo:bIsSpectator", AttributeType.Boolean),
+        ("Engine.PlayerReplicationInfo:bReadyToPlay", AttributeType.Boolean),
+        ("Engine.PlayerReplicationInfo:bTimedOut", AttributeType.Boolean),
+        ("Engine.PlayerReplicationInfo:bWaitingPlayer", AttributeType.Boolean),
+        ("Engine.PlayerReplicationInfo:Ping", AttributeType.Byte),
+        ("Engine.PlayerReplicationInfo:PlayerID", AttributeType.Int),
+        ("Engine.PlayerReplicationInfo:PlayerName", AttributeType.String),
+        ("Engine.PlayerReplicationInfo:RemoteUserData", AttributeType.String),
+        ("Engine.PlayerReplicationInfo:Score", AttributeType.Int),
+        ("Engine.PlayerReplicationInfo:Team", AttributeType.FlaggedInt),
+        ("Engine.PlayerReplicationInfo:UniqueId", AttributeType.UniqueId),
+        ("Engine.ReplicatedActor_ORS:ReplicatedOwner", AttributeType.FlaggedInt),
+        ("Engine.TeamInfo:Score", AttributeType.Int),
+        ("ProjectX.GRI_X:bGameStarted", AttributeType.Boolean),
+        ("ProjectX.GRI_X:GameServerID", AttributeType.GameServer),
+        ("ProjectX.GRI_X:MatchGuid", AttributeType.String),
+        ("ProjectX.GRI_X:MatchGUID", AttributeType.String),
+        ("ProjectX.GRI_X:ReplicatedGameMutatorIndex", AttributeType.Int),
+        ("ProjectX.GRI_X:ReplicatedGamePlaylist", AttributeType.Int),
+        ("ProjectX.GRI_X:ReplicatedServerRegion", AttributeType.String),
+        ("ProjectX.GRI_X:Reservations", AttributeType.Reservation),
+        ("TAGame.Ball_Breakout_TA:AppliedDamage", AttributeType.AppliedDamage),
+        ("TAGame.Ball_Breakout_TA:DamageIndex", AttributeType.Int),
+        ("TAGame.Ball_Breakout_TA:LastTeamTouch", AttributeType.Byte),
+        ("TAGame.Ball_God_TA:TargetSpeed", AttributeType.Float),
+        ("TAGame.Ball_Haunted_TA:bIsBallBeamed", AttributeType.Boolean),
+        ("TAGame.Ball_Haunted_TA:DeactivatedGoalIndex", AttributeType.Byte),
+        ("TAGame.Ball_Haunted_TA:LastTeamTouch", AttributeType.Byte),
+        ("TAGame.Ball_Haunted_TA:ReplicatedBeamBrokenValue", AttributeType.Byte),
+        ("TAGame.Ball_Haunted_TA:TotalActiveBeams", AttributeType.Byte),
+        ("TAGame.Ball_TA:GameEvent", AttributeType.FlaggedInt),
+        ("TAGame.Ball_TA:HitTeamNum", AttributeType.Byte),
+        ("TAGame.Ball_TA:ReplicatedAddedCarBounceScale", AttributeType.Float),
+        ("TAGame.Ball_TA:ReplicatedBallMaxLinearSpeedScale", AttributeType.Float),
+        ("TAGame.Ball_TA:ReplicatedBallScale", AttributeType.Float),
+        ("TAGame.Ball_TA:ReplicatedExplosionData", AttributeType.Explosion),
+        ("TAGame.Ball_TA:ReplicatedExplosionDataExtended", AttributeType.ExtendedExplosion),
+        ("TAGame.Ball_TA:ReplicatedPhysMatOverride", AttributeType.FlaggedInt),
+        ("TAGame.Ball_TA:ReplicatedWorldBounceScale", AttributeType.Float),
+        ("TAGame.BreakOutActor_Platform_TA:DamageState", AttributeType.DamageState),
+        ("TAGame.CameraSettingsActor_TA:bMouseCameraToggleEnabled", AttributeType.Boolean),
+        ("TAGame.CameraSettingsActor_TA:bUsingBehindView", AttributeType.Boolean),
+        ("TAGame.CameraSettingsActor_TA:bUsingSecondaryCamera", AttributeType.Boolean),
+        ("TAGame.CameraSettingsActor_TA:bUsingSwivel", AttributeType.Boolean),
+        ("TAGame.CameraSettingsActor_TA:CameraPitch", AttributeType.Byte),
+        ("TAGame.CameraSettingsActor_TA:CameraYaw", AttributeType.Byte),
+        ("TAGame.CameraSettingsActor_TA:PRI", AttributeType.FlaggedInt),
+        ("TAGame.CameraSettingsActor_TA:ProfileSettings", AttributeType.CamSettings),
+        ("TAGame.Cannon_TA:FireCount", AttributeType.Byte),
+        ("TAGame.Cannon_TA:Pitch", AttributeType.Float),
+        ("TAGame.Car_TA:AddedBallForceMultiplier", AttributeType.Float),
+        ("TAGame.Car_TA:AddedCarForceMultiplier", AttributeType.Float),
+        ("TAGame.Car_TA:AttachedPickup", AttributeType.FlaggedInt),
+        ("TAGame.Car_TA:ClubColors", AttributeType.ClubColors),
+        ("TAGame.Car_TA:ReplicatedCarScale", AttributeType.Float),
+        ("TAGame.Car_TA:ReplicatedDemolish_CustomFX", AttributeType.CustomDemolish),
+        ("TAGame.Car_TA:ReplicatedDemolish", AttributeType.Demolish),
+        ("TAGame.Car_TA:ReplicatedDemolishExtended", AttributeType.CustomDemolishExtended),
+        ("TAGame.Car_TA:ReplicatedDemolishGoalExplosion", AttributeType.CustomDemolish),
+        ("TAGame.Car_TA:RumblePickups", AttributeType.FlaggedInt),
+        ("TAGame.Car_TA:TeamPaint", AttributeType.TeamPaint),
+        ("TAGame.CarComponent_Boost_TA:bNoBoost", AttributeType.Boolean),
+        ("TAGame.CarComponent_Boost_TA:BoostModifier", AttributeType.Float),
+        ("TAGame.CarComponent_Boost_TA:bUnlimitedBoost", AttributeType.Boolean),
+        ("TAGame.CarComponent_Boost_TA:RechargeDelay", AttributeType.Float),
+        ("TAGame.CarComponent_Boost_TA:RechargeRate", AttributeType.Float),
+        ("TAGame.CarComponent_Boost_TA:ReplicatedBoost", AttributeType.Boost),
+        ("TAGame.CarComponent_Boost_TA:ReplicatedBoostAmount", AttributeType.Byte),
+        ("TAGame.CarComponent_Boost_TA:UnlimitedBoostRefCount", AttributeType.Int),
+        ("TAGame.CarComponent_Dodge_TA:DodgeImpulse", AttributeType.Location),
+        ("TAGame.CarComponent_Dodge_TA:DodgeTorque", AttributeType.Location),
+        ("TAGame.CarComponent_DoubleJump_TA:DoubleJumpImpulse", AttributeType.Location),
+        ("TAGame.CarComponent_FlipCar_TA:bFlipRight", AttributeType.Boolean),
+        ("TAGame.CarComponent_FlipCar_TA:FlipCarTime", AttributeType.Float),
+        ("TAGame.CarComponent_TA:ReplicatedActive", AttributeType.Byte),
+        ("TAGame.CarComponent_TA:ReplicatedActivityTime", AttributeType.Float),
+        ("TAGame.CarComponent_TA:Vehicle", AttributeType.FlaggedInt),
+        ("TAGame.CrowdActor_TA:GameEvent", AttributeType.FlaggedInt),
+        ("TAGame.CrowdActor_TA:ModifiedNoise", AttributeType.Float),
+        ("TAGame.CrowdActor_TA:ReplicatedCountDownNumber", AttributeType.Int),
+        ("TAGame.CrowdActor_TA:ReplicatedOneShotSound", AttributeType.FlaggedInt),
+        ("TAGame.CrowdActor_TA:ReplicatedRoundCountDownNumber", AttributeType.Int),
+        ("TAGame.CrowdManager_TA:GameEvent", AttributeType.FlaggedInt),
+        ("TAGame.CrowdManager_TA:ReplicatedGlobalOneShotSound", AttributeType.FlaggedInt),
+        ("TAGame.GameEvent_Soccar_TA:bBallHasBeenHit", AttributeType.Boolean),
+        ("TAGame.GameEvent_Soccar_TA:bClubMatch", AttributeType.Boolean),
+        ("TAGame.GameEvent_Soccar_TA:bMatchEnded", AttributeType.Boolean),
+        ("TAGame.GameEvent_Soccar_TA:bNoContest", AttributeType.Boolean),
+        ("TAGame.GameEvent_Soccar_TA:bOverTime", AttributeType.Boolean),
+        ("TAGame.GameEvent_Soccar_TA:bUnlimitedTime", AttributeType.Boolean),
+        ("TAGame.GameEvent_Soccar_TA:GameTime", AttributeType.Int),
+        ("TAGame.GameEvent_Soccar_TA:GameWinner", AttributeType.FlaggedInt),
+        ("TAGame.GameEvent_Soccar_TA:MatchWinner", AttributeType.FlaggedInt),
+        ("TAGame.GameEvent_Soccar_TA:MaxScore", AttributeType.Int),
+        ("TAGame.GameEvent_Soccar_TA:MVP", AttributeType.FlaggedInt),
+        ("TAGame.GameEvent_Soccar_TA:ReplicatedMusicStinger", AttributeType.MusicStinger),
+        ("TAGame.GameEvent_Soccar_TA:ReplicatedScoredOnTeam", AttributeType.Byte),
+        ("TAGame.GameEvent_Soccar_TA:ReplicatedServerPerformanceState", AttributeType.Byte),
+        ("TAGame.GameEvent_Soccar_TA:ReplicatedStatEvent", AttributeType.StatEvent),
+        ("TAGame.GameEvent_Soccar_TA:RoundNum", AttributeType.Int),
+        ("TAGame.GameEvent_Soccar_TA:SecondsRemaining", AttributeType.Int),
+        ("TAGame.GameEvent_Soccar_TA:SeriesLength", AttributeType.Int),
+        ("TAGame.GameEvent_Soccar_TA:SubRulesArchetype", AttributeType.FlaggedInt),
+        ("TAGame.GameEvent_SoccarPrivate_TA:MatchSettings", AttributeType.PrivateMatchSettings),
+        ("TAGame.GameEvent_TA:bAllowReadyUp", AttributeType.Boolean),
+        ("TAGame.GameEvent_TA:bCanVoteToForfeit", AttributeType.Boolean),
+        ("TAGame.GameEvent_TA:bHasLeaveMatchPenalty", AttributeType.Boolean),
+        ("TAGame.GameEvent_TA:BotSkill", AttributeType.Int),
+        ("TAGame.GameEvent_TA:GameMode", AttributeType.GameMode),
+        ("TAGame.GameEvent_TA:MatchStartEpoch", AttributeType.Int64),
+        ("TAGame.GameEvent_TA:MatchTypeClass", AttributeType.FlaggedInt),
+        ("TAGame.GameEvent_TA:ReplicatedGameStateTimeRemaining", AttributeType.Int),
+        ("TAGame.GameEvent_TA:ReplicatedRoundCountDownNumber", AttributeType.Int),
+        ("TAGame.GameEvent_TA:ReplicatedStateIndex", AttributeType.Byte),
+        ("TAGame.GameEvent_TA:ReplicatedStateName", AttributeType.Int),
+        ("TAGame.GameEvent_Team_TA:bForfeit", AttributeType.Boolean),
+        ("TAGame.GameEvent_Team_TA:MaxTeamSize", AttributeType.Int),
+        ("TAGame.GRI_TA:NewDedicatedServerIP", AttributeType.String),
+        ("TAGame.MaxTimeWarningData_TA:EndGameEpochTime", AttributeType.Int64),
+        ("TAGame.MaxTimeWarningData_TA:EndGameWarningEpochTime", AttributeType.Int64),
+        ("TAGame.PRI_TA:bIsDistracted", AttributeType.Boolean),
+        ("TAGame.PRI_TA:bIsInSplitScreen", AttributeType.Boolean),
+        ("TAGame.PRI_TA:bMatchMVP", AttributeType.Boolean),
+        ("TAGame.PRI_TA:bOnlineLoadoutSet", AttributeType.Boolean),
+        ("TAGame.PRI_TA:bOnlineLoadoutsSet", AttributeType.Boolean),
+        ("TAGame.PRI_TA:BotProductName", AttributeType.Int),
+        ("TAGame.PRI_TA:bReady", AttributeType.Boolean),
+        ("TAGame.PRI_TA:bUsingBehindView", AttributeType.Boolean),
+        ("TAGame.PRI_TA:bUsingItems", AttributeType.Boolean),
+        ("TAGame.PRI_TA:bUsingSecondaryCamera", AttributeType.Boolean),
+        ("TAGame.PRI_TA:CameraPitch", AttributeType.Byte),
+        ("TAGame.PRI_TA:CameraSettings", AttributeType.CamSettings),
+        ("TAGame.PRI_TA:CameraYaw", AttributeType.Byte),
+        ("TAGame.PRI_TA:ClientLoadout", AttributeType.Loadout),
+        ("TAGame.PRI_TA:ClientLoadoutOnline", AttributeType.LoadoutOnline),
+        ("TAGame.PRI_TA:ClientLoadouts", AttributeType.Loadouts),
+        ("TAGame.PRI_TA:ClientLoadoutsOnline", AttributeType.LoadoutsOnline),
+        ("TAGame.PRI_TA:ClubID", AttributeType.Int64),
+        ("TAGame.PRI_TA:CurrentVoiceRoom", AttributeType.String),
+        ("TAGame.PRI_TA:MatchAssists", AttributeType.Int),
+        ("TAGame.PRI_TA:MatchBreakoutDamage", AttributeType.Int),
+        ("TAGame.PRI_TA:MatchGoals", AttributeType.Int),
+        ("TAGame.PRI_TA:MatchSaves", AttributeType.Int),
+        ("TAGame.PRI_TA:MatchScore", AttributeType.Int),
+        ("TAGame.PRI_TA:MatchShots", AttributeType.Int),
+        ("TAGame.PRI_TA:MaxTimeTillItem", AttributeType.Int),
+        ("TAGame.PRI_TA:PartyLeader", AttributeType.PartyLeader),
+        ("TAGame.PRI_TA:PawnType", AttributeType.Byte),
+        ("TAGame.PRI_TA:PersistentCamera", AttributeType.FlaggedInt),
+        ("TAGame.PRI_TA:PlayerHistoryKey", AttributeType.PlayerHistoryKey),
+        ("TAGame.PRI_TA:PlayerHistoryValid", AttributeType.Boolean),
+        ("TAGame.PRI_TA:PrimaryTitle", AttributeType.Title),
+        ("TAGame.PRI_TA:ReplicatedGameEvent", AttributeType.FlaggedInt),
+        ("TAGame.PRI_TA:ReplicatedWorstNetQualityBeyondLatency", AttributeType.Byte),
+        ("TAGame.PRI_TA:RepStatTitles", AttributeType.RepStatTitle),
+        ("TAGame.PRI_TA:SecondaryTitle", AttributeType.Title),
+        ("TAGame.PRI_TA:SkillTier", AttributeType.FlaggedByte),
+        ("TAGame.PRI_TA:SpectatorShortcut", AttributeType.Int),
+        ("TAGame.PRI_TA:SteeringSensitivity", AttributeType.Float),
+        ("TAGame.PRI_TA:TimeTillItem", AttributeType.Int),
+        ("TAGame.PRI_TA:Title", AttributeType.Int),
+        ("TAGame.PRI_TA:TotalXP", AttributeType.Int),
+        ("TAGame.RBActor_TA:bFrozen", AttributeType.Boolean),
+        ("TAGame.RBActor_TA:bIgnoreSyncing", AttributeType.Boolean),
+        ("TAGame.RBActor_TA:bReplayActor", AttributeType.Boolean),
+        ("TAGame.RBActor_TA:ReplicatedRBState", AttributeType.RigidBodyState),
+        ("TAGame.RBActor_TA:WeldedInfo", AttributeType.WeldedInfo),
+        ("TAGame.RumblePickups_TA:AttachedPickup", AttributeType.FlaggedInt),
+        ("TAGame.RumblePickups_TA:ConcurrentItemCount", AttributeType.Int),
+        ("TAGame.RumblePickups_TA:PickupInfo", AttributeType.PickupInfo),
+        ("TAGame.SpecialPickup_BallFreeze_TA:RepOrigSpeed", AttributeType.Float),
+        ("TAGame.SpecialPickup_BallVelcro_TA:AttachTime", AttributeType.Float),
+        ("TAGame.SpecialPickup_BallVelcro_TA:bBroken", AttributeType.Boolean),
+        ("TAGame.SpecialPickup_BallVelcro_TA:bHit", AttributeType.Boolean),
+        ("TAGame.SpecialPickup_BallVelcro_TA:BreakTime", AttributeType.Float),
+        ("TAGame.SpecialPickup_Football_TA:WeldedBall", AttributeType.FlaggedInt),
+        ("TAGame.SpecialPickup_Rugby_TA:bBallWelded", AttributeType.Boolean),
+        ("TAGame.SpecialPickup_Targeted_TA:Targeted", AttributeType.FlaggedInt),
+        ("TAGame.Team_Soccar_TA:GameScore", AttributeType.Int),
+        ("TAGame.Team_TA:ClubColors", AttributeType.ClubColors),
+        ("TAGame.Team_TA:ClubID", AttributeType.Int64),
+        ("TAGame.Team_TA:CustomTeamName", AttributeType.String),
+        ("TAGame.Team_TA:Difficulty", AttributeType.Int),
+        ("TAGame.Team_TA:GameEvent", AttributeType.FlaggedInt),
+        ("TAGame.Team_TA:LogoData", AttributeType.FlaggedInt),
+        ("TAGame.Vehicle_TA:bDriving", AttributeType.Boolean),
+        ("TAGame.Vehicle_TA:bHasPostMatchCelebration", AttributeType.Boolean),
+        ("TAGame.Vehicle_TA:bPodiumMode", AttributeType.Boolean),
+        ("TAGame.Vehicle_TA:bReplicatedHandbrake", AttributeType.Boolean),
+        ("TAGame.Vehicle_TA:ReplicatedSteer", AttributeType.Byte),
+        ("TAGame.Vehicle_TA:ReplicatedThrottle", AttributeType.Byte),
+        ("TAGame.VehiclePickup_TA:bNoPickup", AttributeType.Boolean),
+        ("TAGame.VehiclePickup_TA:NewReplicatedPickupData", AttributeType.PickupNew),
+        ("TAGame.VehiclePickup_TA:ReplicatedPickupData", AttributeType.Pickup)
+      ]
diff --git a/src/lib/Rattletrap/Exception/CrcMismatch.hs b/src/lib/Rattletrap/Exception/CrcMismatch.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/CrcMismatch.hs
@@ -0,0 +1,11 @@
+module Rattletrap.Exception.CrcMismatch where
+
+import qualified Control.Exception as Exception
+import qualified Data.Word as Word
+
+data CrcMismatch = CrcMismatch Word.Word32 Word.Word32
+  deriving (Eq, Show)
+
+instance Exception.Exception CrcMismatch where
+  displayException (CrcMismatch x y) =
+    unwords ["invalid CRC: expected", show x, "but got", show y]
diff --git a/src/lib/Rattletrap/Exception/Empty.hs b/src/lib/Rattletrap/Exception/Empty.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/Empty.hs
@@ -0,0 +1,8 @@
+module Rattletrap.Exception.Empty where
+
+import qualified Control.Exception as Exception
+
+data Empty = Empty
+  deriving (Eq, Show)
+
+instance Exception.Exception Empty
diff --git a/src/lib/Rattletrap/Exception/Fail.hs b/src/lib/Rattletrap/Exception/Fail.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/Fail.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Exception.Fail where
+
+import qualified Control.Exception as Exception
+
+newtype Fail
+  = Fail String
+  deriving (Eq, Show)
+
+instance Exception.Exception Fail where
+  displayException (Fail x) = x
diff --git a/src/lib/Rattletrap/Exception/InvalidComponent.hs b/src/lib/Rattletrap/Exception/InvalidComponent.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/InvalidComponent.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.InvalidComponent where
+
+import qualified Control.Exception as Exception
+
+newtype InvalidComponent
+  = InvalidComponent Word
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidComponent
diff --git a/src/lib/Rattletrap/Exception/InvalidJson.hs b/src/lib/Rattletrap/Exception/InvalidJson.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/InvalidJson.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.InvalidJson where
+
+import qualified Control.Exception as Exception
+
+newtype InvalidJson
+  = InvalidJson String
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidJson
diff --git a/src/lib/Rattletrap/Exception/MissingAttributeLimit.hs b/src/lib/Rattletrap/Exception/MissingAttributeLimit.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/MissingAttributeLimit.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.MissingAttributeLimit where
+
+import qualified Control.Exception as Exception
+
+newtype MissingAttributeLimit
+  = MissingAttributeLimit Word
+  deriving (Eq, Show)
+
+instance Exception.Exception MissingAttributeLimit
diff --git a/src/lib/Rattletrap/Exception/MissingAttributeName.hs b/src/lib/Rattletrap/Exception/MissingAttributeName.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/MissingAttributeName.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.MissingAttributeName where
+
+import qualified Control.Exception as Exception
+
+newtype MissingAttributeName
+  = MissingAttributeName Word
+  deriving (Eq, Show)
+
+instance Exception.Exception MissingAttributeName
diff --git a/src/lib/Rattletrap/Exception/MissingClassName.hs b/src/lib/Rattletrap/Exception/MissingClassName.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/MissingClassName.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.MissingClassName where
+
+import qualified Control.Exception as Exception
+
+newtype MissingClassName
+  = MissingClassName String
+  deriving (Eq, Show)
+
+instance Exception.Exception MissingClassName
diff --git a/src/lib/Rattletrap/Exception/MissingObjectName.hs b/src/lib/Rattletrap/Exception/MissingObjectName.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/MissingObjectName.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Exception.MissingObjectName where
+
+import qualified Control.Exception as Exception
+import qualified Data.Word as Word
+
+newtype MissingObjectName
+  = MissingObjectName Word.Word32
+  deriving (Eq, Show)
+
+instance Exception.Exception MissingObjectName
diff --git a/src/lib/Rattletrap/Exception/MissingProductName.hs b/src/lib/Rattletrap/Exception/MissingProductName.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/MissingProductName.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Exception.MissingProductName where
+
+import qualified Control.Exception as Exception
+import qualified Data.Word as Word
+
+newtype MissingProductName
+  = MissingProductName Word.Word32
+  deriving (Eq, Show)
+
+instance Exception.Exception MissingProductName
diff --git a/src/lib/Rattletrap/Exception/NotEnoughInput.hs b/src/lib/Rattletrap/Exception/NotEnoughInput.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/NotEnoughInput.hs
@@ -0,0 +1,8 @@
+module Rattletrap.Exception.NotEnoughInput where
+
+import qualified Control.Exception as Exception
+
+data NotEnoughInput = NotEnoughInput
+  deriving (Eq, Show)
+
+instance Exception.Exception NotEnoughInput
diff --git a/src/lib/Rattletrap/Exception/UnknownActor.hs b/src/lib/Rattletrap/Exception/UnknownActor.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/UnknownActor.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.UnknownActor where
+
+import qualified Control.Exception as Exception
+
+newtype UnknownActor
+  = UnknownActor Word
+  deriving (Eq, Show)
+
+instance Exception.Exception UnknownActor
diff --git a/src/lib/Rattletrap/Exception/UnknownAttribute.hs b/src/lib/Rattletrap/Exception/UnknownAttribute.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/UnknownAttribute.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.UnknownAttribute where
+
+import qualified Control.Exception as Exception
+
+newtype UnknownAttribute
+  = UnknownAttribute String
+  deriving (Eq, Show)
+
+instance Exception.Exception UnknownAttribute
diff --git a/src/lib/Rattletrap/Exception/UnknownName.hs b/src/lib/Rattletrap/Exception/UnknownName.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/UnknownName.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Exception.UnknownName where
+
+import qualified Control.Exception as Exception
+import qualified Data.Word as Word
+
+newtype UnknownName
+  = UnknownName Word.Word32
+  deriving (Eq, Show)
+
+instance Exception.Exception UnknownName
diff --git a/src/lib/Rattletrap/Exception/UnknownProduct.hs b/src/lib/Rattletrap/Exception/UnknownProduct.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/UnknownProduct.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.UnknownProduct where
+
+import qualified Control.Exception as Exception
+
+newtype UnknownProduct
+  = UnknownProduct String
+  deriving (Eq, Show)
+
+instance Exception.Exception UnknownProduct
diff --git a/src/lib/Rattletrap/Exception/UnknownProperty.hs b/src/lib/Rattletrap/Exception/UnknownProperty.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/UnknownProperty.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.UnknownProperty where
+
+import qualified Control.Exception as Exception
+
+newtype UnknownProperty
+  = UnknownProperty String
+  deriving (Eq, Show)
+
+instance Exception.Exception UnknownProperty
diff --git a/src/lib/Rattletrap/Exception/UnknownSystemId.hs b/src/lib/Rattletrap/Exception/UnknownSystemId.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/UnknownSystemId.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Exception.UnknownSystemId where
+
+import qualified Control.Exception as Exception
+import qualified Data.Word as Word
+
+newtype UnknownSystemId
+  = UnknownSystemId Word.Word8
+  deriving (Eq, Show)
+
+instance Exception.Exception UnknownSystemId
diff --git a/src/lib/Rattletrap/Get.hs b/src/lib/Rattletrap/Get.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Get.hs
@@ -0,0 +1,68 @@
+module Rattletrap.Get where
+
+import qualified Control.Applicative as Applicative
+import qualified Control.Exception as Exception
+import qualified Data.Bifunctor as Bifunctor
+import qualified Rattletrap.Exception.Empty as Empty
+import qualified Rattletrap.Exception.Fail as Fail
+
+newtype Get s m a = Get (s -> m (Either ([String], Exception.SomeException) (s, a)))
+
+instance (Functor m) => Functor (Get s m) where
+  fmap f g = Get $ fmap (fmap (fmap f)) . run g
+
+instance (Monad m) => Applicative (Get s m) where
+  pure x = Get $ \s -> pure $ Right (s, x)
+
+  gf <*> gx = Get $ \s1 -> do
+    r <- run gf s1
+    case r of
+      Left e -> pure $ Left e
+      Right (s2, f) -> run (fmap f gx) s2
+
+instance (Monad m) => Monad (Get s m) where
+  g >>= f = Get $ \s1 -> do
+    r <- run g s1
+    case r of
+      Left e -> pure $ Left e
+      Right (s2, x) -> run (f x) s2
+
+instance (Monad m) => MonadFail (Get s m) where
+  fail = throw . Fail.Fail
+
+instance (Monad m) => Applicative.Alternative (Get s m) where
+  empty = throw Empty.Empty
+
+  gx <|> gy = Get $ \s -> do
+    r <- run gx s
+    case r of
+      Left _ -> run gy s
+      Right x -> pure $ Right x
+
+run :: Get s m a -> s -> m (Either ([String], Exception.SomeException) (s, a))
+run (Get f) = f
+
+get :: (Applicative m) => Get s m s
+get = Get $ \s -> pure $ Right (s, s)
+
+put :: (Applicative m) => s -> Get s m ()
+put s = Get $ \_ -> pure $ Right (s, ())
+
+lift :: (Functor m) => m a -> Get s m a
+lift m = Get $ \s -> fmap (\x -> Right (s, x)) m
+
+throw :: (Exception.Exception e, Applicative m) => e -> Get s m a
+throw = Get . const . pure . Left . (,) [] . Exception.toException
+
+embed :: (Monad m) => Get s m a -> s -> Get t m a
+embed g s = do
+  r <- lift $ run g s
+  case r of
+    Left (ls, e) -> labels ls $ throw e
+    Right (_, x) -> pure x
+
+labels :: (Functor m) => [String] -> Get s m a -> Get s m a
+labels ls g = Get $ fmap (Bifunctor.first $ Bifunctor.first (ls <>)) . run g
+
+label :: (Functor m) => String -> Get s m a -> Get s m a
+label = labels . pure
diff --git a/src/lib/Rattletrap/Schema.hs b/src/lib/Rattletrap/Schema.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Schema.hs
@@ -0,0 +1,68 @@
+module Rattletrap.Schema where
+
+import qualified Data.Text as Text
+import qualified Rattletrap.Utility.Json as Json
+
+data Schema = Schema
+  { name :: Text.Text,
+    json :: Json.Value
+  }
+  deriving (Eq, Show)
+
+named :: String -> Json.Value -> Schema
+named n j = Schema {name = Text.pack n, json = j}
+
+ref :: Schema -> Json.Value
+ref s = Json.object [Json.pair "$ref" $ Text.pack "#/definitions/" <> name s]
+
+object :: [((Json.Key, Json.Value), Bool)] -> Json.Value
+object xs =
+  Json.object
+    [ Json.pair "type" "object",
+      Json.pair "properties" . Json.object $
+        fmap
+          ((\(k, v) -> Json.pair (Json.keyToString k) v) . fst)
+          xs,
+      Json.pair "required" . fmap (fst . fst) $ filter snd xs
+    ]
+
+maybe :: Schema -> Schema
+maybe s =
+  Schema
+    { name = Text.pack "maybe-" <> name s,
+      json = oneOf [ref s, json Rattletrap.Schema.null]
+    }
+
+oneOf :: [Json.Value] -> Json.Value
+oneOf xs = Json.object [Json.pair "oneOf" xs]
+
+tuple :: [Json.Value] -> Json.Value
+tuple xs =
+  Json.object
+    [ Json.pair "type" "array",
+      Json.pair "items" xs,
+      Json.pair "minItems" $ length xs,
+      Json.pair "maxItems" $ length xs
+    ]
+
+array :: Schema -> Schema
+array s =
+  Schema
+    { name = Text.pack "array-" <> name s,
+      json = Json.object [Json.pair "type" "array", Json.pair "items" $ ref s]
+    }
+
+boolean :: Schema
+boolean = named "boolean" $ Json.object [Json.pair "type" "boolean"]
+
+integer :: Schema
+integer = named "integer" $ Json.object [Json.pair "type" "integer"]
+
+null :: Schema
+null = named "null" $ Json.object [Json.pair "type" "null"]
+
+number :: Schema
+number = named "number" $ Json.object [Json.pair "type" "number"]
+
+string :: Schema
+string = named "string" $ Json.object [Json.pair "type" "string"]
diff --git a/src/lib/Rattletrap/Type/Attribute.hs b/src/lib/Rattletrap/Type/Attribute.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute.hs
@@ -0,0 +1,109 @@
+module Rattletrap.Type.Attribute where
+
+import qualified Control.Exception as Exception
+import qualified Data.Map as Map
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Exception.MissingAttributeLimit as MissingAttributeLimit
+import qualified Rattletrap.Exception.MissingAttributeName as MissingAttributeName
+import qualified Rattletrap.Exception.UnknownActor as UnknownActor
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.AttributeValue as AttributeValue
+import qualified Rattletrap.Type.ClassAttributeMap as ClassAttributeMap
+import qualified Rattletrap.Type.CompressedWord as CompressedWord
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data Attribute = Attribute
+  { id :: CompressedWord.CompressedWord,
+    -- | Read-only! Changing an attribute's name requires editing the class
+    -- attribute map.
+    name :: Str.Str,
+    value :: AttributeValue.AttributeValue
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Attribute where
+  parseJSON = Json.withObject "Attribute" $ \object -> do
+    id_ <- Json.required object "id"
+    name <- Json.required object "name"
+    value <- Json.required object "value"
+    pure Attribute {Rattletrap.Type.Attribute.id = id_, name, value}
+
+instance Json.ToJSON Attribute where
+  toJSON x =
+    Json.object
+      [ Json.pair "id" $ Rattletrap.Type.Attribute.id x,
+        Json.pair "name" $ name x,
+        Json.pair "value" $ value x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute" $
+    Schema.object
+      [ (Json.pair "id" $ Schema.ref CompressedWord.schema, True),
+        (Json.pair "name" $ Schema.ref Str.schema, True),
+        (Json.pair "value" $ Schema.ref AttributeValue.schema, True)
+      ]
+
+bitPut :: Attribute -> BitPut.BitPut
+bitPut attribute =
+  CompressedWord.bitPut (Rattletrap.Type.Attribute.id attribute)
+    <> AttributeValue.bitPut (value attribute)
+
+bitGet ::
+  Version.Version ->
+  Maybe Str.Str ->
+  ClassAttributeMap.ClassAttributeMap ->
+  Map.Map CompressedWord.CompressedWord U32.U32 ->
+  CompressedWord.CompressedWord ->
+  BitGet.BitGet Attribute
+bitGet version buildVersion classes actors actor =
+  BitGet.label "Attribute" $ do
+    attributes <- lookupAttributeMap classes actors actor
+    limit <- lookupAttributeIdLimit attributes actor
+    id_ <- BitGet.label "id" $ CompressedWord.bitGet limit
+    name <- lookupAttributeName classes attributes id_
+    value <-
+      BitGet.label "value" $
+        AttributeValue.bitGet
+          version
+          buildVersion
+          (ClassAttributeMap.objectMap classes)
+          name
+    pure Attribute {Rattletrap.Type.Attribute.id = id_, name, value}
+
+lookupAttributeMap ::
+  ClassAttributeMap.ClassAttributeMap ->
+  Map.Map CompressedWord.CompressedWord U32.U32 ->
+  CompressedWord.CompressedWord ->
+  BitGet.BitGet (Map.Map U32.U32 U32.U32)
+lookupAttributeMap classes actors actor =
+  fromMaybe
+    (UnknownActor.UnknownActor $ CompressedWord.value actor)
+    (ClassAttributeMap.getAttributeMap classes actors actor)
+
+lookupAttributeIdLimit ::
+  Map.Map U32.U32 U32.U32 ->
+  CompressedWord.CompressedWord ->
+  BitGet.BitGet Word
+lookupAttributeIdLimit attributes actor =
+  fromMaybe
+    (MissingAttributeLimit.MissingAttributeLimit $ CompressedWord.value actor)
+    (ClassAttributeMap.getAttributeIdLimit attributes)
+
+lookupAttributeName ::
+  ClassAttributeMap.ClassAttributeMap ->
+  Map.Map U32.U32 U32.U32 ->
+  CompressedWord.CompressedWord ->
+  BitGet.BitGet Str.Str
+lookupAttributeName classes attributes attribute =
+  fromMaybe
+    (MissingAttributeName.MissingAttributeName $ CompressedWord.value attribute)
+    (ClassAttributeMap.getAttributeName classes attributes attribute)
+
+fromMaybe :: (Exception.Exception e) => e -> Maybe a -> BitGet.BitGet a
+fromMaybe message = maybe (BitGet.throw message) pure
diff --git a/src/lib/Rattletrap/Type/Attribute/AppliedDamage.hs b/src/lib/Rattletrap/Type/Attribute/AppliedDamage.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/AppliedDamage.hs
@@ -0,0 +1,60 @@
+module Rattletrap.Type.Attribute.AppliedDamage where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.I32 as I32
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Type.Vector as Vector
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data AppliedDamage = AppliedDamage
+  { unknown1 :: U8.U8,
+    location :: Vector.Vector,
+    unknown3 :: I32.I32,
+    unknown4 :: I32.I32
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON AppliedDamage where
+  parseJSON = Json.withObject "AppliedDamage" $ \object -> do
+    unknown1 <- Json.required object "unknown1"
+    location <- Json.required object "location"
+    unknown3 <- Json.required object "unknown3"
+    unknown4 <- Json.required object "unknown4"
+    pure AppliedDamage {unknown1, location, unknown3, unknown4}
+
+instance Json.ToJSON AppliedDamage where
+  toJSON x =
+    Json.object
+      [ Json.pair "unknown1" $ unknown1 x,
+        Json.pair "location" $ location x,
+        Json.pair "unknown3" $ unknown3 x,
+        Json.pair "unknown4" $ unknown4 x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-applied-damage" $
+    Schema.object
+      [ (Json.pair "unknown1" $ Schema.ref U8.schema, True),
+        (Json.pair "location" $ Schema.ref Vector.schema, True),
+        (Json.pair "unknown3" $ Schema.ref I32.schema, True),
+        (Json.pair "unknown4" $ Schema.ref I32.schema, True)
+      ]
+
+bitPut :: AppliedDamage -> BitPut.BitPut
+bitPut appliedDamageAttribute =
+  U8.bitPut (unknown1 appliedDamageAttribute)
+    <> Vector.bitPut (location appliedDamageAttribute)
+    <> I32.bitPut (unknown3 appliedDamageAttribute)
+    <> I32.bitPut (unknown4 appliedDamageAttribute)
+
+bitGet :: Version.Version -> BitGet.BitGet AppliedDamage
+bitGet version = BitGet.label "AppliedDamage" $ do
+  unknown1 <- BitGet.label "unknown1" U8.bitGet
+  location <- BitGet.label "location" $ Vector.bitGet version
+  unknown3 <- BitGet.label "unknown3" I32.bitGet
+  unknown4 <- BitGet.label "unknown4" I32.bitGet
+  pure AppliedDamage {unknown1, location, unknown3, unknown4}
diff --git a/src/lib/Rattletrap/Type/Attribute/Boolean.hs b/src/lib/Rattletrap/Type/Attribute/Boolean.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Boolean.hs
@@ -0,0 +1,28 @@
+module Rattletrap.Type.Attribute.Boolean where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Boolean = Boolean
+  { value :: Bool
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Boolean where
+  parseJSON = fmap Boolean . Json.parseJSON
+
+instance Json.ToJSON Boolean where
+  toJSON = Json.toJSON . value
+
+schema :: Schema.Schema
+schema = Schema.named "attribute-boolean" $ Schema.ref Schema.boolean
+
+bitPut :: Boolean -> BitPut.BitPut
+bitPut booleanAttribute = BitPut.bool (value booleanAttribute)
+
+bitGet :: BitGet.BitGet Boolean
+bitGet = BitGet.label "Boolean" $ do
+  value <- BitGet.label "value" BitGet.bool
+  pure Boolean {value}
diff --git a/src/lib/Rattletrap/Type/Attribute/Boost.hs b/src/lib/Rattletrap/Type/Attribute/Boost.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Boost.hs
@@ -0,0 +1,57 @@
+module Rattletrap.Type.Attribute.Boost where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Utility.Json as Json
+
+data Boost = Boost
+  { grantCount :: U8.U8,
+    boostAmount :: U8.U8,
+    unused1 :: U8.U8,
+    unused2 :: U8.U8
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Boost where
+  parseJSON = Json.withObject "Boost" $ \object -> do
+    grantCount <- Json.required object "grantCount"
+    boostAmount <- Json.required object "boostAmount"
+    unused1 <- Json.required object "unused1"
+    unused2 <- Json.required object "unused2"
+    pure Boost {grantCount, boostAmount, unused1, unused2}
+
+instance Json.ToJSON Boost where
+  toJSON x =
+    Json.object
+      [ Json.pair "grantCount" $ grantCount x,
+        Json.pair "boostAmount" $ boostAmount x,
+        Json.pair "unused1" $ unused1 x,
+        Json.pair "unused2" $ unused2 x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "boost" $
+    Schema.object
+      [ (Json.pair "grantCount" $ Schema.ref U8.schema, True),
+        (Json.pair "boostAmount" $ Schema.ref U8.schema, True),
+        (Json.pair "unused1" $ Schema.ref U8.schema, True),
+        (Json.pair "unused2" $ Schema.ref U8.schema, True)
+      ]
+
+bitPut :: Boost -> BitPut.BitPut
+bitPut appliedDamageAttribute =
+  U8.bitPut (grantCount appliedDamageAttribute)
+    <> U8.bitPut (boostAmount appliedDamageAttribute)
+    <> U8.bitPut (unused1 appliedDamageAttribute)
+    <> U8.bitPut (unused2 appliedDamageAttribute)
+
+bitGet :: BitGet.BitGet Boost
+bitGet = BitGet.label "Boost" $ do
+  grantCount <- BitGet.label "grantCount" U8.bitGet
+  boostAmount <- BitGet.label "boostAmount" U8.bitGet
+  unused1 <- BitGet.label "unused1" U8.bitGet
+  unused2 <- BitGet.label "unused2" U8.bitGet
+  pure Boost {grantCount, boostAmount, unused1, unused2}
diff --git a/src/lib/Rattletrap/Type/Attribute/Byte.hs b/src/lib/Rattletrap/Type/Attribute/Byte.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Byte.hs
@@ -0,0 +1,29 @@
+module Rattletrap.Type.Attribute.Byte where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Byte = Byte
+  { value :: U8.U8
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Byte where
+  parseJSON = fmap Byte . Json.parseJSON
+
+instance Json.ToJSON Byte where
+  toJSON = Json.toJSON . value
+
+schema :: Schema.Schema
+schema = Schema.named "attribute-byte" $ Schema.ref U8.schema
+
+bitPut :: Byte -> BitPut.BitPut
+bitPut byteAttribute = U8.bitPut (value byteAttribute)
+
+bitGet :: BitGet.BitGet Byte
+bitGet = BitGet.label "Byte" $ do
+  value <- BitGet.label "value" U8.bitGet
+  pure Byte {value}
diff --git a/src/lib/Rattletrap/Type/Attribute/CamSettings.hs b/src/lib/Rattletrap/Type/Attribute/CamSettings.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/CamSettings.hs
@@ -0,0 +1,99 @@
+module Rattletrap.Type.Attribute.CamSettings where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.F32 as F32
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+import qualified Rattletrap.Utility.Monad as Monad
+
+data CamSettings = CamSettings
+  { fov :: F32.F32,
+    height :: F32.F32,
+    angle :: F32.F32,
+    distance :: F32.F32,
+    stiffness :: F32.F32,
+    swivelSpeed :: F32.F32,
+    transitionSpeed :: Maybe F32.F32
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON CamSettings where
+  parseJSON = Json.withObject "CamSettings" $ \object -> do
+    fov <- Json.required object "fov"
+    height <- Json.required object "height"
+    angle <- Json.required object "angle"
+    distance <- Json.required object "distance"
+    stiffness <- Json.required object "stiffness"
+    swivelSpeed <- Json.required object "swivel_speed"
+    transitionSpeed <- Json.optional object "transition_speed"
+    pure
+      CamSettings
+        { fov,
+          height,
+          angle,
+          distance,
+          stiffness,
+          swivelSpeed,
+          transitionSpeed
+        }
+
+instance Json.ToJSON CamSettings where
+  toJSON x =
+    Json.object
+      [ Json.pair "fov" $ fov x,
+        Json.pair "height" $ height x,
+        Json.pair "angle" $ angle x,
+        Json.pair "distance" $ distance x,
+        Json.pair "stiffness" $ stiffness x,
+        Json.pair "swivel_speed" $ swivelSpeed x,
+        Json.pair "transition_speed" $ transitionSpeed x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-cam-settings" $
+    Schema.object
+      [ (Json.pair "fov" $ Schema.ref F32.schema, True),
+        (Json.pair "height" $ Schema.ref F32.schema, True),
+        (Json.pair "angle" $ Schema.ref F32.schema, True),
+        (Json.pair "distance" $ Schema.ref F32.schema, True),
+        (Json.pair "stiffness" $ Schema.ref F32.schema, True),
+        (Json.pair "swivel_speed" $ Schema.ref F32.schema, True),
+        ( Json.pair "transition_speed" . Schema.json $ Schema.maybe F32.schema,
+          False
+        )
+      ]
+
+bitPut :: CamSettings -> BitPut.BitPut
+bitPut camSettingsAttribute =
+  F32.bitPut (fov camSettingsAttribute)
+    <> F32.bitPut (height camSettingsAttribute)
+    <> F32.bitPut (angle camSettingsAttribute)
+    <> F32.bitPut (distance camSettingsAttribute)
+    <> F32.bitPut (stiffness camSettingsAttribute)
+    <> F32.bitPut (swivelSpeed camSettingsAttribute)
+    <> foldMap F32.bitPut (transitionSpeed camSettingsAttribute)
+
+bitGet :: Version.Version -> BitGet.BitGet CamSettings
+bitGet version = BitGet.label "CamSettings" $ do
+  fov <- BitGet.label "fov" F32.bitGet
+  height <- BitGet.label "height" F32.bitGet
+  angle <- BitGet.label "angle" F32.bitGet
+  distance <- BitGet.label "distance" F32.bitGet
+  stiffness <- BitGet.label "stiffness" F32.bitGet
+  swivelSpeed <- BitGet.label "swivelSpeed" F32.bitGet
+  transitionSpeed <-
+    BitGet.label "transitionSpeed" $
+      Monad.whenMaybe (Version.atLeast 868 20 0 version) F32.bitGet
+  pure
+    CamSettings
+      { fov,
+        height,
+        angle,
+        distance,
+        stiffness,
+        swivelSpeed,
+        transitionSpeed
+      }
diff --git a/src/lib/Rattletrap/Type/Attribute/ClubColors.hs b/src/lib/Rattletrap/Type/Attribute/ClubColors.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/ClubColors.hs
@@ -0,0 +1,57 @@
+module Rattletrap.Type.Attribute.ClubColors where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Utility.Json as Json
+
+data ClubColors = ClubColors
+  { blueFlag :: Bool,
+    blueColor :: U8.U8,
+    orangeFlag :: Bool,
+    orangeColor :: U8.U8
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON ClubColors where
+  parseJSON = Json.withObject "ClubColors" $ \object -> do
+    blueFlag <- Json.required object "blue_flag"
+    blueColor <- Json.required object "blue_color"
+    orangeFlag <- Json.required object "orange_flag"
+    orangeColor <- Json.required object "orange_color"
+    pure ClubColors {blueFlag, blueColor, orangeFlag, orangeColor}
+
+instance Json.ToJSON ClubColors where
+  toJSON x =
+    Json.object
+      [ Json.pair "blue_flag" $ blueFlag x,
+        Json.pair "blue_color" $ blueColor x,
+        Json.pair "orange_flag" $ orangeFlag x,
+        Json.pair "orange_color" $ orangeColor x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-club-colors" $
+    Schema.object
+      [ (Json.pair "blue_flag" $ Schema.ref Schema.boolean, True),
+        (Json.pair "blue_color" $ Schema.ref U8.schema, True),
+        (Json.pair "orange_flag" $ Schema.ref Schema.boolean, True),
+        (Json.pair "orange_color" $ Schema.ref U8.schema, True)
+      ]
+
+bitPut :: ClubColors -> BitPut.BitPut
+bitPut clubColorsAttribute =
+  BitPut.bool (blueFlag clubColorsAttribute)
+    <> U8.bitPut (blueColor clubColorsAttribute)
+    <> BitPut.bool (orangeFlag clubColorsAttribute)
+    <> U8.bitPut (orangeColor clubColorsAttribute)
+
+bitGet :: BitGet.BitGet ClubColors
+bitGet = BitGet.label "ClubColors" $ do
+  blueFlag <- BitGet.label "blueFlag" BitGet.bool
+  blueColor <- BitGet.label "blueColor" U8.bitGet
+  orangeFlag <- BitGet.label "orangeFlag" BitGet.bool
+  orangeColor <- BitGet.label "orangeColor" U8.bitGet
+  pure ClubColors {blueFlag, blueColor, orangeFlag, orangeColor}
diff --git a/src/lib/Rattletrap/Type/Attribute/CustomDemolish.hs b/src/lib/Rattletrap/Type/Attribute/CustomDemolish.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/CustomDemolish.hs
@@ -0,0 +1,58 @@
+module Rattletrap.Type.Attribute.CustomDemolish where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Attribute.Demolish as Demolish
+import qualified Rattletrap.Type.I32 as I32
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data CustomDemolish = CustomDemolish
+  { flag :: Bool,
+    id :: I32.I32,
+    demolish :: Demolish.Demolish
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON CustomDemolish where
+  parseJSON = Json.withObject "CustomDemolish" $ \object -> do
+    flag <- Json.required object "flag"
+    id_ <- Json.required object "id"
+    demolish <- Json.required object "demolish"
+    pure CustomDemolish {flag, Rattletrap.Type.Attribute.CustomDemolish.id = id_, demolish}
+
+instance Json.ToJSON CustomDemolish where
+  toJSON x =
+    Json.object
+      [ Json.pair "flag" $ flag x,
+        Json.pair "id" $ Rattletrap.Type.Attribute.CustomDemolish.id x,
+        Json.pair "demolish" $ demolish x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-custom-demolish" $
+    Schema.object
+      [ (Json.pair "flag" $ Schema.ref Schema.boolean, True),
+        (Json.pair "id" $ Schema.ref I32.schema, True),
+        (Json.pair "demolish" $ Schema.ref Demolish.schema, True)
+      ]
+
+bitPut :: CustomDemolish -> BitPut.BitPut
+bitPut x =
+  BitPut.bool (flag x)
+    <> I32.bitPut (Rattletrap.Type.Attribute.CustomDemolish.id x)
+    <> Demolish.bitPut (demolish x)
+
+bitGet :: Version.Version -> BitGet.BitGet CustomDemolish
+bitGet version = BitGet.label "CustomDemolish" $ do
+  flag <- BitGet.label "flag" BitGet.bool
+  id_ <- BitGet.label "id" I32.bitGet
+  demolish <- BitGet.label "demolish" $ Demolish.bitGet version
+  pure
+    CustomDemolish
+      { flag,
+        Rattletrap.Type.Attribute.CustomDemolish.id = id_,
+        demolish
+      }
diff --git a/src/lib/Rattletrap/Type/Attribute/CustomDemolishExtended.hs b/src/lib/Rattletrap/Type/Attribute/CustomDemolishExtended.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/CustomDemolishExtended.hs
@@ -0,0 +1,71 @@
+module Rattletrap.Type.Attribute.CustomDemolishExtended where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Attribute.CustomDemolish as CustomDemolish
+import qualified Rattletrap.Type.ObjectTarget as ObjectTarget
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data CustomDemolishExtended = CustomDemolishExtended
+  { attackerPri :: ObjectTarget.ObjectTarget,
+    selfDemoFx :: ObjectTarget.ObjectTarget,
+    selfDemolish :: Bool,
+    customDemolish :: CustomDemolish.CustomDemolish
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON CustomDemolishExtended where
+  parseJSON = Json.withObject "CustomDemolishExtended" $ \object -> do
+    attackerPri <- Json.required object "attacker_pri"
+    selfDemoFx <- Json.required object "self_demo_fx"
+    selfDemolish <- Json.required object "self_demolish"
+    customDemolish <- Json.required object "custom_demolish"
+    pure
+      CustomDemolishExtended
+        { attackerPri,
+          selfDemoFx,
+          selfDemolish,
+          customDemolish
+        }
+
+instance Json.ToJSON CustomDemolishExtended where
+  toJSON x =
+    Json.object
+      [ Json.pair "attacker_pri" $ attackerPri x,
+        Json.pair "self_demo_fx" $ selfDemoFx x,
+        Json.pair "self_demolish" $ selfDemolish x,
+        Json.pair "custom_demolish" $ customDemolish x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-custom-demolish-extended" $
+    Schema.object
+      [ (Json.pair "attacker_pri" $ Schema.ref ObjectTarget.schema, True),
+        (Json.pair "self_demo_fx" $ Schema.ref ObjectTarget.schema, True),
+        (Json.pair "self_demolish" $ Schema.ref Schema.boolean, True),
+        (Json.pair "custom_demolish" $ Schema.ref CustomDemolish.schema, True)
+      ]
+
+bitPut :: CustomDemolishExtended -> BitPut.BitPut
+bitPut demolishAttribute =
+  ObjectTarget.bitPut (attackerPri demolishAttribute)
+    <> ObjectTarget.bitPut (selfDemoFx demolishAttribute)
+    <> BitPut.bool (selfDemolish demolishAttribute)
+    <> CustomDemolish.bitPut (customDemolish demolishAttribute)
+
+bitGet :: Version.Version -> BitGet.BitGet CustomDemolishExtended
+bitGet version = BitGet.label "CustomDemolishExtended" $ do
+  attackerPri <- BitGet.label "attackerPri" ObjectTarget.bitGet
+  selfDemoFx <- BitGet.label "selfDemoFx" ObjectTarget.bitGet
+  selfDemolish <- BitGet.label "selfDemolish" BitGet.bool
+  customDemolish <- BitGet.label "customDemolish" $ CustomDemolish.bitGet version
+  pure
+    CustomDemolishExtended
+      { attackerPri,
+        selfDemoFx,
+        selfDemolish,
+        customDemolish
+      }
diff --git a/src/lib/Rattletrap/Type/Attribute/DamageState.hs b/src/lib/Rattletrap/Type/Attribute/DamageState.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/DamageState.hs
@@ -0,0 +1,88 @@
+module Rattletrap.Type.Attribute.DamageState where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.I32 as I32
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Type.Vector as Vector
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data DamageState = DamageState
+  { unknown1 :: U8.U8,
+    unknown2 :: Bool,
+    unknown3 :: I32.I32,
+    unknown4 :: Vector.Vector,
+    unknown5 :: Bool,
+    unknown6 :: Bool
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON DamageState where
+  parseJSON = Json.withObject "DamageState" $ \object -> do
+    unknown1 <- Json.required object "unknown1"
+    unknown2 <- Json.required object "unknown2"
+    unknown3 <- Json.required object "unknown3"
+    unknown4 <- Json.required object "unknown4"
+    unknown5 <- Json.required object "unknown5"
+    unknown6 <- Json.required object "unknown6"
+    pure
+      DamageState
+        { unknown1,
+          unknown2,
+          unknown3,
+          unknown4,
+          unknown5,
+          unknown6
+        }
+
+instance Json.ToJSON DamageState where
+  toJSON x =
+    Json.object
+      [ Json.pair "unknown1" $ unknown1 x,
+        Json.pair "unknown2" $ unknown2 x,
+        Json.pair "unknown3" $ unknown3 x,
+        Json.pair "unknown4" $ unknown4 x,
+        Json.pair "unknown5" $ unknown5 x,
+        Json.pair "unknown6" $ unknown6 x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-damage-state" $
+    Schema.object
+      [ (Json.pair "unknown1" $ Schema.ref U8.schema, True),
+        (Json.pair "unknown2" $ Schema.ref Schema.boolean, True),
+        (Json.pair "unknown3" $ Schema.ref I32.schema, True),
+        (Json.pair "unknown4" $ Schema.ref Vector.schema, True),
+        (Json.pair "unknown5" $ Schema.ref Schema.boolean, True),
+        (Json.pair "unknown6" $ Schema.ref Schema.boolean, True)
+      ]
+
+bitPut :: DamageState -> BitPut.BitPut
+bitPut damageStateAttribute =
+  U8.bitPut (unknown1 damageStateAttribute)
+    <> BitPut.bool (unknown2 damageStateAttribute)
+    <> I32.bitPut (unknown3 damageStateAttribute)
+    <> Vector.bitPut (unknown4 damageStateAttribute)
+    <> BitPut.bool (unknown5 damageStateAttribute)
+    <> BitPut.bool (unknown6 damageStateAttribute)
+
+bitGet :: Version.Version -> BitGet.BitGet DamageState
+bitGet version = BitGet.label "DamageState" $ do
+  unknown1 <- BitGet.label "unknown1" U8.bitGet
+  unknown2 <- BitGet.label "unknown2" BitGet.bool
+  unknown3 <- BitGet.label "unknown3" I32.bitGet
+  unknown4 <- BitGet.label "unknown4" $ Vector.bitGet version
+  unknown5 <- BitGet.label "unknown5" BitGet.bool
+  unknown6 <- BitGet.label "unknown6" BitGet.bool
+  pure
+    DamageState
+      { unknown1,
+        unknown2,
+        unknown3,
+        unknown4,
+        unknown5,
+        unknown6
+      }
diff --git a/src/lib/Rattletrap/Type/Attribute/Demolish.hs b/src/lib/Rattletrap/Type/Attribute/Demolish.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Demolish.hs
@@ -0,0 +1,87 @@
+module Rattletrap.Type.Attribute.Demolish where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.Vector as Vector
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data Demolish = Demolish
+  { attackerFlag :: Bool,
+    attackerActorId :: U32.U32,
+    victimFlag :: Bool,
+    victimActorId :: U32.U32,
+    attackerVelocity :: Vector.Vector,
+    victimVelocity :: Vector.Vector
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Demolish where
+  parseJSON = Json.withObject "Demolish" $ \object -> do
+    attackerFlag <- Json.required object "attacker_flag"
+    attackerActorId <- Json.required object "attacker_actor_id"
+    victimFlag <- Json.required object "victim_flag"
+    victimActorId <- Json.required object "victim_actor_id"
+    attackerVelocity <- Json.required object "attacker_velocity"
+    victimVelocity <- Json.required object "victim_velocity"
+    pure
+      Demolish
+        { attackerFlag,
+          attackerActorId,
+          victimFlag,
+          victimActorId,
+          attackerVelocity,
+          victimVelocity
+        }
+
+instance Json.ToJSON Demolish where
+  toJSON x =
+    Json.object
+      [ Json.pair "attacker_flag" $ attackerFlag x,
+        Json.pair "attacker_actor_id" $ attackerActorId x,
+        Json.pair "victim_flag" $ victimFlag x,
+        Json.pair "victim_actor_id" $ victimActorId x,
+        Json.pair "attacker_velocity" $ attackerVelocity x,
+        Json.pair "victim_velocity" $ victimVelocity x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-demolish" $
+    Schema.object
+      [ (Json.pair "attacker_flag" $ Schema.ref Schema.boolean, True),
+        (Json.pair "attacker_actor_id" $ Schema.ref U32.schema, True),
+        (Json.pair "victim_flag" $ Schema.ref Schema.boolean, True),
+        (Json.pair "victim_actor_id" $ Schema.ref U32.schema, True),
+        (Json.pair "attacker_velocity" $ Schema.ref Vector.schema, True),
+        (Json.pair "victim_velocity" $ Schema.ref Vector.schema, True)
+      ]
+
+bitPut :: Demolish -> BitPut.BitPut
+bitPut demolishAttribute =
+  BitPut.bool (attackerFlag demolishAttribute)
+    <> U32.bitPut (attackerActorId demolishAttribute)
+    <> BitPut.bool (victimFlag demolishAttribute)
+    <> U32.bitPut (victimActorId demolishAttribute)
+    <> Vector.bitPut (attackerVelocity demolishAttribute)
+    <> Vector.bitPut (victimVelocity demolishAttribute)
+
+bitGet :: Version.Version -> BitGet.BitGet Demolish
+bitGet version = BitGet.label "Demolish" $ do
+  attackerFlag <- BitGet.label "attackerFlag" BitGet.bool
+  attackerActorId <- BitGet.label "attackerActorId" U32.bitGet
+  victimFlag <- BitGet.label "victimFlag" BitGet.bool
+  victimActorId <- BitGet.label "victimActorId" U32.bitGet
+  attackerVelocity <- BitGet.label "attackerVelocity" $ Vector.bitGet version
+  victimVelocity <- BitGet.label "victimVelocity" $ Vector.bitGet version
+  pure
+    Demolish
+      { attackerFlag,
+        attackerActorId,
+        victimFlag,
+        victimActorId,
+        attackerVelocity,
+        victimVelocity
+      }
diff --git a/src/lib/Rattletrap/Type/Attribute/Enum.hs b/src/lib/Rattletrap/Type/Attribute/Enum.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Enum.hs
@@ -0,0 +1,30 @@
+module Rattletrap.Type.Attribute.Enum where
+
+import qualified Data.Word as Word
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Utility.Json as Json
+import Prelude hiding (Enum)
+
+newtype Enum = Enum
+  { value :: Word.Word16
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Enum where
+  parseJSON = fmap Enum . Json.parseJSON
+
+instance Json.ToJSON Enum where
+  toJSON = Json.toJSON . value
+
+schema :: Schema.Schema
+schema = Schema.named "attribute-enum" $ Schema.ref Schema.integer
+
+bitPut :: Enum -> BitPut.BitPut
+bitPut enumAttribute = BitPut.bits 11 (value enumAttribute)
+
+bitGet :: BitGet.BitGet Enum
+bitGet = BitGet.label "Enum" $ do
+  value <- BitGet.label "value" $ BitGet.bits 11
+  pure Enum {value}
diff --git a/src/lib/Rattletrap/Type/Attribute/Explosion.hs b/src/lib/Rattletrap/Type/Attribute/Explosion.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Explosion.hs
@@ -0,0 +1,53 @@
+module Rattletrap.Type.Attribute.Explosion where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.I32 as I32
+import qualified Rattletrap.Type.Vector as Vector
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data Explosion = Explosion
+  { flag :: Bool,
+    actorId :: I32.I32,
+    location :: Vector.Vector
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Explosion where
+  parseJSON = Json.withObject "Explosion" $ \object -> do
+    flag <- Json.required object "flag"
+    actorId <- Json.required object "actor_id"
+    location <- Json.required object "location"
+    pure Explosion {flag, actorId, location}
+
+instance Json.ToJSON Explosion where
+  toJSON x =
+    Json.object
+      [ Json.pair "flag" $ flag x,
+        Json.pair "actor_id" $ actorId x,
+        Json.pair "location" $ location x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-explosion" $
+    Schema.object
+      [ (Json.pair "flag" $ Schema.ref Schema.boolean, True),
+        (Json.pair "actor_id" $ Schema.ref I32.schema, True),
+        (Json.pair "location" $ Schema.ref Vector.schema, True)
+      ]
+
+bitPut :: Explosion -> BitPut.BitPut
+bitPut explosionAttribute =
+  BitPut.bool (flag explosionAttribute)
+    <> I32.bitPut (actorId explosionAttribute)
+    <> Vector.bitPut (location explosionAttribute)
+
+bitGet :: Version.Version -> BitGet.BitGet Explosion
+bitGet version = BitGet.label "Explosion" $ do
+  flag <- BitGet.label "flag" BitGet.bool
+  actorId <- BitGet.label "actorId" I32.bitGet
+  location <- BitGet.label "location" $ Vector.bitGet version
+  pure Explosion {flag, actorId, location}
diff --git a/src/lib/Rattletrap/Type/Attribute/ExtendedExplosion.hs b/src/lib/Rattletrap/Type/Attribute/ExtendedExplosion.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/ExtendedExplosion.hs
@@ -0,0 +1,43 @@
+module Rattletrap.Type.Attribute.ExtendedExplosion where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Attribute.Explosion as Explosion
+import qualified Rattletrap.Type.Attribute.FlaggedInt as FlaggedInt
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data ExtendedExplosion = ExtendedExplosion
+  { explosion :: Explosion.Explosion,
+    unknown :: FlaggedInt.FlaggedInt
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON ExtendedExplosion where
+  parseJSON = Json.withObject "ExtendedExplosion" $ \object -> do
+    explosion <- Json.required object "explosion"
+    unknown <- Json.required object "unknown"
+    pure ExtendedExplosion {explosion, unknown}
+
+instance Json.ToJSON ExtendedExplosion where
+  toJSON x =
+    Json.object
+      [Json.pair "explosion" $ explosion x, Json.pair "unknown" $ unknown x]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-extended-explosion" $
+    Schema.object
+      [ (Json.pair "explosion" $ Schema.ref Explosion.schema, True),
+        (Json.pair "unknown" $ Schema.ref FlaggedInt.schema, True)
+      ]
+
+bitPut :: ExtendedExplosion -> BitPut.BitPut
+bitPut x = Explosion.bitPut (explosion x) <> FlaggedInt.bitPut (unknown x)
+
+bitGet :: Version.Version -> BitGet.BitGet ExtendedExplosion
+bitGet version = BitGet.label "ExtendedExplosion" $ do
+  explosion <- BitGet.label "explosion" $ Explosion.bitGet version
+  unknown <- BitGet.label "unknown" FlaggedInt.bitGet
+  pure ExtendedExplosion {explosion, unknown}
diff --git a/src/lib/Rattletrap/Type/Attribute/FlaggedByte.hs b/src/lib/Rattletrap/Type/Attribute/FlaggedByte.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/FlaggedByte.hs
@@ -0,0 +1,42 @@
+module Rattletrap.Type.Attribute.FlaggedByte where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Utility.Json as Json
+
+data FlaggedByte = FlaggedByte
+  { flag :: Bool,
+    byte :: U8.U8
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON FlaggedByte where
+  parseJSON = Json.withObject "FlaggedByte" $ \object -> do
+    flag <- Json.required object "flag"
+    byte <- Json.required object "byte"
+    pure FlaggedByte {flag, byte}
+
+instance Json.ToJSON FlaggedByte where
+  toJSON x =
+    Json.object [Json.pair "flag" $ flag x, Json.pair "byte" $ byte x]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-flagged-byte" $
+    Schema.object
+      [ (Json.pair "flag" $ Schema.ref Schema.boolean, True),
+        (Json.pair "byte" $ Schema.ref U8.schema, True)
+      ]
+
+bitPut :: FlaggedByte -> BitPut.BitPut
+bitPut flaggedByteAttribute =
+  BitPut.bool (flag flaggedByteAttribute)
+    <> U8.bitPut (byte flaggedByteAttribute)
+
+bitGet :: BitGet.BitGet FlaggedByte
+bitGet = BitGet.label "FlaggedByte" $ do
+  flag <- BitGet.label "flag" BitGet.bool
+  byte <- BitGet.label "byte" U8.bitGet
+  pure FlaggedByte {flag, byte}
diff --git a/src/lib/Rattletrap/Type/Attribute/FlaggedInt.hs b/src/lib/Rattletrap/Type/Attribute/FlaggedInt.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/FlaggedInt.hs
@@ -0,0 +1,41 @@
+module Rattletrap.Type.Attribute.FlaggedInt where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.I32 as I32
+import qualified Rattletrap.Utility.Json as Json
+
+data FlaggedInt = FlaggedInt
+  { flag :: Bool,
+    int :: I32.I32
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON FlaggedInt where
+  parseJSON = Json.withObject "FlaggedInt" $ \object -> do
+    flag <- Json.required object "flag"
+    int <- Json.required object "int"
+    pure FlaggedInt {flag, int}
+
+instance Json.ToJSON FlaggedInt where
+  toJSON x = Json.object [Json.pair "flag" $ flag x, Json.pair "int" $ int x]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-flagged-int" $
+    Schema.object
+      [ (Json.pair "flag" $ Schema.ref Schema.boolean, True),
+        (Json.pair "int" $ Schema.ref I32.schema, True)
+      ]
+
+bitPut :: FlaggedInt -> BitPut.BitPut
+bitPut flaggedIntAttribute =
+  BitPut.bool (flag flaggedIntAttribute)
+    <> I32.bitPut (int flaggedIntAttribute)
+
+bitGet :: BitGet.BitGet FlaggedInt
+bitGet = BitGet.label "FlaggedInt" $ do
+  flag <- BitGet.label "flag" BitGet.bool
+  int <- BitGet.label "int" I32.bitGet
+  pure FlaggedInt {flag, int}
diff --git a/src/lib/Rattletrap/Type/Attribute/Float.hs b/src/lib/Rattletrap/Type/Attribute/Float.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Float.hs
@@ -0,0 +1,30 @@
+module Rattletrap.Type.Attribute.Float where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.F32 as F32
+import qualified Rattletrap.Utility.Json as Json
+import Prelude hiding (Float)
+
+newtype Float = Float
+  { value :: F32.F32
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Float where
+  parseJSON = fmap Float . Json.parseJSON
+
+instance Json.ToJSON Float where
+  toJSON = Json.toJSON . value
+
+schema :: Schema.Schema
+schema = Schema.named "attribute-float" $ Schema.ref F32.schema
+
+bitPut :: Float -> BitPut.BitPut
+bitPut floatAttribute = F32.bitPut (value floatAttribute)
+
+bitGet :: BitGet.BitGet Float
+bitGet = BitGet.label "Float" $ do
+  value <- BitGet.label "value" F32.bitGet
+  pure Float {value}
diff --git a/src/lib/Rattletrap/Type/Attribute/GameMode.hs b/src/lib/Rattletrap/Type/Attribute/GameMode.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/GameMode.hs
@@ -0,0 +1,46 @@
+module Rattletrap.Type.Attribute.GameMode where
+
+import qualified Data.Word as Word
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data GameMode = GameMode
+  { -- | This field is guaranteed to be small. In other words, it won't overflow.
+    -- It's stored as a regular 'Int' rather than something more precise like an
+    -- 'Int8' because it just gets passed to functions that expect 'Int's.
+    -- There's no reason to do a bunch of conversions.
+    numBits :: Int,
+    word :: Word.Word8
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON GameMode where
+  parseJSON = Json.withObject "GameMode" $ \object -> do
+    numBits <- Json.required object "num_bits"
+    word <- Json.required object "word"
+    pure GameMode {numBits, word}
+
+instance Json.ToJSON GameMode where
+  toJSON x =
+    Json.object [Json.pair "num_bits" $ numBits x, Json.pair "word" $ word x]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-game-mode" $
+    Schema.object
+      [ (Json.pair "num_bits" $ Schema.ref Schema.integer, True),
+        (Json.pair "word" $ Schema.ref Schema.integer, True)
+      ]
+
+bitPut :: GameMode -> BitPut.BitPut
+bitPut gameModeAttribute = do
+  BitPut.bits (numBits gameModeAttribute) (word gameModeAttribute)
+
+bitGet :: Version.Version -> BitGet.BitGet GameMode
+bitGet version = BitGet.label "GameMode" $ do
+  let numBits = if Version.atLeast 868 12 0 version then 8 else 2 :: Int
+  word <- BitGet.label "word" $ BitGet.bits numBits
+  pure GameMode {numBits, word}
diff --git a/src/lib/Rattletrap/Type/Attribute/GameServer.hs b/src/lib/Rattletrap/Type/Attribute/GameServer.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/GameServer.hs
@@ -0,0 +1,43 @@
+module Rattletrap.Type.Attribute.GameServer where
+
+import qualified Data.Foldable as Foldable
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Attribute.QWord as QWord
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Utility.Json as Json
+
+data GameServer
+  = Old QWord.QWord
+  | New Str.Str
+  deriving (Eq, Show)
+
+instance Json.FromJSON GameServer where
+  parseJSON = Json.withObject "GameServer" $ \x ->
+    Foldable.asum
+      [fmap Old $ Json.required x "old", fmap New $ Json.required x "new"]
+
+instance Json.ToJSON GameServer where
+  toJSON x = case x of
+    Old y -> Json.object [Json.pair "old" $ Json.toJSON y]
+    New y -> Json.object [Json.pair "new" $ Json.toJSON y]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-game-server" . Schema.oneOf $
+    fmap
+      (\(k, v) -> Schema.object [(Json.pair k v, True)])
+      [("old", Schema.ref QWord.schema), ("new", Schema.ref Str.schema)]
+
+bitPut :: GameServer -> BitPut.BitPut
+bitPut x = case x of
+  Old y -> QWord.bitPut y
+  New y -> Str.bitPut y
+
+bitGet :: Maybe Str.Str -> BitGet.BitGet GameServer
+bitGet buildVersion =
+  BitGet.label "GameServer" $
+    if buildVersion >= Just (Str.fromString "221120.42953.406184")
+      then BitGet.label "New" $ fmap New Str.bitGet
+      else BitGet.label "Old" $ fmap Old QWord.bitGet
diff --git a/src/lib/Rattletrap/Type/Attribute/Int.hs b/src/lib/Rattletrap/Type/Attribute/Int.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Int.hs
@@ -0,0 +1,30 @@
+module Rattletrap.Type.Attribute.Int where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.I32 as I32
+import qualified Rattletrap.Utility.Json as Json
+import Prelude hiding (Int)
+
+newtype Int = Int
+  { value :: I32.I32
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Int where
+  parseJSON = fmap Int . Json.parseJSON
+
+instance Json.ToJSON Int where
+  toJSON = Json.toJSON . value
+
+schema :: Schema.Schema
+schema = Schema.named "attribute-int" $ Schema.ref I32.schema
+
+bitPut :: Int -> BitPut.BitPut
+bitPut intAttribute = I32.bitPut (value intAttribute)
+
+bitGet :: BitGet.BitGet Int
+bitGet = BitGet.label "Int" $ do
+  value <- BitGet.label "value" I32.bitGet
+  pure Int {value}
diff --git a/src/lib/Rattletrap/Type/Attribute/Int64.hs b/src/lib/Rattletrap/Type/Attribute/Int64.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Int64.hs
@@ -0,0 +1,29 @@
+module Rattletrap.Type.Attribute.Int64 where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.I64 as I64
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Int64 = Int64
+  { value :: I64.I64
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Int64 where
+  parseJSON = fmap Int64 . Json.parseJSON
+
+instance Json.ToJSON Int64 where
+  toJSON = Json.toJSON . value
+
+schema :: Schema.Schema
+schema = Schema.named "attribute-int64" $ Schema.ref I64.schema
+
+putInt64Attribute :: Int64 -> BitPut.BitPut
+putInt64Attribute int64Attribute = I64.bitPut (value int64Attribute)
+
+bitGet :: BitGet.BitGet Int64
+bitGet = BitGet.label "Int64" $ do
+  value <- BitGet.label "value" I64.bitGet
+  pure Int64 {value}
diff --git a/src/lib/Rattletrap/Type/Attribute/Loadout.hs b/src/lib/Rattletrap/Type/Attribute/Loadout.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Loadout.hs
@@ -0,0 +1,194 @@
+module Rattletrap.Type.Attribute.Loadout where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Utility.Json as Json
+import qualified Rattletrap.Utility.Monad as Monad
+
+data Loadout = Loadout
+  { version :: U8.U8,
+    body :: U32.U32,
+    decal :: U32.U32,
+    wheels :: U32.U32,
+    -- | Now known as "rocket boost".
+    rocketTrail :: U32.U32,
+    antenna :: U32.U32,
+    topper :: U32.U32,
+    unknown1 :: U32.U32,
+    unknown2 :: Maybe U32.U32,
+    engineAudio :: Maybe U32.U32,
+    trail :: Maybe U32.U32,
+    goalExplosion :: Maybe U32.U32,
+    banner :: Maybe U32.U32,
+    unknown3 :: Maybe U32.U32,
+    unknown4 :: Maybe U32.U32,
+    unknown5 :: Maybe U32.U32,
+    unknown6 :: Maybe U32.U32
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Loadout where
+  parseJSON = Json.withObject "Loadout" $ \object -> do
+    version <- Json.required object "version"
+    body <- Json.required object "body"
+    decal <- Json.required object "decal"
+    wheels <- Json.required object "wheels"
+    rocketTrail <- Json.required object "rocket_trail"
+    antenna <- Json.required object "antenna"
+    topper <- Json.required object "topper"
+    unknown1 <- Json.required object "unknown1"
+    unknown2 <- Json.optional object "unknown2"
+    engineAudio <- Json.optional object "engine_audio"
+    trail <- Json.optional object "trail"
+    goalExplosion <- Json.optional object "goal_explosion"
+    banner <- Json.optional object "banner"
+    unknown3 <- Json.optional object "unknown3"
+    unknown4 <- Json.optional object "unknown4"
+    unknown5 <- Json.optional object "unknown5"
+    unknown6 <- Json.optional object "unknown6"
+    pure
+      Loadout
+        { version,
+          body,
+          decal,
+          wheels,
+          rocketTrail,
+          antenna,
+          topper,
+          unknown1,
+          unknown2,
+          engineAudio,
+          trail,
+          goalExplosion,
+          banner,
+          unknown3,
+          unknown4,
+          unknown5,
+          unknown6
+        }
+
+instance Json.ToJSON Loadout where
+  toJSON x =
+    Json.object
+      [ Json.pair "version" $ version x,
+        Json.pair "body" $ body x,
+        Json.pair "decal" $ decal x,
+        Json.pair "wheels" $ wheels x,
+        Json.pair "rocket_trail" $ rocketTrail x,
+        Json.pair "antenna" $ antenna x,
+        Json.pair "topper" $ topper x,
+        Json.pair "unknown1" $ unknown1 x,
+        Json.pair "unknown2" $ unknown2 x,
+        Json.pair "engine_audio" $ engineAudio x,
+        Json.pair "trail" $ trail x,
+        Json.pair "goal_explosion" $ goalExplosion x,
+        Json.pair "banner" $ banner x,
+        Json.pair "unknown3" $ unknown3 x,
+        Json.pair "unknown4" $ unknown4 x,
+        Json.pair "unknown5" $ unknown5 x,
+        Json.pair "unknown6" $ unknown6 x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-loadout" $
+    Schema.object
+      [ (Json.pair "version" $ Schema.ref U8.schema, True),
+        (Json.pair "body" $ Schema.ref U32.schema, True),
+        (Json.pair "decal" $ Schema.ref U32.schema, True),
+        (Json.pair "wheels" $ Schema.ref U32.schema, True),
+        (Json.pair "rocket_trail" $ Schema.ref U32.schema, True),
+        (Json.pair "antenna" $ Schema.ref U32.schema, True),
+        (Json.pair "topper" $ Schema.ref U32.schema, True),
+        (Json.pair "unknown1" $ Schema.ref U32.schema, True),
+        (Json.pair "unknown2" . Schema.json $ Schema.maybe U32.schema, False),
+        (Json.pair "engine_audio" . Schema.json $ Schema.maybe U32.schema, False),
+        (Json.pair "trail" . Schema.json $ Schema.maybe U32.schema, False),
+        (Json.pair "goal_explosion" . Schema.json $ Schema.maybe U32.schema, False),
+        (Json.pair "banner" . Schema.json $ Schema.maybe U32.schema, False),
+        (Json.pair "unknown3" . Schema.json $ Schema.maybe U32.schema, False),
+        (Json.pair "unknown4" . Schema.json $ Schema.maybe U32.schema, False),
+        (Json.pair "unknown5" . Schema.json $ Schema.maybe U32.schema, False),
+        (Json.pair "unknown6" . Schema.json $ Schema.maybe U32.schema, False)
+      ]
+
+bitPut :: Loadout -> BitPut.BitPut
+bitPut loadoutAttribute =
+  U8.bitPut (version loadoutAttribute)
+    <> U32.bitPut (body loadoutAttribute)
+    <> U32.bitPut (decal loadoutAttribute)
+    <> U32.bitPut (wheels loadoutAttribute)
+    <> U32.bitPut (rocketTrail loadoutAttribute)
+    <> U32.bitPut (antenna loadoutAttribute)
+    <> U32.bitPut (topper loadoutAttribute)
+    <> U32.bitPut (unknown1 loadoutAttribute)
+    <> foldMap U32.bitPut (unknown2 loadoutAttribute)
+    <> foldMap U32.bitPut (engineAudio loadoutAttribute)
+    <> foldMap U32.bitPut (trail loadoutAttribute)
+    <> foldMap U32.bitPut (goalExplosion loadoutAttribute)
+    <> foldMap U32.bitPut (banner loadoutAttribute)
+    <> foldMap U32.bitPut (unknown3 loadoutAttribute)
+    <> foldMap U32.bitPut (unknown4 loadoutAttribute)
+    <> foldMap U32.bitPut (unknown5 loadoutAttribute)
+    <> foldMap U32.bitPut (unknown6 loadoutAttribute)
+
+bitGet :: BitGet.BitGet Loadout
+bitGet = BitGet.label "Loadout" $ do
+  version <- BitGet.label "version" U8.bitGet
+  body <- BitGet.label "body" U32.bitGet
+  decal <- BitGet.label "decal" U32.bitGet
+  wheels <- BitGet.label "wheels" U32.bitGet
+  rocketTrail <- BitGet.label "rocketTrail" U32.bitGet
+  antenna <- BitGet.label "antenna" U32.bitGet
+  topper <- BitGet.label "topper" U32.bitGet
+  unknown1 <- BitGet.label "unknown1" U32.bitGet
+  unknown2 <-
+    BitGet.label "unknown2" $
+      Monad.whenMaybe (U8.toWord8 version >= 11) U32.bitGet
+  engineAudio <-
+    BitGet.label "engineAudio" $
+      Monad.whenMaybe (U8.toWord8 version >= 16) U32.bitGet
+  trail <-
+    BitGet.label "trail" $
+      Monad.whenMaybe (U8.toWord8 version >= 16) U32.bitGet
+  goalExplosion <-
+    BitGet.label "goalExplosion" $
+      Monad.whenMaybe (U8.toWord8 version >= 16) U32.bitGet
+  banner <-
+    BitGet.label "banner" $
+      Monad.whenMaybe (U8.toWord8 version >= 17) U32.bitGet
+  unknown3 <-
+    BitGet.label "unknown3" $
+      Monad.whenMaybe (U8.toWord8 version >= 19) U32.bitGet
+  unknown4 <-
+    BitGet.label "unknown4" $
+      Monad.whenMaybe (U8.toWord8 version >= 22) U32.bitGet
+  unknown5 <-
+    BitGet.label "unknown5" $
+      Monad.whenMaybe (U8.toWord8 version >= 22) U32.bitGet
+  unknown6 <-
+    BitGet.label "unknown6" $
+      Monad.whenMaybe (U8.toWord8 version >= 22) U32.bitGet
+  pure
+    Loadout
+      { version,
+        body,
+        decal,
+        wheels,
+        rocketTrail,
+        antenna,
+        topper,
+        unknown1,
+        unknown2,
+        engineAudio,
+        trail,
+        goalExplosion,
+        banner,
+        unknown3,
+        unknown4,
+        unknown5,
+        unknown6
+      }
diff --git a/src/lib/Rattletrap/Type/Attribute/LoadoutOnline.hs b/src/lib/Rattletrap/Type/Attribute/LoadoutOnline.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/LoadoutOnline.hs
@@ -0,0 +1,47 @@
+module Rattletrap.Type.Attribute.LoadoutOnline where
+
+import qualified Data.Map as Map
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Attribute.Product as Product
+import qualified Rattletrap.Type.List as RList
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+newtype LoadoutOnline = LoadoutOnline
+  { value :: RList.List (RList.List Product.Product)
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON LoadoutOnline where
+  parseJSON = fmap LoadoutOnline . Json.parseJSON
+
+instance Json.ToJSON LoadoutOnline where
+  toJSON = Json.toJSON . value
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-loadout-online"
+    . Schema.json
+    . RList.schema
+    $ RList.schema Product.schema
+
+bitPut :: LoadoutOnline -> BitPut.BitPut
+bitPut loadoutAttribute =
+  let attributes = RList.toList $ value loadoutAttribute
+   in (U8.bitPut . U8.fromWord8 . fromIntegral $ length attributes)
+        <> foldMap Product.putProductAttributes attributes
+
+bitGet ::
+  Version.Version -> Map.Map U32.U32 Str.Str -> BitGet.BitGet LoadoutOnline
+bitGet version objectMap = BitGet.label "LoadoutOnline" $ do
+  size <- BitGet.label "size" U8.bitGet
+  value <-
+    BitGet.label "value"
+      . RList.replicateM (fromIntegral $ U8.toWord8 size)
+      $ Product.decodeProductAttributesBits version objectMap
+  pure LoadoutOnline {value}
diff --git a/src/lib/Rattletrap/Type/Attribute/Loadouts.hs b/src/lib/Rattletrap/Type/Attribute/Loadouts.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Loadouts.hs
@@ -0,0 +1,42 @@
+module Rattletrap.Type.Attribute.Loadouts where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Attribute.Loadout as Loadout
+import qualified Rattletrap.Utility.Json as Json
+
+data Loadouts = Loadouts
+  { blue :: Loadout.Loadout,
+    orange :: Loadout.Loadout
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Loadouts where
+  parseJSON = Json.withObject "Loadouts" $ \object -> do
+    blue <- Json.required object "blue"
+    orange <- Json.required object "orange"
+    pure Loadouts {blue, orange}
+
+instance Json.ToJSON Loadouts where
+  toJSON x =
+    Json.object [Json.pair "blue" $ blue x, Json.pair "orange" $ orange x]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-loadouts" $
+    Schema.object
+      [ (Json.pair "blue" $ Schema.ref Loadout.schema, True),
+        (Json.pair "orange" $ Schema.ref Loadout.schema, True)
+      ]
+
+bitPut :: Loadouts -> BitPut.BitPut
+bitPut loadoutsAttribute =
+  Loadout.bitPut (blue loadoutsAttribute)
+    <> Loadout.bitPut (orange loadoutsAttribute)
+
+bitGet :: BitGet.BitGet Loadouts
+bitGet = BitGet.label "Loadouts" $ do
+  blue <- BitGet.label "blue" Loadout.bitGet
+  orange <- BitGet.label "orange" Loadout.bitGet
+  pure Loadouts {blue, orange}
diff --git a/src/lib/Rattletrap/Type/Attribute/LoadoutsOnline.hs b/src/lib/Rattletrap/Type/Attribute/LoadoutsOnline.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/LoadoutsOnline.hs
@@ -0,0 +1,62 @@
+module Rattletrap.Type.Attribute.LoadoutsOnline where
+
+import qualified Data.Map as Map
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Attribute.LoadoutOnline as LoadoutOnline
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data LoadoutsOnline = LoadoutsOnline
+  { blue :: LoadoutOnline.LoadoutOnline,
+    orange :: LoadoutOnline.LoadoutOnline,
+    unknown1 :: Bool,
+    unknown2 :: Bool
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON LoadoutsOnline where
+  parseJSON = Json.withObject "LoadoutsOnline" $ \object -> do
+    blue <- Json.required object "blue"
+    orange <- Json.required object "orange"
+    unknown1 <- Json.required object "unknown1"
+    unknown2 <- Json.required object "unknown2"
+    pure LoadoutsOnline {blue, orange, unknown1, unknown2}
+
+instance Json.ToJSON LoadoutsOnline where
+  toJSON x =
+    Json.object
+      [ Json.pair "blue" $ blue x,
+        Json.pair "orange" $ orange x,
+        Json.pair "unknown1" $ unknown1 x,
+        Json.pair "unknown2" $ unknown2 x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-loadouts-online" $
+    Schema.object
+      [ (Json.pair "blue" $ Schema.ref LoadoutOnline.schema, True),
+        (Json.pair "orange" $ Schema.ref LoadoutOnline.schema, True),
+        (Json.pair "unknown1" $ Schema.ref Schema.boolean, True),
+        (Json.pair "unknown2" $ Schema.ref Schema.boolean, True)
+      ]
+
+bitPut :: LoadoutsOnline -> BitPut.BitPut
+bitPut loadoutsOnlineAttribute =
+  LoadoutOnline.bitPut (blue loadoutsOnlineAttribute)
+    <> LoadoutOnline.bitPut (orange loadoutsOnlineAttribute)
+    <> BitPut.bool (unknown1 loadoutsOnlineAttribute)
+    <> BitPut.bool (unknown2 loadoutsOnlineAttribute)
+
+bitGet ::
+  Version.Version -> Map.Map U32.U32 Str.Str -> BitGet.BitGet LoadoutsOnline
+bitGet version objectMap = BitGet.label "LoadoutsOnline" $ do
+  blue <- BitGet.label "blue" $ LoadoutOnline.bitGet version objectMap
+  orange <- BitGet.label "orange" $ LoadoutOnline.bitGet version objectMap
+  unknown1 <- BitGet.label "unknown1" BitGet.bool
+  unknown2 <- BitGet.label "unknown2" BitGet.bool
+  pure LoadoutsOnline {blue, orange, unknown1, unknown2}
diff --git a/src/lib/Rattletrap/Type/Attribute/Location.hs b/src/lib/Rattletrap/Type/Attribute/Location.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Location.hs
@@ -0,0 +1,30 @@
+module Rattletrap.Type.Attribute.Location where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Vector as Vector
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Location = Location
+  { value :: Vector.Vector
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Location where
+  parseJSON = fmap Location . Json.parseJSON
+
+instance Json.ToJSON Location where
+  toJSON = Json.toJSON . value
+
+schema :: Schema.Schema
+schema = Schema.named "attribute-location" $ Schema.ref Vector.schema
+
+bitPut :: Location -> BitPut.BitPut
+bitPut locationAttribute = Vector.bitPut (value locationAttribute)
+
+bitGet :: Version.Version -> BitGet.BitGet Location
+bitGet version = BitGet.label "Location" $ do
+  value <- BitGet.label "value" $ Vector.bitGet version
+  pure Location {value}
diff --git a/src/lib/Rattletrap/Type/Attribute/MusicStinger.hs b/src/lib/Rattletrap/Type/Attribute/MusicStinger.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/MusicStinger.hs
@@ -0,0 +1,52 @@
+module Rattletrap.Type.Attribute.MusicStinger where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Utility.Json as Json
+
+data MusicStinger = MusicStinger
+  { flag :: Bool,
+    cue :: U32.U32,
+    trigger :: U8.U8
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON MusicStinger where
+  parseJSON = Json.withObject "MusicStinger" $ \object -> do
+    flag <- Json.required object "flag"
+    cue <- Json.required object "cue"
+    trigger <- Json.required object "trigger"
+    pure MusicStinger {flag, cue, trigger}
+
+instance Json.ToJSON MusicStinger where
+  toJSON x =
+    Json.object
+      [ Json.pair "flag" $ flag x,
+        Json.pair "cue" $ cue x,
+        Json.pair "trigger" $ trigger x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-music-stinger" $
+    Schema.object
+      [ (Json.pair "flag" $ Schema.ref Schema.boolean, True),
+        (Json.pair "cue" $ Schema.ref U32.schema, True),
+        (Json.pair "trigger" $ Schema.ref U8.schema, True)
+      ]
+
+bitPut :: MusicStinger -> BitPut.BitPut
+bitPut musicStingerAttribute =
+  BitPut.bool (flag musicStingerAttribute)
+    <> U32.bitPut (cue musicStingerAttribute)
+    <> U8.bitPut (trigger musicStingerAttribute)
+
+bitGet :: BitGet.BitGet MusicStinger
+bitGet = BitGet.label "MusicStinger" $ do
+  flag <- BitGet.label "flag" BitGet.bool
+  cue <- BitGet.label "cue" U32.bitGet
+  trigger <- BitGet.label "trigger" U8.bitGet
+  pure MusicStinger {flag, cue, trigger}
diff --git a/src/lib/Rattletrap/Type/Attribute/PartyLeader.hs b/src/lib/Rattletrap/Type/Attribute/PartyLeader.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/PartyLeader.hs
@@ -0,0 +1,70 @@
+module Rattletrap.Type.Attribute.PartyLeader where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.RemoteId as RemoteId
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data PartyLeader = PartyLeader
+  { systemId :: U8.U8,
+    remoteId :: Maybe RemoteId.RemoteId,
+    localId :: Maybe U8.U8
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON PartyLeader where
+  parseJSON = Json.withObject "PartyLeader" $ \object -> do
+    systemId <- Json.required object "system_id"
+    maybeId <- Json.optional object "id"
+    pure
+      PartyLeader
+        { systemId,
+          remoteId = fmap fst maybeId,
+          localId = fmap snd maybeId
+        }
+
+instance Json.ToJSON PartyLeader where
+  toJSON x =
+    Json.object
+      [ Json.pair "system_id" $ systemId x,
+        Json.pair "id" $ case (remoteId x, localId x) of
+          (Just r, Just l) -> Just (r, l)
+          _ -> Nothing
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-party-leader" $
+    Schema.object
+      [ (Json.pair "system_id" $ Schema.ref U8.schema, True),
+        ( Json.pair "id" $
+            Schema.oneOf
+              [ Schema.tuple [Schema.ref RemoteId.schema, Schema.ref U8.schema],
+                Schema.ref Schema.null
+              ],
+          False
+        )
+      ]
+
+bitPut :: PartyLeader -> BitPut.BitPut
+bitPut x =
+  U8.bitPut (systemId x)
+    <> foldMap RemoteId.bitPut (remoteId x)
+    <> foldMap
+      U8.bitPut
+      (localId x)
+
+bitGet :: Version.Version -> BitGet.BitGet PartyLeader
+bitGet version = BitGet.label "PartyLeader" $ do
+  systemId <- BitGet.label "systemId" U8.bitGet
+  (remoteId, localId) <-
+    if systemId == U8.fromWord8 0
+      then pure (Nothing, Nothing)
+      else do
+        remoteId <- BitGet.label "remoteId" $ RemoteId.bitGet version systemId
+        localId <- BitGet.label "localId" U8.bitGet
+        pure (Just remoteId, Just localId)
+  pure PartyLeader {systemId, remoteId, localId}
diff --git a/src/lib/Rattletrap/Type/Attribute/Pickup.hs b/src/lib/Rattletrap/Type/Attribute/Pickup.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Pickup.hs
@@ -0,0 +1,52 @@
+module Rattletrap.Type.Attribute.Pickup where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Json as Json
+import qualified Rattletrap.Utility.Monad as Monad
+
+data Pickup = Pickup
+  { instigatorId :: Maybe U32.U32,
+    pickedUp :: Bool
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Pickup where
+  parseJSON = Json.withObject "Pickup" $ \object -> do
+    instigatorId <- Json.optional object "instigator_id"
+    pickedUp <- Json.required object "picked_up"
+    pure Pickup {instigatorId, pickedUp}
+
+instance Json.ToJSON Pickup where
+  toJSON x =
+    Json.object
+      [ Json.pair "instigator_id" $ instigatorId x,
+        Json.pair "picked_up" $ pickedUp x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-pickup" $
+    Schema.object
+      [ (Json.pair "instigator_id" . Schema.json $ Schema.maybe U32.schema, False),
+        (Json.pair "picked_up" $ Schema.ref Schema.boolean, True)
+      ]
+
+bitPut :: Pickup -> BitPut.BitPut
+bitPut x =
+  maybe
+    (BitPut.bool False)
+    (\y -> BitPut.bool True <> U32.bitPut y)
+    (instigatorId x)
+    <> BitPut.bool (pickedUp x)
+
+bitGet :: BitGet.BitGet Pickup
+bitGet = BitGet.label "Pickup" $ do
+  instigator <- BitGet.label "instigator" BitGet.bool
+  instigatorId <-
+    BitGet.label "instigatorId" $
+      Monad.whenMaybe instigator U32.bitGet
+  pickedUp <- BitGet.label "pickedUp" BitGet.bool
+  pure Pickup {instigatorId, pickedUp}
diff --git a/src/lib/Rattletrap/Type/Attribute/PickupInfo.hs b/src/lib/Rattletrap/Type/Attribute/PickupInfo.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/PickupInfo.hs
@@ -0,0 +1,94 @@
+module Rattletrap.Type.Attribute.PickupInfo where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.I32 as I32
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Json as Json
+
+data PickupInfo = PickupInfo
+  { unknown1 :: Bool,
+    unknown2 :: Bool,
+    unknown3 :: U32.U32,
+    unknown4 :: I32.I32,
+    unknown5 :: I32.I32,
+    unknown6 :: Bool,
+    unknown7 :: Bool
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON PickupInfo where
+  parseJSON = Json.withObject "PickupInfo" $ \object -> do
+    unknown1 <- Json.required object "unknown1"
+    unknown2 <- Json.required object "unknown2"
+    unknown3 <- Json.required object "unknown3"
+    unknown4 <- Json.required object "unknown4"
+    unknown5 <- Json.required object "unknown5"
+    unknown6 <- Json.required object "unknown6"
+    unknown7 <- Json.required object "unknown7"
+    pure
+      PickupInfo
+        { unknown1,
+          unknown2,
+          unknown3,
+          unknown4,
+          unknown5,
+          unknown6,
+          unknown7
+        }
+
+instance Json.ToJSON PickupInfo where
+  toJSON x =
+    Json.object
+      [ Json.pair "unknown1" $ unknown1 x,
+        Json.pair "unknown2" $ unknown2 x,
+        Json.pair "unknown3" $ unknown3 x,
+        Json.pair "unknown4" $ unknown4 x,
+        Json.pair "unknown5" $ unknown5 x,
+        Json.pair "unknown6" $ unknown6 x,
+        Json.pair "unknown7" $ unknown7 x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "pickup-info" $
+    Schema.object
+      [ (Json.pair "unknown1" $ Schema.ref Schema.boolean, True),
+        (Json.pair "unknown2" $ Schema.ref Schema.boolean, True),
+        (Json.pair "unknown3" $ Schema.ref U32.schema, True),
+        (Json.pair "unknown4" $ Schema.ref I32.schema, True),
+        (Json.pair "unknown5" $ Schema.ref I32.schema, True),
+        (Json.pair "unknown6" $ Schema.ref Schema.boolean, True),
+        (Json.pair "unknown7" $ Schema.ref Schema.boolean, True)
+      ]
+
+bitPut :: PickupInfo -> BitPut.BitPut
+bitPut x =
+  BitPut.bool (unknown1 x)
+    <> BitPut.bool (unknown2 x)
+    <> U32.bitPut (unknown3 x)
+    <> I32.bitPut (unknown4 x)
+    <> I32.bitPut (unknown5 x)
+    <> BitPut.bool (unknown6 x)
+    <> BitPut.bool (unknown7 x)
+
+bitGet :: BitGet.BitGet PickupInfo
+bitGet = BitGet.label "PickupInfo" $ do
+  unknown1 <- BitGet.label "unknown1" BitGet.bool
+  unknown2 <- BitGet.label "unknown2" BitGet.bool
+  unknown3 <- BitGet.label "unknown3" U32.bitGet
+  unknown4 <- BitGet.label "unknown4" I32.bitGet
+  unknown5 <- BitGet.label "unknown5" I32.bitGet
+  unknown6 <- BitGet.label "unknown6" BitGet.bool
+  unknown7 <- BitGet.label "unknown7" BitGet.bool
+  pure
+    PickupInfo
+      { unknown1,
+        unknown2,
+        unknown3,
+        unknown4,
+        unknown5,
+        unknown6,
+        unknown7
+      }
diff --git a/src/lib/Rattletrap/Type/Attribute/PickupNew.hs b/src/lib/Rattletrap/Type/Attribute/PickupNew.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/PickupNew.hs
@@ -0,0 +1,53 @@
+module Rattletrap.Type.Attribute.PickupNew where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Utility.Json as Json
+import qualified Rattletrap.Utility.Monad as Monad
+
+data PickupNew = PickupNew
+  { instigatorId :: Maybe U32.U32,
+    pickedUp :: U8.U8
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON PickupNew where
+  parseJSON = Json.withObject "PickupNew" $ \object -> do
+    instigatorId <- Json.optional object "instigator_id"
+    pickedUp <- Json.required object "picked_up"
+    pure PickupNew {instigatorId, pickedUp}
+
+instance Json.ToJSON PickupNew where
+  toJSON x =
+    Json.object
+      [ Json.pair "instigator_id" $ instigatorId x,
+        Json.pair "picked_up" $ pickedUp x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-pickup-new" $
+    Schema.object
+      [ (Json.pair "instigator_id" . Schema.json $ Schema.maybe U32.schema, False),
+        (Json.pair "picked_up" $ Schema.ref U8.schema, True)
+      ]
+
+bitPut :: PickupNew -> BitPut.BitPut
+bitPut x =
+  maybe
+    (BitPut.bool False)
+    (\y -> BitPut.bool True <> U32.bitPut y)
+    (instigatorId x)
+    <> U8.bitPut (pickedUp x)
+
+bitGet :: BitGet.BitGet PickupNew
+bitGet = BitGet.label "PickupNew" $ do
+  instigator <- BitGet.label "instigator" BitGet.bool
+  instigatorId <-
+    BitGet.label "instigatorId" $
+      Monad.whenMaybe instigator U32.bitGet
+  pickedUp <- BitGet.label "pickedUp" U8.bitGet
+  pure PickupNew {instigatorId, pickedUp}
diff --git a/src/lib/Rattletrap/Type/Attribute/PlayerHistoryKey.hs b/src/lib/Rattletrap/Type/Attribute/PlayerHistoryKey.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/PlayerHistoryKey.hs
@@ -0,0 +1,30 @@
+module Rattletrap.Type.Attribute.PlayerHistoryKey where
+
+import qualified Data.Word as Word
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Utility.Json as Json
+
+newtype PlayerHistoryKey = PlayerHistoryKey
+  { unknown :: Word.Word16
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON PlayerHistoryKey where
+  parseJSON = fmap PlayerHistoryKey . Json.parseJSON
+
+instance Json.ToJSON PlayerHistoryKey where
+  toJSON = Json.toJSON . unknown
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-player-history-key" $ Schema.ref Schema.number
+
+bitPut :: PlayerHistoryKey -> BitPut.BitPut
+bitPut = BitPut.bits 14 . unknown
+
+bitGet :: BitGet.BitGet PlayerHistoryKey
+bitGet = BitGet.label "PlayerHistoryKey" $ do
+  unknown <- BitGet.label "unknown" $ BitGet.bits 14
+  pure PlayerHistoryKey {unknown}
diff --git a/src/lib/Rattletrap/Type/Attribute/PrivateMatchSettings.hs b/src/lib/Rattletrap/Type/Attribute/PrivateMatchSettings.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/PrivateMatchSettings.hs
@@ -0,0 +1,86 @@
+module Rattletrap.Type.Attribute.PrivateMatchSettings where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Json as Json
+
+data PrivateMatchSettings = PrivateMatchSettings
+  { mutators :: Str.Str,
+    joinableBy :: U32.U32,
+    maxPlayers :: U32.U32,
+    gameName :: Str.Str,
+    password :: Str.Str,
+    flag :: Bool
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON PrivateMatchSettings where
+  parseJSON = Json.withObject "PrivateMatchSettings" $ \object -> do
+    mutators <- Json.required object "mutators"
+    joinableBy <- Json.required object "joinable_by"
+    maxPlayers <- Json.required object "max_players"
+    gameName <- Json.required object "game_name"
+    password <- Json.required object "password"
+    flag <- Json.required object "flag"
+    pure
+      PrivateMatchSettings
+        { mutators,
+          joinableBy,
+          maxPlayers,
+          gameName,
+          password,
+          flag
+        }
+
+instance Json.ToJSON PrivateMatchSettings where
+  toJSON x =
+    Json.object
+      [ Json.pair "mutators" $ mutators x,
+        Json.pair "joinable_by" $ joinableBy x,
+        Json.pair "max_players" $ maxPlayers x,
+        Json.pair "game_name" $ gameName x,
+        Json.pair "password" $ password x,
+        Json.pair "flag" $ flag x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-private-match-settings" $
+    Schema.object
+      [ (Json.pair "mutators" $ Schema.ref Str.schema, True),
+        (Json.pair "joinable_by" $ Schema.ref U32.schema, True),
+        (Json.pair "max_players" $ Schema.ref U32.schema, True),
+        (Json.pair "game_name" $ Schema.ref Str.schema, True),
+        (Json.pair "password" $ Schema.ref Str.schema, True),
+        (Json.pair "flag" $ Schema.ref Schema.boolean, True)
+      ]
+
+bitPut :: PrivateMatchSettings -> BitPut.BitPut
+bitPut privateMatchSettingsAttribute =
+  Str.bitPut (mutators privateMatchSettingsAttribute)
+    <> U32.bitPut (joinableBy privateMatchSettingsAttribute)
+    <> U32.bitPut (maxPlayers privateMatchSettingsAttribute)
+    <> Str.bitPut (gameName privateMatchSettingsAttribute)
+    <> Str.bitPut (password privateMatchSettingsAttribute)
+    <> BitPut.bool (flag privateMatchSettingsAttribute)
+
+bitGet :: BitGet.BitGet PrivateMatchSettings
+bitGet = BitGet.label "PrivateMatchSettings" $ do
+  mutators <- BitGet.label "mutators" Str.bitGet
+  joinableBy <- BitGet.label "joinableBy" U32.bitGet
+  maxPlayers <- BitGet.label "maxPlayers" U32.bitGet
+  gameName <- BitGet.label "gameName" Str.bitGet
+  password <- BitGet.label "password" Str.bitGet
+  flag <- BitGet.label "flag" BitGet.bool
+  pure
+    PrivateMatchSettings
+      { mutators,
+        joinableBy,
+        maxPlayers,
+        gameName,
+        password,
+        flag
+      }
diff --git a/src/lib/Rattletrap/Type/Attribute/Product.hs b/src/lib/Rattletrap/Type/Attribute/Product.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Product.hs
@@ -0,0 +1,78 @@
+module Rattletrap.Type.Attribute.Product where
+
+import qualified Data.Map as Map
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Attribute.ProductValue as ProductValue
+import qualified Rattletrap.Type.List as RList
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data Product = Product
+  { unknown :: Bool,
+    objectId :: U32.U32,
+    -- | read-only
+    objectName :: Maybe Str.Str,
+    value :: ProductValue.ProductValue
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Product where
+  parseJSON = Json.withObject "Product" $ \object -> do
+    unknown <- Json.required object "unknown"
+    objectId <- Json.required object "object_id"
+    objectName <- Json.optional object "object_name"
+    value <- Json.required object "value"
+    pure Product {unknown, objectId, objectName, value}
+
+instance Json.ToJSON Product where
+  toJSON x =
+    Json.object
+      [ Json.pair "unknown" $ unknown x,
+        Json.pair "object_id" $ objectId x,
+        Json.pair "object_name" $ objectName x,
+        Json.pair "value" $ value x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-product" $
+    Schema.object
+      [ (Json.pair "unknown" $ Schema.ref Schema.boolean, True),
+        (Json.pair "object_id" $ Schema.ref U32.schema, True),
+        (Json.pair "object_name" . Schema.json $ Schema.maybe Str.schema, False),
+        (Json.pair "value" $ Schema.ref ProductValue.schema, True)
+      ]
+
+putProductAttributes :: RList.List Product -> BitPut.BitPut
+putProductAttributes attributes =
+  let v = RList.toList attributes
+   in (U8.bitPut . U8.fromWord8 . fromIntegral $ length v) <> foldMap bitPut v
+
+bitPut :: Product -> BitPut.BitPut
+bitPut attribute =
+  BitPut.bool (unknown attribute)
+    <> U32.bitPut (objectId attribute)
+    <> ProductValue.bitPut (value attribute)
+
+decodeProductAttributesBits ::
+  Version.Version ->
+  Map.Map U32.U32 Str.Str ->
+  BitGet.BitGet (RList.List Product)
+decodeProductAttributesBits version objectMap = do
+  size <- U8.bitGet
+  RList.replicateM (fromIntegral $ U8.toWord8 size) $ bitGet version objectMap
+
+bitGet :: Version.Version -> Map.Map U32.U32 Str.Str -> BitGet.BitGet Product
+bitGet version objectMap = BitGet.label "Product" $ do
+  unknown <- BitGet.label "unknown" BitGet.bool
+  objectId <- BitGet.label "objectId" U32.bitGet
+  let objectName = Map.lookup objectId objectMap
+  value <-
+    BitGet.label "value" $
+      ProductValue.bitGet version objectId objectName
+  pure Product {unknown, objectId, objectName, value}
diff --git a/src/lib/Rattletrap/Type/Attribute/ProductValue.hs b/src/lib/Rattletrap/Type/Attribute/ProductValue.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/ProductValue.hs
@@ -0,0 +1,126 @@
+module Rattletrap.Type.Attribute.ProductValue where
+
+import qualified Data.Foldable as Foldable
+import qualified Data.Word as Word
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Exception.MissingProductName as MissingProductName
+import qualified Rattletrap.Exception.UnknownProduct as UnknownProduct
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.CompressedWord as CompressedWord
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+import qualified Rattletrap.Utility.Monad as Monad
+
+data ProductValue
+  = PaintedOld CompressedWord.CompressedWord
+  | PaintedNew Word.Word32
+  | TeamEditionOld CompressedWord.CompressedWord
+  | TeamEditionNew Word.Word32
+  | SpecialEdition Word.Word32
+  | UserColorOld (Maybe Word.Word32)
+  | UserColorNew U32.U32
+  | TitleId Str.Str
+  deriving (Eq, Show)
+
+instance Json.FromJSON ProductValue where
+  parseJSON = Json.withObject "ProductValue" $ \object ->
+    Foldable.asum
+      [ fmap PaintedOld $ Json.required object "painted_old",
+        fmap PaintedNew $ Json.required object "painted_new",
+        fmap TeamEditionOld $ Json.required object "team_edition_old",
+        fmap TeamEditionNew $ Json.required object "team_edition_new",
+        fmap SpecialEdition $ Json.required object "special_edition",
+        fmap UserColorOld $ Json.required object "user_color_old",
+        fmap UserColorNew $ Json.required object "user_color_new",
+        fmap TitleId $ Json.required object "title_id"
+      ]
+
+instance Json.ToJSON ProductValue where
+  toJSON x = case x of
+    PaintedOld y -> Json.object [Json.pair "painted_old" y]
+    PaintedNew y -> Json.object [Json.pair "painted_new" y]
+    TeamEditionOld y -> Json.object [Json.pair "team_edition_old" y]
+    TeamEditionNew y -> Json.object [Json.pair "team_edition_new" y]
+    SpecialEdition y -> Json.object [Json.pair "special_edition" y]
+    UserColorOld y -> Json.object [Json.pair "user_color_old" y]
+    UserColorNew y -> Json.object [Json.pair "user_color_new" y]
+    TitleId y -> Json.object [Json.pair "title_id" y]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-product-value" . Schema.oneOf $
+    fmap
+      (\(k, v) -> Schema.object [(Json.pair k v, True)])
+      [ ("painted_old", Schema.ref CompressedWord.schema),
+        ("painted_new", Schema.ref Schema.integer),
+        ("team_edition_old", Schema.ref CompressedWord.schema),
+        ("team_edition_new", Schema.ref Schema.integer),
+        ("special_edition", Schema.ref Schema.integer),
+        ("user_color_old", Schema.json $ Schema.maybe Schema.integer),
+        ("user_color_new", Schema.ref U32.schema),
+        ("title_id", Schema.ref Str.schema)
+      ]
+
+bitPut :: ProductValue -> BitPut.BitPut
+bitPut val = case val of
+  PaintedOld x -> CompressedWord.bitPut x
+  PaintedNew x -> BitPut.bits 31 x
+  TeamEditionOld x -> CompressedWord.bitPut x
+  TeamEditionNew x -> BitPut.bits 31 x
+  SpecialEdition x -> BitPut.bits 31 x
+  UserColorOld x -> case x of
+    Nothing -> BitPut.bool False
+    Just y -> BitPut.bool True <> BitPut.bits 31 y
+  UserColorNew x -> U32.bitPut x
+  TitleId x -> Str.bitPut x
+
+bitGet ::
+  Version.Version -> U32.U32 -> Maybe Str.Str -> BitGet.BitGet ProductValue
+bitGet version objectId maybeObjectName =
+  BitGet.label "ProductValue" $ case fmap Str.toString maybeObjectName of
+    Just "TAGame.ProductAttribute_Painted_TA" -> decodePainted version
+    Just "TAGame.ProductAttribute_SpecialEdition_TA" -> decodeSpecialEdition
+    Just "TAGame.ProductAttribute_TeamEdition_TA" -> decodeTeamEdition version
+    Just "TAGame.ProductAttribute_TitleID_TA" -> decodeTitle
+    Just "TAGame.ProductAttribute_UserColor_TA" -> decodeColor version
+    Just x -> BitGet.throw $ UnknownProduct.UnknownProduct x
+    Nothing ->
+      BitGet.throw . MissingProductName.MissingProductName $
+        U32.toWord32
+          objectId
+
+decodeSpecialEdition :: BitGet.BitGet ProductValue
+decodeSpecialEdition =
+  BitGet.label "SpecialEdition" . fmap SpecialEdition $ BitGet.bits 31
+
+decodePainted :: Version.Version -> BitGet.BitGet ProductValue
+decodePainted version =
+  BitGet.label "Painted" $
+    if hasNewPainted version
+      then fmap PaintedNew $ BitGet.bits 31
+      else fmap PaintedOld $ CompressedWord.bitGet 13
+
+decodeTeamEdition :: Version.Version -> BitGet.BitGet ProductValue
+decodeTeamEdition version =
+  BitGet.label "TeamEdition" $
+    if hasNewPainted version
+      then fmap TeamEditionNew $ BitGet.bits 31
+      else fmap TeamEditionOld $ CompressedWord.bitGet 13
+
+decodeColor :: Version.Version -> BitGet.BitGet ProductValue
+decodeColor version =
+  BitGet.label "UserColor" $
+    if Version.atLeast 868 23 8 version
+      then fmap UserColorNew U32.bitGet
+      else do
+        hasValue <- BitGet.bool
+        fmap UserColorOld $ Monad.whenMaybe hasValue (BitGet.bits 31)
+
+hasNewPainted :: Version.Version -> Bool
+hasNewPainted = Version.atLeast 868 18 0
+
+decodeTitle :: BitGet.BitGet ProductValue
+decodeTitle = BitGet.label "Title" $ fmap TitleId Str.bitGet
diff --git a/src/lib/Rattletrap/Type/Attribute/QWord.hs b/src/lib/Rattletrap/Type/Attribute/QWord.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/QWord.hs
@@ -0,0 +1,29 @@
+module Rattletrap.Type.Attribute.QWord where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U64 as U64
+import qualified Rattletrap.Utility.Json as Json
+
+newtype QWord = QWord
+  { value :: U64.U64
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON QWord where
+  parseJSON = fmap QWord . Json.parseJSON
+
+instance Json.ToJSON QWord where
+  toJSON = Json.toJSON . value
+
+schema :: Schema.Schema
+schema = Schema.named "attribute-q-word" $ Schema.ref U64.schema
+
+bitPut :: QWord -> BitPut.BitPut
+bitPut qWordAttribute = U64.bitPut (value qWordAttribute)
+
+bitGet :: BitGet.BitGet QWord
+bitGet = BitGet.label "QWord" $ do
+  value <- BitGet.label "value" U64.bitGet
+  pure QWord {value}
diff --git a/src/lib/Rattletrap/Type/Attribute/RepStatTitle.hs b/src/lib/Rattletrap/Type/Attribute/RepStatTitle.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/RepStatTitle.hs
@@ -0,0 +1,59 @@
+module Rattletrap.Type.Attribute.RepStatTitle where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Attribute.FlaggedInt as FlaggedInt
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Json as Json
+
+data RepStatTitle = RepStatTitle
+  { unknown :: Bool,
+    name :: Str.Str,
+    target :: FlaggedInt.FlaggedInt,
+    value :: U32.U32
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON RepStatTitle where
+  parseJSON = Json.withObject "RepStatTitle" $ \object -> do
+    unknown <- Json.required object "unknown"
+    name <- Json.required object "name"
+    target <- Json.required object "target"
+    value <- Json.required object "value"
+    pure RepStatTitle {unknown, name, target, value}
+
+instance Json.ToJSON RepStatTitle where
+  toJSON x =
+    Json.object
+      [ Json.pair "unknown" $ unknown x,
+        Json.pair "name" $ name x,
+        Json.pair "target" $ target x,
+        Json.pair "value" $ value x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-rep-stat-title" $
+    Schema.object
+      [ (Json.pair "unknown" $ Schema.ref Schema.boolean, True),
+        (Json.pair "name" $ Schema.ref Str.schema, True),
+        (Json.pair "target" $ Schema.ref FlaggedInt.schema, True),
+        (Json.pair "value" $ Schema.ref U32.schema, True)
+      ]
+
+bitPut :: RepStatTitle -> BitPut.BitPut
+bitPut x =
+  BitPut.bool (unknown x)
+    <> Str.bitPut (name x)
+    <> FlaggedInt.bitPut (target x)
+    <> U32.bitPut (value x)
+
+bitGet :: BitGet.BitGet RepStatTitle
+bitGet = BitGet.label "RepStatTitle" $ do
+  unknown <- BitGet.label "unknown" BitGet.bool
+  name <- BitGet.label "name" Str.bitGet
+  target <- BitGet.label "target" FlaggedInt.bitGet
+  value <- BitGet.label "value" U32.bitGet
+  pure RepStatTitle {unknown, name, target, value}
diff --git a/src/lib/Rattletrap/Type/Attribute/Reservation.hs b/src/lib/Rattletrap/Type/Attribute/Reservation.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Reservation.hs
@@ -0,0 +1,82 @@
+module Rattletrap.Type.Attribute.Reservation where
+
+import qualified Data.Word as Word
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Attribute.UniqueId as UniqueId
+import qualified Rattletrap.Type.CompressedWord as CompressedWord
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+import qualified Rattletrap.Utility.Monad as Monad
+
+data Reservation = Reservation
+  { number :: CompressedWord.CompressedWord,
+    uniqueId :: UniqueId.UniqueId,
+    name :: Maybe Str.Str,
+    unknown1 :: Bool,
+    unknown2 :: Bool,
+    unknown3 :: Maybe Word.Word8
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Reservation where
+  parseJSON = Json.withObject "Reservation" $ \object -> do
+    number <- Json.required object "number"
+    uniqueId <- Json.required object "unique_id"
+    name <- Json.optional object "name"
+    unknown1 <- Json.required object "unknown1"
+    unknown2 <- Json.required object "unknown2"
+    unknown3 <- Json.optional object "unknown3"
+    pure Reservation {number, uniqueId, name, unknown1, unknown2, unknown3}
+
+instance Json.ToJSON Reservation where
+  toJSON x =
+    Json.object
+      [ Json.pair "number" $ number x,
+        Json.pair "unique_id" $ uniqueId x,
+        Json.pair "name" $ name x,
+        Json.pair "unknown1" $ unknown1 x,
+        Json.pair "unknown2" $ unknown2 x,
+        Json.pair "unknown3" $ unknown3 x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-reservation" $
+    Schema.object
+      [ (Json.pair "number" $ Schema.ref CompressedWord.schema, True),
+        (Json.pair "unique_id" $ Schema.ref UniqueId.schema, True),
+        (Json.pair "name" . Schema.json $ Schema.maybe Str.schema, False),
+        (Json.pair "unknown1" $ Schema.ref Schema.boolean, True),
+        (Json.pair "unknown2" $ Schema.ref Schema.boolean, True),
+        (Json.pair "unknown3" . Schema.json $ Schema.maybe Schema.integer, False)
+      ]
+
+bitPut :: Reservation -> BitPut.BitPut
+bitPut reservationAttribute =
+  CompressedWord.bitPut (number reservationAttribute)
+    <> UniqueId.bitPut (uniqueId reservationAttribute)
+    <> foldMap Str.bitPut (name reservationAttribute)
+    <> BitPut.bool (unknown1 reservationAttribute)
+    <> BitPut.bool (unknown2 reservationAttribute)
+    <> foldMap (BitPut.bits 6) (unknown3 reservationAttribute)
+
+bitGet :: Version.Version -> BitGet.BitGet Reservation
+bitGet version = BitGet.label "Reservation" $ do
+  number <- BitGet.label "number" $ CompressedWord.bitGet 7
+  uniqueId <- BitGet.label "uniqueId" $ UniqueId.bitGet version
+  name <-
+    BitGet.label "name" $
+      Monad.whenMaybe
+        (UniqueId.systemId uniqueId /= U8.fromWord8 0)
+        Str.bitGet
+  unknown1 <- BitGet.label "unknown1" BitGet.bool
+  unknown2 <- BitGet.label "unknown2" BitGet.bool
+  unknown3 <-
+    BitGet.label "unknown3"
+      . Monad.whenMaybe (Version.atLeast 868 12 0 version)
+      $ BitGet.bits 6
+  pure Reservation {number, uniqueId, name, unknown1, unknown2, unknown3}
diff --git a/src/lib/Rattletrap/Type/Attribute/RigidBodyState.hs b/src/lib/Rattletrap/Type/Attribute/RigidBodyState.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/RigidBodyState.hs
@@ -0,0 +1,88 @@
+module Rattletrap.Type.Attribute.RigidBodyState where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Rotation as Rotation
+import qualified Rattletrap.Type.Vector as Vector
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+import qualified Rattletrap.Utility.Monad as Monad
+
+data RigidBodyState = RigidBodyState
+  { sleeping :: Bool,
+    location :: Vector.Vector,
+    rotation :: Rotation.Rotation,
+    linearVelocity :: Maybe Vector.Vector,
+    angularVelocity :: Maybe Vector.Vector
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON RigidBodyState where
+  parseJSON = Json.withObject "RigidBodyState" $ \object -> do
+    sleeping <- Json.required object "sleeping"
+    location <- Json.required object "location"
+    rotation <- Json.required object "rotation"
+    linearVelocity <- Json.optional object "linear_velocity"
+    angularVelocity <- Json.optional object "angular_velocity"
+    pure
+      RigidBodyState
+        { sleeping,
+          location,
+          rotation,
+          linearVelocity,
+          angularVelocity
+        }
+
+instance Json.ToJSON RigidBodyState where
+  toJSON x =
+    Json.object
+      [ Json.pair "sleeping" $ sleeping x,
+        Json.pair "location" $ location x,
+        Json.pair "rotation" $ rotation x,
+        Json.pair "linear_velocity" $ linearVelocity x,
+        Json.pair "angular_velocity" $ angularVelocity x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-rigid-body-state" $
+    Schema.object
+      [ (Json.pair "sleeping" $ Schema.ref Schema.boolean, True),
+        (Json.pair "location" $ Schema.ref Vector.schema, True),
+        (Json.pair "rotation" $ Schema.ref Rotation.schema, True),
+        ( Json.pair "linear_velocity" . Schema.json $ Schema.maybe Vector.schema,
+          False
+        ),
+        ( Json.pair "angular_velocity" . Schema.json $ Schema.maybe Vector.schema,
+          False
+        )
+      ]
+
+bitPut :: RigidBodyState -> BitPut.BitPut
+bitPut rigidBodyStateAttribute =
+  BitPut.bool (sleeping rigidBodyStateAttribute)
+    <> Vector.bitPut (location rigidBodyStateAttribute)
+    <> Rotation.bitPut (rotation rigidBodyStateAttribute)
+    <> foldMap Vector.bitPut (linearVelocity rigidBodyStateAttribute)
+    <> foldMap Vector.bitPut (angularVelocity rigidBodyStateAttribute)
+
+bitGet :: Version.Version -> BitGet.BitGet RigidBodyState
+bitGet version = BitGet.label "RigidBodyState" $ do
+  sleeping <- BitGet.label "sleeping" BitGet.bool
+  location <- BitGet.label "location" $ Vector.bitGet version
+  rotation <- BitGet.label "rotation" $ Rotation.bitGet version
+  linearVelocity <-
+    BitGet.label "linearVelocity" $
+      Monad.whenMaybe (not sleeping) (Vector.bitGet version)
+  angularVelocity <-
+    BitGet.label "angularVelocity" $
+      Monad.whenMaybe (not sleeping) (Vector.bitGet version)
+  pure
+    RigidBodyState
+      { sleeping,
+        location,
+        rotation,
+        linearVelocity,
+        angularVelocity
+      }
diff --git a/src/lib/Rattletrap/Type/Attribute/Rotation.hs b/src/lib/Rattletrap/Type/Attribute/Rotation.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Rotation.hs
@@ -0,0 +1,29 @@
+module Rattletrap.Type.Attribute.Rotation where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Int8Vector as Int8Vector
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Rotation = Rotation
+  { value :: Int8Vector.Int8Vector
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Rotation where
+  parseJSON = fmap Rotation . Json.parseJSON
+
+instance Json.ToJSON Rotation where
+  toJSON = Json.toJSON . value
+
+schema :: Schema.Schema
+schema = Schema.named "attribute-rotation" $ Schema.ref Int8Vector.schema
+
+bitPut :: Rotation -> BitPut.BitPut
+bitPut = Int8Vector.bitPut . value
+
+bitGet :: BitGet.BitGet Rotation
+bitGet = BitGet.label "Rotation" $ do
+  value <- BitGet.label "value" Int8Vector.bitGet
+  pure Rotation {value}
diff --git a/src/lib/Rattletrap/Type/Attribute/StatEvent.hs b/src/lib/Rattletrap/Type/Attribute/StatEvent.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/StatEvent.hs
@@ -0,0 +1,43 @@
+module Rattletrap.Type.Attribute.StatEvent where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.I32 as I32
+import qualified Rattletrap.Utility.Json as Json
+
+data StatEvent = StatEvent
+  { unknown :: Bool,
+    objectId :: I32.I32
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON StatEvent where
+  parseJSON = Json.withObject "StatEvent" $ \object -> do
+    unknown <- Json.required object "unknown"
+    objectId <- Json.required object "object_id"
+    pure StatEvent {unknown, objectId}
+
+instance Json.ToJSON StatEvent where
+  toJSON x =
+    Json.object
+      [Json.pair "unknown" $ unknown x, Json.pair "object_id" $ objectId x]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-stat-event" $
+    Schema.object
+      [ (Json.pair "unknown" $ Schema.ref Schema.boolean, True),
+        (Json.pair "object_id" $ Schema.ref I32.schema, True)
+      ]
+
+bitPut :: StatEvent -> BitPut.BitPut
+bitPut statEventAttribute =
+  BitPut.bool (unknown statEventAttribute)
+    <> I32.bitPut (objectId statEventAttribute)
+
+bitGet :: BitGet.BitGet StatEvent
+bitGet = BitGet.label "StatEvent" $ do
+  unknown <- BitGet.label "unknown" BitGet.bool
+  objectId <- BitGet.label "objectId" I32.bitGet
+  pure StatEvent {unknown, objectId}
diff --git a/src/lib/Rattletrap/Type/Attribute/String.hs b/src/lib/Rattletrap/Type/Attribute/String.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/String.hs
@@ -0,0 +1,30 @@
+module Rattletrap.Type.Attribute.String where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Utility.Json as Json
+import Prelude hiding (String)
+
+newtype String = String
+  { value :: Str.Str
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON String where
+  parseJSON = fmap String . Json.parseJSON
+
+instance Json.ToJSON String where
+  toJSON = Json.toJSON . value
+
+schema :: Schema.Schema
+schema = Schema.named "attribute-string" $ Schema.ref Str.schema
+
+bitPut :: String -> BitPut.BitPut
+bitPut stringAttribute = Str.bitPut (value stringAttribute)
+
+bitGet :: BitGet.BitGet String
+bitGet = BitGet.label "String" $ do
+  value <- BitGet.label "value" Str.bitGet
+  pure String {value}
diff --git a/src/lib/Rattletrap/Type/Attribute/TeamPaint.hs b/src/lib/Rattletrap/Type/Attribute/TeamPaint.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/TeamPaint.hs
@@ -0,0 +1,78 @@
+module Rattletrap.Type.Attribute.TeamPaint where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Utility.Json as Json
+
+data TeamPaint = TeamPaint
+  { team :: U8.U8,
+    primaryColor :: U8.U8,
+    accentColor :: U8.U8,
+    primaryFinish :: U32.U32,
+    accentFinish :: U32.U32
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON TeamPaint where
+  parseJSON = Json.withObject "TeamPaint" $ \object -> do
+    team <- Json.required object "team"
+    primaryColor <- Json.required object "primary_color"
+    accentColor <- Json.required object "accent_color"
+    primaryFinish <- Json.required object "primary_finish"
+    accentFinish <- Json.required object "accent_finish"
+    pure
+      TeamPaint
+        { team,
+          primaryColor,
+          accentColor,
+          primaryFinish,
+          accentFinish
+        }
+
+instance Json.ToJSON TeamPaint where
+  toJSON x =
+    Json.object
+      [ Json.pair "team" $ team x,
+        Json.pair "primary_color" $ primaryColor x,
+        Json.pair "accent_color" $ accentColor x,
+        Json.pair "primary_finish" $ primaryFinish x,
+        Json.pair "accent_finish" $ accentFinish x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-team-paint" $
+    Schema.object
+      [ (Json.pair "team" $ Schema.ref U8.schema, True),
+        (Json.pair "primary_color" $ Schema.ref U8.schema, True),
+        (Json.pair "accent_color" $ Schema.ref U8.schema, True),
+        (Json.pair "primary_finish" $ Schema.ref U32.schema, True),
+        (Json.pair "accent_finish" $ Schema.ref U32.schema, True)
+      ]
+
+bitPut :: TeamPaint -> BitPut.BitPut
+bitPut teamPaintAttribute =
+  U8.bitPut (team teamPaintAttribute)
+    <> U8.bitPut (primaryColor teamPaintAttribute)
+    <> U8.bitPut (accentColor teamPaintAttribute)
+    <> U32.bitPut (primaryFinish teamPaintAttribute)
+    <> U32.bitPut (accentFinish teamPaintAttribute)
+
+bitGet :: BitGet.BitGet TeamPaint
+bitGet = BitGet.label "TeamPaint" $ do
+  team <- BitGet.label "team" U8.bitGet
+  primaryColor <- BitGet.label "primaryColor" U8.bitGet
+  accentColor <- BitGet.label "accentColor" U8.bitGet
+  primaryFinish <- BitGet.label "primaryFinish" U32.bitGet
+  accentFinish <- BitGet.label "accentFinish" U32.bitGet
+  pure
+    TeamPaint
+      { team,
+        primaryColor,
+        accentColor,
+        primaryFinish,
+        accentFinish
+      }
diff --git a/src/lib/Rattletrap/Type/Attribute/Title.hs b/src/lib/Rattletrap/Type/Attribute/Title.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/Title.hs
@@ -0,0 +1,101 @@
+module Rattletrap.Type.Attribute.Title where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Json as Json
+
+data Title = Title
+  { unknown1 :: Bool,
+    unknown2 :: Bool,
+    unknown3 :: U32.U32,
+    unknown4 :: U32.U32,
+    unknown5 :: U32.U32,
+    unknown6 :: U32.U32,
+    unknown7 :: U32.U32,
+    unknown8 :: Bool
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Title where
+  parseJSON = Json.withObject "Title" $ \object -> do
+    unknown1 <- Json.required object "unknown1"
+    unknown2 <- Json.required object "unknown2"
+    unknown3 <- Json.required object "unknown3"
+    unknown4 <- Json.required object "unknown4"
+    unknown5 <- Json.required object "unknown5"
+    unknown6 <- Json.required object "unknown6"
+    unknown7 <- Json.required object "unknown7"
+    unknown8 <- Json.required object "unknown8"
+    pure
+      Title
+        { unknown1,
+          unknown2,
+          unknown3,
+          unknown4,
+          unknown5,
+          unknown6,
+          unknown7,
+          unknown8
+        }
+
+instance Json.ToJSON Title where
+  toJSON x =
+    Json.object
+      [ Json.pair "unknown1" $ unknown1 x,
+        Json.pair "unknown2" $ unknown2 x,
+        Json.pair "unknown3" $ unknown3 x,
+        Json.pair "unknown4" $ unknown4 x,
+        Json.pair "unknown5" $ unknown5 x,
+        Json.pair "unknown6" $ unknown6 x,
+        Json.pair "unknown7" $ unknown7 x,
+        Json.pair "unknown8" $ unknown8 x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-title" $
+    Schema.object
+      [ (Json.pair "unknown1" $ Schema.ref Schema.boolean, True),
+        (Json.pair "unknown2" $ Schema.ref Schema.boolean, True),
+        (Json.pair "unknown3" $ Schema.ref U32.schema, True),
+        (Json.pair "unknown4" $ Schema.ref U32.schema, True),
+        (Json.pair "unknown5" $ Schema.ref U32.schema, True),
+        (Json.pair "unknown6" $ Schema.ref U32.schema, True),
+        (Json.pair "unknown7" $ Schema.ref U32.schema, True),
+        (Json.pair "unknown8" $ Schema.ref Schema.boolean, True)
+      ]
+
+bitPut :: Title -> BitPut.BitPut
+bitPut titleAttribute =
+  BitPut.bool (unknown1 titleAttribute)
+    <> BitPut.bool (unknown2 titleAttribute)
+    <> U32.bitPut (unknown3 titleAttribute)
+    <> U32.bitPut (unknown4 titleAttribute)
+    <> U32.bitPut (unknown5 titleAttribute)
+    <> U32.bitPut (unknown6 titleAttribute)
+    <> U32.bitPut (unknown7 titleAttribute)
+    <> BitPut.bool (unknown8 titleAttribute)
+
+bitGet :: BitGet.BitGet Title
+bitGet = BitGet.label "Title" $ do
+  unknown1 <- BitGet.label "unknown1" BitGet.bool
+  unknown2 <- BitGet.label "unknown2" BitGet.bool
+  unknown3 <- BitGet.label "unknown3" U32.bitGet
+  unknown4 <- BitGet.label "unknown4" U32.bitGet
+  unknown5 <- BitGet.label "unknown5" U32.bitGet
+  unknown6 <- BitGet.label "unknown6" U32.bitGet
+  unknown7 <- BitGet.label "unknown7" U32.bitGet
+  unknown8 <- BitGet.label "unknown8" BitGet.bool
+  pure
+    Title
+      { unknown1,
+        unknown2,
+        unknown3,
+        unknown4,
+        unknown5,
+        unknown6,
+        unknown7,
+        unknown8
+      }
diff --git a/src/lib/Rattletrap/Type/Attribute/UniqueId.hs b/src/lib/Rattletrap/Type/Attribute/UniqueId.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/UniqueId.hs
@@ -0,0 +1,53 @@
+module Rattletrap.Type.Attribute.UniqueId where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.RemoteId as RemoteId
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data UniqueId = UniqueId
+  { systemId :: U8.U8,
+    remoteId :: RemoteId.RemoteId,
+    localId :: U8.U8
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON UniqueId where
+  parseJSON = Json.withObject "UniqueId" $ \object -> do
+    systemId <- Json.required object "system_id"
+    remoteId <- Json.required object "remote_id"
+    localId <- Json.required object "local_id"
+    pure UniqueId {systemId, remoteId, localId}
+
+instance Json.ToJSON UniqueId where
+  toJSON x =
+    Json.object
+      [ Json.pair "system_id" $ systemId x,
+        Json.pair "remote_id" $ remoteId x,
+        Json.pair "local_id" $ localId x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-unique-id" $
+    Schema.object
+      [ (Json.pair "system_id" $ Schema.ref U8.schema, True),
+        (Json.pair "remote_id" $ Schema.ref RemoteId.schema, True),
+        (Json.pair "local_id" $ Schema.ref U8.schema, True)
+      ]
+
+bitPut :: UniqueId -> BitPut.BitPut
+bitPut uniqueIdAttribute =
+  U8.bitPut (systemId uniqueIdAttribute)
+    <> RemoteId.bitPut (remoteId uniqueIdAttribute)
+    <> U8.bitPut (localId uniqueIdAttribute)
+
+bitGet :: Version.Version -> BitGet.BitGet UniqueId
+bitGet version = BitGet.label "UniqueId" $ do
+  systemId <- BitGet.label "systemId" U8.bitGet
+  remoteId <- BitGet.label "remoteId" $ RemoteId.bitGet version systemId
+  localId <- BitGet.label "localId" U8.bitGet
+  pure UniqueId {systemId, remoteId, localId}
diff --git a/src/lib/Rattletrap/Type/Attribute/WeldedInfo.hs b/src/lib/Rattletrap/Type/Attribute/WeldedInfo.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Attribute/WeldedInfo.hs
@@ -0,0 +1,67 @@
+module Rattletrap.Type.Attribute.WeldedInfo where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.F32 as F32
+import qualified Rattletrap.Type.I32 as I32
+import qualified Rattletrap.Type.Int8Vector as Int8Vector
+import qualified Rattletrap.Type.Vector as Vector
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data WeldedInfo = WeldedInfo
+  { active :: Bool,
+    actorId :: I32.I32,
+    offset :: Vector.Vector,
+    mass :: F32.F32,
+    rotation :: Int8Vector.Int8Vector
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON WeldedInfo where
+  parseJSON = Json.withObject "WeldedInfo" $ \object -> do
+    active <- Json.required object "active"
+    actorId <- Json.required object "actor_id"
+    offset <- Json.required object "offset"
+    mass <- Json.required object "mass"
+    rotation <- Json.required object "rotation"
+    pure WeldedInfo {active, actorId, offset, mass, rotation}
+
+instance Json.ToJSON WeldedInfo where
+  toJSON x =
+    Json.object
+      [ Json.pair "active" $ active x,
+        Json.pair "actor_id" $ actorId x,
+        Json.pair "offset" $ offset x,
+        Json.pair "mass" $ mass x,
+        Json.pair "rotation" $ rotation x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-welded-info" $
+    Schema.object
+      [ (Json.pair "active" $ Schema.ref Schema.boolean, True),
+        (Json.pair "actor_id" $ Schema.ref I32.schema, True),
+        (Json.pair "offset" $ Schema.ref Vector.schema, True),
+        (Json.pair "mass" $ Schema.ref F32.schema, True),
+        (Json.pair "rotation" $ Schema.ref Int8Vector.schema, True)
+      ]
+
+bitPut :: WeldedInfo -> BitPut.BitPut
+bitPut weldedInfoAttribute =
+  BitPut.bool (active weldedInfoAttribute)
+    <> I32.bitPut (actorId weldedInfoAttribute)
+    <> Vector.bitPut (offset weldedInfoAttribute)
+    <> F32.bitPut (mass weldedInfoAttribute)
+    <> Int8Vector.bitPut (rotation weldedInfoAttribute)
+
+bitGet :: Version.Version -> BitGet.BitGet WeldedInfo
+bitGet version = BitGet.label "WeldedInfo" $ do
+  active <- BitGet.label "active" BitGet.bool
+  actorId <- BitGet.label "actorId" I32.bitGet
+  offset <- BitGet.label "offset" $ Vector.bitGet version
+  mass <- BitGet.label "mass" F32.bitGet
+  rotation <- BitGet.label "rotation" Int8Vector.bitGet
+  pure WeldedInfo {active, actorId, offset, mass, rotation}
diff --git a/src/lib/Rattletrap/Type/AttributeMapping.hs b/src/lib/Rattletrap/Type/AttributeMapping.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/AttributeMapping.hs
@@ -0,0 +1,41 @@
+module Rattletrap.Type.AttributeMapping where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Json as Json
+
+data AttributeMapping = AttributeMapping
+  { objectId :: U32.U32,
+    streamId :: U32.U32
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON AttributeMapping where
+  parseJSON = Json.withObject "AttributeMapping" $ \object -> do
+    objectId <- Json.required object "object_id"
+    streamId <- Json.required object "stream_id"
+    pure AttributeMapping {objectId, streamId}
+
+instance Json.ToJSON AttributeMapping where
+  toJSON x =
+    Json.object
+      [Json.pair "object_id" $ objectId x, Json.pair "stream_id" $ streamId x]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attributeMapping" $
+    Schema.object
+      [ (Json.pair "object_id" $ Schema.ref U32.schema, True),
+        (Json.pair "stream_id" $ Schema.ref U32.schema, True)
+      ]
+
+bytePut :: AttributeMapping -> BytePut.BytePut
+bytePut x = U32.bytePut (objectId x) <> U32.bytePut (streamId x)
+
+byteGet :: ByteGet.ByteGet AttributeMapping
+byteGet = ByteGet.label "AttributeMapping" $ do
+  objectId <- ByteGet.label "objectId" U32.byteGet
+  streamId <- ByteGet.label "streamId" U32.byteGet
+  pure AttributeMapping {objectId, streamId}
diff --git a/src/lib/Rattletrap/Type/AttributeType.hs b/src/lib/Rattletrap/Type/AttributeType.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/AttributeType.hs
@@ -0,0 +1,47 @@
+module Rattletrap.Type.AttributeType where
+
+data AttributeType
+  = AppliedDamage
+  | Boolean
+  | Boost
+  | Byte
+  | CamSettings
+  | ClubColors
+  | CustomDemolish
+  | CustomDemolishExtended
+  | DamageState
+  | Demolish
+  | Enum
+  | Explosion
+  | ExtendedExplosion
+  | FlaggedInt
+  | FlaggedByte
+  | Float
+  | GameMode
+  | GameServer
+  | Int
+  | Int64
+  | Loadout
+  | LoadoutOnline
+  | Loadouts
+  | LoadoutsOnline
+  | Location
+  | MusicStinger
+  | PartyLeader
+  | Pickup
+  | PickupInfo
+  | PickupNew
+  | PlayerHistoryKey
+  | PrivateMatchSettings
+  | QWord
+  | RepStatTitle
+  | Reservation
+  | RigidBodyState
+  | Rotation
+  | StatEvent
+  | String
+  | TeamPaint
+  | Title
+  | UniqueId
+  | WeldedInfo
+  deriving (Eq, Show)
diff --git a/src/lib/Rattletrap/Type/AttributeValue.hs b/src/lib/Rattletrap/Type/AttributeValue.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/AttributeValue.hs
@@ -0,0 +1,364 @@
+module Rattletrap.Type.AttributeValue where
+
+import qualified Data.Foldable as Foldable
+import qualified Data.Map as Map
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Data as Data
+import qualified Rattletrap.Exception.UnknownAttribute as UnknownAttribute
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Attribute.AppliedDamage as AppliedDamage
+import qualified Rattletrap.Type.Attribute.Boolean as Boolean
+import qualified Rattletrap.Type.Attribute.Boost as Boost
+import qualified Rattletrap.Type.Attribute.Byte as Byte
+import qualified Rattletrap.Type.Attribute.CamSettings as CamSettings
+import qualified Rattletrap.Type.Attribute.ClubColors as ClubColors
+import qualified Rattletrap.Type.Attribute.CustomDemolish as CustomDemolish
+import qualified Rattletrap.Type.Attribute.CustomDemolishExtended as CustomDemolishExtended
+import qualified Rattletrap.Type.Attribute.DamageState as DamageState
+import qualified Rattletrap.Type.Attribute.Demolish as Demolish
+import qualified Rattletrap.Type.Attribute.Enum as Enum
+import qualified Rattletrap.Type.Attribute.Explosion as Explosion
+import qualified Rattletrap.Type.Attribute.ExtendedExplosion as ExtendedExplosion
+import qualified Rattletrap.Type.Attribute.FlaggedByte as FlaggedByte
+import qualified Rattletrap.Type.Attribute.FlaggedInt as FlaggedInt
+import qualified Rattletrap.Type.Attribute.Float as Float
+import qualified Rattletrap.Type.Attribute.GameMode as GameMode
+import qualified Rattletrap.Type.Attribute.GameServer as GameServer
+import qualified Rattletrap.Type.Attribute.Int as Int
+import qualified Rattletrap.Type.Attribute.Int64 as Int64
+import qualified Rattletrap.Type.Attribute.Loadout as Loadout
+import qualified Rattletrap.Type.Attribute.LoadoutOnline as LoadoutOnline
+import qualified Rattletrap.Type.Attribute.Loadouts as Loadouts
+import qualified Rattletrap.Type.Attribute.LoadoutsOnline as LoadoutsOnline
+import qualified Rattletrap.Type.Attribute.Location as Location
+import qualified Rattletrap.Type.Attribute.MusicStinger as MusicStinger
+import qualified Rattletrap.Type.Attribute.PartyLeader as PartyLeader
+import qualified Rattletrap.Type.Attribute.Pickup as Pickup
+import qualified Rattletrap.Type.Attribute.PickupInfo as PickupInfo
+import qualified Rattletrap.Type.Attribute.PickupNew as PickupNew
+import qualified Rattletrap.Type.Attribute.PlayerHistoryKey as PlayerHistoryKey
+import qualified Rattletrap.Type.Attribute.PrivateMatchSettings as PrivateMatchSettings
+import qualified Rattletrap.Type.Attribute.QWord as QWord
+import qualified Rattletrap.Type.Attribute.RepStatTitle as RepStatTitle
+import qualified Rattletrap.Type.Attribute.Reservation as Reservation
+import qualified Rattletrap.Type.Attribute.RigidBodyState as RigidBodyState
+import qualified Rattletrap.Type.Attribute.Rotation as Rotation
+import qualified Rattletrap.Type.Attribute.StatEvent as StatEvent
+import qualified Rattletrap.Type.Attribute.String as String
+import qualified Rattletrap.Type.Attribute.TeamPaint as TeamPaint
+import qualified Rattletrap.Type.Attribute.Title as Title
+import qualified Rattletrap.Type.Attribute.UniqueId as UniqueId
+import qualified Rattletrap.Type.Attribute.WeldedInfo as WeldedInfo
+import qualified Rattletrap.Type.AttributeType as AttributeType
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data AttributeValue
+  = AppliedDamage AppliedDamage.AppliedDamage
+  | Boolean Boolean.Boolean
+  | Boost Boost.Boost
+  | Byte Byte.Byte
+  | CamSettings CamSettings.CamSettings
+  | ClubColors ClubColors.ClubColors
+  | CustomDemolish CustomDemolish.CustomDemolish
+  | CustomDemolishExtended CustomDemolishExtended.CustomDemolishExtended
+  | DamageState DamageState.DamageState
+  | Demolish Demolish.Demolish
+  | Enum Enum.Enum
+  | Explosion Explosion.Explosion
+  | ExtendedExplosion ExtendedExplosion.ExtendedExplosion
+  | FlaggedInt FlaggedInt.FlaggedInt
+  | FlaggedByte FlaggedByte.FlaggedByte
+  | Float Float.Float
+  | GameMode GameMode.GameMode
+  | GameServer GameServer.GameServer
+  | Int Int.Int
+  | Int64 Int64.Int64
+  | Loadout Loadout.Loadout
+  | LoadoutOnline LoadoutOnline.LoadoutOnline
+  | Loadouts Loadouts.Loadouts
+  | LoadoutsOnline LoadoutsOnline.LoadoutsOnline
+  | Location Location.Location
+  | MusicStinger MusicStinger.MusicStinger
+  | PartyLeader PartyLeader.PartyLeader
+  | Pickup Pickup.Pickup
+  | PickupInfo PickupInfo.PickupInfo
+  | PickupNew PickupNew.PickupNew
+  | PlayerHistoryKey PlayerHistoryKey.PlayerHistoryKey
+  | PrivateMatchSettings PrivateMatchSettings.PrivateMatchSettings
+  | QWord QWord.QWord
+  | RepStatTitle RepStatTitle.RepStatTitle
+  | Reservation Reservation.Reservation
+  | RigidBodyState RigidBodyState.RigidBodyState
+  | Rotation Rotation.Rotation
+  | StatEvent StatEvent.StatEvent
+  | String String.String
+  | TeamPaint TeamPaint.TeamPaint
+  | Title Title.Title
+  | UniqueId UniqueId.UniqueId
+  | WeldedInfo WeldedInfo.WeldedInfo
+  deriving (Eq, Show)
+
+instance Json.FromJSON AttributeValue where
+  parseJSON = Json.withObject "AttributeValue" $ \object ->
+    Foldable.asum
+      [ fmap AppliedDamage $ Json.required object "applied_damage",
+        fmap Boolean $ Json.required object "boolean",
+        fmap Boost $ Json.required object "boost",
+        fmap Byte $ Json.required object "byte",
+        fmap CamSettings $ Json.required object "cam_settings",
+        fmap ClubColors $ Json.required object "club_colors",
+        fmap CustomDemolish $ Json.required object "custom_demolish",
+        fmap CustomDemolishExtended $ Json.required object "custom_demolish_extended",
+        fmap DamageState $ Json.required object "damage_state",
+        fmap Demolish $ Json.required object "demolish",
+        fmap Enum $ Json.required object "enum",
+        fmap Explosion $ Json.required object "explosion",
+        fmap ExtendedExplosion $ Json.required object "extended_explosion",
+        fmap FlaggedByte $ Json.required object "flagged_byte",
+        fmap FlaggedInt $ Json.required object "flagged_int",
+        fmap Float $ Json.required object "float",
+        fmap GameMode $ Json.required object "game_mode",
+        fmap GameServer $ Json.required object "game_server",
+        fmap Int $ Json.required object "int",
+        fmap Int64 $ Json.required object "int64",
+        fmap Loadout $ Json.required object "loadout",
+        fmap LoadoutOnline $ Json.required object "loadout_online",
+        fmap Loadouts $ Json.required object "loadouts",
+        fmap LoadoutsOnline $ Json.required object "loadouts_online",
+        fmap Location $ Json.required object "location",
+        fmap MusicStinger $ Json.required object "music_stinger",
+        fmap PartyLeader $ Json.required object "party_leader",
+        fmap Pickup $ Json.required object "pickup",
+        fmap PickupInfo $ Json.required object "pickup_info",
+        fmap PickupNew $ Json.required object "pickup_new",
+        fmap PlayerHistoryKey $ Json.required object "player_history_key",
+        fmap PrivateMatchSettings $ Json.required object "private_match_settings",
+        fmap QWord $ Json.required object "q_word",
+        fmap RepStatTitle $ Json.required object "rep_stat_title",
+        fmap Reservation $ Json.required object "reservation",
+        fmap RigidBodyState $ Json.required object "rigid_body_state",
+        fmap Rotation $ Json.required object "rotation",
+        fmap StatEvent $ Json.required object "stat_event",
+        fmap String $ Json.required object "string",
+        fmap TeamPaint $ Json.required object "team_paint",
+        fmap Title $ Json.required object "title",
+        fmap UniqueId $ Json.required object "unique_id",
+        fmap WeldedInfo $ Json.required object "welded_info"
+      ]
+
+instance Json.ToJSON AttributeValue where
+  toJSON x = case x of
+    AppliedDamage y -> Json.object [Json.pair "applied_damage" y]
+    Boolean y -> Json.object [Json.pair "boolean" y]
+    Boost y -> Json.object [Json.pair "boost" y]
+    Byte y -> Json.object [Json.pair "byte" y]
+    CamSettings y -> Json.object [Json.pair "cam_settings" y]
+    ClubColors y -> Json.object [Json.pair "club_colors" y]
+    CustomDemolish y -> Json.object [Json.pair "custom_demolish" y]
+    CustomDemolishExtended y -> Json.object [Json.pair "custom_demolish_extended" y]
+    DamageState y -> Json.object [Json.pair "damage_state" y]
+    Demolish y -> Json.object [Json.pair "demolish" y]
+    Enum y -> Json.object [Json.pair "enum" y]
+    Explosion y -> Json.object [Json.pair "explosion" y]
+    ExtendedExplosion y -> Json.object [Json.pair "extended_explosion" y]
+    FlaggedByte y -> Json.object [Json.pair "flagged_byte" y]
+    FlaggedInt y -> Json.object [Json.pair "flagged_int" y]
+    Float y -> Json.object [Json.pair "float" y]
+    GameMode y -> Json.object [Json.pair "game_mode" y]
+    GameServer y -> Json.object [Json.pair "game_server" y]
+    Int y -> Json.object [Json.pair "int" y]
+    Int64 y -> Json.object [Json.pair "int64" y]
+    Loadout y -> Json.object [Json.pair "loadout" y]
+    LoadoutOnline y -> Json.object [Json.pair "loadout_online" y]
+    Loadouts y -> Json.object [Json.pair "loadouts" y]
+    LoadoutsOnline y -> Json.object [Json.pair "loadouts_online" y]
+    Location y -> Json.object [Json.pair "location" y]
+    MusicStinger y -> Json.object [Json.pair "music_stinger" y]
+    PartyLeader y -> Json.object [Json.pair "party_leader" y]
+    Pickup y -> Json.object [Json.pair "pickup" y]
+    PickupInfo y -> Json.object [Json.pair "pickup_info" y]
+    PickupNew y -> Json.object [Json.pair "pickup_new" y]
+    PlayerHistoryKey y -> Json.object [Json.pair "player_history_key" y]
+    PrivateMatchSettings y ->
+      Json.object [Json.pair "private_match_settings" y]
+    QWord y -> Json.object [Json.pair "q_word" y]
+    RepStatTitle y -> Json.object [Json.pair "rep_stat_title" y]
+    Reservation y -> Json.object [Json.pair "reservation" y]
+    RigidBodyState y -> Json.object [Json.pair "rigid_body_state" y]
+    Rotation y -> Json.object [Json.pair "rotation" y]
+    StatEvent y -> Json.object [Json.pair "stat_event" y]
+    String y -> Json.object [Json.pair "string" y]
+    TeamPaint y -> Json.object [Json.pair "team_paint" y]
+    Title y -> Json.object [Json.pair "title" y]
+    UniqueId y -> Json.object [Json.pair "unique_id" y]
+    WeldedInfo y -> Json.object [Json.pair "welded_info" y]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "attribute-value" . Schema.oneOf $
+    fmap
+      (\(k, v) -> Schema.object [(Json.pair k $ Schema.ref v, True)])
+      [ ("applied_damage", AppliedDamage.schema),
+        ("boolean", Boolean.schema),
+        ("boost", Boost.schema),
+        ("byte", Byte.schema),
+        ("cam_settings", CamSettings.schema),
+        ("club_colors", ClubColors.schema),
+        ("custom_demolish", CustomDemolish.schema),
+        ("custom_demolish_extended", CustomDemolishExtended.schema),
+        ("damage_state", DamageState.schema),
+        ("demolish", Demolish.schema),
+        ("enum", Enum.schema),
+        ("explosion", Explosion.schema),
+        ("extended_explosion", ExtendedExplosion.schema),
+        ("flagged_byte", FlaggedByte.schema),
+        ("flagged_int", FlaggedInt.schema),
+        ("float", Float.schema),
+        ("game_mode", GameMode.schema),
+        ("game_server", GameServer.schema),
+        ("int", Int.schema),
+        ("int64", Int64.schema),
+        ("loadout_online", LoadoutOnline.schema),
+        ("loadout", Loadout.schema),
+        ("loadouts_online", LoadoutsOnline.schema),
+        ("loadouts", Loadouts.schema),
+        ("location", Location.schema),
+        ("music_stinger", MusicStinger.schema),
+        ("party_leader", PartyLeader.schema),
+        ("pickup_info", PickupInfo.schema),
+        ("pickup_new", PickupNew.schema),
+        ("pickup", Pickup.schema),
+        ("player_history_key", PlayerHistoryKey.schema),
+        ("private_match_settings", PrivateMatchSettings.schema),
+        ("q_word", QWord.schema),
+        ("rep_stat_title", RepStatTitle.schema),
+        ("reservation", Reservation.schema),
+        ("rigid_body_state", RigidBodyState.schema),
+        ("rotation", Rotation.schema),
+        ("stat_event", StatEvent.schema),
+        ("string", String.schema),
+        ("team_paint", TeamPaint.schema),
+        ("title", Title.schema),
+        ("unique_id", UniqueId.schema),
+        ("welded_info", WeldedInfo.schema)
+      ]
+
+bitPut :: AttributeValue -> BitPut.BitPut
+bitPut value = case value of
+  AppliedDamage x -> AppliedDamage.bitPut x
+  Boolean x -> Boolean.bitPut x
+  Boost x -> Boost.bitPut x
+  Byte x -> Byte.bitPut x
+  CamSettings x -> CamSettings.bitPut x
+  ClubColors x -> ClubColors.bitPut x
+  CustomDemolish x -> CustomDemolish.bitPut x
+  CustomDemolishExtended x -> CustomDemolishExtended.bitPut x
+  DamageState x -> DamageState.bitPut x
+  Demolish x -> Demolish.bitPut x
+  Enum x -> Enum.bitPut x
+  Explosion x -> Explosion.bitPut x
+  ExtendedExplosion x -> ExtendedExplosion.bitPut x
+  FlaggedInt x -> FlaggedInt.bitPut x
+  FlaggedByte x -> FlaggedByte.bitPut x
+  Float x -> Float.bitPut x
+  GameMode x -> GameMode.bitPut x
+  GameServer x -> GameServer.bitPut x
+  Int x -> Int.bitPut x
+  Int64 x -> Int64.putInt64Attribute x
+  Loadout x -> Loadout.bitPut x
+  LoadoutOnline x -> LoadoutOnline.bitPut x
+  Loadouts x -> Loadouts.bitPut x
+  LoadoutsOnline x -> LoadoutsOnline.bitPut x
+  Location x -> Location.bitPut x
+  MusicStinger x -> MusicStinger.bitPut x
+  PartyLeader x -> PartyLeader.bitPut x
+  Pickup x -> Pickup.bitPut x
+  PickupInfo x -> PickupInfo.bitPut x
+  PickupNew x -> PickupNew.bitPut x
+  PlayerHistoryKey x -> PlayerHistoryKey.bitPut x
+  PrivateMatchSettings x -> PrivateMatchSettings.bitPut x
+  QWord x -> QWord.bitPut x
+  RepStatTitle x -> RepStatTitle.bitPut x
+  Reservation x -> Reservation.bitPut x
+  RigidBodyState x -> RigidBodyState.bitPut x
+  Rotation x -> Rotation.bitPut x
+  StatEvent x -> StatEvent.bitPut x
+  String x -> String.bitPut x
+  TeamPaint x -> TeamPaint.bitPut x
+  Title x -> Title.bitPut x
+  UniqueId x -> UniqueId.bitPut x
+  WeldedInfo x -> WeldedInfo.bitPut x
+
+bitGet ::
+  Version.Version ->
+  Maybe Str.Str ->
+  Map.Map U32.U32 Str.Str ->
+  Str.Str ->
+  BitGet.BitGet AttributeValue
+bitGet version buildVersion objectMap name =
+  BitGet.label "AttributeValue" $ do
+    constructor <- case Map.lookup (Str.toText name) Data.attributeTypes of
+      Nothing ->
+        BitGet.throw . UnknownAttribute.UnknownAttribute $ Str.toString name
+      Just x -> pure x
+    case constructor of
+      AttributeType.AppliedDamage ->
+        fmap AppliedDamage $ AppliedDamage.bitGet version
+      AttributeType.Boolean -> fmap Boolean Boolean.bitGet
+      AttributeType.Boost -> fmap Boost Boost.bitGet
+      AttributeType.Byte -> fmap Byte Byte.bitGet
+      AttributeType.CamSettings ->
+        fmap CamSettings $ CamSettings.bitGet version
+      AttributeType.ClubColors -> fmap ClubColors ClubColors.bitGet
+      AttributeType.CustomDemolish ->
+        fmap CustomDemolish $ CustomDemolish.bitGet version
+      AttributeType.CustomDemolishExtended -> fmap CustomDemolishExtended $ CustomDemolishExtended.bitGet version
+      AttributeType.DamageState ->
+        fmap DamageState $ DamageState.bitGet version
+      AttributeType.Demolish -> fmap Demolish $ Demolish.bitGet version
+      AttributeType.Enum -> fmap Enum Enum.bitGet
+      AttributeType.Explosion -> fmap Explosion $ Explosion.bitGet version
+      AttributeType.ExtendedExplosion ->
+        fmap ExtendedExplosion $ ExtendedExplosion.bitGet version
+      AttributeType.FlaggedInt -> fmap FlaggedInt FlaggedInt.bitGet
+      AttributeType.FlaggedByte -> fmap FlaggedByte FlaggedByte.bitGet
+      AttributeType.Float -> fmap Float Float.bitGet
+      AttributeType.GameMode -> fmap GameMode $ GameMode.bitGet version
+      AttributeType.GameServer ->
+        fmap GameServer $ GameServer.bitGet buildVersion
+      AttributeType.Int -> fmap Int Int.bitGet
+      AttributeType.Int64 -> fmap Int64 Int64.bitGet
+      AttributeType.Loadout -> fmap Loadout Loadout.bitGet
+      AttributeType.LoadoutOnline ->
+        fmap LoadoutOnline $ LoadoutOnline.bitGet version objectMap
+      AttributeType.Loadouts -> fmap Loadouts Loadouts.bitGet
+      AttributeType.LoadoutsOnline ->
+        fmap LoadoutsOnline $ LoadoutsOnline.bitGet version objectMap
+      AttributeType.Location -> fmap Location $ Location.bitGet version
+      AttributeType.MusicStinger -> fmap MusicStinger MusicStinger.bitGet
+      AttributeType.PartyLeader ->
+        fmap PartyLeader $ PartyLeader.bitGet version
+      AttributeType.Pickup -> fmap Pickup Pickup.bitGet
+      AttributeType.PickupInfo -> fmap PickupInfo PickupInfo.bitGet
+      AttributeType.PickupNew -> fmap PickupNew PickupNew.bitGet
+      AttributeType.PlayerHistoryKey ->
+        fmap PlayerHistoryKey PlayerHistoryKey.bitGet
+      AttributeType.PrivateMatchSettings ->
+        fmap PrivateMatchSettings PrivateMatchSettings.bitGet
+      AttributeType.QWord -> fmap QWord QWord.bitGet
+      AttributeType.RepStatTitle -> fmap RepStatTitle RepStatTitle.bitGet
+      AttributeType.Reservation ->
+        fmap Reservation $ Reservation.bitGet version
+      AttributeType.RigidBodyState ->
+        fmap RigidBodyState $ RigidBodyState.bitGet version
+      AttributeType.Rotation -> fmap Rotation Rotation.bitGet
+      AttributeType.StatEvent -> fmap StatEvent StatEvent.bitGet
+      AttributeType.String -> fmap String String.bitGet
+      AttributeType.TeamPaint -> fmap TeamPaint TeamPaint.bitGet
+      AttributeType.Title -> fmap Title Title.bitGet
+      AttributeType.UniqueId -> fmap UniqueId $ UniqueId.bitGet version
+      AttributeType.WeldedInfo -> fmap WeldedInfo $ WeldedInfo.bitGet version
diff --git a/src/lib/Rattletrap/Type/Cache.hs b/src/lib/Rattletrap/Type/Cache.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Cache.hs
@@ -0,0 +1,65 @@
+module Rattletrap.Type.Cache where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.AttributeMapping as AttributeMapping
+import qualified Rattletrap.Type.List as RList
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Json as Json
+
+data Cache = Cache
+  { classId :: U32.U32,
+    parentCacheId :: U32.U32,
+    cacheId :: U32.U32,
+    attributeMappings :: RList.List AttributeMapping.AttributeMapping
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Cache where
+  parseJSON = Json.withObject "Cache" $ \object -> do
+    classId <- Json.required object "class_id"
+    parentCacheId <- Json.required object "parent_cache_id"
+    cacheId <- Json.required object "cache_id"
+    attributeMappings <- Json.required object "attribute_mappings"
+    pure Cache {classId, parentCacheId, cacheId, attributeMappings}
+
+instance Json.ToJSON Cache where
+  toJSON x =
+    Json.object
+      [ Json.pair "class_id" $ classId x,
+        Json.pair "parent_cache_id" $ parentCacheId x,
+        Json.pair "cache_id" $ cacheId x,
+        Json.pair "attribute_mappings" $ attributeMappings x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "cache" $
+    Schema.object
+      [ (Json.pair "class_id" $ Schema.ref U32.schema, True),
+        (Json.pair "parent_cache_id" $ Schema.ref U32.schema, True),
+        (Json.pair "cache_id" $ Schema.ref U32.schema, True),
+        ( Json.pair "attribute_mappings" . Schema.json $
+            RList.schema
+              AttributeMapping.schema,
+          True
+        )
+      ]
+
+bytePut :: Cache -> BytePut.BytePut
+bytePut x =
+  U32.bytePut (classId x)
+    <> U32.bytePut (parentCacheId x)
+    <> U32.bytePut (cacheId x)
+    <> RList.bytePut AttributeMapping.bytePut (attributeMappings x)
+
+byteGet :: ByteGet.ByteGet Cache
+byteGet = ByteGet.label "Cache" $ do
+  classId <- ByteGet.label "classId" U32.byteGet
+  parentCacheId <- ByteGet.label "parentCacheId" U32.byteGet
+  cacheId <- ByteGet.label "cacheId" U32.byteGet
+  attributeMappings <-
+    ByteGet.label "attributeMappings" $
+      RList.byteGet AttributeMapping.byteGet
+  pure Cache {classId, parentCacheId, cacheId, attributeMappings}
diff --git a/src/lib/Rattletrap/Type/ClassAttributeMap.hs b/src/lib/Rattletrap/Type/ClassAttributeMap.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/ClassAttributeMap.hs
@@ -0,0 +1,323 @@
+module Rattletrap.Type.ClassAttributeMap where
+
+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
+import qualified Rattletrap.Data as Data
+import qualified Rattletrap.Type.AttributeMapping as AttributeMapping
+import qualified Rattletrap.Type.Cache as Cache
+import qualified Rattletrap.Type.ClassMapping as ClassMapping
+import qualified Rattletrap.Type.CompressedWord as CompressedWord
+import qualified Rattletrap.Type.List as RList
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+
+-- | 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 'make'.
+data ClassAttributeMap = ClassAttributeMap
+  { -- | A map from object IDs to their names.
+    objectMap :: Map.Map U32.U32 Str.Str,
+    -- | A map from object IDs to their class IDs.
+    objectClassMap :: Map.Map U32.U32 U32.U32,
+    -- | A map from class IDs to a map from attribute stream IDs to attribute
+    -- IDs.
+    value :: Map.Map U32.U32 (Map.Map U32.U32 U32.U32),
+    nameMap :: IntMap.IntMap Str.Str
+  }
+  deriving (Eq, Show)
+
+type Bimap l r = (Map.Map l r, Map.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'.
+make ::
+  -- | From 'Rattletrap.Content.objects'.
+  RList.List Str.Str ->
+  -- | From 'Rattletrap.Content.classMappings'.
+  RList.List ClassMapping.ClassMapping ->
+  -- | From 'Rattletrap.Content.caches'.
+  RList.List Cache.Cache ->
+  -- | From 'Rattletrap.Content.names'.
+  RList.List Str.Str ->
+  ClassAttributeMap
+make 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 :: RList.List Str.Str -> IntMap.IntMap Str.Str
+makeNameMap names =
+  IntMap.fromDistinctAscList (zip [0 ..] (RList.toList names))
+
+getName :: IntMap.IntMap Str.Str -> U32.U32 -> Maybe Str.Str
+getName nameMap_ nameIndex =
+  IntMap.lookup (fromIntegral (U32.toWord32 nameIndex)) nameMap_
+
+makeObjectClassMap ::
+  Map.Map U32.U32 Str.Str ->
+  Bimap U32.U32 Str.Str ->
+  Map.Map U32.U32 U32.U32
+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.Map U32.U32 Str.Str ->
+  Bimap U32.U32 Str.Str ->
+  U32.U32 ->
+  Maybe U32.U32
+getClassId objectMap_ classMap objectId = do
+  objectName <- getObjectName objectMap_ objectId
+  className <- getClassName objectName
+  lookupR className classMap
+
+makeClassCache ::
+  Bimap U32.U32 Str.Str ->
+  RList.List Cache.Cache ->
+  [(Maybe Str.Str, U32.U32, U32.U32, U32.U32)]
+makeClassCache classMap caches =
+  fmap
+    ( \cache ->
+        let classId = Cache.classId cache
+         in ( lookupL classId classMap,
+              classId,
+              Cache.cacheId cache,
+              Cache.parentCacheId cache
+            )
+    )
+    (RList.toList caches)
+
+makeClassMap :: RList.List ClassMapping.ClassMapping -> Bimap U32.U32 Str.Str
+makeClassMap classMappings =
+  bimap
+    ( fmap
+        ( \classMapping ->
+            (ClassMapping.streamId classMapping, ClassMapping.name classMapping)
+        )
+        (RList.toList classMappings)
+    )
+
+makeAttributeMap ::
+  RList.List Cache.Cache -> Map.Map U32.U32 (Map.Map U32.U32 U32.U32)
+makeAttributeMap caches =
+  Map.fromList
+    ( fmap
+        ( \cache ->
+            ( Cache.classId cache,
+              Map.fromList
+                ( fmap
+                    ( \attributeMapping ->
+                        ( AttributeMapping.streamId attributeMapping,
+                          AttributeMapping.objectId attributeMapping
+                        )
+                    )
+                    (RList.toList (Cache.attributeMappings cache))
+                )
+            )
+        )
+        (RList.toList caches)
+    )
+
+makeShallowParentMap ::
+  [(Maybe Str.Str, U32.U32, U32.U32, U32.U32)] -> Map.Map U32.U32 U32.U32
+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.Str, U32.U32, U32.U32, U32.U32)] -> Map.Map U32.U32 [U32.U32]
+makeParentMap classCache =
+  let shallowParentMap = makeShallowParentMap classCache
+   in Map.mapWithKey
+        (\classId _ -> getParentClasses shallowParentMap classId)
+        shallowParentMap
+
+getParentClasses :: Map.Map U32.U32 U32.U32 -> U32.U32 -> [U32.U32]
+getParentClasses shallowParentMap classId =
+  case Map.lookup classId shallowParentMap of
+    Nothing -> []
+    Just parentClassId ->
+      parentClassId : getParentClasses shallowParentMap parentClassId
+
+getParentClass ::
+  Maybe Str.Str ->
+  U32.U32 ->
+  [(Maybe Str.Str, U32.U32, U32.U32, U32.U32)] ->
+  Maybe U32.U32
+getParentClass maybeClassName parentCacheId xs = case maybeClassName of
+  Nothing -> getParentClassById parentCacheId xs
+  Just className -> getParentClassByName className parentCacheId xs
+
+getParentClassById ::
+  U32.U32 -> [(Maybe Str.Str, U32.U32, U32.U32, U32.U32)] -> Maybe U32.U32
+getParentClassById parentCacheId xs =
+  case dropWhile (\(_, _, cacheId, _) -> cacheId /= parentCacheId) xs of
+    [] ->
+      if parentCacheId == U32.fromWord32 0
+        then Nothing
+        else
+          getParentClassById
+            (U32.fromWord32 (U32.toWord32 parentCacheId - 1))
+            xs
+    (_, parentClassId, _, _) : _ -> Just parentClassId
+
+getParentClassByName ::
+  Str.Str ->
+  U32.U32 ->
+  [(Maybe Str.Str, U32.U32, U32.U32, U32.U32)] ->
+  Maybe U32.U32
+getParentClassByName className parentCacheId xs =
+  case Map.lookup (Str.toText className) Data.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, _, _, _) ->
+                            fmap Str.toText maybeClassName == Just parentClassName
+                        )
+                        xs
+                    )
+                )
+            )
+        )
+
+makeObjectMap :: RList.List Str.Str -> Map.Map U32.U32 Str.Str
+makeObjectMap objects =
+  Map.fromAscList (zip (fmap U32.fromWord32 [0 ..]) (RList.toList objects))
+
+getObjectName :: Map.Map U32.U32 Str.Str -> U32.U32 -> Maybe Str.Str
+getObjectName objectMap_ objectId = Map.lookup objectId objectMap_
+
+getClassName :: Str.Str -> Maybe Str.Str
+getClassName rawObjectName =
+  fmap Str.fromText $
+    Map.lookup
+      (Str.toText $ normalizeObjectName rawObjectName)
+      Data.objectClasses
+
+normalizeObjectName :: Str.Str -> Str.Str
+normalizeObjectName objectName =
+  let name = Str.toText 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.fromText crowdActor
+        else
+          if Text.isInfixOf crowdManager name
+            then Str.fromText crowdManager
+            else
+              if Text.isInfixOf boostPickup name
+                then Str.fromText boostPickup
+                else
+                  if Text.isInfixOf mapScoreboard name
+                    then Str.fromText mapScoreboard
+                    else
+                      if Text.isInfixOf breakout name
+                        then Str.fromText breakout
+                        else objectName
+
+classHasLocation :: Str.Str -> Bool
+classHasLocation className =
+  Set.member (Str.toText className) Data.classesWithLocation
+
+classHasRotation :: Str.Str -> Bool
+classHasRotation className =
+  Set.member (Str.toText className) Data.classesWithRotation
+
+getAttributeIdLimit :: Map.Map U32.U32 U32.U32 -> Maybe Word
+getAttributeIdLimit attributeMap = do
+  ((streamId, _), _) <- Map.maxViewWithKey attributeMap
+  pure (fromIntegral (U32.toWord32 streamId))
+
+getAttributeName ::
+  ClassAttributeMap ->
+  Map.Map U32.U32 U32.U32 ->
+  CompressedWord.CompressedWord ->
+  Maybe Str.Str
+getAttributeName classAttributeMap attributeMap streamId = do
+  let key = U32.fromWord32 (fromIntegral (CompressedWord.value streamId))
+  attributeId <- Map.lookup key attributeMap
+  let objectMap_ = objectMap classAttributeMap
+  Map.lookup attributeId objectMap_
+
+getAttributeMap ::
+  ClassAttributeMap ->
+  Map.Map CompressedWord.CompressedWord U32.U32 ->
+  CompressedWord.CompressedWord ->
+  Maybe (Map.Map U32.U32 U32.U32)
+getAttributeMap classAttributeMap actorMap actorId = do
+  objectId <- Map.lookup actorId actorMap
+  let objectClassMap_ = objectClassMap classAttributeMap
+  classId <- Map.lookup objectId objectClassMap_
+  let value_ = value classAttributeMap
+  Map.lookup classId value_
diff --git a/src/lib/Rattletrap/Type/ClassMapping.hs b/src/lib/Rattletrap/Type/ClassMapping.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/ClassMapping.hs
@@ -0,0 +1,41 @@
+module Rattletrap.Type.ClassMapping where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Json as Json
+
+data ClassMapping = ClassMapping
+  { name :: Str.Str,
+    streamId :: U32.U32
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON ClassMapping where
+  parseJSON = Json.withObject "ClassMapping" $ \object -> do
+    name <- Json.required object "name"
+    streamId <- Json.required object "stream_id"
+    pure ClassMapping {name, streamId}
+
+instance Json.ToJSON ClassMapping where
+  toJSON x =
+    Json.object [Json.pair "name" $ name x, Json.pair "stream_id" $ streamId x]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "classMapping" $
+    Schema.object
+      [ (Json.pair "name" $ Schema.ref Str.schema, True),
+        (Json.pair "stream_id" $ Schema.ref U32.schema, True)
+      ]
+
+bytePut :: ClassMapping -> BytePut.BytePut
+bytePut x = Str.bytePut (name x) <> U32.bytePut (streamId x)
+
+byteGet :: ByteGet.ByteGet ClassMapping
+byteGet = ByteGet.label "ClassMapping" $ do
+  name <- ByteGet.label "name" Str.byteGet
+  streamId <- ByteGet.label "streamId" U32.byteGet
+  pure ClassMapping {name, streamId}
diff --git a/src/lib/Rattletrap/Type/CompressedWord.hs b/src/lib/Rattletrap/Type/CompressedWord.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/CompressedWord.hs
@@ -0,0 +1,89 @@
+module Rattletrap.Type.CompressedWord where
+
+import qualified Data.Bits as Bits
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Utility.Json as Json
+
+-- | Although there's no guarantee that these values will not overflow, it's
+-- exceptionally unlikely. Most 'CompressedWord's are very small.
+data CompressedWord = CompressedWord
+  { limit :: Word,
+    value :: Word
+  }
+  deriving (Eq, Ord, Show)
+
+instance Json.FromJSON CompressedWord where
+  parseJSON = Json.withObject "CompressedWord" $ \object -> do
+    limit <- Json.required object "limit"
+    value <- Json.required object "value"
+    pure CompressedWord {limit, value}
+
+instance Json.ToJSON CompressedWord where
+  toJSON x =
+    Json.object [Json.pair "limit" $ limit x, Json.pair "value" $ value x]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "compressedWord" $
+    Schema.object
+      [ (Json.pair "limit" $ Json.object [Json.pair "type" "integer"], True),
+        (Json.pair "value" $ Json.object [Json.pair "type" "integer"], True)
+      ]
+
+bitPut :: CompressedWord -> BitPut.BitPut
+bitPut compressedWord =
+  let limit_ = limit compressedWord
+      value_ = value compressedWord
+      maxBits = getMaxBits limit_
+   in putCompressedWordStep limit_ value_ maxBits 0 0
+
+putCompressedWordStep :: Word -> Word -> Int -> Int -> Word -> BitPut.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 mempty
+        else
+          let bit = Bits.testBit value_ position
+              delta = if bit then x else 0
+           in BitPut.bool bit
+                <> putCompressedWordStep
+                  limit_
+                  value_
+                  maxBits
+                  (position + 1)
+                  (soFar + delta)
+    else mempty
+
+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
+
+bitGet :: Word -> BitGet.BitGet CompressedWord
+bitGet = bitGetNew
+
+bitGetNew :: Word -> BitGet.BitGet CompressedWord
+bitGetNew limit = do
+  value <-
+    if limit < 1
+      then pure 0
+      else do
+        let numBits =
+              max (0 :: Int)
+                . subtract 1
+                . ceiling
+                . logBase (2 :: Double)
+                $ fromIntegral limit
+        partial <- BitGet.bits numBits
+        let next = partial + Bits.shiftL 1 numBits
+        if next > limit
+          then pure partial
+          else do
+            x <- BitGet.bool
+            pure $ if x then next else partial
+  pure CompressedWord {limit, value}
diff --git a/src/lib/Rattletrap/Type/CompressedWordVector.hs b/src/lib/Rattletrap/Type/CompressedWordVector.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/CompressedWordVector.hs
@@ -0,0 +1,50 @@
+module Rattletrap.Type.CompressedWordVector where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.CompressedWord as CompressedWord
+import qualified Rattletrap.Utility.Json as Json
+
+data CompressedWordVector = CompressedWordVector
+  { x :: CompressedWord.CompressedWord,
+    y :: CompressedWord.CompressedWord,
+    z :: CompressedWord.CompressedWord
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON CompressedWordVector where
+  parseJSON = Json.withObject "CompressedWordVector" $ \object -> do
+    x <- Json.required object "x"
+    y <- Json.required object "y"
+    z <- Json.required object "z"
+    pure CompressedWordVector {x, y, z}
+
+instance Json.ToJSON CompressedWordVector where
+  toJSON a =
+    Json.object [Json.pair "x" $ x a, Json.pair "y" $ y a, Json.pair "z" $ z a]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "compressed-word-vector" $
+    Schema.object
+      [ (Json.pair "x" $ Schema.ref CompressedWord.schema, True),
+        (Json.pair "y" $ Schema.ref CompressedWord.schema, True),
+        (Json.pair "z" $ Schema.ref CompressedWord.schema, True)
+      ]
+
+bitPut :: CompressedWordVector -> BitPut.BitPut
+bitPut compressedWordVector =
+  CompressedWord.bitPut (x compressedWordVector)
+    <> CompressedWord.bitPut (y compressedWordVector)
+    <> CompressedWord.bitPut (z compressedWordVector)
+
+bitGet :: BitGet.BitGet CompressedWordVector
+bitGet = do
+  x <- CompressedWord.bitGet limit
+  y <- CompressedWord.bitGet limit
+  z <- CompressedWord.bitGet limit
+  pure CompressedWordVector {x, y, z}
+
+limit :: Word
+limit = 65536
diff --git a/src/lib/Rattletrap/Type/Content.hs b/src/lib/Rattletrap/Type/Content.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Content.hs
@@ -0,0 +1,247 @@
+module Rattletrap.Type.Content where
+
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Lazy as LazyByteString
+import qualified Data.Word as Word
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Cache as Cache
+import qualified Rattletrap.Type.ClassAttributeMap as ClassAttributeMap
+import qualified Rattletrap.Type.ClassMapping as ClassMapping
+import qualified Rattletrap.Type.Frame as Frame
+import qualified Rattletrap.Type.Keyframe as Keyframe
+import qualified Rattletrap.Type.List as RList
+import qualified Rattletrap.Type.Mark as Mark
+import qualified Rattletrap.Type.Message as Message
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Bytes as Bytes
+import qualified Rattletrap.Utility.Json as Json
+
+type Content = ContentWith (RList.List Frame.Frame)
+
+-- | Contains low-level game data about a 'Rattletrap.Replay.Replay'.
+data ContentWith frames = Content
+  { -- | This typically only has one element, like @stadium_oob_audio_map@.
+    levels :: RList.List Str.Str,
+    -- | 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.
+    keyframes :: RList.List Keyframe.Keyframe,
+    -- | The size of the stream in bytes. This is only really necessary because
+    -- the stream has some arbitrary amount of padding at the end.
+    streamSize :: U32.U32,
+    -- | The actual game data. This is where all the interesting information is.
+    frames :: frames,
+    -- | Debugging messages. In newer replays, this is always empty.
+    messages :: RList.List Message.Message,
+    -- | Tick marks shown on the scrubber when watching a replay.
+    marks :: RList.List Mark.Mark,
+    -- | A list of @.upk@ files to load, like
+    -- @..\\..\\TAGame\\CookedPCConsole\\Stadium_P.upk@.
+    packages :: RList.List Str.Str,
+    -- | Objects in the stream. Used for the
+    -- 'Rattletrap.Type.ClassAttributeMap.ClassAttributeMap'.
+    objects :: RList.List Str.Str,
+    -- | It's not clear what these are used for. This list is usually not empty,
+    -- but appears unused otherwise.
+    names :: RList.List Str.Str,
+    -- | A mapping between classes and their ID in the stream. Used for the
+    -- 'Rattletrap.Type.ClassAttributeMap.ClassAttributeMap'.
+    classMappings :: RList.List ClassMapping.ClassMapping,
+    -- | A list of classes along with their parent classes and attributes. Used
+    -- for the 'Rattletrap.Type.ClassAttributeMap.ClassAttributeMap'.
+    caches :: RList.List Cache.Cache,
+    unknown :: [Word.Word8]
+  }
+  deriving (Eq, Show)
+
+instance (Json.FromJSON frames) => Json.FromJSON (ContentWith frames) where
+  parseJSON = Json.withObject "Content" $ \object -> do
+    levels <- Json.required object "levels"
+    keyframes <- Json.required object "key_frames"
+    streamSize <- Json.required object "stream_size"
+    frames <- Json.required object "frames"
+    messages <- Json.required object "messages"
+    marks <- Json.required object "marks"
+    packages <- Json.required object "packages"
+    objects <- Json.required object "objects"
+    names <- Json.required object "names"
+    classMappings <- Json.required object "class_mappings"
+    caches <- Json.required object "caches"
+    unknown <- Json.required object "unknown"
+    pure
+      Content
+        { levels,
+          keyframes,
+          streamSize,
+          frames,
+          messages,
+          marks,
+          packages,
+          objects,
+          names,
+          classMappings,
+          caches,
+          unknown
+        }
+
+instance (Json.ToJSON frames) => Json.ToJSON (ContentWith frames) where
+  toJSON x =
+    Json.object
+      [ Json.pair "levels" $ levels x,
+        Json.pair "key_frames" $ keyframes x,
+        Json.pair "stream_size" $ streamSize x,
+        Json.pair "frames" $ frames x,
+        Json.pair "messages" $ messages x,
+        Json.pair "marks" $ marks x,
+        Json.pair "packages" $ packages x,
+        Json.pair "objects" $ objects x,
+        Json.pair "names" $ names x,
+        Json.pair "class_mappings" $ classMappings x,
+        Json.pair "caches" $ caches x,
+        Json.pair "unknown" $ unknown x
+      ]
+
+schema :: Schema.Schema -> Schema.Schema
+schema s =
+  Schema.named "content" $
+    Schema.object
+      [ (Json.pair "levels" . Schema.json $ RList.schema Str.schema, True),
+        (Json.pair "key_frames" . Schema.json $ RList.schema Keyframe.schema, True),
+        (Json.pair "stream_size" $ Schema.ref U32.schema, True),
+        (Json.pair "frames" $ Schema.json s, True),
+        (Json.pair "messages" . Schema.json $ RList.schema Message.schema, True),
+        (Json.pair "marks" . Schema.json $ RList.schema Mark.schema, True),
+        (Json.pair "packages" . Schema.json $ RList.schema Str.schema, True),
+        (Json.pair "objects" . Schema.json $ RList.schema Str.schema, True),
+        (Json.pair "names" . Schema.json $ RList.schema Str.schema, True),
+        ( Json.pair "class_mappings" . Schema.json $
+            RList.schema
+              ClassMapping.schema,
+          True
+        ),
+        (Json.pair "caches" . Schema.json $ RList.schema Cache.schema, True),
+        (Json.pair "unknown" . Schema.json $ Schema.array U8.schema, True)
+      ]
+
+empty :: Content
+empty =
+  Content
+    { levels = RList.empty,
+      keyframes = RList.empty,
+      streamSize = U32.fromWord32 0,
+      frames = RList.empty,
+      messages = RList.empty,
+      marks = RList.empty,
+      packages = RList.empty,
+      objects = RList.empty,
+      names = RList.empty,
+      classMappings = RList.empty,
+      caches = RList.empty,
+      unknown = []
+    }
+
+bytePut :: Content -> BytePut.BytePut
+bytePut x =
+  RList.bytePut Str.bytePut (levels x)
+    <> RList.bytePut Keyframe.bytePut (keyframes x)
+    <> putFrames x
+    <> RList.bytePut Message.bytePut (messages x)
+    <> RList.bytePut Mark.bytePut (marks x)
+    <> RList.bytePut Str.bytePut (packages x)
+    <> RList.bytePut Str.bytePut (objects x)
+    <> RList.bytePut Str.bytePut (names x)
+    <> RList.bytePut ClassMapping.bytePut (classMappings x)
+    <> RList.bytePut Cache.bytePut (caches x)
+    <> foldMap BytePut.word8 (unknown x)
+
+putFrames :: Content -> BytePut.BytePut
+putFrames x =
+  let stream =
+        BytePut.toByteString . BitPut.toBytePut . Frame.putFrames $ frames x
+      -- This is a little strange. When parsing a binary replay, the stream size
+      -- is given before the stream itself. When generating the JSON, the stream
+      -- size is included. That allows a bit-for-bit identical binary replay to
+      -- be generated from the JSON. However if you modify the JSON before
+      -- converting it back into binary, the stream size might be different.
+      --
+      -- If it was possible to know how much padding the stream required without
+      -- carrying it along as extra data on the side, this logic could go away.
+      -- Unforunately that isn't currently known. See this issue for details:
+      -- <https://github.com/tfausak/rattletrap/issues/171>.
+      expectedStreamSize = streamSize x
+      actualStreamSize =
+        U32.fromWord32 . fromIntegral $ ByteString.length stream
+      streamSize_ =
+        U32.fromWord32 $
+          max (U32.toWord32 expectedStreamSize) (U32.toWord32 actualStreamSize)
+   in U32.bytePut streamSize_
+        <> BytePut.byteString (Bytes.padBytes (U32.toWord32 streamSize_) stream)
+
+byteGet ::
+  Maybe Str.Str ->
+  -- | Version numbers, usually from 'Rattletrap.Header.getVersion'.
+  Version.Version ->
+  -- | The number of frames in the stream, usually from
+  -- 'Rattletrap.Header.getNumFrames'.
+  Int ->
+  -- | The maximum number of channels in the stream, usually from
+  -- 'Rattletrap.Header.getMaxChannels'.
+  Word ->
+  -- | 'Rattletrap.Header.getBuildVersion'
+  Maybe Str.Str ->
+  ByteGet.ByteGet Content
+byteGet matchType version numFrames maxChannels buildVersion =
+  ByteGet.label "Content" $ do
+    levels <- ByteGet.label "levels" $ RList.byteGet Str.byteGet
+    keyframes <- ByteGet.label "keyframes" $ RList.byteGet Keyframe.byteGet
+    streamSize <- ByteGet.label "streamSize" U32.byteGet
+    stream <-
+      ByteGet.label "stream" . ByteGet.byteString . fromIntegral $
+        U32.toWord32
+          streamSize
+    messages <- ByteGet.label "messages" $ RList.byteGet Message.byteGet
+    marks <- ByteGet.label "marks" $ RList.byteGet Mark.byteGet
+    packages <- ByteGet.label "packages" $ RList.byteGet Str.byteGet
+    objects <- ByteGet.label "objects" $ RList.byteGet Str.byteGet
+    names <- ByteGet.label "names" $ RList.byteGet Str.byteGet
+    classMappings <-
+      ByteGet.label "classMappings" $
+        RList.byteGet ClassMapping.byteGet
+    caches <- ByteGet.label "caches" $ RList.byteGet Cache.byteGet
+    let classAttributeMap =
+          ClassAttributeMap.make objects classMappings caches names
+        getFrames =
+          BitGet.toByteGet $
+            Frame.decodeFramesBits
+              matchType
+              version
+              buildVersion
+              numFrames
+              maxChannels
+              classAttributeMap
+    frames <- ByteGet.label "frames" $ ByteGet.embed getFrames stream
+    unknown <-
+      ByteGet.label "unknown" $
+        fmap LazyByteString.unpack ByteGet.remaining
+    pure
+      Content
+        { levels,
+          keyframes,
+          streamSize,
+          frames,
+          messages,
+          marks,
+          packages,
+          objects,
+          names,
+          classMappings,
+          caches,
+          unknown
+        }
diff --git a/src/lib/Rattletrap/Type/Dictionary.hs b/src/lib/Rattletrap/Type/Dictionary.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Dictionary.hs
@@ -0,0 +1,76 @@
+module Rattletrap.Type.Dictionary where
+
+import qualified Data.Text as Text
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.List as RList
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Utility.Json as Json
+
+data Dictionary a = Dictionary
+  { elements :: RList.List (Str.Str, a),
+    lastKey :: Str.Str
+  }
+  deriving (Eq, Show)
+
+instance (Json.FromJSON a) => Json.FromJSON (Dictionary a) where
+  parseJSON = Json.withObject "Dictionary" $ \o -> do
+    elements <- Json.required o "elements"
+    lastKey <- Json.required o "last_key"
+    pure Dictionary {elements = elements, lastKey = lastKey}
+
+instance (Json.ToJSON a) => Json.ToJSON (Dictionary a) where
+  toJSON x =
+    Json.object
+      [ Json.pair "elements" . RList.toList $ elements x,
+        Json.pair "last_key" $ lastKey x
+      ]
+
+schema :: Schema.Schema -> Schema.Schema
+schema s =
+  Schema.named ("dictionary-" <> Text.unpack (Schema.name s)) $
+    Schema.object
+      [ (Json.pair "elements" . Schema.json . Schema.array $ elementSchema s, True),
+        (Json.pair "last_key" $ Schema.ref Str.schema, True)
+      ]
+
+elementSchema :: Schema.Schema -> Schema.Schema
+elementSchema s =
+  Schema.named ("dictionary-element-" <> Text.unpack (Schema.name s)) $
+    Schema.tuple
+      [ Schema.ref Str.schema,
+        Schema.ref s
+      ]
+
+lookup :: Str.Str -> Dictionary a -> Maybe a
+lookup k = Prelude.lookup k . RList.toList . elements
+
+bytePut :: (a -> BytePut.BytePut) -> Dictionary a -> BytePut.BytePut
+bytePut f x =
+  foldMap (\(k, v) -> Str.bytePut k <> f v) (RList.toList $ elements x)
+    <> Str.bytePut (lastKey x)
+
+byteGet :: ByteGet.ByteGet a -> ByteGet.ByteGet (Dictionary a)
+byteGet = ByteGet.label "Dictionary" . byteGetWith 0 []
+
+byteGetWith ::
+  Int ->
+  [(Int, (Str.Str, a))] ->
+  ByteGet.ByteGet a ->
+  ByteGet.ByteGet (Dictionary a)
+byteGetWith i xs f = do
+  k <- ByteGet.label ("key (" <> show i <> ")") Str.byteGet
+  if isNone k
+    then
+      pure
+        Dictionary
+          { elements = RList.fromList . reverse $ fmap snd xs,
+            lastKey = k
+          }
+    else do
+      v <- ByteGet.label ("value (" <> Str.toString k <> ")") f
+      byteGetWith (i + 1) ((i, (k, v)) : xs) f
+
+isNone :: Str.Str -> Bool
+isNone = (== Text.pack "None") . Text.filter (/= '\x00') . Str.toText
diff --git a/src/lib/Rattletrap/Type/F32.hs b/src/lib/Rattletrap/Type/F32.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/F32.hs
@@ -0,0 +1,39 @@
+module Rattletrap.Type.F32 where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Utility.Json as Json
+
+newtype F32
+  = F32 Float
+  deriving (Eq, Show)
+
+instance Json.FromJSON F32 where
+  parseJSON = fmap fromFloat . Json.parseJSON
+
+instance Json.ToJSON F32 where
+  toJSON = Json.toJSON . toFloat
+
+schema :: Schema.Schema
+schema = Schema.named "f32" $ Json.object [Json.pair "type" "number"]
+
+fromFloat :: Float -> F32
+fromFloat = F32
+
+toFloat :: F32 -> Float
+toFloat (F32 x) = x
+
+bytePut :: F32 -> BytePut.BytePut
+bytePut = BytePut.float . toFloat
+
+bitPut :: F32 -> BitPut.BitPut
+bitPut = BitPut.fromBytePut . bytePut
+
+byteGet :: ByteGet.ByteGet F32
+byteGet = ByteGet.label "F32" $ fmap fromFloat ByteGet.float
+
+bitGet :: BitGet.BitGet F32
+bitGet = BitGet.fromByteGet byteGet 4
diff --git a/src/lib/Rattletrap/Type/Frame.hs b/src/lib/Rattletrap/Type/Frame.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Frame.hs
@@ -0,0 +1,140 @@
+module Rattletrap.Type.Frame where
+
+import qualified Data.Map as Map
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.ClassAttributeMap as ClassAttributeMap
+import qualified Rattletrap.Type.CompressedWord as CompressedWord
+import qualified Rattletrap.Type.F32 as F32
+import qualified Rattletrap.Type.List as RList
+import qualified Rattletrap.Type.Replication as Replication
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data Frame = Frame
+  { -- | Time in seconds since the beginning of the match.
+    time :: F32.F32,
+    -- | Time in seconds since the last frame. Usually about 0.03 since there
+    -- are 30 frames per second.
+    delta :: F32.F32,
+    replications :: RList.List Replication.Replication
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Frame where
+  parseJSON = Json.withObject "Frame" $ \object -> do
+    time <- Json.required object "time"
+    delta <- Json.required object "delta"
+    replications <- Json.required object "replications"
+    pure Frame {time, delta, replications}
+
+instance Json.ToJSON Frame where
+  toJSON x =
+    Json.object
+      [ Json.pair "time" $ time x,
+        Json.pair "delta" $ delta x,
+        Json.pair "replications" $ replications x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "frame" $
+    Schema.object
+      [ (Json.pair "time" $ Schema.ref F32.schema, True),
+        (Json.pair "delta" $ Schema.ref F32.schema, True),
+        ( Json.pair "replications" . Schema.json $ RList.schema Replication.schema,
+          True
+        )
+      ]
+
+putFrames :: RList.List Frame -> BitPut.BitPut
+putFrames = foldMap bitPut . RList.toList
+
+bitPut :: Frame -> BitPut.BitPut
+bitPut frame =
+  F32.bitPut (time frame)
+    <> F32.bitPut (delta frame)
+    <> Replication.putReplications (replications frame)
+
+decodeFramesBits ::
+  Maybe Str.Str ->
+  Version.Version ->
+  Maybe Str.Str ->
+  Int ->
+  Word ->
+  ClassAttributeMap.ClassAttributeMap ->
+  BitGet.BitGet (RList.List Frame)
+decodeFramesBits matchType version buildVersion count limit classes =
+  fmap snd $
+    decodeFramesBitsWith
+      matchType
+      version
+      buildVersion
+      count
+      limit
+      classes
+      Map.empty
+      0
+      []
+
+decodeFramesBitsWith ::
+  Maybe Str.Str ->
+  Version.Version ->
+  Maybe Str.Str ->
+  Int ->
+  Word ->
+  ClassAttributeMap.ClassAttributeMap ->
+  Map.Map CompressedWord.CompressedWord U32.U32 ->
+  Int ->
+  [Frame] ->
+  BitGet.BitGet
+    ( Map.Map
+        CompressedWord.CompressedWord
+        U32.U32,
+      RList.List Frame
+    )
+decodeFramesBitsWith matchType version buildVersion count limit classes actorMap index frames =
+  if index >= count
+    then pure (actorMap, RList.fromList $ reverse frames)
+    else do
+      (newActorMap, frame) <-
+        BitGet.label ("element (" <> show index <> ")") $
+          bitGet matchType version buildVersion limit classes actorMap
+      decodeFramesBitsWith
+        matchType
+        version
+        buildVersion
+        count
+        limit
+        classes
+        newActorMap
+        (index + 1)
+        $ frame
+          : frames
+
+bitGet ::
+  Maybe Str.Str ->
+  Version.Version ->
+  Maybe Str.Str ->
+  Word ->
+  ClassAttributeMap.ClassAttributeMap ->
+  Map.Map CompressedWord.CompressedWord U32.U32 ->
+  BitGet.BitGet
+    (Map.Map CompressedWord.CompressedWord U32.U32, Frame)
+bitGet matchType version buildVersion limit classes actorMap =
+  BitGet.label "Frame" $ do
+    time <- BitGet.label "time" F32.bitGet
+    delta <- BitGet.label "delta" F32.bitGet
+    (newActorMap, replications) <-
+      BitGet.label "replications" $
+        Replication.decodeReplicationsBits
+          matchType
+          version
+          buildVersion
+          limit
+          classes
+          actorMap
+    pure (newActorMap, Frame {time, delta, replications})
diff --git a/src/lib/Rattletrap/Type/Header.hs b/src/lib/Rattletrap/Type/Header.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Header.hs
@@ -0,0 +1,113 @@
+module Rattletrap.Type.Header where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Dictionary as Dictionary
+import qualified Rattletrap.Type.Property as Property
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+-- | Contains high-level metadata about a 'Rattletrap.Replay.Replay'.
+data Header = Header
+  { version :: Version.Version,
+    -- | Always @TAGame.Replay_Soccar_TA@.
+    label :: Str.Str,
+    -- | 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@.
+    properties :: Dictionary.Dictionary Property.Property
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Header where
+  parseJSON = Json.withObject "Header" $ \object -> do
+    major <- Json.required object "engine_version"
+    minor <- Json.required object "licensee_version"
+    patch <- Json.optional object "patch_version"
+    label <- Json.required object "label"
+    properties <- Json.required object "properties"
+    pure
+      Header
+        { version =
+            Version.Version
+              { Version.major,
+                Version.minor,
+                Version.patch
+              },
+          label,
+          properties
+        }
+
+instance Json.ToJSON Header where
+  toJSON x =
+    Json.object
+      [ Json.pair "engine_version" . Version.major $ version x,
+        Json.pair "licensee_version" . Version.minor $ version x,
+        Json.pair "patch_version" . Version.patch $ version x,
+        Json.pair "label" $ label x,
+        Json.pair "properties" $ properties x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "header" $
+    Schema.object
+      [ (Json.pair "engine_version" $ Schema.ref U32.schema, True),
+        (Json.pair "licensee_version" $ Schema.ref U32.schema, True),
+        (Json.pair "patch_version" . Schema.json $ Schema.maybe U32.schema, False),
+        (Json.pair "label" $ Schema.ref Str.schema, True),
+        ( Json.pair "properties" . Schema.json $ Dictionary.schema Property.schema,
+          True
+        )
+      ]
+
+bytePut :: Header -> BytePut.BytePut
+bytePut x =
+  Version.bytePut (version x)
+    <> Str.bytePut (label x)
+    <> Dictionary.bytePut
+      Property.bytePut
+      (properties x)
+
+byteGet :: ByteGet.ByteGet Header
+byteGet = ByteGet.label "Header" $ do
+  version <- ByteGet.label "version" Version.byteGet
+  label <- ByteGet.label "label" Str.byteGet
+  properties <-
+    ByteGet.label "properties" $
+      Dictionary.byteGet Property.byteGet
+  pure Header {version, label, properties}
diff --git a/src/lib/Rattletrap/Type/I32.hs b/src/lib/Rattletrap/Type/I32.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/I32.hs
@@ -0,0 +1,46 @@
+module Rattletrap.Type.I32 where
+
+import qualified Data.Int as Int
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Utility.Json as Json
+
+newtype I32
+  = I32 Int.Int32
+  deriving (Eq, Show)
+
+instance Json.FromJSON I32 where
+  parseJSON = fmap fromInt32 . Json.parseJSON
+
+instance Json.ToJSON I32 where
+  toJSON = Json.toJSON . toInt32
+
+schema :: Schema.Schema
+schema =
+  Schema.named "i32" $
+    Json.object
+      [ Json.pair "type" "integer",
+        Json.pair "minimum" (minBound :: Int.Int32),
+        Json.pair "maximum" (maxBound :: Int.Int32)
+      ]
+
+fromInt32 :: Int.Int32 -> I32
+fromInt32 = I32
+
+toInt32 :: I32 -> Int.Int32
+toInt32 (I32 x) = x
+
+bytePut :: I32 -> BytePut.BytePut
+bytePut = BytePut.int32 . toInt32
+
+bitPut :: I32 -> BitPut.BitPut
+bitPut = BitPut.fromBytePut . bytePut
+
+byteGet :: ByteGet.ByteGet I32
+byteGet = ByteGet.label "I32" $ fmap fromInt32 ByteGet.int32
+
+bitGet :: BitGet.BitGet I32
+bitGet = BitGet.fromByteGet byteGet 4
diff --git a/src/lib/Rattletrap/Type/I64.hs b/src/lib/Rattletrap/Type/I64.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/I64.hs
@@ -0,0 +1,48 @@
+module Rattletrap.Type.I64 where
+
+import qualified Data.Int as Int
+import qualified Data.Text as Text
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Utility.Json as Json
+import qualified Text.Read as Read
+
+newtype I64
+  = I64 Int.Int64
+  deriving (Eq, Show)
+
+instance Json.FromJSON I64 where
+  parseJSON =
+    Json.withText "I64" $
+      either fail (pure . fromInt64)
+        . Read.readEither
+        . Text.unpack
+
+instance Json.ToJSON I64 where
+  toJSON = Json.toJSON . show . toInt64
+
+schema :: Schema.Schema
+schema =
+  Schema.named "i64" $
+    Json.object [Json.pair "type" "string", Json.pair "pattern" "^-?[0-9]+$"]
+
+fromInt64 :: Int.Int64 -> I64
+fromInt64 = I64
+
+toInt64 :: I64 -> Int.Int64
+toInt64 (I64 x) = x
+
+bytePut :: I64 -> BytePut.BytePut
+bytePut = BytePut.int64 . toInt64
+
+bitPut :: I64 -> BitPut.BitPut
+bitPut = BitPut.fromBytePut . bytePut
+
+byteGet :: ByteGet.ByteGet I64
+byteGet = fmap fromInt64 ByteGet.int64
+
+bitGet :: BitGet.BitGet I64
+bitGet = BitGet.fromByteGet byteGet 8
diff --git a/src/lib/Rattletrap/Type/I8.hs b/src/lib/Rattletrap/Type/I8.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/I8.hs
@@ -0,0 +1,46 @@
+module Rattletrap.Type.I8 where
+
+import qualified Data.Int as Int
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Utility.Json as Json
+
+newtype I8
+  = I8 Int.Int8
+  deriving (Eq, Show)
+
+instance Json.FromJSON I8 where
+  parseJSON = fmap fromInt8 . Json.parseJSON
+
+instance Json.ToJSON I8 where
+  toJSON = Json.toJSON . toInt8
+
+schema :: Schema.Schema
+schema =
+  Schema.named "i8" $
+    Json.object
+      [ Json.pair "type" "integer",
+        Json.pair "minimum" (minBound :: Int.Int8),
+        Json.pair "maximum" (maxBound :: Int.Int8)
+      ]
+
+fromInt8 :: Int.Int8 -> I8
+fromInt8 = I8
+
+toInt8 :: I8 -> Int.Int8
+toInt8 (I8 x) = x
+
+bytePut :: I8 -> BytePut.BytePut
+bytePut = BytePut.int8 . toInt8
+
+bitPut :: I8 -> BitPut.BitPut
+bitPut = BitPut.fromBytePut . bytePut
+
+byteGet :: ByteGet.ByteGet I8
+byteGet = ByteGet.label "I8" $ fmap fromInt8 ByteGet.int8
+
+bitGet :: BitGet.BitGet I8
+bitGet = BitGet.fromByteGet byteGet 1
diff --git a/src/lib/Rattletrap/Type/Initialization.hs b/src/lib/Rattletrap/Type/Initialization.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Initialization.hs
@@ -0,0 +1,56 @@
+module Rattletrap.Type.Initialization where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Int8Vector as Int8Vector
+import qualified Rattletrap.Type.Vector as Vector
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+import qualified Rattletrap.Utility.Monad as Monad
+
+data Initialization = Initialization
+  { -- | Not every class has an initial location. See
+    -- 'Rattletrap.Data.classesWithLocation'.
+    location :: Maybe Vector.Vector,
+    -- | Only classes with location can have rotation, but not every one does.
+    -- See 'Rattletrap.Data.classesWithRotation'.
+    rotation :: Maybe Int8Vector.Int8Vector
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Initialization where
+  parseJSON = Json.withObject "Initialization" $ \object -> do
+    location <- Json.optional object "location"
+    rotation <- Json.optional object "rotation"
+    pure Initialization {location, rotation}
+
+instance Json.ToJSON Initialization where
+  toJSON x =
+    Json.object
+      [Json.pair "location" $ location x, Json.pair "rotation" $ rotation x]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "initialization" $
+    Schema.object
+      [ (Json.pair "location" . Schema.json $ Schema.maybe Vector.schema, False),
+        ( Json.pair "rotation" . Schema.json $ Schema.maybe Int8Vector.schema,
+          False
+        )
+      ]
+
+bitPut :: Initialization -> BitPut.BitPut
+bitPut initialization =
+  foldMap Vector.bitPut (location initialization)
+    <> foldMap Int8Vector.bitPut (rotation initialization)
+
+bitGet :: Version.Version -> Bool -> Bool -> BitGet.BitGet Initialization
+bitGet version hasLocation hasRotation = BitGet.label "Initialization" $ do
+  location <-
+    BitGet.label "location" $
+      Monad.whenMaybe hasLocation (Vector.bitGet version)
+  rotation <-
+    BitGet.label "rotation" $
+      Monad.whenMaybe hasRotation Int8Vector.bitGet
+  pure Initialization {location, rotation}
diff --git a/src/lib/Rattletrap/Type/Int8Vector.hs b/src/lib/Rattletrap/Type/Int8Vector.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Int8Vector.hs
@@ -0,0 +1,58 @@
+module Rattletrap.Type.Int8Vector where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.I8 as I8
+import qualified Rattletrap.Utility.Json as Json
+import qualified Rattletrap.Utility.Monad as Monad
+
+data Int8Vector = Int8Vector
+  { x :: Maybe I8.I8,
+    y :: Maybe I8.I8,
+    z :: Maybe I8.I8
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Int8Vector where
+  parseJSON = Json.withObject "Int8Vector" $ \object -> do
+    x <- Json.optional object "x"
+    y <- Json.optional object "y"
+    z <- Json.optional object "z"
+    pure Int8Vector {x, y, z}
+
+instance Json.ToJSON Int8Vector where
+  toJSON a =
+    Json.object [Json.pair "x" $ x a, Json.pair "y" $ y a, Json.pair "z" $ z a]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "int8Vector" $
+    Schema.object
+      [ (Json.pair "x" . Schema.json $ Schema.maybe I8.schema, False),
+        (Json.pair "y" . Schema.json $ Schema.maybe I8.schema, False),
+        (Json.pair "z" . Schema.json $ Schema.maybe I8.schema, False)
+      ]
+
+bitPut :: Int8Vector -> BitPut.BitPut
+bitPut int8Vector =
+  putInt8VectorField (x int8Vector)
+    <> putInt8VectorField (y int8Vector)
+    <> putInt8VectorField (z int8Vector)
+
+putInt8VectorField :: Maybe I8.I8 -> BitPut.BitPut
+putInt8VectorField maybeField = case maybeField of
+  Nothing -> BitPut.bool False
+  Just field -> BitPut.bool True <> I8.bitPut field
+
+bitGet :: BitGet.BitGet Int8Vector
+bitGet = BitGet.label "Int8Vector" $ do
+  x <- BitGet.label "x" decodeFieldBits
+  y <- BitGet.label "y" decodeFieldBits
+  z <- BitGet.label "z" decodeFieldBits
+  pure Int8Vector {x, y, z}
+
+decodeFieldBits :: BitGet.BitGet (Maybe I8.I8)
+decodeFieldBits = do
+  hasField <- BitGet.bool
+  Monad.whenMaybe hasField I8.bitGet
diff --git a/src/lib/Rattletrap/Type/Keyframe.hs b/src/lib/Rattletrap/Type/Keyframe.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Keyframe.hs
@@ -0,0 +1,53 @@
+module Rattletrap.Type.Keyframe where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.F32 as F32
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Json as Json
+
+data Keyframe = Keyframe
+  { -- | When this key frame occurs, in seconds.
+    time :: F32.F32,
+    -- | The frame number of this key frame, starting from 0.
+    frame :: U32.U32,
+    -- | The bit position of this key frame in the stream.
+    position :: U32.U32
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Keyframe where
+  parseJSON = Json.withObject "Keyframe" $ \object -> do
+    time <- Json.required object "time"
+    frame <- Json.required object "frame"
+    position <- Json.required object "position"
+    pure Keyframe {time, frame, position}
+
+instance Json.ToJSON Keyframe where
+  toJSON x =
+    Json.object
+      [ Json.pair "time" $ time x,
+        Json.pair "frame" $ frame x,
+        Json.pair "position" $ position x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "keyframe" $
+    Schema.object
+      [ (Json.pair "time" $ Schema.ref F32.schema, True),
+        (Json.pair "frame" $ Schema.ref U32.schema, True),
+        (Json.pair "position" $ Schema.ref U32.schema, True)
+      ]
+
+bytePut :: Keyframe -> BytePut.BytePut
+bytePut x =
+  F32.bytePut (time x) <> U32.bytePut (frame x) <> U32.bytePut (position x)
+
+byteGet :: ByteGet.ByteGet Keyframe
+byteGet = ByteGet.label "Keyframe" $ do
+  time <- ByteGet.label "time" F32.byteGet
+  frame <- ByteGet.label "frame" U32.byteGet
+  position <- ByteGet.label "position" U32.byteGet
+  pure Keyframe {time, frame, position}
diff --git a/src/lib/Rattletrap/Type/List.hs b/src/lib/Rattletrap/Type/List.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/List.hs
@@ -0,0 +1,57 @@
+module Rattletrap.Type.List where
+
+import qualified Control.Monad as Monad
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Json as Json
+
+newtype List a
+  = List [a]
+  deriving (Eq, Show)
+
+instance (Json.FromJSON a) => Json.FromJSON (List a) where
+  parseJSON = fmap fromList . Json.parseJSON
+
+instance (Json.ToJSON a) => Json.ToJSON (List a) where
+  toJSON = Json.toJSON . toList
+
+schema :: Schema.Schema -> Schema.Schema
+schema = Schema.array
+
+fromList :: [a] -> List a
+fromList = List
+
+empty :: List a
+empty = fromList []
+
+toList :: List a -> [a]
+toList (List x) = x
+
+bytePut :: (a -> BytePut.BytePut) -> List a -> BytePut.BytePut
+bytePut f x =
+  let v = toList x
+   in (U32.bytePut . U32.fromWord32 . fromIntegral $ length v) <> foldMap f v
+
+byteGet :: ByteGet.ByteGet a -> ByteGet.ByteGet (List a)
+byteGet f = ByteGet.label "List" $ do
+  size <- ByteGet.label "size" U32.byteGet
+  generateM (fromIntegral $ U32.toWord32 size) $
+    \i -> ByteGet.label ("element (" <> show i <> ")") f
+
+generateM :: (Monad m) => Int -> (Int -> m a) -> m (List a)
+generateM n f = fmap fromList $ mapM f [0 .. n - 1]
+
+replicateM :: (Monad m) => Int -> m a -> m (List a)
+replicateM n = fmap fromList . Monad.replicateM n
+
+untilM :: (Monad m) => m (Maybe a) -> m (List a)
+untilM f = untilMWith f 0 []
+
+untilMWith :: (Monad m) => m (Maybe a) -> Int -> [(Int, a)] -> m (List a)
+untilMWith f i xs = do
+  m <- f
+  case m of
+    Nothing -> pure . fromList . reverse $ fmap snd xs
+    Just x -> untilMWith f (i + 1) ((i, x) : xs)
diff --git a/src/lib/Rattletrap/Type/Mark.hs b/src/lib/Rattletrap/Type/Mark.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Mark.hs
@@ -0,0 +1,43 @@
+module Rattletrap.Type.Mark where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Json as Json
+
+data Mark = Mark
+  { -- | Which type of mark this is, like @Team0Goal@.
+    value :: Str.Str,
+    -- | Which frame this mark belongs to, starting from 0.
+    frame :: U32.U32
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Mark where
+  parseJSON = Json.withObject "Mark" $ \object -> do
+    value <- Json.required object "value"
+    frame <- Json.required object "frame"
+    pure Mark {value, frame}
+
+instance Json.ToJSON Mark where
+  toJSON x =
+    Json.object [Json.pair "value" $ value x, Json.pair "frame" $ frame x]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "mark" $
+    Schema.object
+      [ (Json.pair "value" $ Schema.ref Str.schema, True),
+        (Json.pair "frame" $ Schema.ref U32.schema, True)
+      ]
+
+bytePut :: Mark -> BytePut.BytePut
+bytePut x = Str.bytePut (value x) <> U32.bytePut (frame x)
+
+byteGet :: ByteGet.ByteGet Mark
+byteGet = ByteGet.label "Mark" $ do
+  value <- ByteGet.label "value" Str.byteGet
+  frame <- ByteGet.label "frame" U32.byteGet
+  pure Mark {value, frame}
diff --git a/src/lib/Rattletrap/Type/Message.hs b/src/lib/Rattletrap/Type/Message.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Message.hs
@@ -0,0 +1,53 @@
+module Rattletrap.Type.Message where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Json as Json
+
+data Message = Message
+  { -- | Which frame this message belongs to, starting from 0.
+    frame :: U32.U32,
+    -- | The primary player's name.
+    name :: Str.Str,
+    -- | The content of the message.
+    value :: Str.Str
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Message where
+  parseJSON = Json.withObject "Message" $ \object -> do
+    frame <- Json.required object "frame"
+    name <- Json.required object "name"
+    value <- Json.required object "value"
+    pure Message {frame, name, value}
+
+instance Json.ToJSON Message where
+  toJSON x =
+    Json.object
+      [ Json.pair "frame" $ frame x,
+        Json.pair "name" $ name x,
+        Json.pair "value" $ value x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "message" $
+    Schema.object
+      [ (Json.pair "frame" $ Schema.ref U32.schema, True),
+        (Json.pair "name" $ Schema.ref Str.schema, True),
+        (Json.pair "value" $ Schema.ref Str.schema, True)
+      ]
+
+bytePut :: Message -> BytePut.BytePut
+bytePut x =
+  U32.bytePut (frame x) <> Str.bytePut (name x) <> Str.bytePut (value x)
+
+byteGet :: ByteGet.ByteGet Message
+byteGet = ByteGet.label "Message" $ do
+  frame <- ByteGet.label "frame" U32.byteGet
+  name <- ByteGet.label "name" Str.byteGet
+  value <- ByteGet.label "value" Str.byteGet
+  pure Message {frame, name, value}
diff --git a/src/lib/Rattletrap/Type/ObjectTarget.hs b/src/lib/Rattletrap/Type/ObjectTarget.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/ObjectTarget.hs
@@ -0,0 +1,53 @@
+module Rattletrap.Type.ObjectTarget where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.I32 as I32
+import qualified Rattletrap.Utility.Json as Json
+
+data ObjectTarget = ObjectTarget
+  { isActor :: Bool,
+    targetIndex :: I32.I32
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON ObjectTarget where
+  parseJSON = Json.withObject "ObjectTarget" $ \object -> do
+    isActor <- Json.required object "is_actor"
+    targetIndex <- Json.required object "target_index"
+    pure
+      ObjectTarget
+        { isActor,
+          targetIndex
+        }
+
+instance Json.ToJSON ObjectTarget where
+  toJSON x =
+    Json.object
+      [ Json.pair "is_actor" $ isActor x,
+        Json.pair "target_index" $ targetIndex x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "object-target" $
+    Schema.object
+      [ (Json.pair "is_actor" $ Schema.ref Schema.boolean, True),
+        (Json.pair "target_index" $ Schema.ref I32.schema, True)
+      ]
+
+bitPut :: ObjectTarget -> BitPut.BitPut
+bitPut demolishAttribute =
+  BitPut.bool (isActor demolishAttribute)
+    <> I32.bitPut (targetIndex demolishAttribute)
+
+bitGet :: BitGet.BitGet ObjectTarget
+bitGet = BitGet.label "ObjectTarget" $ do
+  isActor <- BitGet.label "isActor" BitGet.bool
+  targetIndex <- BitGet.label "targetIndex" I32.bitGet
+  pure
+    ObjectTarget
+      { isActor,
+        targetIndex
+      }
diff --git a/src/lib/Rattletrap/Type/Property.hs b/src/lib/Rattletrap/Type/Property.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property.hs
@@ -0,0 +1,62 @@
+module Rattletrap.Type.Property where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.PropertyValue as PropertyValue
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Json as Json
+
+data Property = Property
+  { kind :: Str.Str,
+    -- | Not used.
+    size :: U32.U32,
+    index :: U32.U32,
+    value :: PropertyValue.PropertyValue Property
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Property where
+  parseJSON = Json.withObject "Property" $ \object -> do
+    kind <- Json.required object "kind"
+    size <- Json.required object "size"
+    index <- Json.required object "index"
+    value <- Json.required object "value"
+    pure Property {kind, size, index, value}
+
+instance Json.ToJSON Property where
+  toJSON x =
+    Json.object
+      [ Json.pair "kind" $ kind x,
+        Json.pair "size" $ size x,
+        Json.pair "index" $ index x,
+        Json.pair "value" $ value x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "property" $
+    Schema.object
+      [ (Json.pair "kind" $ Schema.ref Str.schema, True),
+        (Json.pair "size" $ Schema.ref U32.schema, True),
+        (Json.pair "index" $ Schema.ref U32.schema, True),
+        (Json.pair "value" . Schema.ref $ PropertyValue.schema schema, True)
+      ]
+
+bytePut :: Property -> BytePut.BytePut
+bytePut x =
+  Str.bytePut (kind x)
+    <> U32.bytePut (size x)
+    <> U32.bytePut (index x)
+    <> PropertyValue.bytePut
+      bytePut
+      (value x)
+
+byteGet :: ByteGet.ByteGet Property
+byteGet = ByteGet.label "Property" $ do
+  kind <- ByteGet.label "kind" Str.byteGet
+  size <- ByteGet.label "size" U32.byteGet
+  index <- ByteGet.label "index" U32.byteGet
+  value <- ByteGet.label "value" $ PropertyValue.byteGet byteGet kind
+  pure Property {kind, size, index, value}
diff --git a/src/lib/Rattletrap/Type/Property/Array.hs b/src/lib/Rattletrap/Type/Property/Array.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/Array.hs
@@ -0,0 +1,37 @@
+module Rattletrap.Type.Property.Array where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Dictionary as Dictionary
+import qualified Rattletrap.Type.List as RList
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Array a
+  = Array (RList.List (Dictionary.Dictionary a))
+  deriving (Eq, Show)
+
+fromList :: RList.List (Dictionary.Dictionary a) -> Array a
+fromList = Array
+
+toList :: Array a -> RList.List (Dictionary.Dictionary a)
+toList (Array x) = x
+
+instance (Json.FromJSON a) => Json.FromJSON (Array a) where
+  parseJSON = fmap fromList . Json.parseJSON
+
+instance (Json.ToJSON a) => Json.ToJSON (Array a) where
+  toJSON = Json.toJSON . toList
+
+schema :: Schema.Schema -> Schema.Schema
+schema s =
+  Schema.named "property-array" . Schema.json . RList.schema $
+    Dictionary.schema
+      s
+
+bytePut :: (a -> BytePut.BytePut) -> Array a -> BytePut.BytePut
+bytePut f = RList.bytePut (Dictionary.bytePut f) . toList
+
+byteGet :: ByteGet.ByteGet a -> ByteGet.ByteGet (Array a)
+byteGet =
+  ByteGet.label "Array" . fmap fromList . RList.byteGet . Dictionary.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/Bool.hs b/src/lib/Rattletrap/Type/Property/Bool.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/Bool.hs
@@ -0,0 +1,33 @@
+module Rattletrap.Type.Property.Bool where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Utility.Json as Json
+import Prelude hiding (Bool)
+
+newtype Bool
+  = Bool U8.U8
+  deriving (Eq, Show)
+
+fromU8 :: U8.U8 -> Bool
+fromU8 = Bool
+
+toU8 :: Bool -> U8.U8
+toU8 (Bool x) = x
+
+instance Json.FromJSON Bool where
+  parseJSON = fmap fromU8 . Json.parseJSON
+
+instance Json.ToJSON Bool where
+  toJSON = Json.toJSON . toU8
+
+schema :: Schema.Schema
+schema = U8.schema
+
+bytePut :: Bool -> BytePut.BytePut
+bytePut = U8.bytePut . toU8
+
+byteGet :: ByteGet.ByteGet Bool
+byteGet = ByteGet.label "Bool" $ fmap fromU8 U8.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/Byte.hs b/src/lib/Rattletrap/Type/Property/Byte.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/Byte.hs
@@ -0,0 +1,53 @@
+module Rattletrap.Type.Property.Byte where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Utility.Json as Json
+
+data Byte = Byte
+  { key :: Str.Str,
+    value :: Maybe (Either U8.U8 Str.Str)
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Byte where
+  parseJSON json = do
+    (key, value) <- Json.parseJSON json
+    pure Byte {key, value}
+
+instance Json.ToJSON Byte where
+  toJSON byte = Json.toJSON (key byte, value byte)
+
+schema :: Schema.Schema
+schema =
+  Schema.named "property-byte" $
+    Schema.tuple
+      [ Schema.ref Str.schema,
+        Schema.oneOf
+          [ Schema.ref Schema.null,
+            Schema.object [(Json.pair "Left" $ Schema.ref U8.schema, True)],
+            Schema.object [(Json.pair "Right" $ Schema.ref Str.schema, True)]
+          ]
+      ]
+
+bytePut :: Byte -> BytePut.BytePut
+bytePut byte = Str.bytePut (key byte) <> foldMap (either U8.bytePut Str.bytePut) (value byte)
+
+byteGet :: ByteGet.ByteGet Byte
+byteGet = ByteGet.label "Byte" $ do
+  key <- ByteGet.label "key" Str.byteGet
+  let isSteam = key == Str.fromString "OnlinePlatform_Steam"
+      isPlayStation = key == Str.fromString "OnlinePlatform_PS4"
+      isNone = key == Str.fromString "None"
+  value <-
+    ByteGet.label "value" $
+      if isSteam || isPlayStation
+        then pure Nothing
+        else
+          if isNone
+            then Just . Left <$> U8.byteGet
+            else Just . Right <$> Str.byteGet
+  pure Byte {key, value}
diff --git a/src/lib/Rattletrap/Type/Property/Float.hs b/src/lib/Rattletrap/Type/Property/Float.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/Float.hs
@@ -0,0 +1,33 @@
+module Rattletrap.Type.Property.Float where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.F32 as F32
+import qualified Rattletrap.Utility.Json as Json
+import Prelude hiding (Float)
+
+newtype Float
+  = Float F32.F32
+  deriving (Eq, Show)
+
+fromF32 :: F32.F32 -> Float
+fromF32 = Float
+
+toF32 :: Float -> F32.F32
+toF32 (Float x) = x
+
+instance Json.FromJSON Float where
+  parseJSON = fmap fromF32 . Json.parseJSON
+
+instance Json.ToJSON Float where
+  toJSON = Json.toJSON . toF32
+
+schema :: Schema.Schema
+schema = F32.schema
+
+bytePut :: Float -> BytePut.BytePut
+bytePut = F32.bytePut . toF32
+
+byteGet :: ByteGet.ByteGet Float
+byteGet = ByteGet.label "Float" $ fmap fromF32 F32.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/Int.hs b/src/lib/Rattletrap/Type/Property/Int.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/Int.hs
@@ -0,0 +1,33 @@
+module Rattletrap.Type.Property.Int where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.I32 as I32
+import qualified Rattletrap.Utility.Json as Json
+import Prelude hiding (Int)
+
+newtype Int
+  = Int I32.I32
+  deriving (Eq, Show)
+
+fromI32 :: I32.I32 -> Int
+fromI32 = Int
+
+toI32 :: Int -> I32.I32
+toI32 (Int x) = x
+
+instance Json.FromJSON Int where
+  parseJSON = fmap fromI32 . Json.parseJSON
+
+instance Json.ToJSON Int where
+  toJSON = Json.toJSON . toI32
+
+schema :: Schema.Schema
+schema = I32.schema
+
+bytePut :: Int -> BytePut.BytePut
+bytePut = I32.bytePut . toI32
+
+byteGet :: ByteGet.ByteGet Int
+byteGet = ByteGet.label "I32" $ fmap fromI32 I32.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/Name.hs b/src/lib/Rattletrap/Type/Property/Name.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/Name.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Type.Property.Name where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Name
+  = Name Str.Str
+  deriving (Eq, Show)
+
+fromStr :: Str.Str -> Name
+fromStr = Name
+
+toStr :: Name -> Str.Str
+toStr (Name x) = x
+
+instance Json.FromJSON Name where
+  parseJSON = fmap fromStr . Json.parseJSON
+
+instance Json.ToJSON Name where
+  toJSON = Json.toJSON . toStr
+
+schema :: Schema.Schema
+schema = Str.schema
+
+bytePut :: Name -> BytePut.BytePut
+bytePut = Str.bytePut . toStr
+
+byteGet :: ByteGet.ByteGet Name
+byteGet = ByteGet.label "Name" $ fmap fromStr Str.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/QWord.hs b/src/lib/Rattletrap/Type/Property/QWord.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/QWord.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Type.Property.QWord where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U64 as U64
+import qualified Rattletrap.Utility.Json as Json
+
+newtype QWord
+  = QWord U64.U64
+  deriving (Eq, Show)
+
+fromU64 :: U64.U64 -> QWord
+fromU64 = QWord
+
+toU64 :: QWord -> U64.U64
+toU64 (QWord x) = x
+
+instance Json.FromJSON QWord where
+  parseJSON = fmap fromU64 . Json.parseJSON
+
+instance Json.ToJSON QWord where
+  toJSON = Json.toJSON . toU64
+
+schema :: Schema.Schema
+schema = U64.schema
+
+bytePut :: QWord -> BytePut.BytePut
+bytePut = U64.bytePut . toU64
+
+byteGet :: ByteGet.ByteGet QWord
+byteGet = ByteGet.label "QWord" $ fmap fromU64 U64.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/Str.hs b/src/lib/Rattletrap/Type/Property/Str.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/Str.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Type.Property.Str where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Str
+  = Str Str.Str
+  deriving (Eq, Show)
+
+fromStr :: Str.Str -> Str
+fromStr = Str
+
+toStr :: Str -> Str.Str
+toStr (Str x) = x
+
+instance Json.FromJSON Str where
+  parseJSON = fmap fromStr . Json.parseJSON
+
+instance Json.ToJSON Str where
+  toJSON = Json.toJSON . toStr
+
+schema :: Schema.Schema
+schema = Str.schema
+
+bytePut :: Str -> BytePut.BytePut
+bytePut = Str.bytePut . toStr
+
+byteGet :: ByteGet.ByteGet Str
+byteGet = ByteGet.label "Str" $ fmap fromStr Str.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/Struct.hs b/src/lib/Rattletrap/Type/Property/Struct.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/Struct.hs
@@ -0,0 +1,46 @@
+module Rattletrap.Type.Property.Struct where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Dictionary as Dictionary
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Utility.Json as Json
+
+data Struct a = Struct
+  { name :: Str.Str,
+    fields :: Dictionary.Dictionary a
+  }
+  deriving (Eq, Show)
+
+instance (Json.FromJSON a) => Json.FromJSON (Struct a) where
+  parseJSON = Json.withObject "Struct" $ \o -> do
+    name <- Json.required o "name"
+    fields <- Json.required o "fields"
+    pure Struct {name, fields}
+
+instance (Json.ToJSON a) => Json.ToJSON (Struct a) where
+  toJSON x =
+    Json.object
+      [ Json.pair "name" $ name x,
+        Json.pair "fields" $ fields x
+      ]
+
+schema :: Schema.Schema -> Schema.Schema
+schema s =
+  Schema.named "property-struct" $
+    Schema.object
+      [ (Json.pair "name" $ Schema.ref Str.schema, True),
+        (Json.pair "fields" $ Schema.ref (Dictionary.schema s), True)
+      ]
+
+bytePut :: (a -> BytePut.BytePut) -> Struct a -> BytePut.BytePut
+bytePut p x =
+  Str.bytePut (name x)
+    <> Dictionary.bytePut p (fields x)
+
+byteGet :: ByteGet.ByteGet a -> ByteGet.ByteGet (Struct a)
+byteGet g = ByteGet.label "Struct" $ do
+  name <- ByteGet.label "name" Str.byteGet
+  fields <- ByteGet.label "fields" $ Dictionary.byteGet g
+  pure Struct {name, fields}
diff --git a/src/lib/Rattletrap/Type/PropertyValue.hs b/src/lib/Rattletrap/Type/PropertyValue.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/PropertyValue.hs
@@ -0,0 +1,102 @@
+module Rattletrap.Type.PropertyValue where
+
+import qualified Data.Foldable as Foldable
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Exception.UnknownProperty as UnknownProperty
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Property.Array as Property.Array
+import qualified Rattletrap.Type.Property.Bool as Property.Bool
+import qualified Rattletrap.Type.Property.Byte as Property.Byte
+import qualified Rattletrap.Type.Property.Float as Property.Float
+import qualified Rattletrap.Type.Property.Int as Property.Int
+import qualified Rattletrap.Type.Property.Name as Property.Name
+import qualified Rattletrap.Type.Property.QWord as Property.QWord
+import qualified Rattletrap.Type.Property.Str as Property.Str
+import qualified Rattletrap.Type.Property.Struct as Property.Struct
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Utility.Json as Json
+
+data PropertyValue a
+  = -- | Yes, a list of dictionaries. No, it doesn't make sense. These usually
+    -- only have one element.
+    Array (Property.Array.Array a)
+  | Bool Property.Bool.Bool
+  | -- | This is a strange name for essentially a key-value pair.
+    Byte Property.Byte.Byte
+  | Float Property.Float.Float
+  | Int Property.Int.Int
+  | -- | It's unclear how exactly this is different than a 'StrProperty'.
+    Name Property.Name.Name
+  | QWord Property.QWord.QWord
+  | Str Property.Str.Str
+  | Struct (Property.Struct.Struct a)
+  deriving (Eq, Show)
+
+instance (Json.FromJSON a) => Json.FromJSON (PropertyValue a) where
+  parseJSON = Json.withObject "PropertyValue" $ \object ->
+    Foldable.asum
+      [ fmap Array $ Json.required object "array",
+        fmap Bool $ Json.required object "bool",
+        fmap Byte $ Json.required object "byte",
+        fmap Float $ Json.required object "float",
+        fmap Int $ Json.required object "int",
+        fmap Name $ Json.required object "name",
+        fmap QWord $ Json.required object "q_word",
+        fmap Str $ Json.required object "str",
+        fmap Struct $ Json.required object "struct"
+      ]
+
+instance (Json.ToJSON a) => Json.ToJSON (PropertyValue a) where
+  toJSON x = case x of
+    Array y -> Json.object [Json.pair "array" y]
+    Bool y -> Json.object [Json.pair "bool" y]
+    Byte y -> Json.object [Json.pair "byte" y]
+    Float y -> Json.object [Json.pair "float" y]
+    Int y -> Json.object [Json.pair "int" y]
+    Name y -> Json.object [Json.pair "name" y]
+    QWord y -> Json.object [Json.pair "q_word" y]
+    Str y -> Json.object [Json.pair "str" y]
+    Struct y -> Json.object [Json.pair "struct" y]
+
+schema :: Schema.Schema -> Schema.Schema
+schema s =
+  Schema.named "property-value" . Schema.oneOf $
+    fmap
+      (\(k, v) -> Schema.object [(Json.pair k v, True)])
+      [ ("array", Schema.ref $ Property.Array.schema s),
+        ("bool", Schema.ref Property.Bool.schema),
+        ("byte", Schema.ref Property.Byte.schema),
+        ("float", Schema.ref Property.Float.schema),
+        ("int", Schema.ref Property.Int.schema),
+        ("name", Schema.ref Property.Name.schema),
+        ("q_word", Schema.ref Property.QWord.schema),
+        ("str", Schema.ref Property.Str.schema),
+        ("struct", Schema.ref $ Property.Struct.schema s)
+      ]
+
+bytePut :: (a -> BytePut.BytePut) -> PropertyValue a -> BytePut.BytePut
+bytePut putProperty value = case value of
+  Array x -> Property.Array.bytePut putProperty x
+  Bool x -> Property.Bool.bytePut x
+  Byte x -> Property.Byte.bytePut x
+  Float x -> Property.Float.bytePut x
+  Int x -> Property.Int.bytePut x
+  Name x -> Property.Name.bytePut x
+  QWord x -> Property.QWord.bytePut x
+  Str x -> Property.Str.bytePut x
+  Struct x -> Property.Struct.bytePut putProperty x
+
+byteGet :: ByteGet.ByteGet a -> Str.Str -> ByteGet.ByteGet (PropertyValue a)
+byteGet getProperty kind =
+  ByteGet.label "PropertyValue" $ case Str.toString kind of
+    "ArrayProperty" -> fmap Array $ Property.Array.byteGet getProperty
+    "BoolProperty" -> fmap Bool Property.Bool.byteGet
+    "ByteProperty" -> fmap Byte Property.Byte.byteGet
+    "FloatProperty" -> fmap Float Property.Float.byteGet
+    "IntProperty" -> fmap Int Property.Int.byteGet
+    "NameProperty" -> fmap Name Property.Name.byteGet
+    "QWordProperty" -> fmap QWord Property.QWord.byteGet
+    "StrProperty" -> fmap Str Property.Str.byteGet
+    "StructProperty" -> fmap Struct $ Property.Struct.byteGet getProperty
+    x -> ByteGet.throw $ UnknownProperty.UnknownProperty x
diff --git a/src/lib/Rattletrap/Type/Quaternion.hs b/src/lib/Rattletrap/Type/Quaternion.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Quaternion.hs
@@ -0,0 +1,167 @@
+module Rattletrap.Type.Quaternion where
+
+import qualified Data.List as List
+import qualified Data.Maybe as Maybe
+import qualified Data.Ord as Ord
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Exception.InvalidComponent as InvalidComponent
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.CompressedWord as CompressedWord
+import qualified Rattletrap.Utility.Json as Json
+
+data Quaternion = Quaternion
+  { x :: Double,
+    y :: Double,
+    z :: Double,
+    w :: Double
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Quaternion where
+  parseJSON = Json.withObject "Quaternion" $ \object -> do
+    x <- Json.required object "x"
+    y <- Json.required object "y"
+    z <- Json.required object "z"
+    w <- Json.required object "w"
+    pure Quaternion {x, y, z, w}
+
+instance Json.ToJSON Quaternion where
+  toJSON a =
+    Json.object
+      [ Json.pair "x" $ x a,
+        Json.pair "y" $ y a,
+        Json.pair "z" $ z a,
+        Json.pair "w" $ w a
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "quaternion" $
+    Schema.object
+      [ (Json.pair "x" $ Schema.ref Schema.number, True),
+        (Json.pair "y" $ Schema.ref Schema.number, True),
+        (Json.pair "z" $ Schema.ref Schema.number, True),
+        (Json.pair "w" $ Schema.ref Schema.number, True)
+      ]
+
+data Component
+  = X
+  | Y
+  | Z
+  | W
+  deriving (Eq, Show)
+
+toQuaternion :: Component -> Double -> Double -> Double -> Quaternion
+toQuaternion component a b c =
+  let d = toPart a b c
+   in case component of
+        X -> Quaternion d a b c
+        Y -> Quaternion a d b c
+        Z -> Quaternion a b d c
+        W -> Quaternion a b c d
+
+toPart :: Double -> Double -> Double -> Double
+toPart a b c = sqrt (1 - (a * a) - (b * b) - (c * c))
+
+compressPart :: Double -> CompressedWord.CompressedWord
+compressPart =
+  CompressedWord.CompressedWord maxCompressedValue
+    . round
+    . (* wordToDouble maxCompressedValue)
+    . (+ 0.5)
+    . (/ 2.0)
+    . (/ maxValue)
+
+decompressPart :: CompressedWord.CompressedWord -> Double
+decompressPart x_ =
+  (* maxValue)
+    . (* 2.0)
+    . subtract 0.5
+    . (/ wordToDouble (CompressedWord.limit x_))
+    . wordToDouble
+    $ CompressedWord.value x_
+
+maxComponent :: Quaternion -> Component
+maxComponent quaternion =
+  let x_ = x quaternion
+      y_ = y quaternion
+      z_ = z quaternion
+      w_ = w quaternion
+      parts = [(x_, X), (y_, Y), (z_, Z), (w_, W)]
+      biggestPart = maximumOn fst parts
+      roundTrip = decompressPart . compressPart
+      computedPart =
+        Maybe.fromMaybe
+          biggestPart
+          (List.find (\(value, _) -> value /= roundTrip value) parts)
+   in snd
+        ( if (biggestPart == computedPart)
+            || (abs (fst biggestPart - fst computedPart) > 0.00001)
+            then biggestPart
+            else computedPart
+        )
+
+maximumOn :: (Foldable t, Ord b) => (a -> b) -> t a -> a
+maximumOn f = List.maximumBy (Ord.comparing f)
+
+numBits :: Word
+numBits = 18
+
+wordToDouble :: Word -> Double
+wordToDouble = fromIntegral
+
+maxCompressedValue :: Word
+maxCompressedValue = (2 ^ numBits) - 1
+
+maxValue :: Double
+maxValue = 1.0 / sqrt 2.0
+
+bitPut :: Quaternion -> BitPut.BitPut
+bitPut q =
+  let c = maxComponent q
+   in putComponent c <> case c of
+        X -> putParts (y q) (z q) (w q)
+        Y -> putParts (x q) (z q) (w q)
+        Z -> putParts (x q) (y q) (w q)
+        W -> putParts (x q) (y q) (z q)
+
+putComponent :: Component -> BitPut.BitPut
+putComponent component =
+  CompressedWord.bitPut
+    ( CompressedWord.CompressedWord
+        3
+        ( case component of
+            X -> 0
+            Y -> 1
+            Z -> 2
+            W -> 3
+        )
+    )
+
+putParts :: Double -> Double -> Double -> BitPut.BitPut
+putParts a b c = putPart a <> putPart b <> putPart c
+
+putPart :: Double -> BitPut.BitPut
+putPart = CompressedWord.bitPut . compressPart
+
+bitGet :: BitGet.BitGet Quaternion
+bitGet = do
+  component <- decodeComponent
+  a <- decodePart
+  b <- decodePart
+  c <- decodePart
+  pure $ toQuaternion component a b c
+
+decodeComponent :: BitGet.BitGet Component
+decodeComponent = do
+  x_ <- CompressedWord.bitGet 3
+  case CompressedWord.value x_ of
+    0 -> pure X
+    1 -> pure Y
+    2 -> pure Z
+    3 -> pure W
+    y_ -> BitGet.throw $ InvalidComponent.InvalidComponent y_
+
+decodePart :: BitGet.BitGet Double
+decodePart = fmap decompressPart $ CompressedWord.bitGet maxCompressedValue
diff --git a/src/lib/Rattletrap/Type/RemoteId.hs b/src/lib/Rattletrap/Type/RemoteId.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/RemoteId.hs
@@ -0,0 +1,92 @@
+module Rattletrap.Type.RemoteId where
+
+import qualified Data.Foldable as Foldable
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Exception.UnknownSystemId as UnknownSystemId
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.RemoteId.Epic as Epic
+import qualified Rattletrap.Type.RemoteId.PlayStation as PlayStation
+import qualified Rattletrap.Type.RemoteId.PsyNet as PsyNet
+import qualified Rattletrap.Type.RemoteId.QQ as QQ
+import qualified Rattletrap.Type.RemoteId.Splitscreen as Splitscreen
+import qualified Rattletrap.Type.RemoteId.Steam as Steam
+import qualified Rattletrap.Type.RemoteId.Switch as Switch
+import qualified Rattletrap.Type.RemoteId.Xbox as Xbox
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data RemoteId
+  = PlayStation PlayStation.PlayStation
+  | PsyNet PsyNet.PsyNet
+  | QQ QQ.QQ
+  | -- | Really only 24 bits.
+    Splitscreen Splitscreen.Splitscreen
+  | Steam Steam.Steam
+  | Switch Switch.Switch
+  | Xbox Xbox.Xbox
+  | Epic Epic.Epic
+  deriving (Eq, Show)
+
+instance Json.FromJSON RemoteId where
+  parseJSON = Json.withObject "RemoteId" $ \object ->
+    Foldable.asum
+      [ fmap PlayStation $ Json.required object "play_station",
+        fmap PsyNet $ Json.required object "psy_net",
+        fmap QQ $ Json.required object "qq",
+        fmap Splitscreen $ Json.required object "splitscreen",
+        fmap Steam $ Json.required object "steam",
+        fmap Switch $ Json.required object "switch",
+        fmap Xbox $ Json.required object "xbox",
+        fmap Epic $ Json.required object "epic"
+      ]
+
+instance Json.ToJSON RemoteId where
+  toJSON x = case x of
+    PlayStation y -> Json.object [Json.pair "play_station" y]
+    PsyNet y -> Json.object [Json.pair "psy_net" y]
+    QQ y -> Json.object [Json.pair "qq" y]
+    Splitscreen y -> Json.object [Json.pair "splitscreen" y]
+    Steam y -> Json.object [Json.pair "steam" y]
+    Switch y -> Json.object [Json.pair "switch" y]
+    Xbox y -> Json.object [Json.pair "xbox" y]
+    Epic y -> Json.object [Json.pair "epic" y]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "remote-id" . Schema.oneOf $
+    fmap
+      (\(k, v) -> Schema.object [(Json.pair k v, True)])
+      [ ("play_station", Schema.ref PlayStation.schema),
+        ("psy_net", Schema.ref PsyNet.schema),
+        ("qq", Schema.ref QQ.schema),
+        ("splitscreen", Schema.ref Splitscreen.schema),
+        ("steam", Schema.ref Steam.schema),
+        ("switch", Schema.ref Switch.schema),
+        ("xbox", Schema.ref Xbox.schema),
+        ("epic", Schema.ref Epic.schema)
+      ]
+
+bitPut :: RemoteId -> BitPut.BitPut
+bitPut remoteId = case remoteId of
+  PlayStation x -> PlayStation.bitPut x
+  PsyNet x -> PsyNet.bitPut x
+  QQ x -> QQ.bitPut x
+  Splitscreen x -> Splitscreen.bitPut x
+  Steam x -> Steam.bitPut x
+  Switch x -> Switch.bitPut x
+  Xbox x -> Xbox.bitPut x
+  Epic x -> Epic.bitPut x
+
+bitGet :: Version.Version -> U8.U8 -> BitGet.BitGet RemoteId
+bitGet version systemId = case U8.toWord8 systemId of
+  0 -> fmap Splitscreen Splitscreen.bitGet
+  1 -> fmap Steam Steam.bitGet
+  2 -> fmap PlayStation $ PlayStation.bitGet version
+  4 -> fmap Xbox Xbox.bitGet
+  5 -> fmap QQ QQ.bitGet
+  6 -> fmap Switch Switch.bitGet
+  7 -> fmap PsyNet $ PsyNet.bitGet version
+  11 -> fmap Epic Epic.bitGet
+  x -> BitGet.throw $ UnknownSystemId.UnknownSystemId x
diff --git a/src/lib/Rattletrap/Type/RemoteId/Epic.hs b/src/lib/Rattletrap/Type/RemoteId/Epic.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/RemoteId/Epic.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Type.RemoteId.Epic where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Epic
+  = Epic Str.Str
+  deriving (Eq, Show)
+
+instance Json.FromJSON Epic where
+  parseJSON = fmap fromStr . Json.parseJSON
+
+instance Json.ToJSON Epic where
+  toJSON = Json.toJSON . toStr
+
+fromStr :: Str.Str -> Epic
+fromStr = Epic
+
+toStr :: Epic -> Str.Str
+toStr (Epic x) = x
+
+schema :: Schema.Schema
+schema = Str.schema
+
+bitPut :: Epic -> BitPut.BitPut
+bitPut = Str.bitPut . toStr
+
+bitGet :: BitGet.BitGet Epic
+bitGet = BitGet.label "Epic" $ fmap fromStr Str.bitGet
diff --git a/src/lib/Rattletrap/Type/RemoteId/PlayStation.hs b/src/lib/Rattletrap/Type/RemoteId/PlayStation.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/RemoteId/PlayStation.hs
@@ -0,0 +1,55 @@
+module Rattletrap.Type.RemoteId.PlayStation where
+
+import qualified Data.ByteString as ByteString
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+import qualified Data.Word as Word
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Bytes as Bytes
+import qualified Rattletrap.Utility.Json as Json
+
+data PlayStation = PlayStation
+  { name :: Text.Text,
+    code :: [Word.Word8]
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON PlayStation where
+  parseJSON json = do
+    (name, code) <- Json.parseJSON json
+    pure PlayStation {name, code}
+
+instance Json.ToJSON PlayStation where
+  toJSON x = Json.toJSON (name x, code x)
+
+schema :: Schema.Schema
+schema =
+  Schema.named "remote-id-play-station" $
+    Schema.tuple
+      [Schema.ref Schema.string, Schema.json $ Schema.array Schema.number]
+
+bitPut :: PlayStation -> BitPut.BitPut
+bitPut x =
+  let nameBytes = Bytes.padBytes (16 :: Int) . Bytes.encodeLatin1 $ name x
+      codeBytes = ByteString.pack $ code x
+   in BitPut.byteString nameBytes <> BitPut.byteString codeBytes
+
+bitGet :: Version.Version -> BitGet.BitGet PlayStation
+bitGet version = BitGet.label "PlayStation" $ do
+  name <- BitGet.label "name" getCode
+  code <- BitGet.label "code" $ getName version
+  pure PlayStation {name, code}
+
+getCode :: BitGet.BitGet Text.Text
+getCode =
+  fmap (Text.dropWhileEnd (== '\x00') . Text.decodeLatin1) $
+    BitGet.byteString 16
+
+getName :: Version.Version -> BitGet.BitGet [Word.Word8]
+getName version =
+  fmap ByteString.unpack
+    . BitGet.byteString
+    $ if Version.atLeast 868 20 1 version then 24 else 16
diff --git a/src/lib/Rattletrap/Type/RemoteId/PsyNet.hs b/src/lib/Rattletrap/Type/RemoteId/PsyNet.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/RemoteId/PsyNet.hs
@@ -0,0 +1,56 @@
+module Rattletrap.Type.RemoteId.PsyNet where
+
+import qualified Control.Applicative as Applicative
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U64 as U64
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data PsyNet
+  = New U64.U64
+  | Old U64.U64 U64.U64 U64.U64 U64.U64
+  deriving (Eq, Show)
+
+instance Json.FromJSON PsyNet where
+  parseJSON = Json.withObject "PsyNet" $ \object -> do
+    let new = fmap New $ Json.required object "Left"
+        old = do
+          (a, b, c, d) <- Json.required object "Right"
+          pure $ Old a b c d
+    new Applicative.<|> old
+
+instance Json.ToJSON PsyNet where
+  toJSON x = case x of
+    New a -> Json.object [Json.pair "Left" a]
+    Old a b c d -> Json.object [Json.pair "Right" (a, b, c, d)]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "remote-id-psy-net" $
+    Schema.oneOf
+      [ Schema.object [(Json.pair "Left" $ Schema.ref U64.schema, True)],
+        Schema.object
+          [ ( Json.pair "Right" . Schema.tuple . replicate 4 $ Schema.ref U64.schema,
+              True
+            )
+          ]
+      ]
+
+bitPut :: PsyNet -> BitPut.BitPut
+bitPut x = case x of
+  New l -> U64.bitPut l
+  Old a b c d -> U64.bitPut a <> U64.bitPut b <> U64.bitPut c <> U64.bitPut d
+
+bitGet :: Version.Version -> BitGet.BitGet PsyNet
+bitGet version =
+  BitGet.label "PsyNet" $
+    if Version.atLeast 868 24 10 version
+      then BitGet.label "New" $ fmap New U64.bitGet
+      else BitGet.label "Old" $ do
+        a <- U64.bitGet
+        b <- U64.bitGet
+        c <- U64.bitGet
+        d <- U64.bitGet
+        pure $ Old a b c d
diff --git a/src/lib/Rattletrap/Type/RemoteId/QQ.hs b/src/lib/Rattletrap/Type/RemoteId/QQ.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/RemoteId/QQ.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Type.RemoteId.QQ where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U64 as U64
+import qualified Rattletrap.Utility.Json as Json
+
+newtype QQ
+  = QQ U64.U64
+  deriving (Eq, Show)
+
+instance Json.FromJSON QQ where
+  parseJSON = fmap fromU64 . Json.parseJSON
+
+instance Json.ToJSON QQ where
+  toJSON = Json.toJSON . toU64
+
+fromU64 :: U64.U64 -> QQ
+fromU64 = QQ
+
+toU64 :: QQ -> U64.U64
+toU64 (QQ x) = x
+
+schema :: Schema.Schema
+schema = U64.schema
+
+bitPut :: QQ -> BitPut.BitPut
+bitPut = U64.bitPut . toU64
+
+bitGet :: BitGet.BitGet QQ
+bitGet = BitGet.label "QQ" $ fmap fromU64 U64.bitGet
diff --git a/src/lib/Rattletrap/Type/RemoteId/Splitscreen.hs b/src/lib/Rattletrap/Type/RemoteId/Splitscreen.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/RemoteId/Splitscreen.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Type.RemoteId.Splitscreen where
+
+import qualified Data.Word as Word
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Splitscreen
+  = Splitscreen Word.Word32
+  deriving (Eq, Show)
+
+instance Json.FromJSON Splitscreen where
+  parseJSON = fmap fromWord32 . Json.parseJSON
+
+instance Json.ToJSON Splitscreen where
+  toJSON = Json.toJSON . toWord32
+
+fromWord32 :: Word.Word32 -> Splitscreen
+fromWord32 = Splitscreen
+
+toWord32 :: Splitscreen -> Word.Word32
+toWord32 (Splitscreen x) = x
+
+schema :: Schema.Schema
+schema = Schema.integer
+
+bitPut :: Splitscreen -> BitPut.BitPut
+bitPut = BitPut.bits 24 . toWord32
+
+bitGet :: BitGet.BitGet Splitscreen
+bitGet = BitGet.label "Splitscreen" . fmap fromWord32 $ BitGet.bits 24
diff --git a/src/lib/Rattletrap/Type/RemoteId/Steam.hs b/src/lib/Rattletrap/Type/RemoteId/Steam.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/RemoteId/Steam.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Type.RemoteId.Steam where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U64 as U64
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Steam
+  = Steam U64.U64
+  deriving (Eq, Show)
+
+instance Json.FromJSON Steam where
+  parseJSON = fmap fromU64 . Json.parseJSON
+
+instance Json.ToJSON Steam where
+  toJSON = Json.toJSON . toU64
+
+fromU64 :: U64.U64 -> Steam
+fromU64 = Steam
+
+toU64 :: Steam -> U64.U64
+toU64 (Steam x) = x
+
+schema :: Schema.Schema
+schema = U64.schema
+
+bitPut :: Steam -> BitPut.BitPut
+bitPut = U64.bitPut . toU64
+
+bitGet :: BitGet.BitGet Steam
+bitGet = BitGet.label "Steam" $ fmap fromU64 U64.bitGet
diff --git a/src/lib/Rattletrap/Type/RemoteId/Switch.hs b/src/lib/Rattletrap/Type/RemoteId/Switch.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/RemoteId/Switch.hs
@@ -0,0 +1,41 @@
+module Rattletrap.Type.RemoteId.Switch where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U64 as U64
+import qualified Rattletrap.Utility.Json as Json
+
+data Switch = Switch
+  { a :: U64.U64,
+    b :: U64.U64,
+    c :: U64.U64,
+    d :: U64.U64
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Switch where
+  parseJSON json = do
+    (a, b, c, d) <- Json.parseJSON json
+    pure Switch {a, b, c, d}
+
+instance Json.ToJSON Switch where
+  toJSON x = Json.toJSON (a x, b x, c x, d x)
+
+schema :: Schema.Schema
+schema =
+  Schema.named "remote-id-switch" . Schema.tuple . replicate 4 $
+    Schema.ref
+      U64.schema
+
+bitPut :: Switch -> BitPut.BitPut
+bitPut x =
+  U64.bitPut (a x) <> U64.bitPut (b x) <> U64.bitPut (c x) <> U64.bitPut (d x)
+
+bitGet :: BitGet.BitGet Switch
+bitGet = BitGet.label "Switch" $ do
+  a <- U64.bitGet
+  b <- U64.bitGet
+  c <- U64.bitGet
+  d <- U64.bitGet
+  pure Switch {a, b, c, d}
diff --git a/src/lib/Rattletrap/Type/RemoteId/Xbox.hs b/src/lib/Rattletrap/Type/RemoteId/Xbox.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/RemoteId/Xbox.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Type.RemoteId.Xbox where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U64 as U64
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Xbox
+  = Xbox U64.U64
+  deriving (Eq, Show)
+
+instance Json.FromJSON Xbox where
+  parseJSON = fmap fromU64 . Json.parseJSON
+
+instance Json.ToJSON Xbox where
+  toJSON = Json.toJSON . toU64
+
+fromU64 :: U64.U64 -> Xbox
+fromU64 = Xbox
+
+toU64 :: Xbox -> U64.U64
+toU64 (Xbox x) = x
+
+schema :: Schema.Schema
+schema = U64.schema
+
+bitPut :: Xbox -> BitPut.BitPut
+bitPut = U64.bitPut . toU64
+
+bitGet :: BitGet.BitGet Xbox
+bitGet = BitGet.label "Xbox" $ fmap fromU64 U64.bitGet
diff --git a/src/lib/Rattletrap/Type/Replay.hs b/src/lib/Rattletrap/Type/Replay.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Replay.hs
@@ -0,0 +1,134 @@
+module Rattletrap.Type.Replay where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Content as Content
+import qualified Rattletrap.Type.Dictionary as Dictionary
+import qualified Rattletrap.Type.Header as Header
+import qualified Rattletrap.Type.I32 as I32
+import qualified Rattletrap.Type.Property as Property
+import qualified Rattletrap.Type.Property.Int as Property.Int
+import qualified Rattletrap.Type.Property.Name as Property.Name
+import qualified Rattletrap.Type.Property.Str as Property.Str
+import qualified Rattletrap.Type.PropertyValue as PropertyValue
+import qualified Rattletrap.Type.Section as Section
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Json as Json
+import qualified Rattletrap.Version as Version
+
+type Replay =
+  ReplayWith
+    (Section.Section Header.Header)
+    (Section.Section Content.Content)
+
+-- | A Rocket League replay.
+data ReplayWith header content = Replay
+  { -- | This has most of the high-level metadata.
+    header :: header,
+    -- | This has most of the low-level game data.
+    content :: content
+  }
+  deriving (Eq, Show)
+
+instance (Json.FromJSON h, Json.FromJSON c) => Json.FromJSON (ReplayWith h c) where
+  parseJSON = Json.withObject "Replay" $ \object -> do
+    header <- Json.required object "header"
+    content <- Json.required object "content"
+    pure Replay {header, content}
+
+instance (Json.ToJSON h, Json.ToJSON c) => Json.ToJSON (ReplayWith h c) where
+  toJSON x =
+    Json.object
+      [ Json.pair "$schema" schemaUrl,
+        Json.pair "header" $ header x,
+        Json.pair "content" $ content x
+      ]
+
+schema :: Schema.Schema -> Schema.Schema -> Schema.Schema
+schema h c =
+  Schema.named "replay" $
+    Schema.object
+      [ (Json.pair "header" $ Schema.ref h, True),
+        (Json.pair "content" $ Schema.ref c, True)
+      ]
+
+schemaUrl :: String
+schemaUrl =
+  mconcat
+    [ "https://github.com/tfausak/rattletrap/releases/download/",
+      Version.string,
+      "/rattletrap-",
+      Version.string,
+      "-schema.json"
+    ]
+
+bytePut :: Replay -> BytePut.BytePut
+bytePut x =
+  Section.bytePut Header.bytePut (header x)
+    <> Section.bytePut Content.bytePut (content x)
+
+byteGet :: Bool -> Bool -> ByteGet.ByteGet Replay
+byteGet fast skip = ByteGet.label "Replay" $ do
+  header <- ByteGet.label "header" $ do
+    section <-
+      Section.byteGet skip $ ByteGet.byteString . fromIntegral . U32.toWord32
+    body <- ByteGet.embed Header.byteGet $ Section.body section
+    pure section {Section.body}
+  content <- ByteGet.label "content" $ do
+    section <-
+      Section.byteGet skip $ ByteGet.byteString . fromIntegral . U32.toWord32
+    body <-
+      if fast
+        then pure Content.empty
+        else
+          ByteGet.embed (getContent $ Section.body header) $
+            Section.body section
+    pure section {Section.body}
+  pure Replay {header, content}
+
+getContent :: Header.Header -> ByteGet.ByteGet Content.Content
+getContent h =
+  Content.byteGet
+    (getMatchType h)
+    (Header.version h)
+    (getNumFrames h)
+    (getMaxChannels h)
+    (getBuildVersion h)
+
+getMatchType :: Header.Header -> Maybe Str.Str
+getMatchType header = do
+  Property.Property {Property.value} <-
+    Dictionary.lookup (Str.fromString "MatchType") $ Header.properties header
+  case value of
+    PropertyValue.Name x -> Just $ Property.Name.toStr x
+    _ -> Nothing
+
+getNumFrames :: Header.Header -> Int
+getNumFrames header_ =
+  case Dictionary.lookup
+    (Str.fromString "NumFrames")
+    (Header.properties header_) of
+    Just (Property.Property _ _ _ (PropertyValue.Int numFrames)) ->
+      fromIntegral (I32.toInt32 (Property.Int.toI32 numFrames))
+    _ -> 0
+
+getMaxChannels :: Header.Header -> Word
+getMaxChannels header_ =
+  subtract 1 $
+    case Dictionary.lookup
+      (Str.fromString "MaxChannels")
+      (Header.properties header_) of
+      Just (Property.Property _ _ _ (PropertyValue.Int maxChannels)) ->
+        fromIntegral (I32.toInt32 (Property.Int.toI32 maxChannels))
+      _ -> 1023
+
+getBuildVersion :: Header.Header -> Maybe Str.Str
+getBuildVersion header = do
+  property <-
+    Dictionary.lookup (Str.fromString "BuildVersion") $
+      Header.properties header
+  case Property.value property of
+    PropertyValue.Str x -> Just $ Property.Str.toStr x
+    _ -> Nothing
diff --git a/src/lib/Rattletrap/Type/Replication.hs b/src/lib/Rattletrap/Type/Replication.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Replication.hs
@@ -0,0 +1,128 @@
+module Rattletrap.Type.Replication where
+
+import qualified Data.Map as Map
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.ClassAttributeMap as ClassAttributeMap
+import qualified Rattletrap.Type.CompressedWord as CompressedWord
+import qualified Rattletrap.Type.List as RList
+import qualified Rattletrap.Type.ReplicationValue as ReplicationValue
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data Replication = Replication
+  { actorId :: CompressedWord.CompressedWord,
+    value :: ReplicationValue.ReplicationValue
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Replication where
+  parseJSON = Json.withObject "Replication" $ \object -> do
+    actorId <- Json.required object "actor_id"
+    value <- Json.required object "value"
+    pure Replication {actorId, value}
+
+instance Json.ToJSON Replication where
+  toJSON x =
+    Json.object [Json.pair "actor_id" $ actorId x, Json.pair "value" $ value x]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "replication" $
+    Schema.object
+      [ (Json.pair "actor_id" $ Schema.ref CompressedWord.schema, True),
+        (Json.pair "value" $ Schema.ref ReplicationValue.schema, True)
+      ]
+
+putReplications :: RList.List Replication -> BitPut.BitPut
+putReplications xs =
+  foldMap (\x -> BitPut.bool True <> bitPut x) (RList.toList xs)
+    <> BitPut.bool False
+
+bitPut :: Replication -> BitPut.BitPut
+bitPut replication =
+  CompressedWord.bitPut (actorId replication)
+    <> ReplicationValue.bitPut (value replication)
+
+decodeReplicationsBits ::
+  Maybe Str.Str ->
+  Version.Version ->
+  Maybe Str.Str ->
+  Word ->
+  ClassAttributeMap.ClassAttributeMap ->
+  Map.Map CompressedWord.CompressedWord U32.U32 ->
+  BitGet.BitGet
+    ( Map.Map CompressedWord.CompressedWord U32.U32,
+      RList.List Replication
+    )
+decodeReplicationsBits matchType version buildVersion limit classes actorMap =
+  decodeReplicationsBitsWith
+    matchType
+    version
+    buildVersion
+    limit
+    classes
+    actorMap
+    0
+    []
+
+decodeReplicationsBitsWith ::
+  Maybe Str.Str ->
+  Version.Version ->
+  Maybe Str.Str ->
+  Word ->
+  ClassAttributeMap.ClassAttributeMap ->
+  Map.Map CompressedWord.CompressedWord U32.U32 ->
+  Int ->
+  [Replication] ->
+  BitGet.BitGet
+    ( Map.Map CompressedWord.CompressedWord U32.U32,
+      RList.List Replication
+    )
+decodeReplicationsBitsWith matchType version buildVersion limit classes actorMap index replications =
+  do
+    hasReplication <- BitGet.bool
+    if hasReplication
+      then do
+        (newActorMap, replication) <-
+          BitGet.label ("element (" <> show index <> ")") $
+            bitGet matchType version buildVersion limit classes actorMap
+        decodeReplicationsBitsWith
+          matchType
+          version
+          buildVersion
+          limit
+          classes
+          newActorMap
+          (index + 1)
+          $ replication
+            : replications
+      else pure (actorMap, RList.fromList $ reverse replications)
+
+bitGet ::
+  Maybe Str.Str ->
+  Version.Version ->
+  Maybe Str.Str ->
+  Word ->
+  ClassAttributeMap.ClassAttributeMap ->
+  Map.Map CompressedWord.CompressedWord U32.U32 ->
+  BitGet.BitGet
+    ( Map.Map CompressedWord.CompressedWord U32.U32,
+      Replication
+    )
+bitGet matchType version buildVersion limit classes actorMap =
+  BitGet.label "Replication" $ do
+    actorId <- BitGet.label "actorId" $ CompressedWord.bitGet limit
+    (newActorMap, value) <-
+      BitGet.label "value" $
+        ReplicationValue.bitGet
+          matchType
+          version
+          buildVersion
+          classes
+          actorId
+          actorMap
+    pure (newActorMap, Replication {actorId, value})
diff --git a/src/lib/Rattletrap/Type/Replication/Destroyed.hs b/src/lib/Rattletrap/Type/Replication/Destroyed.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Replication/Destroyed.hs
@@ -0,0 +1,30 @@
+module Rattletrap.Type.Replication.Destroyed where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Utility.Json as Json
+
+-- | 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 Destroyed = Destroyed
+  deriving (Eq, Show)
+
+instance Json.FromJSON Destroyed where
+  parseJSON json = do
+    () <- Json.parseJSON json
+    pure Destroyed
+
+instance Json.ToJSON Destroyed where
+  toJSON = const $ Json.toJSON ()
+
+schema :: Schema.Schema
+schema =
+  Schema.named "replication-destroyed" $ Json.object [Json.pair "type" "array"]
+
+bitPut :: Destroyed -> BitPut.BitPut
+bitPut _ = mempty
+
+bitGet :: BitGet.BitGet Destroyed
+bitGet = BitGet.label "Destroyed" $ pure Destroyed
diff --git a/src/lib/Rattletrap/Type/Replication/Spawned.hs b/src/lib/Rattletrap/Type/Replication/Spawned.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Replication/Spawned.hs
@@ -0,0 +1,164 @@
+module Rattletrap.Type.Replication.Spawned where
+
+import qualified Data.Map as Map
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Exception.MissingClassName as MissingClassName
+import qualified Rattletrap.Exception.MissingObjectName as MissingObjectName
+import qualified Rattletrap.Exception.UnknownName as UnknownName
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.ClassAttributeMap as ClassAttributeMap
+import qualified Rattletrap.Type.CompressedWord as CompressedWord
+import qualified Rattletrap.Type.Initialization as Initialization
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+import qualified Rattletrap.Utility.Monad as Monad
+
+data Spawned = Spawned
+  { -- | Unclear what this is.
+    flag :: Bool,
+    nameIndex :: Maybe U32.U32,
+    -- | Read-only! Changing a replication's name requires editing the
+    -- 'nameIndex' and maybe the class attribute map.
+    name :: Maybe Str.Str,
+    objectId :: U32.U32,
+    -- | Read-only! Changing a replication's object requires editing the class
+    -- attribute map.
+    objectName :: Str.Str,
+    -- | Read-only! Changing a replication's class requires editing the class
+    -- attribute map.
+    className :: Str.Str,
+    initialization :: Initialization.Initialization
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Spawned where
+  parseJSON = Json.withObject "Spawned" $ \object -> do
+    flag <- Json.required object "flag"
+    nameIndex <- Json.optional object "name_index"
+    name <- Json.optional object "name"
+    objectId <- Json.required object "object_id"
+    objectName <- Json.required object "object_name"
+    className <- Json.required object "class_name"
+    initialization <- Json.required object "initialization"
+    pure
+      Spawned
+        { flag,
+          nameIndex,
+          name,
+          objectId,
+          objectName,
+          className,
+          initialization
+        }
+
+instance Json.ToJSON Spawned where
+  toJSON x =
+    Json.object
+      [ Json.pair "flag" $ flag x,
+        Json.pair "name_index" $ nameIndex x,
+        Json.pair "name" $ name x,
+        Json.pair "object_id" $ objectId x,
+        Json.pair "object_name" $ objectName x,
+        Json.pair "class_name" $ className x,
+        Json.pair "initialization" $ initialization x
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "replication-spawned" $
+    Schema.object
+      [ (Json.pair "flag" $ Schema.ref Schema.boolean, True),
+        (Json.pair "name_index" . Schema.json $ Schema.maybe U32.schema, False),
+        (Json.pair "name" . Schema.json $ Schema.maybe Str.schema, False),
+        (Json.pair "object_id" $ Schema.ref U32.schema, True),
+        (Json.pair "object_name" $ Schema.ref Str.schema, True),
+        (Json.pair "class_name" $ Schema.ref Str.schema, True),
+        (Json.pair "initialization" $ Schema.ref Initialization.schema, True)
+      ]
+
+bitPut :: Spawned -> BitPut.BitPut
+bitPut spawnedReplication =
+  BitPut.bool (flag spawnedReplication)
+    <> foldMap U32.bitPut (nameIndex spawnedReplication)
+    <> U32.bitPut (objectId spawnedReplication)
+    <> Initialization.bitPut (initialization spawnedReplication)
+
+bitGet ::
+  Maybe Str.Str ->
+  Version.Version ->
+  ClassAttributeMap.ClassAttributeMap ->
+  CompressedWord.CompressedWord ->
+  Map.Map CompressedWord.CompressedWord U32.U32 ->
+  BitGet.BitGet
+    (Map.Map CompressedWord.CompressedWord U32.U32, Spawned)
+bitGet matchType version classAttributeMap actorId actorMap =
+  BitGet.label "Spawned" $ do
+    flag <- BitGet.label "flag" BitGet.bool
+    nameIndex <-
+      BitGet.label "nameIndex" $
+        Monad.whenMaybe (hasNameIndex matchType version) U32.bitGet
+    name <- lookupName classAttributeMap nameIndex
+    objectId <- BitGet.label "objectId" U32.bitGet
+    objectName <- lookupObjectName classAttributeMap objectId
+    className <- lookupClassName objectName
+    let hasLocation = ClassAttributeMap.classHasLocation className
+    let hasRotation = ClassAttributeMap.classHasRotation className
+    initialization <-
+      BitGet.label "initialization" $
+        Initialization.bitGet version hasLocation hasRotation
+    pure
+      ( Map.insert actorId objectId actorMap,
+        Spawned
+          { flag,
+            nameIndex,
+            name,
+            objectId,
+            objectName,
+            className,
+            initialization
+          }
+      )
+
+hasNameIndex :: Maybe Str.Str -> Version.Version -> Bool
+hasNameIndex matchType version =
+  Version.atLeast 868 20 0 version
+    || Version.atLeast 868 14 0 version
+      && (matchType /= Just (Str.fromString "Lan"))
+
+lookupName ::
+  ClassAttributeMap.ClassAttributeMap ->
+  Maybe U32.U32 ->
+  BitGet.BitGet (Maybe Str.Str)
+lookupName classAttributeMap maybeNameIndex = case maybeNameIndex of
+  Nothing -> pure Nothing
+  Just nameIndex_ ->
+    case ClassAttributeMap.getName
+      (ClassAttributeMap.nameMap classAttributeMap)
+      nameIndex_ of
+      Nothing ->
+        BitGet.throw . UnknownName.UnknownName $ U32.toWord32 nameIndex_
+      Just name_ -> pure (Just name_)
+
+lookupObjectName ::
+  ClassAttributeMap.ClassAttributeMap -> U32.U32 -> BitGet.BitGet Str.Str
+lookupObjectName classAttributeMap objectId_ =
+  case ClassAttributeMap.getObjectName
+    (ClassAttributeMap.objectMap classAttributeMap)
+    objectId_ of
+    Nothing ->
+      BitGet.throw . MissingObjectName.MissingObjectName $
+        U32.toWord32
+          objectId_
+    Just objectName_ -> pure objectName_
+
+lookupClassName :: Str.Str -> BitGet.BitGet Str.Str
+lookupClassName objectName_ =
+  case ClassAttributeMap.getClassName objectName_ of
+    Nothing ->
+      BitGet.throw . MissingClassName.MissingClassName $
+        Str.toString
+          objectName_
+    Just className_ -> pure className_
diff --git a/src/lib/Rattletrap/Type/Replication/Updated.hs b/src/lib/Rattletrap/Type/Replication/Updated.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Replication/Updated.hs
@@ -0,0 +1,52 @@
+module Rattletrap.Type.Replication.Updated where
+
+import qualified Data.Map as Map
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Attribute as Attribute
+import qualified Rattletrap.Type.ClassAttributeMap as ClassAttributeMap
+import qualified Rattletrap.Type.CompressedWord as CompressedWord
+import qualified Rattletrap.Type.List as RList
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+import qualified Rattletrap.Utility.Monad as Monad
+
+newtype Updated = Updated
+  { attributes :: RList.List Attribute.Attribute
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Updated where
+  parseJSON = fmap Updated . Json.parseJSON
+
+instance Json.ToJSON Updated where
+  toJSON = Json.toJSON . attributes
+
+schema :: Schema.Schema
+schema =
+  Schema.named "replication-updated" . Schema.json $
+    RList.schema
+      Attribute.schema
+
+bitPut :: Updated -> BitPut.BitPut
+bitPut x =
+  foldMap
+    (\y -> BitPut.bool True <> Attribute.bitPut y)
+    (RList.toList $ attributes x)
+    <> BitPut.bool False
+
+bitGet ::
+  Version.Version ->
+  Maybe Str.Str ->
+  ClassAttributeMap.ClassAttributeMap ->
+  Map.Map CompressedWord.CompressedWord U32.U32 ->
+  CompressedWord.CompressedWord ->
+  BitGet.BitGet Updated
+bitGet version buildVersion classes actors actor =
+  BitGet.label "Updated" . fmap Updated . RList.untilM $ do
+    p <- BitGet.bool
+    Monad.whenMaybe p $
+      Attribute.bitGet version buildVersion classes actors actor
diff --git a/src/lib/Rattletrap/Type/ReplicationValue.hs b/src/lib/Rattletrap/Type/ReplicationValue.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/ReplicationValue.hs
@@ -0,0 +1,95 @@
+module Rattletrap.Type.ReplicationValue where
+
+import qualified Data.Foldable as Foldable
+import qualified Data.Map as Map
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.ClassAttributeMap as ClassAttributeMap
+import qualified Rattletrap.Type.CompressedWord as CompressedWord
+import qualified Rattletrap.Type.Replication.Destroyed as Destroyed
+import qualified Rattletrap.Type.Replication.Spawned as Spawned
+import qualified Rattletrap.Type.Replication.Updated as Updated
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data ReplicationValue
+  = -- | Creates a new actor.
+    Spawned Spawned.Spawned
+  | -- | Updates an existing actor.
+    Updated Updated.Updated
+  | -- | Destroys an existing actor.
+    Destroyed Destroyed.Destroyed
+  deriving (Eq, Show)
+
+instance Json.FromJSON ReplicationValue where
+  parseJSON = Json.withObject "ReplicationValue" $ \object ->
+    Foldable.asum
+      [ fmap Spawned $ Json.required object "spawned",
+        fmap Updated $ Json.required object "updated",
+        fmap Destroyed $ Json.required object "destroyed"
+      ]
+
+instance Json.ToJSON ReplicationValue where
+  toJSON x = case x of
+    Spawned y -> Json.object [Json.pair "spawned" y]
+    Updated y -> Json.object [Json.pair "updated" y]
+    Destroyed y -> Json.object [Json.pair "destroyed" y]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "replicationValue" . Schema.oneOf $
+    fmap
+      (\(k, v) -> Schema.object [(Json.pair k $ Schema.ref v, True)])
+      [ ("spawned", Spawned.schema),
+        ("updated", Updated.schema),
+        ("destroyed", Destroyed.schema)
+      ]
+
+bitPut :: ReplicationValue -> BitPut.BitPut
+bitPut value = case value of
+  Spawned x -> BitPut.bool True <> BitPut.bool True <> Spawned.bitPut x
+  Updated x -> BitPut.bool True <> BitPut.bool False <> Updated.bitPut x
+  Destroyed x -> BitPut.bool False <> Destroyed.bitPut x
+
+bitGet ::
+  Maybe Str.Str ->
+  Version.Version ->
+  Maybe Str.Str ->
+  ClassAttributeMap.ClassAttributeMap ->
+  CompressedWord.CompressedWord ->
+  Map.Map CompressedWord.CompressedWord U32.U32 ->
+  BitGet.BitGet
+    ( Map.Map CompressedWord.CompressedWord U32.U32,
+      ReplicationValue
+    )
+bitGet matchType version buildVersion classAttributeMap actorId actorMap =
+  BitGet.label "ReplicationValue" $ do
+    isOpen <- BitGet.bool
+    if isOpen
+      then do
+        isNew <- BitGet.bool
+        if isNew
+          then do
+            (newActorMap, spawned) <-
+              Spawned.bitGet
+                matchType
+                version
+                classAttributeMap
+                actorId
+                actorMap
+            pure (newActorMap, Spawned spawned)
+          else do
+            updated <-
+              Updated.bitGet
+                version
+                buildVersion
+                classAttributeMap
+                actorMap
+                actorId
+            pure (actorMap, Updated updated)
+      else do
+        destroyed <- Destroyed.bitGet
+        pure (actorMap, Destroyed destroyed)
diff --git a/src/lib/Rattletrap/Type/Rotation.hs b/src/lib/Rattletrap/Type/Rotation.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Rotation.hs
@@ -0,0 +1,48 @@
+module Rattletrap.Type.Rotation where
+
+import qualified Data.Foldable as Foldable
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.CompressedWordVector as CompressedWordVector
+import qualified Rattletrap.Type.Quaternion as Quaternion
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data Rotation
+  = CompressedWordVector CompressedWordVector.CompressedWordVector
+  | Quaternion Quaternion.Quaternion
+  deriving (Eq, Show)
+
+instance Json.FromJSON Rotation where
+  parseJSON = Json.withObject "Rotation" $ \object ->
+    Foldable.asum
+      [ fmap CompressedWordVector $ Json.required object "compressed_word_vector",
+        fmap Quaternion $ Json.required object "quaternion"
+      ]
+
+instance Json.ToJSON Rotation where
+  toJSON x = case x of
+    CompressedWordVector y ->
+      Json.object [Json.pair "compressed_word_vector" y]
+    Quaternion y -> Json.object [Json.pair "quaternion" y]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "rotation" . Schema.oneOf $
+    fmap
+      (\(k, v) -> Schema.object [(Json.pair k $ Schema.ref v, True)])
+      [ ("compressed_word_vector", CompressedWordVector.schema),
+        ("quaternion", Quaternion.schema)
+      ]
+
+bitPut :: Rotation -> BitPut.BitPut
+bitPut r = case r of
+  CompressedWordVector cwv -> CompressedWordVector.bitPut cwv
+  Quaternion q -> Quaternion.bitPut q
+
+bitGet :: Version.Version -> BitGet.BitGet Rotation
+bitGet version =
+  if Version.atLeast 868 22 7 version
+    then fmap Quaternion Quaternion.bitGet
+    else fmap CompressedWordVector CompressedWordVector.bitGet
diff --git a/src/lib/Rattletrap/Type/Section.hs b/src/lib/Rattletrap/Type/Section.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Section.hs
@@ -0,0 +1,96 @@
+module Rattletrap.Type.Section where
+
+import qualified Control.Monad as Monad
+import qualified Data.ByteString as ByteString
+import qualified Data.Text as Text
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Exception.CrcMismatch as CrcMismatch
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Crc as Crc
+import qualified Rattletrap.Utility.Json as Json
+
+-- | 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
+  { -- | read only
+    size :: U32.U32,
+    -- | read only
+    crc :: U32.U32,
+    -- | The actual content in the section.
+    body :: a
+  }
+  deriving (Eq, Show)
+
+instance (Json.FromJSON a) => Json.FromJSON (Section a) where
+  parseJSON = Json.withObject "Section" $ \object -> do
+    size <- Json.required object "size"
+    crc <- Json.required object "crc"
+    body <- Json.required object "body"
+    pure Section {size, crc, body}
+
+instance (Json.ToJSON a) => Json.ToJSON (Section a) where
+  toJSON x =
+    Json.object
+      [ Json.pair "size" $ size x,
+        Json.pair "crc" $ crc x,
+        Json.pair "body" $ body x
+      ]
+
+schema :: Schema.Schema -> Schema.Schema
+schema s =
+  Schema.named ("section-" <> Text.unpack (Schema.name s)) $
+    Schema.object
+      [ (Json.pair "size" $ Schema.ref U32.schema, True),
+        (Json.pair "crc" $ Schema.ref U32.schema, True),
+        (Json.pair "body" $ Schema.ref s, True)
+      ]
+
+create :: (a -> BytePut.BytePut) -> a -> Section a
+create encode body_ =
+  let bytes = BytePut.toByteString $ encode body_
+   in Section
+        { size = U32.fromWord32 . fromIntegral $ ByteString.length bytes,
+          crc = U32.fromWord32 $ Crc.compute bytes,
+          body = body_
+        }
+
+-- | Given a way to put the 'body', puts a section. This will also put
+-- the size and CRC.
+bytePut :: (a -> BytePut.BytePut) -> Section a -> BytePut.BytePut
+bytePut putBody section =
+  let rawBody = BytePut.toByteString . putBody $ body section
+      size_ = ByteString.length rawBody
+      crc_ = Crc.compute rawBody
+   in U32.bytePut (U32.fromWord32 (fromIntegral size_))
+        <> U32.bytePut (U32.fromWord32 crc_)
+        <> BytePut.byteString rawBody
+
+byteGet ::
+  Bool -> (U32.U32 -> ByteGet.ByteGet a) -> ByteGet.ByteGet (Section a)
+byteGet skip getBody = ByteGet.label "Section" $ do
+  size <- ByteGet.label "size" U32.byteGet
+  crc <- ByteGet.label "crc" U32.byteGet
+  body <- ByteGet.label "body" $ do
+    rawBody <- ByteGet.byteString . fromIntegral $ U32.toWord32 size
+    Monad.unless skip $ do
+      let expected = U32.toWord32 crc
+          actual = Crc.compute rawBody
+      Monad.when (actual /= expected) . ByteGet.throw $
+        CrcMismatch.CrcMismatch
+          expected
+          actual
+    ByteGet.embed (getBody size) rawBody
+  pure Section {size, crc, body}
+
+crcMessage :: U32.U32 -> U32.U32 -> String
+crcMessage actual expected =
+  unwords
+    [ "[RT10] actual CRC",
+      show actual,
+      "does not match expected CRC",
+      show expected
+    ]
diff --git a/src/lib/Rattletrap/Type/Str.hs b/src/lib/Rattletrap/Type/Str.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Str.hs
@@ -0,0 +1,101 @@
+module Rattletrap.Type.Str where
+
+import qualified Data.ByteString as ByteString
+import qualified Data.Char as Char
+import qualified Data.Int as Int
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+import qualified Data.Text.Encoding.Error as Text
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.I32 as I32
+import qualified Rattletrap.Utility.Bytes as Bytes
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Str
+  = Str Text.Text
+  deriving (Eq, Ord, Show)
+
+instance Json.FromJSON Str where
+  parseJSON = fmap fromText . Json.parseJSON
+
+instance Json.ToJSON Str where
+  toJSON = Json.toJSON . toText
+
+schema :: Schema.Schema
+schema = Schema.named "str" $ Json.object [Json.pair "type" "string"]
+
+fromText :: Text.Text -> Str
+fromText = Str
+
+toText :: Str -> Text.Text
+toText (Str x) = x
+
+fromString :: String -> Str
+fromString = fromText . Text.pack
+
+toString :: Str -> String
+toString = Text.unpack . toText
+
+bytePut :: Str -> BytePut.BytePut
+bytePut text =
+  let size = getTextSize text
+      encode = getTextEncoder size
+   in I32.bytePut size <> (BytePut.byteString . encode . addNull $ toText text)
+
+bitPut :: Str -> BitPut.BitPut
+bitPut = BitPut.fromBytePut . bytePut
+
+getTextSize :: Str -> I32.I32
+getTextSize text =
+  let value = toText text
+      scale = if Text.all Char.isLatin1 value then 1 else -1 :: Int.Int32
+      rawSize =
+        if Text.null value
+          then 0
+          else fromIntegral (Text.length value) + 1 :: Int.Int32
+      size =
+        if value == Text.pack "\x00\x00\x00None"
+          then 0x05000000
+          else scale * rawSize :: Int.Int32
+   in I32.fromInt32 size
+
+getTextEncoder :: I32.I32 -> Text.Text -> ByteString.ByteString
+getTextEncoder size text =
+  if I32.toInt32 size < 0
+    then Text.encodeUtf16LE text
+    else Bytes.encodeLatin1 text
+
+addNull :: Text.Text -> Text.Text
+addNull text = if Text.null text then text else Text.snoc text '\x00'
+
+byteGet :: ByteGet.ByteGet Str
+byteGet = ByteGet.label "Str" $ do
+  size <- ByteGet.label "size" I32.byteGet
+  bytes <- ByteGet.label "value" . ByteGet.byteString $ normalizeTextSize size
+  pure . fromText . dropNull $ getTextDecoder size bytes
+
+bitGet :: BitGet.BitGet Str
+bitGet = do
+  rawSize <- I32.bitGet
+  bytes <- BitGet.byteString (normalizeTextSize rawSize)
+  pure (fromText (dropNull (getTextDecoder rawSize bytes)))
+
+normalizeTextSize :: (Integral a) => I32.I32 -> a
+normalizeTextSize size = case I32.toInt32 size of
+  0x05000000 -> 8
+  x -> (if x < 0 then negate . (*) 2 else id) $ fromIntegral x
+
+getTextDecoder :: I32.I32 -> ByteString.ByteString -> Text.Text
+getTextDecoder size bytes =
+  let decode =
+        if I32.toInt32 size < 0
+          then Text.decodeUtf16LEWith Text.lenientDecode
+          else Text.decodeLatin1
+   in decode bytes
+
+dropNull :: Text.Text -> Text.Text
+dropNull = Text.dropWhileEnd (== '\x00')
diff --git a/src/lib/Rattletrap/Type/U32.hs b/src/lib/Rattletrap/Type/U32.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/U32.hs
@@ -0,0 +1,46 @@
+module Rattletrap.Type.U32 where
+
+import qualified Data.Word as Word
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Utility.Json as Json
+
+newtype U32
+  = U32 Word.Word32
+  deriving (Eq, Ord, Show)
+
+instance Json.FromJSON U32 where
+  parseJSON = fmap fromWord32 . Json.parseJSON
+
+instance Json.ToJSON U32 where
+  toJSON = Json.toJSON . toWord32
+
+schema :: Schema.Schema
+schema =
+  Schema.named "u32" $
+    Json.object
+      [ Json.pair "type" "integer",
+        Json.pair "minimum" (minBound :: Word.Word32),
+        Json.pair "maximum" (maxBound :: Word.Word32)
+      ]
+
+fromWord32 :: Word.Word32 -> U32
+fromWord32 = U32
+
+toWord32 :: U32 -> Word.Word32
+toWord32 (U32 x) = x
+
+bytePut :: U32 -> BytePut.BytePut
+bytePut = BytePut.word32 . toWord32
+
+bitPut :: U32 -> BitPut.BitPut
+bitPut = BitPut.fromBytePut . bytePut
+
+byteGet :: ByteGet.ByteGet U32
+byteGet = ByteGet.label "U32" $ fmap fromWord32 ByteGet.word32
+
+bitGet :: BitGet.BitGet U32
+bitGet = BitGet.fromByteGet byteGet 4
diff --git a/src/lib/Rattletrap/Type/U64.hs b/src/lib/Rattletrap/Type/U64.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/U64.hs
@@ -0,0 +1,48 @@
+module Rattletrap.Type.U64 where
+
+import qualified Data.Text as Text
+import qualified Data.Word as Word
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Utility.Json as Json
+import qualified Text.Read as Read
+
+newtype U64
+  = U64 Word.Word64
+  deriving (Eq, Show)
+
+instance Json.FromJSON U64 where
+  parseJSON =
+    Json.withText "U64" $
+      either fail (pure . fromWord64)
+        . Read.readEither
+        . Text.unpack
+
+instance Json.ToJSON U64 where
+  toJSON = Json.toJSON . show . toWord64
+
+schema :: Schema.Schema
+schema =
+  Schema.named "u64" $
+    Json.object [Json.pair "type" "string", Json.pair "pattern" "^[0-9]+$"]
+
+fromWord64 :: Word.Word64 -> U64
+fromWord64 = U64
+
+toWord64 :: U64 -> Word.Word64
+toWord64 (U64 x) = x
+
+bytePut :: U64 -> BytePut.BytePut
+bytePut = BytePut.word64 . toWord64
+
+bitPut :: U64 -> BitPut.BitPut
+bitPut = BitPut.fromBytePut . bytePut
+
+byteGet :: ByteGet.ByteGet U64
+byteGet = ByteGet.label "U64" $ fmap fromWord64 ByteGet.word64
+
+bitGet :: BitGet.BitGet U64
+bitGet = BitGet.fromByteGet byteGet 8
diff --git a/src/lib/Rattletrap/Type/U8.hs b/src/lib/Rattletrap/Type/U8.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/U8.hs
@@ -0,0 +1,46 @@
+module Rattletrap.Type.U8 where
+
+import qualified Data.Word as Word
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Utility.Json as Json
+
+newtype U8
+  = U8 Word.Word8
+  deriving (Eq, Show)
+
+instance Json.FromJSON U8 where
+  parseJSON = fmap fromWord8 . Json.parseJSON
+
+instance Json.ToJSON U8 where
+  toJSON = Json.toJSON . toWord8
+
+schema :: Schema.Schema
+schema =
+  Schema.named "u8" $
+    Json.object
+      [ Json.pair "type" "integer",
+        Json.pair "minimum" (minBound :: Word.Word8),
+        Json.pair "maximum" (maxBound :: Word.Word8)
+      ]
+
+fromWord8 :: Word.Word8 -> U8
+fromWord8 = U8
+
+toWord8 :: U8 -> Word.Word8
+toWord8 (U8 x) = x
+
+bytePut :: U8 -> BytePut.BytePut
+bytePut = BytePut.word8 . toWord8
+
+bitPut :: U8 -> BitPut.BitPut
+bitPut = BitPut.fromBytePut . bytePut
+
+byteGet :: ByteGet.ByteGet U8
+byteGet = ByteGet.label "U8" $ fmap fromWord8 ByteGet.word8
+
+bitGet :: BitGet.BitGet U8
+bitGet = BitGet.fromByteGet byteGet 1
diff --git a/src/lib/Rattletrap/Type/Vector.hs b/src/lib/Rattletrap/Type/Vector.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Vector.hs
@@ -0,0 +1,93 @@
+module Rattletrap.Type.Vector where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.CompressedWord as CompressedWord
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+data Vector = Vector
+  { size :: CompressedWord.CompressedWord,
+    -- | This field is guaranteed to be small. In other words, it won't overflow.
+    -- It's stored as a regular 'Word' rather than something more precise like a
+    -- 'Word8' because it just gets passed to a functions that expect 'Word's.
+    -- There's no reason to do a bunch of conversions.
+    bias :: Word,
+    -- | See 'bias'.
+    x :: Int,
+    -- | See 'bias'.
+    y :: Int,
+    -- | See 'bias'.
+    z :: Int
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Vector where
+  parseJSON = Json.withObject "Vector" $ \object -> do
+    size <- Json.required object "size"
+    bias <- Json.required object "bias"
+    x <- Json.required object "x"
+    y <- Json.required object "y"
+    z <- Json.required object "z"
+    pure Vector {size, bias, x, y, z}
+
+instance Json.ToJSON Vector where
+  toJSON a =
+    Json.object
+      [ Json.pair "size" $ size a,
+        Json.pair "bias" $ bias a,
+        Json.pair "x" $ x a,
+        Json.pair "y" $ y a,
+        Json.pair "z" $ z a
+      ]
+
+schema :: Schema.Schema
+schema =
+  Schema.named "vector" $
+    Schema.object
+      [ (Json.pair "size" $ Schema.ref CompressedWord.schema, True),
+        (Json.pair "bias" $ Schema.ref Schema.integer, True),
+        (Json.pair "x" $ Schema.ref Schema.integer, True),
+        (Json.pair "y" $ Schema.ref Schema.integer, True),
+        (Json.pair "z" $ Schema.ref Schema.integer, True)
+      ]
+
+bitPut :: Vector -> BitPut.BitPut
+bitPut vector =
+  let bitSize =
+        round (logBase (2 :: Float) (fromIntegral (bias vector))) - 1 :: Word
+      dx = fromIntegral (x vector + fromIntegral (bias vector)) :: Word
+      dy = fromIntegral (y vector + fromIntegral (bias vector)) :: Word
+      dz = fromIntegral (z vector + fromIntegral (bias vector)) :: Word
+      limit = 2 ^ (bitSize + 2) :: Word
+   in CompressedWord.bitPut (size vector)
+        <> CompressedWord.bitPut (CompressedWord.CompressedWord limit dx)
+        <> CompressedWord.bitPut (CompressedWord.CompressedWord limit dy)
+        <> CompressedWord.bitPut (CompressedWord.CompressedWord limit dz)
+
+bitGet :: Version.Version -> BitGet.BitGet Vector
+bitGet version = BitGet.label "Vector" $ do
+  size <-
+    BitGet.label "size"
+      . CompressedWord.bitGet
+      $ if Version.atLeast 868 22 7 version then 21 else 19
+  let limit = getLimit size
+      bias = getBias size
+  x <- BitGet.label "x" $ getPart limit bias
+  y <- BitGet.label "y" $ getPart limit bias
+  z <- BitGet.label "z" $ getPart limit bias
+  pure Vector {size, bias, x, y, z}
+
+getPart :: Word -> Word -> BitGet.BitGet Int
+getPart limit bias = fmap (fromDelta bias) (CompressedWord.bitGet limit)
+
+getLimit :: CompressedWord.CompressedWord -> Word
+getLimit = (2 ^) . (+ 2) . CompressedWord.value
+
+getBias :: CompressedWord.CompressedWord -> Word
+getBias = (2 ^) . (+ 1) . CompressedWord.value
+
+fromDelta :: Word -> CompressedWord.CompressedWord -> Int
+fromDelta bias_ x_ =
+  fromIntegral (CompressedWord.value x_) - fromIntegral bias_
diff --git a/src/lib/Rattletrap/Type/Version.hs b/src/lib/Rattletrap/Type/Version.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Version.hs
@@ -0,0 +1,38 @@
+module Rattletrap.Type.Version where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Monad as Monad
+
+data Version = Version
+  { major :: U32.U32,
+    minor :: U32.U32,
+    patch :: Maybe U32.U32
+  }
+  deriving (Eq, Show)
+
+atLeast :: Int -> Int -> Int -> Version -> Bool
+atLeast m n p v =
+  (U32.toWord32 (major v) >= fromIntegral m)
+    && (U32.toWord32 (minor v) >= fromIntegral n)
+    && (maybe 0 U32.toWord32 (patch v) >= fromIntegral p)
+
+bytePut :: Version -> BytePut.BytePut
+bytePut x =
+  U32.bytePut (major x)
+    <> U32.bytePut (minor x)
+    <> foldMap
+      U32.bytePut
+      (patch x)
+
+byteGet :: ByteGet.ByteGet Version
+byteGet = ByteGet.label "Version" $ do
+  major <- ByteGet.label "major" U32.byteGet
+  minor <- ByteGet.label "minor" U32.byteGet
+  patch <-
+    ByteGet.label "patch" $
+      Monad.whenMaybe
+        (U32.toWord32 major >= 868 && U32.toWord32 minor >= 18)
+        U32.byteGet
+  pure Version {major, minor, patch}
diff --git a/src/lib/Rattletrap/Utility/Bytes.hs b/src/lib/Rattletrap/Utility/Bytes.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Utility/Bytes.hs
@@ -0,0 +1,13 @@
+module Rattletrap.Utility.Bytes where
+
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Char8 as Latin1
+import qualified Data.Text as Text
+
+encodeLatin1 :: Text.Text -> ByteString.ByteString
+encodeLatin1 text = Latin1.pack (Text.unpack text)
+
+padBytes :: (Integral a) => a -> ByteString.ByteString -> ByteString.ByteString
+padBytes size bytes =
+  bytes
+    <> ByteString.replicate (fromIntegral size - ByteString.length bytes) 0x00
diff --git a/src/lib/Rattletrap/Utility/Crc.hs b/src/lib/Rattletrap/Utility/Crc.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Utility/Crc.hs
@@ -0,0 +1,293 @@
+module Rattletrap.Utility.Crc where
+
+import qualified Data.Array.Unboxed as Array
+import qualified Data.Bits as Bits
+import qualified Data.ByteString as ByteString
+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.
+--
+-- @
+-- compute ('Data.ByteString.Lazy.pack' [0x00])
+-- @
+--
+-- This CRC uses an initial value of @0xefcbf201@ and a polynomial of
+-- @0x04c11db7@.
+compute :: ByteString.ByteString -> Word.Word32
+compute = Bits.complement . ByteString.foldl' update initial
+
+update :: Word.Word32 -> Word.Word8 -> Word.Word32
+update crc byte =
+  let index = Bits.xor byte . unsafeWord32ToWord8 $ Bits.shiftR crc 24
+      left = table Array.! index
+      right = Bits.shiftL crc 8
+   in Bits.xor left right
+
+unsafeWord32ToWord8 :: Word.Word32 -> Word.Word8
+unsafeWord32ToWord8 = fromIntegral
+
+initial :: Word.Word32
+initial = Bits.complement 0xefcbf201
+
+table :: Array.Array Word.Word8 Word.Word32
+table =
+  Array.listArray
+    (0, 255)
+    [ 0x00000000,
+      0x04c11db7,
+      0x09823b6e,
+      0x0d4326d9,
+      0x130476dc,
+      0x17c56b6b,
+      0x1a864db2,
+      0x1e475005,
+      0x2608edb8,
+      0x22c9f00f,
+      0x2f8ad6d6,
+      0x2b4bcb61,
+      0x350c9b64,
+      0x31cd86d3,
+      0x3c8ea00a,
+      0x384fbdbd,
+      0x4c11db70,
+      0x48d0c6c7,
+      0x4593e01e,
+      0x4152fda9,
+      0x5f15adac,
+      0x5bd4b01b,
+      0x569796c2,
+      0x52568b75,
+      0x6a1936c8,
+      0x6ed82b7f,
+      0x639b0da6,
+      0x675a1011,
+      0x791d4014,
+      0x7ddc5da3,
+      0x709f7b7a,
+      0x745e66cd,
+      0x9823b6e0,
+      0x9ce2ab57,
+      0x91a18d8e,
+      0x95609039,
+      0x8b27c03c,
+      0x8fe6dd8b,
+      0x82a5fb52,
+      0x8664e6e5,
+      0xbe2b5b58,
+      0xbaea46ef,
+      0xb7a96036,
+      0xb3687d81,
+      0xad2f2d84,
+      0xa9ee3033,
+      0xa4ad16ea,
+      0xa06c0b5d,
+      0xd4326d90,
+      0xd0f37027,
+      0xddb056fe,
+      0xd9714b49,
+      0xc7361b4c,
+      0xc3f706fb,
+      0xceb42022,
+      0xca753d95,
+      0xf23a8028,
+      0xf6fb9d9f,
+      0xfbb8bb46,
+      0xff79a6f1,
+      0xe13ef6f4,
+      0xe5ffeb43,
+      0xe8bccd9a,
+      0xec7dd02d,
+      0x34867077,
+      0x30476dc0,
+      0x3d044b19,
+      0x39c556ae,
+      0x278206ab,
+      0x23431b1c,
+      0x2e003dc5,
+      0x2ac12072,
+      0x128e9dcf,
+      0x164f8078,
+      0x1b0ca6a1,
+      0x1fcdbb16,
+      0x018aeb13,
+      0x054bf6a4,
+      0x0808d07d,
+      0x0cc9cdca,
+      0x7897ab07,
+      0x7c56b6b0,
+      0x71159069,
+      0x75d48dde,
+      0x6b93dddb,
+      0x6f52c06c,
+      0x6211e6b5,
+      0x66d0fb02,
+      0x5e9f46bf,
+      0x5a5e5b08,
+      0x571d7dd1,
+      0x53dc6066,
+      0x4d9b3063,
+      0x495a2dd4,
+      0x44190b0d,
+      0x40d816ba,
+      0xaca5c697,
+      0xa864db20,
+      0xa527fdf9,
+      0xa1e6e04e,
+      0xbfa1b04b,
+      0xbb60adfc,
+      0xb6238b25,
+      0xb2e29692,
+      0x8aad2b2f,
+      0x8e6c3698,
+      0x832f1041,
+      0x87ee0df6,
+      0x99a95df3,
+      0x9d684044,
+      0x902b669d,
+      0x94ea7b2a,
+      0xe0b41de7,
+      0xe4750050,
+      0xe9362689,
+      0xedf73b3e,
+      0xf3b06b3b,
+      0xf771768c,
+      0xfa325055,
+      0xfef34de2,
+      0xc6bcf05f,
+      0xc27dede8,
+      0xcf3ecb31,
+      0xcbffd686,
+      0xd5b88683,
+      0xd1799b34,
+      0xdc3abded,
+      0xd8fba05a,
+      0x690ce0ee,
+      0x6dcdfd59,
+      0x608edb80,
+      0x644fc637,
+      0x7a089632,
+      0x7ec98b85,
+      0x738aad5c,
+      0x774bb0eb,
+      0x4f040d56,
+      0x4bc510e1,
+      0x46863638,
+      0x42472b8f,
+      0x5c007b8a,
+      0x58c1663d,
+      0x558240e4,
+      0x51435d53,
+      0x251d3b9e,
+      0x21dc2629,
+      0x2c9f00f0,
+      0x285e1d47,
+      0x36194d42,
+      0x32d850f5,
+      0x3f9b762c,
+      0x3b5a6b9b,
+      0x0315d626,
+      0x07d4cb91,
+      0x0a97ed48,
+      0x0e56f0ff,
+      0x1011a0fa,
+      0x14d0bd4d,
+      0x19939b94,
+      0x1d528623,
+      0xf12f560e,
+      0xf5ee4bb9,
+      0xf8ad6d60,
+      0xfc6c70d7,
+      0xe22b20d2,
+      0xe6ea3d65,
+      0xeba91bbc,
+      0xef68060b,
+      0xd727bbb6,
+      0xd3e6a601,
+      0xdea580d8,
+      0xda649d6f,
+      0xc423cd6a,
+      0xc0e2d0dd,
+      0xcda1f604,
+      0xc960ebb3,
+      0xbd3e8d7e,
+      0xb9ff90c9,
+      0xb4bcb610,
+      0xb07daba7,
+      0xae3afba2,
+      0xaafbe615,
+      0xa7b8c0cc,
+      0xa379dd7b,
+      0x9b3660c6,
+      0x9ff77d71,
+      0x92b45ba8,
+      0x9675461f,
+      0x8832161a,
+      0x8cf30bad,
+      0x81b02d74,
+      0x857130c3,
+      0x5d8a9099,
+      0x594b8d2e,
+      0x5408abf7,
+      0x50c9b640,
+      0x4e8ee645,
+      0x4a4ffbf2,
+      0x470cdd2b,
+      0x43cdc09c,
+      0x7b827d21,
+      0x7f436096,
+      0x7200464f,
+      0x76c15bf8,
+      0x68860bfd,
+      0x6c47164a,
+      0x61043093,
+      0x65c52d24,
+      0x119b4be9,
+      0x155a565e,
+      0x18197087,
+      0x1cd86d30,
+      0x029f3d35,
+      0x065e2082,
+      0x0b1d065b,
+      0x0fdc1bec,
+      0x3793a651,
+      0x3352bbe6,
+      0x3e119d3f,
+      0x3ad08088,
+      0x2497d08d,
+      0x2056cd3a,
+      0x2d15ebe3,
+      0x29d4f654,
+      0xc5a92679,
+      0xc1683bce,
+      0xcc2b1d17,
+      0xc8ea00a0,
+      0xd6ad50a5,
+      0xd26c4d12,
+      0xdf2f6bcb,
+      0xdbee767c,
+      0xe3a1cbc1,
+      0xe760d676,
+      0xea23f0af,
+      0xeee2ed18,
+      0xf0a5bd1d,
+      0xf464a0aa,
+      0xf9278673,
+      0xfde69bc4,
+      0x89b8fd09,
+      0x8d79e0be,
+      0x803ac667,
+      0x84fbdbd0,
+      0x9abc8bd5,
+      0x9e7d9662,
+      0x933eb0bb,
+      0x97ffad0c,
+      0xafb010b1,
+      0xab710d06,
+      0xa6322bdf,
+      0xa2f33668,
+      0xbcb4666d,
+      0xb8757bda,
+      0xb5365d03,
+      0xb1f740b4
+    ]
diff --git a/src/lib/Rattletrap/Utility/Helper.hs b/src/lib/Rattletrap/Utility/Helper.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Utility/Helper.hs
@@ -0,0 +1,46 @@
+-- | This module provides helper functions for converting replays to and from
+-- both their binary format and JSON.
+module Rattletrap.Utility.Helper where
+
+import qualified Control.Exception as Exception
+import qualified Data.Bifunctor as Bifunctor
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Lazy as LazyByteString
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Exception.InvalidJson as InvalidJson
+import qualified Rattletrap.Type.Content as Content
+import qualified Rattletrap.Type.Replay as Replay
+import qualified Rattletrap.Type.Section as Section
+import qualified Rattletrap.Utility.Json as Json
+
+-- | Parses a raw replay.
+decodeReplayFile ::
+  Bool ->
+  Bool ->
+  ByteString.ByteString ->
+  Either ([String], Exception.SomeException) Replay.Replay
+decodeReplayFile fast = ByteGet.run . Replay.byteGet fast
+
+-- | Encodes a replay as JSON.
+encodeReplayJson :: Replay.Replay -> LazyByteString.ByteString
+encodeReplayJson = Json.encodePretty
+
+-- | Parses a JSON replay.
+decodeReplayJson ::
+  ByteString.ByteString ->
+  Either ([String], Exception.SomeException) Replay.Replay
+decodeReplayJson =
+  Bifunctor.first ((,) [] . Exception.toException . InvalidJson.InvalidJson)
+    . Json.decode
+
+-- | Encodes a raw replay.
+encodeReplayFile :: Bool -> Replay.Replay -> LazyByteString.ByteString
+encodeReplayFile fast replay =
+  BytePut.toLazyByteString . Replay.bytePut $
+    if fast
+      then
+        replay
+          { Replay.content = Section.create Content.bytePut Content.empty
+          }
+      else replay
diff --git a/src/lib/Rattletrap/Utility/Json.hs b/src/lib/Rattletrap/Utility/Json.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Utility/Json.hs
@@ -0,0 +1,55 @@
+{- hlint ignore "Avoid restricted extensions" -}
+{-# LANGUAGE CPP #-}
+
+module Rattletrap.Utility.Json
+  ( module Rattletrap.Utility.Json,
+    Aeson.FromJSON (parseJSON),
+    Key.Key,
+    Aeson.ToJSON (toJSON),
+    Aeson.Value,
+    Aeson.encode,
+    Aeson.object,
+    Aeson.withObject,
+    Aeson.withText,
+  )
+where
+
+import qualified Data.Aeson as Aeson
+import qualified Data.Aeson.Encode.Pretty as Aeson
+import qualified Data.Aeson.Key as Key
+import qualified Data.Aeson.Types as Aeson
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Lazy as LazyByteString
+
+keyToString :: Key.Key -> String
+keyToString = Key.toString
+
+required ::
+  (Aeson.FromJSON value) => Aeson.Object -> String -> Aeson.Parser value
+required object key = object Aeson..: Key.fromString key
+
+optional ::
+  (Aeson.FromJSON value) =>
+  Aeson.Object ->
+  String ->
+  Aeson.Parser (Maybe value)
+optional object key = object Aeson..:? Key.fromString key
+
+# if MIN_VERSION_aeson(2, 2, 0)
+pair :: (Aeson.ToJSON value, Aeson.KeyValue e p) => String -> value -> p
+# else
+pair :: (Aeson.ToJSON value, Aeson.KeyValue p) => String -> value -> p
+# endif
+pair key value = Key.fromString key Aeson..= value
+
+decode :: (Aeson.FromJSON a) => ByteString.ByteString -> Either String a
+decode = Aeson.eitherDecodeStrict'
+
+encodePretty :: (Aeson.ToJSON a) => a -> LazyByteString.ByteString
+encodePretty =
+  Aeson.encodePretty'
+    Aeson.defConfig
+      { Aeson.confCompare = compare,
+        Aeson.confIndent = Aeson.Tab,
+        Aeson.confTrailingNewline = True
+      }
diff --git a/src/lib/Rattletrap/Utility/Monad.hs b/src/lib/Rattletrap/Utility/Monad.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Utility/Monad.hs
@@ -0,0 +1,4 @@
+module Rattletrap.Utility.Monad where
+
+whenMaybe :: (Applicative m) => Bool -> m a -> m (Maybe a)
+whenMaybe p f = if p then fmap Just f else pure Nothing
diff --git a/src/lib/Rattletrap/Version.hs b/src/lib/Rattletrap/Version.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Version.hs
@@ -0,0 +1,7 @@
+module Rattletrap.Version where
+
+import qualified Data.Version as Version
+import qualified Paths_rattletrap as Package
+
+string :: String
+string = Version.showVersion Package.version
diff --git a/src/test/Main.hs b/src/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/test/Main.hs
@@ -0,0 +1,181 @@
+import qualified Control.Monad as Monad
+import qualified Data.ByteString as ByteString
+import qualified GHC.Clock as Clock
+import qualified Rattletrap
+import qualified System.FilePath as FilePath
+import qualified Text.Printf as Printf
+
+main :: IO ()
+main = do
+  let directory = "output"
+  generateSchema directory
+  mapM_ (testReplay directory) replays
+
+generateSchema :: FilePath -> IO ()
+generateSchema directory =
+  Rattletrap.rattletrap
+    ""
+    ["--schema", "--output", FilePath.combine directory "schema.json"]
+
+testReplay :: FilePath -> (FilePath, String) -> IO ()
+testReplay directory (uuid, name) = do
+  let inputFile = FilePath.combine "replays" $ uuid <> ".replay"
+  input <- ByteString.readFile inputFile
+  let mb = fromIntegral (ByteString.length input) / (1024 * 1024 :: Double)
+  Printf.printf "- replay: %s %s (%.3f mb)\n" uuid name mb
+  let jsonFile = FilePath.combine directory $ uuid <> ".json"
+  do
+    (s, ()) <- withDuration $ decode inputFile jsonFile
+    Printf.printf "  decode: %.3f s @ %.3f mb/s\n" s (mb / s)
+  let outputFile = FilePath.combine directory $ uuid <> ".replay"
+  do
+    (s, ()) <- withDuration $ encode jsonFile outputFile
+    Printf.printf "  encode: %.3f s @ %.3f mb/s\n" s (mb / s)
+  output <- ByteString.readFile outputFile
+  Monad.when (output /= input) $ fail "output does not match input"
+
+withDuration :: IO a -> IO (Double, a)
+withDuration action = do
+  before <- Clock.getMonotonicTime
+  result <- action
+  after <- Clock.getMonotonicTime
+  pure (after - before, result)
+
+decode :: FilePath -> FilePath -> IO ()
+decode input output =
+  Rattletrap.rattletrap "" ["--compact", "--input", input, "--output", output]
+
+encode :: FilePath -> FilePath -> IO ()
+encode input output =
+  Rattletrap.rattletrap "" ["--input", input, "--output", output]
+
+replays :: [(String, String)]
+replays =
+  [ ("0008", "a flip time"), -- https://github.com/tfausak/rattletrap/commit/ee7afa0
+    ("000b", "nintendo switch"), -- https://github.com/tfausak/rattletrap/pull/60
+    ("0121", "RLCS 2"), -- https://github.com/nickbabcock/boxcars/pull/120
+    ("0416", "v1.78 demolition"), -- https://github.com/tfausak/rattletrap/pull/164
+    ("07e9", "a game mode before Neo Tokyo"), -- https://github.com/tfausak/rattletrap/commit/b806f9b
+    ("0ad2", "some Latin-1 text"), -- https://github.com/tfausak/rattletrap/commit/13a8b2d
+    ("0ae9", "fte"), -- https://github.com/jjbott/RocketLeagueReplayParser/issues/50
+    ("0ca5", "with QQ remote ID"), -- https://github.com/nickbabcock/boxcars/pull/69
+    ("0e76", "v1.95 rumble"), -- https://github.com/tfausak/rattletrap/pull/237
+    ("1205", "rumble mode"), -- https://github.com/tfausak/rattletrap/commit/5256500
+    ("160c", "a dedicated server IP"), -- https://github.com/tfausak/rattletrap/commit/5c64a6d
+    ("16d5", "new property types"), -- https://github.com/tfausak/rattletrap/pull/41
+    ("18d6", "an online loadout attribute"), -- https://github.com/tfausak/rattletrap/commit/a9900e7
+    ("1a12", "overtime"), -- https://github.com/tfausak/rattletrap/commit/ada6053
+    ("1ae4", "a game time"), -- https://github.com/tfausak/rattletrap/commit/d08176e
+    ("1bc2", "no padding after the frames"), -- https://github.com/tfausak/rattletrap/commit/c9a2dd8
+    ("1d1d", "a camera pitch"), -- https://github.com/tfausak/rattletrap/commit/ee7afa0
+    ("1d25", "bumblebee"), -- https://github.com/tfausak/rattletrap/issues/280
+    ("1ec9", "a V1.63 match"), -- https://github.com/tfausak/rattletrap/pull/132
+    ("1ef9", "a private hoops match"), -- https://github.com/tfausak/rattletrap/commit/5570839
+    ("1f37", "splitscreen players"), -- https://github.com/tfausak/rattletrap/commit/c4d2f32
+    ("2066", "replicated boost"), -- https://github.com/tfausak/rattletrap/issues/296
+    ("2114", "a match save"), -- https://github.com/tfausak/rattletrap/commit/ee7afa0
+    ("21a8", "v1.66"), -- https://github.com/tfausak/rattletrap/pull/142
+    ("2266", "dropshot"), -- https://github.com/tfausak/rattletrap/pull/36
+    ("22ba", "a vote to forfeit"), -- https://github.com/tfausak/rattletrap/commit/86656bb
+    ("27b6", "some UTF-16 text"), -- https://github.com/tfausak/rattletrap/commit/c4d2f32
+    ("29f5", "frames"), -- https://github.com/tfausak/rattletrap/commit/bf4b6af
+    ("2cf8", "demo v2.01"), -- https://github.com/tfausak/rattletrap/pull/243
+    ("2cfe", "a new playstation id"), -- https://github.com/tfausak/rattletrap/issues/51
+    ("3381", "patch 1.37"), -- https://github.com/tfausak/rattletrap/pull/48
+    ("35f6", "heatseeker v2.01"), -- https://github.com/tfausak/rattletrap/commit/57d86c2
+    ("372d", "a camera yaw attribute"), -- https://github.com/tfausak/rattletrap/commit/9c1516c
+    ("383e", "older unknown content field"), -- https://github.com/tfausak/rattletrap/pull/123
+    ("387f", "a frozen attribute"), -- https://github.com/tfausak/rattletrap/commit/93ce196
+    ("3abd", "rlcs"), -- https://github.com/tfausak/rattletrap/pull/86
+    ("3bd0", "v2.45"), -- https://github.com/tfausak/rattletrap/issues/311
+    ("3ea1", "a custom team name"), -- https://github.com/tfausak/rattletrap/commit/cf4d145
+    ("4050", "v2.08 dodge impulse"), -- https://github.com/tfausak/rattletrap/issues/247
+    ("4126", "a game mode after Neo Tokyo"), -- https://github.com/tfausak/rattletrap/commit/a1cf21e
+    ("419a", "a club match"), -- https://github.com/tfausak/rattletrap/commit/8e35043
+    ("42f0", "reservations after Neo Tokyo"), -- https://github.com/tfausak/rattletrap/commit/163684f
+    ("42f2", "anniversary ball"), -- https://github.com/tfausak/rattletrap/issues/147
+    ("43a9", "tutorial"), -- https://github.com/nickbabcock/boxcars/pull/70
+    ("4bc3", "with timed out attribute"), -- https://github.com/tfausak/rattletrap/pull/98
+    ("504e", "some messages"), -- https://github.com/tfausak/rattletrap/commit/1d4a538
+    ("5123", "rep stat title"), -- https://github.com/nickbabcock/boxcars/pull/78
+    ("520e", "no pickup attribute"), -- https://github.com/tfausak/rattletrap/pull/38
+    ("524f", "quat edge case"), -- https://github.com/tfausak/rattletrap/pull/87
+    ("52aa", "a match-ending attribute"), -- https://github.com/tfausak/rattletrap/commit/5c64a6d
+    ("540d", "a demolish attribute"), -- https://github.com/tfausak/rattletrap/commit/65ce033
+    ("54ae", "replicated car scale"), -- https://github.com/nickbabcock/boxcars/pull/79
+    ("551c", "private match settings"), -- https://github.com/tfausak/rattletrap/commit/5c9ebfc
+    ("59d3", "v2.23"), -- https://github.com/tfausak/rattletrap/issues/274
+    ("5a06", "esports items"), -- https://github.com/tfausak/rattletrap/pull/114
+    ("5e0b", "max channels"), -- https://github.com/tfausak/rattletrap/issues/254
+    ("6210", "different player history key"), -- https://github.com/tfausak/rattletrap/pull/63
+    ("6320", "a forfeit attribute"), -- https://github.com/tfausak/rattletrap/pull/20
+    ("6688", "a malformed byte property"), -- https://github.com/tfausak/rattletrap/commit/b1ec18b
+    ("6b0d", "patch 1.37"), -- https://github.com/tfausak/rattletrap/pull/48
+    ("6d1b", "a flip right"), -- https://github.com/tfausak/rattletrap/commit/ee7afa0
+    ("6f7c", "a map with numbers"), -- https://github.com/tfausak/rattletrap/commit/2629511
+    ("7083", "weird basketball capitalization"), -- https://github.com/tfausak/rattletrap/pull/63
+    ("7109", "a boost modifier"), -- https://github.com/tfausak/rattletrap/commit/ee7afa0
+    ("717a", "v2.43"), -- https://github.com/tfausak/rattletrap/issues/307
+    ("7256", "special edition"), -- https://github.com/tfausak/rattletrap/pull/103
+    ("7588", "another malformed byte property"), -- https://github.com/nickbabcock/boxcars/pull/68
+    ("75ce", "primary and secondary titles"), -- https://github.com/tfausak/rattletrap/pull/69
+    ("79ae", "voice update replay"), -- https://github.com/tfausak/rattletrap/pull/265
+    ("7bf6", "an online loadouts attribute"), -- https://github.com/tfausak/rattletrap/commit/89d02f7
+    ("81d1", "gridiron"), -- https://github.com/tfausak/rattletrap/pull/180
+    ("83aa", "v2.46 replicated demolish extended"), -- https://github.com/tfausak/rattletrap/issues/316
+    ("89cb", "remote user data"), -- https://github.com/tfausak/rattletrap/commit/163684f
+    ("8ae5", "new painted items"), -- https://github.com/tfausak/rattletrap/pull/43
+    ("92a6", "with server performance state"), -- https://github.com/tfausak/rattletrap/pull/93
+    ("946f", "patch 1.43"), -- https://github.com/tfausak/rattletrap/pull/69
+    ("9704", "a batarang"), -- https://github.com/tfausak/rattletrap/commit/5958e5c
+    ("98e5", "a player using behind view"), -- https://github.com/tfausak/rattletrap/commit/163684f
+    ("9a2c", "ghost hunt"), -- https://github.com/tfausak/rattletrap/pull/160
+    ("9e35", "spike rush"), -- https://github.com/tfausak/rattletrap/pull/160
+    ("9eaa", "newer replay without trailing bytes"), -- https://github.com/tfausak/rattletrap/pull/126
+    ("a09e", "a tournament"), -- https://github.com/tfausak/rattletrap/pull/69
+    ("a128", "a round count down"), -- https://github.com/tfausak/rattletrap/commit/8bb778d
+    ("a184", "max score"), -- https://github.com/tfausak/rattletrap/pull/158
+    ("a1c0", "epic system id"), -- https://github.com/tfausak/rattletrap/pull/167
+    ("a52f", "some more mutators"), -- https://github.com/tfausak/rattletrap/commit/ee7afa0
+    ("a558", "extended explosion data"), -- https://github.com/tfausak/rattletrap/pull/44
+    ("a671", "a waiting player"), -- https://github.com/tfausak/rattletrap/commit/163684f
+    ("a676", "new user color"), -- https://github.com/tfausak/rattletrap/pull/93
+    ("a7f0", "a ready attribute"), -- https://github.com/tfausak/rattletrap/commit/78af1fd
+    ("a9df", "salty shores patch 1.45"), -- https://github.com/tfausak/rattletrap/pull/78
+    ("aa70", "patch 1.50 - TitleID attribute"), -- https://github.com/tfausak/rattletrap/pull/93
+    ("ae46", "mvp"), -- https://github.com/nickbabcock/boxcars/pull/80
+    ("afb1", "patch 1.37"), -- https://github.com/tfausak/rattletrap/pull/48
+    ("b9f9", "a party leader"), -- https://github.com/tfausak/rattletrap/commit/bba2cfd
+    ("c14f", "some mutators"), -- https://github.com/tfausak/rattletrap/commit/bba2cfd
+    ("c23b", "new psynet id"), -- https://github.com/tfausak/rattletrap/pull/118
+    ("c62c", "more boolean attributes"), -- https://github.com/nickbabcock/boxcars/pull/77
+    ("c837", "a spectator"), -- https://github.com/tfausak/rattletrap/commit/bba2cfd
+    ("cc4c", "after Starbase ARC"), -- https://github.com/tfausak/rattletrap/pull/20
+    ("d044", "hoops mutators"), -- https://github.com/tfausak/rattletrap/pull/34
+    ("d1d5", "v1.68"), -- https://github.com/tfausak/rattletrap/pull/146
+    ("d236", "rlcs s2"), -- https://github.com/tfausak/rattletrap/pull/88
+    ("d428", "a private hockey match"), -- https://github.com/tfausak/rattletrap/commit/4c104b2
+    ("d44c", "ranked tournament"), -- https://github.com/tfausak/rattletrap/pull/167
+    ("d52e", "psynet system id"), -- https://github.com/tfausak/rattletrap/pull/99
+    ("d5d6", "health max"), -- https://github.com/nickbabcock/boxcars/pull/80
+    ("d7fb", "an explosion attribute"), -- https://github.com/tfausak/rattletrap/commit/c554e3e
+    ("d818", "heatseeker"), -- https://github.com/tfausak/rattletrap/pull/160
+    ("db70", "new lag indicator"), -- https://github.com/tfausak/rattletrap/pull/69
+    ("dcab", "weird ball attribute value"), -- https://github.com/tfausak/rattletrap/issues/149
+    ("dcb3", "a pawn type attribute"), -- https://github.com/tfausak/rattletrap/commit/7d7f438
+    ("dd14", "v1.88"), -- https://github.com/tfausak/rattletrap/pull/170
+    ("de56", "a problematic product attribute"), -- https://github.com/tfausak/rattletrap/issues/51
+    ("e2f9", "bTearOff"), -- https://github.com/nickbabcock/boxcars/pull/76
+    ("e80d", "unlimited time"), -- https://github.com/tfausak/rattletrap/pull/76
+    ("e978", "distracted"), -- https://github.com/tfausak/rattletrap/issues/156
+    ("eae3", "an actor/object ID collision"), -- https://github.com/tfausak/rattletrap/commit/d8fad06
+    ("eae8", "custom team colors"), -- https://github.com/tfausak/rattletrap/commit/809240f
+    ("ecd5", "new match guid attribute"), -- https://github.com/tfausak/rattletrap/issues/270
+    ("edbb", "remote role"), -- https://github.com/tfausak/rattletrap/pull/106
+    ("f08e", "post game lobby"), -- https://github.com/jjbott/RocketLeagueReplayParser/issues/46
+    ("f299", "a location attribute"), -- https://github.com/tfausak/rattletrap/commit/21b09c5
+    ("f7b9", "a hockey game event"), -- https://github.com/tfausak/rattletrap/commit/3e16d7f
+    ("f811", "no frames"), -- https://github.com/tfausak/rattletrap/commit/bf4b6af
+    ("fdc7", "an MVP"), -- https://github.com/tfausak/rattletrap/commit/65019d2
+    ("ffb7", "with difficulty") -- https://github.com/tfausak/rattletrap/pull/167
+  ]
diff --git a/stack.yaml b/stack.yaml
deleted file mode 100644
--- a/stack.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-resolver: nightly-2017-11-25
-
-packages:
-  - .
-  - tools
diff --git a/tests/Main.hs b/tests/Main.hs
deleted file mode 100644
--- a/tests/Main.hs
+++ /dev/null
@@ -1,106 +0,0 @@
-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
-
-main :: IO ()
-main = Hspec.hspec spec
-
-spec :: Hspec.Spec
-spec = Hspec.describe "Rattletrap" (mapM_ (uncurry itCanRoundTrip) replays)
-
-itCanRoundTrip :: String -> String -> Hspec.Spec
-itCanRoundTrip uuid description =
-  Hspec.it (specName uuid description) (specBody uuid)
-
-specName :: String -> String -> String
-specName uuid description = unwords [take 4 uuid, description]
-
-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")
-    )
-
-pathToReplay :: String -> FilePath
-pathToReplay uuid =
-  FilePath.joinPath ["replays", FilePath.addExtension uuid ".replay"]
-
-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")
-  ]
