diff --git a/HLint.hs b/HLint.hs
new file mode 100644
--- /dev/null
+++ b/HLint.hs
@@ -0,0 +1,10 @@
+module HLint
+  (
+  ) where
+
+import "hint" HLint.Default
+import "hint" HLint.Dollar
+
+ignore "Use &&&"
+
+ignore "Use <|>"
diff --git a/LICENSE.markdown b/LICENSE.markdown
new file mode 100644
--- /dev/null
+++ b/LICENSE.markdown
@@ -0,0 +1,23 @@
+# [The MIT License (MIT)][]
+
+Copyright (c) 2016 Taylor Fausak <taylor@fausak.me>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+[The MIT License (MIT)]: http://opensource.org/licenses/MIT
diff --git a/README.markdown b/README.markdown
new file mode 100644
--- /dev/null
+++ b/README.markdown
@@ -0,0 +1,66 @@
+# [Rattletrap][]
+
+[![Windows build badge][]][windows build]
+[![Build badge][]][build]
+
+Rattletrap parses and generates [Rocket League][] replays.
+
+## Install
+
+Rattletrap does not yet offer compiled binaries for download. To install it,
+first install [Stack][]. Then download or clone this repository and run
+`stack --install-ghc install`.
+
+## Parse
+
+Rattletrap can parse (decode) Rocket League replays and output them as JSON.
+
+``` sh
+> rattletrap decode input.replay output.json
+# or
+> rattletrap decode input.replay > output.json
+# or
+> rattletrap.decode < input.replay > output.json
+```
+
+The resulting JSON is minified, but extremely large. A 4.3 MB replay file turns
+into a 123 MB JSON file. To easily view the file, use a JSON pretty printer and
+a pager. For example:
+
+``` sh
+> cat output.json | python -m json.tool | less
+```
+
+## Generate
+
+Rattletrap can also generate (encode) Rocket League replays from JSON files.
+
+``` sh
+> rattletrap encode input.json output.replay
+# or
+> rattletrap encode input.json > output.replay
+# or
+> rattletrap.encode < input.json > output.replay
+```
+
+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
+[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
+[Stack]: https://docs.haskellstack.org/en/stable/README/
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,8 @@
+module Main
+  ( main
+  ) where
+
+import qualified Distribution.Simple
+
+main :: IO ()
+main = Distribution.Simple.defaultMain
diff --git a/executable/Main.hs b/executable/Main.hs
new file mode 100644
--- /dev/null
+++ b/executable/Main.hs
@@ -0,0 +1,121 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Main
+  ( main
+  ) where
+
+import Rattletrap
+
+import qualified Control.Monad as Monad
+import qualified Data.Aeson as Aeson
+import qualified Data.Aeson.Casing as Casing
+import qualified Data.Aeson.TH 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 Language.Haskell.TH as TH
+import qualified System.Environment as Environment
+
+main :: IO ()
+main = do
+  args <- Environment.getArgs
+  mainWithArgs args
+
+mainWithArgs :: [String] -> IO ()
+mainWithArgs args =
+  case args of
+    "decode":files -> do
+      (getInput, putOutput) <- getIO files
+      input <- getInput
+      let replay = Binary.runGet getReplay input
+      let output = Aeson.encode replay
+      putOutput output
+    "encode":files -> do
+      (getInput, putOutput) <- getIO files
+      input <- getInput
+      case Aeson.eitherDecode input of
+        Left message -> fail ("could not parse JSON: " ++ message)
+        Right replay -> do
+          let output = Binary.runPut (putReplay replay)
+          putOutput output
+    _ -> fail ("unexpected arguments " ++ show args)
+
+getIO
+  :: Monad m
+  => [FilePath] -> m (IO ByteString.ByteString, ByteString.ByteString -> IO ())
+getIO files =
+  case files of
+    [] -> pure (ByteString.getContents, ByteString.putStr)
+    [i] -> pure (ByteString.readFile i, ByteString.putStr)
+    [i, o] -> pure (ByteString.readFile i, ByteString.writeFile o)
+    _ -> fail ("unexpected arguments " ++ show files)
+
+$(Monad.foldM
+    (\declarations name -> do
+       let options =
+             Casing.aesonDrop (length (TH.nameBase name)) Casing.snakeCase
+       newDeclarations <- Aeson.deriveJSON options name
+       pure (newDeclarations ++ declarations))
+    []
+    [ ''Attribute
+    , ''AttributeMapping
+    , ''AttributeValue
+    , ''BooleanAttributeValue
+    , ''ByteAttributeValue
+    , ''Cache
+    , ''CamSettingsAttributeValue
+    , ''ClassMapping
+    , ''CompressedWord
+    , ''CompressedWordVector
+    , ''Content
+    , ''DemolishAttributeValue
+    , ''DestroyedReplicationValue
+    , ''Dictionary
+    , ''EnumAttributeValue
+    , ''ExplosionAttributeValue
+    , ''FlaggedIntAttributeValue
+    , ''Float32
+    , ''FloatAttributeValue
+    , ''Frame
+    , ''GameModeAttributeValue
+    , ''Header
+    , ''Initialization
+    , ''Int32
+    , ''Int8
+    , ''Int8Vector
+    , ''IntAttributeValue
+    , ''KeyFrame
+    , ''List
+    , ''LoadoutAttributeValue
+    , ''LoadoutOnlineAttributeValue
+    , ''LoadoutsAttributeValue
+    , ''LoadoutsOnlineAttributeValue
+    , ''LocationAttributeValue
+    , ''Mark
+    , ''Message
+    , ''MusicStingerAttributeValue
+    , ''PartyLeaderAttributeValue
+    , ''PickupAttributeValue
+    , ''PrivateMatchSettingsAttributeValue
+    , ''Property
+    , ''PropertyValue
+    , ''QWordAttributeValue
+    , ''RemoteId
+    , ''Replay
+    , ''Replication
+    , ''ReplicationValue
+    , ''ReservationAttributeValue
+    , ''RigidBodyStateAttributeValue
+    , ''SpawnedReplicationValue
+    , ''StringAttributeValue
+    , ''TeamPaintAttributeValue
+    , ''Text
+    , ''UniqueIdAttributeValue
+    , ''UpdatedReplicationValue
+    , ''Vector
+    , ''WeldedInfoAttributeValue
+    , ''Word32
+    , ''Word64
+    , ''Word8
+    ])
diff --git a/library/Rattletrap.hs b/library/Rattletrap.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap.hs
@@ -0,0 +1,41 @@
+module Rattletrap
+  ( module Export
+  ) where
+
+import Rattletrap.ActorMap as Export
+import Rattletrap.Attribute as Export
+import Rattletrap.AttributeMapping as Export
+import Rattletrap.AttributeValue as Export
+import Rattletrap.AttributeValueType as Export
+import Rattletrap.Cache as Export
+import Rattletrap.ClassAttributeMap as Export
+import Rattletrap.ClassMapping as Export
+import Rattletrap.CompressedWord as Export
+import Rattletrap.CompressedWordVector as Export
+import Rattletrap.Content as Export
+import Rattletrap.Crc as Export
+import Rattletrap.Data as Export
+import Rattletrap.Dictionary as Export
+import Rattletrap.Float32 as Export
+import Rattletrap.Frame as Export
+import Rattletrap.Header as Export
+import Rattletrap.Initialization as Export
+import Rattletrap.Int32 as Export
+import Rattletrap.Int8 as Export
+import Rattletrap.Int8Vector as Export
+import Rattletrap.KeyFrame as Export
+import Rattletrap.List as Export
+import Rattletrap.Mark as Export
+import Rattletrap.Message as Export
+import Rattletrap.Property as Export
+import Rattletrap.PropertyValue as Export
+import Rattletrap.RemoteId as Export
+import Rattletrap.Replay as Export
+import Rattletrap.Replication as Export
+import Rattletrap.ReplicationValue as Export
+import Rattletrap.Text as Export
+import Rattletrap.Utility as Export
+import Rattletrap.Vector as Export
+import Rattletrap.Word32 as Export
+import Rattletrap.Word64 as Export
+import Rattletrap.Word8 as Export
diff --git a/library/Rattletrap/ActorMap.hs b/library/Rattletrap/ActorMap.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/ActorMap.hs
@@ -0,0 +1,14 @@
+module Rattletrap.ActorMap where
+
+import Rattletrap.CompressedWord
+import Rattletrap.Word32
+
+import qualified Data.Map as Map
+
+type ActorMap = Map.Map CompressedWord Word32
+
+makeActorMap :: ActorMap
+makeActorMap = Map.empty
+
+updateActorMap :: CompressedWord -> Word32 -> ActorMap -> ActorMap
+updateActorMap = Map.insert
diff --git a/library/Rattletrap/Attribute.hs b/library/Rattletrap/Attribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Attribute.hs
@@ -0,0 +1,57 @@
+module Rattletrap.Attribute where
+
+import Rattletrap.ActorMap
+import Rattletrap.AttributeValue
+import Rattletrap.ClassAttributeMap
+import Rattletrap.CompressedWord
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data Attribute = Attribute
+  { attributeId :: CompressedWord
+  , attributeValue :: AttributeValue
+  } deriving (Eq, Ord, Show)
+
+getAttributes
+  :: (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)
+  -> ClassAttributeMap
+  -> ActorMap
+  -> CompressedWord
+  -> BinaryBit.BitGet Attribute
+getAttribute version classAttributeMap actorMap actorId =
+  case getAttributeIdLimit classAttributeMap actorMap actorId of
+    Nothing -> fail ("could not get attribute ID limit for " ++ show actorId)
+    Just limit -> do
+      id_ <- getCompressedWord limit
+      case getAttributeName classAttributeMap actorMap actorId id_ of
+        Nothing -> fail ("could not get attribute name for " ++ show id_)
+        Just name -> do
+          value <- getAttributeValue version name
+          pure Attribute {attributeId = id_, attributeValue = value}
+
+putAttribute :: Attribute -> BinaryBit.BitPut ()
+putAttribute attribute = do
+  BinaryBit.putBool True
+  putCompressedWord (attributeId attribute)
+  putAttributeValue (attributeValue attribute)
diff --git a/library/Rattletrap/AttributeMapping.hs b/library/Rattletrap/AttributeMapping.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeMapping.hs
@@ -0,0 +1,23 @@
+module Rattletrap.AttributeMapping where
+
+import Rattletrap.Word32
+
+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
+    {attributeMappingObjectId = objectId, attributeMappingStreamId = streamId}
+
+putAttributeMapping :: AttributeMapping -> Binary.Put
+putAttributeMapping attributeMapping = do
+  putWord32 (attributeMappingObjectId attributeMapping)
+  putWord32 (attributeMappingStreamId attributeMapping)
diff --git a/library/Rattletrap/AttributeValue.hs b/library/Rattletrap/AttributeValue.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue.hs
@@ -0,0 +1,187 @@
+module Rattletrap.AttributeValue
+  ( module Rattletrap.AttributeValue
+  , module Export
+  ) where
+
+import Rattletrap.AttributeValue.Boolean as Export
+import Rattletrap.AttributeValue.Byte as Export
+import Rattletrap.AttributeValue.CamSettings as Export
+import Rattletrap.AttributeValue.Demolish as Export
+import Rattletrap.AttributeValue.Enum as Export
+import Rattletrap.AttributeValue.Explosion as Export
+import Rattletrap.AttributeValue.FlaggedInt as Export
+import Rattletrap.AttributeValue.Float as Export
+import Rattletrap.AttributeValue.GameMode as Export
+import Rattletrap.AttributeValue.Int as Export
+import Rattletrap.AttributeValue.Loadout as Export
+import Rattletrap.AttributeValue.LoadoutOnline as Export
+import Rattletrap.AttributeValue.Loadouts as Export
+import Rattletrap.AttributeValue.LoadoutsOnline as Export
+import Rattletrap.AttributeValue.Location as Export
+import Rattletrap.AttributeValue.MusicStinger as Export
+import Rattletrap.AttributeValue.PartyLeader as Export
+import Rattletrap.AttributeValue.Pickup as Export
+import Rattletrap.AttributeValue.PrivateMatchSettings as Export
+import Rattletrap.AttributeValue.QWord as Export
+import Rattletrap.AttributeValue.Reservation as Export
+import Rattletrap.AttributeValue.RigidBodyState as Export
+import Rattletrap.AttributeValue.String as Export
+import Rattletrap.AttributeValue.TeamPaint as Export
+import Rattletrap.AttributeValue.UniqueId as Export
+import Rattletrap.AttributeValue.WeldedInfo as Export
+
+import Rattletrap.AttributeValueType
+import Rattletrap.Data
+import Rattletrap.Text
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Map as Map
+
+data AttributeValue
+  = BooleanAttribute BooleanAttributeValue
+  | ByteAttribute ByteAttributeValue
+  | CamSettingsAttribute CamSettingsAttributeValue
+  | DemolishAttribute DemolishAttributeValue
+  | EnumAttribute EnumAttributeValue
+  | ExplosionAttribute ExplosionAttributeValue
+  | FlaggedIntAttribute FlaggedIntAttributeValue
+  | FloatAttribute FloatAttributeValue
+  | GameModeAttribute GameModeAttributeValue
+  | IntAttribute IntAttributeValue
+  | LoadoutAttribute LoadoutAttributeValue
+  | LoadoutOnlineAttribute LoadoutOnlineAttributeValue
+  | LoadoutsAttribute LoadoutsAttributeValue
+  | LoadoutsOnlineAttribute LoadoutsOnlineAttributeValue
+  | LocationAttribute LocationAttributeValue
+  | MusicStingerAttribute MusicStingerAttributeValue
+  | PartyLeaderAttribute PartyLeaderAttributeValue
+  | PickupAttribute PickupAttributeValue
+  | PrivateMatchSettingsAttribute PrivateMatchSettingsAttributeValue
+  | QWordAttribute QWordAttributeValue
+  | ReservationAttribute ReservationAttributeValue
+  | RigidBodyStateAttribute RigidBodyStateAttributeValue
+  | StringAttribute StringAttributeValue
+  | TeamPaintAttribute TeamPaintAttributeValue
+  | UniqueIdAttribute UniqueIdAttributeValue
+  | WeldedInfoAttribute WeldedInfoAttributeValue
+  deriving (Eq, Ord, Show)
+
+getAttributeValue :: (Int, Int) -> Text -> BinaryBit.BitGet AttributeValue
+getAttributeValue version name =
+  case Map.lookup name attributeValueTypes of
+    Just constructor ->
+      case constructor of
+        AVBoolean -> do
+          x <- getBooleanAttributeValue
+          pure (BooleanAttribute x)
+        AVByte -> do
+          x <- getByteAttributeValue
+          pure (ByteAttribute x)
+        AVCamSettings -> do
+          x <- getCamSettingsAttributeValue
+          pure (CamSettingsAttribute x)
+        AVDemolish -> do
+          x <- getDemolishAttributeValue
+          pure (DemolishAttribute x)
+        AVEnum -> do
+          x <- getEnumAttributeValue
+          pure (EnumAttribute x)
+        AVExplosion -> do
+          x <- getExplosionAttributeValue
+          pure (ExplosionAttribute x)
+        AVFlaggedInt -> do
+          x <- getFlaggedIntAttributeValue
+          pure (FlaggedIntAttribute x)
+        AVFloat -> do
+          x <- getFloatAttributeValue
+          pure (FloatAttribute x)
+        AVGameMode -> do
+          x <- getGameModeAttributeValue version
+          pure (GameModeAttribute x)
+        AVInt -> do
+          x <- getIntAttributeValue
+          pure (IntAttribute x)
+        AVLoadout -> do
+          x <- getLoadoutAttributeValue
+          pure (LoadoutAttribute x)
+        AVLoadoutOnline -> do
+          x <- getLoadoutOnlineAttributeValue
+          pure (LoadoutOnlineAttribute x)
+        AVLoadouts -> do
+          x <- getLoadoutsAttributeValue
+          pure (LoadoutsAttribute x)
+        AVLoadoutsOnline -> do
+          x <- getLoadoutsOnlineAttributeValue
+          pure (LoadoutsOnlineAttribute x)
+        AVLocation -> do
+          x <- getLocationAttributeValue
+          pure (LocationAttribute x)
+        AVMusicStinger -> do
+          x <- getMusicStingerAttributeValue
+          pure (MusicStingerAttribute x)
+        AVPartyLeader -> do
+          x <- getPartyLeaderAttributeValue
+          pure (PartyLeaderAttribute x)
+        AVPickup -> do
+          x <- getPickupAttributeValue
+          pure (PickupAttribute x)
+        AVPrivateMatchSettings -> do
+          x <- getPrivateMatchSettingsAttributeValue
+          pure (PrivateMatchSettingsAttribute x)
+        AVQWord -> do
+          x <- getQWordAttributeValue
+          pure (QWordAttribute x)
+        AVReservation -> do
+          x <- getReservationAttributeValue version
+          pure (ReservationAttribute x)
+        AVRigidBodyState -> do
+          x <- getRigidBodyStateAttributeValue
+          pure (RigidBodyStateAttribute x)
+        AVString -> do
+          x <- getStringAttributeValue
+          pure (StringAttribute x)
+        AVTeamPaint -> do
+          x <- getTeamPaintAttributeValue
+          pure (TeamPaintAttribute x)
+        AVUniqueId -> do
+          x <- getUniqueIdAttributeValue
+          pure (UniqueIdAttribute x)
+        AVWeldedInfo -> do
+          x <- getWeldedInfoAttributeValue
+          pure (WeldedInfoAttribute x)
+    Nothing -> fail ("don't know how to get attribute value " ++ show name)
+
+attributeValueTypes :: Map.Map Text AttributeValueType
+attributeValueTypes =
+  Map.mapKeys stringToText (Map.fromList rawAttributeValueTypes)
+
+putAttributeValue :: AttributeValue -> BinaryBit.BitPut ()
+putAttributeValue value =
+  case value of
+    BooleanAttribute x -> putBooleanAttributeValue x
+    ByteAttribute x -> putByteAttributeValue x
+    CamSettingsAttribute x -> putCamSettingsAttributeValue x
+    DemolishAttribute x -> putDemolishAttributeValue x
+    EnumAttribute x -> putEnumAttributeValue x
+    ExplosionAttribute x -> putExplosionAttributeValue x
+    FlaggedIntAttribute x -> putFlaggedIntAttributeValue x
+    FloatAttribute x -> putFloatAttributeValue x
+    GameModeAttribute x -> putGameModeAttributeValue x
+    IntAttribute x -> putIntAttributeValue x
+    LoadoutAttribute x -> putLoadoutAttributeValue x
+    LoadoutOnlineAttribute x -> putLoadoutOnlineAttributeValue x
+    LoadoutsAttribute x -> putLoadoutsAttributeValue x
+    LoadoutsOnlineAttribute x -> putLoadoutsOnlineAttributeValue x
+    LocationAttribute x -> putLocationAttributeValue x
+    MusicStingerAttribute x -> putMusicStingerAttributeValue x
+    PartyLeaderAttribute x -> putPartyLeaderAttributeValue x
+    PickupAttribute x -> putPickupAttributeValue x
+    PrivateMatchSettingsAttribute x -> putPrivateMatchSettingsAttributeValue x
+    QWordAttribute x -> putQWordAttributeValue x
+    ReservationAttribute x -> putReservationAttributeValue x
+    RigidBodyStateAttribute x -> putRigidBodyStateAttributeValue x
+    StringAttribute x -> putStringAttributeValue x
+    TeamPaintAttribute x -> putTeamPaintAttributeValue x
+    UniqueIdAttribute x -> putUniqueIdAttributeValue x
+    WeldedInfoAttribute x -> putWeldedInfoAttributeValue x
diff --git a/library/Rattletrap/AttributeValue/Boolean.hs b/library/Rattletrap/AttributeValue/Boolean.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/Boolean.hs
@@ -0,0 +1,17 @@
+module Rattletrap.AttributeValue.Boolean where
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype BooleanAttributeValue = BooleanAttributeValue
+  { booleanAttributeValueValue :: Bool
+  } deriving (Eq, Ord, Show)
+
+getBooleanAttributeValue :: BinaryBit.BitGet BooleanAttributeValue
+getBooleanAttributeValue = do
+  value <- BinaryBit.getBool
+  pure (BooleanAttributeValue value)
+
+putBooleanAttributeValue :: BooleanAttributeValue -> BinaryBit.BitPut ()
+putBooleanAttributeValue booleanAttributeValue =
+  BinaryBit.putBool (booleanAttributeValueValue booleanAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/Byte.hs b/library/Rattletrap/AttributeValue/Byte.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/Byte.hs
@@ -0,0 +1,19 @@
+module Rattletrap.AttributeValue.Byte where
+
+import Rattletrap.Word8
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype ByteAttributeValue = ByteAttributeValue
+  { byteAttributeValueValue :: Word8
+  } deriving (Eq, Ord, Show)
+
+getByteAttributeValue :: BinaryBit.BitGet ByteAttributeValue
+getByteAttributeValue = do
+  value <- getWord8Bits
+  pure (ByteAttributeValue value)
+
+putByteAttributeValue :: ByteAttributeValue -> BinaryBit.BitPut ()
+putByteAttributeValue byteAttributeValue =
+  putWord8Bits (byteAttributeValueValue byteAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/CamSettings.hs b/library/Rattletrap/AttributeValue/CamSettings.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/CamSettings.hs
@@ -0,0 +1,36 @@
+module Rattletrap.AttributeValue.CamSettings where
+
+import Rattletrap.Float32
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data CamSettingsAttributeValue = CamSettingsAttributeValue
+  { camSettingsAttributeValueFov :: Float32
+  , camSettingsAttributeValueHeight :: Float32
+  , camSettingsAttributeValueAngle :: Float32
+  , camSettingsAttributeValueDistance :: Float32
+  , camSettingsAttributeValueStiffness :: Float32
+  , camSettingsAttributeValueSwivelSpeed :: Float32
+  } deriving (Eq, Ord, Show)
+
+getCamSettingsAttributeValue :: BinaryBit.BitGet CamSettingsAttributeValue
+getCamSettingsAttributeValue = do
+  fov <- getFloat32Bits
+  height <- getFloat32Bits
+  angle <- getFloat32Bits
+  distance <- getFloat32Bits
+  stiffness <- getFloat32Bits
+  swivelSpeed <- getFloat32Bits
+  pure
+    (CamSettingsAttributeValue fov height angle distance stiffness swivelSpeed)
+
+putCamSettingsAttributeValue :: CamSettingsAttributeValue -> BinaryBit.BitPut ()
+putCamSettingsAttributeValue camSettingsAttributeValue = do
+  putFloat32Bits (camSettingsAttributeValueFov camSettingsAttributeValue)
+  putFloat32Bits (camSettingsAttributeValueHeight camSettingsAttributeValue)
+  putFloat32Bits (camSettingsAttributeValueAngle camSettingsAttributeValue)
+  putFloat32Bits (camSettingsAttributeValueDistance camSettingsAttributeValue)
+  putFloat32Bits (camSettingsAttributeValueStiffness camSettingsAttributeValue)
+  putFloat32Bits
+    (camSettingsAttributeValueSwivelSpeed camSettingsAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/Demolish.hs b/library/Rattletrap/AttributeValue/Demolish.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/Demolish.hs
@@ -0,0 +1,42 @@
+module Rattletrap.AttributeValue.Demolish where
+
+import Rattletrap.Vector
+import Rattletrap.Word32
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data DemolishAttributeValue = DemolishAttributeValue
+  { demolishAttributeValueAttackerFlag :: Bool
+  , demolishAttributeValueAttackerActorId :: Word32
+  , demolishAttributeValueVictimFlag :: Bool
+  , demolishAttributeValueVictimActorId :: Word32
+  , demolishAttributeValueAttackerVelocity :: Vector
+  , demolishAttributeValueVictimVelocity :: Vector
+  } deriving (Eq, Ord, Show)
+
+getDemolishAttributeValue :: BinaryBit.BitGet DemolishAttributeValue
+getDemolishAttributeValue = do
+  attackerFlag <- BinaryBit.getBool
+  attackerActorId <- getWord32Bits
+  victimFlag <- BinaryBit.getBool
+  victimActorId <- getWord32Bits
+  attackerVelocity <- getVector
+  victimVelocity <- getVector
+  pure
+    (DemolishAttributeValue
+       attackerFlag
+       attackerActorId
+       victimFlag
+       victimActorId
+       attackerVelocity
+       victimVelocity)
+
+putDemolishAttributeValue :: DemolishAttributeValue -> BinaryBit.BitPut ()
+putDemolishAttributeValue demolishAttributeValue = do
+  BinaryBit.putBool (demolishAttributeValueAttackerFlag demolishAttributeValue)
+  putWord32Bits (demolishAttributeValueAttackerActorId demolishAttributeValue)
+  BinaryBit.putBool (demolishAttributeValueVictimFlag demolishAttributeValue)
+  putWord32Bits (demolishAttributeValueVictimActorId demolishAttributeValue)
+  putVector (demolishAttributeValueAttackerVelocity demolishAttributeValue)
+  putVector (demolishAttributeValueVictimVelocity demolishAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/Enum.hs b/library/Rattletrap/AttributeValue/Enum.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/Enum.hs
@@ -0,0 +1,18 @@
+module Rattletrap.AttributeValue.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 EnumAttributeValue = EnumAttributeValue
+  { enumAttributeValueValue :: Word.Word16
+  } deriving (Eq, Ord, Show)
+
+getEnumAttributeValue :: BinaryBit.BitGet EnumAttributeValue
+getEnumAttributeValue = do
+  value <- BinaryBit.getWord16be 11
+  pure (EnumAttributeValue value)
+
+putEnumAttributeValue :: EnumAttributeValue -> BinaryBit.BitPut ()
+putEnumAttributeValue enumAttributeValue =
+  BinaryBit.putWord16be 11 (enumAttributeValueValue enumAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/Explosion.hs b/library/Rattletrap/AttributeValue/Explosion.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/Explosion.hs
@@ -0,0 +1,25 @@
+module Rattletrap.AttributeValue.Explosion where
+
+import Rattletrap.Int32
+import Rattletrap.Vector
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data ExplosionAttributeValue = ExplosionAttributeValue
+  { explosionAttributeValueActorId :: Int32
+  , explosionAttributeValueLocation :: Vector
+  } deriving (Eq, Ord, Show)
+
+getExplosionAttributeValue :: BinaryBit.BitGet ExplosionAttributeValue
+getExplosionAttributeValue = do
+  False <- BinaryBit.getBool
+  actorId <- getInt32Bits
+  location <- getVector
+  pure (ExplosionAttributeValue actorId location)
+
+putExplosionAttributeValue :: ExplosionAttributeValue -> BinaryBit.BitPut ()
+putExplosionAttributeValue explosionAttributeValue = do
+  BinaryBit.putBool False
+  putInt32Bits (explosionAttributeValueActorId explosionAttributeValue)
+  putVector (explosionAttributeValueLocation explosionAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/FlaggedInt.hs b/library/Rattletrap/AttributeValue/FlaggedInt.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/FlaggedInt.hs
@@ -0,0 +1,22 @@
+module Rattletrap.AttributeValue.FlaggedInt where
+
+import Rattletrap.Int32
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data FlaggedIntAttributeValue = FlaggedIntAttributeValue
+  { flaggedIntAttributeValueFlag :: Bool
+  , flaggedIntAttributeValueInt :: Int32
+  } deriving (Eq, Ord, Show)
+
+getFlaggedIntAttributeValue :: BinaryBit.BitGet FlaggedIntAttributeValue
+getFlaggedIntAttributeValue = do
+  flag <- BinaryBit.getBool
+  int <- getInt32Bits
+  pure (FlaggedIntAttributeValue flag int)
+
+putFlaggedIntAttributeValue :: FlaggedIntAttributeValue -> BinaryBit.BitPut ()
+putFlaggedIntAttributeValue flaggedIntAttributeValue = do
+  BinaryBit.putBool (flaggedIntAttributeValueFlag flaggedIntAttributeValue)
+  putInt32Bits (flaggedIntAttributeValueInt flaggedIntAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/Float.hs b/library/Rattletrap/AttributeValue/Float.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/Float.hs
@@ -0,0 +1,19 @@
+module Rattletrap.AttributeValue.Float where
+
+import Rattletrap.Float32
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype FloatAttributeValue = FloatAttributeValue
+  { floatAttributeValueValue :: Float32
+  } deriving (Eq, Ord, Show)
+
+getFloatAttributeValue :: BinaryBit.BitGet FloatAttributeValue
+getFloatAttributeValue = do
+  value <- getFloat32Bits
+  pure (FloatAttributeValue value)
+
+putFloatAttributeValue :: FloatAttributeValue -> BinaryBit.BitPut ()
+putFloatAttributeValue floatAttributeValue =
+  putFloat32Bits (floatAttributeValueValue floatAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/GameMode.hs b/library/Rattletrap/AttributeValue/GameMode.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/GameMode.hs
@@ -0,0 +1,26 @@
+module Rattletrap.AttributeValue.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 GameModeAttributeValue = GameModeAttributeValue
+  { gameModeAttributeValueNumBits :: Int
+  , gameModeAttributeValueWord :: Word.Word8
+  } deriving (Eq, Ord, Show)
+
+getGameModeAttributeValue :: (Int, Int)
+                          -> BinaryBit.BitGet GameModeAttributeValue
+getGameModeAttributeValue version = do
+  let numBits =
+        if version < (868, 12)
+          then 2
+          else 8
+  word <- BinaryBit.getWord8 numBits
+  pure (GameModeAttributeValue numBits word)
+
+putGameModeAttributeValue :: GameModeAttributeValue -> BinaryBit.BitPut ()
+putGameModeAttributeValue gameModeAttributeValue = do
+  let numBits = gameModeAttributeValueNumBits gameModeAttributeValue
+  let word = gameModeAttributeValueWord gameModeAttributeValue
+  BinaryBit.putWord8 numBits word
diff --git a/library/Rattletrap/AttributeValue/Int.hs b/library/Rattletrap/AttributeValue/Int.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/Int.hs
@@ -0,0 +1,19 @@
+module Rattletrap.AttributeValue.Int where
+
+import Rattletrap.Int32
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype IntAttributeValue = IntAttributeValue
+  { intAttributeValueValue :: Int32
+  } deriving (Eq, Ord, Show)
+
+getIntAttributeValue :: BinaryBit.BitGet IntAttributeValue
+getIntAttributeValue = do
+  value <- getInt32Bits
+  pure (IntAttributeValue value)
+
+putIntAttributeValue :: IntAttributeValue -> BinaryBit.BitPut ()
+putIntAttributeValue intAttributeValue =
+  putInt32Bits (intAttributeValueValue intAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/Loadout.hs b/library/Rattletrap/AttributeValue/Loadout.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/Loadout.hs
@@ -0,0 +1,61 @@
+module Rattletrap.AttributeValue.Loadout where
+
+import Rattletrap.Word32
+import Rattletrap.Word8
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data LoadoutAttributeValue = LoadoutAttributeValue
+  { loadoutAttributeValueVersion :: Word8
+  , loadoutAttributeValueBody :: Word32
+  , loadoutAttributeValueDecal :: Word32
+  , loadoutAttributeValueWheels :: Word32
+  , loadoutAttributeValueRocketTrail :: Word32
+  , loadoutAttributeValueAntenna :: Word32
+  , loadoutAttributeValueTopper :: Word32
+  , loadoutAttributeValueUnknown1 :: Word32
+  , loadoutAttributeValueUnknown2 :: Maybe Word32
+  } deriving (Eq, Ord, Show)
+
+getLoadoutAttributeValue :: BinaryBit.BitGet LoadoutAttributeValue
+getLoadoutAttributeValue = do
+  version <- getWord8Bits
+  body <- getWord32Bits
+  decal <- getWord32Bits
+  wheels <- getWord32Bits
+  rocketTrail <- getWord32Bits
+  antenna <- getWord32Bits
+  topper <- getWord32Bits
+  g <- getWord32Bits
+  h <-
+    if version > Word8 10
+      then do
+        h <- getWord32Bits
+        pure (Just h)
+      else pure Nothing
+  pure
+    (LoadoutAttributeValue
+       version
+       body
+       decal
+       wheels
+       rocketTrail
+       antenna
+       topper
+       g
+       h)
+
+putLoadoutAttributeValue :: LoadoutAttributeValue -> BinaryBit.BitPut ()
+putLoadoutAttributeValue loadoutAttributeValue = do
+  putWord8Bits (loadoutAttributeValueVersion loadoutAttributeValue)
+  putWord32Bits (loadoutAttributeValueBody loadoutAttributeValue)
+  putWord32Bits (loadoutAttributeValueDecal loadoutAttributeValue)
+  putWord32Bits (loadoutAttributeValueWheels loadoutAttributeValue)
+  putWord32Bits (loadoutAttributeValueRocketTrail loadoutAttributeValue)
+  putWord32Bits (loadoutAttributeValueAntenna loadoutAttributeValue)
+  putWord32Bits (loadoutAttributeValueTopper loadoutAttributeValue)
+  putWord32Bits (loadoutAttributeValueUnknown1 loadoutAttributeValue)
+  case loadoutAttributeValueUnknown2 loadoutAttributeValue of
+    Nothing -> pure ()
+    Just x -> putWord32Bits x
diff --git a/library/Rattletrap/AttributeValue/LoadoutOnline.hs b/library/Rattletrap/AttributeValue/LoadoutOnline.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/LoadoutOnline.hs
@@ -0,0 +1,42 @@
+module Rattletrap.AttributeValue.LoadoutOnline where
+
+import Rattletrap.CompressedWord
+import Rattletrap.Word32
+import Rattletrap.Word8
+
+import qualified Control.Monad as Monad
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype LoadoutOnlineAttributeValue = LoadoutOnlineAttributeValue
+  { loadoutAttributeValueValue :: [[(Word32, CompressedWord)]]
+  } deriving (Eq, Ord, Show)
+
+getLoadoutOnlineAttributeValue :: BinaryBit.BitGet LoadoutOnlineAttributeValue
+getLoadoutOnlineAttributeValue = do
+  size <- getWord8Bits
+  values <-
+    Monad.replicateM
+      (fromIntegral (word8Value size))
+      (do innerSize <- getWord8Bits
+          Monad.replicateM
+            (fromIntegral (word8Value innerSize))
+            (do x <- getWord32Bits
+                y <- getCompressedWord 27
+                pure (x, y)))
+  pure (LoadoutOnlineAttributeValue values)
+
+putLoadoutOnlineAttributeValue :: LoadoutOnlineAttributeValue
+                               -> BinaryBit.BitPut ()
+putLoadoutOnlineAttributeValue loadoutAttributeValue = do
+  let values = loadoutAttributeValueValue loadoutAttributeValue
+  putWord8Bits (Word8 (fromIntegral (length values)))
+  mapM_
+    (\xs -> do
+       putWord8Bits (Word8 (fromIntegral (length xs)))
+       mapM_
+         (\(x, y) -> do
+            putWord32Bits x
+            putCompressedWord y)
+         xs)
+    values
diff --git a/library/Rattletrap/AttributeValue/Loadouts.hs b/library/Rattletrap/AttributeValue/Loadouts.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/Loadouts.hs
@@ -0,0 +1,22 @@
+module Rattletrap.AttributeValue.Loadouts where
+
+import Rattletrap.AttributeValue.Loadout
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data LoadoutsAttributeValue = LoadoutsAttributeValue
+  { loadoutsAttributeValueBlue :: LoadoutAttributeValue
+  , loadoutsAttributeValueOrange :: LoadoutAttributeValue
+  } deriving (Eq, Ord, Show)
+
+getLoadoutsAttributeValue :: BinaryBit.BitGet LoadoutsAttributeValue
+getLoadoutsAttributeValue = do
+  blue <- getLoadoutAttributeValue
+  orange <- getLoadoutAttributeValue
+  pure (LoadoutsAttributeValue blue orange)
+
+putLoadoutsAttributeValue :: LoadoutsAttributeValue -> BinaryBit.BitPut ()
+putLoadoutsAttributeValue loadoutsAttributeValue = do
+  putLoadoutAttributeValue (loadoutsAttributeValueBlue loadoutsAttributeValue)
+  putLoadoutAttributeValue (loadoutsAttributeValueOrange loadoutsAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/LoadoutsOnline.hs b/library/Rattletrap/AttributeValue/LoadoutsOnline.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/LoadoutsOnline.hs
@@ -0,0 +1,34 @@
+module Rattletrap.AttributeValue.LoadoutsOnline where
+
+import Rattletrap.AttributeValue.LoadoutOnline
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data LoadoutsOnlineAttributeValue = LoadoutsOnlineAttributeValue
+  { loadoutsOnlineAttributeValueBlue :: LoadoutOnlineAttributeValue
+  , loadoutsOnlineAttributeValueOrange :: LoadoutOnlineAttributeValue
+  , loadoutsOnlineAttributeValueUnknown1 :: Bool
+  , loadoutsOnlineAttributeValueUnknown2 :: Bool
+  } deriving (Eq, Ord, Show)
+
+getLoadoutsOnlineAttributeValue :: BinaryBit.BitGet LoadoutsOnlineAttributeValue
+getLoadoutsOnlineAttributeValue = do
+  blueLoadout <- getLoadoutOnlineAttributeValue
+  orangeLoadout <- getLoadoutOnlineAttributeValue
+  unknown1 <- BinaryBit.getBool
+  unknown2 <- BinaryBit.getBool
+  pure
+    (LoadoutsOnlineAttributeValue blueLoadout orangeLoadout unknown1 unknown2)
+
+putLoadoutsOnlineAttributeValue :: LoadoutsOnlineAttributeValue
+                                -> BinaryBit.BitPut ()
+putLoadoutsOnlineAttributeValue loadoutsOnlineAttributeValue = do
+  putLoadoutOnlineAttributeValue
+    (loadoutsOnlineAttributeValueBlue loadoutsOnlineAttributeValue)
+  putLoadoutOnlineAttributeValue
+    (loadoutsOnlineAttributeValueOrange loadoutsOnlineAttributeValue)
+  BinaryBit.putBool
+    (loadoutsOnlineAttributeValueUnknown1 loadoutsOnlineAttributeValue)
+  BinaryBit.putBool
+    (loadoutsOnlineAttributeValueUnknown2 loadoutsOnlineAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/Location.hs b/library/Rattletrap/AttributeValue/Location.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/Location.hs
@@ -0,0 +1,19 @@
+module Rattletrap.AttributeValue.Location where
+
+import Rattletrap.Vector
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype LocationAttributeValue = LocationAttributeValue
+  { locationAttributeValueValue :: Vector
+  } deriving (Eq, Ord, Show)
+
+getLocationAttributeValue :: BinaryBit.BitGet LocationAttributeValue
+getLocationAttributeValue = do
+  value <- getVector
+  pure (LocationAttributeValue value)
+
+putLocationAttributeValue :: LocationAttributeValue -> BinaryBit.BitPut ()
+putLocationAttributeValue locationAttributeValue =
+  putVector (locationAttributeValueValue locationAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/MusicStinger.hs b/library/Rattletrap/AttributeValue/MusicStinger.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/MusicStinger.hs
@@ -0,0 +1,27 @@
+module Rattletrap.AttributeValue.MusicStinger where
+
+import Rattletrap.Word32
+import Rattletrap.Word8
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data MusicStingerAttributeValue = MusicStingerAttributeValue
+  { musicStingerAttributeValueFlag :: Bool
+  , musicStingerAttributeValueCue :: Word32
+  , musicStingerAttributeValueTrigger :: Word8
+  } deriving (Eq, Ord, Show)
+
+getMusicStingerAttributeValue :: BinaryBit.BitGet MusicStingerAttributeValue
+getMusicStingerAttributeValue = do
+  flag <- BinaryBit.getBool
+  cue <- getWord32Bits
+  trigger <- getWord8Bits
+  pure (MusicStingerAttributeValue flag cue trigger)
+
+putMusicStingerAttributeValue :: MusicStingerAttributeValue
+                              -> BinaryBit.BitPut ()
+putMusicStingerAttributeValue musicStingerAttributeValue = do
+  BinaryBit.putBool (musicStingerAttributeValueFlag musicStingerAttributeValue)
+  putWord32Bits (musicStingerAttributeValueCue musicStingerAttributeValue)
+  putWord8Bits (musicStingerAttributeValueTrigger musicStingerAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/PartyLeader.hs b/library/Rattletrap/AttributeValue/PartyLeader.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/PartyLeader.hs
@@ -0,0 +1,33 @@
+module Rattletrap.AttributeValue.PartyLeader where
+
+import Rattletrap.RemoteId
+import Rattletrap.Word8
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data PartyLeaderAttributeValue = PartyLeaderAttributeValue
+  { partyLeaderAttributeValueSystemId :: Word8
+  , partyLeaderAttributeValueId :: Maybe (RemoteId, Word8)
+  } deriving (Eq, Ord, Show)
+
+getPartyLeaderAttributeValue :: BinaryBit.BitGet PartyLeaderAttributeValue
+getPartyLeaderAttributeValue = do
+  systemId <- getWord8Bits
+  maybeRemoteAndLocalId <-
+    if systemId == Word8 0
+      then pure Nothing
+      else do
+        remoteId <- getRemoteId systemId
+        localId <- getWord8Bits
+        pure (Just (remoteId, localId))
+  pure (PartyLeaderAttributeValue systemId maybeRemoteAndLocalId)
+
+putPartyLeaderAttributeValue :: PartyLeaderAttributeValue -> BinaryBit.BitPut ()
+putPartyLeaderAttributeValue partyLeaderAttributeValue = do
+  putWord8Bits (partyLeaderAttributeValueSystemId partyLeaderAttributeValue)
+  case partyLeaderAttributeValueId partyLeaderAttributeValue of
+    Nothing -> pure ()
+    Just (remoteId, localId) -> do
+      putRemoteId remoteId
+      putWord8Bits localId
diff --git a/library/Rattletrap/AttributeValue/Pickup.hs b/library/Rattletrap/AttributeValue/Pickup.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/Pickup.hs
@@ -0,0 +1,32 @@
+module Rattletrap.AttributeValue.Pickup where
+
+import Rattletrap.Word32
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data PickupAttributeValue = PickupAttributeValue
+  { pickupAttributeValueInstigatorId :: Maybe Word32
+  , pickupAttributeValuePickedUp :: Bool
+  } deriving (Eq, Ord, Show)
+
+getPickupAttributeValue :: BinaryBit.BitGet PickupAttributeValue
+getPickupAttributeValue = do
+  instigator <- BinaryBit.getBool
+  maybeInstigatorId <-
+    if instigator
+      then do
+        instigatorId <- getWord32Bits
+        pure (Just instigatorId)
+      else pure Nothing
+  pickedUp <- BinaryBit.getBool
+  pure (PickupAttributeValue maybeInstigatorId pickedUp)
+
+putPickupAttributeValue :: PickupAttributeValue -> BinaryBit.BitPut ()
+putPickupAttributeValue pickupAttributeValue = do
+  case pickupAttributeValueInstigatorId pickupAttributeValue of
+    Nothing -> BinaryBit.putBool False
+    Just instigatorId -> do
+      BinaryBit.putBool True
+      putWord32Bits instigatorId
+  BinaryBit.putBool (pickupAttributeValuePickedUp pickupAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/PrivateMatchSettings.hs b/library/Rattletrap/AttributeValue/PrivateMatchSettings.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/PrivateMatchSettings.hs
@@ -0,0 +1,54 @@
+module Rattletrap.AttributeValue.PrivateMatchSettings where
+
+import Rattletrap.Text
+import Rattletrap.Word32
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data PrivateMatchSettingsAttributeValue = PrivateMatchSettingsAttributeValue
+  { privateMatchSettingsAttributeValueMutators :: Text
+  , privateMatchSettingsAttributeValueJoinableBy :: Word32
+  , privateMatchSettingsAttributeValueMaxPlayers :: Word32
+  , privateMatchSettingsAttributeValueGameName :: Text
+  , privateMatchSettingsAttributeValuePassword :: Text
+  , privateMatchSettingsAttributeValueFlag :: Bool
+  } deriving (Eq, Ord, Show)
+
+getPrivateMatchSettingsAttributeValue :: BinaryBit.BitGet PrivateMatchSettingsAttributeValue
+getPrivateMatchSettingsAttributeValue = do
+  mutators <- getTextBits
+  joinableBy <- getWord32Bits
+  maxPlayers <- getWord32Bits
+  gameName <- getTextBits
+  password <- getTextBits
+  flag <- BinaryBit.getBool
+  pure
+    (PrivateMatchSettingsAttributeValue
+       mutators
+       joinableBy
+       maxPlayers
+       gameName
+       password
+       flag)
+
+putPrivateMatchSettingsAttributeValue :: PrivateMatchSettingsAttributeValue
+                                      -> BinaryBit.BitPut ()
+putPrivateMatchSettingsAttributeValue privateMatchSettingsAttributeValue = do
+  putTextBits
+    (privateMatchSettingsAttributeValueMutators
+       privateMatchSettingsAttributeValue)
+  putWord32Bits
+    (privateMatchSettingsAttributeValueJoinableBy
+       privateMatchSettingsAttributeValue)
+  putWord32Bits
+    (privateMatchSettingsAttributeValueMaxPlayers
+       privateMatchSettingsAttributeValue)
+  putTextBits
+    (privateMatchSettingsAttributeValueGameName
+       privateMatchSettingsAttributeValue)
+  putTextBits
+    (privateMatchSettingsAttributeValuePassword
+       privateMatchSettingsAttributeValue)
+  BinaryBit.putBool
+    (privateMatchSettingsAttributeValueFlag privateMatchSettingsAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/QWord.hs b/library/Rattletrap/AttributeValue/QWord.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/QWord.hs
@@ -0,0 +1,19 @@
+module Rattletrap.AttributeValue.QWord where
+
+import Rattletrap.Word64
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype QWordAttributeValue = QWordAttributeValue
+  { qWordAttributeValueValue :: Word64
+  } deriving (Eq, Ord, Show)
+
+getQWordAttributeValue :: BinaryBit.BitGet QWordAttributeValue
+getQWordAttributeValue = do
+  value <- getWord64Bits
+  pure (QWordAttributeValue value)
+
+putQWordAttributeValue :: QWordAttributeValue -> BinaryBit.BitPut ()
+putQWordAttributeValue qWordAttributeValue =
+  putWord64Bits (qWordAttributeValueValue qWordAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/Reservation.hs b/library/Rattletrap/AttributeValue/Reservation.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/Reservation.hs
@@ -0,0 +1,56 @@
+module Rattletrap.AttributeValue.Reservation where
+
+import Rattletrap.AttributeValue.UniqueId
+import Rattletrap.CompressedWord
+import Rattletrap.Text
+import Rattletrap.Word8
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Word as Word
+
+data ReservationAttributeValue = ReservationAttributeValue
+  { reservationAttributeValueNumber :: CompressedWord
+  , reservationAttributeValueUniqueId :: UniqueIdAttributeValue
+  , reservationAttributeValueName :: Maybe Text
+  , reservationAttributeValueUnknown1 :: Bool
+  , reservationAttributeValueUnknown2 :: Bool
+  , reservationAttributeValueUnknown3 :: Maybe Word.Word8
+  } deriving (Eq, Ord, Show)
+
+getReservationAttributeValue :: (Int, Int)
+                             -> BinaryBit.BitGet ReservationAttributeValue
+getReservationAttributeValue version = do
+  number <- getCompressedWord 7
+  uniqueId <- getUniqueIdAttributeValue
+  name <-
+    if uniqueIdAttributeValueSystemId uniqueId == Word8 0
+      then pure Nothing
+      else do
+        name <- getTextBits
+        pure (Just name)
+  a <- BinaryBit.getBool
+  b <- BinaryBit.getBool
+  mc <-
+    if version < (868, 12)
+      then pure Nothing
+      else do
+        c <- BinaryBit.getWord8 6
+        pure (Just c)
+  pure (ReservationAttributeValue number uniqueId name a b mc)
+
+putReservationAttributeValue :: ReservationAttributeValue -> BinaryBit.BitPut ()
+putReservationAttributeValue reservationAttributeValue = do
+  putCompressedWord (reservationAttributeValueNumber reservationAttributeValue)
+  putUniqueIdAttributeValue
+    (reservationAttributeValueUniqueId reservationAttributeValue)
+  case reservationAttributeValueName reservationAttributeValue of
+    Nothing -> pure ()
+    Just name -> putTextBits name
+  BinaryBit.putBool
+    (reservationAttributeValueUnknown1 reservationAttributeValue)
+  BinaryBit.putBool
+    (reservationAttributeValueUnknown2 reservationAttributeValue)
+  case reservationAttributeValueUnknown3 reservationAttributeValue of
+    Nothing -> pure ()
+    Just c -> BinaryBit.putWord8 6 c
diff --git a/library/Rattletrap/AttributeValue/RigidBodyState.hs b/library/Rattletrap/AttributeValue/RigidBodyState.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/RigidBodyState.hs
@@ -0,0 +1,55 @@
+module Rattletrap.AttributeValue.RigidBodyState where
+
+import Rattletrap.CompressedWordVector
+import Rattletrap.Vector
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data RigidBodyStateAttributeValue = RigidBodyStateAttributeValue
+  { rigidBodyStateAttributeValueSleeping :: Bool
+  , rigidBodyStateAttributeValueLocation :: Vector
+  , rigidBodyStateAttributeValueRotation :: CompressedWordVector
+  , rigidBodyStateAttributeValueLinearVelocity :: Maybe Vector
+  , rigidBodyStateAttributeValueAngularVelocity :: Maybe Vector
+  } deriving (Eq, Ord, Show)
+
+getRigidBodyStateAttributeValue :: BinaryBit.BitGet RigidBodyStateAttributeValue
+getRigidBodyStateAttributeValue = 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
+    (RigidBodyStateAttributeValue
+       sleeping
+       location
+       rotation
+       linearVelocity
+       angularVelocity)
+
+putRigidBodyStateAttributeValue :: RigidBodyStateAttributeValue
+                                -> BinaryBit.BitPut ()
+putRigidBodyStateAttributeValue rigidBodyStateAttributeValue = do
+  BinaryBit.putBool
+    (rigidBodyStateAttributeValueSleeping rigidBodyStateAttributeValue)
+  putVector (rigidBodyStateAttributeValueLocation rigidBodyStateAttributeValue)
+  putCompressedWordVector
+    (rigidBodyStateAttributeValueRotation rigidBodyStateAttributeValue)
+  case rigidBodyStateAttributeValueLinearVelocity rigidBodyStateAttributeValue of
+    Nothing -> pure ()
+    Just linearVelocity -> putVector linearVelocity
+  case rigidBodyStateAttributeValueAngularVelocity rigidBodyStateAttributeValue of
+    Nothing -> pure ()
+    Just angularVelocity -> putVector angularVelocity
diff --git a/library/Rattletrap/AttributeValue/String.hs b/library/Rattletrap/AttributeValue/String.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/String.hs
@@ -0,0 +1,19 @@
+module Rattletrap.AttributeValue.String where
+
+import Rattletrap.Text
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype StringAttributeValue = StringAttributeValue
+  { stringAttributeValueValue :: Text
+  } deriving (Eq, Ord, Show)
+
+getStringAttributeValue :: BinaryBit.BitGet StringAttributeValue
+getStringAttributeValue = do
+  value <- getTextBits
+  pure (StringAttributeValue value)
+
+putStringAttributeValue :: StringAttributeValue -> BinaryBit.BitPut ()
+putStringAttributeValue stringAttributeValue =
+  putTextBits (stringAttributeValueValue stringAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/TeamPaint.hs b/library/Rattletrap/AttributeValue/TeamPaint.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/TeamPaint.hs
@@ -0,0 +1,38 @@
+module Rattletrap.AttributeValue.TeamPaint where
+
+import Rattletrap.Word32
+import Rattletrap.Word8
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data TeamPaintAttributeValue = TeamPaintAttributeValue
+  { teamPaintAttributeValueTeam :: Word8
+  , teamPaintAttributeValuePrimaryColor :: Word8
+  , teamPaintAttributeValueAccentColor :: Word8
+  , teamPaintAttributeValuePrimaryFinish :: Word32
+  , teamPaintAttributeValueAccentFinish :: Word32
+  } deriving (Eq, Ord, Show)
+
+getTeamPaintAttributeValue :: BinaryBit.BitGet TeamPaintAttributeValue
+getTeamPaintAttributeValue = do
+  team <- getWord8Bits
+  primaryColor <- getWord8Bits
+  accentColor <- getWord8Bits
+  primaryFinish <- getWord32Bits
+  accentFinish <- getWord32Bits
+  pure
+    (TeamPaintAttributeValue
+       team
+       primaryColor
+       accentColor
+       primaryFinish
+       accentFinish)
+
+putTeamPaintAttributeValue :: TeamPaintAttributeValue -> BinaryBit.BitPut ()
+putTeamPaintAttributeValue teamPaintAttributeValue = do
+  putWord8Bits (teamPaintAttributeValueTeam teamPaintAttributeValue)
+  putWord8Bits (teamPaintAttributeValuePrimaryColor teamPaintAttributeValue)
+  putWord8Bits (teamPaintAttributeValueAccentColor teamPaintAttributeValue)
+  putWord32Bits (teamPaintAttributeValuePrimaryFinish teamPaintAttributeValue)
+  putWord32Bits (teamPaintAttributeValueAccentFinish teamPaintAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/UniqueId.hs b/library/Rattletrap/AttributeValue/UniqueId.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/UniqueId.hs
@@ -0,0 +1,26 @@
+module Rattletrap.AttributeValue.UniqueId where
+
+import Rattletrap.RemoteId
+import Rattletrap.Word8
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data UniqueIdAttributeValue = UniqueIdAttributeValue
+  { uniqueIdAttributeValueSystemId :: Word8
+  , uniqueIdAttributeValueRemoteId :: RemoteId
+  , uniqueIdAttributeValueLocalId :: Word8
+  } deriving (Eq, Ord, Show)
+
+getUniqueIdAttributeValue :: BinaryBit.BitGet UniqueIdAttributeValue
+getUniqueIdAttributeValue = do
+  systemId <- getWord8Bits
+  remoteId <- getRemoteId systemId
+  localId <- getWord8Bits
+  pure (UniqueIdAttributeValue systemId remoteId localId)
+
+putUniqueIdAttributeValue :: UniqueIdAttributeValue -> BinaryBit.BitPut ()
+putUniqueIdAttributeValue uniqueIdAttributeValue = do
+  putWord8Bits (uniqueIdAttributeValueSystemId uniqueIdAttributeValue)
+  putRemoteId (uniqueIdAttributeValueRemoteId uniqueIdAttributeValue)
+  putWord8Bits (uniqueIdAttributeValueLocalId uniqueIdAttributeValue)
diff --git a/library/Rattletrap/AttributeValue/WeldedInfo.hs b/library/Rattletrap/AttributeValue/WeldedInfo.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValue/WeldedInfo.hs
@@ -0,0 +1,34 @@
+module Rattletrap.AttributeValue.WeldedInfo where
+
+import Rattletrap.Float32
+import Rattletrap.Int32
+import Rattletrap.Int8Vector
+import Rattletrap.Vector
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data WeldedInfoAttributeValue = WeldedInfoAttributeValue
+  { weldedInfoAttributeValueActive :: Bool
+  , weldedInfoAttributeValueActorId :: Int32
+  , weldedInfoAttributeValueOffset :: Vector
+  , weldedInfoAttributeValueMass :: Float32
+  , weldedInfoAttributeValueRotation :: Int8Vector
+  } deriving (Eq, Ord, Show)
+
+getWeldedInfoAttributeValue :: BinaryBit.BitGet WeldedInfoAttributeValue
+getWeldedInfoAttributeValue = do
+  active <- BinaryBit.getBool
+  actorId <- getInt32Bits
+  offset <- getVector
+  mass <- getFloat32Bits
+  rotation <- getInt8Vector
+  pure (WeldedInfoAttributeValue active actorId offset mass rotation)
+
+putWeldedInfoAttributeValue :: WeldedInfoAttributeValue -> BinaryBit.BitPut ()
+putWeldedInfoAttributeValue weldedInfoAttributeValue = do
+  BinaryBit.putBool (weldedInfoAttributeValueActive weldedInfoAttributeValue)
+  putInt32Bits (weldedInfoAttributeValueActorId weldedInfoAttributeValue)
+  putVector (weldedInfoAttributeValueOffset weldedInfoAttributeValue)
+  putFloat32Bits (weldedInfoAttributeValueMass weldedInfoAttributeValue)
+  putInt8Vector (weldedInfoAttributeValueRotation weldedInfoAttributeValue)
diff --git a/library/Rattletrap/AttributeValueType.hs b/library/Rattletrap/AttributeValueType.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/AttributeValueType.hs
@@ -0,0 +1,30 @@
+module Rattletrap.AttributeValueType where
+
+data AttributeValueType
+  = AVBoolean
+  | AVByte
+  | AVCamSettings
+  | AVDemolish
+  | AVEnum
+  | AVExplosion
+  | AVFlaggedInt
+  | AVFloat
+  | AVGameMode
+  | AVInt
+  | AVLoadout
+  | AVLoadoutOnline
+  | AVLoadouts
+  | AVLoadoutsOnline
+  | AVLocation
+  | AVMusicStinger
+  | AVPartyLeader
+  | AVPickup
+  | AVPrivateMatchSettings
+  | AVQWord
+  | AVReservation
+  | AVRigidBodyState
+  | AVString
+  | AVTeamPaint
+  | AVUniqueId
+  | AVWeldedInfo
+  deriving (Eq, Ord, Show)
diff --git a/library/Rattletrap/Cache.hs b/library/Rattletrap/Cache.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Cache.hs
@@ -0,0 +1,35 @@
+module Rattletrap.Cache where
+
+import Rattletrap.AttributeMapping
+import Rattletrap.List
+import Rattletrap.Word32
+
+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
+    { cacheClassId = classId
+    , cacheParentCacheId = parentCacheId
+    , cacheCacheId = cacheId
+    , cacheAttributeMappings = 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
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/ClassAttributeMap.hs
@@ -0,0 +1,247 @@
+module Rattletrap.ClassAttributeMap where
+
+import Rattletrap.ActorMap
+import Rattletrap.AttributeMapping
+import Rattletrap.Cache
+import Rattletrap.ClassMapping
+import Rattletrap.CompressedWord
+import Rattletrap.Data
+import Rattletrap.List
+import Rattletrap.Text
+import Rattletrap.Utility
+import Rattletrap.Word32
+
+import qualified Data.Bimap as Bimap
+import qualified Data.List as List
+import qualified Data.Map as Map
+import qualified Data.Maybe as Maybe
+import qualified Data.Set as Set
+
+data ClassAttributeMap = ClassAttributeMap
+  { classAttributeMapObjectMap :: Bimap.Bimap Word32 Text
+  , classAttributeMapClassMap :: Bimap.Bimap Word32 Text
+  , classAttributeMapValue :: Map.Map Word32 (Bimap.Bimap Word32 Word32)
+  } deriving (Eq, Show)
+
+makeClassAttributeMap :: List Text
+                      -> List ClassMapping
+                      -> List Cache
+                      -> ClassAttributeMap
+makeClassAttributeMap objects classMappings caches =
+  let objectMap = makeObjectMap objects
+      classCache = makeClassCache classMappings caches
+      attributeMap = makeAttributeMap caches
+      classIds = map (\(_, classId, _, _) -> classId) classCache
+      parentMap = makeParentMap classMappings caches
+      value =
+        Map.fromList
+          (map
+             (\classId ->
+                let ownAttributes =
+                      Maybe.fromMaybe
+                        Bimap.empty
+                        (Map.lookup classId attributeMap)
+                    parentsAttributes =
+                      case Map.lookup classId parentMap of
+                        Nothing -> []
+                        Just parentClassIds ->
+                          map
+                            (\parentClassId ->
+                               Maybe.fromMaybe
+                                 Bimap.empty
+                                 (Map.lookup parentClassId attributeMap))
+                            parentClassIds
+                    attributes = ownAttributes : parentsAttributes
+                in (classId, Bimap.fromList (concatMap Bimap.toList attributes)))
+             classIds)
+  in ClassAttributeMap
+     { classAttributeMapObjectMap = objectMap
+     , classAttributeMapClassMap = makeClassMap classMappings
+     , classAttributeMapValue = value
+     }
+
+makeClassCache :: List ClassMapping
+               -> List Cache
+               -> [(Maybe Text, Word32, Word32, Word32)]
+makeClassCache classMappings caches =
+  let classMap = makeClassMap classMappings
+  in 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 (Bimap.Bimap Word32 Word32)
+makeAttributeMap caches =
+  Map.fromList
+    (map
+       (\cache ->
+          ( cacheClassId cache
+          , Bimap.fromList
+              (map
+                 (\attributeMapping ->
+                    ( attributeMappingStreamId attributeMapping
+                    , attributeMappingObjectId attributeMapping))
+                 (listValue (cacheAttributeMappings cache)))))
+       (listValue caches))
+
+makeShallowParentMap :: List ClassMapping -> List Cache -> Map.Map Word32 Word32
+makeShallowParentMap classMappings caches =
+  let classCache = makeClassCache classMappings caches
+  in 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 :: List ClassMapping -> List Cache -> Map.Map Word32 [Word32]
+makeParentMap classMappings caches =
+  let shallowParentMap = makeShallowParentMap classMappings caches
+  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 -> Bimap.Bimap Word32 Text
+makeObjectMap objects =
+  Bimap.fromList (zip (map Word32 [0 ..]) (listValue objects))
+
+getObjectName :: ClassAttributeMap -> Word32 -> Maybe Text
+getObjectName classAttributeMap objectId =
+  Bimap.lookup objectId (classAttributeMapObjectMap classAttributeMap)
+
+getClassName :: Text -> Maybe Text
+getClassName rawObjectName =
+  Map.lookup (normalizeObjectName rawObjectName) objectClasses
+
+normalizeObjectName :: Text -> Text
+normalizeObjectName objectName =
+  stringToText
+    (replace
+       "_[0-9]+$"
+       ""
+       (replace "^[A-Z_a-z]+[.]TheWorld:" "TheWorld:" (textToString 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 :: ClassAttributeMap
+                    -> ActorMap
+                    -> CompressedWord
+                    -> Maybe Word
+getAttributeIdLimit classAttributeMap actorMap actorId = do
+  attributeMap <- getAttributeMap classAttributeMap actorMap actorId
+  let streamIds = Bimap.keys attributeMap
+  let maxStreamId = maximum (Word32 0 : streamIds)
+  let limit = fromIntegral (word32Value maxStreamId)
+  pure limit
+
+getAttributeName :: ClassAttributeMap
+                 -> ActorMap
+                 -> CompressedWord
+                 -> CompressedWord
+                 -> Maybe Text
+getAttributeName classAttributeMap actorMap actorId streamId = do
+  attributeMap <- getAttributeMap classAttributeMap actorMap actorId
+  let key = Word32 (fromIntegral (compressedWordValue streamId))
+  attributeId <- Bimap.lookup key attributeMap
+  let objectMap = classAttributeMapObjectMap classAttributeMap
+  Bimap.lookup attributeId objectMap
+
+getAttributeMap
+  :: ClassAttributeMap
+  -> ActorMap
+  -> CompressedWord
+  -> Maybe (Bimap.Bimap Word32 Word32)
+getAttributeMap classAttributeMap actorMap actorId = do
+  objectId <- Map.lookup actorId actorMap
+  objectName <- getObjectName classAttributeMap objectId
+  className <- getClassName objectName
+  let classMap = classAttributeMapClassMap classAttributeMap
+  classId <- Bimap.lookupR className classMap
+  let value = classAttributeMapValue classAttributeMap
+  Map.lookup classId value
diff --git a/library/Rattletrap/ClassMapping.hs b/library/Rattletrap/ClassMapping.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/ClassMapping.hs
@@ -0,0 +1,22 @@
+module Rattletrap.ClassMapping where
+
+import Rattletrap.Text
+import Rattletrap.Word32
+
+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 {classMappingName = name, classMappingStreamId = streamId}
+
+putClassMapping :: ClassMapping -> Binary.Put
+putClassMapping classMapping = do
+  putText (classMappingName classMapping)
+  putWord32 (classMappingStreamId classMapping)
diff --git a/library/Rattletrap/CompressedWord.hs b/library/Rattletrap/CompressedWord.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/CompressedWord.hs
@@ -0,0 +1,59 @@
+module Rattletrap.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/CompressedWordVector.hs b/library/Rattletrap/CompressedWordVector.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/CompressedWordVector.hs
@@ -0,0 +1,31 @@
+module Rattletrap.CompressedWordVector where
+
+import Rattletrap.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
+    { compressedWordVectorX = x
+    , compressedWordVectorY = y
+    , compressedWordVectorZ = z
+    }
+
+putCompressedWordVector :: CompressedWordVector -> BinaryBit.BitPut ()
+putCompressedWordVector compressedWordVector = do
+  putCompressedWord (compressedWordVectorX compressedWordVector)
+  putCompressedWord (compressedWordVectorY compressedWordVector)
+  putCompressedWord (compressedWordVectorZ compressedWordVector)
diff --git a/library/Rattletrap/Content.hs b/library/Rattletrap/Content.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Content.hs
@@ -0,0 +1,86 @@
+module Rattletrap.Content where
+
+import Rattletrap.ActorMap
+import Rattletrap.Cache
+import Rattletrap.ClassAttributeMap
+import Rattletrap.ClassMapping
+import Rattletrap.Frame
+import Rattletrap.KeyFrame
+import Rattletrap.List
+import Rattletrap.Mark
+import Rattletrap.Message
+import Rattletrap.Text
+import Rattletrap.Utility
+import Rattletrap.Word32
+
+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
+
+data Content = Content
+  { contentLevels :: List Text
+  , contentKeyFrames :: List KeyFrame
+  , contentStreamSize :: Word32
+  , contentFrames :: [Frame]
+  , contentMessages :: List Message
+  , contentMarks :: List Mark
+  , contentPackages :: List Text
+  , contentObjects :: List Text
+  , contentNames :: List Text
+  , contentClassMappings :: List ClassMapping
+  , contentCaches :: List Cache
+  } deriving (Eq, Ord, Show)
+
+getContent :: (Int, Int) -> Int -> Binary.Get Content
+getContent version numFrames = 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
+  let (frames, _) =
+        Binary.runGet
+          (BinaryBit.runBitGet
+             (getFrames version numFrames classAttributeMap makeActorMap))
+          (reverseBytes stream)
+  pure
+    Content
+    { contentLevels = levels
+    , contentKeyFrames = keyFrames
+    , contentStreamSize = streamSize
+    , contentFrames = frames
+    , contentMessages = messages
+    , contentMarks = marks
+    , contentPackages = packages
+    , contentObjects = objects
+    , contentNames = names
+    , contentClassMappings = classMappings
+    , contentCaches = 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
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Crc.hs
@@ -0,0 +1,37 @@
+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
+
+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
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Data.hs
@@ -0,0 +1,349 @@
+module Rattletrap.Data where
+
+import Rattletrap.AttributeValueType
+
+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")
+  -- , ("TAGame.VoteActor_TA", "Engine.ReplicationInfo")
+  ]
+
+rawClassesWithLocation :: [String]
+rawClassesWithLocation =
+  [ "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_TA"
+  , "TAGame.Car_Season_TA"
+  , "TAGame.Car_TA"
+  ]
+
+rawObjectClasses :: [(String, String)]
+rawObjectClasses =
+  [ ("Archetypes.Ball.Ball_Basketball", "TAGame.Ball_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_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_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.CameraSettingsActor_TA:PRI", "TAGame.CameraSettingsActor_TA")
+  , ("TAGame.Default__CameraSettingsActor_TA", "TAGame.CameraSettingsActor_TA")
+  , ("TAGame.Default__PRI_TA", "TAGame.PRI_TA")
+  -- , ("TAGame.Default__VoteActor_TA", "TAGame.VoteActor_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")
+  ]
+
+rawAttributeValueTypes :: [(String, AttributeValueType)]
+rawAttributeValueTypes =
+  [ ("Engine.Actor:bBlockActors", AVBoolean)
+  , ("Engine.Actor:DrawScale", AVFloat)
+  , ("Engine.Actor:Role", AVEnum)
+  , ("Engine.Actor:bCollideActors", AVBoolean)
+  , ("Engine.Actor:bHidden", AVBoolean)
+  , ("Engine.GameReplicationInfo:GameClass", AVFlaggedInt)
+  , ("Engine.GameReplicationInfo:ServerName", AVString)
+  , ("Engine.GameReplicationInfo:bMatchIsOver", AVBoolean)
+  , ("Engine.Pawn:PlayerReplicationInfo", AVFlaggedInt)
+  , ("Engine.PlayerReplicationInfo:Ping", AVByte)
+  , ("Engine.PlayerReplicationInfo:PlayerID", AVInt)
+  , ("Engine.PlayerReplicationInfo:PlayerName", AVString)
+  , ("Engine.PlayerReplicationInfo:RemoteUserData", AVString)
+  , ("Engine.PlayerReplicationInfo:Score", AVInt)
+  , ("Engine.PlayerReplicationInfo:Team", AVFlaggedInt)
+  , ("Engine.PlayerReplicationInfo:UniqueId", AVUniqueId)
+  , ("Engine.PlayerReplicationInfo:bBot", AVBoolean)
+  , ("Engine.PlayerReplicationInfo:bIsSpectator", AVBoolean)
+  , ("Engine.PlayerReplicationInfo:bReadyToPlay", AVBoolean)
+  , ("Engine.PlayerReplicationInfo:bWaitingPlayer", AVBoolean)
+  , ("Engine.TeamInfo:Score", AVInt)
+  , ("ProjectX.GRI_X:GameServerID", AVQWord)
+  , ("ProjectX.GRI_X:ReplicatedGameMutatorIndex", AVInt)
+  , ("ProjectX.GRI_X:ReplicatedGamePlaylist", AVInt)
+  , ("ProjectX.GRI_X:Reservations", AVReservation)
+  , ("ProjectX.GRI_X:bGameStarted", AVBoolean)
+  , ("TAGame.Ball_TA:GameEvent", AVFlaggedInt)
+  , ("TAGame.Ball_TA:HitTeamNum", AVByte)
+  , ("TAGame.Ball_TA:ReplicatedAddedCarBounceScale", AVFloat)
+  , ("TAGame.Ball_TA:ReplicatedBallMaxLinearSpeedScale", AVFloat)
+  , ("TAGame.Ball_TA:ReplicatedBallScale", AVFloat)
+  , ("TAGame.Ball_TA:ReplicatedExplosionData", AVExplosion)
+  , ("TAGame.Ball_TA:ReplicatedWorldBounceScale", AVFloat)
+  , ("TAGame.CameraSettingsActor_TA:CameraPitch", AVByte)
+  , ("TAGame.CameraSettingsActor_TA:CameraYaw", AVByte)
+  , ("TAGame.CameraSettingsActor_TA:PRI", AVFlaggedInt)
+  , ("TAGame.CameraSettingsActor_TA:ProfileSettings", AVCamSettings)
+  , ("TAGame.CameraSettingsActor_TA:bUsingBehindView", AVBoolean)
+  , ("TAGame.CameraSettingsActor_TA:bUsingSecondaryCamera", AVBoolean)
+  , ("TAGame.CarComponent_Boost_TA:BoostModifier", AVFloat)
+  , ("TAGame.CarComponent_Boost_TA:RechargeDelay", AVFloat)
+  , ("TAGame.CarComponent_Boost_TA:RechargeRate", AVFloat)
+  , ("TAGame.CarComponent_Boost_TA:ReplicatedBoostAmount", AVByte)
+  , ("TAGame.CarComponent_Boost_TA:UnlimitedBoostRefCount", AVInt)
+  , ("TAGame.CarComponent_Boost_TA:bNoBoost", AVBoolean)
+  , ("TAGame.CarComponent_Boost_TA:bUnlimitedBoost", AVBoolean)
+  , ("TAGame.CarComponent_Dodge_TA:DodgeTorque", AVLocation)
+  , ("TAGame.CarComponent_FlipCar_TA:FlipCarTime", AVFloat)
+  , ("TAGame.CarComponent_FlipCar_TA:bFlipRight", AVBoolean)
+  , ("TAGame.CarComponent_TA:ReplicatedActive", AVByte)
+  , ("TAGame.CarComponent_TA:ReplicatedActivityTime", AVFloat)
+  , ("TAGame.CarComponent_TA:Vehicle", AVFlaggedInt)
+  , ("TAGame.Car_TA:AddedBallForceMultiplier", AVFloat)
+  , ("TAGame.Car_TA:AddedCarForceMultiplier", AVFloat)
+  , ("TAGame.Car_TA:AttachedPickup", AVFlaggedInt)
+  , ("TAGame.Car_TA:ReplicatedDemolish", AVDemolish)
+  , ("TAGame.Car_TA:TeamPaint", AVTeamPaint)
+  , ("TAGame.CrowdActor_TA:GameEvent", AVFlaggedInt)
+  , ("TAGame.CrowdActor_TA:ModifiedNoise", AVFloat)
+  , ("TAGame.CrowdActor_TA:ReplicatedCountDownNumber", AVInt)
+  , ("TAGame.CrowdActor_TA:ReplicatedOneShotSound", AVFlaggedInt)
+  , ("TAGame.CrowdActor_TA:ReplicatedRoundCountDownNumber", AVInt)
+  , ("TAGame.CrowdManager_TA:GameEvent", AVFlaggedInt)
+  , ("TAGame.CrowdManager_TA:ReplicatedGlobalOneShotSound", AVFlaggedInt)
+  , ("TAGame.GRI_TA:NewDedicatedServerIP", AVString)
+  , ("TAGame.GameEvent_SoccarPrivate_TA:MatchSettings", AVPrivateMatchSettings)
+  , ("TAGame.GameEvent_Soccar_TA:ReplicatedMusicStinger", AVMusicStinger)
+  , ("TAGame.GameEvent_Soccar_TA:ReplicatedScoredOnTeam", AVByte)
+  , ("TAGame.GameEvent_Soccar_TA:RoundNum", AVInt)
+  , ("TAGame.GameEvent_Soccar_TA:SecondsRemaining", AVInt)
+  , ("TAGame.GameEvent_Soccar_TA:SubRulesArchetype", AVFlaggedInt)
+  , ("TAGame.GameEvent_Soccar_TA:bBallHasBeenHit", AVBoolean)
+  , ("TAGame.GameEvent_Soccar_TA:bOverTime", AVBoolean)
+  , ("TAGame.GameEvent_TA:BotSkill", AVInt)
+  , ("TAGame.GameEvent_TA:GameMode", AVGameMode)
+  , ("TAGame.GameEvent_TA:MatchTypeClass", AVFlaggedInt)
+  , ("TAGame.GameEvent_TA:ReplicatedGameStateTimeRemaining", AVInt)
+  , ("TAGame.GameEvent_TA:ReplicatedStateIndex", AVByte)
+  , ("TAGame.GameEvent_TA:ReplicatedStateName", AVInt)
+  , ("TAGame.GameEvent_TA:bCanVoteToForfeit", AVBoolean)
+  , ("TAGame.GameEvent_TA:bHasLeaveMatchPenalty", AVBoolean)
+  , ("TAGame.GameEvent_Team_TA:MaxTeamSize", AVInt)
+  , ("TAGame.PRI_TA:CameraPitch", AVByte)
+  , ("TAGame.PRI_TA:CameraSettings", AVCamSettings)
+  , ("TAGame.PRI_TA:CameraYaw", AVByte)
+  , ("TAGame.PRI_TA:ClientLoadout", AVLoadout)
+  , ("TAGame.PRI_TA:ClientLoadoutOnline", AVLoadoutOnline)
+  , ("TAGame.PRI_TA:ClientLoadouts", AVLoadouts)
+  , ("TAGame.PRI_TA:ClientLoadoutsOnline", AVLoadoutsOnline)
+  , ("TAGame.PRI_TA:MatchAssists", AVInt)
+  , ("TAGame.PRI_TA:MatchGoals", AVInt)
+  , ("TAGame.PRI_TA:MatchSaves", AVInt)
+  , ("TAGame.PRI_TA:MatchScore", AVInt)
+  , ("TAGame.PRI_TA:MatchShots", AVInt)
+  , ("TAGame.PRI_TA:PartyLeader", AVPartyLeader)
+  , ("TAGame.PRI_TA:PawnType", AVByte)
+  , ("TAGame.PRI_TA:PersistentCamera", AVFlaggedInt)
+  , ("TAGame.PRI_TA:ReplicatedGameEvent", AVFlaggedInt)
+  , ("TAGame.PRI_TA:Title", AVInt)
+  , ("TAGame.PRI_TA:TotalXP", AVInt)
+  , ("TAGame.PRI_TA:bIsInSplitScreen", AVBoolean)
+  , ("TAGame.PRI_TA:bMatchMVP", AVBoolean)
+  , ("TAGame.PRI_TA:bOnlineLoadoutSet", AVBoolean)
+  , ("TAGame.PRI_TA:bOnlineLoadoutsSet", AVBoolean)
+  , ("TAGame.PRI_TA:bReady", AVBoolean)
+  , ("TAGame.PRI_TA:bUsingBehindView", AVBoolean)
+  , ("TAGame.PRI_TA:bUsingSecondaryCamera", AVBoolean)
+  , ("TAGame.RBActor_TA:ReplicatedRBState", AVRigidBodyState)
+  , ("TAGame.RBActor_TA:WeldedInfo", AVWeldedInfo)
+  , ("TAGame.RBActor_TA:bFrozen", AVBoolean)
+  , ("TAGame.RBActor_TA:bIgnoreSyncing", AVBoolean)
+  , ("TAGame.RBActor_TA:bReplayActor", AVBoolean)
+  , ("TAGame.SpecialPickup_BallFreeze_TA:RepOrigSpeed", AVFloat)
+  , ("TAGame.SpecialPickup_BallVelcro_TA:AttachTime", AVFloat)
+  , ("TAGame.SpecialPickup_BallVelcro_TA:BreakTime", AVFloat)
+  , ("TAGame.SpecialPickup_BallVelcro_TA:bBroken", AVBoolean)
+  , ("TAGame.SpecialPickup_BallVelcro_TA:bHit", AVBoolean)
+  , ("TAGame.SpecialPickup_Targeted_TA:Targeted", AVFlaggedInt)
+  , ("TAGame.Team_Soccar_TA:GameScore", AVInt)
+  , ("TAGame.Team_TA:CustomTeamName", AVString)
+  , ("TAGame.Team_TA:GameEvent", AVFlaggedInt)
+  , ("TAGame.Team_TA:LogoData", AVFlaggedInt)
+  , ("TAGame.VehiclePickup_TA:ReplicatedPickupData", AVPickup)
+  , ("TAGame.Vehicle_TA:ReplicatedSteer", AVByte)
+  , ("TAGame.Vehicle_TA:ReplicatedThrottle", AVByte)
+  , ("TAGame.Vehicle_TA:bDriving", AVBoolean)
+  , ("TAGame.Vehicle_TA:bReplicatedHandbrake", AVBoolean)
+  ]
+
+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/Dictionary.hs b/library/Rattletrap/Dictionary.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Dictionary.hs
@@ -0,0 +1,33 @@
+module Rattletrap.Dictionary where
+
+import Rattletrap.Text
+
+import qualified Data.Binary as Binary
+
+newtype Dictionary a = Dictionary
+  { dictionaryValue :: [(Text, Maybe a)]
+  } deriving (Eq, Ord, Show)
+
+getDictionary :: Binary.Get a -> Binary.Get (Dictionary a)
+getDictionary getValue = do
+  key <- getText
+  if isNoneKey key
+    then pure (Dictionary [(key, Nothing)])
+    else do
+      value <- getValue
+      let element = (key, Just value)
+      Dictionary elements <- getDictionary getValue
+      pure (Dictionary (element : elements))
+
+putDictionary :: (a -> Binary.Put) -> Dictionary a -> Binary.Put
+putDictionary putValue (Dictionary elements) =
+  mapM_
+    (\(key, maybeValue) -> do
+       putText key
+       case maybeValue of
+         Nothing -> pure ()
+         Just value -> putValue value)
+    elements
+
+isNoneKey :: Text -> Bool
+isNoneKey text = filter (/= '\x00') (textToString text) == "None"
diff --git a/library/Rattletrap/Float32.hs b/library/Rattletrap/Float32.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Float32.hs
@@ -0,0 +1,33 @@
+module Rattletrap.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/Frame.hs b/library/Rattletrap/Frame.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Frame.hs
@@ -0,0 +1,61 @@
+module Rattletrap.Frame where
+
+import Rattletrap.ActorMap
+import Rattletrap.ClassAttributeMap
+import Rattletrap.Float32
+import Rattletrap.Replication
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data Frame = Frame
+  { frameTime :: Float32
+  , frameDelta :: Float32
+  , frameReplications :: [Replication]
+  } deriving (Eq, Ord, Show)
+
+getFrames
+  :: (Int, Int)
+  -> Int
+  -> ClassAttributeMap
+  -> ActorMap
+  -> BinaryBit.BitGet ([Frame], ActorMap)
+getFrames version numFrames classAttributeMap actorMap = do
+  maybeFrame <-
+    if numFrames > 0
+      then getFrame version classAttributeMap actorMap
+      else pure Nothing
+  case maybeFrame of
+    Nothing -> pure ([], actorMap)
+    Just (frame, newActorMap) -> do
+      (frames, newerActorMap) <-
+        getFrames version (numFrames - 1) classAttributeMap newActorMap
+      pure (frame : frames, newerActorMap)
+
+putFrames :: [Frame] -> BinaryBit.BitPut ()
+putFrames = mapM_ putFrame
+
+getFrame
+  :: (Int, Int)
+  -> ClassAttributeMap
+  -> ActorMap
+  -> BinaryBit.BitGet (Maybe (Frame, ActorMap))
+getFrame version classAttributeMap actorMap = do
+  time <- getFloat32Bits
+  delta <- getFloat32Bits
+  (replications, newActorMap) <-
+    getReplications version classAttributeMap actorMap
+  pure
+    (Just
+       ( Frame
+         { frameTime = time
+         , frameDelta = delta
+         , frameReplications = 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
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Header.hs
@@ -0,0 +1,36 @@
+module Rattletrap.Header where
+
+import Rattletrap.Dictionary
+import Rattletrap.Property
+import Rattletrap.Text
+import Rattletrap.Word32
+
+import qualified Data.Binary as Binary
+
+data Header = Header
+  { headerEngineVersion :: Word32
+  , headerLicenseeVersion :: Word32
+  , headerLabel :: Text
+  , headerProperties :: Dictionary Property
+  } deriving (Eq, Ord, Show)
+
+getHeader :: Binary.Get Header
+getHeader = do
+  engineVersion <- getWord32
+  licenseeVersion <- getWord32
+  label <- getText
+  properties <- getDictionary getProperty
+  pure
+    Header
+    { headerEngineVersion = engineVersion
+    , headerLicenseeVersion = licenseeVersion
+    , headerLabel = label
+    , headerProperties = properties
+    }
+
+putHeader :: Header -> Binary.Put
+putHeader header = do
+  putWord32 (headerEngineVersion header)
+  putWord32 (headerLicenseeVersion header)
+  putText (headerLabel header)
+  putDictionary putProperty (headerProperties header)
diff --git a/library/Rattletrap/Initialization.hs b/library/Rattletrap/Initialization.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Initialization.hs
@@ -0,0 +1,39 @@
+module Rattletrap.Initialization where
+
+import Rattletrap.Int8Vector
+import Rattletrap.Vector
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data Initialization = Initialization
+  { initializationLocation :: Maybe Vector
+  , initializationRotation :: Maybe Int8Vector
+  } 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
+    {initializationLocation = location, initializationRotation = 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/Int32.hs b/library/Rattletrap/Int32.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Int32.hs
@@ -0,0 +1,33 @@
+module Rattletrap.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 int32) = Binary.putInt32le 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/Int8.hs b/library/Rattletrap/Int8.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Int8.hs
@@ -0,0 +1,32 @@
+module Rattletrap.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/Int8Vector.hs b/library/Rattletrap/Int8Vector.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Int8Vector.hs
@@ -0,0 +1,42 @@
+module Rattletrap.Int8Vector where
+
+import Rattletrap.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 {int8VectorX = x, int8VectorY = y, int8VectorZ = 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/KeyFrame.hs b/library/Rattletrap/KeyFrame.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/KeyFrame.hs
@@ -0,0 +1,27 @@
+module Rattletrap.KeyFrame where
+
+import Rattletrap.Float32
+import Rattletrap.Word32
+
+import qualified Data.Binary as Binary
+
+data KeyFrame = KeyFrame
+  { keyFrameTime :: Float32
+  , keyFrameFrame :: Word32
+  , keyFramePosition :: Word32
+  } deriving (Eq, Ord, Show)
+
+getKeyFrame :: Binary.Get KeyFrame
+getKeyFrame = do
+  time <- getFloat32
+  frame <- getWord32
+  position <- getWord32
+  pure
+    KeyFrame
+    {keyFrameTime = time, keyFrameFrame = frame, keyFramePosition = position}
+
+putKeyFrame :: KeyFrame -> Binary.Put
+putKeyFrame keyFrame = do
+  putFloat32 (keyFrameTime keyFrame)
+  putWord32 (keyFrameFrame keyFrame)
+  putWord32 (keyFramePosition keyFrame)
diff --git a/library/Rattletrap/List.hs b/library/Rattletrap/List.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/List.hs
@@ -0,0 +1,21 @@
+module Rattletrap.List where
+
+import Rattletrap.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 elements) = do
+  putWord32 (Word32 (fromIntegral (length elements)))
+  mapM_ putElement elements
diff --git a/library/Rattletrap/Mark.hs b/library/Rattletrap/Mark.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Mark.hs
@@ -0,0 +1,22 @@
+module Rattletrap.Mark where
+
+import Rattletrap.Text
+import Rattletrap.Word32
+
+import qualified Data.Binary as Binary
+
+data Mark = Mark
+  { markValue :: Text
+  , markFrame :: Word32
+  } deriving (Eq, Ord, Show)
+
+getMark :: Binary.Get Mark
+getMark = do
+  value <- getText
+  frame <- getWord32
+  pure Mark {markValue = value, markFrame = 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
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Message.hs
@@ -0,0 +1,25 @@
+module Rattletrap.Message where
+
+import Rattletrap.Text
+import Rattletrap.Word32
+
+import qualified Data.Binary as Binary
+
+data Message = Message
+  { messageFrame :: Word32
+  , messageName :: Text
+  , messageValue :: Text
+  } deriving (Eq, Ord, Show)
+
+getMessage :: Binary.Get Message
+getMessage = do
+  frame <- getWord32
+  name <- getText
+  value <- getText
+  pure Message {messageFrame = frame, messageName = name, messageValue = value}
+
+putMessage :: Message -> Binary.Put
+putMessage message = do
+  putWord32 (messageFrame message)
+  putText (messageName message)
+  putText (messageValue message)
diff --git a/library/Rattletrap/Property.hs b/library/Rattletrap/Property.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Property.hs
@@ -0,0 +1,27 @@
+module Rattletrap.Property where
+
+import Rattletrap.PropertyValue
+import Rattletrap.Text
+import Rattletrap.Word64
+
+import qualified Data.Binary as Binary
+
+data Property = Property
+  { propertyKind :: Text
+  , propertySize :: Word64
+  , propertyValue :: PropertyValue Property
+  } deriving (Eq, Ord, Show)
+
+getProperty :: Binary.Get Property
+getProperty = do
+  kind <- getText
+  size <- getWord64
+  value <- getPropertyValue getProperty kind
+  pure
+    Property {propertyKind = kind, propertySize = size, propertyValue = 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
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/PropertyValue.hs
@@ -0,0 +1,74 @@
+module Rattletrap.PropertyValue where
+
+import Rattletrap.Dictionary
+import Rattletrap.Float32
+import Rattletrap.Int32
+import Rattletrap.List
+import Rattletrap.Text
+import Rattletrap.Word64
+import Rattletrap.Word8
+
+import qualified Data.Binary as Binary
+
+data PropertyValue a
+  = ArrayProperty (List (Dictionary a))
+  | BoolProperty Word8
+  | ByteProperty Text
+                 (Maybe Text)
+  | FloatProperty Float32
+  | IntProperty Int32
+  | NameProperty Text
+  | 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
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/RemoteId.hs
@@ -0,0 +1,43 @@
+module Rattletrap.RemoteId where
+
+import Rattletrap.Word64
+import Rattletrap.Word8
+
+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.Word as Word
+
+data RemoteId
+  = PlayStationId [Word.Word8]
+  | SplitscreenId [Word.Word8]
+  | SteamId Word64
+  | XboxId Word64
+  deriving (Eq, Ord, Show)
+
+getRemoteId :: Word8 -> BinaryBit.BitGet RemoteId
+getRemoteId systemId =
+  case word8Value systemId of
+    0 -> do
+      bytes <- BinaryBit.getLazyByteString 3
+      pure (SplitscreenId (ByteString.unpack bytes))
+    1 -> do
+      word64 <- getWord64Bits
+      pure (SteamId word64)
+    2 -> do
+      bytes <- BinaryBit.getLazyByteString 32
+      pure (PlayStationId (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 bytes ->
+      BinaryBit.putByteString (ByteString.toStrict (ByteString.pack bytes))
+    SplitscreenId bytes ->
+      BinaryBit.putByteString (ByteString.toStrict (ByteString.pack bytes))
+    SteamId word64 -> putWord64Bits word64
+    XboxId word64 -> putWord64Bits word64
diff --git a/library/Rattletrap/Replay.hs b/library/Rattletrap/Replay.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Replay.hs
@@ -0,0 +1,55 @@
+module Rattletrap.Replay where
+
+import Rattletrap.Content
+import Rattletrap.Crc
+import Rattletrap.Dictionary
+import Rattletrap.Header
+import Rattletrap.Int32
+import Rattletrap.Property
+import Rattletrap.PropertyValue
+import Rattletrap.Text
+import Rattletrap.Word32
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as ByteString
+
+data Replay = Replay
+  { replayHeader :: Header
+  , replayContent :: Content
+  } deriving (Eq, Ord, Show)
+
+getReplay :: Binary.Get Replay
+getReplay = do
+  _headerSize <- getWord32
+  _headerCrc <- getWord32
+  header <- getHeader
+  _contentSize <- getWord32
+  _contentCrc <- getWord32
+  let majorVersion = fromIntegral (word32Value (headerEngineVersion header))
+  let minorVersion = fromIntegral (word32Value (headerLicenseeVersion header))
+  let version = (majorVersion, minorVersion)
+  let numFrames =
+        case lookup
+               (stringToText "NumFrames")
+               (dictionaryValue (headerProperties header)) of
+          Just (Just (Property _ _ (IntProperty int32))) ->
+            fromIntegral (int32Value int32)
+          _ -> 0
+  content <- getContent version numFrames
+  pure Replay {replayHeader = header, replayContent = content}
+
+putReplay :: Replay -> Binary.Put
+putReplay replay = do
+  let header = Binary.runPut (putHeader (replayHeader replay))
+  let headerSize = ByteString.length header
+  let headerCrc = getCrc32 header
+  putWord32 (Word32 (fromIntegral headerSize))
+  putWord32 (Word32 headerCrc)
+  Binary.putLazyByteString header
+  let content = Binary.runPut (putContent (replayContent replay))
+  let contentSize = ByteString.length content
+  let contentCrc = getCrc32 content
+  putWord32 (Word32 (fromIntegral contentSize))
+  putWord32 (Word32 contentCrc)
+  Binary.putLazyByteString content
diff --git a/library/Rattletrap/Replication.hs b/library/Rattletrap/Replication.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Replication.hs
@@ -0,0 +1,61 @@
+module Rattletrap.Replication where
+
+import Rattletrap.ActorMap
+import Rattletrap.ClassAttributeMap
+import Rattletrap.CompressedWord
+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)
+  -> ClassAttributeMap
+  -> ActorMap
+  -> BinaryBit.BitGet ([Replication], ActorMap)
+getReplications version classAttributeMap actorMap = do
+  maybeReplication <- getReplication version classAttributeMap actorMap
+  case maybeReplication of
+    Nothing -> pure ([], actorMap)
+    Just (replication, newActorMap) -> do
+      (replications, newerActorMap) <-
+        getReplications version classAttributeMap newActorMap
+      pure (replication : replications, newerActorMap)
+
+putReplications :: [Replication] -> BinaryBit.BitPut ()
+putReplications replications = do
+  mapM_ putReplication replications
+  BinaryBit.putBool False
+
+getReplication
+  :: (Int, Int)
+  -> ClassAttributeMap
+  -> ActorMap
+  -> BinaryBit.BitGet (Maybe (Replication, ActorMap))
+getReplication version classAttributeMap actorMap = do
+  hasReplication <- BinaryBit.getBool
+  if not hasReplication
+    then pure Nothing
+    else do
+      actorId <- getCompressedWord maxActorId
+      (value, newActorMap) <-
+        getReplicationValue version classAttributeMap actorMap actorId
+      pure
+        (Just
+           ( Replication
+             {replicationActorId = actorId, replicationValue = value}
+           , newActorMap))
+
+putReplication :: Replication -> BinaryBit.BitPut ()
+putReplication replication = do
+  BinaryBit.putBool True
+  putCompressedWord (replicationActorId replication)
+  putReplicationValue (replicationValue replication)
+
+maxActorId :: Word
+maxActorId = 1023
diff --git a/library/Rattletrap/ReplicationValue.hs b/library/Rattletrap/ReplicationValue.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/ReplicationValue.hs
@@ -0,0 +1,64 @@
+module Rattletrap.ReplicationValue
+  ( module Rattletrap.ReplicationValue
+  , module Export
+  ) where
+
+import Rattletrap.ReplicationValue.Destroyed as Export
+import Rattletrap.ReplicationValue.Spawned as Export
+import Rattletrap.ReplicationValue.Updated as Export
+
+import Rattletrap.ActorMap
+import Rattletrap.ClassAttributeMap
+import Rattletrap.CompressedWord
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data ReplicationValue
+  = SpawnedReplication SpawnedReplicationValue
+  | UpdatedReplication UpdatedReplicationValue
+  | DestroyedReplication DestroyedReplicationValue
+  deriving (Eq, Ord, Show)
+
+getReplicationValue
+  :: (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) <-
+            getSpawnedReplicationValue classAttributeMap actorMap actorId
+          pure (SpawnedReplication x, newActorMap)
+        else do
+          x <-
+            getUpdatedReplicationValue
+              version
+              classAttributeMap
+              actorMap
+              actorId
+          pure (UpdatedReplication x, actorMap)
+    else do
+      x <- getDestroyedReplicationValue
+      pure (DestroyedReplication x, actorMap)
+
+putReplicationValue :: ReplicationValue -> BinaryBit.BitPut ()
+putReplicationValue value =
+  case value of
+    SpawnedReplication x -> do
+      BinaryBit.putBool True
+      BinaryBit.putBool True
+      putSpawnedReplicationValue x
+    UpdatedReplication x -> do
+      BinaryBit.putBool True
+      BinaryBit.putBool False
+      putUpdatedReplicationValue x
+    DestroyedReplication x -> do
+      BinaryBit.putBool False
+      putDestroyedReplicationValue x
diff --git a/library/Rattletrap/ReplicationValue/Destroyed.hs b/library/Rattletrap/ReplicationValue/Destroyed.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/ReplicationValue/Destroyed.hs
@@ -0,0 +1,14 @@
+module Rattletrap.ReplicationValue.Destroyed where
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data DestroyedReplicationValue =
+  DestroyedReplicationValue
+  deriving (Eq, Ord, Show)
+
+getDestroyedReplicationValue :: BinaryBit.BitGet DestroyedReplicationValue
+getDestroyedReplicationValue = pure DestroyedReplicationValue
+
+putDestroyedReplicationValue :: DestroyedReplicationValue -> BinaryBit.BitPut ()
+putDestroyedReplicationValue DestroyedReplicationValue = pure ()
diff --git a/library/Rattletrap/ReplicationValue/Spawned.hs b/library/Rattletrap/ReplicationValue/Spawned.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/ReplicationValue/Spawned.hs
@@ -0,0 +1,45 @@
+module Rattletrap.ReplicationValue.Spawned where
+
+import Rattletrap.ActorMap
+import Rattletrap.ClassAttributeMap
+import Rattletrap.CompressedWord
+import Rattletrap.Initialization
+import Rattletrap.Word32
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data SpawnedReplicationValue = SpawnedReplicationValue
+  { spawnedReplicationValueFlag :: Bool
+  , spawnedReplicationValueObjectId :: Word32
+  , spawnedReplicationValueInitialization :: Initialization
+  } deriving (Eq, Ord, Show)
+
+getSpawnedReplicationValue
+  :: ClassAttributeMap
+  -> ActorMap
+  -> CompressedWord
+  -> BinaryBit.BitGet (SpawnedReplicationValue, ActorMap)
+getSpawnedReplicationValue classAttributeMap actorMap actorId = do
+  flag <- BinaryBit.getBool
+  objectId <- getWord32Bits
+  let newActorMap = updateActorMap actorId objectId actorMap
+  case getObjectName classAttributeMap objectId of
+    Nothing -> fail ("could not get object name for id " ++ show objectId)
+    Just objectName ->
+      case getClassName objectName of
+        Nothing ->
+          fail ("could not get class name for object " ++ show objectName)
+        Just className -> do
+          let hasLocation = classHasLocation className
+          let hasRotation = classHasRotation className
+          initialization <- getInitialization hasLocation hasRotation
+          pure
+            (SpawnedReplicationValue flag objectId initialization, newActorMap)
+
+putSpawnedReplicationValue :: SpawnedReplicationValue -> BinaryBit.BitPut ()
+putSpawnedReplicationValue spawnedReplicationValue = do
+  BinaryBit.putBool (spawnedReplicationValueFlag spawnedReplicationValue)
+  putWord32Bits (spawnedReplicationValueObjectId spawnedReplicationValue)
+  putInitialization
+    (spawnedReplicationValueInitialization spawnedReplicationValue)
diff --git a/library/Rattletrap/ReplicationValue/Updated.hs b/library/Rattletrap/ReplicationValue/Updated.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/ReplicationValue/Updated.hs
@@ -0,0 +1,27 @@
+module Rattletrap.ReplicationValue.Updated where
+
+import Rattletrap.ActorMap
+import Rattletrap.Attribute
+import Rattletrap.ClassAttributeMap
+import Rattletrap.CompressedWord
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data UpdatedReplicationValue = UpdatedReplicationValue
+  { updatedReplicationValueAttributes :: [Attribute]
+  } deriving (Eq, Ord, Show)
+
+getUpdatedReplicationValue
+  :: (Int, Int)
+  -> ClassAttributeMap
+  -> ActorMap
+  -> CompressedWord
+  -> BinaryBit.BitGet UpdatedReplicationValue
+getUpdatedReplicationValue version classAttributeMap actorMap actorId = do
+  attributes <- getAttributes version classAttributeMap actorMap actorId
+  pure (UpdatedReplicationValue attributes)
+
+putUpdatedReplicationValue :: UpdatedReplicationValue -> BinaryBit.BitPut ()
+putUpdatedReplicationValue updatedReplicationValue =
+  putAttributes (updatedReplicationValueAttributes updatedReplicationValue)
diff --git a/library/Rattletrap/Text.hs b/library/Rattletrap/Text.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Text.hs
@@ -0,0 +1,87 @@
+module Rattletrap.Text where
+
+import Rattletrap.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.Char8 as ByteString
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Encoding
+
+data Text = Text
+  { textSize :: Int32
+  , textValue :: Text.Text
+  } deriving (Eq, Ord, Show)
+
+getText :: Binary.Get Text
+getText = do
+  rawSize <- getInt32
+  let decode = getTextDecoder rawSize
+  let size = getTextSize rawSize
+  bytes <- Binary.getLazyByteString size
+  let text = decode bytes
+  pure (Text rawSize text)
+
+putText :: Text -> Binary.Put
+putText text = do
+  let size = textSize text
+  let encode = getTextEncoder size
+  putInt32 size
+  Binary.putLazyByteString (encode (textValue text))
+
+getTextBits :: BinaryBit.BitGet Text
+getTextBits = do
+  rawSize <- getInt32Bits
+  let decode = getTextDecoder rawSize
+  let size = getTextSize rawSize
+  bytes <- BinaryBit.getLazyByteString size
+  let text = decode (reverseBytes bytes)
+  pure (Text rawSize text)
+
+putTextBits :: Text -> BinaryBit.BitPut ()
+putTextBits text = do
+  let size = textSize text
+  let encode = getTextEncoder size
+  putInt32Bits size
+  BinaryBit.putByteString
+    (ByteString.toStrict (reverseBytes (encode (textValue text))))
+
+stringToText :: String -> Text
+stringToText string =
+  let value = Text.snoc (Text.pack string) '\x00'
+      size = Int32 (fromIntegral (Text.length value))
+  in Text size value
+
+textToString :: Text -> String
+textToString text = Text.unpack (Text.dropWhileEnd (== '\x00') (textValue text))
+
+getTextSize
+  :: Integral a
+  => Int32 -> a
+getTextSize (Int32 size) =
+  if size == 0x05000000
+    then 8
+    else if size < 0
+           then (-2 * fromIntegral size)
+           else fromIntegral size
+
+getTextDecoder :: Int32 -> ByteString.ByteString -> Text.Text
+getTextDecoder (Int32 size) bytes =
+  let decode =
+        if size < 0
+          then Encoding.decodeUtf16LE
+          else Encoding.decodeLatin1
+  in decode (ByteString.toStrict bytes)
+
+getTextEncoder :: Int32 -> Text.Text -> ByteString.ByteString
+getTextEncoder (Int32 size) text =
+  if size < 0
+    then ByteString.fromStrict (Encoding.encodeUtf16LE text)
+    else encodeLatin1 text
+
+encodeLatin1 :: Text.Text -> ByteString.ByteString
+encodeLatin1 text = ByteString.pack (Text.unpack text)
diff --git a/library/Rattletrap/Utility.hs b/library/Rattletrap/Utility.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Utility.hs
@@ -0,0 +1,33 @@
+module Rattletrap.Utility where
+
+import qualified Data.Bits as Bits
+import qualified Data.ByteString.Lazy as ByteString
+import qualified Data.Word as Word
+import qualified Text.Regex as Regex
+
+padBytes
+  :: Integral a
+  => a -> ByteString.ByteString -> ByteString.ByteString
+padBytes size bytes =
+  ByteString.concat
+    [ bytes
+    , ByteString.replicate (fromIntegral size - ByteString.length bytes) 0x00
+    ]
+
+replace :: String -> String -> String -> String
+replace needle replacement haystack =
+  Regex.subRegex (Regex.mkRegex needle) haystack replacement
+
+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/Vector.hs b/library/Rattletrap/Vector.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Vector.hs
@@ -0,0 +1,31 @@
+module Rattletrap.Vector where
+
+import Rattletrap.CompressedWord
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data Vector = Vector
+  { vectorBitSize :: CompressedWord
+  , vectorDx :: CompressedWord
+  , vectorDy :: CompressedWord
+  , vectorDz :: CompressedWord
+  } 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
+  pure
+    Vector
+    {vectorBitSize = bitSize, vectorDx = dx, vectorDy = dy, vectorDz = dz}
+
+putVector :: Vector -> BinaryBit.BitPut ()
+putVector vector = do
+  putCompressedWord (vectorBitSize vector)
+  putCompressedWord (vectorDx vector)
+  putCompressedWord (vectorDy vector)
+  putCompressedWord (vectorDz vector)
diff --git a/library/Rattletrap/Word32.hs b/library/Rattletrap/Word32.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Word32.hs
@@ -0,0 +1,33 @@
+module Rattletrap.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 word32) = Binary.putWord32le 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/Word64.hs b/library/Rattletrap/Word64.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Word64.hs
@@ -0,0 +1,33 @@
+module Rattletrap.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/Word8.hs b/library/Rattletrap/Word8.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Word8.hs
@@ -0,0 +1,33 @@
+module Rattletrap.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 word8) = Binary.putWord8 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/rattletrap.cabal b/rattletrap.cabal
new file mode 100644
--- /dev/null
+++ b/rattletrap.cabal
@@ -0,0 +1,140 @@
+name: rattletrap
+version: 0.1.0
+cabal-version: >=1.10
+build-type: Simple
+license: MIT
+license-file: LICENSE.markdown
+maintainer: Taylor Fausak
+synopsis: Parse and generate Rocket League replays.
+description:
+    Rattletrap parses and generates Rocket League replays.
+category: Game
+extra-source-files:
+    HLint.hs
+    README.markdown
+
+library
+    exposed-modules:
+        Rattletrap
+        Rattletrap.ActorMap
+        Rattletrap.Attribute
+        Rattletrap.AttributeMapping
+        Rattletrap.AttributeValue
+        Rattletrap.AttributeValue.Boolean
+        Rattletrap.AttributeValue.Byte
+        Rattletrap.AttributeValue.CamSettings
+        Rattletrap.AttributeValue.Demolish
+        Rattletrap.AttributeValue.Enum
+        Rattletrap.AttributeValue.Explosion
+        Rattletrap.AttributeValue.FlaggedInt
+        Rattletrap.AttributeValue.Float
+        Rattletrap.AttributeValue.GameMode
+        Rattletrap.AttributeValue.Int
+        Rattletrap.AttributeValue.Loadout
+        Rattletrap.AttributeValue.LoadoutOnline
+        Rattletrap.AttributeValue.Loadouts
+        Rattletrap.AttributeValue.LoadoutsOnline
+        Rattletrap.AttributeValue.Location
+        Rattletrap.AttributeValue.MusicStinger
+        Rattletrap.AttributeValue.PartyLeader
+        Rattletrap.AttributeValue.Pickup
+        Rattletrap.AttributeValue.PrivateMatchSettings
+        Rattletrap.AttributeValue.QWord
+        Rattletrap.AttributeValue.Reservation
+        Rattletrap.AttributeValue.RigidBodyState
+        Rattletrap.AttributeValue.String
+        Rattletrap.AttributeValue.TeamPaint
+        Rattletrap.AttributeValue.UniqueId
+        Rattletrap.AttributeValue.WeldedInfo
+        Rattletrap.AttributeValueType
+        Rattletrap.Cache
+        Rattletrap.ClassAttributeMap
+        Rattletrap.ClassMapping
+        Rattletrap.CompressedWord
+        Rattletrap.CompressedWordVector
+        Rattletrap.Content
+        Rattletrap.Crc
+        Rattletrap.Data
+        Rattletrap.Dictionary
+        Rattletrap.Float32
+        Rattletrap.Frame
+        Rattletrap.Header
+        Rattletrap.Initialization
+        Rattletrap.Int32
+        Rattletrap.Int8
+        Rattletrap.Int8Vector
+        Rattletrap.KeyFrame
+        Rattletrap.List
+        Rattletrap.Mark
+        Rattletrap.Message
+        Rattletrap.Property
+        Rattletrap.PropertyValue
+        Rattletrap.RemoteId
+        Rattletrap.Replay
+        Rattletrap.Replication
+        Rattletrap.ReplicationValue
+        Rattletrap.ReplicationValue.Destroyed
+        Rattletrap.ReplicationValue.Spawned
+        Rattletrap.ReplicationValue.Updated
+        Rattletrap.Text
+        Rattletrap.Utility
+        Rattletrap.Vector
+        Rattletrap.Word32
+        Rattletrap.Word64
+        Rattletrap.Word8
+    build-depends:
+        base >=4.9.0.0 && <4.10,
+        bimap >=0.3.2 && <0.4,
+        binary >=0.8.3.0 && <0.9,
+        binary-bits ==0.5.*,
+        bytestring >=0.10.8.1 && <0.11,
+        containers >=0.5.7.1 && <0.6,
+        data-binary-ieee754 >=0.4.4 && <0.5,
+        regex-compat >=0.95.1 && <0.96,
+        text >=1.2.2.1 && <1.3,
+        vector >=0.11.0.0 && <0.12
+    default-language: Haskell2010
+    hs-source-dirs: library
+    ghc-options: -Wall
+
+executable rattletrap
+    main-is: Main.hs
+    build-depends:
+        aeson >=0.11.2.1 && <0.12,
+        aeson-casing >=0.1.0.5 && <0.2,
+        base >=4.9.0.0 && <4.10,
+        binary >=0.8.3.0 && <0.9,
+        bytestring >=0.10.8.1 && <0.11,
+        rattletrap >=0.1.0 && <0.2,
+        template-haskell >=2.11.0.0 && <2.12
+    default-language: Haskell2010
+    hs-source-dirs: executable
+    ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
+
+test-suite lint
+    type: exitcode-stdio-1.0
+    main-is: Lint.hs
+    build-depends:
+        base >=4.9.0.0 && <4.10,
+        hlint >=1.9.35 && <1.10
+    default-language: Haskell2010
+    hs-source-dirs: test
+    other-modules:
+        Test
+    ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
+test-suite test
+    type: exitcode-stdio-1.0
+    main-is: Test.hs
+    build-depends:
+        base >=4.9.0.0 && <4.10,
+        binary >=0.8.3.0 && <0.9,
+        bytestring >=0.10.8.1 && <0.11,
+        filepath >=1.4.1.0 && <1.5,
+        rattletrap >=0.1.0 && <0.2,
+        tasty >=0.11.0.4 && <0.12,
+        tasty-hspec >=1.1.3 && <1.2
+    default-language: Haskell2010
+    hs-source-dirs: test
+    other-modules:
+        Lint
+    ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
diff --git a/test/Lint.hs b/test/Lint.hs
new file mode 100644
--- /dev/null
+++ b/test/Lint.hs
@@ -0,0 +1,13 @@
+module Main
+  ( main
+  ) where
+
+import qualified Language.Haskell.HLint3 as HLint
+import qualified System.Exit as Exit
+
+main :: IO ()
+main = do
+  ideas <- HLint.hlint ["lint", "--cross", "."]
+  if null ideas
+    then Exit.exitSuccess
+    else Exit.exitFailure
diff --git a/test/Test.hs b/test/Test.hs
new file mode 100644
--- /dev/null
+++ b/test/Test.hs
@@ -0,0 +1,88 @@
+module Main
+  ( main
+  ) where
+
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as ByteString
+import qualified Rattletrap
+import qualified System.FilePath as FilePath
+import qualified Test.Tasty as Tasty
+import qualified Test.Tasty.Hspec as Hspec
+
+main :: IO ()
+main = do
+  tests <- Hspec.testSpec "rattletrap" (Hspec.parallel spec)
+  Tasty.defaultMain tests
+
+spec :: Hspec.Spec
+spec = Hspec.describe "Rattletrap" (mapM_ (uncurry itCanGetAndPut) replays)
+
+itCanGetAndPut :: String -> String -> Hspec.Spec
+itCanGetAndPut uuid description =
+  Hspec.it
+    (uuid ++ ": a replay with " ++ description)
+    (do let file = pathToReplay uuid
+        (input, _, output) <- getAndPut file
+        Hspec.shouldBe (output == input) True)
+
+pathToReplay :: String -> FilePath
+pathToReplay uuid =
+  FilePath.joinPath ["test", "replays", FilePath.addExtension uuid ".replay"]
+
+getAndPut :: FilePath
+          -> IO (ByteString.ByteString, Rattletrap.Replay, ByteString.ByteString)
+getAndPut file = do
+  input <- ByteString.readFile file
+  let replay = Binary.runGet Rattletrap.getReplay input
+  let output = Binary.runPut (Rattletrap.putReplay replay)
+  pure (input, replay, output)
+
+replays :: [(String, String)]
+replays =
+  [ ("00080014003600090000036E0F65CCEB", "a flip time")
+  , ("07E925B1423653D44CB8B4B2524792C1", "a game mode before Neo Tokyo")
+  , ("1205D96C4D819800927791820096CD49", "rumble mode")
+  , ("160CA83E41083BFD8E6315B4BFCA0561", "a dedicated server IP")
+  , ("18D6738D415B70B5BE4C299588D3C141", "an online loadout attribute")
+  , ("1A126AC24CAA0DB0E98835BD960B8AF8", "overtime")
+  , ("1BC2D01444ACE577D01E988EADD4DFD0", "no padding after the frames")
+  , ("1D1DE97D4941C86E43FE0093563DB621", "a camera pitch")
+  , ("1EF90FCC4F719F606A5327B3CDD782A4", "a private hoops match")
+  , ("211466D04B983F5A33CC2FA1D5928672", "a match save")
+  , ("22BACD794ABE7B92E50E9CBDBD9C59CE", "a vote to forfeit")
+  , ("29F582C34A65EB34D358A784CBE3C189", "frames")
+  , ("2F817C8C44859C762980AE85B1862A31", "splitscreen players")
+  , ("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")
+  , ("52AA67F94090C19D33C5009E54D31FE4", "a match-ending attribute")
+  , ("540DA764423C8FB24EB9D486D982F16F", "a demolish attribute")
+  , ("551CA4D44FF2B86015DE44A6B5790D4C", "private match settings")
+  , ("6688EEE34BFEB3EC3A9E3283098CC712", "a malformed byte property")
+  , ("6D1B06D844A5BB91B81FD4B5B28F08BA", "a flip right")
+  , ("7109EB9846D303E54B7ACBA792036213", "a boost modifier")
+  , ("7BF6073F4614CE0A438994B9A260DA6A", "an online loadouts attribute")
+  , ("89CBA30E46FA5385BDD35DA4285D4D2E", "remote user data")
+  , ("A128B3AB45D5A18E3EF9CF93C9576BCE", "a round count down")
+  , ("A52F804845573D8DA65E97BF59026A43", "some more mutators")
+  , ("A6711CE74272B2E663DCC9A200A218E3", "a waiting player")
+  , ("A7F001A1417A19BFA8C90990D8F7C2FF", "a ready attribute")
+  -- , ("B82DDB624C393A4A425E68AB40DC2450", "TODO")
+  , ("B9F9B87D4A9D0A3D25D4EC91C0401DE2", "a party leader")
+  , ("C14F7E0E4D9B5E6BE9AD5D8ED56B174C", "some mutators")
+  , ("C375E0EC4971B506C51678B465D35AE9", "some UTF-16 text")
+  , ("C80A9959479CFE51673B3A889D717554", "some Latin-1 text")
+  , ("C8372B1345B1803DEF039F815DBD802D", "a spectator")
+  , ("CB5B422A4424262B2877E9A121329EF2", "a player using behind view")
+  , ("D428F81646A98C25902CE988AE5C14C8", "a private hockey match")
+  , ("D7FB197A451D69075A0C99A2F49A4053", "an explosion attribute")
+  , ("DCB3A6B94A9DBE46FDE5EAA9B012F6C8", "a pawn type attribute")
+  , ("EAE311E84BA35B590A6FDBA6DD4F2FEB", "an actor/object ID collision")
+  , ("F299F176491554B11E34AB91CA76B2CE", "a location attribute")
+  , ("F811C1D24888015E23B598AD8628C742", "no frames")
+  , ("FDC79DA84DD463D4BCCE6B892829AC88", "an MVP")
+  ]
