diff --git a/README.markdown b/README.markdown
--- a/README.markdown
+++ b/README.markdown
@@ -2,6 +2,7 @@
 
 [![Version badge][]][version]
 [![Build badge][]][build]
+[![Docker badge][]][docker]
 
 Rattletrap parses and generates [Rocket League][] replays. Parsing replays can
 be used to analyze data in order to collect high-level statistics like players
@@ -23,7 +24,7 @@
 
 Get Rattletrap by downloading [the latest release][] for your platform.
 
-Rattletrap is also available as [a Docker image][].
+Rattletrap is also available as [a Docker image][docker].
 
 To build Rattletrap from source, install [Stack][]. Then run
 `stack --resolver lts-14.25 install rattletrap`.
@@ -125,10 +126,12 @@
 ```
 
 [Rattletrap]: https://github.com/tfausak/rattletrap
-[Version badge]: https://img.shields.io/hackage/v/rattletrap.svg?logo=haskell&label=version&color=brightgreen
+[Version badge]: https://img.shields.io/hackage/v/rattletrap.svg?logo=haskell
 [version]: https://hackage.haskell.org/package/rattletrap
 [Build badge]: https://github.com/tfausak/rattletrap/workflows/ci/badge.svg
 [build]: https://github.com/tfausak/rattletrap/actions
+[Docker badge]: https://img.shields.io/docker/v/taylorfausak/rattletrap?label=docker&logo=docker&logoColor=white
+[docker]: https://hub.docker.com/r/taylorfausak/rattletrap
 [Rocket League]: https://www.rocketleague.com
 [1.92]: https://www.rocketleague.com/news/patch-notes-v1-92/
 [Ball Chasing]: https://ballchasing.com
@@ -136,5 +139,4 @@
 [C# parser]: https://github.com/jjbott/RocketLeagueReplayParser
 [Rust parser]: https://github.com/nickbabcock/rrrocket
 [the latest release]: https://github.com/tfausak/rattletrap/releases/latest
-[a Docker image]: https://hub.docker.com/r/taylorfausak/rattletrap
 [Stack]: https://docs.haskellstack.org/en/stable/README/
diff --git a/rattletrap.cabal b/rattletrap.cabal
--- a/rattletrap.cabal
+++ b/rattletrap.cabal
@@ -1,7 +1,7 @@
 cabal-version: 2.0
 
 name: rattletrap
-version: 10.0.7
+version: 11.0.0
 synopsis: Parse and generate Rocket League replays.
 description: Rattletrap parses and generates Rocket League replays.
 
@@ -23,9 +23,7 @@
     , aeson >= 1.5.5 && < 1.6
     , aeson-pretty >= 0.8.8 && < 0.9
     , array >= 0.5.4 && < 0.6
-    , binary >= 0.8.8 && < 0.9
     , bytestring >= 0.10.12 && < 0.11
-    , caerbannog >= 0.6.0 && < 0.7
     , containers >= 0.6.2 && < 0.7
     , filepath >= 1.4.2 && < 1.5
     , http-client >= 0.6.4 && < 0.8
@@ -37,8 +35,10 @@
   exposed-modules:
     Paths_rattletrap
     Rattletrap
+    Rattletrap.BitBuilder
     Rattletrap.BitGet
     Rattletrap.BitPut
+    Rattletrap.BitString
     Rattletrap.ByteGet
     Rattletrap.BytePut
     Rattletrap.Console.Config
@@ -107,14 +107,29 @@
     Rattletrap.Type.I8
     Rattletrap.Type.Initialization
     Rattletrap.Type.Int8Vector
-    Rattletrap.Type.KeyFrame
+    Rattletrap.Type.Keyframe
     Rattletrap.Type.List
     Rattletrap.Type.Mark
     Rattletrap.Type.Message
     Rattletrap.Type.Property
+    Rattletrap.Type.Property.Array
+    Rattletrap.Type.Property.Bool
+    Rattletrap.Type.Property.Byte
+    Rattletrap.Type.Property.Float
+    Rattletrap.Type.Property.Int
+    Rattletrap.Type.Property.Name
+    Rattletrap.Type.Property.QWord
+    Rattletrap.Type.Property.Str
     Rattletrap.Type.PropertyValue
     Rattletrap.Type.Quaternion
     Rattletrap.Type.RemoteId
+    Rattletrap.Type.RemoteId.Epic
+    Rattletrap.Type.RemoteId.PlayStation
+    Rattletrap.Type.RemoteId.PsyNet
+    Rattletrap.Type.RemoteId.Splitscreen
+    Rattletrap.Type.RemoteId.Steam
+    Rattletrap.Type.RemoteId.Switch
+    Rattletrap.Type.RemoteId.Xbox
     Rattletrap.Type.Replay
     Rattletrap.Type.Replication
     Rattletrap.Type.Replication.Destroyed
diff --git a/src/exe/Main.hs b/src/exe/Main.hs
--- a/src/exe/Main.hs
+++ b/src/exe/Main.hs
@@ -1,5 +1,4 @@
-module Main
-  ( main
-  ) where
+import qualified Rattletrap
 
-import Rattletrap (main)
+main :: IO ()
+main = Rattletrap.main
diff --git a/src/lib/Rattletrap/BitBuilder.hs b/src/lib/Rattletrap/BitBuilder.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/BitBuilder.hs
@@ -0,0 +1,33 @@
+{- hlint ignore "Avoid restricted extensions" -}
+{-# LANGUAGE BangPatterns #-}
+
+module Rattletrap.BitBuilder where
+
+import qualified Data.Bits as Bits
+import qualified Data.ByteString.Builder as Builder
+import qualified Data.Word as Word
+
+data BitBuilder = BitBuilder
+  { buffer :: Word.Word8
+  , builder :: Builder.Builder
+  , offset :: Int
+  }
+
+empty :: BitBuilder
+empty = BitBuilder { buffer = 0x00, builder = mempty, offset = 0 }
+
+push :: Bool -> BitBuilder -> BitBuilder
+push b x =
+  let !newBuffer = if b then Bits.setBit (buffer x) (offset x) else buffer x
+  in
+    if offset x == 7
+      then BitBuilder
+        { buffer = 0x00
+        , builder = builder x <> Builder.word8 newBuffer
+        , offset = 0
+        }
+      else x { buffer = newBuffer, offset = offset x + 1 }
+
+toBuilder :: BitBuilder -> Builder.Builder
+toBuilder x =
+  if offset x == 0 then builder x else builder x <> Builder.word8 (buffer x)
diff --git a/src/lib/Rattletrap/BitGet.hs b/src/lib/Rattletrap/BitGet.hs
--- a/src/lib/Rattletrap/BitGet.hs
+++ b/src/lib/Rattletrap/BitGet.hs
@@ -1,48 +1,45 @@
 module Rattletrap.BitGet where
 
 import qualified Control.Monad as Monad
-import qualified Data.Binary.Bits.Get as BinaryBits
-import qualified Data.Binary.Get as Binary
 import qualified Data.Bits as Bits
 import qualified Data.ByteString as ByteString
-import qualified Data.ByteString.Lazy as LazyByteString
-import qualified Data.Word as Word
+import qualified Data.Functor.Identity as Identity
+import qualified Rattletrap.BitString as BitString
 import qualified Rattletrap.ByteGet as ByteGet
 import qualified Rattletrap.Get as Get
-import qualified Rattletrap.Utility.Bytes as Utility
 
-type BitGet = BinaryBits.BitGet
+type BitGet = Get.Get BitString.BitString Identity.Identity
 
 toByteGet :: BitGet a -> ByteGet.ByteGet a
-toByteGet = binaryGetToByteGet . BinaryBits.runBitGet
-
-binaryGetToByteGet :: Binary.Get a -> ByteGet.ByteGet a
-binaryGetToByteGet g = do
+toByteGet g = do
   s1 <- Get.get
-  case Binary.runGetOrFail g $ LazyByteString.fromStrict s1 of
-    Left (_, _, x) -> fail x
-    Right (s2, _, x) -> do
-      Get.put $ LazyByteString.toStrict s2
+  case Identity.runIdentity . Get.run g $ BitString.fromByteString s1 of
+    Left e -> fail e
+    Right (s2, x) -> do
+      Get.put $ BitString.byteString s2
       pure x
 
 fromByteGet :: ByteGet.ByteGet a -> Int -> BitGet a
 fromByteGet f n = do
-  x <- BinaryBits.getByteString n
-  either fail pure . ByteGet.run f $ Utility.reverseBytes x
+  x <- byteString n
+  either fail pure $ ByteGet.run f x
 
 bits :: Bits.Bits a => Int -> BitGet a
-bits n =
-  foldr
-      (\bit x -> let y = Bits.shiftL x 1 in if bit then Bits.setBit y 0 else y
-      )
-      Bits.zeroBits
-    <$> Monad.replicateM n bool
+bits n = do
+  let
+    f :: Bits.Bits a => Bool -> a -> a
+    f bit x = let y = Bits.shiftL x 1 in if bit then Bits.setBit y 0 else y
+  xs <- Monad.replicateM n bool
+  pure $ foldr f Bits.zeroBits xs
 
 bool :: BitGet Bool
-bool = BinaryBits.getBool
+bool = do
+  s1 <- Get.get
+  case BitString.pop s1 of
+    Nothing -> fail "BitGet.bool"
+    Just (x, s2) -> do
+      Get.put s2
+      pure x
 
 byteString :: Int -> BitGet ByteString.ByteString
-byteString = BinaryBits.getByteString
-
-word8 :: Int -> BitGet Word.Word8
-word8 = BinaryBits.getWord8
+byteString n = fmap ByteString.pack . Monad.replicateM n $ bits 8
diff --git a/src/lib/Rattletrap/BitPut.hs b/src/lib/Rattletrap/BitPut.hs
--- a/src/lib/Rattletrap/BitPut.hs
+++ b/src/lib/Rattletrap/BitPut.hs
@@ -1,41 +1,32 @@
 module Rattletrap.BitPut where
 
-import qualified Data.Binary.Bits.Put as BinaryBits
-import qualified Data.Binary.Put as Binary
 import qualified Data.Bits as Bits
 import qualified Data.ByteString as ByteString
-import qualified Data.Word as Word
+import qualified Rattletrap.BitBuilder as BitBuilder
 import qualified Rattletrap.BytePut as BytePut
-import qualified Rattletrap.Utility.Bytes as Utility
 
-newtype BitPut = BitPut (BinaryBits.BitPut ())
+newtype BitPut = BitPut (BitBuilder.BitBuilder -> BitBuilder.BitBuilder)
 
 instance Semigroup BitPut where
-  x <> y = fromBinaryBits $ toBinaryBits x >> toBinaryBits y
+  f1 <> f2 = BitPut $ run f2 . run f1
 
 instance Monoid BitPut where
-  mempty = fromBinaryBits $ pure ()
-
-fromBinaryBits :: BinaryBits.BitPut () -> BitPut
-fromBinaryBits = BitPut
+  mempty = BitPut id
 
-toBinaryBits :: BitPut -> BinaryBits.BitPut ()
-toBinaryBits (BitPut x) = x
+run :: BitPut -> BitBuilder.BitBuilder -> BitBuilder.BitBuilder
+run (BitPut f) = f
 
 toBytePut :: BitPut -> BytePut.BytePut
-toBytePut = Binary.execPut . BinaryBits.runBitPut . toBinaryBits
+toBytePut b = BitBuilder.toBuilder $ run b BitBuilder.empty
 
 fromBytePut :: BytePut.BytePut -> BitPut
-fromBytePut = byteString . Utility.reverseBytes . BytePut.toByteString
+fromBytePut = byteString . BytePut.toByteString
 
 bits :: Bits.Bits a => Int -> a -> BitPut
 bits n x = foldMap (bool . Bits.testBit x) [0 .. n - 1]
 
 bool :: Bool -> BitPut
-bool = fromBinaryBits . BinaryBits.putBool
+bool = BitPut . BitBuilder.push
 
 byteString :: ByteString.ByteString -> BitPut
-byteString = fromBinaryBits . BinaryBits.putByteString
-
-word8 :: Int -> Word.Word8 -> BitPut
-word8 n = fromBinaryBits . BinaryBits.putWord8 n
+byteString = foldMap (bits 8) . ByteString.unpack
diff --git a/src/lib/Rattletrap/BitString.hs b/src/lib/Rattletrap/BitString.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/BitString.hs
@@ -0,0 +1,23 @@
+module Rattletrap.BitString where
+
+import qualified Data.Bits as Bits
+import qualified Data.ByteString as ByteString
+
+data BitString = BitString
+  { byteString :: ByteString.ByteString
+  , offset :: Int
+  }
+  deriving (Eq, Show)
+
+fromByteString :: ByteString.ByteString -> BitString
+fromByteString byteString = BitString { byteString, offset = 0 }
+
+pop :: BitString -> Maybe (Bool, BitString)
+pop old = do
+  (word, byteString) <- ByteString.uncons $ byteString old
+  let
+    bit = Bits.testBit word $ offset old
+    new = if offset old == 7
+      then fromByteString byteString
+      else old { offset = offset old + 1 }
+  pure (bit, new)
diff --git a/src/lib/Rattletrap/ByteGet.hs b/src/lib/Rattletrap/ByteGet.hs
--- a/src/lib/Rattletrap/ByteGet.hs
+++ b/src/lib/Rattletrap/ByteGet.hs
@@ -22,16 +22,16 @@
   pure x
 
 float :: ByteGet Float
-float = Float.castWord32ToFloat <$> word32
+float = fmap Float.castWord32ToFloat word32
 
 int8 :: ByteGet Int.Int8
-int8 = fromIntegral <$> word8
+int8 = fmap fromIntegral word8
 
 int32 :: ByteGet Int.Int32
-int32 = fromIntegral <$> word32
+int32 = fmap fromIntegral word32
 
 int64 :: ByteGet Int.Int64
-int64 = fromIntegral <$> word64
+int64 = fmap fromIntegral word64
 
 remaining :: ByteGet LazyByteString.ByteString
 remaining = do
@@ -40,7 +40,7 @@
   pure $ LazyByteString.fromStrict x
 
 word8 :: ByteGet Word.Word8
-word8 = ByteString.head <$> byteString 1
+word8 = fmap ByteString.head $ byteString 1
 
 word32 :: ByteGet Word.Word32
 word32 = do
diff --git a/src/lib/Rattletrap/Console/Config.hs b/src/lib/Rattletrap/Console/Config.hs
--- a/src/lib/Rattletrap/Console/Config.hs
+++ b/src/lib/Rattletrap/Console/Config.hs
@@ -47,8 +47,8 @@
 getMode :: Config -> Mode.Mode
 getMode config =
   let
-    i = FilePath.takeExtension <$> input config
-    o = FilePath.takeExtension <$> output config
+    i = fmap FilePath.takeExtension $ input config
+    o = fmap FilePath.takeExtension $ output config
   in case (i, o) of
     (Just ".json", _) -> Mode.Encode
     (Just ".replay", _) -> Mode.Decode
diff --git a/src/lib/Rattletrap/Console/Main.hs b/src/lib/Rattletrap/Console/Main.hs
--- a/src/lib/Rattletrap/Console/Main.hs
+++ b/src/lib/Rattletrap/Console/Main.hs
@@ -1,13 +1,10 @@
-module Rattletrap.Console.Main
-  ( main
-  , rattletrap
-  ) where
+module Rattletrap.Console.Main where
 
 import qualified Control.Monad as Monad
-import qualified Data.Aeson as Aeson
-import qualified Data.Aeson.Encode.Pretty as Aeson
+import qualified Data.Bool as Bool
 import qualified Data.ByteString as ByteString
 import qualified Data.ByteString.Lazy as LazyByteString
+import qualified Data.Text as Text
 import qualified Network.HTTP.Client as Client
 import qualified Network.HTTP.Client.TLS as Client
 import qualified Rattletrap.Console.Config as Config
@@ -70,14 +67,19 @@
 import qualified Rattletrap.Type.I8 as I8
 import qualified Rattletrap.Type.Initialization as Initialization
 import qualified Rattletrap.Type.Int8Vector as Int8Vector
-import qualified Rattletrap.Type.KeyFrame as KeyFrame
+import qualified Rattletrap.Type.Keyframe as Keyframe
 import qualified Rattletrap.Type.List as List
 import qualified Rattletrap.Type.Mark as Mark
 import qualified Rattletrap.Type.Message as Message
 import qualified Rattletrap.Type.Property as Property
+import qualified Rattletrap.Type.Property.Array as Property.Array
+import qualified Rattletrap.Type.Property.Byte as Property.Byte
 import qualified Rattletrap.Type.PropertyValue as PropertyValue
 import qualified Rattletrap.Type.Quaternion as Quaternion
 import qualified Rattletrap.Type.RemoteId as RemoteId
+import qualified Rattletrap.Type.RemoteId.PlayStation as RemoteId.PlayStation
+import qualified Rattletrap.Type.RemoteId.PsyNet as RemoteId.PsyNet
+import qualified Rattletrap.Type.RemoteId.Switch as RemoteId.Switch
 import qualified Rattletrap.Type.Replay as Replay
 import qualified Rattletrap.Type.Replication as Replication
 import qualified Rattletrap.Type.Replication.Destroyed as Replication.Destroyed
@@ -127,18 +129,7 @@
   putStrLn Version.string
 
 schemaMain :: Config.Config -> IO ()
-schemaMain config = do
-  let
-    json = Aeson.encodePretty'
-      Aeson.defConfig
-        { Aeson.confCompare = compare
-        , Aeson.confIndent = Aeson.Tab
-        , Aeson.confTrailingNewline = True
-        }
-      schema
-  case Config.output config of
-    Nothing -> LazyByteString.putStr json
-    Just file -> LazyByteString.writeFile file json
+schemaMain config = putOutput config $ encodeJson config schema
 
 defaultMain :: Config.Config -> IO ()
 defaultMain config = do
@@ -148,16 +139,16 @@
   let encode = getEncoder config
   putOutput config (encode replay)
 
-schema :: Aeson.Value
+schema :: Json.Value
 schema =
   let contentSchema = Content.schema $ List.schema Frame.schema
   in
-    Aeson.object
-      [ Json.pair "$schema" "https://json-schema.org/draft-07/schema"
+    Json.object
+      [ Json.pair "$schema" "http://json-schema.org/draft-07/schema"
       , Json.pair "$id" Replay.schemaUrl
       , Json.pair "$ref" "#/definitions/replay"
-      , Json.pair "definitions" . Aeson.object $ fmap
-        (\s -> Schema.name s Aeson..= Schema.json s)
+      , Json.pair "definitions" . Json.object $ fmap
+        (\s -> Json.pair (Text.unpack $ Schema.name s) $ Schema.json s)
         [ Attribute.schema
         , Attribute.AppliedDamage.schema
         , Attribute.Boolean.schema
@@ -214,14 +205,19 @@
         , I8.schema
         , Initialization.schema
         , Int8Vector.schema
-        , KeyFrame.schema
+        , Keyframe.schema
         , List.schema Attribute.Product.schema
         , Mark.schema
         , Message.schema
         , Property.schema
+        , Property.Array.schema Property.schema
+        , Property.Byte.schema
         , PropertyValue.schema Property.schema
         , Quaternion.schema
         , RemoteId.schema
+        , RemoteId.PlayStation.schema
+        , RemoteId.PsyNet.schema
+        , RemoteId.Switch.schema
         , Replay.schema (Section.schema Header.schema)
         . Section.schema
         $ contentSchema
@@ -255,8 +251,7 @@
 
 getEncoder :: Config.Config -> Replay.Replay -> LazyByteString.ByteString
 getEncoder config = case Config.getMode config of
-  Mode.Decode ->
-    if Config.compact config then Aeson.encode else Rattletrap.encodeReplayJson
+  Mode.Decode -> encodeJson config
   Mode.Encode -> Rattletrap.encodeReplayFile $ Config.fast config
 
 getInput :: Config.Config -> IO ByteString.ByteString
@@ -272,6 +267,9 @@
 putOutput :: Config.Config -> LazyByteString.ByteString -> IO ()
 putOutput =
   maybe LazyByteString.putStr LazyByteString.writeFile . Config.output
+
+encodeJson :: Json.ToJSON a => Config.Config -> a -> LazyByteString.ByteString
+encodeJson = Bool.bool Json.encodePretty Json.encode . Config.compact
 
 getConfig :: [String] -> IO Config.Config
 getConfig arguments = do
diff --git a/src/lib/Rattletrap/Type/Attribute.hs b/src/lib/Rattletrap/Type/Attribute.hs
--- a/src/lib/Rattletrap/Type/Attribute.hs
+++ b/src/lib/Rattletrap/Type/Attribute.hs
@@ -57,13 +57,13 @@
 bitGet version classes actors actor = do
   attributes <- lookupAttributeMap classes actors actor
   limit <- lookupAttributeIdLimit attributes actor
-  attribute <- CompressedWord.bitGet limit
-  name_ <- lookupAttributeName classes attributes attribute
-  Attribute attribute name_
-    <$> AttributeValue.bitGet
-          version
-          (ClassAttributeMap.objectMap classes)
-          name_
+  id <- CompressedWord.bitGet limit
+  name <- lookupAttributeName classes attributes id
+  value <- AttributeValue.bitGet
+    version
+    (ClassAttributeMap.objectMap classes)
+    name
+  pure Attribute { id, name, value }
 
 lookupAttributeMap
   :: ClassAttributeMap.ClassAttributeMap
diff --git a/src/lib/Rattletrap/Type/Attribute/AppliedDamage.hs b/src/lib/Rattletrap/Type/Attribute/AppliedDamage.hs
--- a/src/lib/Rattletrap/Type/Attribute/AppliedDamage.hs
+++ b/src/lib/Rattletrap/Type/Attribute/AppliedDamage.hs
@@ -49,9 +49,9 @@
     <> I32.bitPut (unknown4 appliedDamageAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet AppliedDamage
-bitGet version =
-  AppliedDamage
-    <$> U8.bitGet
-    <*> Vector.bitGet version
-    <*> I32.bitGet
-    <*> I32.bitGet
+bitGet version = do
+  unknown1 <- U8.bitGet
+  location <- Vector.bitGet version
+  unknown3 <- I32.bitGet
+  unknown4 <- I32.bitGet
+  pure AppliedDamage { unknown1, location, unknown3, unknown4 }
diff --git a/src/lib/Rattletrap/Type/Attribute/Boolean.hs b/src/lib/Rattletrap/Type/Attribute/Boolean.hs
--- a/src/lib/Rattletrap/Type/Attribute/Boolean.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Boolean.hs
@@ -22,4 +22,6 @@
 bitPut booleanAttribute = BitPut.bool (value booleanAttribute)
 
 bitGet :: BitGet.BitGet Boolean
-bitGet = Boolean <$> BitGet.bool
+bitGet = do
+  value <- BitGet.bool
+  pure Boolean { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/Byte.hs b/src/lib/Rattletrap/Type/Attribute/Byte.hs
--- a/src/lib/Rattletrap/Type/Attribute/Byte.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Byte.hs
@@ -23,4 +23,6 @@
 bitPut byteAttribute = U8.bitPut (value byteAttribute)
 
 bitGet :: BitGet.BitGet Byte
-bitGet = Byte <$> U8.bitGet
+bitGet = do
+  value <- U8.bitGet
+  pure Byte { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/CamSettings.hs b/src/lib/Rattletrap/Type/Attribute/CamSettings.hs
--- a/src/lib/Rattletrap/Type/Attribute/CamSettings.hs
+++ b/src/lib/Rattletrap/Type/Attribute/CamSettings.hs
@@ -6,7 +6,7 @@
 import qualified Rattletrap.Type.F32 as F32
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
-import Rattletrap.Utility.Monad
+import qualified Rattletrap.Utility.Monad as Monad
 
 data CamSettings = CamSettings
   { fov :: F32.F32
@@ -73,16 +73,22 @@
     <> foldMap F32.bitPut (transitionSpeed camSettingsAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet CamSettings
-bitGet version =
-  CamSettings
-    <$> F32.bitGet
-    <*> F32.bitGet
-    <*> F32.bitGet
-    <*> F32.bitGet
-    <*> F32.bitGet
-    <*> F32.bitGet
-    <*> whenMaybe (hasTransitionSpeed version) F32.bitGet
-
-hasTransitionSpeed :: Version.Version -> Bool
-hasTransitionSpeed v =
-  Version.major v >= 868 && Version.minor v >= 20 && Version.patch v >= 0
+bitGet version = do
+  fov <- F32.bitGet
+  height <- F32.bitGet
+  angle <- F32.bitGet
+  distance <- F32.bitGet
+  stiffness <- F32.bitGet
+  swivelSpeed <- F32.bitGet
+  transitionSpeed <- Monad.whenMaybe
+    (Version.atLeast 868 20 0 version)
+    F32.bitGet
+  pure CamSettings
+    { fov
+    , height
+    , angle
+    , distance
+    , stiffness
+    , swivelSpeed
+    , transitionSpeed
+    }
diff --git a/src/lib/Rattletrap/Type/Attribute/ClubColors.hs b/src/lib/Rattletrap/Type/Attribute/ClubColors.hs
--- a/src/lib/Rattletrap/Type/Attribute/ClubColors.hs
+++ b/src/lib/Rattletrap/Type/Attribute/ClubColors.hs
@@ -46,5 +46,9 @@
     <> U8.bitPut (orangeColor clubColorsAttribute)
 
 bitGet :: BitGet.BitGet ClubColors
-bitGet =
-  ClubColors <$> BitGet.bool <*> U8.bitGet <*> BitGet.bool <*> U8.bitGet
+bitGet = do
+  blueFlag <- BitGet.bool
+  blueColor <- U8.bitGet
+  orangeFlag <- BitGet.bool
+  orangeColor <- U8.bitGet
+  pure ClubColors { blueFlag, blueColor, orangeFlag, orangeColor }
diff --git a/src/lib/Rattletrap/Type/Attribute/CustomDemolish.hs b/src/lib/Rattletrap/Type/Attribute/CustomDemolish.hs
--- a/src/lib/Rattletrap/Type/Attribute/CustomDemolish.hs
+++ b/src/lib/Rattletrap/Type/Attribute/CustomDemolish.hs
@@ -44,5 +44,12 @@
     <> Demolish.bitPut (demolish x)
 
 bitGet :: Version.Version -> BitGet.BitGet CustomDemolish
-bitGet version =
-  CustomDemolish <$> BitGet.bool <*> I32.bitGet <*> Demolish.bitGet version
+bitGet version = do
+  flag <- BitGet.bool
+  id <- I32.bitGet
+  demolish <- Demolish.bitGet version
+  pure CustomDemolish
+    { flag
+    , Rattletrap.Type.Attribute.CustomDemolish.id
+    , demolish
+    }
diff --git a/src/lib/Rattletrap/Type/Attribute/DamageState.hs b/src/lib/Rattletrap/Type/Attribute/DamageState.hs
--- a/src/lib/Rattletrap/Type/Attribute/DamageState.hs
+++ b/src/lib/Rattletrap/Type/Attribute/DamageState.hs
@@ -66,11 +66,18 @@
     <> BitPut.bool (unknown6 damageStateAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet DamageState
-bitGet version =
-  DamageState
-    <$> U8.bitGet
-    <*> BitGet.bool
-    <*> I32.bitGet
-    <*> Vector.bitGet version
-    <*> BitGet.bool
-    <*> BitGet.bool
+bitGet version = do
+  unknown1 <- U8.bitGet
+  unknown2 <- BitGet.bool
+  unknown3 <- I32.bitGet
+  unknown4 <- Vector.bitGet version
+  unknown5 <- BitGet.bool
+  unknown6 <- BitGet.bool
+  pure DamageState
+    { unknown1
+    , unknown2
+    , unknown3
+    , unknown4
+    , unknown5
+    , unknown6
+    }
diff --git a/src/lib/Rattletrap/Type/Attribute/Demolish.hs b/src/lib/Rattletrap/Type/Attribute/Demolish.hs
--- a/src/lib/Rattletrap/Type/Attribute/Demolish.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Demolish.hs
@@ -65,11 +65,18 @@
     <> Vector.bitPut (victimVelocity demolishAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet Demolish
-bitGet version =
-  Demolish
-    <$> BitGet.bool
-    <*> U32.bitGet
-    <*> BitGet.bool
-    <*> U32.bitGet
-    <*> Vector.bitGet version
-    <*> Vector.bitGet version
+bitGet version = do
+  attackerFlag <- BitGet.bool
+  attackerActorId <- U32.bitGet
+  victimFlag <- BitGet.bool
+  victimActorId <- U32.bitGet
+  attackerVelocity <- Vector.bitGet version
+  victimVelocity <- Vector.bitGet version
+  pure Demolish
+    { attackerFlag
+    , attackerActorId
+    , victimFlag
+    , victimActorId
+    , attackerVelocity
+    , victimVelocity
+    }
diff --git a/src/lib/Rattletrap/Type/Attribute/Enum.hs b/src/lib/Rattletrap/Type/Attribute/Enum.hs
--- a/src/lib/Rattletrap/Type/Attribute/Enum.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Enum.hs
@@ -24,4 +24,6 @@
 bitPut enumAttribute = BitPut.bits 11 (value enumAttribute)
 
 bitGet :: BitGet.BitGet Enum
-bitGet = Enum <$> BitGet.bits 11
+bitGet = do
+  value <- BitGet.bits 11
+  pure Enum { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/Explosion.hs b/src/lib/Rattletrap/Type/Attribute/Explosion.hs
--- a/src/lib/Rattletrap/Type/Attribute/Explosion.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Explosion.hs
@@ -43,5 +43,8 @@
     <> Vector.bitPut (location explosionAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet Explosion
-bitGet version =
-  Explosion <$> BitGet.bool <*> I32.bitGet <*> Vector.bitGet version
+bitGet version = do
+  flag <- BitGet.bool
+  actorId <- I32.bitGet
+  location <- Vector.bitGet version
+  pure Explosion { flag, actorId, location }
diff --git a/src/lib/Rattletrap/Type/Attribute/ExtendedExplosion.hs b/src/lib/Rattletrap/Type/Attribute/ExtendedExplosion.hs
--- a/src/lib/Rattletrap/Type/Attribute/ExtendedExplosion.hs
+++ b/src/lib/Rattletrap/Type/Attribute/ExtendedExplosion.hs
@@ -34,5 +34,7 @@
 bitPut x = Explosion.bitPut (explosion x) <> FlaggedInt.bitPut (unknown x)
 
 bitGet :: Version.Version -> BitGet.BitGet ExtendedExplosion
-bitGet version =
-  ExtendedExplosion <$> Explosion.bitGet version <*> FlaggedInt.bitGet
+bitGet version = do
+  explosion <- Explosion.bitGet version
+  unknown <- FlaggedInt.bitGet
+  pure ExtendedExplosion { explosion, unknown }
diff --git a/src/lib/Rattletrap/Type/Attribute/FlaggedByte.hs b/src/lib/Rattletrap/Type/Attribute/FlaggedByte.hs
--- a/src/lib/Rattletrap/Type/Attribute/FlaggedByte.hs
+++ b/src/lib/Rattletrap/Type/Attribute/FlaggedByte.hs
@@ -33,4 +33,7 @@
   <> U8.bitPut (byte flaggedByteAttribute)
 
 bitGet :: BitGet.BitGet FlaggedByte
-bitGet = FlaggedByte <$> BitGet.bool <*> U8.bitGet
+bitGet = do
+  flag <- BitGet.bool
+  byte <- U8.bitGet
+  pure FlaggedByte { flag, byte }
diff --git a/src/lib/Rattletrap/Type/Attribute/FlaggedInt.hs b/src/lib/Rattletrap/Type/Attribute/FlaggedInt.hs
--- a/src/lib/Rattletrap/Type/Attribute/FlaggedInt.hs
+++ b/src/lib/Rattletrap/Type/Attribute/FlaggedInt.hs
@@ -32,4 +32,7 @@
   <> I32.bitPut (int flaggedIntAttribute)
 
 bitGet :: BitGet.BitGet FlaggedInt
-bitGet = FlaggedInt <$> BitGet.bool <*> I32.bitGet
+bitGet = do
+  flag <- BitGet.bool
+  int <- I32.bitGet
+  pure FlaggedInt { flag, int }
diff --git a/src/lib/Rattletrap/Type/Attribute/Float.hs b/src/lib/Rattletrap/Type/Attribute/Float.hs
--- a/src/lib/Rattletrap/Type/Attribute/Float.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Float.hs
@@ -24,4 +24,6 @@
 bitPut floatAttribute = F32.bitPut (value floatAttribute)
 
 bitGet :: BitGet.BitGet Float
-bitGet = Float <$> F32.bitGet
+bitGet = do
+  value <- F32.bitGet
+  pure Float { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/GameMode.hs b/src/lib/Rattletrap/Type/Attribute/GameMode.hs
--- a/src/lib/Rattletrap/Type/Attribute/GameMode.hs
+++ b/src/lib/Rattletrap/Type/Attribute/GameMode.hs
@@ -35,15 +35,10 @@
 
 bitPut :: GameMode -> BitPut.BitPut
 bitPut gameModeAttribute = do
-  BitPut.word8 (numBits gameModeAttribute) (word gameModeAttribute)
+  BitPut.bits (numBits gameModeAttribute) (word gameModeAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet GameMode
-bitGet version =
-  GameMode (numBits_ version) <$> BitGet.word8 (numBits_ version)
-
-numBits_ :: Version.Version -> Int
-numBits_ version = if has8Bits version then 8 else 2
-
-has8Bits :: Version.Version -> Bool
-has8Bits v =
-  Version.major v >= 868 && Version.minor v >= 12 && Version.patch v >= 0
+bitGet version = do
+  let numBits = if Version.atLeast 868 12 0 version then 8 else 2 :: Int
+  word <- BitGet.bits numBits
+  pure GameMode { numBits, word }
diff --git a/src/lib/Rattletrap/Type/Attribute/Int.hs b/src/lib/Rattletrap/Type/Attribute/Int.hs
--- a/src/lib/Rattletrap/Type/Attribute/Int.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Int.hs
@@ -24,4 +24,6 @@
 bitPut intAttribute = I32.bitPut (value intAttribute)
 
 bitGet :: BitGet.BitGet Int
-bitGet = Int <$> I32.bitGet
+bitGet = do
+  value <- I32.bitGet
+  pure Int { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/Int64.hs b/src/lib/Rattletrap/Type/Attribute/Int64.hs
--- a/src/lib/Rattletrap/Type/Attribute/Int64.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Int64.hs
@@ -23,4 +23,6 @@
 putInt64Attribute int64Attribute = I64.bitPut (value int64Attribute)
 
 bitGet :: BitGet.BitGet Int64
-bitGet = Int64 <$> I64.bitGet
+bitGet = do
+  value <- I64.bitGet
+  pure Int64 { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/Loadout.hs b/src/lib/Rattletrap/Type/Attribute/Loadout.hs
--- a/src/lib/Rattletrap/Type/Attribute/Loadout.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Loadout.hs
@@ -6,7 +6,7 @@
 import qualified Rattletrap.Type.U32 as U32
 import qualified Rattletrap.Type.U8 as U8
 import qualified Rattletrap.Utility.Json as Json
-import Rattletrap.Utility.Monad
+import qualified Rattletrap.Utility.Monad as Monad
 
 data Loadout = Loadout
   { version :: U8.U8
@@ -133,21 +133,39 @@
 
 bitGet :: BitGet.BitGet Loadout
 bitGet = do
-  version_ <- U8.bitGet
-  Loadout version_
-    <$> U32.bitGet
-    <*> U32.bitGet
-    <*> U32.bitGet
-    <*> U32.bitGet
-    <*> U32.bitGet
-    <*> U32.bitGet
-    <*> U32.bitGet
-    <*> whenMaybe (U8.toWord8 version_ >= 11) U32.bitGet
-    <*> whenMaybe (U8.toWord8 version_ >= 16) U32.bitGet
-    <*> whenMaybe (U8.toWord8 version_ >= 16) U32.bitGet
-    <*> whenMaybe (U8.toWord8 version_ >= 16) U32.bitGet
-    <*> whenMaybe (U8.toWord8 version_ >= 17) U32.bitGet
-    <*> whenMaybe (U8.toWord8 version_ >= 19) U32.bitGet
-    <*> whenMaybe (U8.toWord8 version_ >= 22) U32.bitGet
-    <*> whenMaybe (U8.toWord8 version_ >= 22) U32.bitGet
-    <*> whenMaybe (U8.toWord8 version_ >= 22) U32.bitGet
+  version <- U8.bitGet
+  body <- U32.bitGet
+  decal <- U32.bitGet
+  wheels <- U32.bitGet
+  rocketTrail <- U32.bitGet
+  antenna <- U32.bitGet
+  topper <- U32.bitGet
+  unknown1 <- U32.bitGet
+  unknown2 <- Monad.whenMaybe (U8.toWord8 version >= 11) U32.bitGet
+  engineAudio <- Monad.whenMaybe (U8.toWord8 version >= 16) U32.bitGet
+  trail <- Monad.whenMaybe (U8.toWord8 version >= 16) U32.bitGet
+  goalExplosion <- Monad.whenMaybe (U8.toWord8 version >= 16) U32.bitGet
+  banner <- Monad.whenMaybe (U8.toWord8 version >= 17) U32.bitGet
+  unknown3 <- Monad.whenMaybe (U8.toWord8 version >= 19) U32.bitGet
+  unknown4 <- Monad.whenMaybe (U8.toWord8 version >= 22) U32.bitGet
+  unknown5 <- Monad.whenMaybe (U8.toWord8 version >= 22) U32.bitGet
+  unknown6 <- Monad.whenMaybe (U8.toWord8 version >= 22) U32.bitGet
+  pure Loadout
+    { version
+    , body
+    , decal
+    , wheels
+    , rocketTrail
+    , antenna
+    , topper
+    , unknown1
+    , unknown2
+    , engineAudio
+    , trail
+    , goalExplosion
+    , banner
+    , unknown3
+    , unknown4
+    , unknown5
+    , unknown6
+    }
diff --git a/src/lib/Rattletrap/Type/Attribute/LoadoutOnline.hs b/src/lib/Rattletrap/Type/Attribute/LoadoutOnline.hs
--- a/src/lib/Rattletrap/Type/Attribute/LoadoutOnline.hs
+++ b/src/lib/Rattletrap/Type/Attribute/LoadoutOnline.hs
@@ -41,6 +41,7 @@
   :: Version.Version -> Map.Map U32.U32 Str.Str -> BitGet.BitGet LoadoutOnline
 bitGet version objectMap = do
   size <- U8.bitGet
-  LoadoutOnline <$> List.replicateM
-    (fromIntegral (U8.toWord8 size))
-    (Product.decodeProductAttributesBits version objectMap)
+  value <-
+    List.replicateM (fromIntegral $ U8.toWord8 size)
+      $ Product.decodeProductAttributesBits version objectMap
+  pure LoadoutOnline { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/Loadouts.hs b/src/lib/Rattletrap/Type/Attribute/Loadouts.hs
--- a/src/lib/Rattletrap/Type/Attribute/Loadouts.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Loadouts.hs
@@ -33,4 +33,7 @@
   <> Loadout.bitPut (orange loadoutsAttribute)
 
 bitGet :: BitGet.BitGet Loadouts
-bitGet = Loadouts <$> Loadout.bitGet <*> Loadout.bitGet
+bitGet = do
+  blue <- Loadout.bitGet
+  orange <- Loadout.bitGet
+  pure Loadouts { blue, orange }
diff --git a/src/lib/Rattletrap/Type/Attribute/LoadoutsOnline.hs b/src/lib/Rattletrap/Type/Attribute/LoadoutsOnline.hs
--- a/src/lib/Rattletrap/Type/Attribute/LoadoutsOnline.hs
+++ b/src/lib/Rattletrap/Type/Attribute/LoadoutsOnline.hs
@@ -52,9 +52,9 @@
 
 bitGet
   :: Version.Version -> Map.Map U32.U32 Str.Str -> BitGet.BitGet LoadoutsOnline
-bitGet version objectMap =
-  LoadoutsOnline
-    <$> LoadoutOnline.bitGet version objectMap
-    <*> LoadoutOnline.bitGet version objectMap
-    <*> BitGet.bool
-    <*> BitGet.bool
+bitGet version objectMap = do
+  blue <- LoadoutOnline.bitGet version objectMap
+  orange <- LoadoutOnline.bitGet version objectMap
+  unknown1 <- BitGet.bool
+  unknown2 <- BitGet.bool
+  pure LoadoutsOnline { blue, orange, unknown1, unknown2 }
diff --git a/src/lib/Rattletrap/Type/Attribute/Location.hs b/src/lib/Rattletrap/Type/Attribute/Location.hs
--- a/src/lib/Rattletrap/Type/Attribute/Location.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Location.hs
@@ -24,4 +24,6 @@
 bitPut locationAttribute = Vector.bitPut (value locationAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet Location
-bitGet version = Location <$> Vector.bitGet version
+bitGet version = do
+  value <- Vector.bitGet version
+  pure Location { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/MusicStinger.hs b/src/lib/Rattletrap/Type/Attribute/MusicStinger.hs
--- a/src/lib/Rattletrap/Type/Attribute/MusicStinger.hs
+++ b/src/lib/Rattletrap/Type/Attribute/MusicStinger.hs
@@ -42,4 +42,8 @@
     <> U8.bitPut (trigger musicStingerAttribute)
 
 bitGet :: BitGet.BitGet MusicStinger
-bitGet = MusicStinger <$> BitGet.bool <*> U32.bitGet <*> U8.bitGet
+bitGet = do
+  flag <- BitGet.bool
+  cue <- U32.bitGet
+  trigger <- U8.bitGet
+  pure MusicStinger { flag, cue, trigger }
diff --git a/src/lib/Rattletrap/Type/Attribute/PartyLeader.hs b/src/lib/Rattletrap/Type/Attribute/PartyLeader.hs
--- a/src/lib/Rattletrap/Type/Attribute/PartyLeader.hs
+++ b/src/lib/Rattletrap/Type/Attribute/PartyLeader.hs
@@ -1,6 +1,5 @@
 module Rattletrap.Type.Attribute.PartyLeader where
 
-import Prelude hiding (id)
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
 import qualified Rattletrap.Schema as Schema
@@ -8,23 +7,31 @@
 import qualified Rattletrap.Type.U8 as U8
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
-import Rattletrap.Utility.Monad
 
 data PartyLeader = PartyLeader
   { systemId :: U8.U8
-  , id :: Maybe (RemoteId.RemoteId, U8.U8)
+  , remoteId :: Maybe RemoteId.RemoteId
+  , localId :: Maybe U8.U8
   }
   deriving (Eq, Show)
 
 instance Json.FromJSON PartyLeader where
   parseJSON = Json.withObject "PartyLeader" $ \object -> do
     systemId <- Json.required object "system_id"
-    id <- Json.optional object "id"
-    pure PartyLeader { systemId, id }
+    maybeId <- Json.optional object "id"
+    pure PartyLeader
+      { systemId
+      , remoteId = fmap fst maybeId
+      , localId = fmap snd maybeId
+      }
 
 instance Json.ToJSON PartyLeader where
-  toJSON x =
-    Json.object [Json.pair "system_id" $ systemId x, Json.pair "id" $ id x]
+  toJSON x = Json.object
+    [ Json.pair "system_id" $ systemId x
+    , Json.pair "id" $ case (remoteId x, localId x) of
+      (Just r, Just l) -> Just (r, l)
+      _ -> Nothing
+    ]
 
 schema :: Schema.Schema
 schema = Schema.named "attribute-party-leader" $ Schema.object
@@ -38,13 +45,18 @@
   ]
 
 bitPut :: PartyLeader -> BitPut.BitPut
-bitPut x = U8.bitPut (systemId x) <> foldMap
-  (\(y, z) -> RemoteId.bitPut y <> U8.bitPut z)
-  (Rattletrap.Type.Attribute.PartyLeader.id x)
+bitPut x =
+  U8.bitPut (systemId x) <> foldMap RemoteId.bitPut (remoteId x) <> foldMap
+    U8.bitPut
+    (localId x)
 
 bitGet :: Version.Version -> BitGet.BitGet PartyLeader
 bitGet version = do
-  systemId_ <- U8.bitGet
-  PartyLeader systemId_ <$> whenMaybe
-    (systemId_ /= U8.fromWord8 0)
-    ((,) <$> RemoteId.bitGet version systemId_ <*> U8.bitGet)
+  systemId <- U8.bitGet
+  (remoteId, localId) <- if systemId == U8.fromWord8 0
+    then pure (Nothing, Nothing)
+    else do
+      remoteId <- RemoteId.bitGet version systemId
+      localId <- U8.bitGet
+      pure (Just remoteId, Just localId)
+  pure PartyLeader { systemId, remoteId, localId }
diff --git a/src/lib/Rattletrap/Type/Attribute/Pickup.hs b/src/lib/Rattletrap/Type/Attribute/Pickup.hs
--- a/src/lib/Rattletrap/Type/Attribute/Pickup.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Pickup.hs
@@ -5,7 +5,7 @@
 import qualified Rattletrap.Schema as Schema
 import qualified Rattletrap.Type.U32 as U32
 import qualified Rattletrap.Utility.Json as Json
-import Rattletrap.Utility.Monad
+import qualified Rattletrap.Utility.Monad as Monad
 
 data Pickup = Pickup
   { instigatorId :: Maybe U32.U32
@@ -42,4 +42,6 @@
 bitGet :: BitGet.BitGet Pickup
 bitGet = do
   instigator <- BitGet.bool
-  Pickup <$> whenMaybe instigator U32.bitGet <*> BitGet.bool
+  instigatorId <- Monad.whenMaybe instigator U32.bitGet
+  pickedUp <- BitGet.bool
+  pure Pickup { instigatorId, pickedUp }
diff --git a/src/lib/Rattletrap/Type/Attribute/PickupNew.hs b/src/lib/Rattletrap/Type/Attribute/PickupNew.hs
--- a/src/lib/Rattletrap/Type/Attribute/PickupNew.hs
+++ b/src/lib/Rattletrap/Type/Attribute/PickupNew.hs
@@ -6,7 +6,7 @@
 import qualified Rattletrap.Type.U32 as U32
 import qualified Rattletrap.Type.U8 as U8
 import qualified Rattletrap.Utility.Json as Json
-import Rattletrap.Utility.Monad
+import qualified Rattletrap.Utility.Monad as Monad
 
 data PickupNew = PickupNew
   { instigatorId :: Maybe U32.U32
@@ -43,4 +43,6 @@
 bitGet :: BitGet.BitGet PickupNew
 bitGet = do
   instigator <- BitGet.bool
-  PickupNew <$> whenMaybe instigator U32.bitGet <*> U8.bitGet
+  instigatorId <- Monad.whenMaybe instigator U32.bitGet
+  pickedUp <- U8.bitGet
+  pure PickupNew { instigatorId, pickedUp }
diff --git a/src/lib/Rattletrap/Type/Attribute/PlayerHistoryKey.hs b/src/lib/Rattletrap/Type/Attribute/PlayerHistoryKey.hs
--- a/src/lib/Rattletrap/Type/Attribute/PlayerHistoryKey.hs
+++ b/src/lib/Rattletrap/Type/Attribute/PlayerHistoryKey.hs
@@ -24,4 +24,6 @@
 bitPut = BitPut.bits 14 . unknown
 
 bitGet :: BitGet.BitGet PlayerHistoryKey
-bitGet = PlayerHistoryKey <$> BitGet.bits 14
+bitGet = do
+  unknown <- BitGet.bits 14
+  pure PlayerHistoryKey { unknown }
diff --git a/src/lib/Rattletrap/Type/Attribute/PrivateMatchSettings.hs b/src/lib/Rattletrap/Type/Attribute/PrivateMatchSettings.hs
--- a/src/lib/Rattletrap/Type/Attribute/PrivateMatchSettings.hs
+++ b/src/lib/Rattletrap/Type/Attribute/PrivateMatchSettings.hs
@@ -64,11 +64,18 @@
     <> BitPut.bool (flag privateMatchSettingsAttribute)
 
 bitGet :: BitGet.BitGet PrivateMatchSettings
-bitGet =
-  PrivateMatchSettings
-    <$> Str.bitGet
-    <*> U32.bitGet
-    <*> U32.bitGet
-    <*> Str.bitGet
-    <*> Str.bitGet
-    <*> BitGet.bool
+bitGet = do
+  mutators <- Str.bitGet
+  joinableBy <- U32.bitGet
+  maxPlayers <- U32.bitGet
+  gameName <- Str.bitGet
+  password <- Str.bitGet
+  flag <- BitGet.bool
+  pure PrivateMatchSettings
+    { mutators
+    , joinableBy
+    , maxPlayers
+    , gameName
+    , password
+    , flag
+    }
diff --git a/src/lib/Rattletrap/Type/Attribute/ProductValue.hs b/src/lib/Rattletrap/Type/Attribute/ProductValue.hs
--- a/src/lib/Rattletrap/Type/Attribute/ProductValue.hs
+++ b/src/lib/Rattletrap/Type/Attribute/ProductValue.hs
@@ -10,7 +10,7 @@
 import qualified Rattletrap.Type.U32 as U32
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
-import Rattletrap.Utility.Monad
+import qualified Rattletrap.Utility.Monad as Monad
 
 data ProductValue
   = PaintedOld CompressedWord.CompressedWord
@@ -25,14 +25,14 @@
 
 instance Json.FromJSON ProductValue where
   parseJSON = Json.withObject "ProductValue" $ \object -> Foldable.asum
-    [ PaintedOld <$> Json.required object "painted_old"
-    , PaintedNew <$> Json.required object "painted_new"
-    , TeamEditionOld <$> Json.required object "team_edition_old"
-    , TeamEditionNew <$> Json.required object "team_edition_new"
-    , SpecialEdition <$> Json.required object "special_edition"
-    , UserColorOld <$> Json.required object "user_color_old"
-    , UserColorNew <$> Json.required object "user_color_new"
-    , TitleId <$> Json.required object "title_id"
+    [ fmap PaintedOld $ Json.required object "painted_old"
+    , fmap PaintedNew $ Json.required object "painted_new"
+    , fmap TeamEditionOld $ Json.required object "team_edition_old"
+    , fmap TeamEditionNew $ Json.required object "team_edition_new"
+    , fmap SpecialEdition $ Json.required object "special_edition"
+    , fmap UserColorOld $ Json.required object "user_color_old"
+    , fmap UserColorNew $ Json.required object "user_color_new"
+    , fmap TitleId $ Json.required object "title_id"
     ]
 
 instance Json.ToJSON ProductValue where
@@ -75,7 +75,7 @@
 bitGet
   :: Version.Version -> U32.U32 -> Maybe Str.Str -> BitGet.BitGet ProductValue
 bitGet version objectId maybeObjectName =
-  case Str.toString <$> maybeObjectName of
+  case fmap Str.toString maybeObjectName of
     Just "TAGame.ProductAttribute_Painted_TA" -> decodePainted version
     Just "TAGame.ProductAttribute_SpecialEdition_TA" -> decodeSpecialEdition
     Just "TAGame.ProductAttribute_TeamEdition_TA" -> decodeTeamEdition version
@@ -90,32 +90,27 @@
     Nothing -> fail ("[RT06] missing object name for ID " <> show objectId)
 
 decodeSpecialEdition :: BitGet.BitGet ProductValue
-decodeSpecialEdition = SpecialEdition <$> BitGet.bits 31
+decodeSpecialEdition = fmap SpecialEdition $ BitGet.bits 31
 
 decodePainted :: Version.Version -> BitGet.BitGet ProductValue
 decodePainted version = if hasNewPainted version
-  then PaintedNew <$> BitGet.bits 31
-  else PaintedOld <$> CompressedWord.bitGet 13
+  then fmap PaintedNew $ BitGet.bits 31
+  else fmap PaintedOld $ CompressedWord.bitGet 13
 
 decodeTeamEdition :: Version.Version -> BitGet.BitGet ProductValue
 decodeTeamEdition version = if hasNewPainted version
-  then TeamEditionNew <$> BitGet.bits 31
-  else TeamEditionOld <$> CompressedWord.bitGet 13
+  then fmap TeamEditionNew $ BitGet.bits 31
+  else fmap TeamEditionOld $ CompressedWord.bitGet 13
 
 decodeColor :: Version.Version -> BitGet.BitGet ProductValue
-decodeColor version = if hasNewColor version
-  then UserColorNew <$> U32.bitGet
+decodeColor version = if Version.atLeast 868 23 8 version
+  then fmap UserColorNew U32.bitGet
   else do
     hasValue <- BitGet.bool
-    UserColorOld <$> whenMaybe hasValue (BitGet.bits 31)
+    fmap UserColorOld $ Monad.whenMaybe hasValue (BitGet.bits 31)
 
 hasNewPainted :: Version.Version -> Bool
-hasNewPainted v =
-  Version.major v >= 868 && Version.minor v >= 18 && Version.patch v >= 0
-
-hasNewColor :: Version.Version -> Bool
-hasNewColor v =
-  Version.major v >= 868 && Version.minor v >= 23 && Version.patch v >= 8
+hasNewPainted = Version.atLeast 868 18 0
 
 decodeTitle :: BitGet.BitGet ProductValue
-decodeTitle = TitleId <$> Str.bitGet
+decodeTitle = fmap TitleId Str.bitGet
diff --git a/src/lib/Rattletrap/Type/Attribute/QWord.hs b/src/lib/Rattletrap/Type/Attribute/QWord.hs
--- a/src/lib/Rattletrap/Type/Attribute/QWord.hs
+++ b/src/lib/Rattletrap/Type/Attribute/QWord.hs
@@ -23,4 +23,6 @@
 bitPut qWordAttribute = U64.bitPut (value qWordAttribute)
 
 bitGet :: BitGet.BitGet QWord
-bitGet = QWord <$> U64.bitGet
+bitGet = do
+  value <- U64.bitGet
+  pure QWord { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/Reservation.hs b/src/lib/Rattletrap/Type/Attribute/Reservation.hs
--- a/src/lib/Rattletrap/Type/Attribute/Reservation.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Reservation.hs
@@ -10,7 +10,7 @@
 import qualified Rattletrap.Type.U8 as U8
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
-import Rattletrap.Utility.Monad
+import qualified Rattletrap.Utility.Monad as Monad
 
 data Reservation = Reservation
   { number :: CompressedWord.CompressedWord
@@ -59,18 +59,17 @@
     <> foldMap Str.bitPut (name reservationAttribute)
     <> BitPut.bool (unknown1 reservationAttribute)
     <> BitPut.bool (unknown2 reservationAttribute)
-    <> foldMap (BitPut.word8 6) (unknown3 reservationAttribute)
+    <> foldMap (BitPut.bits 6) (unknown3 reservationAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet Reservation
 bitGet version = do
-  number_ <- CompressedWord.bitGet 7
-  uniqueId_ <- UniqueId.bitGet version
-  Reservation number_ uniqueId_
-    <$> whenMaybe (UniqueId.systemId uniqueId_ /= U8.fromWord8 0) Str.bitGet
-    <*> BitGet.bool
-    <*> BitGet.bool
-    <*> whenMaybe (hasUnknown3 version) (BitGet.word8 6)
-
-hasUnknown3 :: Version.Version -> Bool
-hasUnknown3 v =
-  Version.major v >= 868 && Version.minor v >= 12 && Version.patch v >= 0
+  number <- CompressedWord.bitGet 7
+  uniqueId <- UniqueId.bitGet version
+  name <- Monad.whenMaybe
+    (UniqueId.systemId uniqueId /= U8.fromWord8 0)
+    Str.bitGet
+  unknown1 <- BitGet.bool
+  unknown2 <- BitGet.bool
+  unknown3 <- Monad.whenMaybe (Version.atLeast 868 12 0 version)
+    $ BitGet.bits 6
+  pure Reservation { number, uniqueId, name, unknown1, unknown2, unknown3 }
diff --git a/src/lib/Rattletrap/Type/Attribute/RigidBodyState.hs b/src/lib/Rattletrap/Type/Attribute/RigidBodyState.hs
--- a/src/lib/Rattletrap/Type/Attribute/RigidBodyState.hs
+++ b/src/lib/Rattletrap/Type/Attribute/RigidBodyState.hs
@@ -7,7 +7,7 @@
 import qualified Rattletrap.Type.Vector as Vector
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
-import Rattletrap.Utility.Monad
+import qualified Rattletrap.Utility.Monad as Monad
 
 data RigidBodyState = RigidBodyState
   { sleeping :: Bool
@@ -65,9 +65,15 @@
 
 bitGet :: Version.Version -> BitGet.BitGet RigidBodyState
 bitGet version = do
-  sleeping_ <- BitGet.bool
-  RigidBodyState sleeping_
-    <$> Vector.bitGet version
-    <*> Rotation.bitGet version
-    <*> whenMaybe (not sleeping_) (Vector.bitGet version)
-    <*> whenMaybe (not sleeping_) (Vector.bitGet version)
+  sleeping <- BitGet.bool
+  location <- Vector.bitGet version
+  rotation <- Rotation.bitGet version
+  linearVelocity <- Monad.whenMaybe (not sleeping) (Vector.bitGet version)
+  angularVelocity <- Monad.whenMaybe (not sleeping) (Vector.bitGet version)
+  pure RigidBodyState
+    { sleeping
+    , location
+    , rotation
+    , linearVelocity
+    , angularVelocity
+    }
diff --git a/src/lib/Rattletrap/Type/Attribute/StatEvent.hs b/src/lib/Rattletrap/Type/Attribute/StatEvent.hs
--- a/src/lib/Rattletrap/Type/Attribute/StatEvent.hs
+++ b/src/lib/Rattletrap/Type/Attribute/StatEvent.hs
@@ -33,4 +33,7 @@
   <> I32.bitPut (objectId statEventAttribute)
 
 bitGet :: BitGet.BitGet StatEvent
-bitGet = StatEvent <$> BitGet.bool <*> I32.bitGet
+bitGet = do
+  unknown <- BitGet.bool
+  objectId <- I32.bitGet
+  pure StatEvent { unknown, objectId }
diff --git a/src/lib/Rattletrap/Type/Attribute/String.hs b/src/lib/Rattletrap/Type/Attribute/String.hs
--- a/src/lib/Rattletrap/Type/Attribute/String.hs
+++ b/src/lib/Rattletrap/Type/Attribute/String.hs
@@ -24,4 +24,6 @@
 bitPut stringAttribute = Str.bitPut (value stringAttribute)
 
 bitGet :: BitGet.BitGet String
-bitGet = String <$> Str.bitGet
+bitGet = do
+  value <- Str.bitGet
+  pure String { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/TeamPaint.hs b/src/lib/Rattletrap/Type/Attribute/TeamPaint.hs
--- a/src/lib/Rattletrap/Type/Attribute/TeamPaint.hs
+++ b/src/lib/Rattletrap/Type/Attribute/TeamPaint.hs
@@ -58,10 +58,16 @@
     <> U32.bitPut (accentFinish teamPaintAttribute)
 
 bitGet :: BitGet.BitGet TeamPaint
-bitGet =
-  TeamPaint
-    <$> U8.bitGet
-    <*> U8.bitGet
-    <*> U8.bitGet
-    <*> U32.bitGet
-    <*> U32.bitGet
+bitGet = do
+  team <- U8.bitGet
+  primaryColor <- U8.bitGet
+  accentColor <- U8.bitGet
+  primaryFinish <- U32.bitGet
+  accentFinish <- U32.bitGet
+  pure TeamPaint
+    { team
+    , primaryColor
+    , accentColor
+    , primaryFinish
+    , accentFinish
+    }
diff --git a/src/lib/Rattletrap/Type/Attribute/Title.hs b/src/lib/Rattletrap/Type/Attribute/Title.hs
--- a/src/lib/Rattletrap/Type/Attribute/Title.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Title.hs
@@ -75,13 +75,22 @@
     <> BitPut.bool (unknown8 titleAttribute)
 
 bitGet :: BitGet.BitGet Title
-bitGet =
-  Title
-    <$> BitGet.bool
-    <*> BitGet.bool
-    <*> U32.bitGet
-    <*> U32.bitGet
-    <*> U32.bitGet
-    <*> U32.bitGet
-    <*> U32.bitGet
-    <*> BitGet.bool
+bitGet = do
+  unknown1 <- BitGet.bool
+  unknown2 <- BitGet.bool
+  unknown3 <- U32.bitGet
+  unknown4 <- U32.bitGet
+  unknown5 <- U32.bitGet
+  unknown6 <- U32.bitGet
+  unknown7 <- U32.bitGet
+  unknown8 <- BitGet.bool
+  pure Title
+    { unknown1
+    , unknown2
+    , unknown3
+    , unknown4
+    , unknown5
+    , unknown6
+    , unknown7
+    , unknown8
+    }
diff --git a/src/lib/Rattletrap/Type/Attribute/UniqueId.hs b/src/lib/Rattletrap/Type/Attribute/UniqueId.hs
--- a/src/lib/Rattletrap/Type/Attribute/UniqueId.hs
+++ b/src/lib/Rattletrap/Type/Attribute/UniqueId.hs
@@ -44,5 +44,7 @@
 
 bitGet :: Version.Version -> BitGet.BitGet UniqueId
 bitGet version = do
-  systemId_ <- U8.bitGet
-  UniqueId systemId_ <$> RemoteId.bitGet version systemId_ <*> U8.bitGet
+  systemId <- U8.bitGet
+  remoteId <- RemoteId.bitGet version systemId
+  localId <- U8.bitGet
+  pure UniqueId { systemId, remoteId, localId }
diff --git a/src/lib/Rattletrap/Type/Attribute/WeldedInfo.hs b/src/lib/Rattletrap/Type/Attribute/WeldedInfo.hs
--- a/src/lib/Rattletrap/Type/Attribute/WeldedInfo.hs
+++ b/src/lib/Rattletrap/Type/Attribute/WeldedInfo.hs
@@ -55,10 +55,10 @@
     <> Int8Vector.bitPut (rotation weldedInfoAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet WeldedInfo
-bitGet version =
-  WeldedInfo
-    <$> BitGet.bool
-    <*> I32.bitGet
-    <*> Vector.bitGet version
-    <*> F32.bitGet
-    <*> Int8Vector.bitGet
+bitGet version = do
+  active <- BitGet.bool
+  actorId <- I32.bitGet
+  offset <- Vector.bitGet version
+  mass <- F32.bitGet
+  rotation <- Int8Vector.bitGet
+  pure WeldedInfo { active, actorId, offset, mass, rotation }
diff --git a/src/lib/Rattletrap/Type/AttributeMapping.hs b/src/lib/Rattletrap/Type/AttributeMapping.hs
--- a/src/lib/Rattletrap/Type/AttributeMapping.hs
+++ b/src/lib/Rattletrap/Type/AttributeMapping.hs
@@ -32,4 +32,7 @@
 bytePut x = U32.bytePut (objectId x) <> U32.bytePut (streamId x)
 
 byteGet :: ByteGet.ByteGet AttributeMapping
-byteGet = AttributeMapping <$> U32.byteGet <*> U32.byteGet
+byteGet = do
+  objectId <- U32.byteGet
+  streamId <- U32.byteGet
+  pure AttributeMapping { objectId, streamId }
diff --git a/src/lib/Rattletrap/Type/AttributeValue.hs b/src/lib/Rattletrap/Type/AttributeValue.hs
--- a/src/lib/Rattletrap/Type/AttributeValue.hs
+++ b/src/lib/Rattletrap/Type/AttributeValue.hs
@@ -92,43 +92,43 @@
 
 instance Json.FromJSON AttributeValue where
   parseJSON = Json.withObject "AttributeValue" $ \object -> Foldable.asum
-    [ AppliedDamage <$> Json.required object "applied_damage"
-    , Boolean <$> Json.required object "boolean"
-    , Byte <$> Json.required object "byte"
-    , CamSettings <$> Json.required object "cam_settings"
-    , ClubColors <$> Json.required object "club_colors"
-    , CustomDemolish <$> Json.required object "custom_demolish"
-    , DamageState <$> Json.required object "damage_state"
-    , Demolish <$> Json.required object "demolish"
-    , Enum <$> Json.required object "enum"
-    , Explosion <$> Json.required object "explosion"
-    , ExtendedExplosion <$> Json.required object "extended_explosion"
-    , FlaggedByte <$> Json.required object "flagged_byte"
-    , FlaggedInt <$> Json.required object "flagged_int"
-    , Float <$> Json.required object "float"
-    , GameMode <$> Json.required object "game_mode"
-    , Int <$> Json.required object "int"
-    , Int64 <$> Json.required object "int64"
-    , Loadout <$> Json.required object "loadout"
-    , LoadoutOnline <$> Json.required object "loadout_online"
-    , Loadouts <$> Json.required object "loadouts"
-    , LoadoutsOnline <$> Json.required object "loadouts_online"
-    , Location <$> Json.required object "location"
-    , MusicStinger <$> Json.required object "music_stinger"
-    , PartyLeader <$> Json.required object "party_leader"
-    , Pickup <$> Json.required object "pickup"
-    , PickupNew <$> Json.required object "pickup_new"
-    , PlayerHistoryKey <$> Json.required object "player_history_key"
-    , PrivateMatchSettings <$> Json.required object "private_match_settings"
-    , QWord <$> Json.required object "q_word"
-    , Reservation <$> Json.required object "reservation"
-    , RigidBodyState <$> Json.required object "rigid_body_state"
-    , StatEvent <$> Json.required object "stat_event"
-    , String <$> Json.required object "string"
-    , TeamPaint <$> Json.required object "team_paint"
-    , Title <$> Json.required object "title"
-    , UniqueId <$> Json.required object "unique_id"
-    , WeldedInfo <$> Json.required object "welded_info"
+    [ fmap AppliedDamage $ Json.required object "applied_damage"
+    , fmap Boolean $ Json.required object "boolean"
+    , fmap Byte $ Json.required object "byte"
+    , fmap CamSettings $ Json.required object "cam_settings"
+    , fmap ClubColors $ Json.required object "club_colors"
+    , fmap CustomDemolish $ Json.required object "custom_demolish"
+    , fmap DamageState $ Json.required object "damage_state"
+    , fmap Demolish $ Json.required object "demolish"
+    , fmap Enum $ Json.required object "enum"
+    , fmap Explosion $ Json.required object "explosion"
+    , fmap ExtendedExplosion $ Json.required object "extended_explosion"
+    , fmap FlaggedByte $ Json.required object "flagged_byte"
+    , fmap FlaggedInt $ Json.required object "flagged_int"
+    , fmap Float $ Json.required object "float"
+    , fmap GameMode $ Json.required object "game_mode"
+    , fmap Int $ Json.required object "int"
+    , fmap Int64 $ Json.required object "int64"
+    , fmap Loadout $ Json.required object "loadout"
+    , fmap LoadoutOnline $ Json.required object "loadout_online"
+    , fmap Loadouts $ Json.required object "loadouts"
+    , fmap LoadoutsOnline $ Json.required object "loadouts_online"
+    , fmap Location $ Json.required object "location"
+    , fmap MusicStinger $ Json.required object "music_stinger"
+    , fmap PartyLeader $ Json.required object "party_leader"
+    , fmap Pickup $ Json.required object "pickup"
+    , fmap PickupNew $ Json.required object "pickup_new"
+    , fmap PlayerHistoryKey $ Json.required object "player_history_key"
+    , fmap PrivateMatchSettings $ Json.required object "private_match_settings"
+    , fmap QWord $ Json.required object "q_word"
+    , fmap Reservation $ Json.required object "reservation"
+    , fmap RigidBodyState $ Json.required object "rigid_body_state"
+    , fmap StatEvent $ Json.required object "stat_event"
+    , fmap String $ Json.required object "string"
+    , fmap TeamPaint $ Json.required object "team_paint"
+    , fmap Title $ Json.required object "title"
+    , fmap UniqueId $ Json.required object "unique_id"
+    , fmap WeldedInfo $ Json.required object "welded_info"
     ]
 
 instance Json.ToJSON AttributeValue where
@@ -266,47 +266,47 @@
     (Map.lookup (Str.toText name) Data.attributeTypes)
   case constructor of
     AttributeType.AppliedDamage ->
-      AppliedDamage <$> AppliedDamage.bitGet version
-    AttributeType.Boolean -> Boolean <$> Boolean.bitGet
-    AttributeType.Byte -> Byte <$> Byte.bitGet
-    AttributeType.CamSettings -> CamSettings <$> CamSettings.bitGet version
-    AttributeType.ClubColors -> ClubColors <$> ClubColors.bitGet
+      fmap AppliedDamage $ AppliedDamage.bitGet version
+    AttributeType.Boolean -> fmap Boolean Boolean.bitGet
+    AttributeType.Byte -> fmap Byte Byte.bitGet
+    AttributeType.CamSettings -> fmap CamSettings $ CamSettings.bitGet version
+    AttributeType.ClubColors -> fmap ClubColors ClubColors.bitGet
     AttributeType.CustomDemolish ->
-      CustomDemolish <$> CustomDemolish.bitGet version
-    AttributeType.DamageState -> DamageState <$> DamageState.bitGet version
-    AttributeType.Demolish -> Demolish <$> Demolish.bitGet version
-    AttributeType.Enum -> Enum <$> Enum.bitGet
-    AttributeType.Explosion -> Explosion <$> Explosion.bitGet version
+      fmap CustomDemolish $ CustomDemolish.bitGet version
+    AttributeType.DamageState -> fmap DamageState $ DamageState.bitGet version
+    AttributeType.Demolish -> fmap Demolish $ Demolish.bitGet version
+    AttributeType.Enum -> fmap Enum Enum.bitGet
+    AttributeType.Explosion -> fmap Explosion $ Explosion.bitGet version
     AttributeType.ExtendedExplosion ->
-      ExtendedExplosion <$> ExtendedExplosion.bitGet version
-    AttributeType.FlaggedInt -> FlaggedInt <$> FlaggedInt.bitGet
-    AttributeType.FlaggedByte -> FlaggedByte <$> FlaggedByte.bitGet
-    AttributeType.Float -> Float <$> Float.bitGet
-    AttributeType.GameMode -> GameMode <$> GameMode.bitGet version
-    AttributeType.Int -> Int <$> Int.bitGet
-    AttributeType.Int64 -> Int64 <$> Int64.bitGet
-    AttributeType.Loadout -> Loadout <$> Loadout.bitGet
+      fmap ExtendedExplosion $ ExtendedExplosion.bitGet version
+    AttributeType.FlaggedInt -> fmap FlaggedInt FlaggedInt.bitGet
+    AttributeType.FlaggedByte -> fmap FlaggedByte FlaggedByte.bitGet
+    AttributeType.Float -> fmap Float Float.bitGet
+    AttributeType.GameMode -> fmap GameMode $ GameMode.bitGet version
+    AttributeType.Int -> fmap Int Int.bitGet
+    AttributeType.Int64 -> fmap Int64 Int64.bitGet
+    AttributeType.Loadout -> fmap Loadout Loadout.bitGet
     AttributeType.LoadoutOnline ->
-      LoadoutOnline <$> LoadoutOnline.bitGet version objectMap
-    AttributeType.Loadouts -> Loadouts <$> Loadouts.bitGet
+      fmap LoadoutOnline $ LoadoutOnline.bitGet version objectMap
+    AttributeType.Loadouts -> fmap Loadouts Loadouts.bitGet
     AttributeType.LoadoutsOnline ->
-      LoadoutsOnline <$> LoadoutsOnline.bitGet version objectMap
-    AttributeType.Location -> Location <$> Location.bitGet version
-    AttributeType.MusicStinger -> MusicStinger <$> MusicStinger.bitGet
-    AttributeType.PartyLeader -> PartyLeader <$> PartyLeader.bitGet version
-    AttributeType.Pickup -> Pickup <$> Pickup.bitGet
-    AttributeType.PickupNew -> PickupNew <$> PickupNew.bitGet
+      fmap LoadoutsOnline $ LoadoutsOnline.bitGet version objectMap
+    AttributeType.Location -> fmap Location $ Location.bitGet version
+    AttributeType.MusicStinger -> fmap MusicStinger MusicStinger.bitGet
+    AttributeType.PartyLeader -> fmap PartyLeader $ PartyLeader.bitGet version
+    AttributeType.Pickup -> fmap Pickup Pickup.bitGet
+    AttributeType.PickupNew -> fmap PickupNew PickupNew.bitGet
     AttributeType.PlayerHistoryKey ->
-      PlayerHistoryKey <$> PlayerHistoryKey.bitGet
+      fmap PlayerHistoryKey PlayerHistoryKey.bitGet
     AttributeType.PrivateMatchSettings ->
-      PrivateMatchSettings <$> PrivateMatchSettings.bitGet
-    AttributeType.QWord -> QWord <$> QWord.bitGet
-    AttributeType.Reservation -> Reservation <$> Reservation.bitGet version
+      fmap PrivateMatchSettings PrivateMatchSettings.bitGet
+    AttributeType.QWord -> fmap QWord QWord.bitGet
+    AttributeType.Reservation -> fmap Reservation $ Reservation.bitGet version
     AttributeType.RigidBodyState ->
-      RigidBodyState <$> RigidBodyState.bitGet version
-    AttributeType.StatEvent -> StatEvent <$> StatEvent.bitGet
-    AttributeType.String -> String <$> String.bitGet
-    AttributeType.TeamPaint -> TeamPaint <$> TeamPaint.bitGet
-    AttributeType.Title -> Title <$> Title.bitGet
-    AttributeType.UniqueId -> UniqueId <$> UniqueId.bitGet version
-    AttributeType.WeldedInfo -> WeldedInfo <$> WeldedInfo.bitGet version
+      fmap RigidBodyState $ RigidBodyState.bitGet version
+    AttributeType.StatEvent -> fmap StatEvent StatEvent.bitGet
+    AttributeType.String -> fmap String String.bitGet
+    AttributeType.TeamPaint -> fmap TeamPaint TeamPaint.bitGet
+    AttributeType.Title -> fmap Title Title.bitGet
+    AttributeType.UniqueId -> fmap UniqueId $ UniqueId.bitGet version
+    AttributeType.WeldedInfo -> fmap WeldedInfo $ WeldedInfo.bitGet version
diff --git a/src/lib/Rattletrap/Type/Cache.hs b/src/lib/Rattletrap/Type/Cache.hs
--- a/src/lib/Rattletrap/Type/Cache.hs
+++ b/src/lib/Rattletrap/Type/Cache.hs
@@ -51,9 +51,9 @@
     <> List.bytePut AttributeMapping.bytePut (attributeMappings x)
 
 byteGet :: ByteGet.ByteGet Cache
-byteGet =
-  Cache
-    <$> U32.byteGet
-    <*> U32.byteGet
-    <*> U32.byteGet
-    <*> List.byteGet AttributeMapping.byteGet
+byteGet = do
+  classId <- U32.byteGet
+  parentCacheId <- U32.byteGet
+  cacheId <- U32.byteGet
+  attributeMappings <- List.byteGet AttributeMapping.byteGet
+  pure Cache { classId, parentCacheId, cacheId, attributeMappings }
diff --git a/src/lib/Rattletrap/Type/ClassAttributeMap.hs b/src/lib/Rattletrap/Type/ClassAttributeMap.hs
--- a/src/lib/Rattletrap/Type/ClassAttributeMap.hs
+++ b/src/lib/Rattletrap/Type/ClassAttributeMap.hs
@@ -1,15 +1,4 @@
-module Rattletrap.Type.ClassAttributeMap
-  ( ClassAttributeMap(..)
-  , classHasLocation
-  , classHasRotation
-  , getAttributeIdLimit
-  , getAttributeMap
-  , getAttributeName
-  , getClassName
-  , getName
-  , getObjectName
-  , make
-  ) where
+module Rattletrap.Type.ClassAttributeMap where
 
 import qualified Rattletrap.Data as Data
 import qualified Rattletrap.Type.AttributeMapping as AttributeMapping
@@ -262,11 +251,9 @@
 getObjectName objectMap_ objectId = Map.lookup objectId objectMap_
 
 getClassName :: Str.Str -> Maybe Str.Str
-getClassName rawObjectName =
-  Str.fromText
-    <$> Map.lookup
-          (Str.toText $ normalizeObjectName rawObjectName)
-          Data.objectClasses
+getClassName rawObjectName = fmap Str.fromText $ Map.lookup
+  (Str.toText $ normalizeObjectName rawObjectName)
+  Data.objectClasses
 
 normalizeObjectName :: Str.Str -> Str.Str
 normalizeObjectName objectName =
diff --git a/src/lib/Rattletrap/Type/ClassMapping.hs b/src/lib/Rattletrap/Type/ClassMapping.hs
--- a/src/lib/Rattletrap/Type/ClassMapping.hs
+++ b/src/lib/Rattletrap/Type/ClassMapping.hs
@@ -33,4 +33,7 @@
 bytePut x = Str.bytePut (name x) <> U32.bytePut (streamId x)
 
 byteGet :: ByteGet.ByteGet ClassMapping
-byteGet = ClassMapping <$> Str.byteGet <*> U32.byteGet
+byteGet = do
+  name <- Str.byteGet
+  streamId <- U32.byteGet
+  pure ClassMapping { name, streamId }
diff --git a/src/lib/Rattletrap/Type/CompressedWord.hs b/src/lib/Rattletrap/Type/CompressedWord.hs
--- a/src/lib/Rattletrap/Type/CompressedWord.hs
+++ b/src/lib/Rattletrap/Type/CompressedWord.hs
@@ -66,7 +66,9 @@
   in if x < 1024 && x == 2 ^ n then n + 1 else n
 
 bitGet :: Word -> BitGet.BitGet CompressedWord
-bitGet limit_ = CompressedWord limit_ <$> step limit_ (getMaxBits_ limit_) 0 0
+bitGet limit = do
+  value <- step limit (getMaxBits_ limit) 0 0
+  pure CompressedWord { limit, value }
 
 getMaxBits_ :: Word -> Word
 getMaxBits_ x = do
diff --git a/src/lib/Rattletrap/Type/CompressedWordVector.hs b/src/lib/Rattletrap/Type/CompressedWordVector.hs
--- a/src/lib/Rattletrap/Type/CompressedWordVector.hs
+++ b/src/lib/Rattletrap/Type/CompressedWordVector.hs
@@ -38,11 +38,11 @@
     <> CompressedWord.bitPut (z compressedWordVector)
 
 bitGet :: BitGet.BitGet CompressedWordVector
-bitGet =
-  CompressedWordVector
-    <$> CompressedWord.bitGet limit
-    <*> CompressedWord.bitGet limit
-    <*> CompressedWord.bitGet limit
+bitGet = do
+  x <- CompressedWord.bitGet limit
+  y <- CompressedWord.bitGet limit
+  z <- CompressedWord.bitGet limit
+  pure CompressedWordVector { x, y, z }
 
 limit :: Word
 limit = 65536
diff --git a/src/lib/Rattletrap/Type/Content.hs b/src/lib/Rattletrap/Type/Content.hs
--- a/src/lib/Rattletrap/Type/Content.hs
+++ b/src/lib/Rattletrap/Type/Content.hs
@@ -9,7 +9,7 @@
 import qualified Rattletrap.Type.ClassAttributeMap as ClassAttributeMap
 import qualified Rattletrap.Type.ClassMapping as ClassMapping
 import qualified Rattletrap.Type.Frame as Frame
-import qualified Rattletrap.Type.KeyFrame as KeyFrame
+import qualified Rattletrap.Type.Keyframe as Keyframe
 import qualified Rattletrap.Type.List as List
 import qualified Rattletrap.Type.Mark as Mark
 import qualified Rattletrap.Type.Message as Message
@@ -17,12 +17,12 @@
 import qualified Rattletrap.Type.U32 as U32
 import qualified Rattletrap.Type.U8 as U8
 import qualified Rattletrap.Type.Version as Version
-import Rattletrap.Utility.Bytes
+import qualified Rattletrap.Utility.Bytes as Bytes
 import qualified Rattletrap.Utility.Json as Json
 
 import qualified Control.Monad.Trans.State as State
-import qualified Data.ByteString as Bytes
-import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Lazy as LazyByteString
 import qualified Data.Word as Word
 
 type Content = ContentWith (List.List Frame.Frame)
@@ -31,7 +31,7 @@
 data ContentWith frames = Content
   { levels :: List.List Str.Str
   -- ^ This typically only has one element, like @stadium_oob_audio_map@.
-  , keyFrames :: List.List KeyFrame.KeyFrame
+  , keyframes :: List.List Keyframe.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.
@@ -66,7 +66,7 @@
 instance Json.FromJSON frames => Json.FromJSON (ContentWith frames) where
   parseJSON = Json.withObject "Content" $ \object -> do
     levels <- Json.required object "levels"
-    keyFrames <- Json.required object "key_frames"
+    keyframes <- Json.required object "key_frames"
     streamSize <- Json.required object "stream_size"
     frames <- Json.required object "frames"
     messages <- Json.required object "messages"
@@ -79,7 +79,7 @@
     unknown <- Json.required object "unknown"
     pure Content
       { levels
-      , keyFrames
+      , keyframes
       , streamSize
       , frames
       , messages
@@ -95,7 +95,7 @@
 instance Json.ToJSON frames => Json.ToJSON (ContentWith frames) where
   toJSON x = Json.object
     [ Json.pair "levels" $ levels x
-    , Json.pair "key_frames" $ keyFrames x
+    , Json.pair "key_frames" $ keyframes x
     , Json.pair "stream_size" $ streamSize x
     , Json.pair "frames" $ frames x
     , Json.pair "messages" $ messages x
@@ -111,7 +111,7 @@
 schema :: Schema.Schema -> Schema.Schema
 schema s = Schema.named "content" $ Schema.object
   [ (Json.pair "levels" . Schema.json $ List.schema Str.schema, True)
-  , (Json.pair "key_frames" . Schema.json $ List.schema KeyFrame.schema, True)
+  , (Json.pair "key_frames" . Schema.json $ List.schema Keyframe.schema, True)
   , (Json.pair "stream_size" $ Schema.ref U32.schema, True)
   , (Json.pair "frames" $ Schema.json s, True)
   , (Json.pair "messages" . Schema.json $ List.schema Message.schema, True)
@@ -130,7 +130,7 @@
 empty :: Content
 empty = Content
   { levels = List.empty
-  , keyFrames = List.empty
+  , keyframes = List.empty
   , streamSize = U32.fromWord32 0
   , frames = List.empty
   , messages = List.empty
@@ -146,7 +146,7 @@
 bytePut :: Content -> BytePut.BytePut
 bytePut x =
   List.bytePut Str.bytePut (levels x)
-    <> List.bytePut KeyFrame.bytePut (keyFrames x)
+    <> List.bytePut Keyframe.bytePut (keyframes x)
     <> putFrames x
     <> List.bytePut Message.bytePut (messages x)
     <> List.bytePut Mark.bytePut (marks x)
@@ -173,14 +173,16 @@
     -- Unforunately that isn't currently known. See this issue for details:
     -- <https://github.com/tfausak/rattletrap/issues/171>.
     expectedStreamSize = streamSize x
-    actualStreamSize = U32.fromWord32 . fromIntegral $ Bytes.length stream
+    actualStreamSize =
+      U32.fromWord32 . fromIntegral $ ByteString.length stream
     streamSize_ = U32.fromWord32
       $ max (U32.toWord32 expectedStreamSize) (U32.toWord32 actualStreamSize)
-  in U32.bytePut streamSize_ <> BytePut.byteString
-    (reverseBytes (padBytes (U32.toWord32 streamSize_) stream))
+  in U32.bytePut streamSize_
+    <> BytePut.byteString (Bytes.padBytes (U32.toWord32 streamSize_) stream)
 
 byteGet
-  :: Version.Version
+  :: Maybe Str.Str
+  -> Version.Version
   -- ^ Version numbers, usually from 'Rattletrap.Header.getVersion'.
   -> Int
   -- ^ The number of frames in the stream, usually from
@@ -189,42 +191,43 @@
   -- ^ The maximum number of channels in the stream, usually from
   -- 'Rattletrap.Header.getMaxChannels'.
   -> ByteGet.ByteGet Content
-byteGet version numFrames maxChannels = do
-  (levels_, keyFrames_, streamSize_) <-
-    (,,)
-    <$> List.byteGet Str.byteGet
-    <*> List.byteGet KeyFrame.byteGet
-    <*> U32.byteGet
-  (stream, messages_, marks_, packages_, objects_, names_, classMappings_, caches_) <-
-    (,,,,,,,)
-    <$> ByteGet.byteString (fromIntegral (U32.toWord32 streamSize_))
-    <*> List.byteGet Message.byteGet
-    <*> List.byteGet Mark.byteGet
-    <*> List.byteGet Str.byteGet
-    <*> List.byteGet Str.byteGet
-    <*> List.byteGet Str.byteGet
-    <*> List.byteGet ClassMapping.byteGet
-    <*> List.byteGet Cache.byteGet
+byteGet matchType version numFrames maxChannels = do
+  levels <- List.byteGet Str.byteGet
+  keyframes <- List.byteGet Keyframe.byteGet
+  streamSize <- U32.byteGet
+  stream <- ByteGet.byteString . fromIntegral $ U32.toWord32 streamSize
+  messages <- List.byteGet Message.byteGet
+  marks <- List.byteGet Mark.byteGet
+  packages <- List.byteGet Str.byteGet
+  objects <- List.byteGet Str.byteGet
+  names <- List.byteGet Str.byteGet
+  classMappings <- List.byteGet ClassMapping.byteGet
+  caches <- List.byteGet Cache.byteGet
   let
     classAttributeMap =
-      ClassAttributeMap.make objects_ classMappings_ caches_ names_
+      ClassAttributeMap.make objects classMappings caches names
     bitGet = State.evalStateT
-      (Frame.decodeFramesBits version numFrames maxChannels classAttributeMap)
+      (Frame.decodeFramesBits
+        matchType
+        version
+        numFrames
+        maxChannels
+        classAttributeMap
+      )
       mempty
-  frames_ <-
-    either fail pure . ByteGet.run (BitGet.toByteGet bitGet) $ reverseBytes
-      stream
-  Content
-      levels_
-      keyFrames_
-      streamSize_
-      frames_
-      messages_
-      marks_
-      packages_
-      objects_
-      names_
-      classMappings_
-      caches_
-    . LazyBytes.unpack
-    <$> ByteGet.remaining
+  frames <- either fail pure $ ByteGet.run (BitGet.toByteGet bitGet) stream
+  unknown <- fmap LazyByteString.unpack ByteGet.remaining
+  pure Content
+    { levels
+    , keyframes
+    , streamSize
+    , frames
+    , messages
+    , marks
+    , packages
+    , objects
+    , names
+    , classMappings
+    , caches
+    , unknown
+    }
diff --git a/src/lib/Rattletrap/Type/F32.hs b/src/lib/Rattletrap/Type/F32.hs
--- a/src/lib/Rattletrap/Type/F32.hs
+++ b/src/lib/Rattletrap/Type/F32.hs
@@ -33,7 +33,7 @@
 bitPut = BitPut.fromBytePut . bytePut
 
 byteGet :: ByteGet.ByteGet F32
-byteGet = fromFloat <$> ByteGet.float
+byteGet = fmap fromFloat ByteGet.float
 
 bitGet :: BitGet.BitGet F32
 bitGet = BitGet.fromByteGet byteGet 4
diff --git a/src/lib/Rattletrap/Type/Frame.hs b/src/lib/Rattletrap/Type/Frame.hs
--- a/src/lib/Rattletrap/Type/Frame.hs
+++ b/src/lib/Rattletrap/Type/Frame.hs
@@ -8,6 +8,7 @@
 import qualified Rattletrap.Type.F32 as F32
 import qualified Rattletrap.Type.List as List
 import qualified Rattletrap.Type.Replication as Replication
+import qualified Rattletrap.Type.Str as Str
 import qualified Rattletrap.Type.U32 as U32
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
@@ -59,7 +60,8 @@
     <> Replication.putReplications (replications frame)
 
 decodeFramesBits
-  :: Version.Version
+  :: Maybe Str.Str
+  -> Version.Version
   -> Int
   -> Word
   -> ClassAttributeMap.ClassAttributeMap
@@ -67,19 +69,24 @@
        (Map.Map CompressedWord.CompressedWord U32.U32)
        BitGet.BitGet
        (List.List Frame)
-decodeFramesBits version count limit classes =
-  List.replicateM count $ bitGet version limit classes
+decodeFramesBits matchType version count limit classes =
+  List.replicateM count $ bitGet matchType version limit classes
 
 bitGet
-  :: Version.Version
+  :: Maybe Str.Str
+  -> Version.Version
   -> Word
   -> ClassAttributeMap.ClassAttributeMap
   -> State.StateT
        (Map.Map CompressedWord.CompressedWord U32.U32)
        BitGet.BitGet
        Frame
-bitGet version limit classes =
-  Frame
-    <$> Trans.lift F32.bitGet
-    <*> Trans.lift F32.bitGet
-    <*> Replication.decodeReplicationsBits version limit classes
+bitGet matchType version limit classes = do
+  time <- Trans.lift F32.bitGet
+  delta <- Trans.lift F32.bitGet
+  replications <- Replication.decodeReplicationsBits
+    matchType
+    version
+    limit
+    classes
+  pure Frame { time, delta, replications }
diff --git a/src/lib/Rattletrap/Type/Header.hs b/src/lib/Rattletrap/Type/Header.hs
--- a/src/lib/Rattletrap/Type/Header.hs
+++ b/src/lib/Rattletrap/Type/Header.hs
@@ -7,17 +7,12 @@
 import qualified Rattletrap.Type.Property as Property
 import qualified Rattletrap.Type.Str as Str
 import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
-import Rattletrap.Utility.Monad
 
 -- | Contains high-level metadata about a 'Rattletrap.Replay.Replay'.
 data Header = Header
-  { engineVersion :: U32.U32
-  -- ^ The "major" ("engine") version number.
-  , licenseeVersion :: U32.U32
-  -- ^ The "minor" ("licensee") version number.
-  , patchVersion :: Maybe U32.U32
-  -- ^ The "patch" ("net") version number.
+  { version :: Version.Version
   , label :: Str.Str
   -- ^ Always @TAGame.Replay_Soccar_TA@.
   , properties :: Dictionary.Dictionary Property.Property
@@ -60,24 +55,26 @@
 
 instance Json.FromJSON Header where
   parseJSON = Json.withObject "Header" $ \object -> do
-    engineVersion <- Json.required object "engine_version"
-    licenseeVersion <- Json.required object "licensee_version"
-    patchVersion <- Json.optional object "patch_version"
+    major <- Json.required object "engine_version"
+    minor <- Json.required object "licensee_version"
+    patch <- Json.optional object "patch_version"
     label <- Json.required object "label"
     properties <- Json.required object "properties"
     pure Header
-      { engineVersion
-      , licenseeVersion
-      , patchVersion
+      { version = Version.Version
+        { Version.major
+        , Version.minor
+        , Version.patch
+        }
       , label
       , properties
       }
 
 instance Json.ToJSON Header where
   toJSON x = Json.object
-    [ Json.pair "engine_version" $ engineVersion x
-    , Json.pair "licensee_version" $ licenseeVersion x
-    , Json.pair "patch_version" $ patchVersion x
+    [ Json.pair "engine_version" . Version.major $ version x
+    , Json.pair "licensee_version" . Version.minor $ version x
+    , Json.pair "patch_version" . Version.patch $ version x
     , Json.pair "label" $ label x
     , Json.pair "properties" $ properties x
     ]
@@ -95,18 +92,13 @@
 
 bytePut :: Header -> BytePut.BytePut
 bytePut x =
-  U32.bytePut (engineVersion x)
-    <> U32.bytePut (licenseeVersion x)
-    <> foldMap U32.bytePut (patchVersion x)
-    <> Str.bytePut (label x)
-    <> Dictionary.bytePut Property.bytePut (properties x)
+  Version.bytePut (version x) <> Str.bytePut (label x) <> Dictionary.bytePut
+    Property.bytePut
+    (properties x)
 
 byteGet :: ByteGet.ByteGet Header
 byteGet = do
-  (major, minor) <- (,) <$> U32.byteGet <*> U32.byteGet
-  Header major minor
-    <$> whenMaybe
-          (U32.toWord32 major >= 868 && U32.toWord32 minor >= 18)
-          U32.byteGet
-    <*> Str.byteGet
-    <*> Dictionary.byteGet Property.byteGet
+  version <- Version.byteGet
+  label <- Str.byteGet
+  properties <- Dictionary.byteGet Property.byteGet
+  pure Header { version, label, properties }
diff --git a/src/lib/Rattletrap/Type/I32.hs b/src/lib/Rattletrap/Type/I32.hs
--- a/src/lib/Rattletrap/Type/I32.hs
+++ b/src/lib/Rattletrap/Type/I32.hs
@@ -38,7 +38,7 @@
 bitPut = BitPut.fromBytePut . bytePut
 
 byteGet :: ByteGet.ByteGet I32
-byteGet = fromInt32 <$> ByteGet.int32
+byteGet = fmap fromInt32 ByteGet.int32
 
 bitGet :: BitGet.BitGet I32
 bitGet = BitGet.fromByteGet byteGet 4
diff --git a/src/lib/Rattletrap/Type/I64.hs b/src/lib/Rattletrap/Type/I64.hs
--- a/src/lib/Rattletrap/Type/I64.hs
+++ b/src/lib/Rattletrap/Type/I64.hs
@@ -42,7 +42,7 @@
 bitPut = BitPut.fromBytePut . bytePut
 
 byteGet :: ByteGet.ByteGet I64
-byteGet = fromInt64 <$> ByteGet.int64
+byteGet = fmap fromInt64 ByteGet.int64
 
 bitGet :: BitGet.BitGet I64
 bitGet = BitGet.fromByteGet byteGet 8
diff --git a/src/lib/Rattletrap/Type/I8.hs b/src/lib/Rattletrap/Type/I8.hs
--- a/src/lib/Rattletrap/Type/I8.hs
+++ b/src/lib/Rattletrap/Type/I8.hs
@@ -38,7 +38,7 @@
 bitPut = BitPut.fromBytePut . bytePut
 
 byteGet :: ByteGet.ByteGet I8
-byteGet = fromInt8 <$> ByteGet.int8
+byteGet = fmap fromInt8 ByteGet.int8
 
 bitGet :: BitGet.BitGet I8
 bitGet = BitGet.fromByteGet byteGet 1
diff --git a/src/lib/Rattletrap/Type/Initialization.hs b/src/lib/Rattletrap/Type/Initialization.hs
--- a/src/lib/Rattletrap/Type/Initialization.hs
+++ b/src/lib/Rattletrap/Type/Initialization.hs
@@ -7,7 +7,7 @@
 import qualified Rattletrap.Type.Vector as Vector
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
-import Rattletrap.Utility.Monad
+import qualified Rattletrap.Utility.Monad as Monad
 
 data Initialization = Initialization
   { location :: Maybe Vector.Vector
@@ -43,7 +43,7 @@
     <> foldMap Int8Vector.bitPut (rotation initialization)
 
 bitGet :: Version.Version -> Bool -> Bool -> BitGet.BitGet Initialization
-bitGet version hasLocation hasRotation =
-  Initialization
-    <$> whenMaybe hasLocation (Vector.bitGet version)
-    <*> whenMaybe hasRotation Int8Vector.bitGet
+bitGet version hasLocation hasRotation = do
+  location <- Monad.whenMaybe hasLocation (Vector.bitGet version)
+  rotation <- Monad.whenMaybe hasRotation Int8Vector.bitGet
+  pure Initialization { location, rotation }
diff --git a/src/lib/Rattletrap/Type/Int8Vector.hs b/src/lib/Rattletrap/Type/Int8Vector.hs
--- a/src/lib/Rattletrap/Type/Int8Vector.hs
+++ b/src/lib/Rattletrap/Type/Int8Vector.hs
@@ -5,7 +5,7 @@
 import qualified Rattletrap.Schema as Schema
 import qualified Rattletrap.Type.I8 as I8
 import qualified Rattletrap.Utility.Json as Json
-import Rattletrap.Utility.Monad
+import qualified Rattletrap.Utility.Monad as Monad
 
 data Int8Vector = Int8Vector
   { x :: Maybe I8.I8
@@ -44,10 +44,13 @@
   Just field -> BitPut.bool True <> I8.bitPut field
 
 bitGet :: BitGet.BitGet Int8Vector
-bitGet =
-  Int8Vector <$> decodeFieldBits <*> decodeFieldBits <*> decodeFieldBits
+bitGet = do
+  x <- decodeFieldBits
+  y <- decodeFieldBits
+  z <- decodeFieldBits
+  pure Int8Vector { x, y, z }
 
 decodeFieldBits :: BitGet.BitGet (Maybe I8.I8)
 decodeFieldBits = do
   hasField <- BitGet.bool
-  whenMaybe hasField I8.bitGet
+  Monad.whenMaybe hasField I8.bitGet
diff --git a/src/lib/Rattletrap/Type/KeyFrame.hs b/src/lib/Rattletrap/Type/KeyFrame.hs
deleted file mode 100644
--- a/src/lib/Rattletrap/Type/KeyFrame.hs
+++ /dev/null
@@ -1,46 +0,0 @@
-module Rattletrap.Type.KeyFrame where
-
-import qualified Rattletrap.ByteGet as ByteGet
-import qualified Rattletrap.BytePut as BytePut
-import qualified Rattletrap.Schema as Schema
-import qualified Rattletrap.Type.F32 as F32
-import qualified Rattletrap.Type.U32 as U32
-import qualified Rattletrap.Utility.Json as Json
-
-data KeyFrame = KeyFrame
-  { time :: F32.F32
-  -- ^ When this key frame occurs, in seconds.
-  , frame :: U32.U32
-  -- ^ The frame number of this key frame, starting from 0.
-  , position :: U32.U32
-  -- ^ The bit position of this key frame in the stream.
-  }
-  deriving (Eq, Show)
-
-instance Json.FromJSON KeyFrame where
-  parseJSON = Json.withObject "KeyFrame" $ \object -> do
-    time <- Json.required object "time"
-    frame <- Json.required object "frame"
-    position <- Json.required object "position"
-    pure KeyFrame { time, frame, position }
-
-instance Json.ToJSON KeyFrame where
-  toJSON x = Json.object
-    [ Json.pair "time" $ time x
-    , Json.pair "frame" $ frame x
-    , Json.pair "position" $ position x
-    ]
-
-schema :: Schema.Schema
-schema = Schema.named "keyFrame" $ Schema.object
-  [ (Json.pair "time" $ Schema.ref F32.schema, True)
-  , (Json.pair "frame" $ Schema.ref U32.schema, True)
-  , (Json.pair "position" $ Schema.ref U32.schema, True)
-  ]
-
-bytePut :: KeyFrame -> BytePut.BytePut
-bytePut x =
-  F32.bytePut (time x) <> U32.bytePut (frame x) <> U32.bytePut (position x)
-
-byteGet :: ByteGet.ByteGet KeyFrame
-byteGet = KeyFrame <$> F32.byteGet <*> U32.byteGet <*> U32.byteGet
diff --git a/src/lib/Rattletrap/Type/Keyframe.hs b/src/lib/Rattletrap/Type/Keyframe.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Keyframe.hs
@@ -0,0 +1,50 @@
+module Rattletrap.Type.Keyframe where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.F32 as F32
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Json as Json
+
+data Keyframe = Keyframe
+  { time :: F32.F32
+  -- ^ When this key frame occurs, in seconds.
+  , frame :: U32.U32
+  -- ^ The frame number of this key frame, starting from 0.
+  , position :: U32.U32
+  -- ^ The bit position of this key frame in the stream.
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Keyframe where
+  parseJSON = Json.withObject "Keyframe" $ \object -> do
+    time <- Json.required object "time"
+    frame <- Json.required object "frame"
+    position <- Json.required object "position"
+    pure Keyframe { time, frame, position }
+
+instance Json.ToJSON Keyframe where
+  toJSON x = Json.object
+    [ Json.pair "time" $ time x
+    , Json.pair "frame" $ frame x
+    , Json.pair "position" $ position x
+    ]
+
+schema :: Schema.Schema
+schema = Schema.named "keyframe" $ Schema.object
+  [ (Json.pair "time" $ Schema.ref F32.schema, True)
+  , (Json.pair "frame" $ Schema.ref U32.schema, True)
+  , (Json.pair "position" $ Schema.ref U32.schema, True)
+  ]
+
+bytePut :: Keyframe -> BytePut.BytePut
+bytePut x =
+  F32.bytePut (time x) <> U32.bytePut (frame x) <> U32.bytePut (position x)
+
+byteGet :: ByteGet.ByteGet Keyframe
+byteGet = do
+  time <- F32.byteGet
+  frame <- U32.byteGet
+  position <- U32.byteGet
+  pure Keyframe { time, frame, position }
diff --git a/src/lib/Rattletrap/Type/Mark.hs b/src/lib/Rattletrap/Type/Mark.hs
--- a/src/lib/Rattletrap/Type/Mark.hs
+++ b/src/lib/Rattletrap/Type/Mark.hs
@@ -35,4 +35,7 @@
 bytePut x = Str.bytePut (value x) <> U32.bytePut (frame x)
 
 byteGet :: ByteGet.ByteGet Mark
-byteGet = Mark <$> Str.byteGet <*> U32.byteGet
+byteGet = do
+  value <- Str.byteGet
+  frame <- U32.byteGet
+  pure Mark { value, frame }
diff --git a/src/lib/Rattletrap/Type/Message.hs b/src/lib/Rattletrap/Type/Message.hs
--- a/src/lib/Rattletrap/Type/Message.hs
+++ b/src/lib/Rattletrap/Type/Message.hs
@@ -40,10 +40,11 @@
 
 bytePut :: Message -> BytePut.BytePut
 bytePut x =
-  do
-      U32.bytePut (frame x)
-    <> Str.bytePut (name x)
-    <> Str.bytePut (value x)
+  U32.bytePut (frame x) <> Str.bytePut (name x) <> Str.bytePut (value x)
 
 byteGet :: ByteGet.ByteGet Message
-byteGet = Message <$> U32.byteGet <*> Str.byteGet <*> Str.byteGet
+byteGet = do
+  frame <- U32.byteGet
+  name <- Str.byteGet
+  value <- Str.byteGet
+  pure Message { frame, name, value }
diff --git a/src/lib/Rattletrap/Type/Property.hs b/src/lib/Rattletrap/Type/Property.hs
--- a/src/lib/Rattletrap/Type/Property.hs
+++ b/src/lib/Rattletrap/Type/Property.hs
@@ -45,5 +45,7 @@
 
 byteGet :: ByteGet.ByteGet Property
 byteGet = do
-  kind_ <- Str.byteGet
-  Property kind_ <$> U64.byteGet <*> PropertyValue.byteGet byteGet kind_
+  kind <- Str.byteGet
+  size <- U64.byteGet
+  value <- PropertyValue.byteGet byteGet kind
+  pure Property { kind, size, value }
diff --git a/src/lib/Rattletrap/Type/Property/Array.hs b/src/lib/Rattletrap/Type/Property/Array.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/Array.hs
@@ -0,0 +1,35 @@
+module Rattletrap.Type.Property.Array where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Dictionary as Dictionary
+import qualified Rattletrap.Type.List as List
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Array a
+  = Array (List.List (Dictionary.Dictionary a))
+  deriving (Eq, Show)
+
+fromList :: List.List (Dictionary.Dictionary a) -> Array a
+fromList = Array
+
+toList :: Array a -> List.List (Dictionary.Dictionary a)
+toList (Array x) = x
+
+instance Json.FromJSON a => Json.FromJSON (Array a) where
+  parseJSON = fmap fromList . Json.parseJSON
+
+instance Json.ToJSON a => Json.ToJSON (Array a) where
+  toJSON = Json.toJSON . toList
+
+schema :: Schema.Schema -> Schema.Schema
+schema s =
+  Schema.named "property-array" . Schema.json . List.schema $ Dictionary.schema
+    s
+
+bytePut :: (a -> BytePut.BytePut) -> Array a -> BytePut.BytePut
+bytePut f = List.bytePut (Dictionary.bytePut f) . toList
+
+byteGet :: ByteGet.ByteGet a -> ByteGet.ByteGet (Array a)
+byteGet f = fmap fromList $ List.byteGet (Dictionary.byteGet f)
diff --git a/src/lib/Rattletrap/Type/Property/Bool.hs b/src/lib/Rattletrap/Type/Property/Bool.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/Bool.hs
@@ -0,0 +1,33 @@
+module Rattletrap.Type.Property.Bool where
+
+import Prelude hiding (Bool)
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U8 as U8
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Bool
+  = Bool U8.U8
+  deriving (Eq, Show)
+
+fromU8 :: U8.U8 -> Bool
+fromU8 = Bool
+
+toU8 :: Bool -> U8.U8
+toU8 (Bool x) = x
+
+instance Json.FromJSON Bool where
+  parseJSON = fmap fromU8 . Json.parseJSON
+
+instance Json.ToJSON Bool where
+  toJSON = Json.toJSON . toU8
+
+schema :: Schema.Schema
+schema = U8.schema
+
+bytePut :: Bool -> BytePut.BytePut
+bytePut = U8.bytePut . toU8
+
+byteGet :: ByteGet.ByteGet Bool
+byteGet = fmap fromU8 U8.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/Byte.hs b/src/lib/Rattletrap/Type/Property/Byte.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/Byte.hs
@@ -0,0 +1,37 @@
+module Rattletrap.Type.Property.Byte where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Utility.Json as Json
+import qualified Rattletrap.Utility.Monad as Monad
+
+data Byte = Byte
+  { key :: Str.Str
+  , value :: Maybe Str.Str
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Byte where
+  parseJSON json = do
+    (key, value) <- Json.parseJSON json
+    pure Byte { key, value }
+
+instance Json.ToJSON Byte where
+  toJSON byte = Json.toJSON (key byte, value byte)
+
+schema :: Schema.Schema
+schema = Schema.named "property-byte" $ Schema.tuple
+  [Schema.ref Str.schema, Schema.json $ Schema.maybe Str.schema]
+
+bytePut :: Byte -> BytePut.BytePut
+bytePut byte = Str.bytePut (key byte) <> foldMap Str.bytePut (value byte)
+
+byteGet :: ByteGet.ByteGet Byte
+byteGet = do
+  key <- Str.byteGet
+  value <- Monad.whenMaybe
+    (Str.toString key /= "OnlinePlatform_Steam")
+    Str.byteGet
+  pure Byte { key, value }
diff --git a/src/lib/Rattletrap/Type/Property/Float.hs b/src/lib/Rattletrap/Type/Property/Float.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/Float.hs
@@ -0,0 +1,33 @@
+module Rattletrap.Type.Property.Float where
+
+import Prelude hiding (Float)
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.F32 as F32
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Float
+  = Float F32.F32
+  deriving (Eq, Show)
+
+fromF32 :: F32.F32 -> Float
+fromF32 = Float
+
+toF32 :: Float -> F32.F32
+toF32 (Float x) = x
+
+instance Json.FromJSON Float where
+  parseJSON = fmap fromF32 . Json.parseJSON
+
+instance Json.ToJSON Float where
+  toJSON = Json.toJSON . toF32
+
+schema :: Schema.Schema
+schema = F32.schema
+
+bytePut :: Float -> BytePut.BytePut
+bytePut = F32.bytePut . toF32
+
+byteGet :: ByteGet.ByteGet Float
+byteGet = fmap fromF32 F32.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/Int.hs b/src/lib/Rattletrap/Type/Property/Int.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/Int.hs
@@ -0,0 +1,33 @@
+module Rattletrap.Type.Property.Int where
+
+import Prelude hiding (Int)
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.I32 as I32
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Int
+  = Int I32.I32
+  deriving (Eq, Show)
+
+fromI32 :: I32.I32 -> Int
+fromI32 = Int
+
+toI32 :: Int -> I32.I32
+toI32 (Int x) = x
+
+instance Json.FromJSON Int where
+  parseJSON = fmap fromI32 . Json.parseJSON
+
+instance Json.ToJSON Int where
+  toJSON = Json.toJSON . toI32
+
+schema :: Schema.Schema
+schema = I32.schema
+
+bytePut :: Int -> BytePut.BytePut
+bytePut = I32.bytePut . toI32
+
+byteGet :: ByteGet.ByteGet Int
+byteGet = fmap fromI32 I32.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/Name.hs b/src/lib/Rattletrap/Type/Property/Name.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/Name.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Type.Property.Name where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Name
+  = Name Str.Str
+  deriving (Eq, Show)
+
+fromStr :: Str.Str -> Name
+fromStr = Name
+
+toStr :: Name -> Str.Str
+toStr (Name x) = x
+
+instance Json.FromJSON Name where
+  parseJSON = fmap fromStr . Json.parseJSON
+
+instance Json.ToJSON Name where
+  toJSON = Json.toJSON . toStr
+
+schema :: Schema.Schema
+schema = Str.schema
+
+bytePut :: Name -> BytePut.BytePut
+bytePut = Str.bytePut . toStr
+
+byteGet :: ByteGet.ByteGet Name
+byteGet = fmap fromStr Str.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/QWord.hs b/src/lib/Rattletrap/Type/Property/QWord.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/QWord.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Type.Property.QWord where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U64 as U64
+import qualified Rattletrap.Utility.Json as Json
+
+newtype QWord
+  = QWord U64.U64
+  deriving (Eq, Show)
+
+fromU64 :: U64.U64 -> QWord
+fromU64 = QWord
+
+toU64 :: QWord -> U64.U64
+toU64 (QWord x) = x
+
+instance Json.FromJSON QWord where
+  parseJSON = fmap fromU64 . Json.parseJSON
+
+instance Json.ToJSON QWord where
+  toJSON = Json.toJSON . toU64
+
+schema :: Schema.Schema
+schema = U64.schema
+
+bytePut :: QWord -> BytePut.BytePut
+bytePut = U64.bytePut . toU64
+
+byteGet :: ByteGet.ByteGet QWord
+byteGet = fmap fromU64 U64.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/Str.hs b/src/lib/Rattletrap/Type/Property/Str.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/Property/Str.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Type.Property.Str where
+
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Str
+  = Str Str.Str
+  deriving (Eq, Show)
+
+fromStr :: Str.Str -> Str
+fromStr = Str
+
+toStr :: Str -> Str.Str
+toStr (Str x) = x
+
+instance Json.FromJSON Str where
+  parseJSON = fmap fromStr . Json.parseJSON
+
+instance Json.ToJSON Str where
+  toJSON = Json.toJSON . toStr
+
+schema :: Schema.Schema
+schema = Str.schema
+
+bytePut :: Str -> BytePut.BytePut
+bytePut = Str.bytePut . toStr
+
+byteGet :: ByteGet.ByteGet Str
+byteGet = fmap fromStr Str.byteGet
diff --git a/src/lib/Rattletrap/Type/PropertyValue.hs b/src/lib/Rattletrap/Type/PropertyValue.hs
--- a/src/lib/Rattletrap/Type/PropertyValue.hs
+++ b/src/lib/Rattletrap/Type/PropertyValue.hs
@@ -1,52 +1,52 @@
 module Rattletrap.Type.PropertyValue where
 
 import qualified Data.Foldable as Foldable
-import qualified Data.Text as Text
 import qualified Rattletrap.ByteGet as ByteGet
 import qualified Rattletrap.BytePut as BytePut
 import qualified Rattletrap.Schema as Schema
-import qualified Rattletrap.Type.Dictionary as Dictionary
-import qualified Rattletrap.Type.F32 as F32
-import qualified Rattletrap.Type.I32 as I32
-import qualified Rattletrap.Type.List as List
+import qualified Rattletrap.Type.Property.Array as Property.Array
+import qualified Rattletrap.Type.Property.Bool as Property.Bool
+import qualified Rattletrap.Type.Property.Byte as Property.Byte
+import qualified Rattletrap.Type.Property.Float as Property.Float
+import qualified Rattletrap.Type.Property.Int as Property.Int
+import qualified Rattletrap.Type.Property.Name as Property.Name
+import qualified Rattletrap.Type.Property.QWord as Property.QWord
+import qualified Rattletrap.Type.Property.Str as Property.Str
 import qualified Rattletrap.Type.Str as Str
-import qualified Rattletrap.Type.U64 as U64
-import qualified Rattletrap.Type.U8 as U8
 import qualified Rattletrap.Utility.Json as Json
-import Rattletrap.Utility.Monad
 
 data PropertyValue a
-  = Array (List.List (Dictionary.Dictionary a))
+  = Array (Property.Array.Array a)
   -- ^ Yes, a list of dictionaries. No, it doesn't make sense. These usually
   -- only have one element.
-  | Bool U8.U8
-  | Byte Str.Str (Maybe Str.Str)
+  | Bool Property.Bool.Bool
+  | Byte Property.Byte.Byte
   -- ^ This is a strange name for essentially a key-value pair.
-  | Float F32.F32
-  | Int I32.I32
-  | Name Str.Str
+  | Float Property.Float.Float
+  | Int Property.Int.Int
+  | Name Property.Name.Name
   -- ^ It's unclear how exactly this is different than a 'StrProperty'.
-  | QWord U64.U64
-  | Str Str.Str
+  | QWord Property.QWord.QWord
+  | Str Property.Str.Str
   deriving (Eq, Show)
 
 instance Json.FromJSON a => Json.FromJSON (PropertyValue a) where
   parseJSON = Json.withObject "PropertyValue" $ \object -> Foldable.asum
-    [ Array <$> Json.required object "array"
-    , Bool <$> Json.required object "bool"
-    , uncurry Byte <$> Json.required object "byte"
-    , Float <$> Json.required object "float"
-    , Int <$> Json.required object "int"
-    , Name <$> Json.required object "name"
-    , QWord <$> Json.required object "q_word"
-    , Str <$> Json.required object "str"
+    [ fmap Array $ Json.required object "array"
+    , fmap Bool $ Json.required object "bool"
+    , fmap Byte $ Json.required object "byte"
+    , fmap Float $ Json.required object "float"
+    , fmap Int $ Json.required object "int"
+    , fmap Name $ Json.required object "name"
+    , fmap QWord $ Json.required object "q_word"
+    , fmap Str $ Json.required object "str"
     ]
 
 instance Json.ToJSON a => Json.ToJSON (PropertyValue a) where
   toJSON x = case x of
     Array y -> Json.object [Json.pair "array" y]
     Bool y -> Json.object [Json.pair "bool" y]
-    Byte y z -> Json.object [Json.pair "byte" (y, z)]
+    Byte y -> Json.object [Json.pair "byte" y]
     Float y -> Json.object [Json.pair "float" y]
     Int y -> Json.object [Json.pair "int" y]
     Name y -> Json.object [Json.pair "name" y]
@@ -54,45 +54,37 @@
     Str y -> Json.object [Json.pair "str" y]
 
 schema :: Schema.Schema -> Schema.Schema
-schema s =
-  Schema.named ("property-value-" <> Text.unpack (Schema.name s))
-    . Schema.oneOf
-    $ fmap
-        (\(k, v) -> Schema.object [(Json.pair k v, True)])
-        [ ("array", Schema.json . List.schema $ Dictionary.schema s)
-        , ("bool", Schema.ref U8.schema)
-        , ( "byte"
-          , Schema.tuple
-            [Schema.ref Str.schema, Schema.json $ Schema.maybe Str.schema]
-          )
-        , ("float", Schema.ref F32.schema)
-        , ("int", Schema.ref I32.schema)
-        , ("name", Schema.ref Str.schema)
-        , ("q_word", Schema.ref U64.schema)
-        , ("str", Schema.ref Str.schema)
-        ]
+schema s = Schema.named "property-value" . Schema.oneOf $ fmap
+  (\(k, v) -> Schema.object [(Json.pair k v, True)])
+  [ ("array", Schema.ref $ Property.Array.schema s)
+  , ("bool", Schema.ref Property.Bool.schema)
+  , ("byte", Schema.ref Property.Byte.schema)
+  , ("float", Schema.ref Property.Float.schema)
+  , ("int", Schema.ref Property.Int.schema)
+  , ("name", Schema.ref Property.Name.schema)
+  , ("q_word", Schema.ref Property.QWord.schema)
+  , ("str", Schema.ref Property.Str.schema)
+  ]
 
 bytePut :: (a -> BytePut.BytePut) -> PropertyValue a -> BytePut.BytePut
 bytePut putProperty value = case value of
-  Array x -> List.bytePut (Dictionary.bytePut putProperty) x
-  Bool x -> U8.bytePut x
-  Byte k mv -> Str.bytePut k <> foldMap Str.bytePut mv
-  Float x -> F32.bytePut x
-  Int x -> I32.bytePut x
-  Name x -> Str.bytePut x
-  QWord x -> U64.bytePut x
-  Str x -> Str.bytePut x
+  Array x -> Property.Array.bytePut putProperty x
+  Bool x -> Property.Bool.bytePut x
+  Byte x -> Property.Byte.bytePut x
+  Float x -> Property.Float.bytePut x
+  Int x -> Property.Int.bytePut x
+  Name x -> Property.Name.bytePut x
+  QWord x -> Property.QWord.bytePut x
+  Str x -> Property.Str.bytePut x
 
 byteGet :: ByteGet.ByteGet a -> Str.Str -> ByteGet.ByteGet (PropertyValue a)
 byteGet getProperty kind = case Str.toString kind of
-  "ArrayProperty" -> Array <$> List.byteGet (Dictionary.byteGet getProperty)
-  "BoolProperty" -> Bool <$> U8.byteGet
-  "ByteProperty" -> do
-    k <- Str.byteGet
-    Byte k <$> whenMaybe (Str.toString k /= "OnlinePlatform_Steam") Str.byteGet
-  "FloatProperty" -> Float <$> F32.byteGet
-  "IntProperty" -> Int <$> I32.byteGet
-  "NameProperty" -> Name <$> Str.byteGet
-  "QWordProperty" -> QWord <$> U64.byteGet
-  "StrProperty" -> Str <$> Str.byteGet
+  "ArrayProperty" -> fmap Array $ Property.Array.byteGet getProperty
+  "BoolProperty" -> fmap Bool Property.Bool.byteGet
+  "ByteProperty" -> fmap Byte Property.Byte.byteGet
+  "FloatProperty" -> fmap Float Property.Float.byteGet
+  "IntProperty" -> fmap Int Property.Int.byteGet
+  "NameProperty" -> fmap Name Property.Name.byteGet
+  "QWordProperty" -> fmap QWord Property.QWord.byteGet
+  "StrProperty" -> fmap Str Property.Str.byteGet
   _ -> fail ("[RT07] don't know how to read property value " <> show kind)
diff --git a/src/lib/Rattletrap/Type/Quaternion.hs b/src/lib/Rattletrap/Type/Quaternion.hs
--- a/src/lib/Rattletrap/Type/Quaternion.hs
+++ b/src/lib/Rattletrap/Type/Quaternion.hs
@@ -144,8 +144,12 @@
 putPart = CompressedWord.bitPut . compressPart
 
 bitGet :: BitGet.BitGet Quaternion
-bitGet =
-  toQuaternion <$> decodeComponent <*> decodePart <*> decodePart <*> decodePart
+bitGet = do
+  component <- decodeComponent
+  a <- decodePart
+  b <- decodePart
+  c <- decodePart
+  pure $ toQuaternion component a b c
 
 decodeComponent :: BitGet.BitGet Component
 decodeComponent = do
@@ -158,4 +162,4 @@
     y_ -> fail ("[RT08] invalid component: " <> show y_)
 
 decodePart :: BitGet.BitGet Double
-decodePart = decompressPart <$> CompressedWord.bitGet maxCompressedValue
+decodePart = fmap decompressPart $ CompressedWord.bitGet maxCompressedValue
diff --git a/src/lib/Rattletrap/Type/RemoteId.hs b/src/lib/Rattletrap/Type/RemoteId.hs
--- a/src/lib/Rattletrap/Type/RemoteId.hs
+++ b/src/lib/Rattletrap/Type/RemoteId.hs
@@ -3,133 +3,80 @@
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
 import qualified Rattletrap.Schema as Schema
-import qualified Rattletrap.Type.Str as Str
-import qualified Rattletrap.Type.U64 as U64
+import qualified Rattletrap.Type.RemoteId.Epic as Epic
+import qualified Rattletrap.Type.RemoteId.PlayStation as PlayStation
+import qualified Rattletrap.Type.RemoteId.PsyNet as PsyNet
+import qualified Rattletrap.Type.RemoteId.Splitscreen as Splitscreen
+import qualified Rattletrap.Type.RemoteId.Steam as Steam
+import qualified Rattletrap.Type.RemoteId.Switch as Switch
+import qualified Rattletrap.Type.RemoteId.Xbox as Xbox
 import qualified Rattletrap.Type.U8 as U8
 import qualified Rattletrap.Type.Version as Version
-import Rattletrap.Utility.Bytes
 import qualified Rattletrap.Utility.Json as Json
 
-import qualified Data.ByteString as Bytes
 import qualified Data.Foldable as Foldable
-import qualified Data.Text as Text
-import qualified Data.Text.Encoding as Text
-import qualified Data.Word as Word
 
 data RemoteId
-  = PlayStation Text.Text [Word.Word8]
-  | PsyNet (Either U64.U64 (U64.U64, U64.U64, U64.U64, U64.U64))
-  | Splitscreen Word.Word32
+  = PlayStation PlayStation.PlayStation
+  | PsyNet PsyNet.PsyNet
+  | Splitscreen Splitscreen.Splitscreen
   -- ^ Really only 24 bits.
-  | Steam U64.U64
-  | Switch U64.U64 U64.U64 U64.U64 U64.U64
-  | Xbox U64.U64
-  | Epic Str.Str
+  | Steam Steam.Steam
+  | Switch Switch.Switch
+  | Xbox Xbox.Xbox
+  | Epic Epic.Epic
   deriving (Eq, Show)
 
 instance Json.FromJSON RemoteId where
   parseJSON = Json.withObject "RemoteId" $ \object -> Foldable.asum
-    [ uncurry PlayStation <$> Json.required object "play_station"
-    , PsyNet <$> Json.required object "psy_net"
-    , Splitscreen <$> Json.required object "splitscreen"
-    , Steam <$> Json.required object "steam"
-    , uncurry4 Switch <$> Json.required object "switch"
-    , Xbox <$> Json.required object "xbox"
-    , Epic <$> Json.required object "epic"
+    [ fmap PlayStation $ Json.required object "play_station"
+    , fmap PsyNet $ Json.required object "psy_net"
+    , fmap Splitscreen $ Json.required object "splitscreen"
+    , fmap Steam $ Json.required object "steam"
+    , fmap Switch $ Json.required object "switch"
+    , fmap Xbox $ Json.required object "xbox"
+    , fmap Epic $ Json.required object "epic"
     ]
 
-uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
-uncurry4 f (a, b, c, d) = f a b c d
-
 instance Json.ToJSON RemoteId where
   toJSON x = case x of
-    PlayStation y z -> Json.object [Json.pair "play_station" (y, z)]
+    PlayStation y -> Json.object [Json.pair "play_station" y]
     PsyNet y -> Json.object [Json.pair "psy_net" y]
     Splitscreen y -> Json.object [Json.pair "splitscreen" y]
     Steam y -> Json.object [Json.pair "steam" y]
-    Switch y z a b -> Json.object [Json.pair "switch" (y, z, a, b)]
+    Switch y -> Json.object [Json.pair "switch" y]
     Xbox y -> Json.object [Json.pair "xbox" y]
     Epic y -> Json.object [Json.pair "epic" y]
 
 schema :: Schema.Schema
 schema = Schema.named "remote-id" . Schema.oneOf $ fmap
   (\(k, v) -> Schema.object [(Json.pair k v, True)])
-  [ ( "play_station"
-    , Schema.tuple
-      [Schema.ref Schema.string, Schema.json $ Schema.array Schema.number]
-    )
-  , ( "psy_net"
-    , Schema.oneOf
-      [ Schema.object [(Json.pair "Left" $ Schema.ref U64.schema, True)]
-      , Schema.object
-        [ ( Json.pair "Right" . Schema.tuple . replicate 4 $ Schema.ref
-            U64.schema
-          , True
-          )
-        ]
-      ]
-    )
-  , ("splitscreen", Schema.ref Schema.integer)
-  , ("steam", Schema.ref U64.schema)
-  , ("switch", Schema.tuple . replicate 4 $ Schema.ref U64.schema)
-  , ("xbox", Schema.ref U64.schema)
-  , ("epic", Schema.ref Str.schema)
+  [ ("play_station", Schema.ref PlayStation.schema)
+  , ("psy_net", Schema.ref PsyNet.schema)
+  , ("splitscreen", Schema.ref Splitscreen.schema)
+  , ("steam", Schema.ref Steam.schema)
+  , ("switch", Schema.ref Switch.schema)
+  , ("xbox", Schema.ref Xbox.schema)
+  , ("epic", Schema.ref Epic.schema)
   ]
 
 bitPut :: RemoteId -> BitPut.BitPut
 bitPut remoteId = case remoteId of
-  PlayStation name bytes ->
-    let rawName = reverseBytes (padBytes (16 :: Int) (encodeLatin1 name))
-    in BitPut.byteString rawName <> BitPut.byteString (Bytes.pack bytes)
-  PsyNet e -> case e of
-    Left l -> U64.bitPut l
-    Right (a, b, c, d) -> putWord256 a b c d
-  Splitscreen word24 -> BitPut.bits 24 word24
-  Steam word64 -> U64.bitPut word64
-  Switch a b c d -> putWord256 a b c d
-  Xbox word64 -> U64.bitPut word64
-  Epic str -> Str.bitPut str
-
-putWord256 :: U64.U64 -> U64.U64 -> U64.U64 -> U64.U64 -> BitPut.BitPut
-putWord256 a b c d =
-  U64.bitPut a <> U64.bitPut b <> U64.bitPut c <> U64.bitPut d
+  PlayStation x -> PlayStation.bitPut x
+  PsyNet x -> PsyNet.bitPut x
+  Splitscreen x -> Splitscreen.bitPut x
+  Steam x -> Steam.bitPut x
+  Switch x -> Switch.bitPut x
+  Xbox x -> Xbox.bitPut x
+  Epic x -> Epic.bitPut x
 
 bitGet :: Version.Version -> U8.U8 -> BitGet.BitGet RemoteId
 bitGet version systemId = case U8.toWord8 systemId of
-  0 -> Splitscreen <$> BitGet.bits 24
-  1 -> Steam <$> U64.bitGet
-  2 -> PlayStation <$> decodePsName <*> decodePsBytes version
-  4 -> Xbox <$> U64.bitGet
-  6 -> do
-    (a, b, c, d) <- getWord256
-    pure $ Switch a b c d
-  7 -> if psyNetIsU64 version
-    then PsyNet . Left <$> U64.bitGet
-    else PsyNet . Right <$> getWord256
-  11 -> Epic <$> Str.bitGet
+  0 -> fmap Splitscreen Splitscreen.bitGet
+  1 -> fmap Steam Steam.bitGet
+  2 -> fmap PlayStation $ PlayStation.bitGet version
+  4 -> fmap Xbox Xbox.bitGet
+  6 -> fmap Switch Switch.bitGet
+  7 -> fmap PsyNet $ PsyNet.bitGet version
+  11 -> fmap Epic Epic.bitGet
   _ -> fail ("[RT09] unknown system id " <> show systemId)
-
-psyNetIsU64 :: Version.Version -> Bool
-psyNetIsU64 v =
-  Version.major v >= 868 && Version.minor v >= 24 && Version.patch v >= 10
-
-decodePsName :: BitGet.BitGet Text.Text
-decodePsName = fmap
-  (Text.dropWhileEnd (== '\x00') . Text.decodeLatin1 . reverseBytes)
-  (BitGet.byteString 16)
-
-decodePsBytes :: Version.Version -> BitGet.BitGet [Word.Word8]
-decodePsBytes version = Bytes.unpack
-  <$> BitGet.byteString (if playStationIsU24 version then 24 else 16)
-
-playStationIsU24 :: Version.Version -> Bool
-playStationIsU24 v =
-  Version.major v >= 868 && Version.minor v >= 20 && Version.patch v >= 1
-
-getWord256 :: BitGet.BitGet (U64.U64, U64.U64, U64.U64, U64.U64)
-getWord256 = do
-  a <- U64.bitGet
-  b <- U64.bitGet
-  c <- U64.bitGet
-  d <- U64.bitGet
-  pure (a, b, c, d)
diff --git a/src/lib/Rattletrap/Type/RemoteId/Epic.hs b/src/lib/Rattletrap/Type/RemoteId/Epic.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/RemoteId/Epic.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Type.RemoteId.Epic where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Str as Str
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Epic
+  = Epic Str.Str
+  deriving (Eq, Show)
+
+instance Json.FromJSON Epic where
+  parseJSON = fmap fromStr . Json.parseJSON
+
+instance Json.ToJSON Epic where
+  toJSON = Json.toJSON . toStr
+
+fromStr :: Str.Str -> Epic
+fromStr = Epic
+
+toStr :: Epic -> Str.Str
+toStr (Epic x) = x
+
+schema :: Schema.Schema
+schema = Str.schema
+
+bitPut :: Epic -> BitPut.BitPut
+bitPut = Str.bitPut . toStr
+
+bitGet :: BitGet.BitGet Epic
+bitGet = fmap fromStr Str.bitGet
diff --git a/src/lib/Rattletrap/Type/RemoteId/PlayStation.hs b/src/lib/Rattletrap/Type/RemoteId/PlayStation.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/RemoteId/PlayStation.hs
@@ -0,0 +1,53 @@
+module Rattletrap.Type.RemoteId.PlayStation where
+
+import qualified Data.ByteString as ByteString
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+import qualified Data.Word as Word
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Bytes as Bytes
+import qualified Rattletrap.Utility.Json as Json
+
+data PlayStation = PlayStation
+  { name :: Text.Text
+  , code :: [Word.Word8]
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON PlayStation where
+  parseJSON json = do
+    (name, code) <- Json.parseJSON json
+    pure PlayStation { name, code }
+
+instance Json.ToJSON PlayStation where
+  toJSON x = Json.toJSON (name x, code x)
+
+schema :: Schema.Schema
+schema = Schema.named "remote-id-play-station" $ Schema.tuple
+  [Schema.ref Schema.string, Schema.json $ Schema.array Schema.number]
+
+bitPut :: PlayStation -> BitPut.BitPut
+bitPut x =
+  let
+    nameBytes = Bytes.padBytes (16 :: Int) . Bytes.encodeLatin1 $ name x
+    codeBytes = ByteString.pack $ code x
+  in BitPut.byteString nameBytes <> BitPut.byteString codeBytes
+
+bitGet :: Version.Version -> BitGet.BitGet PlayStation
+bitGet version = do
+  name <- getCode
+  code <- getName version
+  pure PlayStation { name, code }
+
+getCode :: BitGet.BitGet Text.Text
+getCode = fmap (Text.dropWhileEnd (== '\x00') . Text.decodeLatin1)
+  $ BitGet.byteString 16
+
+getName :: Version.Version -> BitGet.BitGet [Word.Word8]
+getName version =
+  fmap ByteString.unpack
+    . BitGet.byteString
+    $ if Version.atLeast 868 20 1 version then 24 else 16
diff --git a/src/lib/Rattletrap/Type/RemoteId/PsyNet.hs b/src/lib/Rattletrap/Type/RemoteId/PsyNet.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/RemoteId/PsyNet.hs
@@ -0,0 +1,50 @@
+module Rattletrap.Type.RemoteId.PsyNet where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U64 as U64
+import qualified Rattletrap.Type.Version as Version
+import qualified Rattletrap.Utility.Json as Json
+
+newtype PsyNet
+  = PsyNet (Either U64.U64 (U64.U64, U64.U64, U64.U64, U64.U64))
+  deriving (Eq, Show)
+
+instance Json.FromJSON PsyNet where
+  parseJSON = fmap fromEither . Json.parseJSON
+
+instance Json.ToJSON PsyNet where
+  toJSON = Json.toJSON . toEither
+
+fromEither :: Either U64.U64 (U64.U64, U64.U64, U64.U64, U64.U64) -> PsyNet
+fromEither = PsyNet
+
+toEither :: PsyNet -> Either U64.U64 (U64.U64, U64.U64, U64.U64, U64.U64)
+toEither (PsyNet x) = x
+
+schema :: Schema.Schema
+schema = Schema.named "remote-id-psy-net" $ Schema.oneOf
+  [ Schema.object [(Json.pair "Left" $ Schema.ref U64.schema, True)]
+  , Schema.object
+    [ ( Json.pair "Right" . Schema.tuple . replicate 4 $ Schema.ref U64.schema
+      , True
+      )
+    ]
+  ]
+
+bitPut :: PsyNet -> BitPut.BitPut
+bitPut x = case toEither x of
+  Left l -> U64.bitPut l
+  Right (a, b, c, d) ->
+    U64.bitPut a <> U64.bitPut b <> U64.bitPut c <> U64.bitPut d
+
+bitGet :: Version.Version -> BitGet.BitGet PsyNet
+bitGet version = fmap fromEither $ if Version.atLeast 868 24 10 version
+  then fmap Left U64.bitGet
+  else fmap Right $ do
+    a <- U64.bitGet
+    b <- U64.bitGet
+    c <- U64.bitGet
+    d <- U64.bitGet
+    pure (a, b, c, d)
diff --git a/src/lib/Rattletrap/Type/RemoteId/Splitscreen.hs b/src/lib/Rattletrap/Type/RemoteId/Splitscreen.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/RemoteId/Splitscreen.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Type.RemoteId.Splitscreen where
+
+import qualified Data.Word as Word
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Splitscreen
+  = Splitscreen Word.Word32
+  deriving (Eq, Show)
+
+instance Json.FromJSON Splitscreen where
+  parseJSON = fmap fromWord32 . Json.parseJSON
+
+instance Json.ToJSON Splitscreen where
+  toJSON = Json.toJSON . toWord32
+
+fromWord32 :: Word.Word32 -> Splitscreen
+fromWord32 = Splitscreen
+
+toWord32 :: Splitscreen -> Word.Word32
+toWord32 (Splitscreen x) = x
+
+schema :: Schema.Schema
+schema = Schema.integer
+
+bitPut :: Splitscreen -> BitPut.BitPut
+bitPut = BitPut.bits 24 . toWord32
+
+bitGet :: BitGet.BitGet Splitscreen
+bitGet = fmap fromWord32 $ BitGet.bits 24
diff --git a/src/lib/Rattletrap/Type/RemoteId/Steam.hs b/src/lib/Rattletrap/Type/RemoteId/Steam.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/RemoteId/Steam.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Type.RemoteId.Steam where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U64 as U64
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Steam
+  = Steam U64.U64
+  deriving (Eq, Show)
+
+instance Json.FromJSON Steam where
+  parseJSON = fmap fromU64 . Json.parseJSON
+
+instance Json.ToJSON Steam where
+  toJSON = Json.toJSON . toU64
+
+fromU64 :: U64.U64 -> Steam
+fromU64 = Steam
+
+toU64 :: Steam -> U64.U64
+toU64 (Steam x) = x
+
+schema :: Schema.Schema
+schema = U64.schema
+
+bitPut :: Steam -> BitPut.BitPut
+bitPut = U64.bitPut . toU64
+
+bitGet :: BitGet.BitGet Steam
+bitGet = fmap fromU64 U64.bitGet
diff --git a/src/lib/Rattletrap/Type/RemoteId/Switch.hs b/src/lib/Rattletrap/Type/RemoteId/Switch.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/RemoteId/Switch.hs
@@ -0,0 +1,40 @@
+module Rattletrap.Type.RemoteId.Switch where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U64 as U64
+import qualified Rattletrap.Utility.Json as Json
+
+data Switch = Switch
+  { a :: U64.U64
+  , b :: U64.U64
+  , c :: U64.U64
+  , d :: U64.U64
+  }
+  deriving (Eq, Show)
+
+instance Json.FromJSON Switch where
+  parseJSON json = do
+    (a, b, c, d) <- Json.parseJSON json
+    pure Switch { a, b, c, d }
+
+instance Json.ToJSON Switch where
+  toJSON x = Json.toJSON (a x, b x, c x, d x)
+
+schema :: Schema.Schema
+schema =
+  Schema.named "remote-id-switch" . Schema.tuple . replicate 4 $ Schema.ref
+    U64.schema
+
+bitPut :: Switch -> BitPut.BitPut
+bitPut x =
+  U64.bitPut (a x) <> U64.bitPut (b x) <> U64.bitPut (c x) <> U64.bitPut (d x)
+
+bitGet :: BitGet.BitGet Switch
+bitGet = do
+  a <- U64.bitGet
+  b <- U64.bitGet
+  c <- U64.bitGet
+  d <- U64.bitGet
+  pure Switch { a, b, c, d }
diff --git a/src/lib/Rattletrap/Type/RemoteId/Xbox.hs b/src/lib/Rattletrap/Type/RemoteId/Xbox.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Type/RemoteId/Xbox.hs
@@ -0,0 +1,32 @@
+module Rattletrap.Type.RemoteId.Xbox where
+
+import qualified Rattletrap.BitGet as BitGet
+import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Schema as Schema
+import qualified Rattletrap.Type.U64 as U64
+import qualified Rattletrap.Utility.Json as Json
+
+newtype Xbox
+  = Xbox U64.U64
+  deriving (Eq, Show)
+
+instance Json.FromJSON Xbox where
+  parseJSON = fmap fromU64 . Json.parseJSON
+
+instance Json.ToJSON Xbox where
+  toJSON = Json.toJSON . toU64
+
+fromU64 :: U64.U64 -> Xbox
+fromU64 = Xbox
+
+toU64 :: Xbox -> U64.U64
+toU64 (Xbox x) = x
+
+schema :: Schema.Schema
+schema = U64.schema
+
+bitPut :: Xbox -> BitPut.BitPut
+bitPut = U64.bitPut . toU64
+
+bitGet :: BitGet.BitGet Xbox
+bitGet = fmap fromU64 U64.bitGet
diff --git a/src/lib/Rattletrap/Type/Replay.hs b/src/lib/Rattletrap/Type/Replay.hs
--- a/src/lib/Rattletrap/Type/Replay.hs
+++ b/src/lib/Rattletrap/Type/Replay.hs
@@ -8,11 +8,12 @@
 import qualified Rattletrap.Type.Header as Header
 import qualified Rattletrap.Type.I32 as I32
 import qualified Rattletrap.Type.Property as Property
+import qualified Rattletrap.Type.Property.Int as Property.Int
+import qualified Rattletrap.Type.Property.Name as Property.Name
 import qualified Rattletrap.Type.PropertyValue as PropertyValue
 import qualified Rattletrap.Type.Section as Section
 import qualified Rattletrap.Type.Str as Str
 import qualified Rattletrap.Type.U32 as U32
-import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
 import qualified Rattletrap.Version as Version
 
@@ -76,30 +77,19 @@
     }
 
 getContent :: Header.Header -> ByteGet.ByteGet Content.Content
-getContent h =
-  Content.byteGet (getVersion h) (getNumFrames h) (getMaxChannels h)
-
-getVersion :: Header.Header -> Version.Version
-getVersion x = Version.Version
-  { Version.major = fromIntegral . U32.toWord32 $ Header.engineVersion x
-  , Version.minor = fromIntegral . U32.toWord32 $ Header.licenseeVersion x
-  , Version.patch = getPatchVersion x
-  }
+getContent h = Content.byteGet
+  (getMatchType h)
+  (Header.version h)
+  (getNumFrames h)
+  (getMaxChannels h)
 
-getPatchVersion :: Header.Header -> Int
-getPatchVersion header_ = case Header.patchVersion header_ of
-  Just version -> fromIntegral (U32.toWord32 version)
-  Nothing ->
-    case
-        Dictionary.lookup
-          (Str.fromString "MatchType")
-          (Header.properties header_)
-      of
-      -- This is an ugly, ugly hack to handle replays from season 2 of RLCS.
-      -- See `decodeSpawnedReplicationBits` and #85.
-        Just Property.Property { Property.value = PropertyValue.Name str }
-          | Str.toString str == "Lan" -> -1
-        _ -> 0
+getMatchType :: Header.Header -> Maybe Str.Str
+getMatchType header = do
+  Property.Property { Property.value } <-
+    Dictionary.lookup (Str.fromString "MatchType") $ Header.properties header
+  case value of
+    PropertyValue.Name x -> Just $ Property.Name.toStr x
+    _ -> Nothing
 
 getNumFrames :: Header.Header -> Int
 getNumFrames header_ =
@@ -109,7 +99,7 @@
         (Header.properties header_)
     of
       Just (Property.Property _ _ (PropertyValue.Int numFrames)) ->
-        fromIntegral (I32.toInt32 numFrames)
+        fromIntegral (I32.toInt32 (Property.Int.toI32 numFrames))
       _ -> 0
 
 getMaxChannels :: Header.Header -> Word
@@ -119,6 +109,6 @@
         (Str.fromString "MaxChannels")
         (Header.properties header_)
     of
-      Just (Property.Property _ _ (PropertyValue.Int numFrames)) ->
-        fromIntegral (I32.toInt32 numFrames)
+      Just (Property.Property _ _ (PropertyValue.Int maxChannels)) ->
+        fromIntegral (I32.toInt32 (Property.Int.toI32 maxChannels))
       _ -> 1023
diff --git a/src/lib/Rattletrap/Type/Replication.hs b/src/lib/Rattletrap/Type/Replication.hs
--- a/src/lib/Rattletrap/Type/Replication.hs
+++ b/src/lib/Rattletrap/Type/Replication.hs
@@ -7,9 +7,11 @@
 import qualified Rattletrap.Type.CompressedWord as CompressedWord
 import qualified Rattletrap.Type.List as List
 import qualified Rattletrap.Type.ReplicationValue as ReplicationValue
+import qualified Rattletrap.Type.Str as Str
 import qualified Rattletrap.Type.U32 as U32
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
+import qualified Rattletrap.Utility.Monad as Monad
 
 import qualified Control.Monad.Trans.Class as Trans
 import qualified Control.Monad.Trans.State as State
@@ -47,25 +49,28 @@
   <> ReplicationValue.bitPut (value replication)
 
 decodeReplicationsBits
-  :: Version.Version
+  :: Maybe Str.Str
+  -> Version.Version
   -> Word
   -> ClassAttributeMap.ClassAttributeMap
   -> State.StateT
        (Map.Map CompressedWord.CompressedWord U32.U32)
        BitGet.BitGet
        (List.List Replication)
-decodeReplicationsBits version limit classes = List.untilM $ do
+decodeReplicationsBits matchType version limit classes = List.untilM $ do
   p <- Trans.lift BitGet.bool
-  if p then Just <$> bitGet version limit classes else pure Nothing
+  Monad.whenMaybe p $ bitGet matchType version limit classes
 
 bitGet
-  :: Version.Version
+  :: Maybe Str.Str
+  -> Version.Version
   -> Word
   -> ClassAttributeMap.ClassAttributeMap
   -> State.StateT
        (Map.Map CompressedWord.CompressedWord U32.U32)
        BitGet.BitGet
        Replication
-bitGet version limit classes = do
+bitGet matchType version limit classes = do
   actor <- Trans.lift (CompressedWord.bitGet limit)
-  Replication actor <$> ReplicationValue.bitGet version classes actor
+  fmap (Replication actor)
+    $ ReplicationValue.bitGet matchType version classes actor
diff --git a/src/lib/Rattletrap/Type/Replication/Spawned.hs b/src/lib/Rattletrap/Type/Replication/Spawned.hs
--- a/src/lib/Rattletrap/Type/Replication/Spawned.hs
+++ b/src/lib/Rattletrap/Type/Replication/Spawned.hs
@@ -10,7 +10,7 @@
 import qualified Rattletrap.Type.U32 as U32
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
-import Rattletrap.Utility.Monad
+import qualified Rattletrap.Utility.Monad as Monad
 
 import qualified Control.Monad.Trans.Class as Trans
 import qualified Control.Monad.Trans.State as State
@@ -83,16 +83,18 @@
     <> Initialization.bitPut (initialization spawnedReplication)
 
 bitGet
-  :: Version.Version
+  :: Maybe Str.Str
+  -> Version.Version
   -> ClassAttributeMap.ClassAttributeMap
   -> CompressedWord.CompressedWord
   -> State.StateT
        (Map.Map CompressedWord.CompressedWord U32.U32)
        BitGet.BitGet
        Spawned
-bitGet version classAttributeMap actorId = do
+bitGet matchType version classAttributeMap actorId = do
   flag_ <- Trans.lift BitGet.bool
-  nameIndex_ <- whenMaybe (hasNameIndex version) (Trans.lift U32.bitGet)
+  nameIndex_ <- Monad.whenMaybe (hasNameIndex matchType version)
+    $ Trans.lift U32.bitGet
   name_ <- either fail pure (lookupName classAttributeMap nameIndex_)
   objectId_ <- Trans.lift U32.bitGet
   State.modify (Map.insert actorId objectId_)
@@ -116,9 +118,9 @@
       initialization_
     )
 
-hasNameIndex :: Version.Version -> Bool
-hasNameIndex v =
-  Version.major v >= 868 && Version.minor v >= 14 && Version.patch v >= 0
+hasNameIndex :: Maybe Str.Str -> Version.Version -> Bool
+hasNameIndex matchType version =
+  Version.atLeast 868 14 0 version && matchType /= Just (Str.fromString "Lan")
 
 lookupName
   :: ClassAttributeMap.ClassAttributeMap
diff --git a/src/lib/Rattletrap/Type/Replication/Updated.hs b/src/lib/Rattletrap/Type/Replication/Updated.hs
--- a/src/lib/Rattletrap/Type/Replication/Updated.hs
+++ b/src/lib/Rattletrap/Type/Replication/Updated.hs
@@ -10,6 +10,7 @@
 import qualified Rattletrap.Type.U32 as U32
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
+import qualified Rattletrap.Utility.Monad as Monad
 
 import qualified Data.Map as Map
 
@@ -42,6 +43,4 @@
   -> BitGet.BitGet Updated
 bitGet version classes actors actor = fmap Updated . List.untilM $ do
   p <- BitGet.bool
-  if p
-    then Just <$> Attribute.bitGet version classes actors actor
-    else pure Nothing
+  Monad.whenMaybe p $ Attribute.bitGet version classes actors actor
diff --git a/src/lib/Rattletrap/Type/ReplicationValue.hs b/src/lib/Rattletrap/Type/ReplicationValue.hs
--- a/src/lib/Rattletrap/Type/ReplicationValue.hs
+++ b/src/lib/Rattletrap/Type/ReplicationValue.hs
@@ -8,6 +8,7 @@
 import qualified Rattletrap.Type.Replication.Destroyed as Destroyed
 import qualified Rattletrap.Type.Replication.Spawned as Spawned
 import qualified Rattletrap.Type.Replication.Updated as Updated
+import qualified Rattletrap.Type.Str as Str
 import qualified Rattletrap.Type.U32 as U32
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
@@ -28,9 +29,9 @@
 
 instance Json.FromJSON ReplicationValue where
   parseJSON = Json.withObject "ReplicationValue" $ \object -> Foldable.asum
-    [ Spawned <$> Json.required object "spawned"
-    , Updated <$> Json.required object "updated"
-    , Destroyed <$> Json.required object "destroyed"
+    [ fmap Spawned $ Json.required object "spawned"
+    , fmap Updated $ Json.required object "updated"
+    , fmap Destroyed $ Json.required object "destroyed"
     ]
 
 instance Json.ToJSON ReplicationValue where
@@ -54,21 +55,26 @@
   Destroyed x -> BitPut.bool False <> Destroyed.bitPut x
 
 bitGet
-  :: Version.Version
+  :: Maybe Str.Str
+  -> Version.Version
   -> ClassAttributeMap.ClassAttributeMap
   -> CompressedWord.CompressedWord
   -> State.StateT
        (Map.Map CompressedWord.CompressedWord U32.U32)
        BitGet.BitGet
        ReplicationValue
-bitGet version classAttributeMap actorId = do
+bitGet matchType version classAttributeMap actorId = do
   actorMap <- State.get
   isOpen <- Trans.lift BitGet.bool
   if isOpen
     then do
       isNew <- Trans.lift BitGet.bool
       if isNew
-        then Spawned <$> Spawned.bitGet version classAttributeMap actorId
-        else Updated <$> Trans.lift
-          (Updated.bitGet version classAttributeMap actorMap actorId)
-    else Destroyed <$> Trans.lift Destroyed.bitGet
+        then fmap Spawned
+          $ Spawned.bitGet matchType version classAttributeMap actorId
+        else fmap Updated . Trans.lift $ Updated.bitGet
+          version
+          classAttributeMap
+          actorMap
+          actorId
+    else fmap Destroyed $ Trans.lift Destroyed.bitGet
diff --git a/src/lib/Rattletrap/Type/Rotation.hs b/src/lib/Rattletrap/Type/Rotation.hs
--- a/src/lib/Rattletrap/Type/Rotation.hs
+++ b/src/lib/Rattletrap/Type/Rotation.hs
@@ -16,8 +16,8 @@
 
 instance Json.FromJSON Rotation where
   parseJSON = Json.withObject "Rotation" $ \object -> Foldable.asum
-    [ CompressedWordVector <$> Json.required object "compressed_word_vector"
-    , Quaternion <$> Json.required object "quaternion"
+    [ fmap CompressedWordVector $ Json.required object "compressed_word_vector"
+    , fmap Quaternion $ Json.required object "quaternion"
     ]
 
 instance Json.ToJSON Rotation where
@@ -39,10 +39,6 @@
   Quaternion q -> Quaternion.bitPut q
 
 bitGet :: Version.Version -> BitGet.BitGet Rotation
-bitGet version = if isQuaternion version
-  then Quaternion <$> Quaternion.bitGet
-  else CompressedWordVector <$> CompressedWordVector.bitGet
-
-isQuaternion :: Version.Version -> Bool
-isQuaternion v =
-  Version.major v >= 868 && Version.minor v >= 22 && Version.patch v >= 7
+bitGet version = if Version.atLeast 868 22 7 version
+  then fmap Quaternion Quaternion.bitGet
+  else fmap CompressedWordVector CompressedWordVector.bitGet
diff --git a/src/lib/Rattletrap/Type/Section.hs b/src/lib/Rattletrap/Type/Section.hs
--- a/src/lib/Rattletrap/Type/Section.hs
+++ b/src/lib/Rattletrap/Type/Section.hs
@@ -8,7 +8,7 @@
 import qualified Rattletrap.Utility.Json as Json
 
 import qualified Control.Monad as Monad
-import qualified Data.ByteString as Bytes
+import qualified Data.ByteString as ByteString
 import qualified Data.Text as Text
 
 -- | A section is a large piece of a 'Rattletrap.Replay.Replay'. It has a
@@ -52,7 +52,7 @@
   let bytes = BytePut.toByteString $ encode body_
   in
     Section
-      { size = U32.fromWord32 . fromIntegral $ Bytes.length bytes
+      { size = U32.fromWord32 . fromIntegral $ ByteString.length bytes
       , crc = U32.fromWord32 $ Crc.compute bytes
       , body = body_
       }
@@ -63,7 +63,7 @@
 bytePut putBody section =
   let
     rawBody = BytePut.toByteString . putBody $ body section
-    size_ = Bytes.length rawBody
+    size_ = ByteString.length rawBody
     crc_ = Crc.compute rawBody
   in
     U32.bytePut (U32.fromWord32 (fromIntegral size_))
diff --git a/src/lib/Rattletrap/Type/Str.hs b/src/lib/Rattletrap/Type/Str.hs
--- a/src/lib/Rattletrap/Type/Str.hs
+++ b/src/lib/Rattletrap/Type/Str.hs
@@ -6,16 +6,15 @@
 import qualified Rattletrap.BytePut as BytePut
 import qualified Rattletrap.Schema as Schema
 import qualified Rattletrap.Type.I32 as I32
-import Rattletrap.Utility.Bytes
+import qualified Rattletrap.Utility.Bytes as Bytes
 import qualified Rattletrap.Utility.Json as Json
 
-import qualified Data.ByteString as Bytes
+import qualified Data.ByteString as ByteString
 import qualified Data.Char as Char
 import qualified Data.Int as Int
 import qualified Data.Text as Text
 import qualified Data.Text.Encoding as Text
 import qualified Data.Text.Encoding.Error as Text
-import qualified Debug.Trace as Debug
 
 newtype Str
   = Str Text.Text
@@ -65,9 +64,10 @@
       else scale * rawSize :: Int.Int32
   in I32.fromInt32 size
 
-getTextEncoder :: I32.I32 -> Text.Text -> Bytes.ByteString
-getTextEncoder size text =
-  if I32.toInt32 size < 0 then Text.encodeUtf16LE text else encodeLatin1 text
+getTextEncoder :: I32.I32 -> Text.Text -> ByteString.ByteString
+getTextEncoder size text = if I32.toInt32 size < 0
+  then Text.encodeUtf16LE text
+  else Bytes.encodeLatin1 text
 
 addNull :: Text.Text -> Text.Text
 addNull text = if Text.null text then text else Text.snoc text '\x00'
@@ -82,20 +82,18 @@
 bitGet = do
   rawSize <- I32.bitGet
   bytes <- BitGet.byteString (normalizeTextSize rawSize)
-  pure (fromText (dropNull (getTextDecoder rawSize (reverseBytes bytes))))
+  pure (fromText (dropNull (getTextDecoder rawSize bytes)))
 
 normalizeTextSize :: Integral a => I32.I32 -> a
 normalizeTextSize size = case I32.toInt32 size of
   0x05000000 -> 8
   x -> if x < 0 then (-2 * fromIntegral x) else fromIntegral x
 
-getTextDecoder :: I32.I32 -> Bytes.ByteString -> Text.Text
+getTextDecoder :: I32.I32 -> ByteString.ByteString -> Text.Text
 getTextDecoder size bytes =
   let
     decode = if I32.toInt32 size < 0
-      then Text.decodeUtf16LEWith $ \message input -> do
-        Debug.traceM $ "WARNING: " <> show (Text.DecodeError message input)
-        Text.lenientDecode message input
+      then Text.decodeUtf16LEWith Text.lenientDecode
       else Text.decodeLatin1
   in decode bytes
 
diff --git a/src/lib/Rattletrap/Type/U32.hs b/src/lib/Rattletrap/Type/U32.hs
--- a/src/lib/Rattletrap/Type/U32.hs
+++ b/src/lib/Rattletrap/Type/U32.hs
@@ -38,7 +38,7 @@
 bitPut = BitPut.fromBytePut . bytePut
 
 byteGet :: ByteGet.ByteGet U32
-byteGet = fromWord32 <$> ByteGet.word32
+byteGet = fmap fromWord32 ByteGet.word32
 
 bitGet :: BitGet.BitGet U32
 bitGet = BitGet.fromByteGet byteGet 4
diff --git a/src/lib/Rattletrap/Type/U64.hs b/src/lib/Rattletrap/Type/U64.hs
--- a/src/lib/Rattletrap/Type/U64.hs
+++ b/src/lib/Rattletrap/Type/U64.hs
@@ -42,7 +42,7 @@
 bitPut = BitPut.fromBytePut . bytePut
 
 byteGet :: ByteGet.ByteGet U64
-byteGet = fromWord64 <$> ByteGet.word64
+byteGet = fmap fromWord64 ByteGet.word64
 
 bitGet :: BitGet.BitGet U64
 bitGet = BitGet.fromByteGet byteGet 8
diff --git a/src/lib/Rattletrap/Type/U8.hs b/src/lib/Rattletrap/Type/U8.hs
--- a/src/lib/Rattletrap/Type/U8.hs
+++ b/src/lib/Rattletrap/Type/U8.hs
@@ -38,7 +38,7 @@
 bitPut = BitPut.fromBytePut . bytePut
 
 byteGet :: ByteGet.ByteGet U8
-byteGet = fromWord8 <$> ByteGet.word8
+byteGet = fmap fromWord8 ByteGet.word8
 
 bitGet :: BitGet.BitGet U8
 bitGet = BitGet.fromByteGet byteGet 1
diff --git a/src/lib/Rattletrap/Type/Vector.hs b/src/lib/Rattletrap/Type/Vector.hs
--- a/src/lib/Rattletrap/Type/Vector.hs
+++ b/src/lib/Rattletrap/Type/Vector.hs
@@ -67,18 +67,18 @@
 
 bitGet :: Version.Version -> BitGet.BitGet Vector
 bitGet version = do
-  size_ <- CompressedWord.bitGet (if has21Bits version then 21 else 19)
+  size <- CompressedWord.bitGet
+    $ if Version.atLeast 868 22 7 version then 21 else 19
   let
-    limit = getLimit size_
-    bias_ = getBias size_
-  Vector size_ bias_
-    <$> fmap (fromDelta bias_) (CompressedWord.bitGet limit)
-    <*> fmap (fromDelta bias_) (CompressedWord.bitGet limit)
-    <*> fmap (fromDelta bias_) (CompressedWord.bitGet limit)
+    limit = getLimit size
+    bias = getBias size
+  x <- getPart limit bias
+  y <- getPart limit bias
+  z <- getPart limit bias
+  pure Vector { size, bias, x, y, z }
 
-has21Bits :: Version.Version -> Bool
-has21Bits v =
-  Version.major v >= 868 && Version.minor v >= 22 && Version.patch v >= 7
+getPart :: Word -> Word -> BitGet.BitGet Int
+getPart limit bias = fmap (fromDelta bias) (CompressedWord.bitGet limit)
 
 getLimit :: CompressedWord.CompressedWord -> Word
 getLimit = (2 ^) . (+ 2) . CompressedWord.value
diff --git a/src/lib/Rattletrap/Type/Version.hs b/src/lib/Rattletrap/Type/Version.hs
--- a/src/lib/Rattletrap/Type/Version.hs
+++ b/src/lib/Rattletrap/Type/Version.hs
@@ -1,8 +1,33 @@
 module Rattletrap.Type.Version where
 
+import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Type.U32 as U32
+import qualified Rattletrap.Utility.Monad as Monad
+
 data Version = Version
-  { major :: Int
-  , minor :: Int
-  , patch :: Int
+  { major :: U32.U32
+  , minor :: U32.U32
+  , patch :: Maybe U32.U32
   }
   deriving (Eq, Show)
+
+atLeast :: Int -> Int -> Int -> Version -> Bool
+atLeast m n p v =
+  (U32.toWord32 (major v) >= fromIntegral m)
+    && (U32.toWord32 (minor v) >= fromIntegral n)
+    && (maybe 0 U32.toWord32 (patch v) >= fromIntegral p)
+
+bytePut :: Version -> BytePut.BytePut
+bytePut x = U32.bytePut (major x) <> U32.bytePut (minor x) <> foldMap
+  U32.bytePut
+  (patch x)
+
+byteGet :: ByteGet.ByteGet Version
+byteGet = do
+  major <- U32.byteGet
+  minor <- U32.byteGet
+  patch <- Monad.whenMaybe
+    (U32.toWord32 major >= 868 && U32.toWord32 minor >= 18)
+    U32.byteGet
+  pure Version { major, minor, patch }
diff --git a/src/lib/Rattletrap/Utility/Bytes.hs b/src/lib/Rattletrap/Utility/Bytes.hs
--- a/src/lib/Rattletrap/Utility/Bytes.hs
+++ b/src/lib/Rattletrap/Utility/Bytes.hs
@@ -1,32 +1,12 @@
-module Rattletrap.Utility.Bytes
-  ( encodeLatin1
-  , padBytes
-  , reverseBytes
-  ) where
+module Rattletrap.Utility.Bytes where
 
-import qualified Data.Bits as Bits
-import qualified Data.ByteString as Bytes
-import qualified Data.ByteString.Char8 as Bytes8
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Char8 as Latin1
 import qualified Data.Text as Text
-import qualified Data.Word as Word
 
-encodeLatin1 :: Text.Text -> Bytes.ByteString
-encodeLatin1 text = Bytes8.pack (Text.unpack text)
-
-padBytes :: Integral a => a -> Bytes.ByteString -> Bytes.ByteString
-padBytes size bytes =
-  bytes <> Bytes.replicate (fromIntegral size - Bytes.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
+encodeLatin1 :: Text.Text -> ByteString.ByteString
+encodeLatin1 text = Latin1.pack (Text.unpack text)
 
-reverseBytes :: Bytes.ByteString -> Bytes.ByteString
-reverseBytes = Bytes.map reverseByte
+padBytes :: Integral a => a -> ByteString.ByteString -> ByteString.ByteString
+padBytes size bytes = bytes
+  <> ByteString.replicate (fromIntegral size - ByteString.length bytes) 0x00
diff --git a/src/lib/Rattletrap/Utility/Crc.hs b/src/lib/Rattletrap/Utility/Crc.hs
--- a/src/lib/Rattletrap/Utility/Crc.hs
+++ b/src/lib/Rattletrap/Utility/Crc.hs
@@ -1,10 +1,8 @@
-module Rattletrap.Utility.Crc
-  ( compute
-  ) where
+module Rattletrap.Utility.Crc where
 
 import qualified Data.Array.Unboxed as Array
 import qualified Data.Bits as Bits
-import qualified Data.ByteString as Bytes
+import qualified Data.ByteString as ByteString
 import qualified Data.Word as Word
 
 -- | Computes the CRC32 of some bytes. This is done to ensure that the bytes
@@ -16,8 +14,8 @@
 --
 -- This CRC uses an initial value of @0xefcbf201@ and a polynomial of
 -- @0x04c11db7@.
-compute :: Bytes.ByteString -> Word.Word32
-compute = Bits.complement . Bytes.foldl' update initial
+compute :: ByteString.ByteString -> Word.Word32
+compute = Bits.complement . ByteString.foldl' update initial
 
 update :: Word.Word32 -> Word.Word8 -> Word.Word32
 update crc byte =
diff --git a/src/lib/Rattletrap/Utility/Helper.hs b/src/lib/Rattletrap/Utility/Helper.hs
--- a/src/lib/Rattletrap/Utility/Helper.hs
+++ b/src/lib/Rattletrap/Utility/Helper.hs
@@ -1,42 +1,32 @@
 -- | This module provides helper functions for converting replays to and from
 -- both their binary format and JSON.
-module Rattletrap.Utility.Helper
-  ( decodeReplayFile
-  , encodeReplayJson
-  , decodeReplayJson
-  , encodeReplayFile
-  ) where
+module Rattletrap.Utility.Helper where
 
 import qualified Rattletrap.ByteGet as ByteGet
 import qualified Rattletrap.BytePut as BytePut
 import qualified Rattletrap.Type.Content as Content
 import qualified Rattletrap.Type.Replay as Replay
 import qualified Rattletrap.Type.Section as Section
+import qualified Rattletrap.Utility.Json as Json
 
-import qualified Data.Aeson as Aeson
-import qualified Data.Aeson.Encode.Pretty as Aeson
-import qualified Data.ByteString as Bytes
-import qualified Data.ByteString.Lazy as LazyBytes
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Lazy as LazyByteString
 
 -- | Parses a raw replay.
 decodeReplayFile
-  :: Bool -> Bool -> Bytes.ByteString -> Either String Replay.Replay
+  :: Bool -> Bool -> ByteString.ByteString -> Either String Replay.Replay
 decodeReplayFile fast = ByteGet.run . Replay.byteGet fast
 
 -- | Encodes a replay as JSON.
-encodeReplayJson :: Replay.Replay -> LazyBytes.ByteString
-encodeReplayJson = Aeson.encodePretty' Aeson.defConfig
-  { Aeson.confCompare = compare
-  , Aeson.confIndent = Aeson.Tab
-  , Aeson.confTrailingNewline = True
-  }
+encodeReplayJson :: Replay.Replay -> LazyByteString.ByteString
+encodeReplayJson = Json.encodePretty
 
 -- | Parses a JSON replay.
-decodeReplayJson :: Bytes.ByteString -> Either String Replay.Replay
-decodeReplayJson = Aeson.eitherDecodeStrict'
+decodeReplayJson :: ByteString.ByteString -> Either String Replay.Replay
+decodeReplayJson = Json.decode
 
 -- | Encodes a raw replay.
-encodeReplayFile :: Bool -> Replay.Replay -> LazyBytes.ByteString
+encodeReplayFile :: Bool -> Replay.Replay -> LazyByteString.ByteString
 encodeReplayFile fast replay =
   BytePut.toLazyByteString . Replay.bytePut $ if fast
     then replay
diff --git a/src/lib/Rattletrap/Utility/Json.hs b/src/lib/Rattletrap/Utility/Json.hs
--- a/src/lib/Rattletrap/Utility/Json.hs
+++ b/src/lib/Rattletrap/Utility/Json.hs
@@ -3,13 +3,17 @@
   , Aeson.FromJSON(parseJSON)
   , Aeson.ToJSON(toJSON)
   , Aeson.Value
+  , Aeson.encode
   , Aeson.object
   , Aeson.withObject
   , Aeson.withText
   ) where
 
 import qualified Data.Aeson as Aeson
+import qualified Data.Aeson.Encode.Pretty as Aeson
 import qualified Data.Aeson.Types as Aeson
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Lazy as LazyByteString
 import qualified Data.Text as Text
 
 required
@@ -25,3 +29,13 @@
 
 pair :: (Aeson.ToJSON value, Aeson.KeyValue pair) => String -> value -> pair
 pair key value = Text.pack key Aeson..= value
+
+decode :: Aeson.FromJSON a => ByteString.ByteString -> Either String a
+decode = Aeson.eitherDecodeStrict'
+
+encodePretty :: Aeson.ToJSON a => a -> LazyByteString.ByteString
+encodePretty = Aeson.encodePretty' Aeson.defConfig
+  { Aeson.confCompare = compare
+  , Aeson.confIndent = Aeson.Tab
+  , Aeson.confTrailingNewline = True
+  }
diff --git a/src/lib/Rattletrap/Utility/Monad.hs b/src/lib/Rattletrap/Utility/Monad.hs
--- a/src/lib/Rattletrap/Utility/Monad.hs
+++ b/src/lib/Rattletrap/Utility/Monad.hs
@@ -1,4 +1,4 @@
 module Rattletrap.Utility.Monad where
 
 whenMaybe :: Applicative m => Bool -> m a -> m (Maybe a)
-whenMaybe p f = if p then Just <$> f else pure Nothing
+whenMaybe p f = if p then fmap Just f else pure Nothing
diff --git a/src/test/Main.hs b/src/test/Main.hs
--- a/src/test/Main.hs
+++ b/src/test/Main.hs
@@ -1,13 +1,9 @@
-module Main
-  ( main
-  ) where
-
 import qualified Control.Monad as Monad
-import qualified Data.ByteString as Bytes
+import qualified Data.ByteString as ByteString
 import qualified GHC.Clock as Clock
 import qualified Rattletrap
 import qualified System.Exit as Exit
-import qualified System.FilePath as Path
+import qualified System.FilePath as FilePath
 import qualified Test.HUnit as Test
 import qualified Text.Printf as Printf
 
@@ -18,7 +14,7 @@
 runTests test = do
   Rattletrap.rattletrap
     ""
-    ["--schema", "--output", Path.combine directory "schema.json"]
+    ["--schema", "--output", FilePath.combine directory "schema.json"]
   (result, elapsed) <- withElapsed $ Test.runTestTT test
   Printf.printf "Total time: %.3f seconds\n" elapsed
   Monad.when
@@ -38,16 +34,15 @@
 toAssertion :: String -> Test.Assertion
 toAssertion uuid = do
   let
-    inputFile = Path.combine "replays" $ uuid <> ".replay"
-    jsonFile = Path.combine directory $ uuid <> ".json"
-    outputFile = Path.combine directory $ uuid <> ".replay"
-  input <- Bytes.readFile inputFile
+    inputFile = FilePath.combine "replays" $ uuid <> ".replay"
+    jsonFile = FilePath.combine directory $ uuid <> ".json"
+    outputFile = FilePath.combine directory $ uuid <> ".replay"
+  input <- ByteString.readFile inputFile
   decode inputFile jsonFile
   encode jsonFile outputFile
-  output <- Bytes.readFile outputFile
-  Monad.unless
-    (output == input)
-    (Test.assertFailure "output does not match input")
+  output <- ByteString.readFile outputFile
+  Monad.when (output /= input)
+    $ Test.assertFailure "output does not match input"
 
 decode :: FilePath -> FilePath -> IO ()
 decode input output =
