diff --git a/library/Rattletrap/Attribute.hs b/library/Rattletrap/Attribute.hs
--- a/library/Rattletrap/Attribute.hs
+++ b/library/Rattletrap/Attribute.hs
@@ -16,8 +16,8 @@
   , attributeValue :: AttributeValue
   } deriving (Eq, Ord, Show)
 
-getAttributes
-  :: (Int, Int)
+getAttributes ::
+     (Int, Int)
   -> ClassAttributeMap
   -> ActorMap
   -> CompressedWord
@@ -36,8 +36,8 @@
   mapM_ putAttribute attributes
   BinaryBit.putBool False
 
-getAttribute
-  :: (Int, Int)
+getAttribute ::
+     (Int, Int)
   -> ClassAttributeMap
   -> ActorMap
   -> CompressedWord
@@ -54,7 +54,11 @@
           case getAttributeName classAttributeMap attributeMap id_ of
             Nothing -> fail ("could not get attribute name for " ++ show id_)
             Just name -> do
-              value <- getAttributeValue version name
+              value <-
+                getAttributeValue
+                  version
+                  (classAttributeMapObjectMap classAttributeMap)
+                  name
               pure (Attribute id_ name value)
 
 putAttribute :: Attribute -> BinaryBit.BitPut ()
diff --git a/library/Rattletrap/Attribute/AppliedDamage.hs b/library/Rattletrap/Attribute/AppliedDamage.hs
--- a/library/Rattletrap/Attribute/AppliedDamage.hs
+++ b/library/Rattletrap/Attribute/AppliedDamage.hs
@@ -18,12 +18,7 @@
   location <- getVector
   unknown3 <- getInt32Bits
   unknown4 <- getInt32Bits
-  pure
-    (AppliedDamageAttribute
-       unknown1
-       location
-       unknown3
-       unknown4)
+  pure (AppliedDamageAttribute unknown1 location unknown3 unknown4)
 
 putAppliedDamageAttribute :: AppliedDamageAttribute -> BinaryBit.BitPut ()
 putAppliedDamageAttribute appliedDamageAttribute = do
diff --git a/library/Rattletrap/Attribute/CamSettings.hs b/library/Rattletrap/Attribute/CamSettings.hs
--- a/library/Rattletrap/Attribute/CamSettings.hs
+++ b/library/Rattletrap/Attribute/CamSettings.hs
@@ -12,17 +12,32 @@
   , camSettingsAttributeDistance :: Float32
   , camSettingsAttributeStiffness :: Float32
   , camSettingsAttributeSwivelSpeed :: Float32
+  , camSettingsAttributeTransitionSpeed :: Maybe Float32
   } deriving (Eq, Ord, Show)
 
-getCamSettingsAttribute :: BinaryBit.BitGet CamSettingsAttribute
-getCamSettingsAttribute = do
+getCamSettingsAttribute :: (Int, Int) -> BinaryBit.BitGet CamSettingsAttribute
+getCamSettingsAttribute version = do
   fov <- getFloat32Bits
   height <- getFloat32Bits
   angle <- getFloat32Bits
   distance <- getFloat32Bits
   stiffness <- getFloat32Bits
   swivelSpeed <- getFloat32Bits
-  pure (CamSettingsAttribute fov height angle distance stiffness swivelSpeed)
+  transitionSpeed <-
+    if version >= (868, 20)
+      then do
+        x <- getFloat32Bits
+        pure (Just x)
+      else pure Nothing
+  pure
+    (CamSettingsAttribute
+       fov
+       height
+       angle
+       distance
+       stiffness
+       swivelSpeed
+       transitionSpeed)
 
 putCamSettingsAttribute :: CamSettingsAttribute -> BinaryBit.BitPut ()
 putCamSettingsAttribute camSettingsAttribute = do
@@ -32,3 +47,6 @@
   putFloat32Bits (camSettingsAttributeDistance camSettingsAttribute)
   putFloat32Bits (camSettingsAttributeStiffness camSettingsAttribute)
   putFloat32Bits (camSettingsAttributeSwivelSpeed camSettingsAttribute)
+  case camSettingsAttributeTransitionSpeed camSettingsAttribute of
+    Nothing -> pure ()
+    Just transitionSpeed -> putFloat32Bits transitionSpeed
diff --git a/library/Rattletrap/Attribute/DamageState.hs b/library/Rattletrap/Attribute/DamageState.hs
--- a/library/Rattletrap/Attribute/DamageState.hs
+++ b/library/Rattletrap/Attribute/DamageState.hs
@@ -23,13 +23,7 @@
   unknown5 <- BinaryBit.getBool
   unknown6 <- BinaryBit.getBool
   pure
-    (DamageStateAttribute
-       unknown1
-       unknown2
-       unknown3
-       unknown4
-       unknown5
-       unknown6)
+    (DamageStateAttribute unknown1 unknown2 unknown3 unknown4 unknown5 unknown6)
 
 putDamageStateAttribute :: DamageStateAttribute -> BinaryBit.BitPut ()
 putDamageStateAttribute damageStateAttribute = do
diff --git a/library/Rattletrap/Attribute/Loadout.hs b/library/Rattletrap/Attribute/Loadout.hs
--- a/library/Rattletrap/Attribute/Loadout.hs
+++ b/library/Rattletrap/Attribute/Loadout.hs
@@ -19,6 +19,7 @@
   , loadoutAttributeEngineAudio :: Maybe Word32
   , loadoutAttributeTrail :: Maybe Word32
   , loadoutAttributeGoalExplosion :: Maybe Word32
