diff --git a/HLint.hs b/HLint.hs
deleted file mode 100644
--- a/HLint.hs
+++ /dev/null
@@ -1,12 +0,0 @@
-module HLint where
-
-import "hint" HLint.Default
-import "hint" HLint.Dollar
-
-ignore "Eta reduce"
-
-ignore "Use &&&"
-
-ignore "Use <|>"
-
-ignore "Use uncurry"
diff --git a/executable/Main.hs b/executable/Main.hs
deleted file mode 100644
--- a/executable/Main.hs
+++ /dev/null
@@ -1,5 +0,0 @@
-module Main
-  ( module Rattletrap
-  ) where
-
-import Rattletrap (main)
diff --git a/library/Rattletrap.hs b/library/Rattletrap.hs
deleted file mode 100644
--- a/library/Rattletrap.hs
+++ /dev/null
@@ -1,58 +0,0 @@
-module Rattletrap
-  ( module Rattletrap.ActorMap
-  , module Rattletrap.Attribute
-  , module Rattletrap.AttributeMapping
-  , module Rattletrap.AttributeType
-  , module Rattletrap.AttributeValue
-  , module Rattletrap.Cache
-  , module Rattletrap.ClassAttributeMap
-  , module Rattletrap.ClassMapping
-  , module Rattletrap.Content
-  , module Rattletrap.Crc
-  , module Rattletrap.Data
-  , module Rattletrap.Frame
-  , module Rattletrap.Header
-  , module Rattletrap.Initialization
-  , module Rattletrap.KeyFrame
-  , module Rattletrap.Main
-  , module Rattletrap.Mark
-  , module Rattletrap.Message
-  , module Rattletrap.Primitive
-  , module Rattletrap.Property
-  , module Rattletrap.PropertyValue
-  , module Rattletrap.RemoteId
-  , module Rattletrap.Replay
-  , module Rattletrap.Replication
-  , module Rattletrap.ReplicationValue
-  , module Rattletrap.Utility
-  , module Rattletrap.Version
-  ) where
-
-import Rattletrap.ActorMap
-import Rattletrap.Attribute
-import Rattletrap.AttributeMapping
-import Rattletrap.AttributeType
-import Rattletrap.AttributeValue
-import Rattletrap.Cache
-import Rattletrap.ClassAttributeMap
-import Rattletrap.ClassMapping
-import Rattletrap.Content
-import Rattletrap.Crc
-import Rattletrap.Data
-import Rattletrap.Frame
-import Rattletrap.Header
-import Rattletrap.Initialization
-import Rattletrap.Json ()
-import Rattletrap.KeyFrame
-import Rattletrap.Main
-import Rattletrap.Mark
-import Rattletrap.Message
-import Rattletrap.Primitive
-import Rattletrap.Property
-import Rattletrap.PropertyValue
-import Rattletrap.RemoteId
-import Rattletrap.Replay
-import Rattletrap.Replication
-import Rattletrap.ReplicationValue
-import Rattletrap.Utility
-import Rattletrap.Version
diff --git a/library/Rattletrap/ActorMap.hs b/library/Rattletrap/ActorMap.hs
deleted file mode 100644
--- a/library/Rattletrap/ActorMap.hs
+++ /dev/null
@@ -1,13 +0,0 @@
-module Rattletrap.ActorMap where
-
-import Rattletrap.Primitive
-
-import qualified Data.Map.Strict as Map
-
-type ActorMap = Map.Map CompressedWord Word32
-
-makeActorMap :: ActorMap
-makeActorMap = Map.empty
-
-updateActorMap :: CompressedWord -> Word32 -> ActorMap -> ActorMap
-updateActorMap actorId objectId actorMap = Map.insert actorId objectId actorMap
diff --git a/library/Rattletrap/Attribute.hs b/library/Rattletrap/Attribute.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute.hs
+++ /dev/null
@@ -1,62 +0,0 @@
-module Rattletrap.Attribute where
-
-import Rattletrap.ActorMap
-import Rattletrap.AttributeValue
-import Rattletrap.ClassAttributeMap
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data Attribute = Attribute
-  { attributeId :: CompressedWord
-  , attribute_name :: Text
-  , 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 getAttributeMap classAttributeMap actorMap actorId of
-    Nothing -> fail ("could not get attribute map for " ++ show actorId)
-    Just attributeMap ->
-      case getAttributeIdLimit attributeMap of
-        Nothing ->
-          fail ("could not get attribute ID limit for " ++ show actorId)
-        Just limit -> do
-          id_ <- getCompressedWord limit
-          case getAttributeName classAttributeMap attributeMap id_ of
-            Nothing -> fail ("could not get attribute name for " ++ show id_)
-            Just name -> do
-              value <- getAttributeValue version name
-              pure (Attribute id_ name value)
-
-putAttribute :: Attribute -> BinaryBit.BitPut ()
-putAttribute attribute = do
-  BinaryBit.putBool True
-  putCompressedWord (attributeId attribute)
-  putAttributeValue (attributeValue attribute)
diff --git a/library/Rattletrap/Attribute/Boolean.hs b/library/Rattletrap/Attribute/Boolean.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Boolean.hs
+++ /dev/null
@@ -1,17 +0,0 @@
-module Rattletrap.Attribute.Boolean where
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype BooleanAttribute = BooleanAttribute
-  { booleanAttributeValue :: Bool
-  } deriving (Eq, Ord, Show)
-
-getBooleanAttribute :: BinaryBit.BitGet BooleanAttribute
-getBooleanAttribute = do
-  value <- BinaryBit.getBool
-  pure (BooleanAttribute value)
-
-putBooleanAttribute :: BooleanAttribute -> BinaryBit.BitPut ()
-putBooleanAttribute booleanAttribute =
-  BinaryBit.putBool (booleanAttributeValue booleanAttribute)
diff --git a/library/Rattletrap/Attribute/Byte.hs b/library/Rattletrap/Attribute/Byte.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Byte.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module Rattletrap.Attribute.Byte where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype ByteAttribute = ByteAttribute
-  { byteAttributeValue :: Word8
-  } deriving (Eq, Ord, Show)
-
-getByteAttribute :: BinaryBit.BitGet ByteAttribute
-getByteAttribute = do
-  value <- getWord8Bits
-  pure (ByteAttribute value)
-
-putByteAttribute :: ByteAttribute -> BinaryBit.BitPut ()
-putByteAttribute byteAttribute =
-  putWord8Bits (byteAttributeValue byteAttribute)
diff --git a/library/Rattletrap/Attribute/CamSettings.hs b/library/Rattletrap/Attribute/CamSettings.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/CamSettings.hs
+++ /dev/null
@@ -1,36 +0,0 @@
-module Rattletrap.Attribute.CamSettings where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data CamSettingsAttribute = CamSettingsAttribute
-  { camSettingsAttributeFov :: Float32
-  , camSettingsAttributeHeight :: Float32
-  , camSettingsAttributeAngle :: Float32
-  , camSettingsAttributeDistance :: Float32
-  , camSettingsAttributeStiffness :: Float32
-  , camSettingsAttributeSwivelSpeed :: Float32
-  } deriving (Eq, Ord, Show)
-
-getCamSettingsAttribute :: BinaryBit.BitGet CamSettingsAttribute
-getCamSettingsAttribute = do
-  fov <- getFloat32Bits
-  height <- getFloat32Bits
-  angle <- getFloat32Bits
-  distance <- getFloat32Bits
-  stiffness <- getFloat32Bits
-  swivelSpeed <- getFloat32Bits
-  pure
-    (CamSettingsAttribute fov height angle distance stiffness swivelSpeed)
-
-putCamSettingsAttribute :: CamSettingsAttribute -> BinaryBit.BitPut ()
-putCamSettingsAttribute camSettingsAttribute = do
-  putFloat32Bits (camSettingsAttributeFov camSettingsAttribute)
-  putFloat32Bits (camSettingsAttributeHeight camSettingsAttribute)
-  putFloat32Bits (camSettingsAttributeAngle camSettingsAttribute)
-  putFloat32Bits (camSettingsAttributeDistance camSettingsAttribute)
-  putFloat32Bits (camSettingsAttributeStiffness camSettingsAttribute)
-  putFloat32Bits
-    (camSettingsAttributeSwivelSpeed camSettingsAttribute)
diff --git a/library/Rattletrap/Attribute/ClubColors.hs b/library/Rattletrap/Attribute/ClubColors.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/ClubColors.hs
+++ /dev/null
@@ -1,29 +0,0 @@
-module Rattletrap.Attribute.ClubColors where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data ClubColorsAttribute = ClubColorsAttribute
-  { clubColorsAttributeBlueFlag :: Bool
-  , clubColorsAttributeBlueColor :: Word8
-  , clubColorsAttributeOrangeFlag :: Bool
-  , clubColorsAttributeOrangeColor :: Word8
-  } deriving (Eq, Ord, Show)
-
-getClubColorsAttribute :: BinaryBit.BitGet ClubColorsAttribute
-getClubColorsAttribute = do
-  blueFlag <- BinaryBit.getBool
-  blueColor <- getWord8Bits
-  orangeFlag <- BinaryBit.getBool
-  orangeColor <- getWord8Bits
-  pure (ClubColorsAttribute blueFlag blueColor orangeFlag orangeColor)
-
-putClubColorsAttribute :: ClubColorsAttribute -> BinaryBit.BitPut ()
-putClubColorsAttribute clubColorsAttribute = do
-  BinaryBit.putBool (clubColorsAttributeBlueFlag clubColorsAttribute)
-  putWord8Bits (clubColorsAttributeBlueColor clubColorsAttribute)
-  BinaryBit.putBool
-    (clubColorsAttributeOrangeFlag clubColorsAttribute)
-  putWord8Bits (clubColorsAttributeOrangeColor clubColorsAttribute)
diff --git a/library/Rattletrap/Attribute/Demolish.hs b/library/Rattletrap/Attribute/Demolish.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Demolish.hs
+++ /dev/null
@@ -1,41 +0,0 @@
-module Rattletrap.Attribute.Demolish where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data DemolishAttribute = DemolishAttribute
-  { demolishAttributeAttackerFlag :: Bool
-  , demolishAttributeAttackerActorId :: Word32
-  , demolishAttributeVictimFlag :: Bool
-  , demolishAttributeVictimActorId :: Word32
-  , demolishAttributeAttackerVelocity :: Vector
-  , demolishAttributeVictimVelocity :: Vector
-  } deriving (Eq, Ord, Show)
-
-getDemolishAttribute :: BinaryBit.BitGet DemolishAttribute
-getDemolishAttribute = do
-  attackerFlag <- BinaryBit.getBool
-  attackerActorId <- getWord32Bits
-  victimFlag <- BinaryBit.getBool
-  victimActorId <- getWord32Bits
-  attackerVelocity <- getVector
-  victimVelocity <- getVector
-  pure
-    (DemolishAttribute
-       attackerFlag
-       attackerActorId
-       victimFlag
-       victimActorId
-       attackerVelocity
-       victimVelocity)
-
-putDemolishAttribute :: DemolishAttribute -> BinaryBit.BitPut ()
-putDemolishAttribute demolishAttribute = do
-  BinaryBit.putBool (demolishAttributeAttackerFlag demolishAttribute)
-  putWord32Bits (demolishAttributeAttackerActorId demolishAttribute)
-  BinaryBit.putBool (demolishAttributeVictimFlag demolishAttribute)
-  putWord32Bits (demolishAttributeVictimActorId demolishAttribute)
-  putVector (demolishAttributeAttackerVelocity demolishAttribute)
-  putVector (demolishAttributeVictimVelocity demolishAttribute)
diff --git a/library/Rattletrap/Attribute/Enum.hs b/library/Rattletrap/Attribute/Enum.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Enum.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module Rattletrap.Attribute.Enum where
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Word as Word
-
-newtype EnumAttribute = EnumAttribute
-  { enumAttributeValue :: Word.Word16
-  } deriving (Eq, Ord, Show)
-
-getEnumAttribute :: BinaryBit.BitGet EnumAttribute
-getEnumAttribute = do
-  value <- BinaryBit.getWord16be 11
-  pure (EnumAttribute value)
-
-putEnumAttribute :: EnumAttribute -> BinaryBit.BitPut ()
-putEnumAttribute enumAttribute =
-  BinaryBit.putWord16be 11 (enumAttributeValue enumAttribute)
diff --git a/library/Rattletrap/Attribute/Explosion.hs b/library/Rattletrap/Attribute/Explosion.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Explosion.hs
+++ /dev/null
@@ -1,24 +0,0 @@
-module Rattletrap.Attribute.Explosion where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data ExplosionAttribute = ExplosionAttribute
-  { explosionAttributeActorId :: Int32
-  , explosionAttributeLocation :: Vector
-  } deriving (Eq, Ord, Show)
-
-getExplosionAttribute :: BinaryBit.BitGet ExplosionAttribute
-getExplosionAttribute = do
-  False <- BinaryBit.getBool
-  actorId <- getInt32Bits
-  location <- getVector
-  pure (ExplosionAttribute actorId location)
-
-putExplosionAttribute :: ExplosionAttribute -> BinaryBit.BitPut ()
-putExplosionAttribute explosionAttribute = do
-  BinaryBit.putBool False
-  putInt32Bits (explosionAttributeActorId explosionAttribute)
-  putVector (explosionAttributeLocation explosionAttribute)
diff --git a/library/Rattletrap/Attribute/FlaggedInt.hs b/library/Rattletrap/Attribute/FlaggedInt.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/FlaggedInt.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-module Rattletrap.Attribute.FlaggedInt where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data FlaggedIntAttribute = FlaggedIntAttribute
-  { flaggedIntAttributeFlag :: Bool
-  , flaggedIntAttributeInt :: Int32
-  } deriving (Eq, Ord, Show)
-
-getFlaggedIntAttribute :: BinaryBit.BitGet FlaggedIntAttribute
-getFlaggedIntAttribute = do
-  flag <- BinaryBit.getBool
-  int <- getInt32Bits
-  pure (FlaggedIntAttribute flag int)
-
-putFlaggedIntAttribute :: FlaggedIntAttribute -> BinaryBit.BitPut ()
-putFlaggedIntAttribute flaggedIntAttribute = do
-  BinaryBit.putBool (flaggedIntAttributeFlag flaggedIntAttribute)
-  putInt32Bits (flaggedIntAttributeInt flaggedIntAttribute)
diff --git a/library/Rattletrap/Attribute/Float.hs b/library/Rattletrap/Attribute/Float.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Float.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module Rattletrap.Attribute.Float where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype FloatAttribute = FloatAttribute
-  { floatAttributeValue :: Float32
-  } deriving (Eq, Ord, Show)
-
-getFloatAttribute :: BinaryBit.BitGet FloatAttribute
-getFloatAttribute = do
-  value <- getFloat32Bits
-  pure (FloatAttribute value)
-
-putFloatAttribute :: FloatAttribute -> BinaryBit.BitPut ()
-putFloatAttribute floatAttribute =
-  putFloat32Bits (floatAttributeValue floatAttribute)
diff --git a/library/Rattletrap/Attribute/GameMode.hs b/library/Rattletrap/Attribute/GameMode.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/GameMode.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module Rattletrap.Attribute.GameMode where
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Word as Word
-
-data GameModeAttribute = GameModeAttribute
-  { gameModeAttributeNumBits :: Int
-  , gameModeAttributeWord :: Word.Word8
-  } deriving (Eq, Ord, Show)
-
-getGameModeAttribute :: (Int, Int)
-                          -> BinaryBit.BitGet GameModeAttribute
-getGameModeAttribute version = do
-  let numBits =
-        if version < (868, 12)
-          then 2
-          else 8
-  word <- BinaryBit.getWord8 numBits
-  pure (GameModeAttribute numBits word)
-
-putGameModeAttribute :: GameModeAttribute -> BinaryBit.BitPut ()
-putGameModeAttribute gameModeAttribute = do
-  let numBits = gameModeAttributeNumBits gameModeAttribute
-  let word = gameModeAttributeWord gameModeAttribute
-  BinaryBit.putWord8 numBits word
diff --git a/library/Rattletrap/Attribute/Int.hs b/library/Rattletrap/Attribute/Int.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Int.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module Rattletrap.Attribute.Int where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype IntAttribute = IntAttribute
-  { intAttributeValue :: Int32
-  } deriving (Eq, Ord, Show)
-
-getIntAttribute :: BinaryBit.BitGet IntAttribute
-getIntAttribute = do
-  value <- getInt32Bits
-  pure (IntAttribute value)
-
-putIntAttribute :: IntAttribute -> BinaryBit.BitPut ()
-putIntAttribute intAttribute =
-  putInt32Bits (intAttributeValue intAttribute)
diff --git a/library/Rattletrap/Attribute/Loadout.hs b/library/Rattletrap/Attribute/Loadout.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Loadout.hs
+++ /dev/null
@@ -1,60 +0,0 @@
-module Rattletrap.Attribute.Loadout where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data LoadoutAttribute = LoadoutAttribute
-  { loadoutAttributeVersion :: Word8
-  , loadoutAttributeBody :: Word32
-  , loadoutAttributeDecal :: Word32
-  , loadoutAttributeWheels :: Word32
-  , loadoutAttributeRocketTrail :: Word32
-  , loadoutAttributeAntenna :: Word32
-  , loadoutAttributeTopper :: Word32
-  , loadoutAttributeUnknown1 :: Word32
-  , loadoutAttributeUnknown2 :: Maybe Word32
-  } deriving (Eq, Ord, Show)
-
-getLoadoutAttribute :: BinaryBit.BitGet LoadoutAttribute
-getLoadoutAttribute = do
-  version <- getWord8Bits
-  body <- getWord32Bits
-  decal <- getWord32Bits
-  wheels <- getWord32Bits
-  rocketTrail <- getWord32Bits
-  antenna <- getWord32Bits
-  topper <- getWord32Bits
-  g <- getWord32Bits
-  h <-
-    if version > Word8 10
-      then do
-        h <- getWord32Bits
-        pure (Just h)
-      else pure Nothing
-  pure
-    (LoadoutAttribute
-       version
-       body
-       decal
-       wheels
-       rocketTrail
-       antenna
-       topper
-       g
-       h)
-
-putLoadoutAttribute :: LoadoutAttribute -> BinaryBit.BitPut ()
-putLoadoutAttribute loadoutAttribute = do
-  putWord8Bits (loadoutAttributeVersion loadoutAttribute)
-  putWord32Bits (loadoutAttributeBody loadoutAttribute)
-  putWord32Bits (loadoutAttributeDecal loadoutAttribute)
-  putWord32Bits (loadoutAttributeWheels loadoutAttribute)
-  putWord32Bits (loadoutAttributeRocketTrail loadoutAttribute)
-  putWord32Bits (loadoutAttributeAntenna loadoutAttribute)
-  putWord32Bits (loadoutAttributeTopper loadoutAttribute)
-  putWord32Bits (loadoutAttributeUnknown1 loadoutAttribute)
-  case loadoutAttributeUnknown2 loadoutAttribute of
-    Nothing -> pure ()
-    Just x -> putWord32Bits x
diff --git a/library/Rattletrap/Attribute/LoadoutOnline.hs b/library/Rattletrap/Attribute/LoadoutOnline.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/LoadoutOnline.hs
+++ /dev/null
@@ -1,39 +0,0 @@
-module Rattletrap.Attribute.LoadoutOnline where
-
-import Rattletrap.Primitive
-
-import qualified Control.Monad as Monad
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype LoadoutOnlineAttribute = LoadoutOnlineAttribute
-  { loadoutAttributeValue :: [[(Word32, CompressedWord)]]
-  } deriving (Eq, Ord, Show)
-
-getLoadoutOnlineAttribute :: BinaryBit.BitGet LoadoutOnlineAttribute
-getLoadoutOnlineAttribute = 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 (LoadoutOnlineAttribute values)
-
-putLoadoutOnlineAttribute :: LoadoutOnlineAttribute -> BinaryBit.BitPut ()
-putLoadoutOnlineAttribute loadoutAttribute = do
-  let values = loadoutAttributeValue loadoutAttribute
-  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/Attribute/Loadouts.hs b/library/Rattletrap/Attribute/Loadouts.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Loadouts.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-module Rattletrap.Attribute.Loadouts where
-
-import Rattletrap.Attribute.Loadout
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data LoadoutsAttribute = LoadoutsAttribute
-  { loadoutsAttributeBlue :: LoadoutAttribute
-  , loadoutsAttributeOrange :: LoadoutAttribute
-  } deriving (Eq, Ord, Show)
-
-getLoadoutsAttribute :: BinaryBit.BitGet LoadoutsAttribute
-getLoadoutsAttribute = do
-  blue <- getLoadoutAttribute
-  orange <- getLoadoutAttribute
-  pure (LoadoutsAttribute blue orange)
-
-putLoadoutsAttribute :: LoadoutsAttribute -> BinaryBit.BitPut ()
-putLoadoutsAttribute loadoutsAttribute = do
-  putLoadoutAttribute (loadoutsAttributeBlue loadoutsAttribute)
-  putLoadoutAttribute (loadoutsAttributeOrange loadoutsAttribute)
diff --git a/library/Rattletrap/Attribute/LoadoutsOnline.hs b/library/Rattletrap/Attribute/LoadoutsOnline.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/LoadoutsOnline.hs
+++ /dev/null
@@ -1,34 +0,0 @@
-module Rattletrap.Attribute.LoadoutsOnline where
-
-import Rattletrap.Attribute.LoadoutOnline
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data LoadoutsOnlineAttribute = LoadoutsOnlineAttribute
-  { loadoutsOnlineAttributeBlue :: LoadoutOnlineAttribute
-  , loadoutsOnlineAttributeOrange :: LoadoutOnlineAttribute
-  , loadoutsOnlineAttributeUnknown1 :: Bool
-  , loadoutsOnlineAttributeUnknown2 :: Bool
-  } deriving (Eq, Ord, Show)
-
-getLoadoutsOnlineAttribute :: BinaryBit.BitGet LoadoutsOnlineAttribute
-getLoadoutsOnlineAttribute = do
-  blueLoadout <- getLoadoutOnlineAttribute
-  orangeLoadout <- getLoadoutOnlineAttribute
-  unknown1 <- BinaryBit.getBool
-  unknown2 <- BinaryBit.getBool
-  pure
-    (LoadoutsOnlineAttribute blueLoadout orangeLoadout unknown1 unknown2)
-
-putLoadoutsOnlineAttribute :: LoadoutsOnlineAttribute
-                                -> BinaryBit.BitPut ()
-putLoadoutsOnlineAttribute loadoutsOnlineAttribute = do
-  putLoadoutOnlineAttribute
-    (loadoutsOnlineAttributeBlue loadoutsOnlineAttribute)
-  putLoadoutOnlineAttribute
-    (loadoutsOnlineAttributeOrange loadoutsOnlineAttribute)
-  BinaryBit.putBool
-    (loadoutsOnlineAttributeUnknown1 loadoutsOnlineAttribute)
-  BinaryBit.putBool
-    (loadoutsOnlineAttributeUnknown2 loadoutsOnlineAttribute)
diff --git a/library/Rattletrap/Attribute/Location.hs b/library/Rattletrap/Attribute/Location.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Location.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module Rattletrap.Attribute.Location where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype LocationAttribute = LocationAttribute
-  { locationAttributeValue :: Vector
-  } deriving (Eq, Ord, Show)
-
-getLocationAttribute :: BinaryBit.BitGet LocationAttribute
-getLocationAttribute = do
-  value <- getVector
-  pure (LocationAttribute value)
-
-putLocationAttribute :: LocationAttribute -> BinaryBit.BitPut ()
-putLocationAttribute locationAttribute =
-  putVector (locationAttributeValue locationAttribute)
diff --git a/library/Rattletrap/Attribute/MusicStinger.hs b/library/Rattletrap/Attribute/MusicStinger.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/MusicStinger.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module Rattletrap.Attribute.MusicStinger where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data MusicStingerAttribute = MusicStingerAttribute
-  { musicStingerAttributeFlag :: Bool
-  , musicStingerAttributeCue :: Word32
-  , musicStingerAttributeTrigger :: Word8
-  } deriving (Eq, Ord, Show)
-
-getMusicStingerAttribute :: BinaryBit.BitGet MusicStingerAttribute
-getMusicStingerAttribute = do
-  flag <- BinaryBit.getBool
-  cue <- getWord32Bits
-  trigger <- getWord8Bits
-  pure (MusicStingerAttribute flag cue trigger)
-
-putMusicStingerAttribute :: MusicStingerAttribute
-                              -> BinaryBit.BitPut ()
-putMusicStingerAttribute musicStingerAttribute = do
-  BinaryBit.putBool (musicStingerAttributeFlag musicStingerAttribute)
-  putWord32Bits (musicStingerAttributeCue musicStingerAttribute)
-  putWord8Bits (musicStingerAttributeTrigger musicStingerAttribute)
diff --git a/library/Rattletrap/Attribute/PartyLeader.hs b/library/Rattletrap/Attribute/PartyLeader.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/PartyLeader.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Rattletrap.Attribute.PartyLeader where
-
-import Rattletrap.Primitive
-import Rattletrap.RemoteId
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data PartyLeaderAttribute = PartyLeaderAttribute
-  { partyLeaderAttributeSystemId :: Word8
-  , partyLeaderAttributeId :: Maybe (RemoteId, Word8)
-  } deriving (Eq, Ord, Show)
-
-getPartyLeaderAttribute :: BinaryBit.BitGet PartyLeaderAttribute
-getPartyLeaderAttribute = do
-  systemId <- getWord8Bits
-  maybeRemoteAndLocalId <-
-    if systemId == Word8 0
-      then pure Nothing
-      else do
-        remoteId <- getRemoteId systemId
-        localId <- getWord8Bits
-        pure (Just (remoteId, localId))
-  pure (PartyLeaderAttribute systemId maybeRemoteAndLocalId)
-
-putPartyLeaderAttribute :: PartyLeaderAttribute -> BinaryBit.BitPut ()
-putPartyLeaderAttribute partyLeaderAttribute = do
-  putWord8Bits (partyLeaderAttributeSystemId partyLeaderAttribute)
-  case partyLeaderAttributeId partyLeaderAttribute of
-    Nothing -> pure ()
-    Just (remoteId, localId) -> do
-      putRemoteId remoteId
-      putWord8Bits localId
diff --git a/library/Rattletrap/Attribute/Pickup.hs b/library/Rattletrap/Attribute/Pickup.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Pickup.hs
+++ /dev/null
@@ -1,32 +0,0 @@
-module Rattletrap.Attribute.Pickup where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data PickupAttribute = PickupAttribute
-  { pickupAttributeInstigatorId :: Maybe Word32
-  , pickupAttributePickedUp :: Bool
-  } deriving (Eq, Ord, Show)
-
-getPickupAttribute :: BinaryBit.BitGet PickupAttribute
-getPickupAttribute = do
-  instigator <- BinaryBit.getBool
-  maybeInstigatorId <-
-    if instigator
-      then do
-        instigatorId <- getWord32Bits
-        pure (Just instigatorId)
-      else pure Nothing
-  pickedUp <- BinaryBit.getBool
-  pure (PickupAttribute maybeInstigatorId pickedUp)
-
-putPickupAttribute :: PickupAttribute -> BinaryBit.BitPut ()
-putPickupAttribute pickupAttribute = do
-  case pickupAttributeInstigatorId pickupAttribute of
-    Nothing -> BinaryBit.putBool False
-    Just instigatorId -> do
-      BinaryBit.putBool True
-      putWord32Bits instigatorId
-  BinaryBit.putBool (pickupAttributePickedUp pickupAttribute)
diff --git a/library/Rattletrap/Attribute/PrivateMatchSettings.hs b/library/Rattletrap/Attribute/PrivateMatchSettings.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/PrivateMatchSettings.hs
+++ /dev/null
@@ -1,53 +0,0 @@
-module Rattletrap.Attribute.PrivateMatchSettings where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data PrivateMatchSettingsAttribute = PrivateMatchSettingsAttribute
-  { privateMatchSettingsAttributeMutators :: Text
-  , privateMatchSettingsAttributeJoinableBy :: Word32
-  , privateMatchSettingsAttributeMaxPlayers :: Word32
-  , privateMatchSettingsAttributeGameName :: Text
-  , privateMatchSettingsAttributePassword :: Text
-  , privateMatchSettingsAttributeFlag :: Bool
-  } deriving (Eq, Ord, Show)
-
-getPrivateMatchSettingsAttribute :: BinaryBit.BitGet PrivateMatchSettingsAttribute
-getPrivateMatchSettingsAttribute = do
-  mutators <- getTextBits
-  joinableBy <- getWord32Bits
-  maxPlayers <- getWord32Bits
-  gameName <- getTextBits
-  password <- getTextBits
-  flag <- BinaryBit.getBool
-  pure
-    (PrivateMatchSettingsAttribute
-       mutators
-       joinableBy
-       maxPlayers
-       gameName
-       password
-       flag)
-
-putPrivateMatchSettingsAttribute :: PrivateMatchSettingsAttribute
-                                      -> BinaryBit.BitPut ()
-putPrivateMatchSettingsAttribute privateMatchSettingsAttribute = do
-  putTextBits
-    (privateMatchSettingsAttributeMutators
-       privateMatchSettingsAttribute)
-  putWord32Bits
-    (privateMatchSettingsAttributeJoinableBy
-       privateMatchSettingsAttribute)
-  putWord32Bits
-    (privateMatchSettingsAttributeMaxPlayers
-       privateMatchSettingsAttribute)
-  putTextBits
-    (privateMatchSettingsAttributeGameName
-       privateMatchSettingsAttribute)
-  putTextBits
-    (privateMatchSettingsAttributePassword
-       privateMatchSettingsAttribute)
-  BinaryBit.putBool
-    (privateMatchSettingsAttributeFlag privateMatchSettingsAttribute)
diff --git a/library/Rattletrap/Attribute/QWord.hs b/library/Rattletrap/Attribute/QWord.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/QWord.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module Rattletrap.Attribute.QWord where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype QWordAttribute = QWordAttribute
-  { qWordAttributeValue :: Word64
-  } deriving (Eq, Ord, Show)
-
-getQWordAttribute :: BinaryBit.BitGet QWordAttribute
-getQWordAttribute = do
-  value <- getWord64Bits
-  pure (QWordAttribute value)
-
-putQWordAttribute :: QWordAttribute -> BinaryBit.BitPut ()
-putQWordAttribute qWordAttribute =
-  putWord64Bits (qWordAttributeValue qWordAttribute)
diff --git a/library/Rattletrap/Attribute/Reservation.hs b/library/Rattletrap/Attribute/Reservation.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/Reservation.hs
+++ /dev/null
@@ -1,54 +0,0 @@
-module Rattletrap.Attribute.Reservation where
-
-import Rattletrap.Attribute.UniqueId
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Word as Word
-
-data ReservationAttribute = ReservationAttribute
-  { reservationAttributeNumber :: CompressedWord
-  , reservationAttributeUniqueId :: UniqueIdAttribute
-  , reservationAttributeName :: Maybe Text
-  , reservationAttributeUnknown1 :: Bool
-  , reservationAttributeUnknown2 :: Bool
-  , reservationAttributeUnknown3 :: Maybe Word.Word8
-  } deriving (Eq, Ord, Show)
-
-getReservationAttribute :: (Int, Int)
-                             -> BinaryBit.BitGet ReservationAttribute
-getReservationAttribute version = do
-  number <- getCompressedWord 7
-  uniqueId <- getUniqueIdAttribute
-  name <-
-    if uniqueIdAttributeSystemId uniqueId == Word8 0
-      then pure Nothing
-      else do
-        name <- getTextBits
-        pure (Just name)
-  a <- BinaryBit.getBool
-  b <- BinaryBit.getBool
-  mc <-
-    if version < (868, 12)
-      then pure Nothing
-      else do
-        c <- BinaryBit.getWord8 6
-        pure (Just c)
-  pure (ReservationAttribute number uniqueId name a b mc)
-
-putReservationAttribute :: ReservationAttribute -> BinaryBit.BitPut ()
-putReservationAttribute reservationAttribute = do
-  putCompressedWord (reservationAttributeNumber reservationAttribute)
-  putUniqueIdAttribute
-    (reservationAttributeUniqueId reservationAttribute)
-  case reservationAttributeName reservationAttribute of
-    Nothing -> pure ()
-    Just name -> putTextBits name
-  BinaryBit.putBool
-    (reservationAttributeUnknown1 reservationAttribute)
-  BinaryBit.putBool
-    (reservationAttributeUnknown2 reservationAttribute)
-  case reservationAttributeUnknown3 reservationAttribute of
-    Nothing -> pure ()
-    Just c -> BinaryBit.putWord8 6 c
diff --git a/library/Rattletrap/Attribute/RigidBodyState.hs b/library/Rattletrap/Attribute/RigidBodyState.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/RigidBodyState.hs
+++ /dev/null
@@ -1,54 +0,0 @@
-module Rattletrap.Attribute.RigidBodyState where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data RigidBodyStateAttribute = RigidBodyStateAttribute
-  { rigidBodyStateAttributeSleeping :: Bool
-  , rigidBodyStateAttributeLocation :: Vector
-  , rigidBodyStateAttributeRotation :: CompressedWordVector
-  , rigidBodyStateAttributeLinearVelocity :: Maybe Vector
-  , rigidBodyStateAttributeAngularVelocity :: Maybe Vector
-  } deriving (Eq, Ord, Show)
-
-getRigidBodyStateAttribute :: BinaryBit.BitGet RigidBodyStateAttribute
-getRigidBodyStateAttribute = do
-  sleeping <- BinaryBit.getBool
-  location <- getVector
-  rotation <- getCompressedWordVector
-  linearVelocity <-
-    if sleeping
-      then pure Nothing
-      else do
-        linearVelocity <- getVector
-        pure (Just linearVelocity)
-  angularVelocity <-
-    if sleeping
-      then pure Nothing
-      else do
-        angularVelocity <- getVector
-        pure (Just angularVelocity)
-  pure
-    (RigidBodyStateAttribute
-       sleeping
-       location
-       rotation
-       linearVelocity
-       angularVelocity)
-
-putRigidBodyStateAttribute :: RigidBodyStateAttribute
-                                -> BinaryBit.BitPut ()
-putRigidBodyStateAttribute rigidBodyStateAttribute = do
-  BinaryBit.putBool
-    (rigidBodyStateAttributeSleeping rigidBodyStateAttribute)
-  putVector (rigidBodyStateAttributeLocation rigidBodyStateAttribute)
-  putCompressedWordVector
-    (rigidBodyStateAttributeRotation rigidBodyStateAttribute)
-  case rigidBodyStateAttributeLinearVelocity rigidBodyStateAttribute of
-    Nothing -> pure ()
-    Just linearVelocity -> putVector linearVelocity
-  case rigidBodyStateAttributeAngularVelocity rigidBodyStateAttribute of
-    Nothing -> pure ()
-    Just angularVelocity -> putVector angularVelocity
diff --git a/library/Rattletrap/Attribute/String.hs b/library/Rattletrap/Attribute/String.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/String.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module Rattletrap.Attribute.String where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype StringAttribute = StringAttribute
-  { stringAttributeValue :: Text
-  } deriving (Eq, Ord, Show)
-
-getStringAttribute :: BinaryBit.BitGet StringAttribute
-getStringAttribute = do
-  value <- getTextBits
-  pure (StringAttribute value)
-
-putStringAttribute :: StringAttribute -> BinaryBit.BitPut ()
-putStringAttribute stringAttribute =
-  putTextBits (stringAttributeValue stringAttribute)
diff --git a/library/Rattletrap/Attribute/TeamPaint.hs b/library/Rattletrap/Attribute/TeamPaint.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/TeamPaint.hs
+++ /dev/null
@@ -1,37 +0,0 @@
-module Rattletrap.Attribute.TeamPaint where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data TeamPaintAttribute = TeamPaintAttribute
-  { teamPaintAttributeTeam :: Word8
-  , teamPaintAttributePrimaryColor :: Word8
-  , teamPaintAttributeAccentColor :: Word8
-  , teamPaintAttributePrimaryFinish :: Word32
-  , teamPaintAttributeAccentFinish :: Word32
-  } deriving (Eq, Ord, Show)
-
-getTeamPaintAttribute :: BinaryBit.BitGet TeamPaintAttribute
-getTeamPaintAttribute = do
-  team <- getWord8Bits
-  primaryColor <- getWord8Bits
-  accentColor <- getWord8Bits
-  primaryFinish <- getWord32Bits
-  accentFinish <- getWord32Bits
-  pure
-    (TeamPaintAttribute
-       team
-       primaryColor
-       accentColor
-       primaryFinish
-       accentFinish)
-
-putTeamPaintAttribute :: TeamPaintAttribute -> BinaryBit.BitPut ()
-putTeamPaintAttribute teamPaintAttribute = do
-  putWord8Bits (teamPaintAttributeTeam teamPaintAttribute)
-  putWord8Bits (teamPaintAttributePrimaryColor teamPaintAttribute)
-  putWord8Bits (teamPaintAttributeAccentColor teamPaintAttribute)
-  putWord32Bits (teamPaintAttributePrimaryFinish teamPaintAttribute)
-  putWord32Bits (teamPaintAttributeAccentFinish teamPaintAttribute)
diff --git a/library/Rattletrap/Attribute/UniqueId.hs b/library/Rattletrap/Attribute/UniqueId.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/UniqueId.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module Rattletrap.Attribute.UniqueId where
-
-import Rattletrap.Primitive
-import Rattletrap.RemoteId
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data UniqueIdAttribute = UniqueIdAttribute
-  { uniqueIdAttributeSystemId :: Word8
-  , uniqueIdAttributeRemoteId :: RemoteId
-  , uniqueIdAttributeLocalId :: Word8
-  } deriving (Eq, Ord, Show)
-
-getUniqueIdAttribute :: BinaryBit.BitGet UniqueIdAttribute
-getUniqueIdAttribute = do
-  systemId <- getWord8Bits
-  remoteId <- getRemoteId systemId
-  localId <- getWord8Bits
-  pure (UniqueIdAttribute systemId remoteId localId)
-
-putUniqueIdAttribute :: UniqueIdAttribute -> BinaryBit.BitPut ()
-putUniqueIdAttribute uniqueIdAttribute = do
-  putWord8Bits (uniqueIdAttributeSystemId uniqueIdAttribute)
-  putRemoteId (uniqueIdAttributeRemoteId uniqueIdAttribute)
-  putWord8Bits (uniqueIdAttributeLocalId uniqueIdAttribute)
diff --git a/library/Rattletrap/Attribute/WeldedInfo.hs b/library/Rattletrap/Attribute/WeldedInfo.hs
deleted file mode 100644
--- a/library/Rattletrap/Attribute/WeldedInfo.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module Rattletrap.Attribute.WeldedInfo where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data WeldedInfoAttribute = WeldedInfoAttribute
-  { weldedInfoAttributeActive :: Bool
-  , weldedInfoAttributeActorId :: Int32
-  , weldedInfoAttributeOffset :: Vector
-  , weldedInfoAttributeMass :: Float32
-  , weldedInfoAttributeRotation :: Int8Vector
-  } deriving (Eq, Ord, Show)
-
-getWeldedInfoAttribute :: BinaryBit.BitGet WeldedInfoAttribute
-getWeldedInfoAttribute = do
-  active <- BinaryBit.getBool
-  actorId <- getInt32Bits
-  offset <- getVector
-  mass <- getFloat32Bits
-  rotation <- getInt8Vector
-  pure (WeldedInfoAttribute active actorId offset mass rotation)
-
-putWeldedInfoAttribute :: WeldedInfoAttribute -> BinaryBit.BitPut ()
-putWeldedInfoAttribute weldedInfoAttribute = do
-  BinaryBit.putBool (weldedInfoAttributeActive weldedInfoAttribute)
-  putInt32Bits (weldedInfoAttributeActorId weldedInfoAttribute)
-  putVector (weldedInfoAttributeOffset weldedInfoAttribute)
-  putFloat32Bits (weldedInfoAttributeMass weldedInfoAttribute)
-  putInt8Vector (weldedInfoAttributeRotation weldedInfoAttribute)
diff --git a/library/Rattletrap/AttributeMapping.hs b/library/Rattletrap/AttributeMapping.hs
deleted file mode 100644
--- a/library/Rattletrap/AttributeMapping.hs
+++ /dev/null
@@ -1,21 +0,0 @@
-module Rattletrap.AttributeMapping where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary as Binary
-
-data AttributeMapping = AttributeMapping
-  { attributeMappingObjectId :: Word32
-  , attributeMappingStreamId :: Word32
-  } deriving (Eq, Ord, Show)
-
-getAttributeMapping :: Binary.Get AttributeMapping
-getAttributeMapping = do
-  objectId <- getWord32
-  streamId <- getWord32
-  pure (AttributeMapping objectId streamId)
-
-putAttributeMapping :: AttributeMapping -> Binary.Put
-putAttributeMapping attributeMapping = do
-  putWord32 (attributeMappingObjectId attributeMapping)
-  putWord32 (attributeMappingStreamId attributeMapping)
diff --git a/library/Rattletrap/AttributeType.hs b/library/Rattletrap/AttributeType.hs
deleted file mode 100644
--- a/library/Rattletrap/AttributeType.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module Rattletrap.AttributeType where
-
-data AttributeType
-  = BooleanAttributeType
-  | ByteAttributeType
-  | CamSettingsAttributeType
-  | ClubColorsAttributeType
-  | DemolishAttributeType
-  | EnumAttributeType
-  | ExplosionAttributeType
-  | FlaggedIntAttributeType
-  | FloatAttributeType
-  | GameModeAttributeType
-  | IntAttributeType
-  | LoadoutAttributeType
-  | LoadoutOnlineAttributeType
-  | LoadoutsAttributeType
-  | LoadoutsOnlineAttributeType
-  | LocationAttributeType
-  | MusicStingerAttributeType
-  | PartyLeaderAttributeType
-  | PickupAttributeType
-  | PrivateMatchSettingsAttributeType
-  | QWordAttributeType
-  | ReservationAttributeType
-  | RigidBodyStateAttributeType
-  | StringAttributeType
-  | TeamPaintAttributeType
-  | UniqueIdAttributeType
-  | WeldedInfoAttributeType
-  deriving (Eq, Ord, Show)
diff --git a/library/Rattletrap/AttributeValue.hs b/library/Rattletrap/AttributeValue.hs
deleted file mode 100644
--- a/library/Rattletrap/AttributeValue.hs
+++ /dev/null
@@ -1,217 +0,0 @@
-module Rattletrap.AttributeValue
-  ( module Rattletrap.AttributeValue
-  , module Rattletrap.Attribute.Boolean
-  , module Rattletrap.Attribute.Byte
-  , module Rattletrap.Attribute.CamSettings
-  , module Rattletrap.Attribute.ClubColors
-  , module Rattletrap.Attribute.Demolish
-  , module Rattletrap.Attribute.Enum
-  , module Rattletrap.Attribute.Explosion
-  , module Rattletrap.Attribute.FlaggedInt
-  , module Rattletrap.Attribute.Float
-  , module Rattletrap.Attribute.GameMode
-  , module Rattletrap.Attribute.Int
-  , module Rattletrap.Attribute.Loadout
-  , module Rattletrap.Attribute.LoadoutOnline
-  , module Rattletrap.Attribute.Loadouts
-  , module Rattletrap.Attribute.LoadoutsOnline
-  , module Rattletrap.Attribute.Location
-  , module Rattletrap.Attribute.MusicStinger
-  , module Rattletrap.Attribute.PartyLeader
-  , module Rattletrap.Attribute.Pickup
-  , module Rattletrap.Attribute.PrivateMatchSettings
-  , module Rattletrap.Attribute.QWord
-  , module Rattletrap.Attribute.Reservation
-  , module Rattletrap.Attribute.RigidBodyState
-  , module Rattletrap.Attribute.String
-  , module Rattletrap.Attribute.TeamPaint
-  , module Rattletrap.Attribute.UniqueId
-  , module Rattletrap.Attribute.WeldedInfo
-  ) where
-
-import Rattletrap.Attribute.Boolean
-import Rattletrap.Attribute.Byte
-import Rattletrap.Attribute.CamSettings
-import Rattletrap.Attribute.ClubColors
-import Rattletrap.Attribute.Demolish
-import Rattletrap.Attribute.Enum
-import Rattletrap.Attribute.Explosion
-import Rattletrap.Attribute.FlaggedInt
-import Rattletrap.Attribute.Float
-import Rattletrap.Attribute.GameMode
-import Rattletrap.Attribute.Int
-import Rattletrap.Attribute.Loadout
-import Rattletrap.Attribute.LoadoutOnline
-import Rattletrap.Attribute.Loadouts
-import Rattletrap.Attribute.LoadoutsOnline
-import Rattletrap.Attribute.Location
-import Rattletrap.Attribute.MusicStinger
-import Rattletrap.Attribute.PartyLeader
-import Rattletrap.Attribute.Pickup
-import Rattletrap.Attribute.PrivateMatchSettings
-import Rattletrap.Attribute.QWord
-import Rattletrap.Attribute.Reservation
-import Rattletrap.Attribute.RigidBodyState
-import Rattletrap.Attribute.String
-import Rattletrap.Attribute.TeamPaint
-import Rattletrap.Attribute.UniqueId
-import Rattletrap.Attribute.WeldedInfo
-import Rattletrap.AttributeType
-import Rattletrap.Data
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Map.Strict as Map
-
-data AttributeValue
-  = BooleanAttributeValue BooleanAttribute
-  | ByteAttributeValue ByteAttribute
-  | CamSettingsAttributeValue CamSettingsAttribute
-  | ClubColorsAttributeValue ClubColorsAttribute
-  | DemolishAttributeValue DemolishAttribute
-  | EnumAttributeValue EnumAttribute
-  | ExplosionAttributeValue ExplosionAttribute
-  | FlaggedIntAttributeValue FlaggedIntAttribute
-  | FloatAttributeValue FloatAttribute
-  | GameModeAttributeValue GameModeAttribute
-  | IntAttributeValue IntAttribute
-  | LoadoutAttributeValue LoadoutAttribute
-  | LoadoutOnlineAttributeValue LoadoutOnlineAttribute
-  | LoadoutsAttributeValue LoadoutsAttribute
-  | LoadoutsOnlineAttributeValue LoadoutsOnlineAttribute
-  | LocationAttributeValue LocationAttribute
-  | MusicStingerAttributeValue MusicStingerAttribute
-  | PartyLeaderAttributeValue PartyLeaderAttribute
-  | PickupAttributeValue PickupAttribute
-  | PrivateMatchSettingsAttributeValue PrivateMatchSettingsAttribute
-  | QWordAttributeValue QWordAttribute
-  | ReservationAttributeValue ReservationAttribute
-  | RigidBodyStateAttributeValue RigidBodyStateAttribute
-  | StringAttributeValue StringAttribute
-  | TeamPaintAttributeValue TeamPaintAttribute
-  | UniqueIdAttributeValue UniqueIdAttribute
-  | WeldedInfoAttributeValue WeldedInfoAttribute
-  deriving (Eq, Ord, Show)
-
-getAttributeValue :: (Int, Int) -> Text -> BinaryBit.BitGet AttributeValue
-getAttributeValue version name =
-  case Map.lookup name attributeTypes of
-    Just constructor ->
-      case constructor of
-        BooleanAttributeType -> do
-          x <- getBooleanAttribute
-          pure (BooleanAttributeValue x)
-        ByteAttributeType -> do
-          x <- getByteAttribute
-          pure (ByteAttributeValue x)
-        CamSettingsAttributeType -> do
-          x <- getCamSettingsAttribute
-          pure (CamSettingsAttributeValue x)
-        ClubColorsAttributeType -> do
-          x <- getClubColorsAttribute
-          pure (ClubColorsAttributeValue x)
-        DemolishAttributeType -> do
-          x <- getDemolishAttribute
-          pure (DemolishAttributeValue x)
-        EnumAttributeType -> do
-          x <- getEnumAttribute
-          pure (EnumAttributeValue x)
-        ExplosionAttributeType -> do
-          x <- getExplosionAttribute
-          pure (ExplosionAttributeValue x)
-        FlaggedIntAttributeType -> do
-          x <- getFlaggedIntAttribute
-          pure (FlaggedIntAttributeValue x)
-        FloatAttributeType -> do
-          x <- getFloatAttribute
-          pure (FloatAttributeValue x)
-        GameModeAttributeType -> do
-          x <- getGameModeAttribute version
-          pure (GameModeAttributeValue x)
-        IntAttributeType -> do
-          x <- getIntAttribute
-          pure (IntAttributeValue x)
-        LoadoutAttributeType -> do
-          x <- getLoadoutAttribute
-          pure (LoadoutAttributeValue x)
-        LoadoutOnlineAttributeType -> do
-          x <- getLoadoutOnlineAttribute
-          pure (LoadoutOnlineAttributeValue x)
-        LoadoutsAttributeType -> do
-          x <- getLoadoutsAttribute
-          pure (LoadoutsAttributeValue x)
-        LoadoutsOnlineAttributeType -> do
-          x <- getLoadoutsOnlineAttribute
-          pure (LoadoutsOnlineAttributeValue x)
-        LocationAttributeType -> do
-          x <- getLocationAttribute
-          pure (LocationAttributeValue x)
-        MusicStingerAttributeType -> do
-          x <- getMusicStingerAttribute
-          pure (MusicStingerAttributeValue x)
-        PartyLeaderAttributeType -> do
-          x <- getPartyLeaderAttribute
-          pure (PartyLeaderAttributeValue x)
-        PickupAttributeType -> do
-          x <- getPickupAttribute
-          pure (PickupAttributeValue x)
-        PrivateMatchSettingsAttributeType -> do
-          x <- getPrivateMatchSettingsAttribute
-          pure (PrivateMatchSettingsAttributeValue x)
-        QWordAttributeType -> do
-          x <- getQWordAttribute
-          pure (QWordAttributeValue x)
-        ReservationAttributeType -> do
-          x <- getReservationAttribute version
-          pure (ReservationAttributeValue x)
-        RigidBodyStateAttributeType -> do
-          x <- getRigidBodyStateAttribute
-          pure (RigidBodyStateAttributeValue x)
-        StringAttributeType -> do
-          x <- getStringAttribute
-          pure (StringAttributeValue x)
-        TeamPaintAttributeType -> do
-          x <- getTeamPaintAttribute
-          pure (TeamPaintAttributeValue x)
-        UniqueIdAttributeType -> do
-          x <- getUniqueIdAttribute
-          pure (UniqueIdAttributeValue x)
-        WeldedInfoAttributeType -> do
-          x <- getWeldedInfoAttribute
-          pure (WeldedInfoAttributeValue x)
-    Nothing -> fail ("don't know how to get attribute value " ++ show name)
-
-attributeTypes :: Map.Map Text AttributeType
-attributeTypes = Map.mapKeys stringToText (Map.fromList rawAttributeTypes)
-
-putAttributeValue :: AttributeValue -> BinaryBit.BitPut ()
-putAttributeValue value =
-  case value of
-    BooleanAttributeValue x -> putBooleanAttribute x
-    ByteAttributeValue x -> putByteAttribute x
-    CamSettingsAttributeValue x -> putCamSettingsAttribute x
-    ClubColorsAttributeValue x -> putClubColorsAttribute x
-    DemolishAttributeValue x -> putDemolishAttribute x
-    EnumAttributeValue x -> putEnumAttribute x
-    ExplosionAttributeValue x -> putExplosionAttribute x
-    FlaggedIntAttributeValue x -> putFlaggedIntAttribute x
-    FloatAttributeValue x -> putFloatAttribute x
-    GameModeAttributeValue x -> putGameModeAttribute x
-    IntAttributeValue x -> putIntAttribute x
-    LoadoutAttributeValue x -> putLoadoutAttribute x
-    LoadoutOnlineAttributeValue x -> putLoadoutOnlineAttribute x
-    LoadoutsAttributeValue x -> putLoadoutsAttribute x
-    LoadoutsOnlineAttributeValue x -> putLoadoutsOnlineAttribute x
-    LocationAttributeValue x -> putLocationAttribute x
-    MusicStingerAttributeValue x -> putMusicStingerAttribute x
-    PartyLeaderAttributeValue x -> putPartyLeaderAttribute x
-    PickupAttributeValue x -> putPickupAttribute x
-    PrivateMatchSettingsAttributeValue x -> putPrivateMatchSettingsAttribute x
-    QWordAttributeValue x -> putQWordAttribute x
-    ReservationAttributeValue x -> putReservationAttribute x
-    RigidBodyStateAttributeValue x -> putRigidBodyStateAttribute x
-    StringAttributeValue x -> putStringAttribute x
-    TeamPaintAttributeValue x -> putTeamPaintAttribute x
-    UniqueIdAttributeValue x -> putUniqueIdAttribute x
-    WeldedInfoAttributeValue x -> putWeldedInfoAttribute x
diff --git a/library/Rattletrap/Cache.hs b/library/Rattletrap/Cache.hs
deleted file mode 100644
--- a/library/Rattletrap/Cache.hs
+++ /dev/null
@@ -1,28 +0,0 @@
-module Rattletrap.Cache where
-
-import Rattletrap.AttributeMapping
-import Rattletrap.Primitive
-
-import qualified Data.Binary as Binary
-
-data Cache = Cache
-  { cacheClassId :: Word32
-  , cacheParentCacheId :: Word32
-  , cacheCacheId :: Word32
-  , cacheAttributeMappings :: List AttributeMapping
-  } deriving (Eq, Ord, Show)
-
-getCache :: Binary.Get Cache
-getCache = do
-  classId <- getWord32
-  parentCacheId <- getWord32
-  cacheId <- getWord32
-  attributeMappings <- getList getAttributeMapping
-  pure (Cache classId parentCacheId cacheId attributeMappings)
-
-putCache :: Cache -> Binary.Put
-putCache cache = do
-  putWord32 (cacheClassId cache)
-  putWord32 (cacheParentCacheId cache)
-  putWord32 (cacheCacheId cache)
-  putList putAttributeMapping (cacheAttributeMappings cache)
diff --git a/library/Rattletrap/ClassAttributeMap.hs b/library/Rattletrap/ClassAttributeMap.hs
deleted file mode 100644
--- a/library/Rattletrap/ClassAttributeMap.hs
+++ /dev/null
@@ -1,269 +0,0 @@
-module Rattletrap.ClassAttributeMap where
-
-import Rattletrap.ActorMap
-import Rattletrap.AttributeMapping
-import Rattletrap.Cache
-import Rattletrap.ClassMapping
-import Rattletrap.Data
-import Rattletrap.Primitive
-
-import qualified Data.Bimap as Bimap
-import qualified Data.List as List
-import qualified Data.Map.Strict as Map
-import qualified Data.Maybe as Maybe
-import qualified Data.Set as Set
-import qualified Data.Text as Text
-
-data ClassAttributeMap = ClassAttributeMap
-  { classAttributeMapObjectMap :: Map.Map Word32 Text
-  , classAttributeMapClassMap :: Bimap.Bimap Word32 Text
-  , classAttributeMapObjectClassMap :: Map.Map Word32 Word32
-  , classAttributeMapValue :: Map.Map Word32 (Map.Map Word32 Word32)
-  } deriving (Eq, Show)
-
-makeClassAttributeMap :: List Text
-                      -> List ClassMapping
-                      -> List Cache
-                      -> ClassAttributeMap
-makeClassAttributeMap objects classMappings caches =
-  let objectMap = makeObjectMap objects
-      classMap = makeClassMap classMappings
-      objectClassMap = makeObjectClassMap objectMap classMap
-      classCache = makeClassCache classMap caches
-      attributeMap = makeAttributeMap caches
-      classIds = map (\(_, classId, _, _) -> classId) classCache
-      parentMap = makeParentMap classCache
-      value =
-        Map.fromList
-          (map
-             (\classId ->
-                let ownAttributes =
-                      Maybe.fromMaybe
-                        Map.empty
-                        (Map.lookup classId attributeMap)
-                    parentsAttributes =
-                      case Map.lookup classId parentMap of
-                        Nothing -> []
-                        Just parentClassIds ->
-                          map
-                            (\parentClassId ->
-                               Maybe.fromMaybe
-                                 Map.empty
-                                 (Map.lookup parentClassId attributeMap))
-                            parentClassIds
-                    attributes = ownAttributes : parentsAttributes
-                in (classId, Map.fromList (concatMap Map.toList attributes)))
-             classIds)
-  in ClassAttributeMap objectMap classMap objectClassMap value
-
-makeObjectClassMap :: Map.Map Word32 Text
-                   -> Bimap.Bimap Word32 Text
-                   -> Map.Map Word32 Word32
-makeObjectClassMap objectMap classMap = do
-  let objectIds = Map.keys objectMap
-  let classIds = map (getClassId objectMap classMap) objectIds
-  let rawPairs = zip objectIds classIds
-  let pairs =
-        Maybe.mapMaybe
-          (\(objectId, maybeClassId) ->
-             case maybeClassId of
-               Nothing -> Nothing
-               Just classId -> Just (objectId, classId))
-          rawPairs
-  Map.fromList pairs
-
-getClassId :: Map.Map Word32 Text
-           -> Bimap.Bimap Word32 Text
-           -> Word32
-           -> Maybe Word32
-getClassId objectMap classMap objectId = do
-  objectName <- getObjectName objectMap objectId
-  className <- getClassName objectName
-  Bimap.lookupR className classMap
-
-makeClassCache :: Bimap.Bimap Word32 Text
-               -> List Cache
-               -> [(Maybe Text, Word32, Word32, Word32)]
-makeClassCache classMap caches =
-  map
-    (\cache ->
-       let classId = cacheClassId cache
-       in ( Bimap.lookup classId classMap
-          , classId
-          , cacheCacheId cache
-          , cacheParentCacheId cache))
-    (listValue caches)
-
-makeClassMap :: List ClassMapping -> Bimap.Bimap Word32 Text
-makeClassMap classMappings =
-  Bimap.fromList
-    (map
-       (\classMapping ->
-          (classMappingStreamId classMapping, classMappingName classMapping))
-       (listValue classMappings))
-
-makeAttributeMap :: List Cache -> Map.Map Word32 (Map.Map Word32 Word32)
-makeAttributeMap caches =
-  Map.fromList
-    (map
-       (\cache ->
-          ( cacheClassId cache
-          , Map.fromList
-              (map
-                 (\attributeMapping ->
-                    ( attributeMappingStreamId attributeMapping
-                    , attributeMappingObjectId attributeMapping))
-                 (listValue (cacheAttributeMappings cache)))))
-       (listValue caches))
-
-makeShallowParentMap :: [(Maybe Text, Word32, Word32, Word32)]
-                     -> Map.Map Word32 Word32
-makeShallowParentMap classCache =
-  Map.fromList
-    (Maybe.mapMaybe
-       (\xs ->
-          case xs of
-            [] -> Nothing
-            (maybeClassName, classId, _, parentCacheId):rest -> do
-              parentClassId <- getParentClass maybeClassName parentCacheId rest
-              pure (classId, parentClassId))
-       (List.tails (reverse classCache)))
-
-makeParentMap :: [(Maybe Text, Word32, Word32, Word32)]
-              -> Map.Map Word32 [Word32]
-makeParentMap classCache =
-  let shallowParentMap = makeShallowParentMap classCache
-  in Map.mapWithKey
-       (\classId _ -> getParentClasses shallowParentMap classId)
-       shallowParentMap
-
-getParentClasses :: Map.Map Word32 Word32 -> Word32 -> [Word32]
-getParentClasses shallowParentMap classId =
-  case Map.lookup classId shallowParentMap of
-    Nothing -> []
-    Just parentClassId ->
-      parentClassId : getParentClasses shallowParentMap parentClassId
-
-getParentClass :: Maybe Text
-               -> Word32
-               -> [(Maybe Text, Word32, Word32, Word32)]
-               -> Maybe Word32
-getParentClass maybeClassName parentCacheId xs =
-  case maybeClassName of
-    Nothing -> getParentClassById parentCacheId xs
-    Just className -> getParentClassByName className parentCacheId xs
-
-getParentClassById :: Word32
-                   -> [(Maybe Text, Word32, Word32, Word32)]
-                   -> Maybe Word32
-getParentClassById parentCacheId xs =
-  case dropWhile (\(_, _, cacheId, _) -> cacheId /= parentCacheId) xs of
-    [] ->
-      if parentCacheId == Word32 0
-        then Nothing
-        else getParentClassById (Word32 (word32Value parentCacheId - 1)) xs
-    (_, parentClassId, _, _):_ -> Just parentClassId
-
-getParentClassByName :: Text
-                     -> Word32
-                     -> [(Maybe Text, Word32, Word32, Word32)]
-                     -> Maybe Word32
-getParentClassByName className parentCacheId xs =
-  case Map.lookup className parentClasses of
-    Nothing -> getParentClassById parentCacheId xs
-    Just parentClassName ->
-      Maybe.maybe
-        (getParentClassById parentCacheId xs)
-        Just
-        (Maybe.listToMaybe
-           (map
-              (\(_, parentClassId, _, _) -> parentClassId)
-              (filter
-                 (\(_, _, cacheId, _) -> cacheId == parentCacheId)
-                 (filter
-                    (\(maybeClassName, _, _, _) ->
-                       maybeClassName == Just parentClassName)
-                    xs))))
-
-parentClasses :: Map.Map Text Text
-parentClasses =
-  Map.map
-    stringToText
-    (Map.mapKeys stringToText (Map.fromList rawParentClasses))
-
-makeObjectMap :: List Text -> Map.Map Word32 Text
-makeObjectMap objects =
-  Map.fromAscList (zip (map Word32 [0 ..]) (listValue objects))
-
-getObjectName :: Map.Map Word32 Text -> Word32 -> Maybe Text
-getObjectName objectMap objectId = Map.lookup objectId objectMap
-
-getClassName :: Text -> Maybe Text
-getClassName rawObjectName =
-  Map.lookup (normalizeObjectName rawObjectName) objectClasses
-
-normalizeObjectName :: Text -> Text
-normalizeObjectName objectName =
-  let name = textValue objectName
-      crowdActor = Text.pack "TheWorld:PersistentLevel.CrowdActor_TA"
-      crowdManager = Text.pack "TheWorld:PersistentLevel.CrowdManager_TA"
-      boostPickup = Text.pack "TheWorld:PersistentLevel.VehiclePickup_Boost_TA"
-      mapScoreboard = Text.pack "TheWorld:PersistentLevel.InMapScoreboard_TA"
-      toText text =
-        Text
-          (Int32 (fromIntegral (Text.length text + 1)))
-          (Text.snoc text '\x00')
-  in if Text.isInfixOf crowdActor name
-       then toText crowdActor
-       else if Text.isInfixOf crowdManager name
-              then toText crowdManager
-              else if Text.isInfixOf boostPickup name
-                     then toText boostPickup
-                     else if Text.isInfixOf mapScoreboard name
-                            then toText mapScoreboard
-                            else objectName
-
-objectClasses :: Map.Map Text Text
-objectClasses =
-  Map.map
-    stringToText
-    (Map.mapKeys stringToText (Map.fromList rawObjectClasses))
-
-classHasLocation :: Text -> Bool
-classHasLocation className = Set.member className classesWithLocation
-
-classesWithLocation :: Set.Set Text
-classesWithLocation = Set.fromList (map stringToText rawClassesWithLocation)
-
-classHasRotation :: Text -> Bool
-classHasRotation className = Set.member className classesWithRotation
-
-classesWithRotation :: Set.Set Text
-classesWithRotation = Set.fromList (map stringToText rawClassesWithRotation)
-
-getAttributeIdLimit :: Map.Map Word32 Word32 -> Maybe Word
-getAttributeIdLimit attributeMap = do
-  ((streamId, _), _) <- Map.maxViewWithKey attributeMap
-  let limit = fromIntegral (word32Value streamId)
-  pure limit
-
-getAttributeName :: ClassAttributeMap
-                 -> Map.Map Word32 Word32
-                 -> CompressedWord
-                 -> Maybe Text
-getAttributeName classAttributeMap attributeMap streamId = do
-  let key = Word32 (fromIntegral (compressedWordValue streamId))
-  attributeId <- Map.lookup key attributeMap
-  let objectMap = classAttributeMapObjectMap classAttributeMap
-  Map.lookup attributeId objectMap
-
-getAttributeMap :: ClassAttributeMap
-                -> ActorMap
-                -> CompressedWord
-                -> Maybe (Map.Map Word32 Word32)
-getAttributeMap classAttributeMap actorMap actorId = do
-  objectId <- Map.lookup actorId actorMap
-  let objectClassMap = classAttributeMapObjectClassMap classAttributeMap
-  classId <- Map.lookup objectId objectClassMap
-  let value = classAttributeMapValue classAttributeMap
-  Map.lookup classId value
diff --git a/library/Rattletrap/ClassMapping.hs b/library/Rattletrap/ClassMapping.hs
deleted file mode 100644
--- a/library/Rattletrap/ClassMapping.hs
+++ /dev/null
@@ -1,21 +0,0 @@
-module Rattletrap.ClassMapping where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary as Binary
-
-data ClassMapping = ClassMapping
-  { classMappingName :: Text
-  , classMappingStreamId :: Word32
-  } deriving (Eq, Ord, Show)
-
-getClassMapping :: Binary.Get ClassMapping
-getClassMapping = do
-  name <- getText
-  streamId <- getWord32
-  pure (ClassMapping name streamId)
-
-putClassMapping :: ClassMapping -> Binary.Put
-putClassMapping classMapping = do
-  putText (classMappingName classMapping)
-  putWord32 (classMappingStreamId classMapping)
diff --git a/library/Rattletrap/Content.hs b/library/Rattletrap/Content.hs
deleted file mode 100644
--- a/library/Rattletrap/Content.hs
+++ /dev/null
@@ -1,92 +0,0 @@
-module Rattletrap.Content where
-
-import Rattletrap.ActorMap
-import Rattletrap.Cache
-import Rattletrap.ClassAttributeMap
-import Rattletrap.ClassMapping
-import Rattletrap.Frame
-import Rattletrap.KeyFrame
-import Rattletrap.Mark
-import Rattletrap.Message
-import Rattletrap.Primitive
-import Rattletrap.Utility
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-
-data Content = Content
-  { contentLevels :: List Text
-  , contentKeyFrames :: List KeyFrame
-  , contentStreamSize :: Word32
-  , contentFrames :: [Frame]
-  , contentTrailingBits :: [Bool]
-  , 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, remainingBits) =
-        Binary.runGet
-          (BinaryBit.runBitGet
-             (do (theFrames, _) <-
-                   getFrames version numFrames classAttributeMap makeActorMap
-                 theRemainingBits <- getRemainingBits
-                 pure (theFrames, theRemainingBits)))
-          (reverseBytes stream)
-  let trailingBits = reverse (dropWhile not (reverse remainingBits))
-  pure
-    (Content
-       levels
-       keyFrames
-       streamSize
-       frames
-       trailingBits
-       messages
-       marks
-       packages
-       objects
-       names
-       classMappings
-       caches)
-
-putContent :: Content -> Binary.Put
-putContent content = do
-  putList putText (contentLevels content)
-  putList putKeyFrame (contentKeyFrames content)
-  let streamSize = contentStreamSize content
-  putWord32 streamSize
-  let stream =
-        Binary.runPut
-          (BinaryBit.runBitPut
-             (do putFrames (contentFrames content)
-                 mapM_ BinaryBit.putBool (contentTrailingBits content)))
-  Binary.putLazyByteString
-    (reverseBytes (padBytes (word32Value streamSize) stream))
-  putList putMessage (contentMessages content)
-  putList putMark (contentMarks content)
-  putList putText (contentPackages content)
-  putList putText (contentObjects content)
-  putList putText (contentNames content)
-  putList putClassMapping (contentClassMappings content)
-  putList putCache (contentCaches content)
diff --git a/library/Rattletrap/Crc.hs b/library/Rattletrap/Crc.hs
deleted file mode 100644
--- a/library/Rattletrap/Crc.hs
+++ /dev/null
@@ -1,37 +0,0 @@
-module Rattletrap.Crc where
-
-import Rattletrap.Data
-
-import qualified Data.Bits as Bits
-import qualified Data.ByteString.Lazy as ByteString
-import qualified Data.Vector.Unboxed as Vector
-import qualified Data.Word as Word
-
-getCrc32 :: ByteString.ByteString -> Word.Word32
-getCrc32 bytes = do
-  let update = crc32Update crc32Table
-  let initial = Bits.complement crc32Initial
-  let crc = ByteString.foldl update initial bytes
-  Bits.complement crc
-
-crc32Update :: Vector.Vector Word.Word32
-            -> Word.Word32
-            -> Word.Word8
-            -> Word.Word32
-crc32Update table crc byte = do
-  let toWord8 =
-        fromIntegral :: (Integral a) =>
-                          a -> Word.Word8
-  let toInt =
-        fromIntegral :: (Integral a) =>
-                          a -> Int
-  let index = toInt (Bits.xor byte (toWord8 (Bits.shiftR crc 24)))
-  let left = Vector.unsafeIndex table index
-  let right = Bits.shiftL crc 8
-  Bits.xor left right
-
-crc32Initial :: Word.Word32
-crc32Initial = 0xefcbf201
-
-crc32Table :: Vector.Vector Word.Word32
-crc32Table = Vector.fromList rawCrc32Table
diff --git a/library/Rattletrap/Data.hs b/library/Rattletrap/Data.hs
deleted file mode 100644
--- a/library/Rattletrap/Data.hs
+++ /dev/null
@@ -1,351 +0,0 @@
-module Rattletrap.Data where
-
-import Rattletrap.AttributeType
-
-rawParentClasses :: [(String, String)]
-rawParentClasses =
-  [ ("Engine.Actor", "Core.Object")
-  , ("Engine.GameReplicationInfo", "Engine.ReplicationInfo")
-  , ("Engine.Info", "Engine.Actor")
-  , ("Engine.Pawn", "Engine.Actor")
-  , ("Engine.PlayerReplicationInfo", "Engine.ReplicationInfo")
-  , ("Engine.ReplicationInfo", "Engine.Info")
-  , ("Engine.TeamInfo", "Engine.ReplicationInfo")
-  , ("ProjectX.GRI_X", "Engine.GameReplicationInfo")
-  , ("ProjectX.PRI_X", "Engine.PlayerReplicationInfo")
-  , ("ProjectX.Pawn_X", "Engine.Pawn")
-  , ("TAGame.Ball_TA", "TAGame.RBActor_TA")
-  , ("TAGame.CameraSettingsActor_TA", "Engine.ReplicationInfo")
-  , ("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.Car_Season_TA", "TAGame.PRI_TA")
-  , ("TAGame.Car_TA", "TAGame.Vehicle_TA")
-  , ("TAGame.CrowdActor_TA", "Engine.ReplicationInfo")
-  , ("TAGame.CrowdManager_TA", "Engine.ReplicationInfo")
-  , ("TAGame.GRI_TA", "ProjectX.GRI_X")
-  , ("TAGame.GameEvent_Season_TA", "TAGame.GameEvent_Soccar_TA")
-  , ("TAGame.GameEvent_SoccarPrivate_TA", "TAGame.GameEvent_Soccar_TA")
-  , ("TAGame.GameEvent_SoccarSplitscreen_TA", "TAGame.GameEvent_SoccarPrivate_TA")
-  , ("TAGame.GameEvent_Soccar_TA", "TAGame.GameEvent_Team_TA")
-  , ("TAGame.GameEvent_TA", "Engine.ReplicationInfo")
-  , ("TAGame.GameEvent_Team_TA", "TAGame.GameEvent_TA")
-  , ("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.VehiclePickup_Boost_TA", "TAGame.VehiclePickup_TA")
-  , ("TAGame.VehiclePickup_TA", "Engine.ReplicationInfo")
-  , ("TAGame.Vehicle_TA", "TAGame.RBActor_TA")
-  , ("TAGame.VoteActor_TA", "Engine.Actor")
-  ]
-
-rawClassesWithLocation :: [String]
-rawClassesWithLocation =
-  [ "TAGame.Ball_TA"
-  , "TAGame.CameraSettingsActor_TA"
-  , "TAGame.CarComponent_Boost_TA"
-  , "TAGame.CarComponent_Dodge_TA"
-  , "TAGame.CarComponent_DoubleJump_TA"
-  , "TAGame.CarComponent_FlipCar_TA"
-  , "TAGame.CarComponent_Jump_TA"
-  , "TAGame.Car_Season_TA"
-  , "TAGame.Car_TA"
-  , "TAGame.GRI_TA"
-  , "TAGame.GameEvent_Season_TA"
-  , "TAGame.GameEvent_SoccarPrivate_TA"
-  , "TAGame.GameEvent_SoccarSplitscreen_TA"
-  , "TAGame.GameEvent_Soccar_TA"
-  , "TAGame.PRI_TA"
-  , "TAGame.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_Hockey", "TAGame.GameEvent_Soccar_TA")
-  , ("Archetypes.GameEvent.GameEvent_HockeyPrivate", "TAGame.GameEvent_SoccarPrivate_TA")
-  , ("Archetypes.GameEvent.GameEvent_HockeySplitscreen", "TAGame.GameEvent_SoccarSplitscreen_TA")
-  , ("Archetypes.GameEvent.GameEvent_Items", "TAGame.GameEvent_Soccar_TA")
-  , ("Archetypes.GameEvent.GameEvent_Season", "TAGame.GameEvent_Season_TA")
-  , ("Archetypes.GameEvent.GameEvent_Season:CarArchetype", "TAGame.Car_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_Items.GameInfo.GameInfo_Items:GameReplicationInfoArchetype", "TAGame.GRI_TA")
-  , ("GameInfo_Season.GameInfo.GameInfo_Season:GameReplicationInfoArchetype", "TAGame.GRI_TA")
-  , ("GameInfo_Soccar.GameInfo.GameInfo_Soccar:GameReplicationInfoArchetype", "TAGame.GRI_TA")
-  , ("Gameinfo_Hockey.GameInfo.Gameinfo_Hockey:GameReplicationInfoArchetype", "TAGame.GRI_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")
-  ]
-
-rawAttributeTypes :: [(String, AttributeType)]
-rawAttributeTypes =
-  [ ("Engine.Actor:DrawScale", FloatAttributeType)
-  , ("Engine.Actor:Role", EnumAttributeType)
-  , ("Engine.Actor:bBlockActors", BooleanAttributeType)
-  , ("Engine.Actor:bCollideActors", BooleanAttributeType)
-  , ("Engine.Actor:bHidden", BooleanAttributeType)
-  , ("Engine.GameReplicationInfo:GameClass", FlaggedIntAttributeType)
-  , ("Engine.GameReplicationInfo:ServerName", StringAttributeType)
-  , ("Engine.GameReplicationInfo:bMatchIsOver", BooleanAttributeType)
-  , ("Engine.Pawn:PlayerReplicationInfo", FlaggedIntAttributeType)
-  , ("Engine.PlayerReplicationInfo:Ping", ByteAttributeType)
-  , ("Engine.PlayerReplicationInfo:PlayerID", IntAttributeType)
-  , ("Engine.PlayerReplicationInfo:PlayerName", StringAttributeType)
-  , ("Engine.PlayerReplicationInfo:RemoteUserData", StringAttributeType)
-  , ("Engine.PlayerReplicationInfo:Score", IntAttributeType)
-  , ("Engine.PlayerReplicationInfo:Team", FlaggedIntAttributeType)
-  , ("Engine.PlayerReplicationInfo:UniqueId", UniqueIdAttributeType)
-  , ("Engine.PlayerReplicationInfo:bBot", BooleanAttributeType)
-  , ("Engine.PlayerReplicationInfo:bIsSpectator", BooleanAttributeType)
-  , ("Engine.PlayerReplicationInfo:bReadyToPlay", BooleanAttributeType)
-  , ("Engine.PlayerReplicationInfo:bWaitingPlayer", BooleanAttributeType)
-  , ("Engine.TeamInfo:Score", IntAttributeType)
-  , ("ProjectX.GRI_X:GameServerID", QWordAttributeType)
-  , ("ProjectX.GRI_X:ReplicatedGameMutatorIndex", IntAttributeType)
-  , ("ProjectX.GRI_X:ReplicatedGamePlaylist", IntAttributeType)
-  , ("ProjectX.GRI_X:Reservations", ReservationAttributeType)
-  , ("ProjectX.GRI_X:bGameStarted", BooleanAttributeType)
-  , ("TAGame.Ball_TA:GameEvent", FlaggedIntAttributeType)
-  , ("TAGame.Ball_TA:HitTeamNum", ByteAttributeType)
-  , ("TAGame.Ball_TA:ReplicatedAddedCarBounceScale", FloatAttributeType)
-  , ("TAGame.Ball_TA:ReplicatedBallMaxLinearSpeedScale", FloatAttributeType)
-  , ("TAGame.Ball_TA:ReplicatedBallScale", FloatAttributeType)
-  , ("TAGame.Ball_TA:ReplicatedExplosionData", ExplosionAttributeType)
-  , ("TAGame.Ball_TA:ReplicatedWorldBounceScale", FloatAttributeType)
-  , ("TAGame.CameraSettingsActor_TA:CameraPitch", ByteAttributeType)
-  , ("TAGame.CameraSettingsActor_TA:CameraYaw", ByteAttributeType)
-  , ("TAGame.CameraSettingsActor_TA:PRI", FlaggedIntAttributeType)
-  , ("TAGame.CameraSettingsActor_TA:ProfileSettings", CamSettingsAttributeType)
-  , ("TAGame.CameraSettingsActor_TA:bUsingBehindView", BooleanAttributeType)
-  , ("TAGame.CameraSettingsActor_TA:bUsingSecondaryCamera", BooleanAttributeType)
-  , ("TAGame.CarComponent_Boost_TA:BoostModifier", FloatAttributeType)
-  , ("TAGame.CarComponent_Boost_TA:RechargeDelay", FloatAttributeType)
-  , ("TAGame.CarComponent_Boost_TA:RechargeRate", FloatAttributeType)
-  , ("TAGame.CarComponent_Boost_TA:ReplicatedBoostAmount", ByteAttributeType)
-  , ("TAGame.CarComponent_Boost_TA:UnlimitedBoostRefCount", IntAttributeType)
-  , ("TAGame.CarComponent_Boost_TA:bNoBoost", BooleanAttributeType)
-  , ("TAGame.CarComponent_Boost_TA:bUnlimitedBoost", BooleanAttributeType)
-  , ("TAGame.CarComponent_Dodge_TA:DodgeTorque", LocationAttributeType)
-  , ("TAGame.CarComponent_FlipCar_TA:FlipCarTime", FloatAttributeType)
-  , ("TAGame.CarComponent_FlipCar_TA:bFlipRight", BooleanAttributeType)
-  , ("TAGame.CarComponent_TA:ReplicatedActive", ByteAttributeType)
-  , ("TAGame.CarComponent_TA:ReplicatedActivityTime", FloatAttributeType)
-  , ("TAGame.CarComponent_TA:Vehicle", FlaggedIntAttributeType)
-  , ("TAGame.Car_TA:AddedBallForceMultiplier", FloatAttributeType)
-  , ("TAGame.Car_TA:AddedCarForceMultiplier", FloatAttributeType)
-  , ("TAGame.Car_TA:AttachedPickup", FlaggedIntAttributeType)
-  , ("TAGame.Car_TA:ClubColors", ClubColorsAttributeType)
-  , ("TAGame.Car_TA:ReplicatedDemolish", DemolishAttributeType)
-  , ("TAGame.Car_TA:TeamPaint", TeamPaintAttributeType)
-  , ("TAGame.CrowdActor_TA:GameEvent", FlaggedIntAttributeType)
-  , ("TAGame.CrowdActor_TA:ModifiedNoise", FloatAttributeType)
-  , ("TAGame.CrowdActor_TA:ReplicatedCountDownNumber", IntAttributeType)
-  , ("TAGame.CrowdActor_TA:ReplicatedOneShotSound", FlaggedIntAttributeType)
-  , ("TAGame.CrowdActor_TA:ReplicatedRoundCountDownNumber", IntAttributeType)
-  , ("TAGame.CrowdManager_TA:GameEvent", FlaggedIntAttributeType)
-  , ("TAGame.CrowdManager_TA:ReplicatedGlobalOneShotSound", FlaggedIntAttributeType)
-  , ("TAGame.GRI_TA:NewDedicatedServerIP", StringAttributeType)
-  , ("TAGame.GameEvent_SoccarPrivate_TA:MatchSettings", PrivateMatchSettingsAttributeType)
-  , ("TAGame.GameEvent_Soccar_TA:ReplicatedMusicStinger", MusicStingerAttributeType)
-  , ("TAGame.GameEvent_Soccar_TA:ReplicatedScoredOnTeam", ByteAttributeType)
-  , ("TAGame.GameEvent_Soccar_TA:RoundNum", IntAttributeType)
-  , ("TAGame.GameEvent_Soccar_TA:SecondsRemaining", IntAttributeType)
-  , ("TAGame.GameEvent_Soccar_TA:SubRulesArchetype", FlaggedIntAttributeType)
-  , ("TAGame.GameEvent_Soccar_TA:bBallHasBeenHit", BooleanAttributeType)
-  , ("TAGame.GameEvent_Soccar_TA:bOverTime", BooleanAttributeType)
-  , ("TAGame.GameEvent_TA:BotSkill", IntAttributeType)
-  , ("TAGame.GameEvent_TA:GameMode", GameModeAttributeType)
-  , ("TAGame.GameEvent_TA:MatchTypeClass", FlaggedIntAttributeType)
-  , ("TAGame.GameEvent_TA:ReplicatedGameStateTimeRemaining", IntAttributeType)
-  , ("TAGame.GameEvent_TA:ReplicatedStateIndex", ByteAttributeType)
-  , ("TAGame.GameEvent_TA:ReplicatedStateName", IntAttributeType)
-  , ("TAGame.GameEvent_TA:bCanVoteToForfeit", BooleanAttributeType)
-  , ("TAGame.GameEvent_TA:bHasLeaveMatchPenalty", BooleanAttributeType)
-  , ("TAGame.GameEvent_Team_TA:MaxTeamSize", IntAttributeType)
-  , ("TAGame.PRI_TA:CameraPitch", ByteAttributeType)
-  , ("TAGame.PRI_TA:CameraSettings", CamSettingsAttributeType)
-  , ("TAGame.PRI_TA:CameraYaw", ByteAttributeType)
-  , ("TAGame.PRI_TA:ClientLoadout", LoadoutAttributeType)
-  , ("TAGame.PRI_TA:ClientLoadoutOnline", LoadoutOnlineAttributeType)
-  , ("TAGame.PRI_TA:ClientLoadouts", LoadoutsAttributeType)
-  , ("TAGame.PRI_TA:ClientLoadoutsOnline", LoadoutsOnlineAttributeType)
-  , ("TAGame.PRI_TA:MatchAssists", IntAttributeType)
-  , ("TAGame.PRI_TA:MatchGoals", IntAttributeType)
-  , ("TAGame.PRI_TA:MatchSaves", IntAttributeType)
-  , ("TAGame.PRI_TA:MatchScore", IntAttributeType)
-  , ("TAGame.PRI_TA:MatchShots", IntAttributeType)
-  , ("TAGame.PRI_TA:PartyLeader", PartyLeaderAttributeType)
-  , ("TAGame.PRI_TA:PawnType", ByteAttributeType)
-  , ("TAGame.PRI_TA:PersistentCamera", FlaggedIntAttributeType)
-  , ("TAGame.PRI_TA:ReplicatedGameEvent", FlaggedIntAttributeType)
-  , ("TAGame.PRI_TA:Title", IntAttributeType)
-  , ("TAGame.PRI_TA:TotalXP", IntAttributeType)
-  , ("TAGame.PRI_TA:bIsInSplitScreen", BooleanAttributeType)
-  , ("TAGame.PRI_TA:bMatchMVP", BooleanAttributeType)
-  , ("TAGame.PRI_TA:bOnlineLoadoutSet", BooleanAttributeType)
-  , ("TAGame.PRI_TA:bOnlineLoadoutsSet", BooleanAttributeType)
-  , ("TAGame.PRI_TA:bReady", BooleanAttributeType)
-  , ("TAGame.PRI_TA:bUsingBehindView", BooleanAttributeType)
-  , ("TAGame.PRI_TA:bUsingSecondaryCamera", BooleanAttributeType)
-  , ("TAGame.RBActor_TA:ReplicatedRBState", RigidBodyStateAttributeType)
-  , ("TAGame.RBActor_TA:WeldedInfo", WeldedInfoAttributeType)
-  , ("TAGame.RBActor_TA:bFrozen", BooleanAttributeType)
-  , ("TAGame.RBActor_TA:bIgnoreSyncing", BooleanAttributeType)
-  , ("TAGame.RBActor_TA:bReplayActor", BooleanAttributeType)
-  , ("TAGame.SpecialPickup_BallFreeze_TA:RepOrigSpeed", FloatAttributeType)
-  , ("TAGame.SpecialPickup_BallVelcro_TA:AttachTime", FloatAttributeType)
-  , ("TAGame.SpecialPickup_BallVelcro_TA:BreakTime", FloatAttributeType)
-  , ("TAGame.SpecialPickup_BallVelcro_TA:bBroken", BooleanAttributeType)
-  , ("TAGame.SpecialPickup_BallVelcro_TA:bHit", BooleanAttributeType)
-  , ("TAGame.SpecialPickup_Targeted_TA:Targeted", FlaggedIntAttributeType)
-  , ("TAGame.Team_Soccar_TA:GameScore", IntAttributeType)
-  , ("TAGame.Team_TA:ClubColors", ClubColorsAttributeType)
-  , ("TAGame.Team_TA:CustomTeamName", StringAttributeType)
-  , ("TAGame.Team_TA:GameEvent", FlaggedIntAttributeType)
-  , ("TAGame.Team_TA:LogoData", FlaggedIntAttributeType)
-  , ("TAGame.VehiclePickup_TA:ReplicatedPickupData", PickupAttributeType)
-  , ("TAGame.Vehicle_TA:ReplicatedSteer", ByteAttributeType)
-  , ("TAGame.Vehicle_TA:ReplicatedThrottle", ByteAttributeType)
-  , ("TAGame.Vehicle_TA:bDriving", BooleanAttributeType)
-  , ("TAGame.Vehicle_TA:bReplicatedHandbrake", BooleanAttributeType)
-  ]
-
-rawCrc32Table :: Integral a => [a]
-rawCrc32Table =
-  [ 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9
-  , 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005
-  , 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61
-  , 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd
-  , 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9
-  , 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75
-  , 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011
-  , 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd
-  , 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039
-  , 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5
-  , 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81
-  , 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d
-  , 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49
-  , 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95
-  , 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1
-  , 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d
-  , 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae
-  , 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072
-  , 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16
-  , 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca
-  , 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde
-  , 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02
-  , 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066
-  , 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba
-  , 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e
-  , 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692
-  , 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6
-  , 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a
-  , 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e
-  , 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2
-  , 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686
-  , 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a
-  , 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637
-  , 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb
-  , 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f
-  , 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53
-  , 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47
-  , 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b
-  , 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff
-  , 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623
-  , 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7
-  , 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b
-  , 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f
-  , 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3
-  , 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7
-  , 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b
-  , 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f
-  , 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3
-  , 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640
-  , 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c
-  , 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8
-  , 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24
-  , 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30
-  , 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec
-  , 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088
-  , 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654
-  , 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0
-  , 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c
-  , 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18
-  , 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4
-  , 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0
-  , 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c
-  , 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668
-  , 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4
-  ]
diff --git a/library/Rattletrap/Frame.hs b/library/Rattletrap/Frame.hs
deleted file mode 100644
--- a/library/Rattletrap/Frame.hs
+++ /dev/null
@@ -1,51 +0,0 @@
-module Rattletrap.Frame where
-
-import Rattletrap.ActorMap
-import Rattletrap.ClassAttributeMap
-import Rattletrap.Primitive
-import Rattletrap.Replication
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data Frame = Frame
-  { frameTime :: Float32
-  , frameDelta :: Float32
-  , frameReplications :: [Replication]
-  } deriving (Eq, Ord, Show)
-
-getFrames
-  :: (Int, Int)
-  -> Int
-  -> ClassAttributeMap
-  -> ActorMap
-  -> BinaryBit.BitGet ([Frame], ActorMap)
-getFrames version numFrames classAttributeMap actorMap =
-  if numFrames <= 0
-    then pure ([], actorMap)
-    else do
-      (frame, newActorMap) <- getFrame version classAttributeMap actorMap
-      (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 (Frame, ActorMap)
-getFrame version classAttributeMap actorMap = do
-  time <- getFloat32Bits
-  delta <- getFloat32Bits
-  (replications, newActorMap) <-
-    getReplications version classAttributeMap actorMap
-  pure (Frame time delta replications, newActorMap)
-
-putFrame :: Frame -> BinaryBit.BitPut ()
-putFrame frame = do
-  putFloat32Bits (frameTime frame)
-  putFloat32Bits (frameDelta frame)
-  putReplications (frameReplications frame)
diff --git a/library/Rattletrap/Header.hs b/library/Rattletrap/Header.hs
deleted file mode 100644
--- a/library/Rattletrap/Header.hs
+++ /dev/null
@@ -1,59 +0,0 @@
-module Rattletrap.Header where
-
-import Rattletrap.Primitive
-import Rattletrap.Property
-import Rattletrap.PropertyValue
-
-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 engineVersion licenseeVersion label properties)
-
-putHeader :: Header -> Binary.Put
-putHeader header = do
-  putWord32 (headerEngineVersion header)
-  putWord32 (headerLicenseeVersion header)
-  putText (headerLabel header)
-  putDictionary putProperty (headerProperties header)
-
-getVersion
-  :: (Integral a, Integral b)
-  => Header -> (a, b)
-getVersion header =
-  let major = getMajorVersion header
-      minor = getMinorVersion header
-  in (major, minor)
-
-getMajorVersion
-  :: (Integral a)
-  => Header -> a
-getMajorVersion header = fromIntegral (word32Value (headerEngineVersion header))
-
-getMinorVersion
-  :: (Integral a)
-  => Header -> a
-getMinorVersion header =
-  fromIntegral (word32Value (headerLicenseeVersion header))
-
-getNumFrames
-  :: (Integral a)
-  => Header -> a
-getNumFrames header =
-  let key = stringToText "NumFrames"
-      properties = dictionaryValue (headerProperties header)
-  in case lookup key properties of
-       Just (Just (Property _ _ (IntProperty numFrames))) ->
-         fromIntegral (int32Value numFrames)
-       _ -> 0
diff --git a/library/Rattletrap/Initialization.hs b/library/Rattletrap/Initialization.hs
deleted file mode 100644
--- a/library/Rattletrap/Initialization.hs
+++ /dev/null
@@ -1,36 +0,0 @@
-module Rattletrap.Initialization where
-
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data Initialization = Initialization
-  { initializationLocation :: Maybe Vector
-  , 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 location rotation)
-
-putInitialization :: Initialization -> BinaryBit.BitPut ()
-putInitialization initialization = do
-  case initializationLocation initialization of
-    Nothing -> pure ()
-    Just location -> putVector location
-  case initializationRotation initialization of
-    Nothing -> pure ()
-    Just rotation -> putInt8Vector rotation
diff --git a/library/Rattletrap/Json.hs b/library/Rattletrap/Json.hs
deleted file mode 100644
--- a/library/Rattletrap/Json.hs
+++ /dev/null
@@ -1,105 +0,0 @@
-{-# LANGUAGE TemplateHaskell #-}
-{-# OPTIONS_GHC -fno-warn-orphans #-}
-
-module Rattletrap.Json where
-
-import Rattletrap.Attribute
-import Rattletrap.AttributeMapping
-import Rattletrap.AttributeValue
-import Rattletrap.Cache
-import Rattletrap.ClassMapping
-import Rattletrap.Content
-import Rattletrap.Frame
-import Rattletrap.Header
-import Rattletrap.Initialization
-import Rattletrap.KeyFrame
-import Rattletrap.Mark
-import Rattletrap.Message
-import Rattletrap.Primitive
-import Rattletrap.Property
-import Rattletrap.PropertyValue
-import Rattletrap.RemoteId
-import Rattletrap.Replay
-import Rattletrap.Replication
-import Rattletrap.ReplicationValue
-
-import qualified Control.Monad as Monad
-import qualified Data.Aeson.Casing as Casing
-import qualified Data.Aeson.TH as Aeson
-import qualified Language.Haskell.TH as TH
-
-$(let optionsFor name =
-        (Casing.aesonDrop (length (TH.nameBase name)) Casing.snakeCase)
-        { Aeson.constructorTagModifier = Casing.snakeCase
-        , Aeson.omitNothingFields = True
-        , Aeson.sumEncoding = Aeson.ObjectWithSingleField
-        , Aeson.unwrapUnaryRecords = True
-        }
-      deriveJSON declarations name = do
-        newDeclarations <- Aeson.deriveJSON (optionsFor name) name
-        pure (newDeclarations ++ declarations)
-      names =
-        [ ''Attribute
-        , ''AttributeMapping
-        , ''AttributeValue
-        , ''BooleanAttribute
-        , ''ByteAttribute
-        , ''Cache
-        , ''CamSettingsAttribute
-        , ''ClassMapping
-        , ''ClubColorsAttribute
-        , ''CompressedWord
-        , ''CompressedWordVector
-        , ''Content
-        , ''DemolishAttribute
-        , ''DestroyedReplication
-        , ''Dictionary
-        , ''EnumAttribute
-        , ''ExplosionAttribute
-        , ''FlaggedIntAttribute
-        , ''Float32
-        , ''FloatAttribute
-        , ''Frame
-        , ''GameModeAttribute
-        , ''Header
-        , ''Initialization
-        , ''Int32
-        , ''Int8
-        , ''Int8Vector
-        , ''IntAttribute
-        , ''KeyFrame
-        , ''List
-        , ''LoadoutAttribute
-        , ''LoadoutOnlineAttribute
-        , ''LoadoutsAttribute
-        , ''LoadoutsOnlineAttribute
-        , ''LocationAttribute
-        , ''Mark
-        , ''Message
-        , ''MusicStingerAttribute
-        , ''PartyLeaderAttribute
-        , ''PickupAttribute
-        , ''PrivateMatchSettingsAttribute
-        , ''Property
-        , ''PropertyValue
-        , ''QWordAttribute
-        , ''RemoteId
-        , ''Replay
-        , ''Replication
-        , ''ReplicationValue
-        , ''ReservationAttribute
-        , ''RigidBodyStateAttribute
-        , ''Section
-        , ''SpawnedReplication
-        , ''StringAttribute
-        , ''TeamPaintAttribute
-        , ''Text
-        , ''UniqueIdAttribute
-        , ''UpdatedReplication
-        , ''Vector
-        , ''WeldedInfoAttribute
-        , ''Word32
-        , ''Word64
-        , ''Word8
-        ]
-  in Monad.foldM deriveJSON [] names)
diff --git a/library/Rattletrap/KeyFrame.hs b/library/Rattletrap/KeyFrame.hs
deleted file mode 100644
--- a/library/Rattletrap/KeyFrame.hs
+++ /dev/null
@@ -1,24 +0,0 @@
-module Rattletrap.KeyFrame where
-
-import Rattletrap.Primitive
-
-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 time frame position)
-
-putKeyFrame :: KeyFrame -> Binary.Put
-putKeyFrame keyFrame = do
-  putFloat32 (keyFrameTime keyFrame)
-  putWord32 (keyFrameFrame keyFrame)
-  putWord32 (keyFramePosition keyFrame)
diff --git a/library/Rattletrap/Main.hs b/library/Rattletrap/Main.hs
deleted file mode 100644
--- a/library/Rattletrap/Main.hs
+++ /dev/null
@@ -1,47 +0,0 @@
-module Rattletrap.Main where
-
-import Rattletrap.Json ()
-import Rattletrap.Replay
-import Rattletrap.Version
-
-import qualified Data.Aeson as Aeson
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.ByteString.Lazy as ByteString
-import qualified Data.Version as Version
-import qualified System.Environment as Environment
-
-main :: IO ()
-main = do
-  args <- Environment.getArgs
-  mainWithArgs args
-
-mainWithArgs :: [String] -> IO ()
-mainWithArgs args =
-  case args of
-    ["version"] -> putStrLn (Version.showVersion version)
-    "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)
diff --git a/library/Rattletrap/Mark.hs b/library/Rattletrap/Mark.hs
deleted file mode 100644
--- a/library/Rattletrap/Mark.hs
+++ /dev/null
@@ -1,21 +0,0 @@
-module Rattletrap.Mark where
-
-import Rattletrap.Primitive
-
-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 value frame)
-
-putMark :: Mark -> Binary.Put
-putMark mark = do
-  putText (markValue mark)
-  putWord32 (markFrame mark)
diff --git a/library/Rattletrap/Message.hs b/library/Rattletrap/Message.hs
deleted file mode 100644
--- a/library/Rattletrap/Message.hs
+++ /dev/null
@@ -1,24 +0,0 @@
-module Rattletrap.Message where
-
-import Rattletrap.Primitive
-
-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 frame name value)
-
-putMessage :: Message -> Binary.Put
-putMessage message = do
-  putWord32 (messageFrame message)
-  putText (messageName message)
-  putText (messageValue message)
diff --git a/library/Rattletrap/Primitive.hs b/library/Rattletrap/Primitive.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module Rattletrap.Primitive
-  ( module Rattletrap.Primitive.CompressedWord
-  , module Rattletrap.Primitive.CompressedWordVector
-  , module Rattletrap.Primitive.Dictionary
-  , module Rattletrap.Primitive.Float32
-  , module Rattletrap.Primitive.Int8
-  , module Rattletrap.Primitive.Int8Vector
-  , module Rattletrap.Primitive.Int32
-  , module Rattletrap.Primitive.List
-  , module Rattletrap.Primitive.Section
-  , module Rattletrap.Primitive.Text
-  , module Rattletrap.Primitive.Vector
-  , module Rattletrap.Primitive.Word8
-  , module Rattletrap.Primitive.Word32
-  , module Rattletrap.Primitive.Word64
-  ) where
-
-import Rattletrap.Primitive.CompressedWord
-import Rattletrap.Primitive.CompressedWordVector
-import Rattletrap.Primitive.Dictionary
-import Rattletrap.Primitive.Float32
-import Rattletrap.Primitive.Int8
-import Rattletrap.Primitive.Int8Vector
-import Rattletrap.Primitive.Int32
-import Rattletrap.Primitive.List
-import Rattletrap.Primitive.Section
-import Rattletrap.Primitive.Text
-import Rattletrap.Primitive.Vector
-import Rattletrap.Primitive.Word8
-import Rattletrap.Primitive.Word32
-import Rattletrap.Primitive.Word64
diff --git a/library/Rattletrap/Primitive/CompressedWord.hs b/library/Rattletrap/Primitive/CompressedWord.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/CompressedWord.hs
+++ /dev/null
@@ -1,59 +0,0 @@
-module Rattletrap.Primitive.CompressedWord where
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Bits as Bits
-
-data CompressedWord = CompressedWord
-  { compressedWordLimit :: Word
-  , compressedWordValue :: Word
-  } deriving (Eq, Ord, Show)
-
-getCompressedWord :: Word -> BinaryBit.BitGet CompressedWord
-getCompressedWord limit = do
-  value <- getCompressedWordStep limit (getMaxBits limit) 0 0
-  pure (CompressedWord limit value)
-
-putCompressedWord :: CompressedWord -> BinaryBit.BitPut ()
-putCompressedWord compressedWord = do
-  let limit = compressedWordLimit compressedWord
-  let value = compressedWordValue compressedWord
-  let maxBits = getMaxBits limit
-  let go position soFar =
-        if position < maxBits
-          then do
-            let x = Bits.shiftL 1 position
-            if maxBits > 1 && position == maxBits - 1 && soFar + x > limit
-              then pure ()
-              else do
-                let bit = Bits.testBit value position
-                BinaryBit.putBool bit
-                let delta =
-                      if bit
-                        then x
-                        else 0
-                go (position + 1) (soFar + delta)
-          else pure ()
-  go 0 0
-
-getMaxBits
-  :: (Integral a, Integral b)
-  => a -> b
-getMaxBits x = do
-  let n = max 1 (ceiling (logBase (2 :: Double) (fromIntegral (max 1 x))))
-  if x < 1024 && x == 2 ^ n
-    then n + 1
-    else n
-
-getCompressedWordStep :: Word -> Word -> Word -> Word -> BinaryBit.BitGet Word
-getCompressedWordStep limit maxBits position value = do
-  let x = Bits.shiftL 1 (fromIntegral position)
-  if position < maxBits && value + x <= limit
-    then do
-      bit <- BinaryBit.getBool
-      let newValue =
-            if bit
-              then value + x
-              else value
-      getCompressedWordStep limit maxBits (position + 1) newValue
-    else pure value
diff --git a/library/Rattletrap/Primitive/CompressedWordVector.hs b/library/Rattletrap/Primitive/CompressedWordVector.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/CompressedWordVector.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module Rattletrap.Primitive.CompressedWordVector where
-
-import Rattletrap.Primitive.CompressedWord
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data CompressedWordVector = CompressedWordVector
-  { compressedWordVectorX :: CompressedWord
-  , compressedWordVectorY :: CompressedWord
-  , compressedWordVectorZ :: CompressedWord
-  } deriving (Eq, Ord, Show)
-
-getCompressedWordVector :: BinaryBit.BitGet CompressedWordVector
-getCompressedWordVector = do
-  let limit = 65536
-  x <- getCompressedWord limit
-  y <- getCompressedWord limit
-  z <- getCompressedWord limit
-  pure (CompressedWordVector x y z)
-
-putCompressedWordVector :: CompressedWordVector -> BinaryBit.BitPut ()
-putCompressedWordVector compressedWordVector = do
-  putCompressedWord (compressedWordVectorX compressedWordVector)
-  putCompressedWord (compressedWordVectorY compressedWordVector)
-  putCompressedWord (compressedWordVectorZ compressedWordVector)
diff --git a/library/Rattletrap/Primitive/Dictionary.hs b/library/Rattletrap/Primitive/Dictionary.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Dictionary.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Rattletrap.Primitive.Dictionary where
-
-import Rattletrap.Primitive.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 =
-  mapM_
-    (\(key, maybeValue) -> do
-       putText key
-       case maybeValue of
-         Nothing -> pure ()
-         Just value -> putValue value)
-    (dictionaryValue dictionary)
-
-isNoneKey :: Text -> Bool
-isNoneKey text = filter (/= '\x00') (textToString text) == "None"
diff --git a/library/Rattletrap/Primitive/Float32.hs b/library/Rattletrap/Primitive/Float32.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Float32.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Rattletrap.Primitive.Float32 where
-
-import Rattletrap.Utility
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.IEEE754 as IEEE754
-import qualified Data.Binary.Put as Binary
-import qualified Data.ByteString.Lazy as ByteString
-
-newtype Float32 = Float32
-  { float32Value :: Float
-  } deriving (Eq, Ord, Show)
-
-getFloat32 :: Binary.Get Float32
-getFloat32 = do
-  float32 <- IEEE754.getFloat32le
-  pure (Float32 float32)
-
-putFloat32 :: Float32 -> Binary.Put
-putFloat32 float32 = IEEE754.putFloat32le (float32Value float32)
-
-getFloat32Bits :: BinaryBit.BitGet Float32
-getFloat32Bits = do
-  bytes <- BinaryBit.getLazyByteString 4
-  pure (Binary.runGet getFloat32 (reverseBytes bytes))
-
-putFloat32Bits :: Float32 -> BinaryBit.BitPut ()
-putFloat32Bits float32 = do
-  let bytes = Binary.runPut (putFloat32 float32)
-  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Primitive/Int32.hs b/library/Rattletrap/Primitive/Int32.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Int32.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Rattletrap.Primitive.Int32 where
-
-import Rattletrap.Utility
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.ByteString.Lazy as ByteString
-import qualified Data.Int as Int
-
-newtype Int32 = Int32
-  { int32Value :: Int.Int32
-  } deriving (Eq, Ord, Show)
-
-getInt32 :: Binary.Get Int32
-getInt32 = do
-  int32 <- Binary.getInt32le
-  pure (Int32 int32)
-
-putInt32 :: Int32 -> Binary.Put
-putInt32 int32 = Binary.putInt32le (int32Value int32)
-
-getInt32Bits :: BinaryBit.BitGet Int32
-getInt32Bits = do
-  bytes <- BinaryBit.getLazyByteString 4
-  pure (Binary.runGet getInt32 (reverseBytes bytes))
-
-putInt32Bits :: Int32 -> BinaryBit.BitPut ()
-putInt32Bits int32 = do
-  let bytes = Binary.runPut (putInt32 int32)
-  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Primitive/Int8.hs b/library/Rattletrap/Primitive/Int8.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Int8.hs
+++ /dev/null
@@ -1,32 +0,0 @@
-module Rattletrap.Primitive.Int8 where
-
-import Rattletrap.Utility
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.ByteString.Lazy as ByteString
-import qualified Data.Int as Int
-
-newtype Int8 = Int8
-  { int8Value :: Int.Int8
-  } deriving (Eq, Ord, Show)
-
-getInt8 :: Binary.Get Int8
-getInt8 = do
-  int8 <- Binary.getInt8
-  pure (Int8 int8)
-
-putInt8 :: Int8 -> Binary.Put
-putInt8 int8 = Binary.putInt8 (int8Value int8)
-
-getInt8Bits :: BinaryBit.BitGet Int8
-getInt8Bits = do
-  bytes <- BinaryBit.getLazyByteString 1
-  pure (Binary.runGet getInt8 (reverseBytes bytes))
-
-putInt8Bits :: Int8 -> BinaryBit.BitPut ()
-putInt8Bits int8 = do
-  let bytes = Binary.runPut (putInt8 int8)
-  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Primitive/Int8Vector.hs b/library/Rattletrap/Primitive/Int8Vector.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Int8Vector.hs
+++ /dev/null
@@ -1,42 +0,0 @@
-module Rattletrap.Primitive.Int8Vector where
-
-import Rattletrap.Primitive.Int8
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data Int8Vector = Int8Vector
-  { int8VectorX :: Maybe Int8
-  , int8VectorY :: Maybe Int8
-  , int8VectorZ :: Maybe Int8
-  } deriving (Eq, Ord, Show)
-
-getInt8Vector :: BinaryBit.BitGet Int8Vector
-getInt8Vector = do
-  x <- getInt8VectorField
-  y <- getInt8VectorField
-  z <- getInt8VectorField
-  pure (Int8Vector x y z)
-
-putInt8Vector :: Int8Vector -> BinaryBit.BitPut ()
-putInt8Vector int8Vector = do
-  putInt8VectorField (int8VectorX int8Vector)
-  putInt8VectorField (int8VectorY int8Vector)
-  putInt8VectorField (int8VectorZ int8Vector)
-
-getInt8VectorField :: BinaryBit.BitGet (Maybe Int8)
-getInt8VectorField = do
-  hasField <- BinaryBit.getBool
-  if hasField
-    then do
-      field <- getInt8Bits
-      pure (Just field)
-    else pure Nothing
-
-putInt8VectorField :: Maybe Int8 -> BinaryBit.BitPut ()
-putInt8VectorField maybeField =
-  case maybeField of
-    Nothing -> BinaryBit.putBool False
-    Just field -> do
-      BinaryBit.putBool True
-      putInt8Bits field
diff --git a/library/Rattletrap/Primitive/List.hs b/library/Rattletrap/Primitive/List.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/List.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-module Rattletrap.Primitive.List where
-
-import Rattletrap.Primitive.Word32
-
-import qualified Control.Monad as Monad
-import qualified Data.Binary as Binary
-
-newtype List a = List
-  { listValue :: [a]
-  } deriving (Eq, Ord, Show)
-
-getList :: Binary.Get a -> Binary.Get (List a)
-getList getElement = do
-  size <- getWord32
-  elements <- Monad.replicateM (fromIntegral (word32Value size)) getElement
-  pure (List elements)
-
-putList :: (a -> Binary.Put) -> List a -> Binary.Put
-putList putElement list = do
-  let elements = listValue list
-  putWord32 (Word32 (fromIntegral (length elements)))
-  mapM_ putElement elements
diff --git a/library/Rattletrap/Primitive/Section.hs b/library/Rattletrap/Primitive/Section.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Section.hs
+++ /dev/null
@@ -1,38 +0,0 @@
-module Rattletrap.Primitive.Section where
-
-import Rattletrap.Crc
-import Rattletrap.Primitive.Word32
-
-import qualified Control.Monad as Monad
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.ByteString.Lazy as ByteString
-
-newtype Section a = Section
-  { sectionBody :: a
-  } deriving (Eq, Ord, Show)
-
-getSection :: Binary.Get a -> Binary.Get (Section a)
-getSection getBody = do
-  size <- getWord32
-  crc <- getWord32
-  rawBody <- Binary.getLazyByteString (fromIntegral (word32Value size))
-  let actualCrc = Word32 (getCrc32 rawBody)
-  Monad.when (actualCrc /= crc) (fail (crcMessage actualCrc crc))
-  let body = Binary.runGet getBody rawBody
-  pure (Section body)
-
-putSection :: (a -> Binary.Put) -> Section a -> Binary.Put
-putSection putBody section = do
-  let rawBody = Binary.runPut (putBody (sectionBody section))
-  let size = ByteString.length rawBody
-  let crc = getCrc32 rawBody
-  putWord32 (Word32 (fromIntegral size))
-  putWord32 (Word32 crc)
-  Binary.putLazyByteString rawBody
-
-crcMessage :: Word32 -> Word32 -> String
-crcMessage actual expected =
-  unwords
-    ["actual CRC", show actual, "does not match expected CRC", show expected]
diff --git a/library/Rattletrap/Primitive/Text.hs b/library/Rattletrap/Primitive/Text.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Text.hs
+++ /dev/null
@@ -1,88 +0,0 @@
-module Rattletrap.Primitive.Text where
-
-import Rattletrap.Primitive.Int32
-import Rattletrap.Utility
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.ByteString.Lazy.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 size =
-  case int32Value size of
-    0x05000000 -> 8
-    x ->
-      if x < 0
-        then (-2 * fromIntegral x)
-        else fromIntegral x
-
-getTextDecoder :: Int32 -> ByteString.ByteString -> Text.Text
-getTextDecoder size bytes =
-  let decode =
-        if size < Int32 0
-          then Encoding.decodeUtf16LE
-          else Encoding.decodeLatin1
-  in decode (ByteString.toStrict bytes)
-
-getTextEncoder :: Int32 -> Text.Text -> ByteString.ByteString
-getTextEncoder size text =
-  if size < Int32 0
-    then ByteString.fromStrict (Encoding.encodeUtf16LE text)
-    else encodeLatin1 text
-
-encodeLatin1 :: Text.Text -> ByteString.ByteString
-encodeLatin1 text = ByteString.pack (Text.unpack text)
diff --git a/library/Rattletrap/Primitive/Vector.hs b/library/Rattletrap/Primitive/Vector.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Vector.hs
+++ /dev/null
@@ -1,29 +0,0 @@
-module Rattletrap.Primitive.Vector where
-
-import Rattletrap.Primitive.CompressedWord
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data Vector = Vector
-  { 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 bitSize dx dy 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/Primitive/Word32.hs b/library/Rattletrap/Primitive/Word32.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Word32.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Rattletrap.Primitive.Word32 where
-
-import Rattletrap.Utility
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.ByteString.Lazy as ByteString
-import qualified Data.Word as Word
-
-newtype Word32 = Word32
-  { word32Value :: Word.Word32
-  } deriving (Eq, Ord, Show)
-
-getWord32 :: Binary.Get Word32
-getWord32 = do
-  word32 <- Binary.getWord32le
-  pure (Word32 word32)
-
-putWord32 :: Word32 -> Binary.Put
-putWord32 word32 = Binary.putWord32le (word32Value word32)
-
-getWord32Bits :: BinaryBit.BitGet Word32
-getWord32Bits = do
-  bytes <- BinaryBit.getLazyByteString 4
-  pure (Binary.runGet getWord32 (reverseBytes bytes))
-
-putWord32Bits :: Word32 -> BinaryBit.BitPut ()
-putWord32Bits word32 = do
-  let bytes = Binary.runPut (putWord32 word32)
-  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Primitive/Word64.hs b/library/Rattletrap/Primitive/Word64.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Word64.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Rattletrap.Primitive.Word64 where
-
-import Rattletrap.Utility
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.ByteString.Lazy as ByteString
-import qualified Data.Word as Word
-
-newtype Word64 = Word64
-  { word64Value :: Word.Word64
-  } deriving (Eq, Ord, Show)
-
-getWord64 :: Binary.Get Word64
-getWord64 = do
-  word64 <- Binary.getWord64le
-  pure (Word64 word64)
-
-putWord64 :: Word64 -> Binary.Put
-putWord64 word64 = Binary.putWord64le (word64Value word64)
-
-getWord64Bits :: BinaryBit.BitGet Word64
-getWord64Bits = do
-  bytes <- BinaryBit.getLazyByteString 8
-  pure (Binary.runGet getWord64 (reverseBytes bytes))
-
-putWord64Bits :: Word64 -> BinaryBit.BitPut ()
-putWord64Bits word64 = do
-  let bytes = Binary.runPut (putWord64 word64)
-  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Primitive/Word8.hs b/library/Rattletrap/Primitive/Word8.hs
deleted file mode 100644
--- a/library/Rattletrap/Primitive/Word8.hs
+++ /dev/null
@@ -1,33 +0,0 @@
-module Rattletrap.Primitive.Word8 where
-
-import Rattletrap.Utility
-
-import qualified Data.Binary as Binary
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-import qualified Data.Binary.Get as Binary
-import qualified Data.Binary.Put as Binary
-import qualified Data.ByteString.Lazy as ByteString
-import qualified Data.Word as Word
-
-newtype Word8 = Word8
-  { word8Value :: Word.Word8
-  } deriving (Eq, Ord, Show)
-
-getWord8 :: Binary.Get Word8
-getWord8 = do
-  word8 <- Binary.getWord8
-  pure (Word8 word8)
-
-putWord8 :: Word8 -> Binary.Put
-putWord8 word8 = Binary.putWord8 (word8Value word8)
-
-getWord8Bits :: BinaryBit.BitGet Word8
-getWord8Bits = do
-  bytes <- BinaryBit.getLazyByteString 1
-  pure (Binary.runGet getWord8 (reverseBytes bytes))
-
-putWord8Bits :: Word8 -> BinaryBit.BitPut ()
-putWord8Bits word8 = do
-  let bytes = Binary.runPut (putWord8 word8)
-  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/library/Rattletrap/Property.hs b/library/Rattletrap/Property.hs
deleted file mode 100644
--- a/library/Rattletrap/Property.hs
+++ /dev/null
@@ -1,25 +0,0 @@
-module Rattletrap.Property where
-
-import Rattletrap.Primitive
-import Rattletrap.PropertyValue
-
-import qualified Data.Binary as Binary
-
-data Property = Property
-  { propertyKind :: Text
-  , propertySize :: Word64
-  , propertyValue :: PropertyValue Property
-  } deriving (Eq, Ord, Show)
-
-getProperty :: Binary.Get Property
-getProperty = do
-  kind <- getText
-  size <- getWord64
-  value <- getPropertyValue getProperty kind
-  pure (Property kind size value)
-
-putProperty :: Property -> Binary.Put
-putProperty property = do
-  putText (propertyKind property)
-  putWord64 (propertySize property)
-  putPropertyValue putProperty (propertyValue property)
diff --git a/library/Rattletrap/PropertyValue.hs b/library/Rattletrap/PropertyValue.hs
deleted file mode 100644
--- a/library/Rattletrap/PropertyValue.hs
+++ /dev/null
@@ -1,68 +0,0 @@
-module Rattletrap.PropertyValue where
-
-import Rattletrap.Primitive
-
-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
deleted file mode 100644
--- a/library/Rattletrap/RemoteId.hs
+++ /dev/null
@@ -1,42 +0,0 @@
-module Rattletrap.RemoteId where
-
-import Rattletrap.Primitive
-
-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
deleted file mode 100644
--- a/library/Rattletrap/Replay.hs
+++ /dev/null
@@ -1,25 +0,0 @@
-module Rattletrap.Replay where
-
-import Rattletrap.Content
-import Rattletrap.Header
-import Rattletrap.Primitive.Section
-
-import qualified Data.Binary as Binary
-
-data Replay = Replay
-  { replayHeader :: Section Header
-  , replayContent :: Section Content
-  } deriving (Eq, Ord, Show)
-
-getReplay :: Binary.Get Replay
-getReplay = do
-  header <- getSection getHeader
-  let version = getVersion (sectionBody header)
-  let numFrames = getNumFrames (sectionBody header)
-  content <- getSection (getContent version numFrames)
-  pure (Replay header content)
-
-putReplay :: Replay -> Binary.Put
-putReplay replay = do
-  putSection putHeader (replayHeader replay)
-  putSection putContent (replayContent replay)
diff --git a/library/Rattletrap/Replication.hs b/library/Rattletrap/Replication.hs
deleted file mode 100644
--- a/library/Rattletrap/Replication.hs
+++ /dev/null
@@ -1,57 +0,0 @@
-module Rattletrap.Replication where
-
-import Rattletrap.ActorMap
-import Rattletrap.ClassAttributeMap
-import Rattletrap.Primitive
-import Rattletrap.ReplicationValue
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data Replication = Replication
-  { replicationActorId :: CompressedWord
-  , replicationValue :: ReplicationValue
-  } deriving (Eq, Ord, Show)
-
-getReplications
-  :: (Int, Int)
-  -> 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 actorId 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/Replication/Destroyed.hs b/library/Rattletrap/Replication/Destroyed.hs
deleted file mode 100644
--- a/library/Rattletrap/Replication/Destroyed.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-module Rattletrap.Replication.Destroyed where
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data DestroyedReplication = DestroyedReplication
-  {
-  } deriving (Eq, Ord, Show)
-
-getDestroyedReplication :: BinaryBit.BitGet DestroyedReplication
-getDestroyedReplication = pure DestroyedReplication
-
-putDestroyedReplication :: DestroyedReplication -> BinaryBit.BitPut ()
-putDestroyedReplication _ = pure ()
diff --git a/library/Rattletrap/Replication/Spawned.hs b/library/Rattletrap/Replication/Spawned.hs
deleted file mode 100644
--- a/library/Rattletrap/Replication/Spawned.hs
+++ /dev/null
@@ -1,57 +0,0 @@
-module Rattletrap.Replication.Spawned where
-
-import Rattletrap.ActorMap
-import Rattletrap.ClassAttributeMap
-import Rattletrap.Initialization
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data SpawnedReplication = SpawnedReplication
-  { spawnedReplicationFlag :: Bool
-  , spawnedReplicationObjectId :: Word32
-  , spawnedReplication_objectName :: Text
-  , spawnedReplication_className :: Text
-  , spawnedReplicationInitialization :: Initialization
-  } deriving (Eq, Ord, Show)
-
-getSpawnedReplication
-  :: ClassAttributeMap
-  -> ActorMap
-  -> CompressedWord
-  -> BinaryBit.BitGet (SpawnedReplication, ActorMap)
-getSpawnedReplication classAttributeMap actorMap actorId = do
-  flag <- BinaryBit.getBool
-  objectId <- getWord32Bits
-  let newActorMap = updateActorMap actorId objectId actorMap
-  objectName <- lookupObjectName classAttributeMap objectId
-  className <- lookupClassName objectName
-  let hasLocation = classHasLocation className
-  let hasRotation = classHasRotation className
-  initialization <- getInitialization hasLocation hasRotation
-  pure
-    ( SpawnedReplication flag objectId objectName className initialization
-    , newActorMap)
-
-putSpawnedReplication :: SpawnedReplication -> BinaryBit.BitPut ()
-putSpawnedReplication spawnedReplication = do
-  BinaryBit.putBool (spawnedReplicationFlag spawnedReplication)
-  putWord32Bits (spawnedReplicationObjectId spawnedReplication)
-  putInitialization (spawnedReplicationInitialization spawnedReplication)
-
-lookupObjectName
-  :: Monad m
-  => ClassAttributeMap -> Word32 -> m Text
-lookupObjectName classAttributeMap objectId =
-  case getObjectName (classAttributeMapObjectMap classAttributeMap) objectId of
-    Nothing -> fail ("could not get object name for id " ++ show objectId)
-    Just objectName -> pure objectName
-
-lookupClassName
-  :: Monad m
-  => Text -> m Text
-lookupClassName objectName =
-  case getClassName objectName of
-    Nothing -> fail ("could not get class name for object " ++ show objectName)
-    Just className -> pure className
diff --git a/library/Rattletrap/Replication/Updated.hs b/library/Rattletrap/Replication/Updated.hs
deleted file mode 100644
--- a/library/Rattletrap/Replication/Updated.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-module Rattletrap.Replication.Updated where
-
-import Rattletrap.ActorMap
-import Rattletrap.Attribute
-import Rattletrap.ClassAttributeMap
-import Rattletrap.Primitive
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-newtype UpdatedReplication = UpdatedReplication
-  { updatedReplicationAttributes :: [Attribute]
-  } deriving (Eq, Ord, Show)
-
-getUpdatedReplication
-  :: (Int, Int)
-  -> ClassAttributeMap
-  -> ActorMap
-  -> CompressedWord
-  -> BinaryBit.BitGet UpdatedReplication
-getUpdatedReplication version classAttributeMap actorMap actorId = do
-  attributes <- getAttributes version classAttributeMap actorMap actorId
-  pure (UpdatedReplication attributes)
-
-putUpdatedReplication :: UpdatedReplication -> BinaryBit.BitPut ()
-putUpdatedReplication updatedReplication =
-  putAttributes (updatedReplicationAttributes updatedReplication)
diff --git a/library/Rattletrap/ReplicationValue.hs b/library/Rattletrap/ReplicationValue.hs
deleted file mode 100644
--- a/library/Rattletrap/ReplicationValue.hs
+++ /dev/null
@@ -1,60 +0,0 @@
-module Rattletrap.ReplicationValue
-  ( module Rattletrap.ReplicationValue
-  , module Rattletrap.Replication.Destroyed
-  , module Rattletrap.Replication.Spawned
-  , module Rattletrap.Replication.Updated
-  ) where
-
-import Rattletrap.ActorMap
-import Rattletrap.ClassAttributeMap
-import Rattletrap.Primitive
-import Rattletrap.Replication.Destroyed
-import Rattletrap.Replication.Spawned
-import Rattletrap.Replication.Updated
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Binary.Bits.Put as BinaryBit
-
-data ReplicationValue
-  = SpawnedReplicationValue SpawnedReplication
-  | UpdatedReplicationValue UpdatedReplication
-  | DestroyedReplicationValue DestroyedReplication
-  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) <-
-            getSpawnedReplication classAttributeMap actorMap actorId
-          pure (SpawnedReplicationValue x, newActorMap)
-        else do
-          x <- getUpdatedReplication version classAttributeMap actorMap actorId
-          pure (UpdatedReplicationValue x, actorMap)
-    else do
-      x <- getDestroyedReplication
-      pure (DestroyedReplicationValue x, actorMap)
-
-putReplicationValue :: ReplicationValue -> BinaryBit.BitPut ()
-putReplicationValue value =
-  case value of
-    SpawnedReplicationValue x -> do
-      BinaryBit.putBool True
-      BinaryBit.putBool True
-      putSpawnedReplication x
-    UpdatedReplicationValue x -> do
-      BinaryBit.putBool True
-      BinaryBit.putBool False
-      putUpdatedReplication x
-    DestroyedReplicationValue x -> do
-      BinaryBit.putBool False
-      putDestroyedReplication x
diff --git a/library/Rattletrap/Utility.hs b/library/Rattletrap/Utility.hs
deleted file mode 100644
--- a/library/Rattletrap/Utility.hs
+++ /dev/null
@@ -1,39 +0,0 @@
-module Rattletrap.Utility where
-
-import qualified Data.Binary.Bits.Get as BinaryBit
-import qualified Data.Bits as Bits
-import qualified Data.ByteString.Lazy as ByteString
-import qualified Data.Word as Word
-
-getRemainingBits :: BinaryBit.BitGet [Bool]
-getRemainingBits = do
-  isEmpty <- BinaryBit.isEmpty
-  if isEmpty
-    then pure []
-    else do
-      bit <- BinaryBit.getBool
-      bits <- getRemainingBits
-      pure (bit : bits)
-
-padBytes
-  :: Integral a
-  => a -> ByteString.ByteString -> ByteString.ByteString
-padBytes size bytes =
-  ByteString.concat
-    [ bytes
-    , ByteString.replicate (fromIntegral size - ByteString.length bytes) 0x00
-    ]
-
-reverseByte :: Word.Word8 -> Word.Word8
-reverseByte byte =
-  Bits.shiftR (byte Bits..&. Bits.bit 7) 7 +
-  Bits.shiftR (byte Bits..&. Bits.bit 6) 5 +
-  Bits.shiftR (byte Bits..&. Bits.bit 5) 3 +
-  Bits.shiftR (byte Bits..&. Bits.bit 4) 1 +
-  Bits.shiftL (byte Bits..&. Bits.bit 3) 1 +
-  Bits.shiftL (byte Bits..&. Bits.bit 2) 3 +
-  Bits.shiftL (byte Bits..&. Bits.bit 1) 5 +
-  Bits.shiftL (byte Bits..&. Bits.bit 0) 7
-
-reverseBytes :: ByteString.ByteString -> ByteString.ByteString
-reverseBytes bytes = ByteString.map reverseByte bytes
diff --git a/library/Rattletrap/Version.hs b/library/Rattletrap/Version.hs
deleted file mode 100644
--- a/library/Rattletrap/Version.hs
+++ /dev/null
@@ -1,7 +0,0 @@
-module Rattletrap.Version where
-
-import qualified Data.Version as Version
-import qualified Paths_rattletrap as This
-
-version :: Version.Version
-version = This.version
diff --git a/rattletrap.cabal b/rattletrap.cabal
--- a/rattletrap.cabal
+++ b/rattletrap.cabal
@@ -3,195 +3,196 @@
 -- see: https://github.com/sol/hpack
 
 name:           rattletrap
-version:        1.0.0
+version:        2.0.0
+cabal-version:  >= 1.10
+build-type:     Simple
+license:        MIT
+license-file:   LICENSE.markdown
+maintainer:     Taylor Fausak
+homepage:       https://github.com/tfausak/rattletrap#readme
+bug-reports:    https://github.com/tfausak/rattletrap/issues
 synopsis:       Parse and generate Rocket League replays.
 description:    Rattletrap parses and generates Rocket League replays.
 category:       Game
-homepage:       https://github.com/tfausak/rattletrap#readme
-bug-reports:    https://github.com/tfausak/rattletrap/issues
-maintainer:     Taylor Fausak
-license:        MIT
-license-file:   LICENSE.markdown
-build-type:     Simple
-cabal-version:  >= 1.10
 
 extra-source-files:
-    HLint.hs
     README.markdown
-    test/replays/00080014003600090000036E0F65CCEB.replay
-    test/replays/07E925B1423653D44CB8B4B2524792C1.replay
-    test/replays/0AD21FEE4A512906BEB6C98136AAF49A.replay
-    test/replays/1205D96C4D819800927791820096CD49.replay
-    test/replays/160CA83E41083BFD8E6315B4BFCA0561.replay
-    test/replays/18D6738D415B70B5BE4C299588D3C141.replay
-    test/replays/1A126AC24CAA0DB0E98835BD960B8AF8.replay
-    test/replays/1BC2D01444ACE577D01E988EADD4DFD0.replay
-    test/replays/1D1DE97D4941C86E43FE0093563DB621.replay
-    test/replays/1EF90FCC4F719F606A5327B3CDD782A4.replay
-    test/replays/1F3798E540B0C37A946561ABBB3037F9.replay
-    test/replays/211466D04B983F5A33CC2FA1D5928672.replay
-    test/replays/22BACD794ABE7B92E50E9CBDBD9C59CE.replay
-    test/replays/27B6A7B64553F0F685874584F96BAB1B.replay
-    test/replays/29F582C34A65EB34D358A784CBE3C189.replay
-    test/replays/372DBFCA4BDB340E4357B6BD43032802.replay
-    test/replays/387F059C47C09E253C875CA990EFD9F2.replay
-    test/replays/3EA147DD485B8DD24810689A7A989E44.replay
-    test/replays/4126861E477F4A03DE2A4080374D7908.replay
-    test/replays/42F0D8DA4FC89AE7B80FCAB7F637A8EA.replay
-    test/replays/504ED825482186E771FAA9B642CE5CE4.replay
-    test/replays/52AA67F94090C19D33C5009E54D31FE4.replay
-    test/replays/540DA764423C8FB24EB9D486D982F16F.replay
-    test/replays/551CA4D44FF2B86015DE44A6B5790D4C.replay
-    test/replays/6688EEE34BFEB3EC3A9E3283098CC712.replay
-    test/replays/6D1B06D844A5BB91B81FD4B5B28F08BA.replay
-    test/replays/6F7CFCD24638F8A6567AB3A8B9958A90.replay
-    test/replays/7109EB9846D303E54B7ACBA792036213.replay
-    test/replays/7BF6073F4614CE0A438994B9A260DA6A.replay
-    test/replays/89CBA30E46FA5385BDD35DA4285D4D2E.replay
-    test/replays/9704208245D7DD851F2FB2BC7DFD9AC3.replay
-    test/replays/98E58A904D713F2DE202358E8573265D.replay
-    test/replays/A128B3AB45D5A18E3EF9CF93C9576BCE.replay
-    test/replays/A52F804845573D8DA65E97BF59026A43.replay
-    test/replays/A6711CE74272B2E663DCC9A200A218E3.replay
-    test/replays/A7F001A1417A19BFA8C90990D8F7C2FF.replay
-    test/replays/B82DDB624C393A4A425E68AB40DC2450.replay
-    test/replays/B9F9B87D4A9D0A3D25D4EC91C0401DE2.replay
-    test/replays/C14F7E0E4D9B5E6BE9AD5D8ED56B174C.replay
-    test/replays/C8372B1345B1803DEF039F815DBD802D.replay
-    test/replays/D428F81646A98C25902CE988AE5C14C8.replay
-    test/replays/D7FB197A451D69075A0C99A2F49A4053.replay
-    test/replays/DCB3A6B94A9DBE46FDE5EAA9B012F6C8.replay
-    test/replays/EAE311E84BA35B590A6FDBA6DD4F2FEB.replay
-    test/replays/EAE8DADA4BB2DC5422792C9B4A67392D.replay
-    test/replays/F299F176491554B11E34AB91CA76B2CE.replay
-    test/replays/F7B9E14545C7467B89A00895980FCD73.replay
-    test/replays/F811C1D24888015E23B598AD8628C742.replay
-    test/replays/FDC79DA84DD463D4BCCE6B892829AC88.replay
+    replays/00080014003600090000036E0F65CCEB.replay
+    replays/07E925B1423653D44CB8B4B2524792C1.replay
+    replays/0AD21FEE4A512906BEB6C98136AAF49A.replay
+    replays/1205D96C4D819800927791820096CD49.replay
+    replays/160CA83E41083BFD8E6315B4BFCA0561.replay
+    replays/18D6738D415B70B5BE4C299588D3C141.replay
+    replays/1A126AC24CAA0DB0E98835BD960B8AF8.replay
+    replays/1AE415514DFC65DCBF8B8391AD35488D.replay
+    replays/1BC2D01444ACE577D01E988EADD4DFD0.replay
+    replays/1D1DE97D4941C86E43FE0093563DB621.replay
+    replays/1EF90FCC4F719F606A5327B3CDD782A4.replay
+    replays/1F3798E540B0C37A946561ABBB3037F9.replay
+    replays/211466D04B983F5A33CC2FA1D5928672.replay
+    replays/22BACD794ABE7B92E50E9CBDBD9C59CE.replay
+    replays/27B6A7B64553F0F685874584F96BAB1B.replay
+    replays/29F582C34A65EB34D358A784CBE3C189.replay
+    replays/372DBFCA4BDB340E4357B6BD43032802.replay
+    replays/387F059C47C09E253C875CA990EFD9F2.replay
+    replays/3EA147DD485B8DD24810689A7A989E44.replay
+    replays/4126861E477F4A03DE2A4080374D7908.replay
+    replays/42F0D8DA4FC89AE7B80FCAB7F637A8EA.replay
+    replays/504ED825482186E771FAA9B642CE5CE4.replay
+    replays/52AA67F94090C19D33C5009E54D31FE4.replay
+    replays/540DA764423C8FB24EB9D486D982F16F.replay
+    replays/551CA4D44FF2B86015DE44A6B5790D4C.replay
+    replays/6320E51C49066A7C210A2993C2201D5F.replay
+    replays/6688EEE34BFEB3EC3A9E3283098CC712.replay
+    replays/6D1B06D844A5BB91B81FD4B5B28F08BA.replay
+    replays/6F7CFCD24638F8A6567AB3A8B9958A90.replay
+    replays/7109EB9846D303E54B7ACBA792036213.replay
+    replays/7BF6073F4614CE0A438994B9A260DA6A.replay
+    replays/89CBA30E46FA5385BDD35DA4285D4D2E.replay
+    replays/9704208245D7DD851F2FB2BC7DFD9AC3.replay
+    replays/98E58A904D713F2DE202358E8573265D.replay
+    replays/A128B3AB45D5A18E3EF9CF93C9576BCE.replay
+    replays/A52F804845573D8DA65E97BF59026A43.replay
+    replays/A6711CE74272B2E663DCC9A200A218E3.replay
+    replays/A7F001A1417A19BFA8C90990D8F7C2FF.replay
+    replays/B9F9B87D4A9D0A3D25D4EC91C0401DE2.replay
+    replays/C14F7E0E4D9B5E6BE9AD5D8ED56B174C.replay
+    replays/C8372B1345B1803DEF039F815DBD802D.replay
+    replays/CC4CA70D4F7A67EBAD0ED9B9923106F7.replay
+    replays/D428F81646A98C25902CE988AE5C14C8.replay
+    replays/D7FB197A451D69075A0C99A2F49A4053.replay
+    replays/DCB3A6B94A9DBE46FDE5EAA9B012F6C8.replay
+    replays/EAE311E84BA35B590A6FDBA6DD4F2FEB.replay
+    replays/EAE8DADA4BB2DC5422792C9B4A67392D.replay
+    replays/F299F176491554B11E34AB91CA76B2CE.replay
+    replays/F7B9E14545C7467B89A00895980FCD73.replay
+    replays/F811C1D24888015E23B598AD8628C742.replay
+    replays/FDC79DA84DD463D4BCCE6B892829AC88.replay
 
 source-repository head
-  type: git
-  location: https://github.com/tfausak/rattletrap
+    type: git
+    location: https://github.com/tfausak/rattletrap
 
 library
-  hs-source-dirs:
-      library
-  default-extensions: Strict
-  ghc-options: -Wall
-  build-depends:
-      aeson >=0.11 && <1.1
-    , aeson-casing >=0.1 && <0.2
-    , base >=4.9 && <4.10
-    , bimap >=0.3 && <0.4
-    , binary >=0.8 && <0.9
-    , binary-bits >=0.5 && <0.6
-    , bytestring >=0.10 && <0.11
-    , containers >=0.5 && <0.6
-    , data-binary-ieee754 >=0.4 && <0.5
-    , template-haskell >=2.11 && <2.12
-    , text >=1.2 && <1.3
-    , vector >=0.11 && <0.12
-  exposed-modules:
-      Rattletrap
-      Rattletrap.ActorMap
-      Rattletrap.Attribute
-      Rattletrap.Attribute.Boolean
-      Rattletrap.Attribute.Byte
-      Rattletrap.Attribute.CamSettings
-      Rattletrap.Attribute.ClubColors
-      Rattletrap.Attribute.Demolish
-      Rattletrap.Attribute.Enum
-      Rattletrap.Attribute.Explosion
-      Rattletrap.Attribute.FlaggedInt
-      Rattletrap.Attribute.Float
-      Rattletrap.Attribute.GameMode
-      Rattletrap.Attribute.Int
-      Rattletrap.Attribute.Loadout
-      Rattletrap.Attribute.LoadoutOnline
-      Rattletrap.Attribute.Loadouts
-      Rattletrap.Attribute.LoadoutsOnline
-      Rattletrap.Attribute.Location
-      Rattletrap.Attribute.MusicStinger
-      Rattletrap.Attribute.PartyLeader
-      Rattletrap.Attribute.Pickup
-      Rattletrap.Attribute.PrivateMatchSettings
-      Rattletrap.Attribute.QWord
-      Rattletrap.Attribute.Reservation
-      Rattletrap.Attribute.RigidBodyState
-      Rattletrap.Attribute.String
-      Rattletrap.Attribute.TeamPaint
-      Rattletrap.Attribute.UniqueId
-      Rattletrap.Attribute.WeldedInfo
-      Rattletrap.AttributeMapping
-      Rattletrap.AttributeType
-      Rattletrap.AttributeValue
-      Rattletrap.Cache
-      Rattletrap.ClassAttributeMap
-      Rattletrap.ClassMapping
-      Rattletrap.Content
-      Rattletrap.Crc
-      Rattletrap.Data
-      Rattletrap.Frame
-      Rattletrap.Header
-      Rattletrap.Initialization
-      Rattletrap.Json
-      Rattletrap.KeyFrame
-      Rattletrap.Main
-      Rattletrap.Mark
-      Rattletrap.Message
-      Rattletrap.Primitive
-      Rattletrap.Primitive.CompressedWord
-      Rattletrap.Primitive.CompressedWordVector
-      Rattletrap.Primitive.Dictionary
-      Rattletrap.Primitive.Float32
-      Rattletrap.Primitive.Int32
-      Rattletrap.Primitive.Int8
-      Rattletrap.Primitive.Int8Vector
-      Rattletrap.Primitive.List
-      Rattletrap.Primitive.Section
-      Rattletrap.Primitive.Text
-      Rattletrap.Primitive.Vector
-      Rattletrap.Primitive.Word32
-      Rattletrap.Primitive.Word64
-      Rattletrap.Primitive.Word8
-      Rattletrap.Property
-      Rattletrap.PropertyValue
-      Rattletrap.RemoteId
-      Rattletrap.Replay
-      Rattletrap.Replication
-      Rattletrap.Replication.Destroyed
-      Rattletrap.Replication.Spawned
-      Rattletrap.Replication.Updated
-      Rattletrap.ReplicationValue
-      Rattletrap.Utility
-      Rattletrap.Version
-  other-modules:
-      Paths_rattletrap
-  default-language: Haskell2010
+    exposed-modules:
+        Rattletrap
+        Rattletrap.ActorMap
+        Rattletrap.Attribute
+        Rattletrap.Attribute.Boolean
+        Rattletrap.Attribute.Byte
+        Rattletrap.Attribute.CamSettings
+        Rattletrap.Attribute.ClubColors
+        Rattletrap.Attribute.Demolish
+        Rattletrap.Attribute.Enum
+        Rattletrap.Attribute.Explosion
+        Rattletrap.Attribute.FlaggedInt
+        Rattletrap.Attribute.Float
+        Rattletrap.Attribute.GameMode
+        Rattletrap.Attribute.Int
+        Rattletrap.Attribute.Loadout
+        Rattletrap.Attribute.LoadoutOnline
+        Rattletrap.Attribute.Loadouts
+        Rattletrap.Attribute.LoadoutsOnline
+        Rattletrap.Attribute.Location
+        Rattletrap.Attribute.MusicStinger
+        Rattletrap.Attribute.PartyLeader
+        Rattletrap.Attribute.Pickup
+        Rattletrap.Attribute.PrivateMatchSettings
+        Rattletrap.Attribute.QWord
+        Rattletrap.Attribute.Reservation
+        Rattletrap.Attribute.RigidBodyState
+        Rattletrap.Attribute.String
+        Rattletrap.Attribute.TeamPaint
+        Rattletrap.Attribute.UniqueId
+        Rattletrap.Attribute.WeldedInfo
+        Rattletrap.AttributeMapping
+        Rattletrap.AttributeType
+        Rattletrap.AttributeValue
+        Rattletrap.Cache
+        Rattletrap.ClassAttributeMap
+        Rattletrap.ClassMapping
+        Rattletrap.Content
+        Rattletrap.Crc
+        Rattletrap.Data
+        Rattletrap.Frame
+        Rattletrap.Header
+        Rattletrap.Initialization
+        Rattletrap.Json
+        Rattletrap.KeyFrame
+        Rattletrap.Main
+        Rattletrap.Mark
+        Rattletrap.Message
+        Rattletrap.Primitive
+        Rattletrap.Primitive.CompressedWord
+        Rattletrap.Primitive.CompressedWordVector
+        Rattletrap.Primitive.Dictionary
+        Rattletrap.Primitive.Float32
+        Rattletrap.Primitive.Int32
+        Rattletrap.Primitive.Int8
+        Rattletrap.Primitive.Int8Vector
+        Rattletrap.Primitive.List
+        Rattletrap.Primitive.Section
+        Rattletrap.Primitive.Text
+        Rattletrap.Primitive.Vector
+        Rattletrap.Primitive.Word32
+        Rattletrap.Primitive.Word64
+        Rattletrap.Primitive.Word8
+        Rattletrap.Property
+        Rattletrap.PropertyValue
+        Rattletrap.RemoteId
+        Rattletrap.Replay
+        Rattletrap.Replication
+        Rattletrap.Replication.Destroyed
+        Rattletrap.Replication.Spawned
+        Rattletrap.Replication.Updated
+        Rattletrap.ReplicationValue
+        Rattletrap.Utility
+        Rattletrap.Version
+    build-depends:
+        aeson >=0.11 && <1.1,
+        aeson-casing >=0.1 && <0.2,
+        base >=4.9 && <4.10,
+        bimap >=0.3 && <0.4,
+        binary >=0.8 && <0.9,
+        binary-bits >=0.5 && <0.6,
+        bytestring >=0.10 && <0.11,
+        containers >=0.5 && <0.6,
+        data-binary-ieee754 >=0.4 && <0.5,
+        template-haskell >=2.11 && <2.12,
+        text >=1.2 && <1.3,
+        vector >=0.11 && <0.12
+    default-language: Haskell2010
+    default-extensions: Strict
+    hs-source-dirs:
+        source/library
+    other-modules:
+        Paths_rattletrap
+    ghc-options: -Wall
 
 executable rattletrap
-  main-is: Main.hs
-  hs-source-dirs:
-      executable
-  default-extensions: Strict
-  ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
-  build-depends:
-      base
-    , rattletrap
-  default-language: Haskell2010
+    main-is: Main.hs
+    build-depends:
+        base,
+        rattletrap
+    default-language: Haskell2010
+    default-extensions: Strict
+    hs-source-dirs:
+        source/executables
+    ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
 
 test-suite test
-  type: exitcode-stdio-1.0
-  main-is: Test.hs
-  hs-source-dirs:
-      test
-  default-extensions: Strict
-  ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
-  build-depends:
-      base
-    , bytestring
-    , filepath >=1.4 && <1.5
-    , rattletrap
-    , tasty >=0.11 && <0.12
-    , tasty-hspec >=1.1 && <1.2
-    , temporary >=1.2 && <1.3
-  default-language: Haskell2010
+    type: exitcode-stdio-1.0
+    main-is: Main.hs
+    build-depends:
+        base,
+        bytestring,
+        filepath >=1.4 && <1.5,
+        rattletrap,
+        tasty >=0.11 && <0.12,
+        tasty-hspec >=1.1 && <1.2,
+        temporary >=1.2 && <1.3
+    default-language: Haskell2010
+    default-extensions: Strict
+    hs-source-dirs:
+        source/tests
+    ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
diff --git a/replays/00080014003600090000036E0F65CCEB.replay b/replays/00080014003600090000036E0F65CCEB.replay
new file mode 100644
Binary files /dev/null and b/replays/00080014003600090000036E0F65CCEB.replay differ
diff --git a/replays/07E925B1423653D44CB8B4B2524792C1.replay b/replays/07E925B1423653D44CB8B4B2524792C1.replay
new file mode 100644
Binary files /dev/null and b/replays/07E925B1423653D44CB8B4B2524792C1.replay differ
diff --git a/replays/0AD21FEE4A512906BEB6C98136AAF49A.replay b/replays/0AD21FEE4A512906BEB6C98136AAF49A.replay
new file mode 100644
Binary files /dev/null and b/replays/0AD21FEE4A512906BEB6C98136AAF49A.replay differ
diff --git a/replays/1205D96C4D819800927791820096CD49.replay b/replays/1205D96C4D819800927791820096CD49.replay
new file mode 100644
Binary files /dev/null and b/replays/1205D96C4D819800927791820096CD49.replay differ
diff --git a/replays/160CA83E41083BFD8E6315B4BFCA0561.replay b/replays/160CA83E41083BFD8E6315B4BFCA0561.replay
new file mode 100644
Binary files /dev/null and b/replays/160CA83E41083BFD8E6315B4BFCA0561.replay differ
diff --git a/replays/18D6738D415B70B5BE4C299588D3C141.replay b/replays/18D6738D415B70B5BE4C299588D3C141.replay
new file mode 100644
Binary files /dev/null and b/replays/18D6738D415B70B5BE4C299588D3C141.replay differ
diff --git a/replays/1A126AC24CAA0DB0E98835BD960B8AF8.replay b/replays/1A126AC24CAA0DB0E98835BD960B8AF8.replay
new file mode 100644
Binary files /dev/null and b/replays/1A126AC24CAA0DB0E98835BD960B8AF8.replay differ
diff --git a/replays/1AE415514DFC65DCBF8B8391AD35488D.replay b/replays/1AE415514DFC65DCBF8B8391AD35488D.replay
new file mode 100644
Binary files /dev/null and b/replays/1AE415514DFC65DCBF8B8391AD35488D.replay differ
diff --git a/replays/1BC2D01444ACE577D01E988EADD4DFD0.replay b/replays/1BC2D01444ACE577D01E988EADD4DFD0.replay
new file mode 100644
Binary files /dev/null and b/replays/1BC2D01444ACE577D01E988EADD4DFD0.replay differ
diff --git a/replays/1D1DE97D4941C86E43FE0093563DB621.replay b/replays/1D1DE97D4941C86E43FE0093563DB621.replay
new file mode 100644
Binary files /dev/null and b/replays/1D1DE97D4941C86E43FE0093563DB621.replay differ
diff --git a/replays/1EF90FCC4F719F606A5327B3CDD782A4.replay b/replays/1EF90FCC4F719F606A5327B3CDD782A4.replay
new file mode 100644
Binary files /dev/null and b/replays/1EF90FCC4F719F606A5327B3CDD782A4.replay differ
diff --git a/replays/1F3798E540B0C37A946561ABBB3037F9.replay b/replays/1F3798E540B0C37A946561ABBB3037F9.replay
new file mode 100644
Binary files /dev/null and b/replays/1F3798E540B0C37A946561ABBB3037F9.replay differ
diff --git a/replays/211466D04B983F5A33CC2FA1D5928672.replay b/replays/211466D04B983F5A33CC2FA1D5928672.replay
new file mode 100644
Binary files /dev/null and b/replays/211466D04B983F5A33CC2FA1D5928672.replay differ
diff --git a/replays/22BACD794ABE7B92E50E9CBDBD9C59CE.replay b/replays/22BACD794ABE7B92E50E9CBDBD9C59CE.replay
new file mode 100644
Binary files /dev/null and b/replays/22BACD794ABE7B92E50E9CBDBD9C59CE.replay differ
diff --git a/replays/27B6A7B64553F0F685874584F96BAB1B.replay b/replays/27B6A7B64553F0F685874584F96BAB1B.replay
new file mode 100644
Binary files /dev/null and b/replays/27B6A7B64553F0F685874584F96BAB1B.replay differ
diff --git a/replays/29F582C34A65EB34D358A784CBE3C189.replay b/replays/29F582C34A65EB34D358A784CBE3C189.replay
new file mode 100644
Binary files /dev/null and b/replays/29F582C34A65EB34D358A784CBE3C189.replay differ
diff --git a/replays/372DBFCA4BDB340E4357B6BD43032802.replay b/replays/372DBFCA4BDB340E4357B6BD43032802.replay
new file mode 100644
Binary files /dev/null and b/replays/372DBFCA4BDB340E4357B6BD43032802.replay differ
diff --git a/replays/387F059C47C09E253C875CA990EFD9F2.replay b/replays/387F059C47C09E253C875CA990EFD9F2.replay
new file mode 100644
Binary files /dev/null and b/replays/387F059C47C09E253C875CA990EFD9F2.replay differ
diff --git a/replays/3EA147DD485B8DD24810689A7A989E44.replay b/replays/3EA147DD485B8DD24810689A7A989E44.replay
new file mode 100644
Binary files /dev/null and b/replays/3EA147DD485B8DD24810689A7A989E44.replay differ
diff --git a/replays/4126861E477F4A03DE2A4080374D7908.replay b/replays/4126861E477F4A03DE2A4080374D7908.replay
new file mode 100644
Binary files /dev/null and b/replays/4126861E477F4A03DE2A4080374D7908.replay differ
diff --git a/replays/42F0D8DA4FC89AE7B80FCAB7F637A8EA.replay b/replays/42F0D8DA4FC89AE7B80FCAB7F637A8EA.replay
new file mode 100644
Binary files /dev/null and b/replays/42F0D8DA4FC89AE7B80FCAB7F637A8EA.replay differ
diff --git a/replays/504ED825482186E771FAA9B642CE5CE4.replay b/replays/504ED825482186E771FAA9B642CE5CE4.replay
new file mode 100644
Binary files /dev/null and b/replays/504ED825482186E771FAA9B642CE5CE4.replay differ
diff --git a/replays/52AA67F94090C19D33C5009E54D31FE4.replay b/replays/52AA67F94090C19D33C5009E54D31FE4.replay
new file mode 100644
Binary files /dev/null and b/replays/52AA67F94090C19D33C5009E54D31FE4.replay differ
diff --git a/replays/540DA764423C8FB24EB9D486D982F16F.replay b/replays/540DA764423C8FB24EB9D486D982F16F.replay
new file mode 100644
Binary files /dev/null and b/replays/540DA764423C8FB24EB9D486D982F16F.replay differ
diff --git a/replays/551CA4D44FF2B86015DE44A6B5790D4C.replay b/replays/551CA4D44FF2B86015DE44A6B5790D4C.replay
new file mode 100644
Binary files /dev/null and b/replays/551CA4D44FF2B86015DE44A6B5790D4C.replay differ
diff --git a/replays/6320E51C49066A7C210A2993C2201D5F.replay b/replays/6320E51C49066A7C210A2993C2201D5F.replay
new file mode 100644
Binary files /dev/null and b/replays/6320E51C49066A7C210A2993C2201D5F.replay differ
diff --git a/replays/6688EEE34BFEB3EC3A9E3283098CC712.replay b/replays/6688EEE34BFEB3EC3A9E3283098CC712.replay
new file mode 100644
Binary files /dev/null and b/replays/6688EEE34BFEB3EC3A9E3283098CC712.replay differ
diff --git a/replays/6D1B06D844A5BB91B81FD4B5B28F08BA.replay b/replays/6D1B06D844A5BB91B81FD4B5B28F08BA.replay
new file mode 100644
Binary files /dev/null and b/replays/6D1B06D844A5BB91B81FD4B5B28F08BA.replay differ
diff --git a/replays/6F7CFCD24638F8A6567AB3A8B9958A90.replay b/replays/6F7CFCD24638F8A6567AB3A8B9958A90.replay
new file mode 100644
Binary files /dev/null and b/replays/6F7CFCD24638F8A6567AB3A8B9958A90.replay differ
diff --git a/replays/7109EB9846D303E54B7ACBA792036213.replay b/replays/7109EB9846D303E54B7ACBA792036213.replay
new file mode 100644
Binary files /dev/null and b/replays/7109EB9846D303E54B7ACBA792036213.replay differ
diff --git a/replays/7BF6073F4614CE0A438994B9A260DA6A.replay b/replays/7BF6073F4614CE0A438994B9A260DA6A.replay
new file mode 100644
Binary files /dev/null and b/replays/7BF6073F4614CE0A438994B9A260DA6A.replay differ
diff --git a/replays/89CBA30E46FA5385BDD35DA4285D4D2E.replay b/replays/89CBA30E46FA5385BDD35DA4285D4D2E.replay
new file mode 100644
Binary files /dev/null and b/replays/89CBA30E46FA5385BDD35DA4285D4D2E.replay differ
diff --git a/replays/9704208245D7DD851F2FB2BC7DFD9AC3.replay b/replays/9704208245D7DD851F2FB2BC7DFD9AC3.replay
new file mode 100644
Binary files /dev/null and b/replays/9704208245D7DD851F2FB2BC7DFD9AC3.replay differ
diff --git a/replays/98E58A904D713F2DE202358E8573265D.replay b/replays/98E58A904D713F2DE202358E8573265D.replay
new file mode 100644
Binary files /dev/null and b/replays/98E58A904D713F2DE202358E8573265D.replay differ
diff --git a/replays/A128B3AB45D5A18E3EF9CF93C9576BCE.replay b/replays/A128B3AB45D5A18E3EF9CF93C9576BCE.replay
new file mode 100644
Binary files /dev/null and b/replays/A128B3AB45D5A18E3EF9CF93C9576BCE.replay differ
diff --git a/replays/A52F804845573D8DA65E97BF59026A43.replay b/replays/A52F804845573D8DA65E97BF59026A43.replay
new file mode 100644
Binary files /dev/null and b/replays/A52F804845573D8DA65E97BF59026A43.replay differ
diff --git a/replays/A6711CE74272B2E663DCC9A200A218E3.replay b/replays/A6711CE74272B2E663DCC9A200A218E3.replay
new file mode 100644
Binary files /dev/null and b/replays/A6711CE74272B2E663DCC9A200A218E3.replay differ
diff --git a/replays/A7F001A1417A19BFA8C90990D8F7C2FF.replay b/replays/A7F001A1417A19BFA8C90990D8F7C2FF.replay
new file mode 100644
Binary files /dev/null and b/replays/A7F001A1417A19BFA8C90990D8F7C2FF.replay differ
diff --git a/replays/B9F9B87D4A9D0A3D25D4EC91C0401DE2.replay b/replays/B9F9B87D4A9D0A3D25D4EC91C0401DE2.replay
new file mode 100644
Binary files /dev/null and b/replays/B9F9B87D4A9D0A3D25D4EC91C0401DE2.replay differ
diff --git a/replays/C14F7E0E4D9B5E6BE9AD5D8ED56B174C.replay b/replays/C14F7E0E4D9B5E6BE9AD5D8ED56B174C.replay
new file mode 100644
Binary files /dev/null and b/replays/C14F7E0E4D9B5E6BE9AD5D8ED56B174C.replay differ
diff --git a/replays/C8372B1345B1803DEF039F815DBD802D.replay b/replays/C8372B1345B1803DEF039F815DBD802D.replay
new file mode 100644
Binary files /dev/null and b/replays/C8372B1345B1803DEF039F815DBD802D.replay differ
diff --git a/replays/CC4CA70D4F7A67EBAD0ED9B9923106F7.replay b/replays/CC4CA70D4F7A67EBAD0ED9B9923106F7.replay
new file mode 100644
Binary files /dev/null and b/replays/CC4CA70D4F7A67EBAD0ED9B9923106F7.replay differ
diff --git a/replays/D428F81646A98C25902CE988AE5C14C8.replay b/replays/D428F81646A98C25902CE988AE5C14C8.replay
new file mode 100644
Binary files /dev/null and b/replays/D428F81646A98C25902CE988AE5C14C8.replay differ
diff --git a/replays/D7FB197A451D69075A0C99A2F49A4053.replay b/replays/D7FB197A451D69075A0C99A2F49A4053.replay
new file mode 100644
Binary files /dev/null and b/replays/D7FB197A451D69075A0C99A2F49A4053.replay differ
diff --git a/replays/DCB3A6B94A9DBE46FDE5EAA9B012F6C8.replay b/replays/DCB3A6B94A9DBE46FDE5EAA9B012F6C8.replay
new file mode 100644
Binary files /dev/null and b/replays/DCB3A6B94A9DBE46FDE5EAA9B012F6C8.replay differ
diff --git a/replays/EAE311E84BA35B590A6FDBA6DD4F2FEB.replay b/replays/EAE311E84BA35B590A6FDBA6DD4F2FEB.replay
new file mode 100644
Binary files /dev/null and b/replays/EAE311E84BA35B590A6FDBA6DD4F2FEB.replay differ
diff --git a/replays/EAE8DADA4BB2DC5422792C9B4A67392D.replay b/replays/EAE8DADA4BB2DC5422792C9B4A67392D.replay
new file mode 100644
Binary files /dev/null and b/replays/EAE8DADA4BB2DC5422792C9B4A67392D.replay differ
diff --git a/replays/F299F176491554B11E34AB91CA76B2CE.replay b/replays/F299F176491554B11E34AB91CA76B2CE.replay
new file mode 100644
Binary files /dev/null and b/replays/F299F176491554B11E34AB91CA76B2CE.replay differ
diff --git a/replays/F7B9E14545C7467B89A00895980FCD73.replay b/replays/F7B9E14545C7467B89A00895980FCD73.replay
new file mode 100644
Binary files /dev/null and b/replays/F7B9E14545C7467B89A00895980FCD73.replay differ
diff --git a/replays/F811C1D24888015E23B598AD8628C742.replay b/replays/F811C1D24888015E23B598AD8628C742.replay
new file mode 100644
Binary files /dev/null and b/replays/F811C1D24888015E23B598AD8628C742.replay differ
diff --git a/replays/FDC79DA84DD463D4BCCE6B892829AC88.replay b/replays/FDC79DA84DD463D4BCCE6B892829AC88.replay
new file mode 100644
Binary files /dev/null and b/replays/FDC79DA84DD463D4BCCE6B892829AC88.replay differ
diff --git a/source/executables/Main.hs b/source/executables/Main.hs
new file mode 100644
--- /dev/null
+++ b/source/executables/Main.hs
@@ -0,0 +1,5 @@
+module Main
+  ( module Rattletrap
+  ) where
+
+import Rattletrap (main)
diff --git a/source/library/Rattletrap.hs b/source/library/Rattletrap.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap.hs
@@ -0,0 +1,85 @@
+-- | Rattletrap is a tool for parsing and generating Rocket League replays. It
+-- is typically used as an executable; see "Rattletrap.Main" for details about
+-- that interface. Using Rattletrap from Haskell is as easy as importing this
+-- module. Together with "Data.ByteString.Lazy" and "Data.Binary.Get", you can
+-- decode a 'Replay' value. Then you can transform it into JSON with
+-- "Data.Aeson". For example:
+--
+-- @
+-- let file = "path-to/some.replay"
+-- bytes <- 'Data.ByteString.Lazy.readFile' file
+-- let replay = 'Data.Binary.Get.runGet' 'Rattletrap.Replay.getReplay' bytes
+-- let json = 'Data.Aeson.encode' replay
+-- 'Data.ByteString.Lazy.putStr' json
+-- @
+--
+-- There are a few unusual aspects of Rattletrap to be aware of:
+--
+-- - In order to improve memory usage, everything Rattletrap does is strict.
+--   Don't expect any lazy values or evaluation.
+-- - Some names, like 'Text', collide with commonly-used modules. Use
+--   qualified imports to avoid collisions.
+-- - No instances of 'Data.Binary.Binary' are provided. Use functions like
+--   'getReplay' instead. This was done to improve type inference and make
+--   profiling easier. Also some types require information that could not be
+--   provided by the 'Data.Binary.Binary' interface.
+-- - Importing one specific module is not recommended because all JSON
+--   instances are defined as orphans in "Rattletrap.Json".
+module Rattletrap
+  ( module Rattletrap.ActorMap
+  , module Rattletrap.Attribute
+  , module Rattletrap.AttributeMapping
+  , module Rattletrap.AttributeType
+  , module Rattletrap.AttributeValue
+  , module Rattletrap.Cache
+  , module Rattletrap.ClassAttributeMap
+  , module Rattletrap.ClassMapping
+  , module Rattletrap.Content
+  , module Rattletrap.Crc
+  , module Rattletrap.Data
+  , module Rattletrap.Frame
+  , module Rattletrap.Header
+  , module Rattletrap.Initialization
+  , module Rattletrap.KeyFrame
+  , module Rattletrap.Main
+  , module Rattletrap.Mark
+  , module Rattletrap.Message
+  , module Rattletrap.Primitive
+  , module Rattletrap.Property
+  , module Rattletrap.PropertyValue
+  , module Rattletrap.RemoteId
+  , module Rattletrap.Replay
+  , module Rattletrap.Replication
+  , module Rattletrap.ReplicationValue
+  , module Rattletrap.Utility
+  , module Rattletrap.Version
+  ) where
+
+import Rattletrap.ActorMap
+import Rattletrap.Attribute
+import Rattletrap.AttributeMapping
+import Rattletrap.AttributeType
+import Rattletrap.AttributeValue
+import Rattletrap.Cache
+import Rattletrap.ClassAttributeMap
+import Rattletrap.ClassMapping
+import Rattletrap.Content
+import Rattletrap.Crc
+import Rattletrap.Data
+import Rattletrap.Frame
+import Rattletrap.Header
+import Rattletrap.Initialization
+import Rattletrap.Json ()
+import Rattletrap.KeyFrame
+import Rattletrap.Main
+import Rattletrap.Mark
+import Rattletrap.Message
+import Rattletrap.Primitive
+import Rattletrap.Property
+import Rattletrap.PropertyValue
+import Rattletrap.RemoteId
+import Rattletrap.Replay
+import Rattletrap.Replication
+import Rattletrap.ReplicationValue
+import Rattletrap.Utility
+import Rattletrap.Version
diff --git a/source/library/Rattletrap/ActorMap.hs b/source/library/Rattletrap/ActorMap.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/ActorMap.hs
@@ -0,0 +1,13 @@
+module Rattletrap.ActorMap where
+
+import Rattletrap.Primitive
+
+import qualified Data.Map.Strict as Map
+
+type ActorMap = Map.Map CompressedWord Word32
+
+makeActorMap :: ActorMap
+makeActorMap = Map.empty
+
+updateActorMap :: CompressedWord -> Word32 -> ActorMap -> ActorMap
+updateActorMap actorId objectId actorMap = Map.insert actorId objectId actorMap
diff --git a/source/library/Rattletrap/Attribute.hs b/source/library/Rattletrap/Attribute.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute.hs
@@ -0,0 +1,64 @@
+module Rattletrap.Attribute where
+
+import Rattletrap.ActorMap
+import Rattletrap.AttributeValue
+import Rattletrap.ClassAttributeMap
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data Attribute = Attribute
+  { attributeId :: CompressedWord
+  , attributeName :: Text
+  -- ^ Read-only! Changing an attribute's name requires editing the class
+  -- attribute map.
+  , attributeValue :: AttributeValue
+  } deriving (Eq, Ord, Show)
+
+getAttributes
+  :: (Int, Int)
+  -> 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 getAttributeMap classAttributeMap actorMap actorId of
+    Nothing -> fail ("could not get attribute map for " ++ show actorId)
+    Just attributeMap ->
+      case getAttributeIdLimit attributeMap of
+        Nothing ->
+          fail ("could not get attribute ID limit for " ++ show actorId)
+        Just limit -> do
+          id_ <- getCompressedWord limit
+          case getAttributeName classAttributeMap attributeMap id_ of
+            Nothing -> fail ("could not get attribute name for " ++ show id_)
+            Just name -> do
+              value <- getAttributeValue version name
+              pure (Attribute id_ name value)
+
+putAttribute :: Attribute -> BinaryBit.BitPut ()
+putAttribute attribute = do
+  BinaryBit.putBool True
+  putCompressedWord (attributeId attribute)
+  putAttributeValue (attributeValue attribute)
diff --git a/source/library/Rattletrap/Attribute/Boolean.hs b/source/library/Rattletrap/Attribute/Boolean.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/Boolean.hs
@@ -0,0 +1,17 @@
+module Rattletrap.Attribute.Boolean where
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype BooleanAttribute = BooleanAttribute
+  { booleanAttributeValue :: Bool
+  } deriving (Eq, Ord, Show)
+
+getBooleanAttribute :: BinaryBit.BitGet BooleanAttribute
+getBooleanAttribute = do
+  value <- BinaryBit.getBool
+  pure (BooleanAttribute value)
+
+putBooleanAttribute :: BooleanAttribute -> BinaryBit.BitPut ()
+putBooleanAttribute booleanAttribute =
+  BinaryBit.putBool (booleanAttributeValue booleanAttribute)
diff --git a/source/library/Rattletrap/Attribute/Byte.hs b/source/library/Rattletrap/Attribute/Byte.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/Byte.hs
@@ -0,0 +1,19 @@
+module Rattletrap.Attribute.Byte where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype ByteAttribute = ByteAttribute
+  { byteAttributeValue :: Word8
+  } deriving (Eq, Ord, Show)
+
+getByteAttribute :: BinaryBit.BitGet ByteAttribute
+getByteAttribute = do
+  value <- getWord8Bits
+  pure (ByteAttribute value)
+
+putByteAttribute :: ByteAttribute -> BinaryBit.BitPut ()
+putByteAttribute byteAttribute =
+  putWord8Bits (byteAttributeValue byteAttribute)
diff --git a/source/library/Rattletrap/Attribute/CamSettings.hs b/source/library/Rattletrap/Attribute/CamSettings.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/CamSettings.hs
@@ -0,0 +1,34 @@
+module Rattletrap.Attribute.CamSettings where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data CamSettingsAttribute = CamSettingsAttribute
+  { camSettingsAttributeFov :: Float32
+  , camSettingsAttributeHeight :: Float32
+  , camSettingsAttributeAngle :: Float32
+  , camSettingsAttributeDistance :: Float32
+  , camSettingsAttributeStiffness :: Float32
+  , camSettingsAttributeSwivelSpeed :: Float32
+  } deriving (Eq, Ord, Show)
+
+getCamSettingsAttribute :: BinaryBit.BitGet CamSettingsAttribute
+getCamSettingsAttribute = do
+  fov <- getFloat32Bits
+  height <- getFloat32Bits
+  angle <- getFloat32Bits
+  distance <- getFloat32Bits
+  stiffness <- getFloat32Bits
+  swivelSpeed <- getFloat32Bits
+  pure (CamSettingsAttribute fov height angle distance stiffness swivelSpeed)
+
+putCamSettingsAttribute :: CamSettingsAttribute -> BinaryBit.BitPut ()
+putCamSettingsAttribute camSettingsAttribute = do
+  putFloat32Bits (camSettingsAttributeFov camSettingsAttribute)
+  putFloat32Bits (camSettingsAttributeHeight camSettingsAttribute)
+  putFloat32Bits (camSettingsAttributeAngle camSettingsAttribute)
+  putFloat32Bits (camSettingsAttributeDistance camSettingsAttribute)
+  putFloat32Bits (camSettingsAttributeStiffness camSettingsAttribute)
+  putFloat32Bits (camSettingsAttributeSwivelSpeed camSettingsAttribute)
diff --git a/source/library/Rattletrap/Attribute/ClubColors.hs b/source/library/Rattletrap/Attribute/ClubColors.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/ClubColors.hs
@@ -0,0 +1,28 @@
+module Rattletrap.Attribute.ClubColors where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data ClubColorsAttribute = ClubColorsAttribute
+  { clubColorsAttributeBlueFlag :: Bool
+  , clubColorsAttributeBlueColor :: Word8
+  , clubColorsAttributeOrangeFlag :: Bool
+  , clubColorsAttributeOrangeColor :: Word8
+  } deriving (Eq, Ord, Show)
+
+getClubColorsAttribute :: BinaryBit.BitGet ClubColorsAttribute
+getClubColorsAttribute = do
+  blueFlag <- BinaryBit.getBool
+  blueColor <- getWord8Bits
+  orangeFlag <- BinaryBit.getBool
+  orangeColor <- getWord8Bits
+  pure (ClubColorsAttribute blueFlag blueColor orangeFlag orangeColor)
+
+putClubColorsAttribute :: ClubColorsAttribute -> BinaryBit.BitPut ()
+putClubColorsAttribute clubColorsAttribute = do
+  BinaryBit.putBool (clubColorsAttributeBlueFlag clubColorsAttribute)
+  putWord8Bits (clubColorsAttributeBlueColor clubColorsAttribute)
+  BinaryBit.putBool (clubColorsAttributeOrangeFlag clubColorsAttribute)
+  putWord8Bits (clubColorsAttributeOrangeColor clubColorsAttribute)
diff --git a/source/library/Rattletrap/Attribute/Demolish.hs b/source/library/Rattletrap/Attribute/Demolish.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/Demolish.hs
@@ -0,0 +1,41 @@
+module Rattletrap.Attribute.Demolish where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data DemolishAttribute = DemolishAttribute
+  { demolishAttributeAttackerFlag :: Bool
+  , demolishAttributeAttackerActorId :: Word32
+  , demolishAttributeVictimFlag :: Bool
+  , demolishAttributeVictimActorId :: Word32
+  , demolishAttributeAttackerVelocity :: Vector
+  , demolishAttributeVictimVelocity :: Vector
+  } deriving (Eq, Ord, Show)
+
+getDemolishAttribute :: BinaryBit.BitGet DemolishAttribute
+getDemolishAttribute = do
+  attackerFlag <- BinaryBit.getBool
+  attackerActorId <- getWord32Bits
+  victimFlag <- BinaryBit.getBool
+  victimActorId <- getWord32Bits
+  attackerVelocity <- getVector
+  victimVelocity <- getVector
+  pure
+    (DemolishAttribute
+       attackerFlag
+       attackerActorId
+       victimFlag
+       victimActorId
+       attackerVelocity
+       victimVelocity)
+
+putDemolishAttribute :: DemolishAttribute -> BinaryBit.BitPut ()
+putDemolishAttribute demolishAttribute = do
+  BinaryBit.putBool (demolishAttributeAttackerFlag demolishAttribute)
+  putWord32Bits (demolishAttributeAttackerActorId demolishAttribute)
+  BinaryBit.putBool (demolishAttributeVictimFlag demolishAttribute)
+  putWord32Bits (demolishAttributeVictimActorId demolishAttribute)
+  putVector (demolishAttributeAttackerVelocity demolishAttribute)
+  putVector (demolishAttributeVictimVelocity demolishAttribute)
diff --git a/source/library/Rattletrap/Attribute/Enum.hs b/source/library/Rattletrap/Attribute/Enum.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/Enum.hs
@@ -0,0 +1,18 @@
+module Rattletrap.Attribute.Enum where
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Word as Word
+
+newtype EnumAttribute = EnumAttribute
+  { enumAttributeValue :: Word.Word16
+  } deriving (Eq, Ord, Show)
+
+getEnumAttribute :: BinaryBit.BitGet EnumAttribute
+getEnumAttribute = do
+  value <- BinaryBit.getWord16be 11
+  pure (EnumAttribute value)
+
+putEnumAttribute :: EnumAttribute -> BinaryBit.BitPut ()
+putEnumAttribute enumAttribute =
+  BinaryBit.putWord16be 11 (enumAttributeValue enumAttribute)
diff --git a/source/library/Rattletrap/Attribute/Explosion.hs b/source/library/Rattletrap/Attribute/Explosion.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/Explosion.hs
@@ -0,0 +1,24 @@
+module Rattletrap.Attribute.Explosion where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data ExplosionAttribute = ExplosionAttribute
+  { explosionAttributeActorId :: Int32
+  , explosionAttributeLocation :: Vector
+  } deriving (Eq, Ord, Show)
+
+getExplosionAttribute :: BinaryBit.BitGet ExplosionAttribute
+getExplosionAttribute = do
+  False <- BinaryBit.getBool
+  actorId <- getInt32Bits
+  location <- getVector
+  pure (ExplosionAttribute actorId location)
+
+putExplosionAttribute :: ExplosionAttribute -> BinaryBit.BitPut ()
+putExplosionAttribute explosionAttribute = do
+  BinaryBit.putBool False
+  putInt32Bits (explosionAttributeActorId explosionAttribute)
+  putVector (explosionAttributeLocation explosionAttribute)
diff --git a/source/library/Rattletrap/Attribute/FlaggedInt.hs b/source/library/Rattletrap/Attribute/FlaggedInt.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/FlaggedInt.hs
@@ -0,0 +1,22 @@
+module Rattletrap.Attribute.FlaggedInt where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data FlaggedIntAttribute = FlaggedIntAttribute
+  { flaggedIntAttributeFlag :: Bool
+  , flaggedIntAttributeInt :: Int32
+  } deriving (Eq, Ord, Show)
+
+getFlaggedIntAttribute :: BinaryBit.BitGet FlaggedIntAttribute
+getFlaggedIntAttribute = do
+  flag <- BinaryBit.getBool
+  int <- getInt32Bits
+  pure (FlaggedIntAttribute flag int)
+
+putFlaggedIntAttribute :: FlaggedIntAttribute -> BinaryBit.BitPut ()
+putFlaggedIntAttribute flaggedIntAttribute = do
+  BinaryBit.putBool (flaggedIntAttributeFlag flaggedIntAttribute)
+  putInt32Bits (flaggedIntAttributeInt flaggedIntAttribute)
diff --git a/source/library/Rattletrap/Attribute/Float.hs b/source/library/Rattletrap/Attribute/Float.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/Float.hs
@@ -0,0 +1,19 @@
+module Rattletrap.Attribute.Float where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype FloatAttribute = FloatAttribute
+  { floatAttributeValue :: Float32
+  } deriving (Eq, Ord, Show)
+
+getFloatAttribute :: BinaryBit.BitGet FloatAttribute
+getFloatAttribute = do
+  value <- getFloat32Bits
+  pure (FloatAttribute value)
+
+putFloatAttribute :: FloatAttribute -> BinaryBit.BitPut ()
+putFloatAttribute floatAttribute =
+  putFloat32Bits (floatAttributeValue floatAttribute)
diff --git a/source/library/Rattletrap/Attribute/GameMode.hs b/source/library/Rattletrap/Attribute/GameMode.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/GameMode.hs
@@ -0,0 +1,25 @@
+module Rattletrap.Attribute.GameMode where
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Word as Word
+
+data GameModeAttribute = GameModeAttribute
+  { gameModeAttributeNumBits :: Int
+  , gameModeAttributeWord :: Word.Word8
+  } deriving (Eq, Ord, Show)
+
+getGameModeAttribute :: (Int, Int) -> BinaryBit.BitGet GameModeAttribute
+getGameModeAttribute version = do
+  let numBits =
+        if version < (868, 12)
+          then 2
+          else 8
+  word <- BinaryBit.getWord8 numBits
+  pure (GameModeAttribute numBits word)
+
+putGameModeAttribute :: GameModeAttribute -> BinaryBit.BitPut ()
+putGameModeAttribute gameModeAttribute = do
+  let numBits = gameModeAttributeNumBits gameModeAttribute
+  let word = gameModeAttributeWord gameModeAttribute
+  BinaryBit.putWord8 numBits word
diff --git a/source/library/Rattletrap/Attribute/Int.hs b/source/library/Rattletrap/Attribute/Int.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/Int.hs
@@ -0,0 +1,18 @@
+module Rattletrap.Attribute.Int where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype IntAttribute = IntAttribute
+  { intAttributeValue :: Int32
+  } deriving (Eq, Ord, Show)
+
+getIntAttribute :: BinaryBit.BitGet IntAttribute
+getIntAttribute = do
+  value <- getInt32Bits
+  pure (IntAttribute value)
+
+putIntAttribute :: IntAttribute -> BinaryBit.BitPut ()
+putIntAttribute intAttribute = putInt32Bits (intAttributeValue intAttribute)
diff --git a/source/library/Rattletrap/Attribute/Loadout.hs b/source/library/Rattletrap/Attribute/Loadout.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/Loadout.hs
@@ -0,0 +1,51 @@
+module Rattletrap.Attribute.Loadout where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data LoadoutAttribute = LoadoutAttribute
+  { loadoutAttributeVersion :: Word8
+  , loadoutAttributeBody :: Word32
+  , loadoutAttributeDecal :: Word32
+  , loadoutAttributeWheels :: Word32
+  , loadoutAttributeRocketTrail :: Word32
+  , loadoutAttributeAntenna :: Word32
+  , loadoutAttributeTopper :: Word32
+  , loadoutAttributeUnknown1 :: Word32
+  , loadoutAttributeUnknown2 :: Maybe Word32
+  } deriving (Eq, Ord, Show)
+
+getLoadoutAttribute :: BinaryBit.BitGet LoadoutAttribute
+getLoadoutAttribute = do
+  version <- getWord8Bits
+  body <- getWord32Bits
+  decal <- getWord32Bits
+  wheels <- getWord32Bits
+  rocketTrail <- getWord32Bits
+  antenna <- getWord32Bits
+  topper <- getWord32Bits
+  g <- getWord32Bits
+  h <-
+    if version > Word8 10
+      then do
+        h <- getWord32Bits
+        pure (Just h)
+      else pure Nothing
+  pure
+    (LoadoutAttribute version body decal wheels rocketTrail antenna topper g h)
+
+putLoadoutAttribute :: LoadoutAttribute -> BinaryBit.BitPut ()
+putLoadoutAttribute loadoutAttribute = do
+  putWord8Bits (loadoutAttributeVersion loadoutAttribute)
+  putWord32Bits (loadoutAttributeBody loadoutAttribute)
+  putWord32Bits (loadoutAttributeDecal loadoutAttribute)
+  putWord32Bits (loadoutAttributeWheels loadoutAttribute)
+  putWord32Bits (loadoutAttributeRocketTrail loadoutAttribute)
+  putWord32Bits (loadoutAttributeAntenna loadoutAttribute)
+  putWord32Bits (loadoutAttributeTopper loadoutAttribute)
+  putWord32Bits (loadoutAttributeUnknown1 loadoutAttribute)
+  case loadoutAttributeUnknown2 loadoutAttribute of
+    Nothing -> pure ()
+    Just x -> putWord32Bits x
diff --git a/source/library/Rattletrap/Attribute/LoadoutOnline.hs b/source/library/Rattletrap/Attribute/LoadoutOnline.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/LoadoutOnline.hs
@@ -0,0 +1,39 @@
+module Rattletrap.Attribute.LoadoutOnline where
+
+import Rattletrap.Primitive
+
+import qualified Control.Monad as Monad
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype LoadoutOnlineAttribute = LoadoutOnlineAttribute
+  { loadoutAttributeValue :: [[(Word32, CompressedWord)]]
+  } deriving (Eq, Ord, Show)
+
+getLoadoutOnlineAttribute :: BinaryBit.BitGet LoadoutOnlineAttribute
+getLoadoutOnlineAttribute = 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 (LoadoutOnlineAttribute values)
+
+putLoadoutOnlineAttribute :: LoadoutOnlineAttribute -> BinaryBit.BitPut ()
+putLoadoutOnlineAttribute loadoutAttribute = do
+  let values = loadoutAttributeValue loadoutAttribute
+  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/source/library/Rattletrap/Attribute/Loadouts.hs b/source/library/Rattletrap/Attribute/Loadouts.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/Loadouts.hs
@@ -0,0 +1,22 @@
+module Rattletrap.Attribute.Loadouts where
+
+import Rattletrap.Attribute.Loadout
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data LoadoutsAttribute = LoadoutsAttribute
+  { loadoutsAttributeBlue :: LoadoutAttribute
+  , loadoutsAttributeOrange :: LoadoutAttribute
+  } deriving (Eq, Ord, Show)
+
+getLoadoutsAttribute :: BinaryBit.BitGet LoadoutsAttribute
+getLoadoutsAttribute = do
+  blue <- getLoadoutAttribute
+  orange <- getLoadoutAttribute
+  pure (LoadoutsAttribute blue orange)
+
+putLoadoutsAttribute :: LoadoutsAttribute -> BinaryBit.BitPut ()
+putLoadoutsAttribute loadoutsAttribute = do
+  putLoadoutAttribute (loadoutsAttributeBlue loadoutsAttribute)
+  putLoadoutAttribute (loadoutsAttributeOrange loadoutsAttribute)
diff --git a/source/library/Rattletrap/Attribute/LoadoutsOnline.hs b/source/library/Rattletrap/Attribute/LoadoutsOnline.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/LoadoutsOnline.hs
@@ -0,0 +1,30 @@
+module Rattletrap.Attribute.LoadoutsOnline where
+
+import Rattletrap.Attribute.LoadoutOnline
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data LoadoutsOnlineAttribute = LoadoutsOnlineAttribute
+  { loadoutsOnlineAttributeBlue :: LoadoutOnlineAttribute
+  , loadoutsOnlineAttributeOrange :: LoadoutOnlineAttribute
+  , loadoutsOnlineAttributeUnknown1 :: Bool
+  , loadoutsOnlineAttributeUnknown2 :: Bool
+  } deriving (Eq, Ord, Show)
+
+getLoadoutsOnlineAttribute :: BinaryBit.BitGet LoadoutsOnlineAttribute
+getLoadoutsOnlineAttribute = do
+  blueLoadout <- getLoadoutOnlineAttribute
+  orangeLoadout <- getLoadoutOnlineAttribute
+  unknown1 <- BinaryBit.getBool
+  unknown2 <- BinaryBit.getBool
+  pure (LoadoutsOnlineAttribute blueLoadout orangeLoadout unknown1 unknown2)
+
+putLoadoutsOnlineAttribute :: LoadoutsOnlineAttribute -> BinaryBit.BitPut ()
+putLoadoutsOnlineAttribute loadoutsOnlineAttribute = do
+  putLoadoutOnlineAttribute
+    (loadoutsOnlineAttributeBlue loadoutsOnlineAttribute)
+  putLoadoutOnlineAttribute
+    (loadoutsOnlineAttributeOrange loadoutsOnlineAttribute)
+  BinaryBit.putBool (loadoutsOnlineAttributeUnknown1 loadoutsOnlineAttribute)
+  BinaryBit.putBool (loadoutsOnlineAttributeUnknown2 loadoutsOnlineAttribute)
diff --git a/source/library/Rattletrap/Attribute/Location.hs b/source/library/Rattletrap/Attribute/Location.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/Location.hs
@@ -0,0 +1,19 @@
+module Rattletrap.Attribute.Location where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype LocationAttribute = LocationAttribute
+  { locationAttributeValue :: Vector
+  } deriving (Eq, Ord, Show)
+
+getLocationAttribute :: BinaryBit.BitGet LocationAttribute
+getLocationAttribute = do
+  value <- getVector
+  pure (LocationAttribute value)
+
+putLocationAttribute :: LocationAttribute -> BinaryBit.BitPut ()
+putLocationAttribute locationAttribute =
+  putVector (locationAttributeValue locationAttribute)
diff --git a/source/library/Rattletrap/Attribute/MusicStinger.hs b/source/library/Rattletrap/Attribute/MusicStinger.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/MusicStinger.hs
@@ -0,0 +1,25 @@
+module Rattletrap.Attribute.MusicStinger where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data MusicStingerAttribute = MusicStingerAttribute
+  { musicStingerAttributeFlag :: Bool
+  , musicStingerAttributeCue :: Word32
+  , musicStingerAttributeTrigger :: Word8
+  } deriving (Eq, Ord, Show)
+
+getMusicStingerAttribute :: BinaryBit.BitGet MusicStingerAttribute
+getMusicStingerAttribute = do
+  flag <- BinaryBit.getBool
+  cue <- getWord32Bits
+  trigger <- getWord8Bits
+  pure (MusicStingerAttribute flag cue trigger)
+
+putMusicStingerAttribute :: MusicStingerAttribute -> BinaryBit.BitPut ()
+putMusicStingerAttribute musicStingerAttribute = do
+  BinaryBit.putBool (musicStingerAttributeFlag musicStingerAttribute)
+  putWord32Bits (musicStingerAttributeCue musicStingerAttribute)
+  putWord8Bits (musicStingerAttributeTrigger musicStingerAttribute)
diff --git a/source/library/Rattletrap/Attribute/PartyLeader.hs b/source/library/Rattletrap/Attribute/PartyLeader.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/PartyLeader.hs
@@ -0,0 +1,33 @@
+module Rattletrap.Attribute.PartyLeader where
+
+import Rattletrap.Primitive
+import Rattletrap.RemoteId
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data PartyLeaderAttribute = PartyLeaderAttribute
+  { partyLeaderAttributeSystemId :: Word8
+  , partyLeaderAttributeId :: Maybe (RemoteId, Word8)
+  } deriving (Eq, Ord, Show)
+
+getPartyLeaderAttribute :: BinaryBit.BitGet PartyLeaderAttribute
+getPartyLeaderAttribute = do
+  systemId <- getWord8Bits
+  maybeRemoteAndLocalId <-
+    if systemId == Word8 0
+      then pure Nothing
+      else do
+        remoteId <- getRemoteId systemId
+        localId <- getWord8Bits
+        pure (Just (remoteId, localId))
+  pure (PartyLeaderAttribute systemId maybeRemoteAndLocalId)
+
+putPartyLeaderAttribute :: PartyLeaderAttribute -> BinaryBit.BitPut ()
+putPartyLeaderAttribute partyLeaderAttribute = do
+  putWord8Bits (partyLeaderAttributeSystemId partyLeaderAttribute)
+  case partyLeaderAttributeId partyLeaderAttribute of
+    Nothing -> pure ()
+    Just (remoteId, localId) -> do
+      putRemoteId remoteId
+      putWord8Bits localId
diff --git a/source/library/Rattletrap/Attribute/Pickup.hs b/source/library/Rattletrap/Attribute/Pickup.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/Pickup.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Attribute.Pickup where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data PickupAttribute = PickupAttribute
+  { pickupAttributeInstigatorId :: Maybe Word32
+  , pickupAttributePickedUp :: Bool
+  } deriving (Eq, Ord, Show)
+
+getPickupAttribute :: BinaryBit.BitGet PickupAttribute
+getPickupAttribute = do
+  instigator <- BinaryBit.getBool
+  maybeInstigatorId <-
+    if instigator
+      then do
+        instigatorId <- getWord32Bits
+        pure (Just instigatorId)
+      else pure Nothing
+  pickedUp <- BinaryBit.getBool
+  pure (PickupAttribute maybeInstigatorId pickedUp)
+
+putPickupAttribute :: PickupAttribute -> BinaryBit.BitPut ()
+putPickupAttribute pickupAttribute = do
+  case pickupAttributeInstigatorId pickupAttribute of
+    Nothing -> BinaryBit.putBool False
+    Just instigatorId -> do
+      BinaryBit.putBool True
+      putWord32Bits instigatorId
+  BinaryBit.putBool (pickupAttributePickedUp pickupAttribute)
diff --git a/source/library/Rattletrap/Attribute/PrivateMatchSettings.hs b/source/library/Rattletrap/Attribute/PrivateMatchSettings.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/PrivateMatchSettings.hs
@@ -0,0 +1,48 @@
+module Rattletrap.Attribute.PrivateMatchSettings where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data PrivateMatchSettingsAttribute = PrivateMatchSettingsAttribute
+  { privateMatchSettingsAttributeMutators :: Text
+  , privateMatchSettingsAttributeJoinableBy :: Word32
+  , privateMatchSettingsAttributeMaxPlayers :: Word32
+  , privateMatchSettingsAttributeGameName :: Text
+  , privateMatchSettingsAttributePassword :: Text
+  , privateMatchSettingsAttributeFlag :: Bool
+  } deriving (Eq, Ord, Show)
+
+getPrivateMatchSettingsAttribute :: BinaryBit.BitGet PrivateMatchSettingsAttribute
+getPrivateMatchSettingsAttribute = do
+  mutators <- getTextBits
+  joinableBy <- getWord32Bits
+  maxPlayers <- getWord32Bits
+  gameName <- getTextBits
+  password <- getTextBits
+  flag <- BinaryBit.getBool
+  pure
+    (PrivateMatchSettingsAttribute
+       mutators
+       joinableBy
+       maxPlayers
+       gameName
+       password
+       flag)
+
+putPrivateMatchSettingsAttribute :: PrivateMatchSettingsAttribute
+                                 -> BinaryBit.BitPut ()
+putPrivateMatchSettingsAttribute privateMatchSettingsAttribute = do
+  putTextBits
+    (privateMatchSettingsAttributeMutators privateMatchSettingsAttribute)
+  putWord32Bits
+    (privateMatchSettingsAttributeJoinableBy privateMatchSettingsAttribute)
+  putWord32Bits
+    (privateMatchSettingsAttributeMaxPlayers privateMatchSettingsAttribute)
+  putTextBits
+    (privateMatchSettingsAttributeGameName privateMatchSettingsAttribute)
+  putTextBits
+    (privateMatchSettingsAttributePassword privateMatchSettingsAttribute)
+  BinaryBit.putBool
+    (privateMatchSettingsAttributeFlag privateMatchSettingsAttribute)
diff --git a/source/library/Rattletrap/Attribute/QWord.hs b/source/library/Rattletrap/Attribute/QWord.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/QWord.hs
@@ -0,0 +1,19 @@
+module Rattletrap.Attribute.QWord where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype QWordAttribute = QWordAttribute
+  { qWordAttributeValue :: Word64
+  } deriving (Eq, Ord, Show)
+
+getQWordAttribute :: BinaryBit.BitGet QWordAttribute
+getQWordAttribute = do
+  value <- getWord64Bits
+  pure (QWordAttribute value)
+
+putQWordAttribute :: QWordAttribute -> BinaryBit.BitPut ()
+putQWordAttribute qWordAttribute =
+  putWord64Bits (qWordAttributeValue qWordAttribute)
diff --git a/source/library/Rattletrap/Attribute/Reservation.hs b/source/library/Rattletrap/Attribute/Reservation.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/Reservation.hs
@@ -0,0 +1,50 @@
+module Rattletrap.Attribute.Reservation where
+
+import Rattletrap.Attribute.UniqueId
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Word as Word
+
+data ReservationAttribute = ReservationAttribute
+  { reservationAttributeNumber :: CompressedWord
+  , reservationAttributeUniqueId :: UniqueIdAttribute
+  , reservationAttributeName :: Maybe Text
+  , reservationAttributeUnknown1 :: Bool
+  , reservationAttributeUnknown2 :: Bool
+  , reservationAttributeUnknown3 :: Maybe Word.Word8
+  } deriving (Eq, Ord, Show)
+
+getReservationAttribute :: (Int, Int) -> BinaryBit.BitGet ReservationAttribute
+getReservationAttribute version = do
+  number <- getCompressedWord 7
+  uniqueId <- getUniqueIdAttribute
+  name <-
+    if uniqueIdAttributeSystemId uniqueId == Word8 0
+      then pure Nothing
+      else do
+        name <- getTextBits
+        pure (Just name)
+  a <- BinaryBit.getBool
+  b <- BinaryBit.getBool
+  mc <-
+    if version < (868, 12)
+      then pure Nothing
+      else do
+        c <- BinaryBit.getWord8 6
+        pure (Just c)
+  pure (ReservationAttribute number uniqueId name a b mc)
+
+putReservationAttribute :: ReservationAttribute -> BinaryBit.BitPut ()
+putReservationAttribute reservationAttribute = do
+  putCompressedWord (reservationAttributeNumber reservationAttribute)
+  putUniqueIdAttribute (reservationAttributeUniqueId reservationAttribute)
+  case reservationAttributeName reservationAttribute of
+    Nothing -> pure ()
+    Just name -> putTextBits name
+  BinaryBit.putBool (reservationAttributeUnknown1 reservationAttribute)
+  BinaryBit.putBool (reservationAttributeUnknown2 reservationAttribute)
+  case reservationAttributeUnknown3 reservationAttribute of
+    Nothing -> pure ()
+    Just c -> BinaryBit.putWord8 6 c
diff --git a/source/library/Rattletrap/Attribute/RigidBodyState.hs b/source/library/Rattletrap/Attribute/RigidBodyState.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/RigidBodyState.hs
@@ -0,0 +1,52 @@
+module Rattletrap.Attribute.RigidBodyState where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data RigidBodyStateAttribute = RigidBodyStateAttribute
+  { rigidBodyStateAttributeSleeping :: Bool
+  , rigidBodyStateAttributeLocation :: Vector
+  , rigidBodyStateAttributeRotation :: CompressedWordVector
+  , rigidBodyStateAttributeLinearVelocity :: Maybe Vector
+  , rigidBodyStateAttributeAngularVelocity :: Maybe Vector
+  } deriving (Eq, Ord, Show)
+
+getRigidBodyStateAttribute :: BinaryBit.BitGet RigidBodyStateAttribute
+getRigidBodyStateAttribute = do
+  sleeping <- BinaryBit.getBool
+  location <- getVector
+  rotation <- getCompressedWordVector
+  linearVelocity <-
+    if sleeping
+      then pure Nothing
+      else do
+        linearVelocity <- getVector
+        pure (Just linearVelocity)
+  angularVelocity <-
+    if sleeping
+      then pure Nothing
+      else do
+        angularVelocity <- getVector
+        pure (Just angularVelocity)
+  pure
+    (RigidBodyStateAttribute
+       sleeping
+       location
+       rotation
+       linearVelocity
+       angularVelocity)
+
+putRigidBodyStateAttribute :: RigidBodyStateAttribute -> BinaryBit.BitPut ()
+putRigidBodyStateAttribute rigidBodyStateAttribute = do
+  BinaryBit.putBool (rigidBodyStateAttributeSleeping rigidBodyStateAttribute)
+  putVector (rigidBodyStateAttributeLocation rigidBodyStateAttribute)
+  putCompressedWordVector
+    (rigidBodyStateAttributeRotation rigidBodyStateAttribute)
+  case rigidBodyStateAttributeLinearVelocity rigidBodyStateAttribute of
+    Nothing -> pure ()
+    Just linearVelocity -> putVector linearVelocity
+  case rigidBodyStateAttributeAngularVelocity rigidBodyStateAttribute of
+    Nothing -> pure ()
+    Just angularVelocity -> putVector angularVelocity
diff --git a/source/library/Rattletrap/Attribute/String.hs b/source/library/Rattletrap/Attribute/String.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/String.hs
@@ -0,0 +1,19 @@
+module Rattletrap.Attribute.String where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype StringAttribute = StringAttribute
+  { stringAttributeValue :: Text
+  } deriving (Eq, Ord, Show)
+
+getStringAttribute :: BinaryBit.BitGet StringAttribute
+getStringAttribute = do
+  value <- getTextBits
+  pure (StringAttribute value)
+
+putStringAttribute :: StringAttribute -> BinaryBit.BitPut ()
+putStringAttribute stringAttribute =
+  putTextBits (stringAttributeValue stringAttribute)
diff --git a/source/library/Rattletrap/Attribute/TeamPaint.hs b/source/library/Rattletrap/Attribute/TeamPaint.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/TeamPaint.hs
@@ -0,0 +1,37 @@
+module Rattletrap.Attribute.TeamPaint where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data TeamPaintAttribute = TeamPaintAttribute
+  { teamPaintAttributeTeam :: Word8
+  , teamPaintAttributePrimaryColor :: Word8
+  , teamPaintAttributeAccentColor :: Word8
+  , teamPaintAttributePrimaryFinish :: Word32
+  , teamPaintAttributeAccentFinish :: Word32
+  } deriving (Eq, Ord, Show)
+
+getTeamPaintAttribute :: BinaryBit.BitGet TeamPaintAttribute
+getTeamPaintAttribute = do
+  team <- getWord8Bits
+  primaryColor <- getWord8Bits
+  accentColor <- getWord8Bits
+  primaryFinish <- getWord32Bits
+  accentFinish <- getWord32Bits
+  pure
+    (TeamPaintAttribute
+       team
+       primaryColor
+       accentColor
+       primaryFinish
+       accentFinish)
+
+putTeamPaintAttribute :: TeamPaintAttribute -> BinaryBit.BitPut ()
+putTeamPaintAttribute teamPaintAttribute = do
+  putWord8Bits (teamPaintAttributeTeam teamPaintAttribute)
+  putWord8Bits (teamPaintAttributePrimaryColor teamPaintAttribute)
+  putWord8Bits (teamPaintAttributeAccentColor teamPaintAttribute)
+  putWord32Bits (teamPaintAttributePrimaryFinish teamPaintAttribute)
+  putWord32Bits (teamPaintAttributeAccentFinish teamPaintAttribute)
diff --git a/source/library/Rattletrap/Attribute/UniqueId.hs b/source/library/Rattletrap/Attribute/UniqueId.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/UniqueId.hs
@@ -0,0 +1,26 @@
+module Rattletrap.Attribute.UniqueId where
+
+import Rattletrap.Primitive
+import Rattletrap.RemoteId
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data UniqueIdAttribute = UniqueIdAttribute
+  { uniqueIdAttributeSystemId :: Word8
+  , uniqueIdAttributeRemoteId :: RemoteId
+  , uniqueIdAttributeLocalId :: Word8
+  } deriving (Eq, Ord, Show)
+
+getUniqueIdAttribute :: BinaryBit.BitGet UniqueIdAttribute
+getUniqueIdAttribute = do
+  systemId <- getWord8Bits
+  remoteId <- getRemoteId systemId
+  localId <- getWord8Bits
+  pure (UniqueIdAttribute systemId remoteId localId)
+
+putUniqueIdAttribute :: UniqueIdAttribute -> BinaryBit.BitPut ()
+putUniqueIdAttribute uniqueIdAttribute = do
+  putWord8Bits (uniqueIdAttributeSystemId uniqueIdAttribute)
+  putRemoteId (uniqueIdAttributeRemoteId uniqueIdAttribute)
+  putWord8Bits (uniqueIdAttributeLocalId uniqueIdAttribute)
diff --git a/source/library/Rattletrap/Attribute/WeldedInfo.hs b/source/library/Rattletrap/Attribute/WeldedInfo.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Attribute/WeldedInfo.hs
@@ -0,0 +1,31 @@
+module Rattletrap.Attribute.WeldedInfo where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data WeldedInfoAttribute = WeldedInfoAttribute
+  { weldedInfoAttributeActive :: Bool
+  , weldedInfoAttributeActorId :: Int32
+  , weldedInfoAttributeOffset :: Vector
+  , weldedInfoAttributeMass :: Float32
+  , weldedInfoAttributeRotation :: Int8Vector
+  } deriving (Eq, Ord, Show)
+
+getWeldedInfoAttribute :: BinaryBit.BitGet WeldedInfoAttribute
+getWeldedInfoAttribute = do
+  active <- BinaryBit.getBool
+  actorId <- getInt32Bits
+  offset <- getVector
+  mass <- getFloat32Bits
+  rotation <- getInt8Vector
+  pure (WeldedInfoAttribute active actorId offset mass rotation)
+
+putWeldedInfoAttribute :: WeldedInfoAttribute -> BinaryBit.BitPut ()
+putWeldedInfoAttribute weldedInfoAttribute = do
+  BinaryBit.putBool (weldedInfoAttributeActive weldedInfoAttribute)
+  putInt32Bits (weldedInfoAttributeActorId weldedInfoAttribute)
+  putVector (weldedInfoAttributeOffset weldedInfoAttribute)
+  putFloat32Bits (weldedInfoAttributeMass weldedInfoAttribute)
+  putInt8Vector (weldedInfoAttributeRotation weldedInfoAttribute)
diff --git a/source/library/Rattletrap/AttributeMapping.hs b/source/library/Rattletrap/AttributeMapping.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/AttributeMapping.hs
@@ -0,0 +1,21 @@
+module Rattletrap.AttributeMapping where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary as Binary
+
+data AttributeMapping = AttributeMapping
+  { attributeMappingObjectId :: Word32
+  , attributeMappingStreamId :: Word32
+  } deriving (Eq, Ord, Show)
+
+getAttributeMapping :: Binary.Get AttributeMapping
+getAttributeMapping = do
+  objectId <- getWord32
+  streamId <- getWord32
+  pure (AttributeMapping objectId streamId)
+
+putAttributeMapping :: AttributeMapping -> Binary.Put
+putAttributeMapping attributeMapping = do
+  putWord32 (attributeMappingObjectId attributeMapping)
+  putWord32 (attributeMappingStreamId attributeMapping)
diff --git a/source/library/Rattletrap/AttributeType.hs b/source/library/Rattletrap/AttributeType.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/AttributeType.hs
@@ -0,0 +1,31 @@
+module Rattletrap.AttributeType where
+
+data AttributeType
+  = BooleanAttributeType
+  | ByteAttributeType
+  | CamSettingsAttributeType
+  | ClubColorsAttributeType
+  | DemolishAttributeType
+  | EnumAttributeType
+  | ExplosionAttributeType
+  | FlaggedIntAttributeType
+  | FloatAttributeType
+  | GameModeAttributeType
+  | IntAttributeType
+  | LoadoutAttributeType
+  | LoadoutOnlineAttributeType
+  | LoadoutsAttributeType
+  | LoadoutsOnlineAttributeType
+  | LocationAttributeType
+  | MusicStingerAttributeType
+  | PartyLeaderAttributeType
+  | PickupAttributeType
+  | PrivateMatchSettingsAttributeType
+  | QWordAttributeType
+  | ReservationAttributeType
+  | RigidBodyStateAttributeType
+  | StringAttributeType
+  | TeamPaintAttributeType
+  | UniqueIdAttributeType
+  | WeldedInfoAttributeType
+  deriving (Eq, Ord, Show)
diff --git a/source/library/Rattletrap/AttributeValue.hs b/source/library/Rattletrap/AttributeValue.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/AttributeValue.hs
@@ -0,0 +1,217 @@
+module Rattletrap.AttributeValue
+  ( module Rattletrap.AttributeValue
+  , module Rattletrap.Attribute.Boolean
+  , module Rattletrap.Attribute.Byte
+  , module Rattletrap.Attribute.CamSettings
+  , module Rattletrap.Attribute.ClubColors
+  , module Rattletrap.Attribute.Demolish
+  , module Rattletrap.Attribute.Enum
+  , module Rattletrap.Attribute.Explosion
+  , module Rattletrap.Attribute.FlaggedInt
+  , module Rattletrap.Attribute.Float
+  , module Rattletrap.Attribute.GameMode
+  , module Rattletrap.Attribute.Int
+  , module Rattletrap.Attribute.Loadout
+  , module Rattletrap.Attribute.LoadoutOnline
+  , module Rattletrap.Attribute.Loadouts
+  , module Rattletrap.Attribute.LoadoutsOnline
+  , module Rattletrap.Attribute.Location
+  , module Rattletrap.Attribute.MusicStinger
+  , module Rattletrap.Attribute.PartyLeader
+  , module Rattletrap.Attribute.Pickup
+  , module Rattletrap.Attribute.PrivateMatchSettings
+  , module Rattletrap.Attribute.QWord
+  , module Rattletrap.Attribute.Reservation
+  , module Rattletrap.Attribute.RigidBodyState
+  , module Rattletrap.Attribute.String
+  , module Rattletrap.Attribute.TeamPaint
+  , module Rattletrap.Attribute.UniqueId
+  , module Rattletrap.Attribute.WeldedInfo
+  ) where
+
+import Rattletrap.Attribute.Boolean
+import Rattletrap.Attribute.Byte
+import Rattletrap.Attribute.CamSettings
+import Rattletrap.Attribute.ClubColors
+import Rattletrap.Attribute.Demolish
+import Rattletrap.Attribute.Enum
+import Rattletrap.Attribute.Explosion
+import Rattletrap.Attribute.FlaggedInt
+import Rattletrap.Attribute.Float
+import Rattletrap.Attribute.GameMode
+import Rattletrap.Attribute.Int
+import Rattletrap.Attribute.Loadout
+import Rattletrap.Attribute.LoadoutOnline
+import Rattletrap.Attribute.Loadouts
+import Rattletrap.Attribute.LoadoutsOnline
+import Rattletrap.Attribute.Location
+import Rattletrap.Attribute.MusicStinger
+import Rattletrap.Attribute.PartyLeader
+import Rattletrap.Attribute.Pickup
+import Rattletrap.Attribute.PrivateMatchSettings
+import Rattletrap.Attribute.QWord
+import Rattletrap.Attribute.Reservation
+import Rattletrap.Attribute.RigidBodyState
+import Rattletrap.Attribute.String
+import Rattletrap.Attribute.TeamPaint
+import Rattletrap.Attribute.UniqueId
+import Rattletrap.Attribute.WeldedInfo
+import Rattletrap.AttributeType
+import Rattletrap.Data
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Map.Strict as Map
+
+data AttributeValue
+  = BooleanAttributeValue BooleanAttribute
+  | ByteAttributeValue ByteAttribute
+  | CamSettingsAttributeValue CamSettingsAttribute
+  | ClubColorsAttributeValue ClubColorsAttribute
+  | DemolishAttributeValue DemolishAttribute
+  | EnumAttributeValue EnumAttribute
+  | ExplosionAttributeValue ExplosionAttribute
+  | FlaggedIntAttributeValue FlaggedIntAttribute
+  | FloatAttributeValue FloatAttribute
+  | GameModeAttributeValue GameModeAttribute
+  | IntAttributeValue IntAttribute
+  | LoadoutAttributeValue LoadoutAttribute
+  | LoadoutOnlineAttributeValue LoadoutOnlineAttribute
+  | LoadoutsAttributeValue LoadoutsAttribute
+  | LoadoutsOnlineAttributeValue LoadoutsOnlineAttribute
+  | LocationAttributeValue LocationAttribute
+  | MusicStingerAttributeValue MusicStingerAttribute
+  | PartyLeaderAttributeValue PartyLeaderAttribute
+  | PickupAttributeValue PickupAttribute
+  | PrivateMatchSettingsAttributeValue PrivateMatchSettingsAttribute
+  | QWordAttributeValue QWordAttribute
+  | ReservationAttributeValue ReservationAttribute
+  | RigidBodyStateAttributeValue RigidBodyStateAttribute
+  | StringAttributeValue StringAttribute
+  | TeamPaintAttributeValue TeamPaintAttribute
+  | UniqueIdAttributeValue UniqueIdAttribute
+  | WeldedInfoAttributeValue WeldedInfoAttribute
+  deriving (Eq, Ord, Show)
+
+getAttributeValue :: (Int, Int) -> Text -> BinaryBit.BitGet AttributeValue
+getAttributeValue version name =
+  case Map.lookup name attributeTypes of
+    Just constructor ->
+      case constructor of
+        BooleanAttributeType -> do
+          x <- getBooleanAttribute
+          pure (BooleanAttributeValue x)
+        ByteAttributeType -> do
+          x <- getByteAttribute
+          pure (ByteAttributeValue x)
+        CamSettingsAttributeType -> do
+          x <- getCamSettingsAttribute
+          pure (CamSettingsAttributeValue x)
+        ClubColorsAttributeType -> do
+          x <- getClubColorsAttribute
+          pure (ClubColorsAttributeValue x)
+        DemolishAttributeType -> do
+          x <- getDemolishAttribute
+          pure (DemolishAttributeValue x)
+        EnumAttributeType -> do
+          x <- getEnumAttribute
+          pure (EnumAttributeValue x)
+        ExplosionAttributeType -> do
+          x <- getExplosionAttribute
+          pure (ExplosionAttributeValue x)
+        FlaggedIntAttributeType -> do
+          x <- getFlaggedIntAttribute
+          pure (FlaggedIntAttributeValue x)
+        FloatAttributeType -> do
+          x <- getFloatAttribute
+          pure (FloatAttributeValue x)
+        GameModeAttributeType -> do
+          x <- getGameModeAttribute version
+          pure (GameModeAttributeValue x)
+        IntAttributeType -> do
+          x <- getIntAttribute
+          pure (IntAttributeValue x)
+        LoadoutAttributeType -> do
+          x <- getLoadoutAttribute
+          pure (LoadoutAttributeValue x)
+        LoadoutOnlineAttributeType -> do
+          x <- getLoadoutOnlineAttribute
+          pure (LoadoutOnlineAttributeValue x)
+        LoadoutsAttributeType -> do
+          x <- getLoadoutsAttribute
+          pure (LoadoutsAttributeValue x)
+        LoadoutsOnlineAttributeType -> do
+          x <- getLoadoutsOnlineAttribute
+          pure (LoadoutsOnlineAttributeValue x)
+        LocationAttributeType -> do
+          x <- getLocationAttribute
+          pure (LocationAttributeValue x)
+        MusicStingerAttributeType -> do
+          x <- getMusicStingerAttribute
+          pure (MusicStingerAttributeValue x)
+        PartyLeaderAttributeType -> do
+          x <- getPartyLeaderAttribute
+          pure (PartyLeaderAttributeValue x)
+        PickupAttributeType -> do
+          x <- getPickupAttribute
+          pure (PickupAttributeValue x)
+        PrivateMatchSettingsAttributeType -> do
+          x <- getPrivateMatchSettingsAttribute
+          pure (PrivateMatchSettingsAttributeValue x)
+        QWordAttributeType -> do
+          x <- getQWordAttribute
+          pure (QWordAttributeValue x)
+        ReservationAttributeType -> do
+          x <- getReservationAttribute version
+          pure (ReservationAttributeValue x)
+        RigidBodyStateAttributeType -> do
+          x <- getRigidBodyStateAttribute
+          pure (RigidBodyStateAttributeValue x)
+        StringAttributeType -> do
+          x <- getStringAttribute
+          pure (StringAttributeValue x)
+        TeamPaintAttributeType -> do
+          x <- getTeamPaintAttribute
+          pure (TeamPaintAttributeValue x)
+        UniqueIdAttributeType -> do
+          x <- getUniqueIdAttribute
+          pure (UniqueIdAttributeValue x)
+        WeldedInfoAttributeType -> do
+          x <- getWeldedInfoAttribute
+          pure (WeldedInfoAttributeValue x)
+    Nothing -> fail ("don't know how to get attribute value " ++ show name)
+
+attributeTypes :: Map.Map Text AttributeType
+attributeTypes = Map.mapKeys stringToText (Map.fromList rawAttributeTypes)
+
+putAttributeValue :: AttributeValue -> BinaryBit.BitPut ()
+putAttributeValue value =
+  case value of
+    BooleanAttributeValue x -> putBooleanAttribute x
+    ByteAttributeValue x -> putByteAttribute x
+    CamSettingsAttributeValue x -> putCamSettingsAttribute x
+    ClubColorsAttributeValue x -> putClubColorsAttribute x
+    DemolishAttributeValue x -> putDemolishAttribute x
+    EnumAttributeValue x -> putEnumAttribute x
+    ExplosionAttributeValue x -> putExplosionAttribute x
+    FlaggedIntAttributeValue x -> putFlaggedIntAttribute x
+    FloatAttributeValue x -> putFloatAttribute x
+    GameModeAttributeValue x -> putGameModeAttribute x
+    IntAttributeValue x -> putIntAttribute x
+    LoadoutAttributeValue x -> putLoadoutAttribute x
+    LoadoutOnlineAttributeValue x -> putLoadoutOnlineAttribute x
+    LoadoutsAttributeValue x -> putLoadoutsAttribute x
+    LoadoutsOnlineAttributeValue x -> putLoadoutsOnlineAttribute x
+    LocationAttributeValue x -> putLocationAttribute x
+    MusicStingerAttributeValue x -> putMusicStingerAttribute x
+    PartyLeaderAttributeValue x -> putPartyLeaderAttribute x
+    PickupAttributeValue x -> putPickupAttribute x
+    PrivateMatchSettingsAttributeValue x -> putPrivateMatchSettingsAttribute x
+    QWordAttributeValue x -> putQWordAttribute x
+    ReservationAttributeValue x -> putReservationAttribute x
+    RigidBodyStateAttributeValue x -> putRigidBodyStateAttribute x
+    StringAttributeValue x -> putStringAttribute x
+    TeamPaintAttributeValue x -> putTeamPaintAttribute x
+    UniqueIdAttributeValue x -> putUniqueIdAttribute x
+    WeldedInfoAttributeValue x -> putWeldedInfoAttribute x
diff --git a/source/library/Rattletrap/Cache.hs b/source/library/Rattletrap/Cache.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Cache.hs
@@ -0,0 +1,28 @@
+module Rattletrap.Cache where
+
+import Rattletrap.AttributeMapping
+import Rattletrap.Primitive
+
+import qualified Data.Binary as Binary
+
+data Cache = Cache
+  { cacheClassId :: Word32
+  , cacheParentCacheId :: Word32
+  , cacheCacheId :: Word32
+  , cacheAttributeMappings :: List AttributeMapping
+  } deriving (Eq, Ord, Show)
+
+getCache :: Binary.Get Cache
+getCache = do
+  classId <- getWord32
+  parentCacheId <- getWord32
+  cacheId <- getWord32
+  attributeMappings <- getList getAttributeMapping
+  pure (Cache classId parentCacheId cacheId attributeMappings)
+
+putCache :: Cache -> Binary.Put
+putCache cache = do
+  putWord32 (cacheClassId cache)
+  putWord32 (cacheParentCacheId cache)
+  putWord32 (cacheCacheId cache)
+  putList putAttributeMapping (cacheAttributeMappings cache)
diff --git a/source/library/Rattletrap/ClassAttributeMap.hs b/source/library/Rattletrap/ClassAttributeMap.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/ClassAttributeMap.hs
@@ -0,0 +1,292 @@
+module Rattletrap.ClassAttributeMap where
+
+import Rattletrap.ActorMap
+import Rattletrap.AttributeMapping
+import Rattletrap.Cache
+import Rattletrap.ClassMapping
+import Rattletrap.Data
+import Rattletrap.Primitive
+
+import qualified Data.Bimap as Bimap
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.List as List
+import qualified Data.Map.Strict as Map
+import qualified Data.Maybe as Maybe
+import qualified Data.Set as Set
+import qualified Data.Text as Text
+
+-- | This data structure holds all the information about classes, objects, and
+-- attributes in the replay. The class hierarchy is not fixed; it is encoded
+-- in the 'Rattletrap.Content.Content'. Similarly, the attributes that belong
+-- to each class are not fixed either. Converting the raw data into a usable
+-- structure is tedious; see 'makeClassAttributeMap'.
+data ClassAttributeMap = ClassAttributeMap
+  { classAttributeMapObjectMap :: Map.Map Word32 Text
+  -- ^ A map from object IDs to their names.
+  , classAttributeMapObjectClassMap :: Map.Map Word32 Word32
+  -- ^ A map from object IDs to their class IDs.
+  , classAttributeMapValue :: Map.Map Word32 (Map.Map Word32 Word32)
+  -- ^ A map from class IDs to a map from attribute stream IDs to attribute
+  -- IDs.
+  , classAttributeMapNameMap :: IntMap.IntMap Text
+  } deriving (Eq, Show)
+
+-- | Makes a 'ClassAttributeMap' given the necessary fields from the
+-- 'Rattletrap.Content.Content'.
+makeClassAttributeMap
+  :: List Text
+  -- ^ From 'Rattletrap.Content.contentObjects'.
+  -> List ClassMapping
+  -- ^ From 'Rattletrap.Content.contentClassMappings'.
+  -> List Cache
+  -- ^ From 'Rattletrap.Content.contentCaches'.
+  -> List Text
+  -- ^ From 'Rattletrap.Content.contentNames'.
+  -> ClassAttributeMap
+makeClassAttributeMap objects classMappings caches names =
+  let objectMap = makeObjectMap objects
+      classMap = makeClassMap classMappings
+      objectClassMap = makeObjectClassMap objectMap classMap
+      classCache = makeClassCache classMap caches
+      attributeMap = makeAttributeMap caches
+      classIds = map (\(_, classId, _, _) -> classId) classCache
+      parentMap = makeParentMap classCache
+      value =
+        Map.fromList
+          (map
+             (\classId ->
+                let ownAttributes =
+                      Maybe.fromMaybe
+                        Map.empty
+                        (Map.lookup classId attributeMap)
+                    parentsAttributes =
+                      case Map.lookup classId parentMap of
+                        Nothing -> []
+                        Just parentClassIds ->
+                          map
+                            (\parentClassId ->
+                               Maybe.fromMaybe
+                                 Map.empty
+                                 (Map.lookup parentClassId attributeMap))
+                            parentClassIds
+                    attributes = ownAttributes : parentsAttributes
+                in (classId, Map.fromList (concatMap Map.toList attributes)))
+             classIds)
+      nameMap = makeNameMap names
+  in ClassAttributeMap objectMap objectClassMap value nameMap
+
+makeNameMap :: List Text -> IntMap.IntMap Text
+makeNameMap names = IntMap.fromDistinctAscList (zip [0 ..] (listValue names))
+
+getName :: IntMap.IntMap Text -> Word32 -> Maybe Text
+getName nameMap nameIndex =
+  IntMap.lookup (fromIntegral (word32Value nameIndex)) nameMap
+
+makeObjectClassMap :: Map.Map Word32 Text
+                   -> Bimap.Bimap Word32 Text
+                   -> Map.Map Word32 Word32
+makeObjectClassMap objectMap classMap = do
+  let objectIds = Map.keys objectMap
+  let classIds = map (getClassId objectMap classMap) objectIds
+  let rawPairs = zip objectIds classIds
+  let pairs =
+        Maybe.mapMaybe
+          (\(objectId, maybeClassId) ->
+             case maybeClassId of
+               Nothing -> Nothing
+               Just classId -> Just (objectId, classId))
+          rawPairs
+  Map.fromList pairs
+
+getClassId :: Map.Map Word32 Text
+           -> Bimap.Bimap Word32 Text
+           -> Word32
+           -> Maybe Word32
+getClassId objectMap classMap objectId = do
+  objectName <- getObjectName objectMap objectId
+  className <- getClassName objectName
+  Bimap.lookupR className classMap
+
+makeClassCache :: Bimap.Bimap Word32 Text
+               -> List Cache
+               -> [(Maybe Text, Word32, Word32, Word32)]
+makeClassCache classMap caches =
+  map
+    (\cache ->
+       let classId = cacheClassId cache
+       in ( Bimap.lookup classId classMap
+          , classId
+          , cacheCacheId cache
+          , cacheParentCacheId cache))
+    (listValue caches)
+
+makeClassMap :: List ClassMapping -> Bimap.Bimap Word32 Text
+makeClassMap classMappings =
+  Bimap.fromList
+    (map
+       (\classMapping ->
+          (classMappingStreamId classMapping, classMappingName classMapping))
+       (listValue classMappings))
+
+makeAttributeMap :: List Cache -> Map.Map Word32 (Map.Map Word32 Word32)
+makeAttributeMap caches =
+  Map.fromList
+    (map
+       (\cache ->
+          ( cacheClassId cache
+          , Map.fromList
+              (map
+                 (\attributeMapping ->
+                    ( attributeMappingStreamId attributeMapping
+                    , attributeMappingObjectId attributeMapping))
+                 (listValue (cacheAttributeMappings cache)))))
+       (listValue caches))
+
+makeShallowParentMap :: [(Maybe Text, Word32, Word32, Word32)]
+                     -> Map.Map Word32 Word32
+makeShallowParentMap classCache =
+  Map.fromList
+    (Maybe.mapMaybe
+       (\xs ->
+          case xs of
+            [] -> Nothing
+            (maybeClassName, classId, _, parentCacheId):rest -> do
+              parentClassId <- getParentClass maybeClassName parentCacheId rest
+              pure (classId, parentClassId))
+       (List.tails (reverse classCache)))
+
+makeParentMap :: [(Maybe Text, Word32, Word32, Word32)]
+              -> Map.Map Word32 [Word32]
+makeParentMap classCache =
+  let shallowParentMap = makeShallowParentMap classCache
+  in Map.mapWithKey
+       (\classId _ -> getParentClasses shallowParentMap classId)
+       shallowParentMap
+
+getParentClasses :: Map.Map Word32 Word32 -> Word32 -> [Word32]
+getParentClasses shallowParentMap classId =
+  case Map.lookup classId shallowParentMap of
+    Nothing -> []
+    Just parentClassId ->
+      parentClassId : getParentClasses shallowParentMap parentClassId
+
+getParentClass :: Maybe Text
+               -> Word32
+               -> [(Maybe Text, Word32, Word32, Word32)]
+               -> Maybe Word32
+getParentClass maybeClassName parentCacheId xs =
+  case maybeClassName of
+    Nothing -> getParentClassById parentCacheId xs
+    Just className -> getParentClassByName className parentCacheId xs
+
+getParentClassById :: Word32
+                   -> [(Maybe Text, Word32, Word32, Word32)]
+                   -> Maybe Word32
+getParentClassById parentCacheId xs =
+  case dropWhile (\(_, _, cacheId, _) -> cacheId /= parentCacheId) xs of
+    [] ->
+      if parentCacheId == Word32 0
+        then Nothing
+        else getParentClassById (Word32 (word32Value parentCacheId - 1)) xs
+    (_, parentClassId, _, _):_ -> Just parentClassId
+
+getParentClassByName :: Text
+                     -> Word32
+                     -> [(Maybe Text, Word32, Word32, Word32)]
+                     -> Maybe Word32
+getParentClassByName className parentCacheId xs =
+  case Map.lookup className parentClasses of
+    Nothing -> getParentClassById parentCacheId xs
+    Just parentClassName ->
+      Maybe.maybe
+        (getParentClassById parentCacheId xs)
+        Just
+        (Maybe.listToMaybe
+           (map
+              (\(_, parentClassId, _, _) -> parentClassId)
+              (filter
+                 (\(_, _, cacheId, _) -> cacheId == parentCacheId)
+                 (filter
+                    (\(maybeClassName, _, _, _) ->
+                       maybeClassName == Just parentClassName)
+                    xs))))
+
+parentClasses :: Map.Map Text Text
+parentClasses =
+  Map.map
+    stringToText
+    (Map.mapKeys stringToText (Map.fromList rawParentClasses))
+
+makeObjectMap :: List Text -> Map.Map Word32 Text
+makeObjectMap objects =
+  Map.fromAscList (zip (map Word32 [0 ..]) (listValue objects))
+
+getObjectName :: Map.Map Word32 Text -> Word32 -> Maybe Text
+getObjectName objectMap objectId = Map.lookup objectId objectMap
+
+getClassName :: Text -> Maybe Text
+getClassName rawObjectName =
+  Map.lookup (normalizeObjectName rawObjectName) objectClasses
+
+normalizeObjectName :: Text -> Text
+normalizeObjectName objectName =
+  let name = textValue objectName
+      crowdActor = Text.pack "TheWorld:PersistentLevel.CrowdActor_TA"
+      crowdManager = Text.pack "TheWorld:PersistentLevel.CrowdManager_TA"
+      boostPickup = Text.pack "TheWorld:PersistentLevel.VehiclePickup_Boost_TA"
+      mapScoreboard = Text.pack "TheWorld:PersistentLevel.InMapScoreboard_TA"
+  in if Text.isInfixOf crowdActor name
+       then Text crowdActor
+       else if Text.isInfixOf crowdManager name
+              then Text crowdManager
+              else if Text.isInfixOf boostPickup name
+                     then Text boostPickup
+                     else if Text.isInfixOf mapScoreboard name
+                            then Text mapScoreboard
+                            else objectName
+
+objectClasses :: Map.Map Text Text
+objectClasses =
+  Map.map
+    stringToText
+    (Map.mapKeys stringToText (Map.fromList rawObjectClasses))
+
+classHasLocation :: Text -> Bool
+classHasLocation className = Set.member className classesWithLocation
+
+classesWithLocation :: Set.Set Text
+classesWithLocation = Set.fromList (map stringToText rawClassesWithLocation)
+
+classHasRotation :: Text -> Bool
+classHasRotation className = Set.member className classesWithRotation
+
+classesWithRotation :: Set.Set Text
+classesWithRotation = Set.fromList (map stringToText rawClassesWithRotation)
+
+getAttributeIdLimit :: Map.Map Word32 Word32 -> Maybe Word
+getAttributeIdLimit attributeMap = do
+  ((streamId, _), _) <- Map.maxViewWithKey attributeMap
+  let limit = fromIntegral (word32Value streamId)
+  pure limit
+
+getAttributeName :: ClassAttributeMap
+                 -> Map.Map Word32 Word32
+                 -> CompressedWord
+                 -> Maybe Text
+getAttributeName classAttributeMap attributeMap streamId = do
+  let key = Word32 (fromIntegral (compressedWordValue streamId))
+  attributeId <- Map.lookup key attributeMap
+  let objectMap = classAttributeMapObjectMap classAttributeMap
+  Map.lookup attributeId objectMap
+
+getAttributeMap
+  :: ClassAttributeMap
+  -> ActorMap
+  -> CompressedWord
+  -> Maybe (Map.Map Word32 Word32)
+getAttributeMap classAttributeMap actorMap actorId = do
+  objectId <- Map.lookup actorId actorMap
+  let objectClassMap = classAttributeMapObjectClassMap classAttributeMap
+  classId <- Map.lookup objectId objectClassMap
+  let value = classAttributeMapValue classAttributeMap
+  Map.lookup classId value
diff --git a/source/library/Rattletrap/ClassMapping.hs b/source/library/Rattletrap/ClassMapping.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/ClassMapping.hs
@@ -0,0 +1,21 @@
+module Rattletrap.ClassMapping where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary as Binary
+
+data ClassMapping = ClassMapping
+  { classMappingName :: Text
+  , classMappingStreamId :: Word32
+  } deriving (Eq, Ord, Show)
+
+getClassMapping :: Binary.Get ClassMapping
+getClassMapping = do
+  name <- getText
+  streamId <- getWord32
+  pure (ClassMapping name streamId)
+
+putClassMapping :: ClassMapping -> Binary.Put
+putClassMapping classMapping = do
+  putText (classMappingName classMapping)
+  putWord32 (classMappingStreamId classMapping)
diff --git a/source/library/Rattletrap/Content.hs b/source/library/Rattletrap/Content.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Content.hs
@@ -0,0 +1,121 @@
+module Rattletrap.Content where
+
+import Rattletrap.ActorMap
+import Rattletrap.Cache
+import Rattletrap.ClassAttributeMap
+import Rattletrap.ClassMapping
+import Rattletrap.Frame
+import Rattletrap.KeyFrame
+import Rattletrap.Mark
+import Rattletrap.Message
+import Rattletrap.Primitive
+import Rattletrap.Utility
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+
+-- | Contains low-level game data about a 'Rattletrap.Replay.Replay'.
+data Content = Content
+  { contentLevels :: List Text
+  -- ^ This typically only has one element, like @stadium_oob_audio_map@.
+  , contentKeyFrames :: List KeyFrame
+  -- ^ A list of which frames are key frames. Although they aren't necessary
+  -- for replay, key frames are frames that replicate every actor. They
+  -- typically happen once every 10 seconds.
+  , contentStreamSize :: Word32
+  -- ^ The size of the stream in bytes. This is only really necessary because
+  -- the stream has some arbitrary amount of padding at the end.
+  , contentFrames :: [Frame]
+  -- ^ The actual game data. This is where all the interesting information is.
+  , contentMessages :: List Message
+  -- ^ Debugging messages. In newer replays, this is always empty.
+  , contentMarks :: List Mark
+  -- ^ Tick marks shown on the scrubber when watching a replay.
+  , contentPackages :: List Text
+  -- ^ A list of @.upk@ files to load, like
+  -- @..\\..\\TAGame\\CookedPCConsole\\Stadium_P.upk@.
+  , contentObjects :: List Text
+  -- ^ Objects in the stream. Used for the
+  -- 'Rattletrap.ClassAttributeMap.ClassAttributeMap'.
+  , contentNames :: List Text
+  -- ^ It's not clear what these are used for. This list is usually not empty,
+  -- but appears unused otherwise.
+  , contentClassMappings :: List ClassMapping
+  -- ^ A mapping between classes and their ID in the stream. Used for the
+  -- 'Rattletrap.ClassAttributeMap.ClassAttributeMap'.
+  , contentCaches :: List Cache
+  -- ^ A list of classes along with their parent classes and attributes. Used
+  -- for the 'Rattletrap.ClassAttributeMap.ClassAttributeMap'.
+  } deriving (Eq, Ord, Show)
+
+getContent
+  :: (Int, Int)
+  -- ^ Major and minor version numbers, usually from
+  -- 'Rattletrap.Header.getVersion'.
+  -> Int
+  -- ^ The number of frames in the stream, usually from
+  -- 'Rattletrap.Header.getNumFrames'.
+  -> Word
+  -- ^ The maximum number of channels in the stream, usually from
+  -- 'Rattletrap.Header.getMaxChannels'.
+  -> Binary.Get Content
+getContent version numFrames maxChannels = do
+  levels <- getList getText
+  keyFrames <- getList getKeyFrame
+  streamSize <- getWord32
+  stream <- Binary.getLazyByteString (fromIntegral (word32Value streamSize))
+  messages <- getList getMessage
+  marks <- getList getMark
+  packages <- getList getText
+  objects <- getList getText
+  names <- getList getText
+  classMappings <- getList getClassMapping
+  caches <- getList getCache
+  let classAttributeMap =
+        makeClassAttributeMap objects classMappings caches names
+  let frames =
+        Binary.runGet
+          (BinaryBit.runBitGet
+             (do (theFrames, _) <-
+                   getFrames
+                     version
+                     numFrames
+                     maxChannels
+                     classAttributeMap
+                     makeActorMap
+                 pure theFrames))
+          (reverseBytes stream)
+  pure
+    (Content
+       levels
+       keyFrames
+       streamSize
+       frames
+       messages
+       marks
+       packages
+       objects
+       names
+       classMappings
+       caches)
+
+putContent :: Content -> Binary.Put
+putContent content = do
+  putList putText (contentLevels content)
+  putList putKeyFrame (contentKeyFrames content)
+  let streamSize = contentStreamSize content
+  putWord32 streamSize
+  let stream =
+        Binary.runPut (BinaryBit.runBitPut (putFrames (contentFrames content)))
+  Binary.putLazyByteString
+    (reverseBytes (padBytes (word32Value streamSize) stream))
+  putList putMessage (contentMessages content)
+  putList putMark (contentMarks content)
+  putList putText (contentPackages content)
+  putList putText (contentObjects content)
+  putList putText (contentNames content)
+  putList putClassMapping (contentClassMappings content)
+  putList putCache (contentCaches content)
diff --git a/source/library/Rattletrap/Crc.hs b/source/library/Rattletrap/Crc.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Crc.hs
@@ -0,0 +1,46 @@
+module Rattletrap.Crc where
+
+import Rattletrap.Data
+
+import qualified Data.Bits as Bits
+import qualified Data.ByteString.Lazy as ByteString
+import qualified Data.Vector.Unboxed as Vector
+import qualified Data.Word as Word
+
+-- | Computes the CRC32 of some bytes. This is done to ensure that the bytes
+-- are valid before trying to parse them.
+--
+-- @
+-- getCrc32 ('Data.ByteString.Lazy.pack' [0x00])
+-- @
+--
+-- This CRC uses an initial value of @0xefcbf201@ and a polynomial of
+-- @0x04c11db7@.
+getCrc32 :: ByteString.ByteString -> Word.Word32
+getCrc32 bytes = do
+  let update = crc32Update crc32Table
+  let initial = Bits.complement crc32Initial
+  let crc = ByteString.foldl update initial bytes
+  Bits.complement crc
+
+crc32Update :: Vector.Vector Word.Word32
+            -> Word.Word32
+            -> Word.Word8
+            -> Word.Word32
+crc32Update table crc byte = do
+  let toWord8 =
+        fromIntegral :: (Integral a) =>
+                          a -> Word.Word8
+  let toInt =
+        fromIntegral :: (Integral a) =>
+                          a -> Int
+  let index = toInt (Bits.xor byte (toWord8 (Bits.shiftR crc 24)))
+  let left = Vector.unsafeIndex table index
+  let right = Bits.shiftL crc 8
+  Bits.xor left right
+
+crc32Initial :: Word.Word32
+crc32Initial = 0xefcbf201
+
+crc32Table :: Vector.Vector Word.Word32
+crc32Table = Vector.fromList rawCrc32Table
diff --git a/source/library/Rattletrap/Data.hs b/source/library/Rattletrap/Data.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Data.hs
@@ -0,0 +1,351 @@
+module Rattletrap.Data where
+
+import Rattletrap.AttributeType
+
+rawParentClasses :: [(String, String)]
+rawParentClasses =
+  [ ("Engine.Actor", "Core.Object")
+  , ("Engine.GameReplicationInfo", "Engine.ReplicationInfo")
+  , ("Engine.Info", "Engine.Actor")
+  , ("Engine.Pawn", "Engine.Actor")
+  , ("Engine.PlayerReplicationInfo", "Engine.ReplicationInfo")
+  , ("Engine.ReplicationInfo", "Engine.Info")
+  , ("Engine.TeamInfo", "Engine.ReplicationInfo")
+  , ("ProjectX.GRI_X", "Engine.GameReplicationInfo")
+  , ("ProjectX.PRI_X", "Engine.PlayerReplicationInfo")
+  , ("ProjectX.Pawn_X", "Engine.Pawn")
+  , ("TAGame.Ball_TA", "TAGame.RBActor_TA")
+  , ("TAGame.CameraSettingsActor_TA", "Engine.ReplicationInfo")
+  , ("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.Car_Season_TA", "TAGame.PRI_TA")
+  , ("TAGame.Car_TA", "TAGame.Vehicle_TA")
+  , ("TAGame.CrowdActor_TA", "Engine.ReplicationInfo")
+  , ("TAGame.CrowdManager_TA", "Engine.ReplicationInfo")
+  , ("TAGame.GRI_TA", "ProjectX.GRI_X")
+  , ("TAGame.GameEvent_Season_TA", "TAGame.GameEvent_Soccar_TA")
+  , ("TAGame.GameEvent_SoccarPrivate_TA", "TAGame.GameEvent_Soccar_TA")
+  , ("TAGame.GameEvent_SoccarSplitscreen_TA", "TAGame.GameEvent_SoccarPrivate_TA")
+  , ("TAGame.GameEvent_Soccar_TA", "TAGame.GameEvent_Team_TA")
+  , ("TAGame.GameEvent_TA", "Engine.ReplicationInfo")
+  , ("TAGame.GameEvent_Team_TA", "TAGame.GameEvent_TA")
+  , ("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.VehiclePickup_Boost_TA", "TAGame.VehiclePickup_TA")
+  , ("TAGame.VehiclePickup_TA", "Engine.ReplicationInfo")
+  , ("TAGame.Vehicle_TA", "TAGame.RBActor_TA")
+  ]
+
+rawClassesWithLocation :: [String]
+rawClassesWithLocation =
+  [ "TAGame.Ball_TA"
+  , "TAGame.CameraSettingsActor_TA"
+  , "TAGame.CarComponent_Boost_TA"
+  , "TAGame.CarComponent_Dodge_TA"
+  , "TAGame.CarComponent_DoubleJump_TA"
+  , "TAGame.CarComponent_FlipCar_TA"
+  , "TAGame.CarComponent_Jump_TA"
+  , "TAGame.Car_Season_TA"
+  , "TAGame.Car_TA"
+  , "TAGame.GRI_TA"
+  , "TAGame.GameEvent_Season_TA"
+  , "TAGame.GameEvent_SoccarPrivate_TA"
+  , "TAGame.GameEvent_SoccarSplitscreen_TA"
+  , "TAGame.GameEvent_Soccar_TA"
+  , "TAGame.PRI_TA"
+  , "TAGame.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_Hockey", "TAGame.GameEvent_Soccar_TA")
+  , ("Archetypes.GameEvent.GameEvent_HockeyPrivate", "TAGame.GameEvent_SoccarPrivate_TA")
+  , ("Archetypes.GameEvent.GameEvent_HockeySplitscreen", "TAGame.GameEvent_SoccarSplitscreen_TA")
+  , ("Archetypes.GameEvent.GameEvent_Items", "TAGame.GameEvent_Soccar_TA")
+  , ("Archetypes.GameEvent.GameEvent_Season", "TAGame.GameEvent_Season_TA")
+  , ("Archetypes.GameEvent.GameEvent_Season:CarArchetype", "TAGame.Car_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_Items.GameInfo.GameInfo_Items:GameReplicationInfoArchetype", "TAGame.GRI_TA")
+  , ("GameInfo_Season.GameInfo.GameInfo_Season:GameReplicationInfoArchetype", "TAGame.GRI_TA")
+  , ("GameInfo_Soccar.GameInfo.GameInfo_Soccar:GameReplicationInfoArchetype", "TAGame.GRI_TA")
+  , ("Gameinfo_Hockey.GameInfo.Gameinfo_Hockey:GameReplicationInfoArchetype", "TAGame.GRI_TA")
+  , ("TAGame.Default__CameraSettingsActor_TA", "TAGame.CameraSettingsActor_TA")
+  , ("TAGame.Default__PRI_TA", "TAGame.PRI_TA")
+  , ("TheWorld:PersistentLevel.CrowdActor_TA", "TAGame.CrowdActor_TA")
+  , ("TheWorld:PersistentLevel.CrowdManager_TA", "TAGame.CrowdManager_TA")
+  , ("TheWorld:PersistentLevel.InMapScoreboard_TA", "TAGame.InMapScoreboard_TA")
+  , ("TheWorld:PersistentLevel.VehiclePickup_Boost_TA", "TAGame.VehiclePickup_Boost_TA")
+  ]
+
+rawAttributeTypes :: [(String, AttributeType)]
+rawAttributeTypes =
+  [ ("Engine.Actor:DrawScale", FloatAttributeType)
+  , ("Engine.Actor:Role", EnumAttributeType)
+  , ("Engine.Actor:bBlockActors", BooleanAttributeType)
+  , ("Engine.Actor:bCollideActors", BooleanAttributeType)
+  , ("Engine.Actor:bHidden", BooleanAttributeType)
+  , ("Engine.GameReplicationInfo:GameClass", FlaggedIntAttributeType)
+  , ("Engine.GameReplicationInfo:ServerName", StringAttributeType)
+  , ("Engine.GameReplicationInfo:bMatchIsOver", BooleanAttributeType)
+  , ("Engine.Pawn:PlayerReplicationInfo", FlaggedIntAttributeType)
+  , ("Engine.PlayerReplicationInfo:Ping", ByteAttributeType)
+  , ("Engine.PlayerReplicationInfo:PlayerID", IntAttributeType)
+  , ("Engine.PlayerReplicationInfo:PlayerName", StringAttributeType)
+  , ("Engine.PlayerReplicationInfo:RemoteUserData", StringAttributeType)
+  , ("Engine.PlayerReplicationInfo:Score", IntAttributeType)
+  , ("Engine.PlayerReplicationInfo:Team", FlaggedIntAttributeType)
+  , ("Engine.PlayerReplicationInfo:UniqueId", UniqueIdAttributeType)
+  , ("Engine.PlayerReplicationInfo:bBot", BooleanAttributeType)
+  , ("Engine.PlayerReplicationInfo:bIsSpectator", BooleanAttributeType)
+  , ("Engine.PlayerReplicationInfo:bReadyToPlay", BooleanAttributeType)
+  , ("Engine.PlayerReplicationInfo:bWaitingPlayer", BooleanAttributeType)
+  , ("Engine.TeamInfo:Score", IntAttributeType)
+  , ("ProjectX.GRI_X:GameServerID", QWordAttributeType)
+  , ("ProjectX.GRI_X:ReplicatedGameMutatorIndex", IntAttributeType)
+  , ("ProjectX.GRI_X:ReplicatedGamePlaylist", IntAttributeType)
+  , ("ProjectX.GRI_X:Reservations", ReservationAttributeType)
+  , ("ProjectX.GRI_X:bGameStarted", BooleanAttributeType)
+  , ("TAGame.Ball_TA:GameEvent", FlaggedIntAttributeType)
+  , ("TAGame.Ball_TA:HitTeamNum", ByteAttributeType)
+  , ("TAGame.Ball_TA:ReplicatedAddedCarBounceScale", FloatAttributeType)
+  , ("TAGame.Ball_TA:ReplicatedBallMaxLinearSpeedScale", FloatAttributeType)
+  , ("TAGame.Ball_TA:ReplicatedBallScale", FloatAttributeType)
+  , ("TAGame.Ball_TA:ReplicatedExplosionData", ExplosionAttributeType)
+  , ("TAGame.Ball_TA:ReplicatedWorldBounceScale", FloatAttributeType)
+  , ("TAGame.CameraSettingsActor_TA:CameraPitch", ByteAttributeType)
+  , ("TAGame.CameraSettingsActor_TA:CameraYaw", ByteAttributeType)
+  , ("TAGame.CameraSettingsActor_TA:PRI", FlaggedIntAttributeType)
+  , ("TAGame.CameraSettingsActor_TA:ProfileSettings", CamSettingsAttributeType)
+  , ("TAGame.CameraSettingsActor_TA:bUsingBehindView", BooleanAttributeType)
+  , ("TAGame.CameraSettingsActor_TA:bUsingSecondaryCamera", BooleanAttributeType)
+  , ("TAGame.CarComponent_Boost_TA:BoostModifier", FloatAttributeType)
+  , ("TAGame.CarComponent_Boost_TA:RechargeDelay", FloatAttributeType)
+  , ("TAGame.CarComponent_Boost_TA:RechargeRate", FloatAttributeType)
+  , ("TAGame.CarComponent_Boost_TA:ReplicatedBoostAmount", ByteAttributeType)
+  , ("TAGame.CarComponent_Boost_TA:UnlimitedBoostRefCount", IntAttributeType)
+  , ("TAGame.CarComponent_Boost_TA:bNoBoost", BooleanAttributeType)
+  , ("TAGame.CarComponent_Boost_TA:bUnlimitedBoost", BooleanAttributeType)
+  , ("TAGame.CarComponent_Dodge_TA:DodgeTorque", LocationAttributeType)
+  , ("TAGame.CarComponent_FlipCar_TA:FlipCarTime", FloatAttributeType)
+  , ("TAGame.CarComponent_FlipCar_TA:bFlipRight", BooleanAttributeType)
+  , ("TAGame.CarComponent_TA:ReplicatedActive", ByteAttributeType)
+  , ("TAGame.CarComponent_TA:ReplicatedActivityTime", FloatAttributeType)
+  , ("TAGame.CarComponent_TA:Vehicle", FlaggedIntAttributeType)
+  , ("TAGame.Car_TA:AddedBallForceMultiplier", FloatAttributeType)
+  , ("TAGame.Car_TA:AddedCarForceMultiplier", FloatAttributeType)
+  , ("TAGame.Car_TA:AttachedPickup", FlaggedIntAttributeType)
+  , ("TAGame.Car_TA:ClubColors", ClubColorsAttributeType)
+  , ("TAGame.Car_TA:ReplicatedDemolish", DemolishAttributeType)
+  , ("TAGame.Car_TA:TeamPaint", TeamPaintAttributeType)
+  , ("TAGame.CrowdActor_TA:GameEvent", FlaggedIntAttributeType)
+  , ("TAGame.CrowdActor_TA:ModifiedNoise", FloatAttributeType)
+  , ("TAGame.CrowdActor_TA:ReplicatedCountDownNumber", IntAttributeType)
+  , ("TAGame.CrowdActor_TA:ReplicatedOneShotSound", FlaggedIntAttributeType)
+  , ("TAGame.CrowdActor_TA:ReplicatedRoundCountDownNumber", IntAttributeType)
+  , ("TAGame.CrowdManager_TA:GameEvent", FlaggedIntAttributeType)
+  , ("TAGame.CrowdManager_TA:ReplicatedGlobalOneShotSound", FlaggedIntAttributeType)
+  , ("TAGame.GRI_TA:NewDedicatedServerIP", StringAttributeType)
+  , ("TAGame.GameEvent_SoccarPrivate_TA:MatchSettings", PrivateMatchSettingsAttributeType)
+  , ("TAGame.GameEvent_Soccar_TA:GameTime", IntAttributeType)
+  , ("TAGame.GameEvent_Soccar_TA:ReplicatedMusicStinger", MusicStingerAttributeType)
+  , ("TAGame.GameEvent_Soccar_TA:ReplicatedScoredOnTeam", ByteAttributeType)
+  , ("TAGame.GameEvent_Soccar_TA:RoundNum", IntAttributeType)
+  , ("TAGame.GameEvent_Soccar_TA:SecondsRemaining", IntAttributeType)
+  , ("TAGame.GameEvent_Soccar_TA:SubRulesArchetype", FlaggedIntAttributeType)
+  , ("TAGame.GameEvent_Soccar_TA:bBallHasBeenHit", BooleanAttributeType)
+  , ("TAGame.GameEvent_Soccar_TA:bOverTime", BooleanAttributeType)
+  , ("TAGame.GameEvent_TA:BotSkill", IntAttributeType)
+  , ("TAGame.GameEvent_TA:GameMode", GameModeAttributeType)
+  , ("TAGame.GameEvent_TA:MatchTypeClass", FlaggedIntAttributeType)
+  , ("TAGame.GameEvent_TA:ReplicatedGameStateTimeRemaining", IntAttributeType)
+  , ("TAGame.GameEvent_TA:ReplicatedStateIndex", ByteAttributeType)
+  , ("TAGame.GameEvent_TA:ReplicatedStateName", IntAttributeType)
+  , ("TAGame.GameEvent_TA:bCanVoteToForfeit", BooleanAttributeType)
+  , ("TAGame.GameEvent_TA:bHasLeaveMatchPenalty", BooleanAttributeType)
+  , ("TAGame.GameEvent_Team_TA:bForfeit", BooleanAttributeType)
+  , ("TAGame.GameEvent_Team_TA:MaxTeamSize", IntAttributeType)
+  , ("TAGame.PRI_TA:CameraPitch", ByteAttributeType)
+  , ("TAGame.PRI_TA:CameraSettings", CamSettingsAttributeType)
+  , ("TAGame.PRI_TA:CameraYaw", ByteAttributeType)
+  , ("TAGame.PRI_TA:ClientLoadout", LoadoutAttributeType)
+  , ("TAGame.PRI_TA:ClientLoadoutOnline", LoadoutOnlineAttributeType)
+  , ("TAGame.PRI_TA:ClientLoadouts", LoadoutsAttributeType)
+  , ("TAGame.PRI_TA:ClientLoadoutsOnline", LoadoutsOnlineAttributeType)
+  , ("TAGame.PRI_TA:MatchAssists", IntAttributeType)
+  , ("TAGame.PRI_TA:MatchGoals", IntAttributeType)
+  , ("TAGame.PRI_TA:MatchSaves", IntAttributeType)
+  , ("TAGame.PRI_TA:MatchScore", IntAttributeType)
+  , ("TAGame.PRI_TA:MatchShots", IntAttributeType)
+  , ("TAGame.PRI_TA:PartyLeader", PartyLeaderAttributeType)
+  , ("TAGame.PRI_TA:PawnType", ByteAttributeType)
+  , ("TAGame.PRI_TA:PersistentCamera", FlaggedIntAttributeType)
+  , ("TAGame.PRI_TA:ReplicatedGameEvent", FlaggedIntAttributeType)
+  , ("TAGame.PRI_TA:Title", IntAttributeType)
+  , ("TAGame.PRI_TA:TotalXP", IntAttributeType)
+  , ("TAGame.PRI_TA:bIsInSplitScreen", BooleanAttributeType)
+  , ("TAGame.PRI_TA:bMatchMVP", BooleanAttributeType)
+  , ("TAGame.PRI_TA:bOnlineLoadoutSet", BooleanAttributeType)
+  , ("TAGame.PRI_TA:bOnlineLoadoutsSet", BooleanAttributeType)
+  , ("TAGame.PRI_TA:bReady", BooleanAttributeType)
+  , ("TAGame.PRI_TA:bUsingBehindView", BooleanAttributeType)
+  , ("TAGame.PRI_TA:bUsingSecondaryCamera", BooleanAttributeType)
+  , ("TAGame.RBActor_TA:ReplicatedRBState", RigidBodyStateAttributeType)
+  , ("TAGame.RBActor_TA:WeldedInfo", WeldedInfoAttributeType)
+  , ("TAGame.RBActor_TA:bFrozen", BooleanAttributeType)
+  , ("TAGame.RBActor_TA:bIgnoreSyncing", BooleanAttributeType)
+  , ("TAGame.RBActor_TA:bReplayActor", BooleanAttributeType)
+  , ("TAGame.SpecialPickup_BallFreeze_TA:RepOrigSpeed", FloatAttributeType)
+  , ("TAGame.SpecialPickup_BallVelcro_TA:AttachTime", FloatAttributeType)
+  , ("TAGame.SpecialPickup_BallVelcro_TA:BreakTime", FloatAttributeType)
+  , ("TAGame.SpecialPickup_BallVelcro_TA:bBroken", BooleanAttributeType)
+  , ("TAGame.SpecialPickup_BallVelcro_TA:bHit", BooleanAttributeType)
+  , ("TAGame.SpecialPickup_Targeted_TA:Targeted", FlaggedIntAttributeType)
+  , ("TAGame.Team_Soccar_TA:GameScore", IntAttributeType)
+  , ("TAGame.Team_TA:ClubColors", ClubColorsAttributeType)
+  , ("TAGame.Team_TA:CustomTeamName", StringAttributeType)
+  , ("TAGame.Team_TA:GameEvent", FlaggedIntAttributeType)
+  , ("TAGame.Team_TA:LogoData", FlaggedIntAttributeType)
+  , ("TAGame.VehiclePickup_TA:ReplicatedPickupData", PickupAttributeType)
+  , ("TAGame.Vehicle_TA:ReplicatedSteer", ByteAttributeType)
+  , ("TAGame.Vehicle_TA:ReplicatedThrottle", ByteAttributeType)
+  , ("TAGame.Vehicle_TA:bDriving", BooleanAttributeType)
+  , ("TAGame.Vehicle_TA:bReplicatedHandbrake", BooleanAttributeType)
+  ]
+
+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/source/library/Rattletrap/Frame.hs b/source/library/Rattletrap/Frame.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Frame.hs
@@ -0,0 +1,62 @@
+module Rattletrap.Frame where
+
+import Rattletrap.ActorMap
+import Rattletrap.ClassAttributeMap
+import Rattletrap.Primitive
+import Rattletrap.Replication
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data Frame = Frame
+  { frameTime :: Float32
+  -- ^ Time in seconds since the beginning of the match.
+  , frameDelta :: Float32
+  -- ^ Time in seconds since the last frame. Usually about 0.03 since there
+  -- are 30 frames per second.
+  , frameReplications :: [Replication]
+  } deriving (Eq, Ord, Show)
+
+getFrames
+  :: (Int, Int)
+  -> Int
+  -> Word
+  -> ClassAttributeMap
+  -> ActorMap
+  -> BinaryBit.BitGet ([Frame], ActorMap)
+getFrames version numFrames maxChannels classAttributeMap actorMap =
+  if numFrames <= 0
+    then pure ([], actorMap)
+    else do
+      (frame, newActorMap) <-
+        getFrame version maxChannels classAttributeMap actorMap
+      (frames, newerActorMap) <-
+        getFrames
+          version
+          (numFrames - 1)
+          maxChannels
+          classAttributeMap
+          newActorMap
+      pure (frame : frames, newerActorMap)
+
+putFrames :: [Frame] -> BinaryBit.BitPut ()
+putFrames = mapM_ putFrame
+
+getFrame
+  :: (Int, Int)
+  -> Word
+  -> ClassAttributeMap
+  -> ActorMap
+  -> BinaryBit.BitGet (Frame, ActorMap)
+getFrame version maxChannels classAttributeMap actorMap = do
+  time <- getFloat32Bits
+  delta <- getFloat32Bits
+  (replications, newActorMap) <-
+    getReplications version maxChannels classAttributeMap actorMap
+  pure (Frame time delta replications, newActorMap)
+
+putFrame :: Frame -> BinaryBit.BitPut ()
+putFrame frame = do
+  putFloat32Bits (frameTime frame)
+  putFloat32Bits (frameDelta frame)
+  putReplications (frameReplications frame)
diff --git a/source/library/Rattletrap/Header.hs b/source/library/Rattletrap/Header.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Header.hs
@@ -0,0 +1,100 @@
+module Rattletrap.Header where
+
+import Rattletrap.Primitive
+import Rattletrap.Property
+import Rattletrap.PropertyValue
+
+import qualified Data.Binary as Binary
+import qualified Data.Map as Map
+
+-- | Contains high-level metadata about a 'Rattletrap.Replay.Replay'.
+data Header = Header
+  { headerEngineVersion :: Word32
+  -- ^ The "major" version number.
+  , headerLicenseeVersion :: Word32
+  -- ^ The "minor" version number.
+  , headerLabel :: Text
+  -- ^ Always @TAGame.Replay_Soccar_TA@.
+  , headerProperties :: Dictionary Property
+  -- ^ These properties determine how a replay will look in the list of
+  -- replays in-game. One element is required for the replay to show up:
+  --
+  -- - MapName: This is a 'Rattletrap.PropertyValue.NameProperty' with a
+  --   case-insensitive map identifier, like @Stadium_P@.
+  --
+  -- There are many other properties that affect how the replay looks in the
+  -- list of replays.
+  --
+  -- - Date: A 'Rattletrap.PropertyValue.StrProperty' with the format
+  --   @YYYY-mm-dd:HH-MM@. Dates are not validated, but the month must be
+  --   between 1 and 12 to show up. The hour is shown modulo 12 with AM or PM.
+  -- - MatchType: A 'Rattletrap.PropertyValue.NameProperty'. If this is not
+  --   one of the expected values, nothing will be shown next to the replay's
+  --   map. The expected values are: @Online@, @Offline@, @Private@, and
+  --   @Season@.
+  -- - NumFrames: This 'Rattletrap.PropertyValue.IntProperty' is used to
+  --   calculate the length of the match. There are 30 frames per second,
+  --   a typical 5-minute match has about 9,000 frames.
+  -- - PrimaryPlayerTeam: This is an 'Rattletrap.PropertyValue.IntProperty'.
+  --   It is either 0 (blue) or 1 (orange). Any other value is ignored. If
+  --   this would be 0, you don't have to set it at all.
+  -- - ReplayName: An optional 'Rattletrap.PropertyValue.StrProperty' with a
+  --   user-supplied name for the replay.
+  -- - Team0Score: The blue team's score as an
+  --   'Rattletrap.PropertyValue.IntProperty'. Can be omitted if the score is
+  --   0.
+  -- - Team1Score: The orange team's score as an
+  --   'Rattletrap.PropertyValue.IntProperty'. Can also be omitted if the
+  --   score is 0.
+  -- - TeamSize: An 'Rattletrap.PropertyValue.IntProperty' with the number of
+  --   players per team. This value is not validated, so you can put absurd
+  --   values like 99. To get an "unfair" team size like 1v4, you must set the
+  --   bUnfairBots 'Rattletrap.PropertyValue.BoolProperty' to @True@.
+  } deriving (Eq, Ord, Show)
+
+getHeader :: Binary.Get Header
+getHeader = do
+  engineVersion <- getWord32
+  licenseeVersion <- getWord32
+  label <- getText
+  properties <- getDictionary getProperty
+  pure (Header engineVersion licenseeVersion label properties)
+
+putHeader :: Header -> Binary.Put
+putHeader header = do
+  putWord32 (headerEngineVersion header)
+  putWord32 (headerLicenseeVersion header)
+  putText (headerLabel header)
+  putDictionary putProperty (headerProperties header)
+
+getVersion :: Header -> (Int, Int)
+getVersion header =
+  let major = getMajorVersion header
+      minor = getMinorVersion header
+  in (major, minor)
+
+getMajorVersion :: Header -> Int
+getMajorVersion header =
+  fromIntegral (word32Value (headerEngineVersion header))
+
+getMinorVersion :: Header -> Int
+getMinorVersion header =
+  fromIntegral (word32Value (headerLicenseeVersion header))
+
+getNumFrames :: Header -> Int
+getNumFrames header =
+  let key = textValue (stringToText "NumFrames")
+      properties = dictionaryValue (headerProperties header)
+  in case Map.lookup key properties of
+       Just (Property _ _ (IntProperty numFrames)) ->
+         fromIntegral (int32Value numFrames)
+       _ -> 0
+
+getMaxChannels :: Header -> Word
+getMaxChannels header =
+  let key = textValue (stringToText "MaxChannels")
+      properties = dictionaryValue (headerProperties header)
+  in case Map.lookup key properties of
+       Just (Property _ _ (IntProperty numFrames)) ->
+         fromIntegral (int32Value numFrames)
+       _ -> 1023
diff --git a/source/library/Rattletrap/Initialization.hs b/source/library/Rattletrap/Initialization.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Initialization.hs
@@ -0,0 +1,40 @@
+module Rattletrap.Initialization where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data Initialization = Initialization
+  { initializationLocation :: Maybe Vector
+  -- ^ Not every class has an initial location. See
+  -- 'Rattletrap.Data.rawClassesWithLocation'.
+  , initializationRotation :: Maybe Int8Vector
+  -- ^ Only classes with location can have rotation, but not every one does.
+  -- See 'Rattletrap.Data.rawClassesWithRotation'.
+  } deriving (Eq, Ord, Show)
+
+getInitialization :: Bool -> Bool -> BinaryBit.BitGet Initialization
+getInitialization hasLocation hasRotation = do
+  location <-
+    if hasLocation
+      then do
+        location <- getVector
+        pure (Just location)
+      else pure Nothing
+  rotation <-
+    if hasRotation
+      then do
+        rotation <- getInt8Vector
+        pure (Just rotation)
+      else pure Nothing
+  pure (Initialization location rotation)
+
+putInitialization :: Initialization -> BinaryBit.BitPut ()
+putInitialization initialization = do
+  case initializationLocation initialization of
+    Nothing -> pure ()
+    Just location -> putVector location
+  case initializationRotation initialization of
+    Nothing -> pure ()
+    Just rotation -> putInt8Vector rotation
diff --git a/source/library/Rattletrap/Json.hs b/source/library/Rattletrap/Json.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Json.hs
@@ -0,0 +1,105 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Rattletrap.Json where
+
+import Rattletrap.Attribute
+import Rattletrap.AttributeMapping
+import Rattletrap.AttributeValue
+import Rattletrap.Cache
+import Rattletrap.ClassMapping
+import Rattletrap.Content
+import Rattletrap.Frame
+import Rattletrap.Header
+import Rattletrap.Initialization
+import Rattletrap.KeyFrame
+import Rattletrap.Mark
+import Rattletrap.Message
+import Rattletrap.Primitive
+import Rattletrap.Property
+import Rattletrap.PropertyValue
+import Rattletrap.RemoteId
+import Rattletrap.Replay
+import Rattletrap.Replication
+import Rattletrap.ReplicationValue
+
+import qualified Control.Monad as Monad
+import qualified Data.Aeson.Casing as Casing
+import qualified Data.Aeson.TH as Aeson
+import qualified Language.Haskell.TH as TH
+
+$(let optionsFor name =
+        (Casing.aesonDrop (length (TH.nameBase name)) Casing.snakeCase)
+        { Aeson.constructorTagModifier = Casing.snakeCase
+        , Aeson.omitNothingFields = True
+        , Aeson.sumEncoding = Aeson.ObjectWithSingleField
+        , Aeson.unwrapUnaryRecords = True
+        }
+      deriveJSON declarations name = do
+        newDeclarations <- Aeson.deriveJSON (optionsFor name) name
+        pure (newDeclarations ++ declarations)
+      names =
+        [ ''Attribute
+        , ''AttributeMapping
+        , ''AttributeValue
+        , ''BooleanAttribute
+        , ''ByteAttribute
+        , ''Cache
+        , ''CamSettingsAttribute
+        , ''ClassMapping
+        , ''ClubColorsAttribute
+        , ''CompressedWord
+        , ''CompressedWordVector
+        , ''Content
+        , ''DemolishAttribute
+        , ''DestroyedReplication
+        , ''Dictionary
+        , ''EnumAttribute
+        , ''ExplosionAttribute
+        , ''FlaggedIntAttribute
+        , ''Float32
+        , ''FloatAttribute
+        , ''Frame
+        , ''GameModeAttribute
+        , ''Header
+        , ''Initialization
+        , ''Int32
+        , ''Int8
+        , ''Int8Vector
+        , ''IntAttribute
+        , ''KeyFrame
+        , ''List
+        , ''LoadoutAttribute
+        , ''LoadoutOnlineAttribute
+        , ''LoadoutsAttribute
+        , ''LoadoutsOnlineAttribute
+        , ''LocationAttribute
+        , ''Mark
+        , ''Message
+        , ''MusicStingerAttribute
+        , ''PartyLeaderAttribute
+        , ''PickupAttribute
+        , ''PrivateMatchSettingsAttribute
+        , ''Property
+        , ''PropertyValue
+        , ''QWordAttribute
+        , ''RemoteId
+        , ''Replay
+        , ''Replication
+        , ''ReplicationValue
+        , ''ReservationAttribute
+        , ''RigidBodyStateAttribute
+        , ''Section
+        , ''SpawnedReplication
+        , ''StringAttribute
+        , ''TeamPaintAttribute
+        , ''Text
+        , ''UniqueIdAttribute
+        , ''UpdatedReplication
+        , ''Vector
+        , ''WeldedInfoAttribute
+        , ''Word32
+        , ''Word64
+        , ''Word8
+        ]
+  in Monad.foldM deriveJSON [] names)
diff --git a/source/library/Rattletrap/KeyFrame.hs b/source/library/Rattletrap/KeyFrame.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/KeyFrame.hs
@@ -0,0 +1,27 @@
+module Rattletrap.KeyFrame where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary as Binary
+
+data KeyFrame = KeyFrame
+  { keyFrameTime :: Float32
+  -- ^ When this key frame occurs, in seconds.
+  , keyFrameFrame :: Word32
+  -- ^ The frame number of this key frame, starting from 0.
+  , keyFramePosition :: Word32
+  -- ^ The bit position of this key frame in the stream.
+  } deriving (Eq, Ord, Show)
+
+getKeyFrame :: Binary.Get KeyFrame
+getKeyFrame = do
+  time <- getFloat32
+  frame <- getWord32
+  position <- getWord32
+  pure (KeyFrame time frame position)
+
+putKeyFrame :: KeyFrame -> Binary.Put
+putKeyFrame keyFrame = do
+  putFloat32 (keyFrameTime keyFrame)
+  putWord32 (keyFrameFrame keyFrame)
+  putWord32 (keyFramePosition keyFrame)
diff --git a/source/library/Rattletrap/Main.hs b/source/library/Rattletrap/Main.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Main.hs
@@ -0,0 +1,58 @@
+module Rattletrap.Main where
+
+import Rattletrap.Json ()
+import Rattletrap.Replay
+import Rattletrap.Version
+
+import qualified Data.Aeson as Aeson
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as ByteString
+import qualified Data.Version as Version
+import qualified System.Environment as Environment
+
+-- | Gets command-line arguments and passes them to 'mainWithArgs'.
+main :: IO ()
+main = do
+  args <- Environment.getArgs
+  mainWithArgs args
+
+-- | Runs the main command-line interface. Can be used in one of three modes:
+--
+-- 1. @mainWithArgs ["version"]@: Prints out the version number and exits.
+--    Mostly useful for debugging.
+-- 2. @mainWithArgs ["decode"]@: Parses a raw replay into JSON. By default
+--    this reads from STDIN and writes to STDOUT. To read from a file, pass a
+--    second argument like @mainWithArgs ["decode", "input.replay"]@. To write
+--    to a file, pass a third argument like
+--    @mainWithArgs ["decode", "input.replay", "output.json"]@.
+-- 3. @mainWithArgs ["encode"]@: Generates a raw replay from JSON. The
+--    handling of input and output is the same as decoding.
+mainWithArgs :: [String] -> IO ()
+mainWithArgs args =
+  case args of
+    ["version"] -> putStrLn (Version.showVersion version)
+    "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 :: [FilePath]
+      -> IO (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)
diff --git a/source/library/Rattletrap/Mark.hs b/source/library/Rattletrap/Mark.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Mark.hs
@@ -0,0 +1,23 @@
+module Rattletrap.Mark where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary as Binary
+
+data Mark = Mark
+  { markValue :: Text
+  -- ^ Which type of mark this is, like @Team0Goal@.
+  , markFrame :: Word32
+  -- ^ Which frame this mark belongs to, starting from 0.
+  } deriving (Eq, Ord, Show)
+
+getMark :: Binary.Get Mark
+getMark = do
+  value <- getText
+  frame <- getWord32
+  pure (Mark value frame)
+
+putMark :: Mark -> Binary.Put
+putMark mark = do
+  putText (markValue mark)
+  putWord32 (markFrame mark)
diff --git a/source/library/Rattletrap/Message.hs b/source/library/Rattletrap/Message.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Message.hs
@@ -0,0 +1,27 @@
+module Rattletrap.Message where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary as Binary
+
+data Message = Message
+  { messageFrame :: Word32
+  -- ^ Which frame this message belongs to, starting from 0.
+  , messageName :: Text
+  -- ^ The primary player's name.
+  , messageValue :: Text
+  -- ^ The content of the message.
+  } deriving (Eq, Ord, Show)
+
+getMessage :: Binary.Get Message
+getMessage = do
+  frame <- getWord32
+  name <- getText
+  value <- getText
+  pure (Message frame name value)
+
+putMessage :: Message -> Binary.Put
+putMessage message = do
+  putWord32 (messageFrame message)
+  putText (messageName message)
+  putText (messageValue message)
diff --git a/source/library/Rattletrap/Primitive.hs b/source/library/Rattletrap/Primitive.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Primitive.hs
@@ -0,0 +1,31 @@
+module Rattletrap.Primitive
+  ( module Rattletrap.Primitive.CompressedWord
+  , module Rattletrap.Primitive.CompressedWordVector
+  , module Rattletrap.Primitive.Dictionary
+  , module Rattletrap.Primitive.Float32
+  , module Rattletrap.Primitive.Int8
+  , module Rattletrap.Primitive.Int8Vector
+  , module Rattletrap.Primitive.Int32
+  , module Rattletrap.Primitive.List
+  , module Rattletrap.Primitive.Section
+  , module Rattletrap.Primitive.Text
+  , module Rattletrap.Primitive.Vector
+  , module Rattletrap.Primitive.Word8
+  , module Rattletrap.Primitive.Word32
+  , module Rattletrap.Primitive.Word64
+  ) where
+
+import Rattletrap.Primitive.CompressedWord
+import Rattletrap.Primitive.CompressedWordVector
+import Rattletrap.Primitive.Dictionary
+import Rattletrap.Primitive.Float32
+import Rattletrap.Primitive.Int32
+import Rattletrap.Primitive.Int8
+import Rattletrap.Primitive.Int8Vector
+import Rattletrap.Primitive.List
+import Rattletrap.Primitive.Section
+import Rattletrap.Primitive.Text
+import Rattletrap.Primitive.Vector
+import Rattletrap.Primitive.Word32
+import Rattletrap.Primitive.Word64
+import Rattletrap.Primitive.Word8
diff --git a/source/library/Rattletrap/Primitive/CompressedWord.hs b/source/library/Rattletrap/Primitive/CompressedWord.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Primitive/CompressedWord.hs
@@ -0,0 +1,59 @@
+module Rattletrap.Primitive.CompressedWord where
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Bits as Bits
+
+data CompressedWord = CompressedWord
+  { compressedWordLimit :: Word
+  , compressedWordValue :: Word
+  } deriving (Eq, Ord, Show)
+
+getCompressedWord :: Word -> BinaryBit.BitGet CompressedWord
+getCompressedWord limit = do
+  value <- getCompressedWordStep limit (getMaxBits limit) 0 0
+  pure (CompressedWord limit value)
+
+putCompressedWord :: CompressedWord -> BinaryBit.BitPut ()
+putCompressedWord compressedWord = do
+  let limit = compressedWordLimit compressedWord
+  let value = compressedWordValue compressedWord
+  let maxBits = getMaxBits limit
+  let go position soFar =
+        if position < maxBits
+          then do
+            let x = Bits.shiftL 1 position
+            if maxBits > 1 && position == maxBits - 1 && soFar + x > limit
+              then pure ()
+              else do
+                let bit = Bits.testBit value position
+                BinaryBit.putBool bit
+                let delta =
+                      if bit
+                        then x
+                        else 0
+                go (position + 1) (soFar + delta)
+          else pure ()
+  go 0 0
+
+getMaxBits
+  :: (Integral a, Integral b)
+  => a -> b
+getMaxBits x = do
+  let n = max 1 (ceiling (logBase (2 :: Double) (fromIntegral (max 1 x))))
+  if x < 1024 && x == 2 ^ n
+    then n + 1
+    else n
+
+getCompressedWordStep :: Word -> Word -> Word -> Word -> BinaryBit.BitGet Word
+getCompressedWordStep limit maxBits position value = do
+  let x = Bits.shiftL 1 (fromIntegral position)
+  if position < maxBits && value + x <= limit
+    then do
+      bit <- BinaryBit.getBool
+      let newValue =
+            if bit
+              then value + x
+              else value
+      getCompressedWordStep limit maxBits (position + 1) newValue
+    else pure value
diff --git a/source/library/Rattletrap/Primitive/CompressedWordVector.hs b/source/library/Rattletrap/Primitive/CompressedWordVector.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Primitive/CompressedWordVector.hs
@@ -0,0 +1,26 @@
+module Rattletrap.Primitive.CompressedWordVector where
+
+import Rattletrap.Primitive.CompressedWord
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data CompressedWordVector = CompressedWordVector
+  { compressedWordVectorX :: CompressedWord
+  , compressedWordVectorY :: CompressedWord
+  , compressedWordVectorZ :: CompressedWord
+  } deriving (Eq, Ord, Show)
+
+getCompressedWordVector :: BinaryBit.BitGet CompressedWordVector
+getCompressedWordVector = do
+  let limit = 65536
+  x <- getCompressedWord limit
+  y <- getCompressedWord limit
+  z <- getCompressedWord limit
+  pure (CompressedWordVector x y z)
+
+putCompressedWordVector :: CompressedWordVector -> BinaryBit.BitPut ()
+putCompressedWordVector compressedWordVector = do
+  putCompressedWord (compressedWordVectorX compressedWordVector)
+  putCompressedWord (compressedWordVectorY compressedWordVector)
+  putCompressedWord (compressedWordVectorZ compressedWordVector)
diff --git a/source/library/Rattletrap/Primitive/Dictionary.hs b/source/library/Rattletrap/Primitive/Dictionary.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Primitive/Dictionary.hs
@@ -0,0 +1,59 @@
+module Rattletrap.Primitive.Dictionary where
+
+import Rattletrap.Primitive.Text
+
+import qualified Data.Binary as Binary
+import qualified Data.Map as Map
+import qualified Data.Text as Text
+
+data Dictionary a = Dictionary
+  { dictionaryKeys :: [Text]
+  -- ^ Objects in JSON aren't ordered, so the order of the keys must be stored
+  -- separately.
+  , dictionaryLastKey :: Text
+  -- ^ The last key is usually @None@ but sometimes contains extra null bytes.
+  , dictionaryValue :: Map.Map Text.Text a
+  -- ^ Be sure to update 'dictionaryKeys' if you add, change, or remove a key
+  -- in this map.
+  } deriving (Eq, Ord, Show)
+
+getDictionary :: Binary.Get a -> Binary.Get (Dictionary a)
+getDictionary getValue = do
+  (elements, lastKey) <- getElements getValue
+  let keys = map fst elements
+  let value = Map.mapKeys textValue (Map.fromList elements)
+  pure (Dictionary keys lastKey value)
+
+getElements :: Binary.Get a -> Binary.Get ([(Text, a)], Text)
+getElements getValue = do
+  (key, maybeValue) <- getElement getValue
+  case maybeValue of
+    Nothing -> pure ([], key)
+    Just value -> do
+      let element = (key, value)
+      (elements, lastKey) <- getElements getValue
+      pure (element : elements, lastKey)
+
+getElement :: Binary.Get a -> Binary.Get (Text, Maybe a)
+getElement getValue = do
+  key <- getText
+  if isNoneKey key
+    then pure (key, Nothing)
+    else do
+      value <- getValue
+      pure (key, Just value)
+
+isNoneKey :: Text -> Bool
+isNoneKey text = filter (/= '\x00') (textToString text) == "None"
+
+putDictionary :: (a -> Binary.Put) -> Dictionary a -> Binary.Put
+putDictionary putValue dictionary = do
+  let elements = dictionaryValue dictionary
+  mapM_
+    (\key -> do
+       putText key
+       case Map.lookup (textValue key) elements of
+         Nothing -> fail ("could not find key " ++ textToString key)
+         Just value -> putValue value)
+    (dictionaryKeys dictionary)
+  putText (dictionaryLastKey dictionary)
diff --git a/source/library/Rattletrap/Primitive/Float32.hs b/source/library/Rattletrap/Primitive/Float32.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Primitive/Float32.hs
@@ -0,0 +1,33 @@
+module Rattletrap.Primitive.Float32 where
+
+import Rattletrap.Utility
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.IEEE754 as IEEE754
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as ByteString
+
+newtype Float32 = Float32
+  { float32Value :: Float
+  } deriving (Eq, Ord, Show)
+
+getFloat32 :: Binary.Get Float32
+getFloat32 = do
+  float32 <- IEEE754.getFloat32le
+  pure (Float32 float32)
+
+putFloat32 :: Float32 -> Binary.Put
+putFloat32 float32 = IEEE754.putFloat32le (float32Value float32)
+
+getFloat32Bits :: BinaryBit.BitGet Float32
+getFloat32Bits = do
+  bytes <- BinaryBit.getLazyByteString 4
+  pure (Binary.runGet getFloat32 (reverseBytes bytes))
+
+putFloat32Bits :: Float32 -> BinaryBit.BitPut ()
+putFloat32Bits float32 = do
+  let bytes = Binary.runPut (putFloat32 float32)
+  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/source/library/Rattletrap/Primitive/Int32.hs b/source/library/Rattletrap/Primitive/Int32.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Primitive/Int32.hs
@@ -0,0 +1,33 @@
+module Rattletrap.Primitive.Int32 where
+
+import Rattletrap.Utility
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as ByteString
+import qualified Data.Int as Int
+
+newtype Int32 = Int32
+  { int32Value :: Int.Int32
+  } deriving (Eq, Ord, Show)
+
+getInt32 :: Binary.Get Int32
+getInt32 = do
+  int32 <- Binary.getInt32le
+  pure (Int32 int32)
+
+putInt32 :: Int32 -> Binary.Put
+putInt32 int32 = Binary.putInt32le (int32Value int32)
+
+getInt32Bits :: BinaryBit.BitGet Int32
+getInt32Bits = do
+  bytes <- BinaryBit.getLazyByteString 4
+  pure (Binary.runGet getInt32 (reverseBytes bytes))
+
+putInt32Bits :: Int32 -> BinaryBit.BitPut ()
+putInt32Bits int32 = do
+  let bytes = Binary.runPut (putInt32 int32)
+  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/source/library/Rattletrap/Primitive/Int8.hs b/source/library/Rattletrap/Primitive/Int8.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Primitive/Int8.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Primitive.Int8 where
+
+import Rattletrap.Utility
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as ByteString
+import qualified Data.Int as Int
+
+newtype Int8 = Int8
+  { int8Value :: Int.Int8
+  } deriving (Eq, Ord, Show)
+
+getInt8 :: Binary.Get Int8
+getInt8 = do
+  int8 <- Binary.getInt8
+  pure (Int8 int8)
+
+putInt8 :: Int8 -> Binary.Put
+putInt8 int8 = Binary.putInt8 (int8Value int8)
+
+getInt8Bits :: BinaryBit.BitGet Int8
+getInt8Bits = do
+  bytes <- BinaryBit.getLazyByteString 1
+  pure (Binary.runGet getInt8 (reverseBytes bytes))
+
+putInt8Bits :: Int8 -> BinaryBit.BitPut ()
+putInt8Bits int8 = do
+  let bytes = Binary.runPut (putInt8 int8)
+  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/source/library/Rattletrap/Primitive/Int8Vector.hs b/source/library/Rattletrap/Primitive/Int8Vector.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Primitive/Int8Vector.hs
@@ -0,0 +1,42 @@
+module Rattletrap.Primitive.Int8Vector where
+
+import Rattletrap.Primitive.Int8
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data Int8Vector = Int8Vector
+  { int8VectorX :: Maybe Int8
+  , int8VectorY :: Maybe Int8
+  , int8VectorZ :: Maybe Int8
+  } deriving (Eq, Ord, Show)
+
+getInt8Vector :: BinaryBit.BitGet Int8Vector
+getInt8Vector = do
+  x <- getInt8VectorField
+  y <- getInt8VectorField
+  z <- getInt8VectorField
+  pure (Int8Vector x y z)
+
+putInt8Vector :: Int8Vector -> BinaryBit.BitPut ()
+putInt8Vector int8Vector = do
+  putInt8VectorField (int8VectorX int8Vector)
+  putInt8VectorField (int8VectorY int8Vector)
+  putInt8VectorField (int8VectorZ int8Vector)
+
+getInt8VectorField :: BinaryBit.BitGet (Maybe Int8)
+getInt8VectorField = do
+  hasField <- BinaryBit.getBool
+  if hasField
+    then do
+      field <- getInt8Bits
+      pure (Just field)
+    else pure Nothing
+
+putInt8VectorField :: Maybe Int8 -> BinaryBit.BitPut ()
+putInt8VectorField maybeField =
+  case maybeField of
+    Nothing -> BinaryBit.putBool False
+    Just field -> do
+      BinaryBit.putBool True
+      putInt8Bits field
diff --git a/source/library/Rattletrap/Primitive/List.hs b/source/library/Rattletrap/Primitive/List.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Primitive/List.hs
@@ -0,0 +1,22 @@
+module Rattletrap.Primitive.List where
+
+import Rattletrap.Primitive.Word32
+
+import qualified Control.Monad as Monad
+import qualified Data.Binary as Binary
+
+newtype List a = List
+  { listValue :: [a]
+  } deriving (Eq, Ord, Show)
+
+getList :: Binary.Get a -> Binary.Get (List a)
+getList getElement = do
+  size <- getWord32
+  elements <- Monad.replicateM (fromIntegral (word32Value size)) getElement
+  pure (List elements)
+
+putList :: (a -> Binary.Put) -> List a -> Binary.Put
+putList putElement list = do
+  let elements = listValue list
+  putWord32 (Word32 (fromIntegral (length elements)))
+  mapM_ putElement elements
diff --git a/source/library/Rattletrap/Primitive/Section.hs b/source/library/Rattletrap/Primitive/Section.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Primitive/Section.hs
@@ -0,0 +1,55 @@
+module Rattletrap.Primitive.Section where
+
+import Rattletrap.Crc
+import Rattletrap.Primitive.Word32
+
+import qualified Control.Monad as Monad
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as ByteString
+
+-- | A section is a large piece of a 'Rattletrap.Replay.Replay'. It has a
+-- 32-bit size (in bytes), a 32-bit CRC (see "Rattletrap.Crc"), and then a
+-- bunch of data (the body). This interface is provided so that you don't have
+-- to think about the size and CRC.
+newtype Section a = Section
+  { sectionBody :: a
+  -- ^ The actual content in the section.
+  } deriving (Eq, Ord, Show)
+
+-- | Given a way to get the 'sectionBody', gets a section. This will fail if
+-- the section's CRC does not match.
+--
+-- @
+-- let header = 'Data.Binary.Get.runGet' ('getSection' 'Rattletrap.Header.getHeader') bytes
+-- @
+getSection :: Binary.Get a -> Binary.Get (Section a)
+getSection getBody = do
+  size <- getWord32
+  crc <- getWord32
+  rawBody <- Binary.getLazyByteString (fromIntegral (word32Value size))
+  let actualCrc = Word32 (getCrc32 rawBody)
+  Monad.when (actualCrc /= crc) (fail (crcMessage actualCrc crc))
+  let body = Binary.runGet getBody rawBody
+  pure (Section body)
+
+-- | Given a way to put the 'sectionBody', puts a section. This will also put
+-- the size and CRC.
+--
+-- @
+-- let bytes = 'Data.Binary.Put.runPut' ('putSection' 'Rattletrap.Content.putContent' content)
+-- @
+putSection :: (a -> Binary.Put) -> Section a -> Binary.Put
+putSection putBody section = do
+  let rawBody = Binary.runPut (putBody (sectionBody section))
+  let size = ByteString.length rawBody
+  let crc = getCrc32 rawBody
+  putWord32 (Word32 (fromIntegral size))
+  putWord32 (Word32 crc)
+  Binary.putLazyByteString rawBody
+
+crcMessage :: Word32 -> Word32 -> String
+crcMessage actual expected =
+  unwords
+    ["actual CRC", show actual, "does not match expected CRC", show expected]
diff --git a/source/library/Rattletrap/Primitive/Text.hs b/source/library/Rattletrap/Primitive/Text.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Primitive/Text.hs
@@ -0,0 +1,108 @@
+module Rattletrap.Primitive.Text where
+
+import Rattletrap.Primitive.Int32
+import Rattletrap.Utility
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy.Char8 as ByteString
+import qualified Data.Char as Char
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Encoding
+
+newtype Text = Text
+  { textValue :: Text.Text
+  } deriving (Eq, Ord, Show)
+
+getText :: Binary.Get Text
+getText = do
+  rawSize <- getInt32
+  let decode = getTextDecoder rawSize
+  let size = normalizeTextSize rawSize
+  bytes <- Binary.getLazyByteString size
+  let text = dropNull (decode bytes)
+  pure (Text text)
+
+putText :: Text -> Binary.Put
+putText text = do
+  let size = getTextSize text
+  let encode = getTextEncoder size
+  putInt32 size
+  Binary.putLazyByteString (encode (addNull (textValue text)))
+
+getTextBits :: BinaryBit.BitGet Text
+getTextBits = do
+  rawSize <- getInt32Bits
+  let decode = getTextDecoder rawSize
+  let size = normalizeTextSize rawSize
+  bytes <- BinaryBit.getLazyByteString size
+  let text = dropNull (decode (reverseBytes bytes))
+  pure (Text text)
+
+putTextBits :: Text -> BinaryBit.BitPut ()
+putTextBits text = do
+  let size = getTextSize text
+  let encode = getTextEncoder size
+  putInt32Bits size
+  BinaryBit.putByteString
+    (ByteString.toStrict (reverseBytes (encode (addNull (textValue text)))))
+
+stringToText :: String -> Text
+stringToText string = Text (Text.pack string)
+
+textToString :: Text -> String
+textToString text = Text.unpack (textValue text)
+
+getTextSize :: Text -> Int32
+getTextSize text =
+  let value = textValue text
+      scale =
+        if Text.all Char.isLatin1 value
+          then 1
+          else -1
+      rawSize =
+        if Text.null value
+          then 0
+          else fromIntegral (Text.length value) + 1
+      size =
+        if value == Text.pack "\x00\x00\x00None"
+          then 0x05000000
+          else scale * rawSize
+  in Int32 size
+
+normalizeTextSize
+  :: Integral a
+  => Int32 -> a
+normalizeTextSize size =
+  case int32Value size of
+    0x05000000 -> 8
+    x ->
+      if x < 0
+        then (-2 * fromIntegral x)
+        else fromIntegral x
+
+getTextDecoder :: Int32 -> ByteString.ByteString -> Text.Text
+getTextDecoder size bytes =
+  let decode =
+        if size < Int32 0
+          then Encoding.decodeUtf16LE
+          else Encoding.decodeLatin1
+  in decode (ByteString.toStrict bytes)
+
+getTextEncoder :: Int32 -> Text.Text -> ByteString.ByteString
+getTextEncoder size text =
+  if size < Int32 0
+    then ByteString.fromStrict (Encoding.encodeUtf16LE text)
+    else encodeLatin1 text
+
+dropNull :: Text.Text -> Text.Text
+dropNull text = Text.dropWhileEnd (== '\x00') text
+
+addNull :: Text.Text -> Text.Text
+addNull text =
+  if Text.null text
+    then text
+    else Text.snoc text '\x00'
diff --git a/source/library/Rattletrap/Primitive/Vector.hs b/source/library/Rattletrap/Primitive/Vector.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Primitive/Vector.hs
@@ -0,0 +1,40 @@
+module Rattletrap.Primitive.Vector where
+
+import Rattletrap.Primitive.CompressedWord
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data Vector = Vector
+  { vectorBias :: Word
+  , vectorX :: Int
+  , vectorY :: Int
+  , vectorZ :: Int
+  } deriving (Eq, Ord, Show)
+
+getVector :: BinaryBit.BitGet Vector
+getVector = do
+  bitSize <- getCompressedWord 19
+  let limit = 2 ^ (compressedWordValue bitSize + 2)
+  dx <- getCompressedWord limit
+  dy <- getCompressedWord limit
+  dz <- getCompressedWord limit
+  let fromCompressedWord x = fromIntegral (compressedWordValue x)
+  let bias = 2 ^ (fromCompressedWord bitSize + 1 :: Word)
+  let x = fromCompressedWord dx - fromIntegral bias
+  let y = fromCompressedWord dy - fromIntegral bias
+  let z = fromCompressedWord dz - fromIntegral bias
+  pure (Vector bias x y z)
+
+putVector :: Vector -> BinaryBit.BitPut ()
+putVector vector = do
+  let bitSize =
+        round (logBase (2 :: Float) (fromIntegral (vectorBias vector))) - 1
+  putCompressedWord (CompressedWord 19 bitSize)
+  let dx = fromIntegral (vectorX vector + fromIntegral (vectorBias vector))
+  let dy = fromIntegral (vectorY vector + fromIntegral (vectorBias vector))
+  let dz = fromIntegral (vectorZ vector + fromIntegral (vectorBias vector))
+  let limit = 2 ^ (bitSize + 2)
+  putCompressedWord (CompressedWord limit dx)
+  putCompressedWord (CompressedWord limit dy)
+  putCompressedWord (CompressedWord limit dz)
diff --git a/source/library/Rattletrap/Primitive/Word32.hs b/source/library/Rattletrap/Primitive/Word32.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Primitive/Word32.hs
@@ -0,0 +1,33 @@
+module Rattletrap.Primitive.Word32 where
+
+import Rattletrap.Utility
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as ByteString
+import qualified Data.Word as Word
+
+newtype Word32 = Word32
+  { word32Value :: Word.Word32
+  } deriving (Eq, Ord, Show)
+
+getWord32 :: Binary.Get Word32
+getWord32 = do
+  word32 <- Binary.getWord32le
+  pure (Word32 word32)
+
+putWord32 :: Word32 -> Binary.Put
+putWord32 word32 = Binary.putWord32le (word32Value word32)
+
+getWord32Bits :: BinaryBit.BitGet Word32
+getWord32Bits = do
+  bytes <- BinaryBit.getLazyByteString 4
+  pure (Binary.runGet getWord32 (reverseBytes bytes))
+
+putWord32Bits :: Word32 -> BinaryBit.BitPut ()
+putWord32Bits word32 = do
+  let bytes = Binary.runPut (putWord32 word32)
+  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/source/library/Rattletrap/Primitive/Word64.hs b/source/library/Rattletrap/Primitive/Word64.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Primitive/Word64.hs
@@ -0,0 +1,33 @@
+module Rattletrap.Primitive.Word64 where
+
+import Rattletrap.Utility
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as ByteString
+import qualified Data.Word as Word
+
+newtype Word64 = Word64
+  { word64Value :: Word.Word64
+  } deriving (Eq, Ord, Show)
+
+getWord64 :: Binary.Get Word64
+getWord64 = do
+  word64 <- Binary.getWord64le
+  pure (Word64 word64)
+
+putWord64 :: Word64 -> Binary.Put
+putWord64 word64 = Binary.putWord64le (word64Value word64)
+
+getWord64Bits :: BinaryBit.BitGet Word64
+getWord64Bits = do
+  bytes <- BinaryBit.getLazyByteString 8
+  pure (Binary.runGet getWord64 (reverseBytes bytes))
+
+putWord64Bits :: Word64 -> BinaryBit.BitPut ()
+putWord64Bits word64 = do
+  let bytes = Binary.runPut (putWord64 word64)
+  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/source/library/Rattletrap/Primitive/Word8.hs b/source/library/Rattletrap/Primitive/Word8.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Primitive/Word8.hs
@@ -0,0 +1,33 @@
+module Rattletrap.Primitive.Word8 where
+
+import Rattletrap.Utility
+
+import qualified Data.Binary as Binary
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Binary.Get as Binary
+import qualified Data.Binary.Put as Binary
+import qualified Data.ByteString.Lazy as ByteString
+import qualified Data.Word as Word
+
+newtype Word8 = Word8
+  { word8Value :: Word.Word8
+  } deriving (Eq, Ord, Show)
+
+getWord8 :: Binary.Get Word8
+getWord8 = do
+  word8 <- Binary.getWord8
+  pure (Word8 word8)
+
+putWord8 :: Word8 -> Binary.Put
+putWord8 word8 = Binary.putWord8 (word8Value word8)
+
+getWord8Bits :: BinaryBit.BitGet Word8
+getWord8Bits = do
+  bytes <- BinaryBit.getLazyByteString 1
+  pure (Binary.runGet getWord8 (reverseBytes bytes))
+
+putWord8Bits :: Word8 -> BinaryBit.BitPut ()
+putWord8Bits word8 = do
+  let bytes = Binary.runPut (putWord8 word8)
+  BinaryBit.putByteString (ByteString.toStrict (reverseBytes bytes))
diff --git a/source/library/Rattletrap/Property.hs b/source/library/Rattletrap/Property.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Property.hs
@@ -0,0 +1,26 @@
+module Rattletrap.Property where
+
+import Rattletrap.Primitive
+import Rattletrap.PropertyValue
+
+import qualified Data.Binary as Binary
+
+data Property = Property
+  { propertyKind :: Text
+  , propertySize :: Word64
+  -- ^ Not used.
+  , propertyValue :: PropertyValue Property
+  } deriving (Eq, Ord, Show)
+
+getProperty :: Binary.Get Property
+getProperty = do
+  kind <- getText
+  size <- getWord64
+  value <- getPropertyValue getProperty kind
+  pure (Property kind size value)
+
+putProperty :: Property -> Binary.Put
+putProperty property = do
+  putText (propertyKind property)
+  putWord64 (propertySize property)
+  putPropertyValue putProperty (propertyValue property)
diff --git a/source/library/Rattletrap/PropertyValue.hs b/source/library/Rattletrap/PropertyValue.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/PropertyValue.hs
@@ -0,0 +1,72 @@
+module Rattletrap.PropertyValue where
+
+import Rattletrap.Primitive
+
+import qualified Data.Binary as Binary
+
+data PropertyValue a
+  = ArrayProperty (List (Dictionary a))
+  -- ^ Yes, a list of dictionaries. No, it doesn't make sense. These usually
+  -- only have one element.
+  | BoolProperty Word8
+  | ByteProperty Text
+                 (Maybe Text)
+  -- ^ This is a strange name for essentially a key-value pair.
+  | FloatProperty Float32
+  | IntProperty Int32
+  | NameProperty Text
+  -- ^ It's unclear how exactly this is different than a 'StrProperty'.
+  | QWordProperty Word64
+  | StrProperty Text
+  deriving (Eq, Ord, Show)
+
+getPropertyValue :: Binary.Get a -> Text -> Binary.Get (PropertyValue a)
+getPropertyValue getProperty kind =
+  case textToString kind of
+    "ArrayProperty" -> do
+      list <- getList (getDictionary getProperty)
+      pure (ArrayProperty list)
+    "BoolProperty" -> do
+      word8 <- getWord8
+      pure (BoolProperty word8)
+    "ByteProperty" -> do
+      k <- getText
+      v <-
+        if textToString k == "OnlinePlatform_Steam"
+          then pure Nothing
+          else do
+            v <- getText
+            pure (Just v)
+      pure (ByteProperty k v)
+    "FloatProperty" -> do
+      float32 <- getFloat32
+      pure (FloatProperty float32)
+    "IntProperty" -> do
+      int32 <- getInt32
+      pure (IntProperty int32)
+    "NameProperty" -> do
+      text <- getText
+      pure (NameProperty text)
+    "QWordProperty" -> do
+      word64 <- getWord64
+      pure (QWordProperty word64)
+    "StrProperty" -> do
+      text <- getText
+      pure (StrProperty text)
+    _ -> fail ("don't know how to read property value " ++ show kind)
+
+putPropertyValue :: (a -> Binary.Put) -> PropertyValue a -> Binary.Put
+putPropertyValue putProperty value =
+  case value of
+    ArrayProperty list -> putList (putDictionary putProperty) list
+    BoolProperty word8 -> putWord8 word8
+    ByteProperty k mv -> do
+      putText k
+      case mv of
+        Nothing -> pure ()
+        Just v -> putText v
+    FloatProperty float32 -> putFloat32 float32
+    IntProperty int32 -> putInt32 int32
+    NameProperty text -> putText text
+    QWordProperty word64 -> putWord64 word64
+    StrProperty text -> putText text
diff --git a/source/library/Rattletrap/RemoteId.hs b/source/library/Rattletrap/RemoteId.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/RemoteId.hs
@@ -0,0 +1,55 @@
+module Rattletrap.RemoteId where
+
+import Rattletrap.Primitive
+import Rattletrap.Utility
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.ByteString.Lazy as ByteString
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Encoding
+import qualified Data.Word as Word
+
+data RemoteId
+  = PlayStationId Text.Text
+                  [Word.Word8]
+  | SplitscreenId Word.Word32
+  | SteamId Word64
+  | XboxId Word64
+  deriving (Eq, Ord, Show)
+
+getRemoteId :: Word8 -> BinaryBit.BitGet RemoteId
+getRemoteId systemId =
+  case word8Value systemId of
+    0 -> do
+      word24 <- BinaryBit.getWord32be 24
+      pure (SplitscreenId word24)
+    1 -> do
+      word64 <- getWord64Bits
+      pure (SteamId word64)
+    2 -> do
+      rawName <- BinaryBit.getLazyByteString 16
+      let name =
+            Text.dropWhileEnd
+              (== '\x00')
+              (Encoding.decodeLatin1
+                 (ByteString.toStrict (reverseBytes rawName)))
+      bytes <- BinaryBit.getLazyByteString 16
+      pure (PlayStationId name (ByteString.unpack bytes))
+    4 -> do
+      word64 <- getWord64Bits
+      pure (XboxId word64)
+    _ -> fail ("unknown system id " ++ show systemId)
+
+putRemoteId :: RemoteId -> BinaryBit.BitPut ()
+putRemoteId remoteId =
+  case remoteId of
+    PlayStationId name bytes -> do
+      let rawName =
+            ByteString.toStrict
+              (reverseBytes (padBytes (16 :: Int) (encodeLatin1 name)))
+      BinaryBit.putByteString rawName
+      BinaryBit.putByteString (ByteString.toStrict (ByteString.pack bytes))
+    SplitscreenId word24 -> BinaryBit.putWord32be 24 word24
+    SteamId word64 -> putWord64Bits word64
+    XboxId word64 -> putWord64Bits word64
diff --git a/source/library/Rattletrap/Replay.hs b/source/library/Rattletrap/Replay.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Replay.hs
@@ -0,0 +1,39 @@
+module Rattletrap.Replay where
+
+import Rattletrap.Content
+import Rattletrap.Header
+import Rattletrap.Primitive.Section
+
+import qualified Data.Binary as Binary
+
+-- | A Rocket League replay.
+data Replay = Replay
+  { replayHeader :: Section Header
+  -- ^ This has most of the high-level metadata.
+  , replayContent :: Section Content
+  -- ^ This has most of the low-level game data.
+  } deriving (Eq, Ord, Show)
+
+-- | Parses a raw replay. Use this with 'Data.Binary.Get.runGet'.
+--
+-- @
+-- let replay = 'Data.Binary.Get.runGet' 'getReplay' bytes
+-- @
+getReplay :: Binary.Get Replay
+getReplay = do
+  header <- getSection getHeader
+  let version = getVersion (sectionBody header)
+  let numFrames = getNumFrames (sectionBody header)
+  let maxChannels = getMaxChannels (sectionBody header)
+  content <- getSection (getContent version numFrames maxChannels)
+  pure (Replay header content)
+
+-- | Generates a raw replay. Use this with 'Data.Binary.Put.runPut'.
+--
+-- @
+-- let bytes = 'Data.Binary.Put.runPut' ('putReplay' replay)
+-- @
+putReplay :: Replay -> Binary.Put
+putReplay replay = do
+  putSection putHeader (replayHeader replay)
+  putSection putContent (replayContent replay)
diff --git a/source/library/Rattletrap/Replication.hs b/source/library/Rattletrap/Replication.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Replication.hs
@@ -0,0 +1,57 @@
+module Rattletrap.Replication where
+
+import Rattletrap.ActorMap
+import Rattletrap.ClassAttributeMap
+import Rattletrap.Primitive
+import Rattletrap.ReplicationValue
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data Replication = Replication
+  { replicationActorId :: CompressedWord
+  , replicationValue :: ReplicationValue
+  } deriving (Eq, Ord, Show)
+
+getReplications
+  :: (Int, Int)
+  -> Word
+  -> ClassAttributeMap
+  -> ActorMap
+  -> BinaryBit.BitGet ([Replication], ActorMap)
+getReplications version maxChannels classAttributeMap actorMap = do
+  maybeReplication <-
+    getReplication version maxChannels classAttributeMap actorMap
+  case maybeReplication of
+    Nothing -> pure ([], actorMap)
+    Just (replication, newActorMap) -> do
+      (replications, newerActorMap) <-
+        getReplications version maxChannels classAttributeMap newActorMap
+      pure (replication : replications, newerActorMap)
+
+putReplications :: [Replication] -> BinaryBit.BitPut ()
+putReplications replications = do
+  mapM_ putReplication replications
+  BinaryBit.putBool False
+
+getReplication
+  :: (Int, Int)
+  -> Word
+  -> ClassAttributeMap
+  -> ActorMap
+  -> BinaryBit.BitGet (Maybe (Replication, ActorMap))
+getReplication version maxChannels classAttributeMap actorMap = do
+  hasReplication <- BinaryBit.getBool
+  if not hasReplication
+    then pure Nothing
+    else do
+      actorId <- getCompressedWord maxChannels
+      (value, newActorMap) <-
+        getReplicationValue version classAttributeMap actorMap actorId
+      pure (Just (Replication actorId value, newActorMap))
+
+putReplication :: Replication -> BinaryBit.BitPut ()
+putReplication replication = do
+  BinaryBit.putBool True
+  putCompressedWord (replicationActorId replication)
+  putReplicationValue (replicationValue replication)
diff --git a/source/library/Rattletrap/Replication/Destroyed.hs b/source/library/Rattletrap/Replication/Destroyed.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Replication/Destroyed.hs
@@ -0,0 +1,17 @@
+module Rattletrap.Replication.Destroyed where
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+-- | Destroyed replications don't actually contain any extra information. All
+-- you need to know is the actor's ID, which is given by the
+-- 'Rattletrap.Replication.Replication'.
+data DestroyedReplication = DestroyedReplication
+  {
+  } deriving (Eq, Ord, Show)
+
+getDestroyedReplication :: BinaryBit.BitGet DestroyedReplication
+getDestroyedReplication = pure DestroyedReplication
+
+putDestroyedReplication :: DestroyedReplication -> BinaryBit.BitPut ()
+putDestroyedReplication _ = pure ()
diff --git a/source/library/Rattletrap/Replication/Spawned.hs b/source/library/Rattletrap/Replication/Spawned.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Replication/Spawned.hs
@@ -0,0 +1,95 @@
+module Rattletrap.Replication.Spawned where
+
+import Rattletrap.ActorMap
+import Rattletrap.ClassAttributeMap
+import Rattletrap.Initialization
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data SpawnedReplication = SpawnedReplication
+  { spawnedReplicationFlag :: Bool
+  -- ^ Unclear what this is.
+  , spawnedReplicationNameIndex :: Maybe Word32
+  , spawnedReplicationName :: Maybe Text
+  -- ^ Read-only! Changing a replication's name requires editing the
+  -- 'spawnedReplicationNameIndex' and maybe the class attribute map.
+  , spawnedReplicationObjectId :: Word32
+  , spawnedReplicationObjectName :: Text
+  -- ^ Read-only! Changing a replication's object requires editing the class
+  -- attribute map.
+  , spawnedReplicationClassName :: Text
+  -- ^ Read-only! Changing a replication's class requires editing the class
+  -- attribute map.
+  , spawnedReplicationInitialization :: Initialization
+  } deriving (Eq, Ord, Show)
+
+getSpawnedReplication
+  :: (Int, Int)
+  -> ClassAttributeMap
+  -> ActorMap
+  -> CompressedWord
+  -> BinaryBit.BitGet (SpawnedReplication, ActorMap)
+getSpawnedReplication version classAttributeMap actorMap actorId = do
+  flag <- BinaryBit.getBool
+  nameIndex <-
+    if version < (868, 14)
+      then pure Nothing
+      else do
+        nameIndex <- getWord32Bits
+        pure (Just nameIndex)
+  name <- lookupName classAttributeMap nameIndex
+  objectId <- getWord32Bits
+  let newActorMap = updateActorMap actorId objectId actorMap
+  objectName <- lookupObjectName classAttributeMap objectId
+  className <- lookupClassName objectName
+  let hasLocation = classHasLocation className
+  let hasRotation = classHasRotation className
+  initialization <- getInitialization hasLocation hasRotation
+  pure
+    ( SpawnedReplication
+        flag
+        nameIndex
+        name
+        objectId
+        objectName
+        className
+        initialization
+    , newActorMap)
+
+putSpawnedReplication :: SpawnedReplication -> BinaryBit.BitPut ()
+putSpawnedReplication spawnedReplication = do
+  BinaryBit.putBool (spawnedReplicationFlag spawnedReplication)
+  case spawnedReplicationNameIndex spawnedReplication of
+    Nothing -> pure ()
+    Just nameIndex -> putWord32Bits nameIndex
+  putWord32Bits (spawnedReplicationObjectId spawnedReplication)
+  putInitialization (spawnedReplicationInitialization spawnedReplication)
+
+lookupName
+  :: Monad m
+  => ClassAttributeMap -> Maybe Word32 -> m (Maybe Text)
+lookupName classAttributeMap maybeNameIndex =
+  case maybeNameIndex of
+    Nothing -> pure Nothing
+    Just nameIndex ->
+      case getName (classAttributeMapNameMap classAttributeMap) nameIndex of
+        Nothing -> fail ("could not get name for index " ++ show nameIndex)
+        Just name -> pure (Just name)
+
+lookupObjectName
+  :: Monad m
+  => ClassAttributeMap -> Word32 -> m Text
+lookupObjectName classAttributeMap objectId =
+  case getObjectName (classAttributeMapObjectMap classAttributeMap) objectId of
+    Nothing -> fail ("could not get object name for id " ++ show objectId)
+    Just objectName -> pure objectName
+
+lookupClassName
+  :: Monad m
+  => Text -> m Text
+lookupClassName objectName =
+  case getClassName objectName of
+    Nothing -> fail ("could not get class name for object " ++ show objectName)
+    Just className -> pure className
diff --git a/source/library/Rattletrap/Replication/Updated.hs b/source/library/Rattletrap/Replication/Updated.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Replication/Updated.hs
@@ -0,0 +1,27 @@
+module Rattletrap.Replication.Updated where
+
+import Rattletrap.ActorMap
+import Rattletrap.Attribute
+import Rattletrap.ClassAttributeMap
+import Rattletrap.Primitive
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+newtype UpdatedReplication = UpdatedReplication
+  { updatedReplicationAttributes :: [Attribute]
+  } deriving (Eq, Ord, Show)
+
+getUpdatedReplication
+  :: (Int, Int)
+  -> ClassAttributeMap
+  -> ActorMap
+  -> CompressedWord
+  -> BinaryBit.BitGet UpdatedReplication
+getUpdatedReplication version classAttributeMap actorMap actorId = do
+  attributes <- getAttributes version classAttributeMap actorMap actorId
+  pure (UpdatedReplication attributes)
+
+putUpdatedReplication :: UpdatedReplication -> BinaryBit.BitPut ()
+putUpdatedReplication updatedReplication =
+  putAttributes (updatedReplicationAttributes updatedReplication)
diff --git a/source/library/Rattletrap/ReplicationValue.hs b/source/library/Rattletrap/ReplicationValue.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/ReplicationValue.hs
@@ -0,0 +1,63 @@
+module Rattletrap.ReplicationValue
+  ( module Rattletrap.ReplicationValue
+  , module Rattletrap.Replication.Destroyed
+  , module Rattletrap.Replication.Spawned
+  , module Rattletrap.Replication.Updated
+  ) where
+
+import Rattletrap.ActorMap
+import Rattletrap.ClassAttributeMap
+import Rattletrap.Primitive
+import Rattletrap.Replication.Destroyed
+import Rattletrap.Replication.Spawned
+import Rattletrap.Replication.Updated
+
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+
+data ReplicationValue
+  = SpawnedReplicationValue SpawnedReplication
+  -- ^ Creates a new actor.
+  | UpdatedReplicationValue UpdatedReplication
+  -- ^ Updates an existing actor.
+  | DestroyedReplicationValue DestroyedReplication
+  -- ^ Destroys an existing actor.
+  deriving (Eq, Ord, Show)
+
+getReplicationValue
+  :: (Int, Int)
+  -> ClassAttributeMap
+  -> ActorMap
+  -> CompressedWord
+  -> BinaryBit.BitGet (ReplicationValue, ActorMap)
+getReplicationValue version classAttributeMap actorMap actorId = do
+  isOpen <- BinaryBit.getBool
+  if isOpen
+    then do
+      isNew <- BinaryBit.getBool
+      if isNew
+        then do
+          (x, newActorMap) <-
+            getSpawnedReplication version classAttributeMap actorMap actorId
+          pure (SpawnedReplicationValue x, newActorMap)
+        else do
+          x <- getUpdatedReplication version classAttributeMap actorMap actorId
+          pure (UpdatedReplicationValue x, actorMap)
+    else do
+      x <- getDestroyedReplication
+      pure (DestroyedReplicationValue x, actorMap)
+
+putReplicationValue :: ReplicationValue -> BinaryBit.BitPut ()
+putReplicationValue value =
+  case value of
+    SpawnedReplicationValue x -> do
+      BinaryBit.putBool True
+      BinaryBit.putBool True
+      putSpawnedReplication x
+    UpdatedReplicationValue x -> do
+      BinaryBit.putBool True
+      BinaryBit.putBool False
+      putUpdatedReplication x
+    DestroyedReplicationValue x -> do
+      BinaryBit.putBool False
+      putDestroyedReplication x
diff --git a/source/library/Rattletrap/Utility.hs b/source/library/Rattletrap/Utility.hs
new file mode 100644
--- /dev/null
+++ b/source/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.ByteString.Lazy.Char8 as ByteString8
+import qualified Data.Text as Text
+import qualified Data.Word as Word
+
+encodeLatin1 :: Text.Text -> ByteString.ByteString
+encodeLatin1 text = ByteString8.pack (Text.unpack text)
+
+padBytes
+  :: Integral a
+  => a -> ByteString.ByteString -> ByteString.ByteString
+padBytes size bytes =
+  ByteString.concat
+    [ bytes
+    , ByteString.replicate (fromIntegral size - ByteString.length bytes) 0x00
+    ]
+
+reverseByte :: Word.Word8 -> Word.Word8
+reverseByte byte =
+  Bits.shiftR (byte Bits..&. Bits.bit 7) 7 +
+  Bits.shiftR (byte Bits..&. Bits.bit 6) 5 +
+  Bits.shiftR (byte Bits..&. Bits.bit 5) 3 +
+  Bits.shiftR (byte Bits..&. Bits.bit 4) 1 +
+  Bits.shiftL (byte Bits..&. Bits.bit 3) 1 +
+  Bits.shiftL (byte Bits..&. Bits.bit 2) 3 +
+  Bits.shiftL (byte Bits..&. Bits.bit 1) 5 +
+  Bits.shiftL (byte Bits..&. Bits.bit 0) 7
+
+reverseBytes :: ByteString.ByteString -> ByteString.ByteString
+reverseBytes bytes = ByteString.map reverseByte bytes
diff --git a/source/library/Rattletrap/Version.hs b/source/library/Rattletrap/Version.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Rattletrap/Version.hs
@@ -0,0 +1,7 @@
+module Rattletrap.Version where
+
+import qualified Data.Version as Version
+import qualified Paths_rattletrap as This
+
+version :: Version.Version
+version = This.version
diff --git a/source/tests/Main.hs b/source/tests/Main.hs
new file mode 100644
--- /dev/null
+++ b/source/tests/Main.hs
@@ -0,0 +1,100 @@
+import qualified Control.Monad as Monad
+import qualified Data.ByteString.Lazy as ByteString
+import qualified Rattletrap
+import qualified System.FilePath as FilePath
+import qualified System.IO.Temp as Temp
+import qualified Test.Tasty as Tasty
+import qualified Test.Tasty.Hspec as Hspec
+
+main :: IO ()
+main = do
+  tests <- Hspec.testSpec "rattletrap" spec
+  Tasty.defaultMain tests
+
+spec :: Hspec.Spec
+spec =
+  Hspec.describe
+    "Rattletrap"
+    (mapM_ (\(uuid, description) -> itCanRoundTrip uuid description) replays)
+
+itCanRoundTrip :: String -> String -> Hspec.Spec
+itCanRoundTrip uuid description =
+  Hspec.it (specName uuid description) (specBody uuid)
+
+specName :: String -> String -> String
+specName uuid description = unwords [take 4 uuid, description]
+
+specBody :: String -> IO ()
+specBody uuid = do
+  let inputFile = pathToReplay uuid
+  input <- ByteString.readFile inputFile
+  Temp.withSystemTempDirectory
+    "replay-"
+    (\directory -> do
+       let jsonFile = FilePath.combine directory "replay.json"
+       Rattletrap.mainWithArgs ["decode", inputFile, jsonFile]
+       let outputFile = FilePath.combine directory "output.replay"
+       Rattletrap.mainWithArgs ["encode", jsonFile, outputFile]
+       output <- ByteString.readFile outputFile
+       Monad.unless
+         (output == input)
+         (Hspec.expectationFailure "output does not match input"))
+
+pathToReplay :: String -> FilePath
+pathToReplay uuid =
+  FilePath.joinPath ["replays", FilePath.addExtension uuid ".replay"]
+
+replays :: [(String, String)]
+replays =
+  [ ("00080014003600090000036E0F65CCEB", "a flip time")
+  , ("07E925B1423653D44CB8B4B2524792C1", "a game mode before Neo Tokyo")
+  , ("0AD21FEE4A512906BEB6C98136AAF49A", "some Latin-1 text")
+  , ("1205D96C4D819800927791820096CD49", "rumble mode")
+  , ("160CA83E41083BFD8E6315B4BFCA0561", "a dedicated server IP")
+  , ("18D6738D415B70B5BE4C299588D3C141", "an online loadout attribute")
+  , ("1A126AC24CAA0DB0E98835BD960B8AF8", "overtime")
+  , ("1AE415514DFC65DCBF8B8391AD35488D", "a game time")
+  , ("1BC2D01444ACE577D01E988EADD4DFD0", "no padding after the frames")
+  , ("1D1DE97D4941C86E43FE0093563DB621", "a camera pitch")
+  , ("1EF90FCC4F719F606A5327B3CDD782A4", "a private hoops match")
+  , ("1F3798E540B0C37A946561ABBB3037F9", "splitscreen players")
+  , ("211466D04B983F5A33CC2FA1D5928672", "a match save")
+  , ("22BACD794ABE7B92E50E9CBDBD9C59CE", "a vote to forfeit")
+  , ("27B6A7B64553F0F685874584F96BAB1B", "some UTF-16 text")
+  , ("29F582C34A65EB34D358A784CBE3C189", "frames")
+  , ("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")
+  , ("6320E51C49066A7C210A2993C2201D5F", "a forfeit attribute")
+  , ("6688EEE34BFEB3EC3A9E3283098CC712", "a malformed byte property")
+  , ("6D1B06D844A5BB91B81FD4B5B28F08BA", "a flip right")
+  , ("6F7CFCD24638F8A6567AB3A8B9958A90", "a map with numbers")
+  , ("7109EB9846D303E54B7ACBA792036213", "a boost modifier")
+  , ("7BF6073F4614CE0A438994B9A260DA6A", "an online loadouts attribute")
+  , ("89CBA30E46FA5385BDD35DA4285D4D2E", "remote user data")
+  , ("9704208245D7DD851F2FB2BC7DFD9AC3", "a batarang")
+  , ("98E58A904D713F2DE202358E8573265D", "a player using behind view")
+  , ("A128B3AB45D5A18E3EF9CF93C9576BCE", "a round count down")
+  , ("A52F804845573D8DA65E97BF59026A43", "some more mutators")
+  , ("A6711CE74272B2E663DCC9A200A218E3", "a waiting player")
+  , ("A7F001A1417A19BFA8C90990D8F7C2FF", "a ready attribute")
+  , ("B9F9B87D4A9D0A3D25D4EC91C0401DE2", "a party leader")
+  , ("C14F7E0E4D9B5E6BE9AD5D8ED56B174C", "some mutators")
+  , ("C8372B1345B1803DEF039F815DBD802D", "a spectator")
+  , ("CC4CA70D4F7A67EBAD0ED9B9923106F7", "after Starbase ARC")
+  , ("D428F81646A98C25902CE988AE5C14C8", "a private hockey match")
+  , ("D7FB197A451D69075A0C99A2F49A4053", "an explosion attribute")
+  , ("DCB3A6B94A9DBE46FDE5EAA9B012F6C8", "a pawn type attribute")
+  , ("EAE311E84BA35B590A6FDBA6DD4F2FEB", "an actor/object ID collision")
+  , ("EAE8DADA4BB2DC5422792C9B4A67392D", "custom team colors")
+  , ("F299F176491554B11E34AB91CA76B2CE", "a location attribute")
+  , ("F7B9E14545C7467B89A00895980FCD73", "a hockey game event")
+  , ("F811C1D24888015E23B598AD8628C742", "no frames")
+  , ("FDC79DA84DD463D4BCCE6B892829AC88", "an MVP")
+  ]
diff --git a/test/Test.hs b/test/Test.hs
deleted file mode 100644
--- a/test/Test.hs
+++ /dev/null
@@ -1,98 +0,0 @@
-import qualified Control.Monad as Monad
-import qualified Data.ByteString.Lazy as ByteString
-import qualified Rattletrap
-import qualified System.FilePath as FilePath
-import qualified System.IO.Temp as Temp
-import qualified Test.Tasty as Tasty
-import qualified Test.Tasty.Hspec as Hspec
-
-main :: IO ()
-main = do
-  tests <- Hspec.testSpec "rattletrap" spec
-  Tasty.defaultMain tests
-
-spec :: Hspec.Spec
-spec =
-  Hspec.describe
-    "Rattletrap"
-    (mapM_ (\(uuid, description) -> itCanRoundTrip uuid description) replays)
-
-itCanRoundTrip :: String -> String -> Hspec.Spec
-itCanRoundTrip uuid description =
-  Hspec.it (specName uuid description) (specBody uuid)
-
-specName :: String -> String -> String
-specName uuid description = unwords [take 4 uuid, description]
-
-specBody :: String -> IO ()
-specBody uuid = do
-  let inputFile = pathToReplay uuid
-  input <- ByteString.readFile inputFile
-  Temp.withSystemTempDirectory
-    "replay-"
-    (\directory -> do
-       let jsonFile = FilePath.combine directory "replay.json"
-       Rattletrap.mainWithArgs ["decode", inputFile, jsonFile]
-       let outputFile = FilePath.combine directory "output.replay"
-       Rattletrap.mainWithArgs ["encode", jsonFile, outputFile]
-       output <- ByteString.readFile outputFile
-       Monad.unless
-         (output == input)
-         (Hspec.expectationFailure "output does not match input"))
-
-pathToReplay :: String -> FilePath
-pathToReplay uuid =
-  FilePath.joinPath ["test", "replays", FilePath.addExtension uuid ".replay"]
-
-replays :: [(String, String)]
-replays =
-  [ ("00080014003600090000036E0F65CCEB", "a flip time")
-  , ("07E925B1423653D44CB8B4B2524792C1", "a game mode before Neo Tokyo")
-  , ("0AD21FEE4A512906BEB6C98136AAF49A", "some Latin-1 text")
-  , ("1205D96C4D819800927791820096CD49", "rumble mode")
-  , ("160CA83E41083BFD8E6315B4BFCA0561", "a dedicated server IP")
-  , ("18D6738D415B70B5BE4C299588D3C141", "an online loadout attribute")
-  , ("1A126AC24CAA0DB0E98835BD960B8AF8", "overtime")
-  , ("1BC2D01444ACE577D01E988EADD4DFD0", "no padding after the frames")
-  , ("1D1DE97D4941C86E43FE0093563DB621", "a camera pitch")
-  , ("1EF90FCC4F719F606A5327B3CDD782A4", "a private hoops match")
-  , ("1F3798E540B0C37A946561ABBB3037F9", "splitscreen players")
-  , ("211466D04B983F5A33CC2FA1D5928672", "a match save")
-  , ("22BACD794ABE7B92E50E9CBDBD9C59CE", "a vote to forfeit")
-  , ("27B6A7B64553F0F685874584F96BAB1B", "some UTF-16 text")
-  , ("29F582C34A65EB34D358A784CBE3C189", "frames")
-  , ("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")
-  , ("6F7CFCD24638F8A6567AB3A8B9958A90", "a map with numbers")
-  , ("7109EB9846D303E54B7ACBA792036213", "a boost modifier")
-  , ("7BF6073F4614CE0A438994B9A260DA6A", "an online loadouts attribute")
-  , ("89CBA30E46FA5385BDD35DA4285D4D2E", "remote user data")
-  , ("9704208245D7DD851F2FB2BC7DFD9AC3", "a batarang")
-  , ("98E58A904D713F2DE202358E8573265D", "a player using behind view")
-  , ("A128B3AB45D5A18E3EF9CF93C9576BCE", "a round count down")
-  , ("A52F804845573D8DA65E97BF59026A43", "some more mutators")
-  , ("A6711CE74272B2E663DCC9A200A218E3", "a waiting player")
-  , ("A7F001A1417A19BFA8C90990D8F7C2FF", "a ready attribute")
-  , ("B82DDB624C393A4A425E68AB40DC2450", "a vote actor and trailing bits")
-  , ("B9F9B87D4A9D0A3D25D4EC91C0401DE2", "a party leader")
-  , ("C14F7E0E4D9B5E6BE9AD5D8ED56B174C", "some mutators")
-  , ("C8372B1345B1803DEF039F815DBD802D", "a spectator")
-  , ("D428F81646A98C25902CE988AE5C14C8", "a private hockey match")
-  , ("D7FB197A451D69075A0C99A2F49A4053", "an explosion attribute")
-  , ("DCB3A6B94A9DBE46FDE5EAA9B012F6C8", "a pawn type attribute")
-  , ("EAE311E84BA35B590A6FDBA6DD4F2FEB", "an actor/object ID collision")
-  , ("EAE8DADA4BB2DC5422792C9B4A67392D", "custom team colors")
-  , ("F299F176491554B11E34AB91CA76B2CE", "a location attribute")
-  , ("F7B9E14545C7467B89A00895980FCD73", "a hockey game event")
-  , ("F811C1D24888015E23B598AD8628C742", "no frames")
-  , ("FDC79DA84DD463D4BCCE6B892829AC88", "an MVP")
-  ]
diff --git a/test/replays/00080014003600090000036E0F65CCEB.replay b/test/replays/00080014003600090000036E0F65CCEB.replay
deleted file mode 100644
Binary files a/test/replays/00080014003600090000036E0F65CCEB.replay and /dev/null differ
diff --git a/test/replays/07E925B1423653D44CB8B4B2524792C1.replay b/test/replays/07E925B1423653D44CB8B4B2524792C1.replay
deleted file mode 100644
Binary files a/test/replays/07E925B1423653D44CB8B4B2524792C1.replay and /dev/null differ
diff --git a/test/replays/0AD21FEE4A512906BEB6C98136AAF49A.replay b/test/replays/0AD21FEE4A512906BEB6C98136AAF49A.replay
deleted file mode 100644
Binary files a/test/replays/0AD21FEE4A512906BEB6C98136AAF49A.replay and /dev/null differ
diff --git a/test/replays/1205D96C4D819800927791820096CD49.replay b/test/replays/1205D96C4D819800927791820096CD49.replay
deleted file mode 100644
Binary files a/test/replays/1205D96C4D819800927791820096CD49.replay and /dev/null differ
diff --git a/test/replays/160CA83E41083BFD8E6315B4BFCA0561.replay b/test/replays/160CA83E41083BFD8E6315B4BFCA0561.replay
deleted file mode 100644
Binary files a/test/replays/160CA83E41083BFD8E6315B4BFCA0561.replay and /dev/null differ
diff --git a/test/replays/18D6738D415B70B5BE4C299588D3C141.replay b/test/replays/18D6738D415B70B5BE4C299588D3C141.replay
deleted file mode 100644
Binary files a/test/replays/18D6738D415B70B5BE4C299588D3C141.replay and /dev/null differ
diff --git a/test/replays/1A126AC24CAA0DB0E98835BD960B8AF8.replay b/test/replays/1A126AC24CAA0DB0E98835BD960B8AF8.replay
deleted file mode 100644
Binary files a/test/replays/1A126AC24CAA0DB0E98835BD960B8AF8.replay and /dev/null differ
diff --git a/test/replays/1BC2D01444ACE577D01E988EADD4DFD0.replay b/test/replays/1BC2D01444ACE577D01E988EADD4DFD0.replay
deleted file mode 100644
Binary files a/test/replays/1BC2D01444ACE577D01E988EADD4DFD0.replay and /dev/null differ
diff --git a/test/replays/1D1DE97D4941C86E43FE0093563DB621.replay b/test/replays/1D1DE97D4941C86E43FE0093563DB621.replay
deleted file mode 100644
Binary files a/test/replays/1D1DE97D4941C86E43FE0093563DB621.replay and /dev/null differ
diff --git a/test/replays/1EF90FCC4F719F606A5327B3CDD782A4.replay b/test/replays/1EF90FCC4F719F606A5327B3CDD782A4.replay
deleted file mode 100644
Binary files a/test/replays/1EF90FCC4F719F606A5327B3CDD782A4.replay and /dev/null differ
diff --git a/test/replays/1F3798E540B0C37A946561ABBB3037F9.replay b/test/replays/1F3798E540B0C37A946561ABBB3037F9.replay
deleted file mode 100644
Binary files a/test/replays/1F3798E540B0C37A946561ABBB3037F9.replay and /dev/null differ
diff --git a/test/replays/211466D04B983F5A33CC2FA1D5928672.replay b/test/replays/211466D04B983F5A33CC2FA1D5928672.replay
deleted file mode 100644
Binary files a/test/replays/211466D04B983F5A33CC2FA1D5928672.replay and /dev/null differ
diff --git a/test/replays/22BACD794ABE7B92E50E9CBDBD9C59CE.replay b/test/replays/22BACD794ABE7B92E50E9CBDBD9C59CE.replay
deleted file mode 100644
Binary files a/test/replays/22BACD794ABE7B92E50E9CBDBD9C59CE.replay and /dev/null differ
diff --git a/test/replays/27B6A7B64553F0F685874584F96BAB1B.replay b/test/replays/27B6A7B64553F0F685874584F96BAB1B.replay
deleted file mode 100644
Binary files a/test/replays/27B6A7B64553F0F685874584F96BAB1B.replay and /dev/null differ
diff --git a/test/replays/29F582C34A65EB34D358A784CBE3C189.replay b/test/replays/29F582C34A65EB34D358A784CBE3C189.replay
deleted file mode 100644
Binary files a/test/replays/29F582C34A65EB34D358A784CBE3C189.replay and /dev/null differ
diff --git a/test/replays/372DBFCA4BDB340E4357B6BD43032802.replay b/test/replays/372DBFCA4BDB340E4357B6BD43032802.replay
deleted file mode 100644
Binary files a/test/replays/372DBFCA4BDB340E4357B6BD43032802.replay and /dev/null differ
diff --git a/test/replays/387F059C47C09E253C875CA990EFD9F2.replay b/test/replays/387F059C47C09E253C875CA990EFD9F2.replay
deleted file mode 100644
Binary files a/test/replays/387F059C47C09E253C875CA990EFD9F2.replay and /dev/null differ
diff --git a/test/replays/3EA147DD485B8DD24810689A7A989E44.replay b/test/replays/3EA147DD485B8DD24810689A7A989E44.replay
deleted file mode 100644
Binary files a/test/replays/3EA147DD485B8DD24810689A7A989E44.replay and /dev/null differ
diff --git a/test/replays/4126861E477F4A03DE2A4080374D7908.replay b/test/replays/4126861E477F4A03DE2A4080374D7908.replay
deleted file mode 100644
Binary files a/test/replays/4126861E477F4A03DE2A4080374D7908.replay and /dev/null differ
diff --git a/test/replays/42F0D8DA4FC89AE7B80FCAB7F637A8EA.replay b/test/replays/42F0D8DA4FC89AE7B80FCAB7F637A8EA.replay
deleted file mode 100644
Binary files a/test/replays/42F0D8DA4FC89AE7B80FCAB7F637A8EA.replay and /dev/null differ
diff --git a/test/replays/504ED825482186E771FAA9B642CE5CE4.replay b/test/replays/504ED825482186E771FAA9B642CE5CE4.replay
deleted file mode 100644
Binary files a/test/replays/504ED825482186E771FAA9B642CE5CE4.replay and /dev/null differ
diff --git a/test/replays/52AA67F94090C19D33C5009E54D31FE4.replay b/test/replays/52AA67F94090C19D33C5009E54D31FE4.replay
deleted file mode 100644
Binary files a/test/replays/52AA67F94090C19D33C5009E54D31FE4.replay and /dev/null differ
diff --git a/test/replays/540DA764423C8FB24EB9D486D982F16F.replay b/test/replays/540DA764423C8FB24EB9D486D982F16F.replay
deleted file mode 100644
Binary files a/test/replays/540DA764423C8FB24EB9D486D982F16F.replay and /dev/null differ
diff --git a/test/replays/551CA4D44FF2B86015DE44A6B5790D4C.replay b/test/replays/551CA4D44FF2B86015DE44A6B5790D4C.replay
deleted file mode 100644
Binary files a/test/replays/551CA4D44FF2B86015DE44A6B5790D4C.replay and /dev/null differ
diff --git a/test/replays/6688EEE34BFEB3EC3A9E3283098CC712.replay b/test/replays/6688EEE34BFEB3EC3A9E3283098CC712.replay
deleted file mode 100644
Binary files a/test/replays/6688EEE34BFEB3EC3A9E3283098CC712.replay and /dev/null differ
diff --git a/test/replays/6D1B06D844A5BB91B81FD4B5B28F08BA.replay b/test/replays/6D1B06D844A5BB91B81FD4B5B28F08BA.replay
deleted file mode 100644
Binary files a/test/replays/6D1B06D844A5BB91B81FD4B5B28F08BA.replay and /dev/null differ
diff --git a/test/replays/6F7CFCD24638F8A6567AB3A8B9958A90.replay b/test/replays/6F7CFCD24638F8A6567AB3A8B9958A90.replay
deleted file mode 100644
Binary files a/test/replays/6F7CFCD24638F8A6567AB3A8B9958A90.replay and /dev/null differ
diff --git a/test/replays/7109EB9846D303E54B7ACBA792036213.replay b/test/replays/7109EB9846D303E54B7ACBA792036213.replay
deleted file mode 100644
Binary files a/test/replays/7109EB9846D303E54B7ACBA792036213.replay and /dev/null differ
diff --git a/test/replays/7BF6073F4614CE0A438994B9A260DA6A.replay b/test/replays/7BF6073F4614CE0A438994B9A260DA6A.replay
deleted file mode 100644
Binary files a/test/replays/7BF6073F4614CE0A438994B9A260DA6A.replay and /dev/null differ
diff --git a/test/replays/89CBA30E46FA5385BDD35DA4285D4D2E.replay b/test/replays/89CBA30E46FA5385BDD35DA4285D4D2E.replay
deleted file mode 100644
Binary files a/test/replays/89CBA30E46FA5385BDD35DA4285D4D2E.replay and /dev/null differ
diff --git a/test/replays/9704208245D7DD851F2FB2BC7DFD9AC3.replay b/test/replays/9704208245D7DD851F2FB2BC7DFD9AC3.replay
deleted file mode 100644
Binary files a/test/replays/9704208245D7DD851F2FB2BC7DFD9AC3.replay and /dev/null differ
diff --git a/test/replays/98E58A904D713F2DE202358E8573265D.replay b/test/replays/98E58A904D713F2DE202358E8573265D.replay
deleted file mode 100644
Binary files a/test/replays/98E58A904D713F2DE202358E8573265D.replay and /dev/null differ
diff --git a/test/replays/A128B3AB45D5A18E3EF9CF93C9576BCE.replay b/test/replays/A128B3AB45D5A18E3EF9CF93C9576BCE.replay
deleted file mode 100644
Binary files a/test/replays/A128B3AB45D5A18E3EF9CF93C9576BCE.replay and /dev/null differ
diff --git a/test/replays/A52F804845573D8DA65E97BF59026A43.replay b/test/replays/A52F804845573D8DA65E97BF59026A43.replay
deleted file mode 100644
Binary files a/test/replays/A52F804845573D8DA65E97BF59026A43.replay and /dev/null differ
diff --git a/test/replays/A6711CE74272B2E663DCC9A200A218E3.replay b/test/replays/A6711CE74272B2E663DCC9A200A218E3.replay
deleted file mode 100644
Binary files a/test/replays/A6711CE74272B2E663DCC9A200A218E3.replay and /dev/null differ
diff --git a/test/replays/A7F001A1417A19BFA8C90990D8F7C2FF.replay b/test/replays/A7F001A1417A19BFA8C90990D8F7C2FF.replay
deleted file mode 100644
Binary files a/test/replays/A7F001A1417A19BFA8C90990D8F7C2FF.replay and /dev/null differ
diff --git a/test/replays/B82DDB624C393A4A425E68AB40DC2450.replay b/test/replays/B82DDB624C393A4A425E68AB40DC2450.replay
deleted file mode 100644
Binary files a/test/replays/B82DDB624C393A4A425E68AB40DC2450.replay and /dev/null differ
diff --git a/test/replays/B9F9B87D4A9D0A3D25D4EC91C0401DE2.replay b/test/replays/B9F9B87D4A9D0A3D25D4EC91C0401DE2.replay
deleted file mode 100644
Binary files a/test/replays/B9F9B87D4A9D0A3D25D4EC91C0401DE2.replay and /dev/null differ
diff --git a/test/replays/C14F7E0E4D9B5E6BE9AD5D8ED56B174C.replay b/test/replays/C14F7E0E4D9B5E6BE9AD5D8ED56B174C.replay
deleted file mode 100644
Binary files a/test/replays/C14F7E0E4D9B5E6BE9AD5D8ED56B174C.replay and /dev/null differ
diff --git a/test/replays/C8372B1345B1803DEF039F815DBD802D.replay b/test/replays/C8372B1345B1803DEF039F815DBD802D.replay
deleted file mode 100644
Binary files a/test/replays/C8372B1345B1803DEF039F815DBD802D.replay and /dev/null differ
diff --git a/test/replays/D428F81646A98C25902CE988AE5C14C8.replay b/test/replays/D428F81646A98C25902CE988AE5C14C8.replay
deleted file mode 100644
Binary files a/test/replays/D428F81646A98C25902CE988AE5C14C8.replay and /dev/null differ
diff --git a/test/replays/D7FB197A451D69075A0C99A2F49A4053.replay b/test/replays/D7FB197A451D69075A0C99A2F49A4053.replay
deleted file mode 100644
Binary files a/test/replays/D7FB197A451D69075A0C99A2F49A4053.replay and /dev/null differ
diff --git a/test/replays/DCB3A6B94A9DBE46FDE5EAA9B012F6C8.replay b/test/replays/DCB3A6B94A9DBE46FDE5EAA9B012F6C8.replay
deleted file mode 100644
Binary files a/test/replays/DCB3A6B94A9DBE46FDE5EAA9B012F6C8.replay and /dev/null differ
diff --git a/test/replays/EAE311E84BA35B590A6FDBA6DD4F2FEB.replay b/test/replays/EAE311E84BA35B590A6FDBA6DD4F2FEB.replay
deleted file mode 100644
Binary files a/test/replays/EAE311E84BA35B590A6FDBA6DD4F2FEB.replay and /dev/null differ
diff --git a/test/replays/EAE8DADA4BB2DC5422792C9B4A67392D.replay b/test/replays/EAE8DADA4BB2DC5422792C9B4A67392D.replay
deleted file mode 100644
Binary files a/test/replays/EAE8DADA4BB2DC5422792C9B4A67392D.replay and /dev/null differ
diff --git a/test/replays/F299F176491554B11E34AB91CA76B2CE.replay b/test/replays/F299F176491554B11E34AB91CA76B2CE.replay
deleted file mode 100644
Binary files a/test/replays/F299F176491554B11E34AB91CA76B2CE.replay and /dev/null differ
diff --git a/test/replays/F7B9E14545C7467B89A00895980FCD73.replay b/test/replays/F7B9E14545C7467B89A00895980FCD73.replay
deleted file mode 100644
Binary files a/test/replays/F7B9E14545C7467B89A00895980FCD73.replay and /dev/null differ
diff --git a/test/replays/F811C1D24888015E23B598AD8628C742.replay b/test/replays/F811C1D24888015E23B598AD8628C742.replay
deleted file mode 100644
Binary files a/test/replays/F811C1D24888015E23B598AD8628C742.replay and /dev/null differ
diff --git a/test/replays/FDC79DA84DD463D4BCCE6B892829AC88.replay b/test/replays/FDC79DA84DD463D4BCCE6B892829AC88.replay
deleted file mode 100644
Binary files a/test/replays/FDC79DA84DD463D4BCCE6B892829AC88.replay and /dev/null differ