+  , loadoutAttributeBanner :: Maybe Word32
   } deriving (Eq, Ord, Show)
 
 getLoadoutAttribute :: BinaryBit.BitGet LoadoutAttribute
@@ -35,6 +36,7 @@
   engineAudio <- getOptional (version >= Word8 16) getWord32Bits
   trail <- getOptional (version >= Word8 16) getWord32Bits
   goalExplosion <- getOptional (version >= Word8 16) getWord32Bits
+  banner <- getOptional (version >= Word8 17) getWord32Bits
   pure
     (LoadoutAttribute
        version
@@ -48,7 +50,8 @@
        unknown2
        engineAudio
        trail
-       goalExplosion)
+       goalExplosion
+       banner)
 
 getOptional :: Bool -> BinaryBit.BitGet a -> BinaryBit.BitGet (Maybe a)
 getOptional p f =
@@ -72,6 +75,7 @@
   putOptional (loadoutAttributeEngineAudio loadoutAttribute) putWord32Bits
   putOptional (loadoutAttributeTrail loadoutAttribute) putWord32Bits
   putOptional (loadoutAttributeGoalExplosion loadoutAttribute) putWord32Bits
+  putOptional (loadoutAttributeBanner loadoutAttribute) putWord32Bits
 
 putOptional :: Maybe a -> (a -> BinaryBit.BitPut ()) -> BinaryBit.BitPut ()
 putOptional m f =
diff --git a/library/Rattletrap/Attribute/LoadoutOnline.hs b/library/Rattletrap/Attribute/LoadoutOnline.hs
--- a/library/Rattletrap/Attribute/LoadoutOnline.hs
+++ b/library/Rattletrap/Attribute/LoadoutOnline.hs
@@ -1,41 +1,31 @@
 module Rattletrap.Attribute.LoadoutOnline where
 
+import Rattletrap.Attribute.ProductAttribute
 import Rattletrap.Primitive
 
 import qualified Control.Monad as Monad
 import qualified Data.Binary.Bits.Get as BinaryBit
 import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Map as Map
 
 newtype LoadoutOnlineAttribute = LoadoutOnlineAttribute
-  { loadoutAttributeValue :: [[(Word32, CompressedWord)]]
+  { loadoutAttributeValue :: [[ProductAttribute]]
   } deriving (Eq, Ord, Show)
 
 getLoadoutOnlineAttribute ::
-     (Int, Int) -> BinaryBit.BitGet LoadoutOnlineAttribute
-getLoadoutOnlineAttribute (major, minor) = do
-  let getOuter = do
-        innerSize <- getWord8Bits
-        Monad.replicateM (fromIntegral (word8Value innerSize)) getInner
-      getInner = do
-        x <- getWord32Bits
-        y <- getCompressedWord limit
-        pure (x, y)
-      limit =
-        if major >= 868 && minor >= 18
-          then 2 ^ (32 :: Int)
-          else 27
+     (Int, Int)
+  -> Map.Map Word32 Text
+  -> BinaryBit.BitGet LoadoutOnlineAttribute
+getLoadoutOnlineAttribute version objectMap = do
   size <- getWord8Bits
-  values <- Monad.replicateM (fromIntegral (word8Value size)) getOuter
+  values <-
+    Monad.replicateM
+      (fromIntegral (word8Value size))
+      (getProductAttributes version objectMap)
   pure (LoadoutOnlineAttribute values)
 
 putLoadoutOnlineAttribute :: LoadoutOnlineAttribute -> BinaryBit.BitPut ()
 putLoadoutOnlineAttribute loadoutAttribute = do
-  let putOuter xs = do
-        putWord8Bits (Word8 (fromIntegral (length xs)))
-        mapM_ putInner xs
-      putInner (x, y) = do
-        putWord32Bits x
-        putCompressedWord y
-      value = loadoutAttributeValue loadoutAttribute
-  putWord8Bits (Word8 (fromIntegral (length value)))
-  mapM_ putOuter value
+  let attributes = loadoutAttributeValue loadoutAttribute
+  putWord8Bits (Word8 (fromIntegral (length attributes)))
+  mapM_ putProductAttributes attributes
diff --git a/library/Rattletrap/Attribute/LoadoutsOnline.hs b/library/Rattletrap/Attribute/LoadoutsOnline.hs
--- a/library/Rattletrap/Attribute/LoadoutsOnline.hs
+++ b/library/Rattletrap/Attribute/LoadoutsOnline.hs
@@ -1,9 +1,11 @@
 module Rattletrap.Attribute.LoadoutsOnline where
 
 import Rattletrap.Attribute.LoadoutOnline
+import Rattletrap.Primitive
 
 import qualified Data.Binary.Bits.Get as BinaryBit
 import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Map as Map
 
 data LoadoutsOnlineAttribute = LoadoutsOnlineAttribute
   { loadoutsOnlineAttributeBlue :: LoadoutOnlineAttribute
@@ -12,10 +14,13 @@
   , loadoutsOnlineAttributeUnknown2 :: Bool
   } deriving (Eq, Ord, Show)
 
-getLoadoutsOnlineAttribute :: (Int, Int) -> BinaryBit.BitGet LoadoutsOnlineAttribute
-getLoadoutsOnlineAttribute version = do
-  blueLoadout <- getLoadoutOnlineAttribute version
-  orangeLoadout <- getLoadoutOnlineAttribute version
+getLoadoutsOnlineAttribute ::
+     (Int, Int)
+  -> Map.Map Word32 Text
+  -> BinaryBit.BitGet LoadoutsOnlineAttribute
+getLoadoutsOnlineAttribute version objectMap = do
+  blueLoadout <- getLoadoutOnlineAttribute version objectMap
+  orangeLoadout <- getLoadoutOnlineAttribute version objectMap
   unknown1 <- BinaryBit.getBool
   unknown2 <- BinaryBit.getBool
   pure (LoadoutsOnlineAttribute blueLoadout orangeLoadout unknown1 unknown2)
diff --git a/library/Rattletrap/Attribute/PrivateMatchSettings.hs b/library/Rattletrap/Attribute/PrivateMatchSettings.hs
--- a/library/Rattletrap/Attribute/PrivateMatchSettings.hs
+++ b/library/Rattletrap/Attribute/PrivateMatchSettings.hs
@@ -14,7 +14,8 @@
   , privateMatchSettingsAttributeFlag :: Bool
   } deriving (Eq, Ord, Show)
 
-getPrivateMatchSettingsAttribute :: BinaryBit.BitGet PrivateMatchSettingsAttribute
+getPrivateMatchSettingsAttribute ::
+     BinaryBit.BitGet PrivateMatchSettingsAttribute
 getPrivateMatchSettingsAttribute = do
   mutators <- getTextBits
   joinableBy <- getWord32Bits
@@ -31,8 +32,8 @@
        password
        flag)
 
-putPrivateMatchSettingsAttribute :: PrivateMatchSettingsAttribute
-                                 -> BinaryBit.BitPut ()
+putPrivateMatchSettingsAttribute ::
+     PrivateMatchSettingsAttribute -> BinaryBit.BitPut ()
 putPrivateMatchSettingsAttribute privateMatchSettingsAttribute = do
   putTextBits
     (privateMatchSettingsAttributeMutators privateMatchSettingsAttribute)
diff --git a/library/Rattletrap/Attribute/ProductAttribute.hs b/library/Rattletrap/Attribute/ProductAttribute.hs
new file mode 100644
--- /dev/null
+++ b/library/Rattletrap/Attribute/ProductAttribute.hs
@@ -0,0 +1,89 @@
+module Rattletrap.Attribute.ProductAttribute where
+
+import Rattletrap.Primitive
+
+import qualified Control.Monad as Monad
+import qualified Data.Binary.Bits.Get as BinaryBit
+import qualified Data.Binary.Bits.Put as BinaryBit
+import qualified Data.Map as Map
+import qualified Data.Word as Word
+
+data ProductAttribute = ProductAttribute
+  { productAttributeUnknown :: Bool
+  , productAttributeObjectId :: Word32
+  , productAttributeObjectName :: Maybe Text
+  -- ^ read-only
+  , productAttributeValue :: Maybe (Either CompressedWord Word.Word32)
+  } deriving (Eq, Ord, Show)
+
+getProductAttributes ::
+     (Int, Int) -> Map.Map Word32 Text -> BinaryBit.BitGet [ProductAttribute]
+getProductAttributes version objectMap = do
+  size <- getWord8Bits
+  Monad.replicateM
+    (fromIntegral (word8Value size))
+    (getProductAttribute version objectMap)
+
+getProductAttribute ::
+     (Int, Int) -> Map.Map Word32 Text -> BinaryBit.BitGet ProductAttribute
+getProductAttribute version objectMap = do
+  flag <- BinaryBit.getBool
+  objectId <- getWord32Bits
+  let objectName = Map.lookup objectId objectMap
+  value <-
+    case objectName of
+      Just name ->
+        case textToString name of
+          "TAGame.ProductAttribute_Painted_TA" ->
+            if version >= (868, 18)
+              then do
+                x <- BinaryBit.getWord32be 31
+                pure (Just (Right x))
+              else do
+                x <- getCompressedWord 14
+                pure (Just (Left x))
+          "TAGame.ProductAttribute_UserColor_TA" -> do
+            hasValue <- BinaryBit.getBool
+            value <-
+              if hasValue
+                then do
+                  x <- BinaryBit.getWord32be 31
+                  pure (Just (Right x))
+                else pure Nothing
+            pure value
+          _ ->
+            fail
+              ("unknown object name " ++
+               show objectName ++ " for ID " ++ show objectId)
+      Nothing -> fail ("missing object name for ID " ++ show objectId)
+  pure (ProductAttribute flag objectId objectName value)
+
+putProductAttributes :: [ProductAttribute] -> BinaryBit.BitPut ()
+putProductAttributes attributes = do
+  putWord8Bits (Word8 (fromIntegral (length attributes)))
+  mapM_ putProductAttribute attributes
+
+putProductAttribute :: ProductAttribute -> BinaryBit.BitPut ()
+putProductAttribute attribute = do
+  BinaryBit.putBool (productAttributeUnknown attribute)
+  putWord32Bits (productAttributeObjectId attribute)
+  case productAttributeObjectName attribute of
+    Just name ->
+      case textToString name of
+        "TAGame.ProductAttribute_Painted_TA" ->
+          case productAttributeValue attribute of
+            Nothing -> pure ()
+            Just (Left x) -> putCompressedWord x
+            Just (Right x) -> BinaryBit.putWord32be 31 x
+        "TAGame.ProductAttribute_UserColor_TA" ->
+          case productAttributeValue attribute of
+            Nothing -> BinaryBit.putBool False
+            Just value -> do
+              BinaryBit.putBool True
+              case value of
+                Left x -> putCompressedWord x
+                Right x -> BinaryBit.putWord32be 31 x
+        _ ->
+          fail ("unknown object name for product attribute " ++ show attribute)
+    Nothing ->
+      fail ("missing object name for product attribute " ++ show attribute)
diff --git a/library/Rattletrap/AttributeValue.hs b/library/Rattletrap/AttributeValue.hs
--- a/library/Rattletrap/AttributeValue.hs
+++ b/library/Rattletrap/AttributeValue.hs
@@ -23,6 +23,7 @@
   , module Rattletrap.Attribute.PartyLeader
   , module Rattletrap.Attribute.Pickup
   , module Rattletrap.Attribute.PrivateMatchSettings
+  , module Rattletrap.Attribute.ProductAttribute
   , module Rattletrap.Attribute.QWord
   , module Rattletrap.Attribute.Reservation
   , module Rattletrap.Attribute.RigidBodyState
@@ -55,6 +56,7 @@
 import Rattletrap.Attribute.PartyLeader
 import Rattletrap.Attribute.Pickup
 import Rattletrap.Attribute.PrivateMatchSettings
+import Rattletrap.Attribute.ProductAttribute
 import Rattletrap.Attribute.QWord
 import Rattletrap.Attribute.Reservation
 import Rattletrap.Attribute.RigidBodyState
@@ -103,8 +105,12 @@
   | WeldedInfoAttributeValue WeldedInfoAttribute
   deriving (Eq, Ord, Show)
 
-getAttributeValue :: (Int, Int) -> Text -> BinaryBit.BitGet AttributeValue
-getAttributeValue version name =
+getAttributeValue ::
+     (Int, Int)
+  -> Map.Map Word32 Text
+  -> Text
+  -> BinaryBit.BitGet AttributeValue
+getAttributeValue version objectMap name =
   case Map.lookup name attributeTypes of
     Just constructor ->
       case constructor of
@@ -118,7 +124,7 @@
           x <- getByteAttribute
           pure (ByteAttributeValue x)
         CamSettingsAttributeType -> do
-          x <- getCamSettingsAttribute
+          x <- getCamSettingsAttribute version
           pure (CamSettingsAttributeValue x)
         ClubColorsAttributeType -> do
           x <- getClubColorsAttribute
@@ -154,13 +160,13 @@
           x <- getLoadoutAttribute
           pure (LoadoutAttributeValue x)
         LoadoutOnlineAttributeType -> do
-          x <- getLoadoutOnlineAttribute version
+          x <- getLoadoutOnlineAttribute version objectMap
           pure (LoadoutOnlineAttributeValue x)
         LoadoutsAttributeType -> do
           x <- getLoadoutsAttribute
           pure (LoadoutsAttributeValue x)
         LoadoutsOnlineAttributeType -> do
-          x <- getLoadoutsOnlineAttribute version
+          x <- getLoadoutsOnlineAttribute version objectMap
           pure (LoadoutsOnlineAttributeValue x)
         LocationAttributeType -> do
           x <- getLocationAttribute
diff --git a/library/Rattletrap/ClassAttributeMap.hs b/library/Rattletrap/ClassAttributeMap.hs
--- a/library/Rattletrap/ClassAttributeMap.hs
+++ b/library/Rattletrap/ClassAttributeMap.hs
@@ -33,8 +33,8 @@
 
 -- | Makes a 'ClassAttributeMap' given the necessary fields from the
 -- 'Rattletrap.Content.Content'.
-makeClassAttributeMap
-  :: List Text
+makeClassAttributeMap ::
+     List Text
   -- ^ From 'Rattletrap.Content.contentObjects'.
   -> List ClassMapping
   -- ^ From 'Rattletrap.Content.contentClassMappings'.
@@ -82,9 +82,8 @@
 getName nameMap nameIndex =
   IntMap.lookup (fromIntegral (word32Value nameIndex)) nameMap
 
-makeObjectClassMap :: Map.Map Word32 Text
-                   -> Bimap.Bimap Word32 Text
-                   -> Map.Map Word32 Word32
+makeObjectClassMap ::
+     Map.Map Word32 Text -> Bimap.Bimap Word32 Text -> Map.Map Word32 Word32
 makeObjectClassMap objectMap classMap = do
   let objectIds = Map.keys objectMap
   let classIds = map (getClassId objectMap classMap) objectIds
@@ -98,18 +97,17 @@
           rawPairs
   Map.fromList pairs
 
-getClassId :: Map.Map Word32 Text
-           -> Bimap.Bimap Word32 Text
-           -> Word32
-           -> Maybe Word32
+getClassId ::
+     Map.Map Word32 Text -> Bimap.Bimap Word32 Text -> Word32 -> Maybe Word32
 getClassId objectMap classMap objectId = do
   objectName <- getObjectName objectMap objectId
   className <- getClassName objectName
   Bimap.lookupR className classMap
 
-makeClassCache :: Bimap.Bimap Word32 Text
-               -> List Cache
-               -> [(Maybe Text, Word32, Word32, Word32)]
+makeClassCache ::
+     Bimap.Bimap Word32 Text
+  -> List Cache
+  -> [(Maybe Text, Word32, Word32, Word32)]
 makeClassCache classMap caches =
   map
     (\cache ->
@@ -142,8 +140,8 @@
                  (listValue (cacheAttributeMappings cache)))))
        (listValue caches))
 
-makeShallowParentMap :: [(Maybe Text, Word32, Word32, Word32)]
-                     -> Map.Map Word32 Word32
+makeShallowParentMap ::
+     [(Maybe Text, Word32, Word32, Word32)] -> Map.Map Word32 Word32
 makeShallowParentMap classCache =
   Map.fromList
     (Maybe.mapMaybe
@@ -155,8 +153,8 @@
               pure (classId, parentClassId))
        (List.tails (reverse classCache)))
 
-makeParentMap :: [(Maybe Text, Word32, Word32, Word32)]
-              -> Map.Map Word32 [Word32]
+makeParentMap ::
+     [(Maybe Text, Word32, Word32, Word32)] -> Map.Map Word32 [Word32]
 makeParentMap classCache =
   let shallowParentMap = makeShallowParentMap classCache
   in Map.mapWithKey
@@ -170,18 +168,18 @@
     Just parentClassId ->
       parentClassId : getParentClasses shallowParentMap parentClassId
 
-getParentClass :: Maybe Text
-               -> Word32
-               -> [(Maybe Text, Word32, Word32, Word32)]
-               -> Maybe Word32
+getParentClass ::
+     Maybe Text
+  -> Word32
+  -> [(Maybe Text, Word32, Word32, Word32)]
+  -> Maybe Word32
 getParentClass maybeClassName parentCacheId xs =
   case maybeClassName of
     Nothing -> getParentClassById parentCacheId xs
     Just className -> getParentClassByName className parentCacheId xs
 
-getParentClassById :: Word32
-                   -> [(Maybe Text, Word32, Word32, Word32)]
-                   -> Maybe Word32
+getParentClassById ::
+     Word32 -> [(Maybe Text, Word32, Word32, Word32)] -> Maybe Word32
 getParentClassById parentCacheId xs =
   case dropWhile (\(_, _, cacheId, _) -> cacheId /= parentCacheId) xs of
     [] ->
@@ -190,10 +188,8 @@
         else getParentClassById (Word32 (word32Value parentCacheId - 1)) xs
     (_, parentClassId, _, _):_ -> Just parentClassId
 
-getParentClassByName :: Text
-                     -> Word32
-                     -> [(Maybe Text, Word32, Word32, Word32)]
-                     -> Maybe Word32
+getParentClassByName ::
+     Text -> Word32 -> [(Maybe Text, Word32, Word32, Word32)] -> Maybe Word32
 getParentClassByName className parentCacheId xs =
   case Map.lookup className parentClasses of
     Nothing -> getParentClassById parentCacheId xs
@@ -205,7 +201,7 @@
            (map
               (\(_, parentClassId, _, _) -> parentClassId)
               (filter
-                 (\(_, _, cacheId, _) -> cacheId == parentCacheId)
+                 (\(_, _, cacheId, _) -> cacheId <= parentCacheId)
                  (filter
                     (\(maybeClassName, _, _, _) ->
                        maybeClassName == Just parentClassName)
@@ -245,8 +241,8 @@
                      else if Text.isInfixOf mapScoreboard name
                             then Text mapScoreboard
                             else if Text.isInfixOf breakout name
-                              then Text breakout
-                              else objectName
+                                   then Text breakout
+                                   else objectName
 
 objectClasses :: Map.Map Text Text
 objectClasses =
@@ -272,18 +268,16 @@
   let limit = fromIntegral (word32Value streamId)
   pure limit
 
-getAttributeName :: ClassAttributeMap
-                 -> Map.Map Word32 Word32
-                 -> CompressedWord
-                 -> Maybe Text
+getAttributeName ::
+     ClassAttributeMap -> Map.Map Word32 Word32 -> CompressedWord -> Maybe Text
 getAttributeName classAttributeMap attributeMap streamId = do
   let key = Word32 (fromIntegral (compressedWordValue streamId))
   attributeId <- Map.lookup key attributeMap
   let objectMap = classAttributeMapObjectMap classAttributeMap
   Map.lookup attributeId objectMap
 
-getAttributeMap
-  :: ClassAttributeMap
+getAttributeMap ::
+     ClassAttributeMap
   -> ActorMap
   -> CompressedWord
   -> Maybe (Map.Map Word32 Word32)
diff --git a/library/Rattletrap/Content.hs b/library/Rattletrap/Content.hs
--- a/library/Rattletrap/Content.hs
+++ b/library/Rattletrap/Content.hs
@@ -51,8 +51,8 @@
   -- for the 'Rattletrap.ClassAttributeMap.ClassAttributeMap'.
   } deriving (Eq, Ord, Show)
 
-getContent
-  :: (Int, Int)
+getContent ::
+     (Int, Int)
   -- ^ Major and minor version numbers, usually from
   -- 'Rattletrap.Header.getVersion'.
   -> Int
diff --git a/library/Rattletrap/Crc.hs b/library/Rattletrap/Crc.hs
--- a/library/Rattletrap/Crc.hs
+++ b/library/Rattletrap/Crc.hs
@@ -23,10 +23,8 @@
   let crc = ByteString.foldl update initial bytes
   Bits.complement crc
 
-crc32Update :: Vector.Vector Word.Word32
-            -> Word.Word32
-            -> Word.Word8
-            -> Word.Word32
+crc32Update ::
+     Vector.Vector Word.Word32 -> Word.Word32 -> Word.Word8 -> Word.Word32
 crc32Update table crc byte = do
   let toWord8 =
         fromIntegral :: (Integral a) =>
diff --git a/library/Rattletrap/Data.hs b/library/Rattletrap/Data.hs
--- a/library/Rattletrap/Data.hs
+++ b/library/Rattletrap/Data.hs
@@ -179,6 +179,7 @@
   , ("Engine.TeamInfo:Score", IntAttributeType)
   , ("ProjectX.GRI_X:bGameStarted", BooleanAttributeType)
   , ("ProjectX.GRI_X:GameServerID", QWordAttributeType)
+  , ("ProjectX.GRI_X:MatchGUID", StringAttributeType)
   , ("ProjectX.GRI_X:ReplicatedGameMutatorIndex", IntAttributeType)
   , ("ProjectX.GRI_X:ReplicatedGamePlaylist", IntAttributeType)
   , ("ProjectX.GRI_X:Reservations", ReservationAttributeType)
@@ -250,6 +251,7 @@
   , ("TAGame.PRI_TA:bMatchMVP", BooleanAttributeType)
   , ("TAGame.PRI_TA:bOnlineLoadoutSet", BooleanAttributeType)
   , ("TAGame.PRI_TA:bOnlineLoadoutsSet", BooleanAttributeType)
+  , ("TAGame.PRI_TA:BotProductName", IntAttributeType)
   , ("TAGame.PRI_TA:bReady", BooleanAttributeType)
   , ("TAGame.PRI_TA:bUsingBehindView", BooleanAttributeType)
   , ("TAGame.PRI_TA:bUsingItems", BooleanAttributeType)
@@ -271,7 +273,9 @@
   , ("TAGame.PRI_TA:PartyLeader", PartyLeaderAttributeType)
   , ("TAGame.PRI_TA:PawnType", ByteAttributeType)
   , ("TAGame.PRI_TA:PersistentCamera", FlaggedIntAttributeType)
+  , ("TAGame.PRI_TA:PlayerHistoryValid", BooleanAttributeType)
   , ("TAGame.PRI_TA:ReplicatedGameEvent", FlaggedIntAttributeType)
+  , ("TAGame.PRI_TA:SteeringSensitivity", FloatAttributeType)
   , ("TAGame.PRI_TA:TimeTillItem", IntAttributeType)
   , ("TAGame.PRI_TA:Title", IntAttributeType)
   , ("TAGame.PRI_TA:TotalXP", IntAttributeType)
diff --git a/library/Rattletrap/Frame.hs b/library/Rattletrap/Frame.hs
--- a/library/Rattletrap/Frame.hs
+++ b/library/Rattletrap/Frame.hs
@@ -17,8 +17,8 @@
   , frameReplications :: [Replication]
   } deriving (Eq, Ord, Show)
 
-getFrames
-  :: (Int, Int)
+getFrames ::
+     (Int, Int)
   -> Int
   -> Word
   -> ClassAttributeMap
@@ -42,8 +42,8 @@
 putFrames :: [Frame] -> BinaryBit.BitPut ()
 putFrames = mapM_ putFrame
 
-getFrame
-  :: (Int, Int)
+getFrame ::
+     (Int, Int)
   -> Word
   -> ClassAttributeMap
   -> ActorMap
diff --git a/library/Rattletrap/Json.hs b/library/Rattletrap/Json.hs
--- a/library/Rattletrap/Json.hs
+++ b/library/Rattletrap/Json.hs
@@ -86,6 +86,7 @@
         , ''PartyLeaderAttribute
         , ''PickupAttribute
         , ''PrivateMatchSettingsAttribute
+        , ''ProductAttribute
         , ''Property
         , ''PropertyValue
         , ''QWordAttribute
diff --git a/library/Rattletrap/Main.hs b/library/Rattletrap/Main.hs
--- a/library/Rattletrap/Main.hs
+++ b/library/Rattletrap/Main.hs
@@ -45,8 +45,9 @@
       putOutput output
     _ -> fail ("unknown arguments: " ++ show args)
 
-getIO :: [FilePath]
-      -> IO (IO ByteString.ByteString, ByteString.ByteString -> IO ())
+getIO ::
+     [FilePath]
+  -> IO (IO ByteString.ByteString, ByteString.ByteString -> IO ())
 getIO files =
   case files of
     [] -> pure (ByteString.getContents, ByteString.putStr)
diff --git a/library/Rattletrap/Primitive/CompressedWord.hs b/library/Rattletrap/Primitive/CompressedWord.hs
--- a/library/Rattletrap/Primitive/CompressedWord.hs
+++ b/library/Rattletrap/Primitive/CompressedWord.hs
@@ -36,9 +36,7 @@
           else pure ()
   go 0 0
 
-getMaxBits
-  :: (Integral a, Integral b)
-  => a -> b
+getMaxBits :: (Integral a, Integral b) => a -> b
 getMaxBits x = do
   let n = max 1 (ceiling (logBase (2 :: Double) (fromIntegral (max 1 x))))
   if x < 1024 && x == 2 ^ n
diff --git a/library/Rattletrap/Primitive/Text.hs b/library/Rattletrap/Primitive/Text.hs
--- a/library/Rattletrap/Primitive/Text.hs
+++ b/library/Rattletrap/Primitive/Text.hs
@@ -73,9 +73,7 @@
           else scale * rawSize
   in Int32 size
 
-normalizeTextSize
-  :: Integral a
-  => Int32 -> a
+normalizeTextSize :: Integral a => Int32 -> a
 normalizeTextSize size =
   case int32Value size of
     0x05000000 -> 8
diff --git a/library/Rattletrap/Replication.hs b/library/Rattletrap/Replication.hs
--- a/library/Rattletrap/Replication.hs
+++ b/library/Rattletrap/Replication.hs
@@ -13,8 +13,8 @@
   , replicationValue :: ReplicationValue
   } deriving (Eq, Ord, Show)
 
-getReplications
-  :: (Int, Int)
+getReplications ::
+     (Int, Int)
   -> Word
   -> ClassAttributeMap
   -> ActorMap
@@ -34,8 +34,8 @@
   mapM_ putReplication replications
   BinaryBit.putBool False
 
-getReplication
-  :: (Int, Int)
+getReplication ::
+     (Int, Int)
   -> Word
   -> ClassAttributeMap
   -> ActorMap
diff --git a/library/Rattletrap/Replication/Spawned.hs b/library/Rattletrap/Replication/Spawned.hs
--- a/library/Rattletrap/Replication/Spawned.hs
+++ b/library/Rattletrap/Replication/Spawned.hs
@@ -26,8 +26,8 @@
   , spawnedReplicationInitialization :: Initialization
   } deriving (Eq, Ord, Show)
 
-getSpawnedReplication
-  :: (Int, Int)
+getSpawnedReplication ::
+     (Int, Int)
   -> ClassAttributeMap
   -> ActorMap
   -> CompressedWord
@@ -68,9 +68,7 @@
   putWord32Bits (spawnedReplicationObjectId spawnedReplication)
   putInitialization (spawnedReplicationInitialization spawnedReplication)
 
-lookupName
-  :: Monad m
-  => ClassAttributeMap -> Maybe Word32 -> m (Maybe Text)
+lookupName :: Monad m => ClassAttributeMap -> Maybe Word32 -> m (Maybe Text)
 lookupName classAttributeMap maybeNameIndex =
   case maybeNameIndex of
     Nothing -> pure Nothing
@@ -79,17 +77,13 @@
         Nothing -> fail ("could not get name for index " ++ show nameIndex)
         Just name -> pure (Just name)
 
-lookupObjectName
-  :: Monad m
-  => ClassAttributeMap -> Word32 -> m Text
+lookupObjectName :: Monad m => ClassAttributeMap -> Word32 -> m Text
 lookupObjectName classAttributeMap objectId =
   case getObjectName (classAttributeMapObjectMap classAttributeMap) objectId of
     Nothing -> fail ("could not get object name for id " ++ show objectId)
     Just objectName -> pure objectName
 
-lookupClassName
-  :: Monad m
-  => Text -> m Text
+lookupClassName :: Monad m => Text -> m Text
 lookupClassName objectName =
   case getClassName objectName of
     Nothing -> fail ("could not get class name for object " ++ show objectName)
diff --git a/library/Rattletrap/Replication/Updated.hs b/library/Rattletrap/Replication/Updated.hs
--- a/library/Rattletrap/Replication/Updated.hs
+++ b/library/Rattletrap/Replication/Updated.hs
@@ -12,8 +12,8 @@
   { updatedReplicationAttributes :: [Attribute]
   } deriving (Eq, Ord, Show)
 
-getUpdatedReplication
-  :: (Int, Int)
+getUpdatedReplication ::
+     (Int, Int)
   -> ClassAttributeMap
   -> ActorMap
   -> CompressedWord
diff --git a/library/Rattletrap/ReplicationValue.hs b/library/Rattletrap/ReplicationValue.hs
--- a/library/Rattletrap/ReplicationValue.hs
+++ b/library/Rattletrap/ReplicationValue.hs
@@ -24,8 +24,8 @@
   -- ^ Destroys an existing actor.
   deriving (Eq, Ord, Show)
 
-getReplicationValue
-  :: (Int, Int)
+getReplicationValue ::
+     (Int, Int)
   -> ClassAttributeMap
   -> ActorMap
   -> CompressedWord
diff --git a/library/Rattletrap/Utility.hs b/library/Rattletrap/Utility.hs
--- a/library/Rattletrap/Utility.hs
+++ b/library/Rattletrap/Utility.hs
@@ -9,9 +9,7 @@
 encodeLatin1 :: Text.Text -> ByteString.ByteString
 encodeLatin1 text = ByteString8.pack (Text.unpack text)
 
-padBytes
-  :: Integral a
-  => a -> ByteString.ByteString -> ByteString.ByteString
+padBytes :: Integral a => a -> ByteString.ByteString -> ByteString.ByteString
 padBytes size bytes =
   ByteString.concat
     [ bytes
diff --git a/package.yaml b/package.yaml
--- a/package.yaml
+++ b/package.yaml
@@ -1,5 +1,5 @@
 name: rattletrap
-version: 2.5.2
+version: 3.0.0
 
 category: Game
 description: Rattletrap parses and generates Rocket League replays.
diff --git a/rattletrap.cabal b/rattletrap.cabal
--- a/rattletrap.cabal
+++ b/rattletrap.cabal
@@ -3,7 +3,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           rattletrap
-version:        2.5.2
+version:        3.0.0
 synopsis:       Parse and generate Rocket League replays.
 description:    Rattletrap parses and generates Rocket League replays.
 category:       Game
@@ -37,6 +37,7 @@
     replays/22BACD794ABE7B92E50E9CBDBD9C59CE.replay
     replays/27B6A7B64553F0F685874584F96BAB1B.replay
     replays/29F582C34A65EB34D358A784CBE3C189.replay
+    replays/338173964F9F71EBDD31058A1936CBB4.replay
     replays/372DBFCA4BDB340E4357B6BD43032802.replay
     replays/387F059C47C09E253C875CA990EFD9F2.replay
     replays/3EA147DD485B8DD24810689A7A989E44.replay
@@ -49,6 +50,7 @@
     replays/551CA4D44FF2B86015DE44A6B5790D4C.replay
     replays/6320E51C49066A7C210A2993C2201D5F.replay
     replays/6688EEE34BFEB3EC3A9E3283098CC712.replay
+    replays/6B0D1614417085A7AAD82EAA30D8DABE.replay
     replays/6D1B06D844A5BB91B81FD4B5B28F08BA.replay
     replays/6F7CFCD24638F8A6567AB3A8B9958A90.replay
     replays/7109EB9846D303E54B7ACBA792036213.replay
@@ -62,6 +64,7 @@
     replays/A558B1B44124D6E021640884E8EEC2A7.replay
     replays/A6711CE74272B2E663DCC9A200A218E3.replay
     replays/A7F001A1417A19BFA8C90990D8F7C2FF.replay
+    replays/AFB1F46A49737E36928E1EABC6F5B7AD.replay
     replays/B9F9B87D4A9D0A3D25D4EC91C0401DE2.replay
     replays/C14F7E0E4D9B5E6BE9AD5D8ED56B174C.replay
     replays/C8372B1345B1803DEF039F815DBD802D.replay
@@ -126,6 +129,7 @@
       Rattletrap.Attribute.PartyLeader
       Rattletrap.Attribute.Pickup
       Rattletrap.Attribute.PrivateMatchSettings
+      Rattletrap.Attribute.ProductAttribute
       Rattletrap.Attribute.QWord
       Rattletrap.Attribute.Reservation
       Rattletrap.Attribute.RigidBodyState
diff --git a/replays/338173964F9F71EBDD31058A1936CBB4.replay b/replays/338173964F9F71EBDD31058A1936CBB4.replay
new file mode 100644
Binary files /dev/null and b/replays/338173964F9F71EBDD31058A1936CBB4.replay differ
diff --git a/replays/6B0D1614417085A7AAD82EAA30D8DABE.replay b/replays/6B0D1614417085A7AAD82EAA30D8DABE.replay
new file mode 100644
Binary files /dev/null and b/replays/6B0D1614417085A7AAD82EAA30D8DABE.replay differ
diff --git a/replays/AFB1F46A49737E36928E1EABC6F5B7AD.replay b/replays/AFB1F46A49737E36928E1EABC6F5B7AD.replay
new file mode 100644
Binary files /dev/null and b/replays/AFB1F46A49737E36928E1EABC6F5B7AD.replay differ
diff --git a/tests/Main.hs b/tests/Main.hs
--- a/tests/Main.hs
+++ b/tests/Main.hs
@@ -58,6 +58,7 @@
   , ("22BACD794ABE7B92E50E9CBDBD9C59CE", "a vote to forfeit")
   , ("27B6A7B64553F0F685874584F96BAB1B", "some UTF-16 text")
   , ("29F582C34A65EB34D358A784CBE3C189", "frames")
+  , ("338173964F9F71EBDD31058A1936CBB4", "patch 1.37")
   , ("372DBFCA4BDB340E4357B6BD43032802", "a camera yaw attribute")
   , ("387F059C47C09E253C875CA990EFD9F2", "a frozen attribute")
   , ("3EA147DD485B8DD24810689A7A989E44", "a custom team name")
@@ -70,6 +71,7 @@
   , ("551CA4D44FF2B86015DE44A6B5790D4C", "private match settings")
   , ("6320E51C49066A7C210A2993C2201D5F", "a forfeit attribute")
   , ("6688EEE34BFEB3EC3A9E3283098CC712", "a malformed byte property")
+  , ("6B0D1614417085A7AAD82EAA30D8DABE", "patch 1.37")
   , ("6D1B06D844A5BB91B81FD4B5B28F08BA", "a flip right")
   , ("6F7CFCD24638F8A6567AB3A8B9958A90", "a map with numbers")
   , ("7109EB9846D303E54B7ACBA792036213", "a boost modifier")
@@ -83,6 +85,7 @@
   , ("A558B1B44124D6E021640884E8EEC2A7", "extended explosion data")
   , ("A6711CE74272B2E663DCC9A200A218E3", "a waiting player")
   , ("A7F001A1417A19BFA8C90990D8F7C2FF", "a ready attribute")
+  , ("AFB1F46A49737E36928E1EABC6F5B7AD", "patch 1.37")
   , ("B9F9B87D4A9D0A3D25D4EC91C0401DE2", "a party leader")
   , ("C14F7E0E4D9B5E6BE9AD5D8ED56B174C", "some mutators")
   , ("C8372B1345B1803DEF039F815DBD802D", "a spectator")
