diff --git a/library/Octane/Data.hs b/library/Octane/Data.hs
new file mode 100644
--- /dev/null
+++ b/library/Octane/Data.hs
@@ -0,0 +1,399 @@
+-- | This module contains largely static data about Rocket League itself, such
+-- as which classes objects belong to.
+module Octane.Data where
+
+import Data.Function ((&))
+import Data.Monoid ((<>))
+
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Text as Text
+
+-- * Classes
+
+-- | A map from class names to a set of object names that are that class. This
+-- is typically not useful by itself, as it is used to build the
+-- 'objectToClass' map.
+classToObjects :: Map.Map Text.Text (Set.Set Text.Text)
+classToObjects = let
+    normal =
+        [ ( "TAGame.Ball_TA",
+            [ "Archetypes.Ball.Ball_Default"
+            , "Archetypes.Ball.Ball_Basketball"
+            , "Archetypes.Ball.Ball_Puck"
+            ])
+        , ( "TAGame.CameraSettingsActor_TA",
+            [ "TAGame.CameraSettingsActor_TA:PRI"
+            , "TAGame.Default__CameraSettingsActor_TA"
+            ])
+        , ( "TAGame.CarComponent_Boost_TA",
+            [ "Archetypes.CarComponents.CarComponent_Boost"
+            ])
+        , ( "TAGame.CarComponent_Dodge_TA",
+            [ "Archetypes.CarComponents.CarComponent_Dodge"
+            ])
+        , ( "TAGame.CarComponent_DoubleJump_TA",
+            [ "Archetypes.CarComponents.CarComponent_DoubleJump"
+            ])
+        , ( "TAGame.CarComponent_FlipCar_TA",
+            [ "Archetypes.CarComponents.CarComponent_FlipCar"
+            ])
+        , ( "TAGame.CarComponent_Jump_TA",
+            [ "Archetypes.CarComponents.CarComponent_Jump"
+            ])
+        , ( "TAGame.Car_Season_TA",
+            [ "Archetypes.GameEvent.GameEvent_Season:CarArchetype"
+            ])
+        , ( "TAGame.Car_TA",
+            [ "Archetypes.Car.Car_Default"
+            ])
+        , ( "TAGame.GameEvent_Season_TA",
+            [ "Archetypes.GameEvent.GameEvent_Season"
+            ])
+        , ( "TAGame.GameEvent_SoccarPrivate_TA",
+            [ "Archetypes.GameEvent.GameEvent_SoccarPrivate"
+            ])
+        , ( "TAGame.GameEvent_SoccarSplitscreen_TA",
+            [ "Archetypes.GameEvent.GameEvent_SoccarSplitscreen"
+            ])
+        , ( "TAGame.GameEvent_Soccar_TA",
+            [ "Archetypes.GameEvent.GameEvent_Soccar"
+            , "Archetypes.GameEvent.GameEvent_Basketball"
+            ])
+        , ( "TAGame.GRI_TA",
+            [ "GameInfo_Basketball.GameInfo.GameInfo_Basketball:GameReplicationInfoArchetype"
+            , "GameInfo_Season.GameInfo.GameInfo_Season:GameReplicationInfoArchetype"
+            , "GameInfo_Soccar.GameInfo.GameInfo_Soccar:GameReplicationInfoArchetype"
+            ])
+        , ( "TAGame.PRI_TA",
+            [ "TAGame.Default__PRI_TA"
+            ])
+        , ( "TAGame.Team_TA",
+            [ "Archetypes.Teams.Team"
+            ])
+        ] & map (\ (klass, objects) ->
+            ( Text.pack klass
+            , objects & map Text.pack & Set.fromList
+            ))
+    special =
+        [ ("TAGame.CrowdActor_TA", ".TheWorld:PersistentLevel.CrowdActor_TA_")
+        , ("TAGame.CrowdManager_TA", ".TheWorld:PersistentLevel.CrowdManager_TA_")
+        , ("TAGame.VehiclePickup_Boost_TA", ".TheWorld:PersistentLevel.VehiclePickup_Boost_TA_")
+        ] & map (\ (klass, suffix) ->
+            ( Text.pack klass
+            , levels & Set.map (\ level -> level <> Text.pack suffix)
+            ))
+    in Map.fromList (normal ++ special)
+
+-- | A set of classes that have an initial location vector.
+locationClasses :: Set.Set Text.Text
+locationClasses =
+    [ "TAGame.Ball_TA"
+    , "TAGame.CameraSettingsActor_TA"
+    , "TAGame.CarComponent_Boost_TA"
+    , "TAGame.CarComponent_Dodge_TA"
+    , "TAGame.CarComponent_DoubleJump_TA"
+    , "TAGame.CarComponent_FlipCar_TA"
+    , "TAGame.CarComponent_Jump_TA"
+    , "TAGame.Car_Season_TA"
+    , "TAGame.Car_TA"
+    , "TAGame.GRI_TA"
+    , "TAGame.GameEvent_Season_TA"
+    , "TAGame.GameEvent_SoccarPrivate_TA"
+    , "TAGame.GameEvent_SoccarSplitscreen_TA"
+    , "TAGame.GameEvent_Soccar_TA"
+    , "TAGame.PRI_TA"
+    , "TAGame.Team_TA"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of classes that have an initial rotation vector.
+rotationClasses :: Set.Set Text.Text
+rotationClasses =
+    [ "TAGame.Ball_TA"
+    , "TAGame.Car_Season_TA"
+    , "TAGame.Car_TA"
+    ] & map Text.pack & Set.fromList
+
+-- * Levels
+
+-- | A set of level names. This is typically not useful by itself, as it is
+-- used to build the 'classToObjects' map. If you are going to use it, be
+-- aware that many of the levels are duplicated with strange capitalization.
+-- For example, this set contains both @Wasteland_P@ and @Wasteland_p@.
+levels :: Set.Set Text.Text
+levels =
+    [ "EuroStadium_Rainy_P"
+    , "HoopsStadium_P"
+    , "Park_Night_P"
+    , "Park_Rainy_P"
+    , "Stadium_p"
+    , "TrainStation_Night_P"
+    , "TrainStation_P"
+    , "Trainstation_Night_P"
+    , "UtopiaStadium_Dusk_P"
+    , "UtopiaStadium_Dusk_p"
+    , "UtopiaStadium_P"
+    , "Utopiastadium_p"
+    , "Wasteland_P"
+    , "Wasteland_p"
+    , "eurostad_oob_audio_map"
+    , "eurostadium_p"
+    , "eurostadium_rainy_audio"
+    , "hoopsstadium_sfx"
+    , "labs_doublegoal_p"
+    , "labs_underpass_p"
+    , "labs_utopia_p"
+    , "park_night_sfx"
+    , "park_p"
+    , "park_rainy_sfx"
+    , "park_sfx"
+    , "stadium_oob_audio_map"
+    , "stadium_p"
+    , "stadium_winter_p"
+    , "trainstation_p"
+    , "utopiastadium_p"
+    , "utopiastadium_sfx"
+    , "wasteland_sfx"
+    ] & map Text.pack & Set.fromList
+
+-- * Objects
+
+-- | A map from object names to their class names. Note that any trailing
+-- numbers have been stripped from the object names. So
+-- @Archetypes.Teams.Team0@ is in this map as @Archetypes.Teams.Team@.
+objectToClass :: Map.Map Text.Text Text.Text
+objectToClass = Map.foldrWithKey
+    (\ klass objects m -> objects
+        & Set.map (\ object -> (object, klass))
+        & Set.toList
+        & Map.fromList
+        & Map.union m)
+    Map.empty
+    classToObjects
+
+-- * Properties
+
+-- | A set of properties that are booleans.
+booleanProperties :: Set.Set Text.Text
+booleanProperties =
+    [ "Engine.Actor:bBlockActors"
+    , "Engine.Actor:bCollideActors"
+    , "Engine.Actor:bHardAttach"
+    , "Engine.Actor:bHidden"
+    , "Engine.Actor:bProjTarget"
+    , "Engine.Actor:bTearOff"
+    , "Engine.GameReplicationInfo:bMatchIsOver"
+    , "Engine.Pawn:bCanSwatTurn"
+    , "Engine.Pawn:bRootMotionFromInterpCurve"
+    , "Engine.Pawn:bSimulateGravity"
+    , "Engine.PlayerReplicationInfo:bBot"
+    , "Engine.PlayerReplicationInfo:bIsSpectator"
+    , "Engine.PlayerReplicationInfo:bOnlySpectator"
+    , "Engine.PlayerReplicationInfo:bOutOfLives"
+    , "Engine.PlayerReplicationInfo:bReadyToPlay"
+    , "Engine.PlayerReplicationInfo:bWaitingPlayer"
+    , "ProjectX.GRI_X:bGameStarted"
+    , "TAGame.CameraSettingsActor_TA:bUsingBehindView"
+    , "TAGame.CameraSettingsActor_TA:bUsingSecondaryCamera"
+    , "TAGame.CarComponent_FlipCar_TA:bFlipRight"
+    , "TAGame.GameEvent_Soccar_TA:bBallHasBeenHit"
+    , "TAGame.GameEvent_Soccar_TA:bOverTime"
+    , "TAGame.GameEvent_TA:bHasLeaveMatchPenalty"
+    , "TAGame.GameEvent_Team_TA:bDisableMutingOtherTeam"
+    , "TAGame.PRI_TA:bIsInSplitScreen"
+    , "TAGame.PRI_TA:bMatchMVP"
+    , "TAGame.PRI_TA:bOnlineLoadoutSet"
+    , "TAGame.PRI_TA:bReady"
+    , "TAGame.PRI_TA:bUsingBehindView"
+    , "TAGame.PRI_TA:bUsingSecondaryCamera"
+    , "TAGame.RBActor_TA:bFrozen"
+    , "TAGame.RBActor_TA:bReplayActor"
+    , "TAGame.Vehicle_TA:bDriving"
+    , "TAGame.Vehicle_TA:bReplicatedHandbrake"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are bytes.
+byteProperties :: Set.Set Text.Text
+byteProperties =
+    [ "Engine.PlayerReplicationInfo:Ping"
+    , "TAGame.Ball_TA:HitTeamNum"
+    , "TAGame.CameraSettingsActor_TA:CameraPitch"
+    , "TAGame.CameraSettingsActor_TA:CameraYaw"
+    , "TAGame.CarComponent_Boost_TA:ReplicatedBoostAmount"
+    , "TAGame.CarComponent_TA:ReplicatedActive"
+    , "TAGame.GameEvent_Soccar_TA:ReplicatedScoredOnTeam"
+    , "TAGame.GameEvent_TA:ReplicatedStateIndex"
+    , "TAGame.PRI_TA:CameraPitch"
+    , "TAGame.PRI_TA:CameraYaw"
+    , "TAGame.Vehicle_TA:ReplicatedSteer"
+    , "TAGame.Vehicle_TA:ReplicatedThrottle"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are camera settings.
+camSettingsProperties :: Set.Set Text.Text
+camSettingsProperties =
+    [ "TAGame.CameraSettingsActor_TA:ProfileSettings"
+    , "TAGame.PRI_TA:CameraSettings"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are demolition.
+demolishProperties :: Set.Set Text.Text
+demolishProperties =
+    [ "TAGame.Car_TA:ReplicatedDemolish"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are enumerations.
+enumProperties :: Set.Set Text.Text
+enumProperties =
+    [ "Engine.Actor:Role"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are explosions.
+explosionProperties :: Set.Set Text.Text
+explosionProperties =
+    [ "TAGame.Ball_TA:ReplicatedExplosionData"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are flagged integers.
+flaggedIntProperties :: Set.Set Text.Text
+flaggedIntProperties =
+    [ "Engine.GameReplicationInfo:GameClass"
+    , "Engine.Actor:ReplicatedCollisionType"
+    , "Engine.Pawn:PlayerReplicationInfo"
+    , "Engine.PlayerReplicationInfo:Team"
+    , "TAGame.Ball_TA:GameEvent"
+    , "TAGame.CameraSettingsActor_TA:PRI"
+    , "TAGame.CarComponent_TA:Vehicle"
+    , "TAGame.CrowdActor_TA:GameEvent"
+    , "TAGame.CrowdActor_TA:ReplicatedOneShotSound"
+    , "TAGame.CrowdManager_TA:GameEvent"
+    , "TAGame.CrowdManager_TA:ReplicatedGlobalOneShotSound"
+    , "TAGame.PRI_TA:PersistentCamera"
+    , "TAGame.PRI_TA:ReplicatedGameEvent"
+    , "TAGame.Team_TA:GameEvent"
+    , "TAGame.Team_TA:LogoData"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are floats.
+floatProperties :: Set.Set Text.Text
+floatProperties =
+    [ "Engine.Actor:DrawScale"
+    , "TAGame.Ball_TA:ReplicatedAddedCarBounceScale"
+    , "TAGame.Ball_TA:ReplicatedBallMaxLinearSpeedScale"
+    , "TAGame.Ball_TA:ReplicatedBallScale"
+    , "TAGame.Ball_TA:ReplicatedWorldBounceScale"
+    , "TAGame.CarComponent_FlipCar_TA:FlipCarTime"
+    , "TAGame.CrowdActor_TA:ModifiedNoise"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are game modes.
+gameModeProperties :: Set.Set Text.Text
+gameModeProperties =
+    [ "TAGame.GameEvent_TA:GameMode"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are integers.
+intProperties :: Set.Set Text.Text
+intProperties =
+    [ "Engine.PlayerReplicationInfo:PlayerID"
+    , "Engine.PlayerReplicationInfo:Score"
+    , "Engine.TeamInfo:Score"
+    , "ProjectX.GRI_X:ReplicatedGameMutatorIndex"
+    , "ProjectX.GRI_X:ReplicatedGamePlaylist"
+    , "TAGame.CrowdActor_TA:ReplicatedCountDownNumber"
+    , "TAGame.GameEvent_Soccar_TA:RoundNum"
+    , "TAGame.GameEvent_Soccar_TA:SecondsRemaining"
+    , "TAGame.GameEvent_TA:BotSkill"
+    , "TAGame.GameEvent_TA:ReplicatedGameStateTimeRemaining"
+    , "TAGame.GameEvent_TA:ReplicatedStateName"
+    , "TAGame.GameEvent_Team_TA:MaxTeamSize"
+    , "TAGame.PRI_TA:MatchAssists"
+    , "TAGame.PRI_TA:MatchGoals"
+    , "TAGame.PRI_TA:MatchSaves"
+    , "TAGame.PRI_TA:MatchScore"
+    , "TAGame.PRI_TA:MatchShots"
+    , "TAGame.PRI_TA:Title"
+    , "TAGame.PRI_TA:TotalXP"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are online loadouts.
+loadoutOnlineProperties :: Set.Set Text.Text
+loadoutOnlineProperties =
+    [ "TAGame.PRI_TA:ClientLoadoutOnline"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are loadouts.
+loadoutProperties :: Set.Set Text.Text
+loadoutProperties =
+    [ "TAGame.PRI_TA:ClientLoadout"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are locations.
+locationProperties :: Set.Set Text.Text
+locationProperties =
+    [ "TAGame.CarComponent_Dodge_TA:DodgeTorque"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are music stingers.
+musicStingerProperties :: Set.Set Text.Text
+musicStingerProperties =
+    [ "TAGame.GameEvent_Soccar_TA:ReplicatedMusicStinger"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are pickups.
+pickupProperties :: Set.Set Text.Text
+pickupProperties =
+    [ "TAGame.VehiclePickup_TA:ReplicatedPickupData"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are private match settings.
+privateMatchSettingsProperties :: Set.Set Text.Text
+privateMatchSettingsProperties =
+    [ "TAGame.GameEvent_SoccarPrivate_TA:MatchSettings"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are Q words, whatever those are.
+qWordProperties :: Set.Set Text.Text
+qWordProperties =
+    [ "ProjectX.GRI_X:GameServerID"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are relation rotations.
+relativeRotationProperties :: Set.Set Text.Text
+relativeRotationProperties =
+    [ "Engine.Actor:RelativeRotation"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are reservations.
+reservationProperties :: Set.Set Text.Text
+reservationProperties =
+    [ "ProjectX.GRI_X:Reservations"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are rigid body states.
+rigidBodyStateProperties :: Set.Set Text.Text
+rigidBodyStateProperties =
+    [ "TAGame.RBActor_TA:ReplicatedRBState"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are strings.
+stringProperties :: Set.Set Text.Text
+stringProperties =
+    [ "Engine.GameReplicationInfo:ServerName"
+    , "Engine.PlayerReplicationInfo:PlayerName"
+    , "Engine.PlayerReplicationInfo:RemoteUserData"
+    , "TAGame.GRI_TA:NewDedicatedServerIP"
+    , "TAGame.Team_TA:CustomTeamName"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are team paints.
+teamPaintProperties :: Set.Set Text.Text
+teamPaintProperties =
+    [ "TAGame.Car_TA:TeamPaint"
+    ] & map Text.pack & Set.fromList
+
+-- | A set of properties that are unique IDs.
+uniqueIdProperties :: Set.Set Text.Text
+uniqueIdProperties =
+    [ "Engine.PlayerReplicationInfo:UniqueId"
+    ] & map Text.pack & Set.fromList
diff --git a/library/Octane/Main.hs b/library/Octane/Main.hs
--- a/library/Octane/Main.hs
+++ b/library/Octane/Main.hs
@@ -2,7 +2,6 @@
 
 import qualified Control.DeepSeq as DeepSeq
 import qualified Control.Monad as Monad
-import qualified Control.Newtype as Newtype
 import qualified Data.Aeson as Aeson
 import qualified Data.Binary as Binary
 import qualified Data.Binary.Get as Binary
@@ -52,14 +51,14 @@
             let frames = Parser.parseFrames replay
             let expectedFrames = replay
                     & Type.replayProperties
-                    & Newtype.unpack
-                    & Map.lookup ("NumFrames" & Text.pack & Newtype.pack)
-                    & Maybe.fromMaybe (Type.IntProperty (Newtype.pack 4) (Newtype.pack 0))
+                    & Type.unpackDictionary
+                    & Map.lookup ("NumFrames" & Text.pack & Type.PCString)
+                    & Maybe.fromMaybe (Type.IntProperty (Type.Word64LE 4) (Type.Word32LE 0))
             let actualFrames = frames
                     & length
                     & fromIntegral
-                    & Newtype.pack
-                    & Type.IntProperty (Newtype.pack 4)
+                    & Type.Word32LE
+                    & Type.IntProperty (Type.Word64LE 4)
                     & DeepSeq.deepseq frames
             Monad.when (expectedFrames /= actualFrames) (error
                 ( "expected "
diff --git a/library/Octane/Parser.hs b/library/Octane/Parser.hs
--- a/library/Octane/Parser.hs
+++ b/library/Octane/Parser.hs
@@ -5,7 +5,6 @@
 import Data.Function ((&))
 
 import qualified Control.DeepSeq as DeepSeq
-import qualified Control.Newtype as Newtype
 import qualified Data.Aeson as Aeson
 import qualified Data.Aeson.Types as Aeson
 import qualified Data.Binary.Bits.Get as Bits
@@ -22,6 +21,7 @@
 import qualified Data.Text.Encoding as Encoding
 import qualified Data.Word as Word
 import qualified GHC.Generics as Generics
+import qualified Octane.Data as Data
 import qualified Octane.Parser.ClassPropertyMap as CPM
 import qualified Octane.Type as Type
 import qualified Text.Printf as Printf
@@ -30,13 +30,13 @@
 parseFrames replay = let
     numFrames = replay
         & Type.replayProperties
-        & Newtype.unpack
-        & Map.lookup ("NumFrames" & Text.pack & Newtype.pack)
+        & Type.unpackDictionary
+        & Map.lookup ("NumFrames" & Text.pack & Type.PCString)
         & (\ property -> case property of
-            Just (Type.IntProperty _ x) -> x & Newtype.unpack & fromIntegral
+            Just (Type.IntProperty _ x) -> x & Type.unpackWord32LE & fromIntegral
             _ -> 0)
     get = replay & extractContext & getFrames numFrames & Bits.runBitGet
-    stream = replay & Type.replayStream & Newtype.unpack & BSL.fromStrict
+    stream = replay & Type.replayStream & Type.unpackStream & BSL.fromStrict
     (_context, frames) = Binary.runGet get stream
     in frames
 
@@ -130,7 +130,7 @@
     let objectName = case context & contextObjectMap & IntMap.lookup objectId of
             Nothing -> error ("could not find object name for id " ++ show objectId)
             Just x -> x
-    let (classId,className) = case CPM.getClass (contextObjectMap context) CPM.archetypeMap (contextClassMap context) objectId of
+    let (classId,className) = case CPM.getClass (contextObjectMap context) Data.objectToClass (contextClassMap context) objectId of
             Nothing -> error ("could not find class for object id " ++ show objectId)
             Just x -> x
     classInit <- getClassInit className
@@ -148,11 +148,11 @@
     return
         ( newContext
         , Replication
-          { replicationActorId = actorId
-          , replicationIsOpen = True
-          , replicationIsNew = Just True
-          , replicationClassInit = Just classInit
-          , replicationProps = []
+          { replicationObjectName = objectName
+          , replicationClassName = className
+          , replicationState = RSOpening
+          , replicationInitialization = Just classInit
+          , replicationProperties = Map.empty
           })
 
 getExistingReplication :: Context
@@ -164,37 +164,43 @@
             Just x -> x
     props <- getProps context thing
     return (context, Replication
-        { replicationActorId = actorId
-        , replicationIsOpen = True
-        , replicationIsNew = Just False
-        , replicationClassInit = Nothing
-        , replicationProps = props
+        { replicationObjectName = thingObjectName thing
+        , replicationClassName = thingClassName thing
+        , replicationState = RSExisting
+        , replicationInitialization = Nothing
+        , replicationProperties = props
         })
 
 getClosedReplication :: Context
                      -> ActorId
                      -> Bits.BitGet (Context, Replication)
 getClosedReplication context actorId = do
+    let thing = case context & contextThings & IntMap.lookup actorId of
+            Nothing -> error ("could not find thing for actor id " ++ show actorId)
+            Just x -> x
     let newThings = context & contextThings & IntMap.delete actorId
     let newContext = context { contextThings = newThings }
     return
         ( newContext
         , Replication
-          { replicationActorId = actorId
-          , replicationIsOpen = False
-          , replicationIsNew = Nothing
-          , replicationClassInit = Nothing
-          , replicationProps = []
+          { replicationObjectName = thingObjectName thing
+          , replicationClassName = thingClassName thing
+          , replicationState = RSClosing
+          , replicationInitialization = Nothing
+          , replicationProperties = Map.empty
           })
 
-getProps :: Context -> Thing -> Bits.BitGet [Prop]
+getProps :: Context -> Thing -> Bits.BitGet (Map.Map Text.Text PropValue)
 getProps context thing = do
     maybeProp <- getMaybeProp context thing
     case maybeProp of
-        Nothing -> return []
+        Nothing -> return Map.empty
         Just prop -> do
+            let k = propName prop
+            let v = propValue prop
+            let m = Map.singleton k v
             props <- getProps context thing
-            return (prop : props)
+            return (Map.union m props)
 
 getMaybeProp :: Context -> Thing -> Bits.BitGet (Maybe Prop)
 getMaybeProp context thing = do
@@ -219,150 +225,238 @@
     value <- getPropValue name
     return (Prop { propName = name, propValue = value })
 
+--
+
 getPropValue :: Text.Text -> Bits.BitGet PropValue
-getPropValue name = case Text.unpack name of
-    _ | Set.member name propsWithRigidBodyState -> do
-        flag <- Bits.getBool
-        position <- getVector
-        rotation <- getFloatVector
-        x <- if flag then return Nothing else fmap Just getVector
-        y <- if flag then return Nothing else fmap Just getVector
-        return (PRigidBodyState flag position rotation x y)
-    _ | Set.member name propsWithFlaggedInt -> do
-        flag <- Bits.getBool
-        int <- getInt32
-        return (PFlaggedInt flag (fromIntegral int))
-    _ | Set.member name propsWithString -> do
+getPropValue name = case Map.lookup name propertyNameToGet of
+    Nothing -> error ("don't know how to read property " ++ show name)
+    Just get -> get
+
+propertyNameToGet :: Map.Map Text.Text (Bits.BitGet PropValue)
+propertyNameToGet =
+    [ (Data.booleanProperties, getBooleanProperty)
+    , (Data.byteProperties, getByteProperty)
+    , (Data.camSettingsProperties, getCamSettingsProperty)
+    , (Data.demolishProperties, getDemolishProperty)
+    , (Data.enumProperties, getEnumProperty)
+    , (Data.explosionProperties, getExplosionProperty)
+    , (Data.flaggedIntProperties, getFlaggedIntProperty)
+    , (Data.floatProperties, getFloatProperty)
+    , (Data.gameModeProperties, getGameModeProperty)
+    , (Data.intProperties, getIntProperty)
+    , (Data.loadoutOnlineProperties, getLoadoutOnlineProperty)
+    , (Data.loadoutProperties, getLoadoutProperty)
+    , (Data.locationProperties, getLocationProperty)
+    , (Data.musicStingerProperties, getMusicStingerProperty)
+    , (Data.pickupProperties, getPickupProperty)
+    , (Data.privateMatchSettingsProperties, getPrivateMatchSettingsProperty)
+    , (Data.qWordProperties, getQWordProperty)
+    , (Data.relativeRotationProperties, getRelativeRotationProperty)
+    , (Data.reservationProperties, getReservationProperty)
+    , (Data.rigidBodyStateProperties, getRigidBodyStateProperty)
+    , (Data.stringProperties, getStringProperty)
+    , (Data.teamPaintProperties, getTeamPaintProperty)
+    , (Data.uniqueIdProperties, getUniqueIdProperty)
+    , (Set.fromList [Text.pack "TAGame.PRI_TA:PartyLeader"], getPartyLeaderProperty)
+    ]
+        & concatMap (\ (ks, v) -> ks & Set.toList & map (\ k -> (k, v)))
+        & Map.fromList
+
+getBooleanProperty :: Bits.BitGet PropValue
+getBooleanProperty = do
+    bool <- Bits.getBool
+    return (PBoolean bool)
+
+getByteProperty :: Bits.BitGet PropValue
+getByteProperty = do
+    int <- getInt8
+    return (PByte int)
+
+getCamSettingsProperty :: Bits.BitGet PropValue
+getCamSettingsProperty = do
+    fov <- getFloat32
+    height <- getFloat32
+    pitch <- getFloat32
+    dist <- getFloat32
+    stiff <- getFloat32
+    swiv <- getFloat32
+    return (PCamSettings fov height pitch dist stiff swiv)
+
+getDemolishProperty :: Bits.BitGet PropValue
+getDemolishProperty = do
+    atkFlag <- Bits.getBool
+    atk <- getInt32
+    vicFlag <- Bits.getBool
+    vic <- getInt32
+    vec1 <- getVector
+    vec2 <- getVector
+    return (PDemolish atkFlag atk vicFlag vic vec1 vec2)
+
+getEnumProperty :: Bits.BitGet PropValue
+getEnumProperty = do
+    x <- Bits.getWord16be 10
+    y <- if x == 1023
+        then Bits.getBool
+        else error ("unexpected enum value " ++ show x)
+    return (PEnum x y)
+
+getExplosionProperty :: Bits.BitGet PropValue
+getExplosionProperty = do
+    noGoal <- Bits.getBool
+    a <- if noGoal then return Nothing else fmap Just getInt32
+    b <- getVector
+    return (PExplosion noGoal a b)
+
+getFlaggedIntProperty :: Bits.BitGet PropValue
+getFlaggedIntProperty = do
+    flag <- Bits.getBool
+    int <- getInt32
+    return (PFlaggedInt flag (fromIntegral int))
+
+getFloatProperty :: Bits.BitGet PropValue
+getFloatProperty = do
+    float <- getFloat32
+    return (PFloat float)
+
+getGameModeProperty :: Bits.BitGet PropValue
+getGameModeProperty = do
+    x <- Bits.getWord8 2
+    if x == 2
+        then return (PGameMode x)
+        else error ("unexpected game mode value " ++ show x)
+
+getIntProperty :: Bits.BitGet PropValue
+getIntProperty = do
+    int <- getInt32
+    return (PInt int)
+
+getLoadoutOnlineProperty :: Bits.BitGet PropValue
+getLoadoutOnlineProperty = do
+    version <- getInt32
+    x <- getInt32
+    y <- getInt32
+    z <- if version >= 12
+        then do
+            value <- getInt8
+            return (Just value)
+        else return Nothing
+    return (PLoadoutOnline version x y z)
+
+getLoadoutProperty :: Bits.BitGet PropValue
+getLoadoutProperty = do
+    version <- getInt8
+    a <- getInt32
+    b <- getInt32
+    c <- getInt32
+    d <- getInt32
+    e <- getInt32
+    f <- getInt32
+    g <- getInt32
+    h <- if version > 10
+        then do
+            value <- getInt32
+            return (Just value)
+        else return Nothing
+    return (PLoadout version a b c d e f g h)
+
+getLocationProperty :: Bits.BitGet PropValue
+getLocationProperty = do
+    vector <- getVector
+    return (PLocation vector)
+
+getMusicStingerProperty :: Bits.BitGet PropValue
+getMusicStingerProperty = do
+    flag <- Bits.getBool
+    cue <- getInt32
+    trigger <- getInt8
+    return (PMusicStinger flag cue trigger)
+
+getPickupProperty :: Bits.BitGet PropValue
+getPickupProperty = do
+    instigator <- Bits.getBool
+    instigatorId <- if instigator then fmap Just getInt32 else return Nothing
+    pickedUp <- Bits.getBool
+    return (PPickup instigator instigatorId pickedUp)
+
+getPrivateMatchSettingsProperty :: Bits.BitGet PropValue
+getPrivateMatchSettingsProperty = do
+    mutators <- getString
+    joinableBy <- getInt32
+    maxPlayers <- getInt32
+    gameName <- getString
+    password <- getString
+    flag <- Bits.getBool
+    return (PPrivateMatchSettings mutators joinableBy maxPlayers gameName password flag)
+
+getQWordProperty :: Bits.BitGet PropValue
+getQWordProperty = do
+    x <- getInt32
+    y <- getInt32
+    return (PQWord x y)
+
+getRelativeRotationProperty :: Bits.BitGet PropValue
+getRelativeRotationProperty = do
+    vector <- getFloatVector
+    return (PRelativeRotation vector)
+
+getReservationProperty :: Bits.BitGet PropValue
+getReservationProperty = do
+    -- I think this is the connection order. The first player to connect
+    -- gets number 0, and it goes up from there. The maximum is 7, which
+    -- would be a full 4x4 game.
+    number <- getInt7
+    (systemId, remoteId, localId) <- getUniqueId
+    playerName <- if systemId == 0 then return Nothing else do
         string <- getString
-        return (PString string)
-    _ | Set.member name propsWithBoolean -> do
-        bool <- Bits.getBool
-        return (PBoolean bool)
-    _ | Set.member name propsWithQWord -> do
-        x <- getInt32
-        y <- getInt32
-        return (PQWord x y)
-    _ | Set.member name propsWithInt -> do
-        int <- getInt32
-        return (PInt int)
-    _ | Set.member name propsWithByte -> do
-        int <- getInt8
-        return (PByte int)
-    _ | Set.member name propsWithCamSettings -> do
-        fov <- getFloat32
-        height <- getFloat32
-        pitch <- getFloat32
-        dist <- getFloat32
-        stiff <- getFloat32
-        swiv <- getFloat32
-        return (PCamSettings fov height pitch dist stiff swiv)
-    _ | Set.member name propsWithLocation -> do
-        vector <- getVector
-        return (PLocation vector)
-    _ | Set.member name propsWithFloat -> do
-        float <- getFloat32
-        return (PFloat float)
-    "Engine.PlayerReplicationInfo:UniqueId" -> do
-        (systemId, remoteId, localId) <- getUniqueId
-        return (PUniqueId systemId remoteId localId)
-    "TAGame.PRI_TA:PartyLeader" -> do
-        systemId <- getSystemId
-        if systemId == 0
-            then do
-                let remoteId = SplitscreenId Nothing
-                let localId = Nothing
-                return (PUniqueId systemId remoteId localId)
-            else do
-                remoteId <- getRemoteId systemId
-                localId <- getLocalId
-                return (PUniqueId systemId remoteId localId)
-    "ProjectX.GRI_X:Reservations" -> do
-        -- I think this is the connection order. The first player to connect
-        -- gets number 0, and it goes up from there. The maximum is 7, which
-        -- would be a full 4x4 game.
-        number <- getInt7
-        (systemId, remoteId, localId) <- getUniqueId
-        playerName <- if systemId == 0 then return Nothing else do
-            string <- getString
-            return (Just string)
-        -- No idea what these two flags are. Might be for bots?
-        a <- Bits.getBool
-        b <- Bits.getBool
-        return (PReservation number systemId remoteId localId playerName a b)
-    "TAGame.PRI_TA:ClientLoadoutOnline" -> do
-        version <- getInt32
-        x <- getInt32
-        y <- getInt32
-        z <- if version >= 12
-            then do
-                value <- getInt8
-                return (Just value)
-            else return Nothing
-        return (PLoadoutOnline version x y z)
-    "TAGame.PRI_TA:ClientLoadout" -> do
-        version <- getInt8
-        a <- getInt32
-        b <- getInt32
-        c <- getInt32
-        d <- getInt32
-        e <- getInt32
-        f <- getInt32
-        g <- getInt32
-        h <- if version > 10
-            then do
-                value <- getInt32
-                return (Just value)
-            else return Nothing
-        return (PLoadout version a b c d e f g h)
-    "TAGame.Car_TA:TeamPaint" -> do
-        team <- getInt8
-        teamColor <- getInt8
-        customColor <- getInt8
-        teamFinish <- getInt32
-        customFinish <- getInt32
-        return (PTeamPaint team teamColor customColor teamFinish customFinish)
-    "TAGame.VehiclePickup_TA:ReplicatedPickupData" -> do
-        instigator <- Bits.getBool
-        instigatorId <- if instigator then fmap Just getInt32 else return Nothing
-        pickedUp <- Bits.getBool
-        return (PPickup instigator instigatorId pickedUp)
-    "Engine.Actor:Role" -> do
-        x <- Bits.getWord16be 11
-        return (PEnum x)
-    "TAGame.Ball_TA:ReplicatedExplosionData" -> do
-        noGoal <- Bits.getBool
-        a <- if noGoal then return Nothing else fmap Just getInt32
-        b <- getVector
-        return (PExplosion noGoal a b)
-    "TAGame.GameEvent_Soccar_TA:ReplicatedMusicStinger" -> do
-        flag <- Bits.getBool
-        cue <- getInt32
-        trigger <- getInt8
-        return (PMusicStinger flag cue trigger)
-    "TAGame.Car_TA:ReplicatedDemolish" -> do
-        hasAtk <- Bits.getBool
-        atk <- getInt32
-        hasVic <- Bits.getBool
-        vic <- getInt32
-        vec1 <- getVector
-        vec2 <- getVector
-        return (PDemolish hasAtk (Just atk) hasVic (Just vic) vec1 vec2)
-    "TAGame.GameEvent_SoccarPrivate_TA:MatchSettings" -> do
-        mutators <- getString
-        joinableBy <- getInt32
-        maxPlayers <- getInt32
-        gameName <- getString
-        password <- getString
-        flag <- Bits.getBool
-        return (PPrivateMatchSettings mutators joinableBy maxPlayers gameName password flag)
-    "Engine.Actor:RelativeRotation" -> do
-        vector <- getFloatVector
-        return (PRelativeRotation vector)
-    "TAGame.GameEvent_TA:GameMode" -> do
-        mode <- Bits.getWord8 2
-        return (PGameMode mode)
-    _ -> fail ("don't know how to read property " ++ show name)
+        return (Just string)
+    -- No idea what these two flags are. Might be for bots?
+    a <- Bits.getBool
+    b <- Bits.getBool
+    return (PReservation number systemId remoteId localId playerName a b)
 
+getRigidBodyStateProperty :: Bits.BitGet PropValue
+getRigidBodyStateProperty = do
+    flag <- Bits.getBool
+    position <- getVector
+    rotation <- getFloatVector
+    x <- if flag then return Nothing else fmap Just getVector
+    y <- if flag then return Nothing else fmap Just getVector
+    return (PRigidBodyState flag position rotation x y)
+
+getStringProperty :: Bits.BitGet PropValue
+getStringProperty = do
+    string <- getString
+    return (PString string)
+
+getTeamPaintProperty :: Bits.BitGet PropValue
+getTeamPaintProperty = do
+    team <- getInt8
+    teamColor <- getInt8
+    customColor <- getInt8
+    teamFinish <- getInt32
+    customFinish <- getInt32
+    return (PTeamPaint team teamColor customColor teamFinish customFinish)
+
+getUniqueIdProperty :: Bits.BitGet PropValue
+getUniqueIdProperty = do
+    (systemId, remoteId, localId) <- getUniqueId
+    return (PUniqueId systemId remoteId localId)
+
+-- | Even though this is just a unique ID property, it must be handled
+-- | specially because it sometimes doesn't have the remote or local IDs.
+getPartyLeaderProperty :: Bits.BitGet PropValue
+getPartyLeaderProperty = do
+    systemId <- getSystemId
+    (remoteId, localId) <- if systemId == 0
+        then return (SplitscreenId Nothing, Nothing)
+        else do
+            remoteId <- getRemoteId systemId
+            localId <- getLocalId
+            return (remoteId, localId)
+    return (PUniqueId systemId remoteId localId)
+
+--
+
 getFloat32 :: Bits.BitGet Float
 getFloat32 = do
     bytes <- Bits.getByteString 4
@@ -427,142 +521,10 @@
     localId <- Bits.getWord8 8
     localId & Just & return
 
-propsWithRigidBodyState :: Set.Set Text.Text
-propsWithRigidBodyState =
-    [ "TAGame.RBActor_TA:ReplicatedRBState"
-    ] & map Text.pack & Set.fromList
-
-propsWithFlaggedInt :: Set.Set Text.Text
-propsWithFlaggedInt =
-    [ "Engine.GameReplicationInfo:GameClass"
-    , "Engine.Actor:ReplicatedCollisionType"
-    , "Engine.Pawn:PlayerReplicationInfo"
-    , "Engine.PlayerReplicationInfo:Team"
-    , "TAGame.Ball_TA:GameEvent"
-    , "TAGame.CameraSettingsActor_TA:PRI"
-    , "TAGame.CarComponent_TA:Vehicle"
-    , "TAGame.CrowdActor_TA:GameEvent"
-    , "TAGame.CrowdActor_TA:ReplicatedOneShotSound"
-    , "TAGame.CrowdManager_TA:GameEvent"
-    , "TAGame.CrowdManager_TA:ReplicatedGlobalOneShotSound"
-    , "TAGame.PRI_TA:PersistentCamera"
-    , "TAGame.PRI_TA:ReplicatedGameEvent"
-    , "TAGame.Team_TA:GameEvent"
-    , "TAGame.Team_TA:LogoData"
-    ] & map Text.pack & Set.fromList
-
-propsWithString :: Set.Set Text.Text
-propsWithString =
-    [ "Engine.GameReplicationInfo:ServerName"
-    , "Engine.PlayerReplicationInfo:PlayerName"
-    , "Engine.PlayerReplicationInfo:RemoteUserData"
-    , "TAGame.GRI_TA:NewDedicatedServerIP"
-    , "TAGame.Team_TA:CustomTeamName"
-    ] & map Text.pack & Set.fromList
-
-propsWithBoolean :: Set.Set Text.Text
-propsWithBoolean =
-    [ "Engine.Actor:bBlockActors"
-    , "Engine.Actor:bCollideActors"
-    , "Engine.Actor:bHardAttach"
-    , "Engine.Actor:bHidden"
-    , "Engine.Actor:bProjTarget"
-    , "Engine.Actor:bTearOff"
-    , "Engine.GameReplicationInfo:bMatchIsOver"
-    , "Engine.Pawn:bCanSwatTurn"
-    , "Engine.Pawn:bRootMotionFromInterpCurve"
-    , "Engine.Pawn:bSimulateGravity"
-    , "Engine.PlayerReplicationInfo:bBot"
-    , "Engine.PlayerReplicationInfo:bIsSpectator"
-    , "Engine.PlayerReplicationInfo:bOnlySpectator"
-    , "Engine.PlayerReplicationInfo:bOutOfLives"
-    , "Engine.PlayerReplicationInfo:bReadyToPlay"
-    , "Engine.PlayerReplicationInfo:bWaitingPlayer"
-    , "ProjectX.GRI_X:bGameStarted"
-    , "TAGame.CameraSettingsActor_TA:bUsingBehindView"
-    , "TAGame.CameraSettingsActor_TA:bUsingSecondaryCamera"
-    , "TAGame.CarComponent_FlipCar_TA:bFlipRight"
-    , "TAGame.GameEvent_Soccar_TA:bBallHasBeenHit"
-    , "TAGame.GameEvent_Soccar_TA:bOverTime"
-    , "TAGame.GameEvent_TA:bHasLeaveMatchPenalty"
-    , "TAGame.GameEvent_Team_TA:bDisableMutingOtherTeam"
-    , "TAGame.PRI_TA:bIsInSplitScreen"
-    , "TAGame.PRI_TA:bMatchMVP"
-    , "TAGame.PRI_TA:bOnlineLoadoutSet"
-    , "TAGame.PRI_TA:bReady"
-    , "TAGame.PRI_TA:bUsingBehindView"
-    , "TAGame.PRI_TA:bUsingSecondaryCamera"
-    , "TAGame.RBActor_TA:bFrozen"
-    , "TAGame.RBActor_TA:bReplayActor"
-    , "TAGame.Vehicle_TA:bDriving"
-    , "TAGame.Vehicle_TA:bReplicatedHandbrake"
-    ] & map Text.pack & Set.fromList
-
-propsWithQWord :: Set.Set Text.Text
-propsWithQWord =
-    [ "ProjectX.GRI_X:GameServerID"
-    ] & map Text.pack & Set.fromList
-
-propsWithInt :: Set.Set Text.Text
-propsWithInt =
-    [ "Engine.PlayerReplicationInfo:PlayerID"
-    , "Engine.PlayerReplicationInfo:Score"
-    , "Engine.TeamInfo:Score"
-    , "ProjectX.GRI_X:ReplicatedGameMutatorIndex"
-    , "ProjectX.GRI_X:ReplicatedGamePlaylist"
-    , "TAGame.CrowdActor_TA:ReplicatedCountDownNumber"
-    , "TAGame.GameEvent_Soccar_TA:RoundNum"
-    , "TAGame.GameEvent_Soccar_TA:SecondsRemaining"
-    , "TAGame.GameEvent_TA:BotSkill"
-    , "TAGame.GameEvent_TA:ReplicatedGameStateTimeRemaining"
-    , "TAGame.GameEvent_TA:ReplicatedStateName"
-    , "TAGame.GameEvent_Team_TA:MaxTeamSize"
-    , "TAGame.PRI_TA:MatchAssists"
-    , "TAGame.PRI_TA:MatchGoals"
-    , "TAGame.PRI_TA:MatchSaves"
-    , "TAGame.PRI_TA:MatchScore"
-    , "TAGame.PRI_TA:MatchShots"
-    , "TAGame.PRI_TA:Title"
-    , "TAGame.PRI_TA:TotalXP"
-    ] & map Text.pack & Set.fromList
-
-propsWithByte :: Set.Set Text.Text
-propsWithByte =
-    [ "Engine.PlayerReplicationInfo:Ping"
-    , "TAGame.Ball_TA:HitTeamNum"
-    , "TAGame.CameraSettingsActor_TA:CameraPitch"
-    , "TAGame.CameraSettingsActor_TA:CameraYaw"
-    , "TAGame.CarComponent_Boost_TA:ReplicatedBoostAmount"
-    , "TAGame.CarComponent_TA:ReplicatedActive"
-    , "TAGame.GameEvent_Soccar_TA:ReplicatedScoredOnTeam"
-    , "TAGame.GameEvent_TA:ReplicatedStateIndex"
-    , "TAGame.PRI_TA:CameraPitch"
-    , "TAGame.PRI_TA:CameraYaw"
-    , "TAGame.Vehicle_TA:ReplicatedSteer"
-    , "TAGame.Vehicle_TA:ReplicatedThrottle"
-    ] & map Text.pack & Set.fromList
-
-propsWithCamSettings :: Set.Set Text.Text
-propsWithCamSettings =
-    [ "TAGame.CameraSettingsActor_TA:ProfileSettings"
-    , "TAGame.PRI_TA:CameraSettings"
-    ] & map Text.pack & Set.fromList
-
-propsWithLocation :: Set.Set Text.Text
-propsWithLocation =
-    [ "TAGame.CarComponent_Dodge_TA:DodgeTorque"
-    ] & map Text.pack & Set.fromList
-
-propsWithFloat :: Set.Set Text.Text
-propsWithFloat =
-    [ "Engine.Actor:DrawScale"
-    , "TAGame.Ball_TA:ReplicatedAddedCarBounceScale"
-    , "TAGame.Ball_TA:ReplicatedBallMaxLinearSpeedScale"
-    , "TAGame.Ball_TA:ReplicatedBallScale"
-    , "TAGame.Ball_TA:ReplicatedWorldBounceScale"
-    , "TAGame.CarComponent_FlipCar_TA:FlipCarTime"
-    , "TAGame.CrowdActor_TA:ModifiedNoise"
-    ] & map Text.pack & Set.fromList
+toJsonOptions :: String -> Aeson.Options
+toJsonOptions name = Aeson.defaultOptions
+    { Aeson.fieldLabelModifier = Aeson.camelTo2 '_' . drop (length name)
+    }
 
 type SystemId = Word.Word8
 
@@ -589,48 +551,40 @@
 
 instance DeepSeq.NFData Prop
 instance Aeson.ToJSON Prop where
-    toJSON =
-        Aeson.genericToJSON
-            Aeson.defaultOptions
-            { Aeson.fieldLabelModifier = drop 4
-            }
+    toJSON = Aeson.genericToJSON (toJsonOptions "Prop")
 
 data PropValue
-    = PRigidBodyState !Bool !(Vector Int) !(Vector Float) !(Maybe (Vector Int)) !(Maybe (Vector Int))
+    = PBoolean !Bool
+    | PByte !Int
+    | PCamSettings !Float !Float !Float !Float !Float !Float
+    | PDemolish !Bool !Int !Bool !Int !(Vector Int) !(Vector Int)
+    | PEnum !Word.Word16 !Bool
+    | PExplosion !Bool !(Maybe Int) !(Vector Int)
     | PFlaggedInt !Bool !Int
-    | PString !Text.Text
-    | PBoolean !Bool
-    | PQWord Int !Int
-    | PReservation !Int !SystemId !RemoteId !LocalId !(Maybe Text.Text) !Bool !Bool
+    | PFloat !Float
+    | PGameMode !Word.Word8
     | PInt !Int
-    | PByte !Int
-    | PUniqueId !SystemId !RemoteId !LocalId
-    | PLoadoutOnline !Int !Int !Int !(Maybe Int)
     | PLoadout !Int !Int !Int !Int !Int !Int !Int !Int !(Maybe Int)
-    | PCamSettings !Float !Float !Float !Float !Float !Float
-    | PTeamPaint !Int !Int !Int !Int !Int
+    | PLoadoutOnline !Int !Int !Int !(Maybe Int)
     | PLocation !(Vector Int)
-    | PPickup !Bool !(Maybe Int) !Bool
-    | PEnum !Word.Word16
-    | PExplosion !Bool !(Maybe Int) !(Vector Int)
     | PMusicStinger !Bool !Int !Int
-    | PFloat !Float
-    | PDemolish !Bool !(Maybe Int) !Bool !(Maybe Int) !(Vector Int) !(Vector Int)
+    | PPickup !Bool !(Maybe Int) !Bool
     | PPrivateMatchSettings !Text.Text !Int !Int !Text.Text !Text.Text !Bool
+    | PQWord !Int !Int
     | PRelativeRotation !(Vector Float)
-    | PGameMode !Word.Word8
+    | PReservation !Int !SystemId !RemoteId !LocalId !(Maybe Text.Text) !Bool !Bool
+    | PRigidBodyState !Bool !(Vector Int) !(Vector Float) !(Maybe (Vector Int)) !(Maybe (Vector Int))
+    | PString !Text.Text
+    | PTeamPaint !Int !Int !Int !Int !Int
+    | PUniqueId !SystemId !RemoteId !LocalId
     deriving (Eq, Generics.Generic, Show)
 
 instance DeepSeq.NFData PropValue
 instance Aeson.ToJSON PropValue where
-    toJSON =
-        Aeson.genericToJSON
-            Aeson.defaultOptions
-            { Aeson.fieldLabelModifier = drop 9
-            }
+    toJSON = Aeson.genericToJSON (toJsonOptions "PropValue")
 
 -- | A frame in the net stream. Each frame has the time since the beginning of
--- | the match, the time since the last frame, and a list of replications.
+-- the match, the time since the last frame, and a list of replications.
 data Frame = Frame
     { frameTime :: !Float
     , frameDelta :: !Float
@@ -639,28 +593,33 @@
 
 instance DeepSeq.NFData Frame
 instance Aeson.ToJSON Frame where
-    toJSON =
-        Aeson.genericToJSON
-            Aeson.defaultOptions
-            { Aeson.fieldLabelModifier = drop 5
-            }
+    toJSON = Aeson.genericToJSON (toJsonOptions "Frame")
 
+data ReplicationState
+    = RSOpening
+    | RSExisting
+    | RSClosing
+    deriving (Eq, Generics.Generic, Show)
+
+instance DeepSeq.NFData ReplicationState
+instance Aeson.ToJSON ReplicationState where
+    toJSON rs = Aeson.toJSON (case rs of
+        RSOpening -> "opening"
+        RSExisting -> "existing"
+        RSClosing -> "closing")
+
 -- | Replication information about an actor in the net stream.
 data Replication = Replication
-    { replicationActorId :: !Int
-    , replicationIsOpen :: !Bool
-    , replicationIsNew :: !(Maybe Bool)
-    , replicationClassInit :: !(Maybe ClassInit)
-    , replicationProps :: ![Prop]
+    { replicationObjectName :: !Text.Text
+    , replicationClassName :: !Text.Text
+    , replicationState :: !ReplicationState
+    , replicationInitialization :: !(Maybe ClassInit)
+    , replicationProperties :: !(Map.Map Text.Text PropValue)
     } deriving (Eq,Generics.Generic,Show)
 
 instance DeepSeq.NFData Replication
 instance Aeson.ToJSON Replication where
-    toJSON =
-        Aeson.genericToJSON
-            Aeson.defaultOptions
-            { Aeson.fieldLabelModifier = drop 11
-            }
+    toJSON = Aeson.genericToJSON (toJsonOptions "Replication")
 
 data Thing = Thing
     { thingFlag :: !Bool
@@ -698,11 +657,7 @@
 
 instance DeepSeq.NFData ClassInit
 instance Aeson.ToJSON ClassInit where
-    toJSON =
-        Aeson.genericToJSON
-            Aeson.defaultOptions
-            { Aeson.fieldLabelModifier = drop 9
-            }
+    toJSON = Aeson.genericToJSON (toJsonOptions "ClassInit")
 
 -- { class stream id => { property stream id => name } }
 type ClassPropertyMap = IntMap.IntMap (IntMap.IntMap Text.Text)
@@ -809,13 +764,13 @@
 getClassInit :: Text.Text -> Bits.BitGet ClassInit
 getClassInit className = do
     location <-
-        if Set.member className CPM.classesWithLocation
+        if Set.member className Data.locationClasses
             then do
                 vector <- getVector
                 return (Just vector)
             else return Nothing
     rotation <-
-        if Set.member className CPM.classesWithRotation
+        if Set.member className Data.rotationClasses
             then do
                 vector <- getVectorBytewise
                 return (Just vector)
diff --git a/library/Octane/Parser/ClassPropertyMap.hs b/library/Octane/Parser/ClassPropertyMap.hs
--- a/library/Octane/Parser/ClassPropertyMap.hs
+++ b/library/Octane/Parser/ClassPropertyMap.hs
@@ -1,20 +1,20 @@
+-- | This module is responsible for building the class property map, which maps
+-- class IDs to a map of property IDs to property names. This map is the
+-- cornerstone of the replay stream parser.
 module Octane.Parser.ClassPropertyMap where
 
 import Data.Function ((&))
-import Debug.Trace
 
-import qualified Control.Newtype as Newtype
 import qualified Data.Char as Char
 import qualified Data.IntMap.Strict as IntMap
 import qualified Data.List as List
 import qualified Data.Map.Strict as Map
 import qualified Data.Maybe as Maybe
-import qualified Data.Set as Set
 import qualified Data.Text as Text
 import qualified Octane.Type as Type
 
 -- | The class property map is a map from class IDs in the stream to a map from
--- | property IDs in the stream to property names.
+-- property IDs in the stream to property names.
 getClassPropertyMap :: Type.Replay -> IntMap.IntMap (IntMap.IntMap Text.Text)
 getClassPropertyMap replay = let
     basicClassPropertyMap = getBasicClassPropertyMap replay
@@ -38,15 +38,15 @@
         & IntMap.fromList
 
 -- | The class cache is a list of 3-tuples where the first element is a class
--- | ID, the second is its cache ID, and the third is its parent's cache ID.
+-- ID, the second is its cache ID, and the third is its parent's cache ID.
 getClassCache :: Type.Replay -> [(Int, Int, Int)]
 getClassCache replay = replay
     & Type.replayCacheItems
-    & Newtype.unpack
+    & Type.unpackList
     & map (\ x ->
-        ( x & Type.cacheItemClassId & Newtype.unpack & fromIntegral
-        , x & Type.cacheItemCacheId & Newtype.unpack & fromIntegral
-        , x & Type.cacheItemParentCacheId & Newtype.unpack & fromIntegral
+        ( x & Type.cacheItemClassId & Type.unpackWord32LE & fromIntegral
+        , x & Type.cacheItemCacheId & Type.unpackWord32LE & fromIntegral
+        , x & Type.cacheItemParentCacheId & Type.unpackWord32LE & fromIntegral
         ))
 
 -- | The class IDs in a replay. Comes from the class cache.
@@ -56,8 +56,8 @@
     & map (\ (x, _, _) -> x)
 
 -- | Gets the parent class ID for the given parent cache ID. This is necessary
--- | because there is not always a class with the given cache ID in the cache.
--- | When that happens, the parent cache ID is decremented and tried again.
+-- because there is not always a class with the given cache ID in the cache.
+-- When that happens, the parent cache ID is decremented and tried again.
 getParentClassId :: Int -> [(Int, Int, Int)] -> Maybe Int
 getParentClassId parentCacheId xs =
     case dropWhile (\ (_, cacheId, _) -> cacheId /= parentCacheId) xs of
@@ -67,8 +67,8 @@
         (parentClassId, _, _) : _ -> Just parentClassId
 
 -- | The basic class map is a naive mapping from class ID to its parent class
--- | ID. It's naive because it only maps the class ID to its immediate parent.
--- | It does not chase the inheritance all the way down.
+-- ID. It's naive because it only maps the class ID to its immediate parent.
+-- It does not chase the inheritance all the way down.
 getBasicClassMap :: Type.Replay -> IntMap.IntMap Int
 getBasicClassMap replay = replay
     & getClassCache
@@ -82,7 +82,7 @@
     & IntMap.fromList
 
 -- | Given a naive mapping from class ID to its parent class ID, return all of
--- | the parent IDs for a given class.
+-- the parent IDs for a given class.
 getParentClassIds :: Int -> IntMap.IntMap Int -> [Int]
 getParentClassIds classId basicClassMap =
     case IntMap.lookup classId basicClassMap of
@@ -105,28 +105,28 @@
 getPropertyMap :: Type.Replay -> IntMap.IntMap Text.Text
 getPropertyMap replay = replay
     & Type.replayObjects
-    & Newtype.unpack
-    & map Newtype.unpack
+    & Type.unpackList
+    & map Type.unpackPCString
     & zip [0 ..]
     & IntMap.fromList
 
 -- | The basic class property map is a naive mapping from class IDs to a
--- | mapping from property IDs to property names. It's naive because it does
--- | not include the properties from the class's parents.
+-- mapping from property IDs to property names. It's naive because it does
+-- not include the properties from the class's parents.
 getBasicClassPropertyMap :: Type.Replay -> IntMap.IntMap (IntMap.IntMap Text.Text)
 getBasicClassPropertyMap replay = let
     propertyMap = getPropertyMap replay
     in replay
         & Type.replayCacheItems
-        & Newtype.unpack
+        & Type.unpackList
         & map (\ x -> let
-            classId = x & Type.cacheItemClassId & Newtype.unpack & fromIntegral
+            classId = x & Type.cacheItemClassId & Type.unpackWord32LE & fromIntegral
             properties = x
                 & Type.cacheItemCacheProperties
-                & Newtype.unpack
+                & Type.unpackList
                 & Maybe.mapMaybe (\ y -> let
-                    streamId = y & Type.cachePropertyStreamId & Newtype.unpack & fromIntegral
-                    propertyId = y & Type.cachePropertyObjectId & Newtype.unpack & fromIntegral
+                    streamId = y & Type.cachePropertyStreamId & Type.unpackWord32LE & fromIntegral
+                    propertyId = y & Type.cachePropertyObjectId & Type.unpackWord32LE & fromIntegral
                     in case IntMap.lookup propertyId propertyMap of
                         Nothing -> Nothing
                         Just name -> Just (streamId, name))
@@ -138,10 +138,10 @@
 getActorMap :: Type.Replay -> Map.Map Text.Text Int
 getActorMap replay = replay
     & Type.replayActors
-    & Newtype.unpack
+    & Type.unpackList
     & map (\ x -> let
-        className = x & Type.actorName & Newtype.unpack
-        classId = x & Type.actorStreamId & Newtype.unpack & fromIntegral
+        className = x & Type.actorName & Type.unpackPCString
+        classId = x & Type.actorStreamId & Type.unpackWord32LE & fromIntegral
         in (className, classId))
     & Map.fromList
 
@@ -154,155 +154,14 @@
     -> Maybe (Int, Text.Text) -- ^ Maybe class ID and class name
 getClass propertyIdsToNames propertyNamesToClassNames classNamesToIds propertyId =
     case IntMap.lookup propertyId propertyIdsToNames of
-        Nothing -> trace ("could not find property name for property id " ++ show propertyId) Nothing
+        Nothing -> Nothing
         Just rawPropertyName -> let
             -- There are a large number of properties that end in numbers that
             -- should all be treated the same. Instead of explicitly mapping
             -- each of them, we can remove the numbers and treat them the same.
             propertyName = rawPropertyName & Text.dropWhileEnd Char.isDigit
             in case Map.lookup propertyName propertyNamesToClassNames of
-                Nothing -> trace ("could not find class name for property name " ++ show propertyName) $ trace (classNamesToIds & Map.toList & map (\ (k, v) -> " " ++ show v ++ " => " ++ show k) & unlines) $ Nothing
+                Nothing -> Nothing
                 Just className -> case Map.lookup className classNamesToIds of
-                    Nothing -> trace ("could not find class id for class name " ++ show className) Nothing
+                    Nothing -> Nothing
                     Just classId -> Just (classId, className)
-
--- | The archetype maps is a mapping from object names to their class names.
-archetypeMap :: Map.Map Text.Text Text.Text
-archetypeMap =
-    -- We start by mapping class names to object names of that class. This
-    -- allows us to avoid repeating the class name a bunch.
-    [ ( "TAGame.Ball_TA"
-      , [ "Archetypes.Ball.Ball_Default"
-        , "Archetypes.Ball.Ball_Basketball"
-        , "Archetypes.Ball.Ball_Puck"
-        ])
-    , ( "TAGame.CameraSettingsActor_TA"
-      , [ "TAGame.CameraSettingsActor_TA:PRI"
-        , "TAGame.Default__CameraSettingsActor_TA"
-        ])
-    , ( "TAGame.CarComponent_Boost_TA"
-      , [ "Archetypes.CarComponents.CarComponent_Boost"
-        ])
-    , ( "TAGame.CarComponent_Dodge_TA"
-      , [ "Archetypes.CarComponents.CarComponent_Dodge"
-        ])
-    , ( "TAGame.CarComponent_DoubleJump_TA"
-      , [ "Archetypes.CarComponents.CarComponent_DoubleJump"
-        ])
-    , ( "TAGame.CarComponent_FlipCar_TA"
-      , [ "Archetypes.CarComponents.CarComponent_FlipCar"
-        ])
-    , ( "TAGame.CarComponent_Jump_TA"
-      , [ "Archetypes.CarComponents.CarComponent_Jump"
-        ])
-    , ( "TAGame.Car_Season_TA"
-      , [ "Archetypes.GameEvent.GameEvent_Season:CarArchetype"
-        ])
-    , ( "TAGame.Car_TA"
-      , [ "Archetypes.Car.Car_Default"
-        ])
-    , ( "TAGame.GameEvent_Season_TA"
-      , [ "Archetypes.GameEvent.GameEvent_Season"
-        ])
-    , ( "TAGame.GameEvent_SoccarPrivate_TA"
-      , [ "Archetypes.GameEvent.GameEvent_SoccarPrivate"
-        ])
-    , ( "TAGame.GameEvent_SoccarSplitscreen_TA"
-      , [ "Archetypes.GameEvent.GameEvent_SoccarSplitscreen"
-        ])
-    , ( "TAGame.GameEvent_Soccar_TA"
-      , [ "Archetypes.GameEvent.GameEvent_Soccar"
-        , "Archetypes.GameEvent.GameEvent_Basketball"
-        ])
-    , ( "TAGame.GRI_TA"
-      , [ "GameInfo_Basketball.GameInfo.GameInfo_Basketball:GameReplicationInfoArchetype"
-        , "GameInfo_Season.GameInfo.GameInfo_Season:GameReplicationInfoArchetype"
-        , "GameInfo_Soccar.GameInfo.GameInfo_Soccar:GameReplicationInfoArchetype"
-        ])
-    , ( "TAGame.PRI_TA"
-      , [ "TAGame.Default__PRI_TA"
-        ])
-    , ( "TAGame.Team_TA"
-      , [ "Archetypes.Teams.Team"
-        ])
-    -- These ones are special. They have specific names for each level.
-    , ( "TAGame.VehiclePickup_Boost_TA"
-      , levels & Set.toList & map (\ level -> level ++ ".TheWorld:PersistentLevel.VehiclePickup_Boost_TA_")
-      )
-    , ( "TAGame.CrowdActor_TA"
-      , levels & Set.toList & map (\ level -> level ++ ".TheWorld:PersistentLevel.CrowdActor_TA_")
-      )
-    , ( "TAGame.CrowdManager_TA"
-      , levels & Set.toList & map (\ level -> level ++ ".TheWorld:PersistentLevel.CrowdManager_TA_")
-      )
-    ]
-        & concatMap (\ (v, ks) -> ks & map (\ k -> (k, v)))
-        & map (\ (k, v) -> (Text.pack k, Text.pack v))
-        & Map.fromList
-
--- | These are the levels that we know about. The capitalization of these
--- | drives me nuts.
-levels :: Set.Set String
-levels =
-    [ "EuroStadium_Rainy_P"
-    , "HoopsStadium_P"
-    , "Park_Night_P"
-    , "Park_Rainy_P"
-    , "Stadium_p"
-    , "TrainStation_Night_P"
-    , "TrainStation_P"
-    , "Trainstation_Night_P"
-    , "UtopiaStadium_Dusk_P"
-    , "UtopiaStadium_Dusk_p"
-    , "UtopiaStadium_P"
-    , "Utopiastadium_p"
-    , "Wasteland_P"
-    , "Wasteland_p"
-    , "eurostad_oob_audio_map"
-    , "eurostadium_p"
-    , "eurostadium_rainy_audio"
-    , "hoopsstadium_sfx"
-    , "labs_doublegoal_p"
-    , "labs_underpass_p"
-    , "labs_utopia_p"
-    , "park_night_sfx"
-    , "park_p"
-    , "park_rainy_sfx"
-    , "park_sfx"
-    , "stadium_oob_audio_map"
-    , "stadium_p"
-    , "stadium_winter_p"
-    , "trainstation_p"
-    , "utopiastadium_p"
-    , "utopiastadium_sfx"
-    , "wasteland_sfx"
-    ] & Set.fromList
-
--- | These classes have an initial location vector.
-classesWithLocation :: Set.Set Text.Text
-classesWithLocation =
-    [ "TAGame.Ball_TA"
-    , "TAGame.CameraSettingsActor_TA"
-    , "TAGame.CarComponent_Boost_TA"
-    , "TAGame.CarComponent_Dodge_TA"
-    , "TAGame.CarComponent_DoubleJump_TA"
-    , "TAGame.CarComponent_FlipCar_TA"
-    , "TAGame.CarComponent_Jump_TA"
-    , "TAGame.Car_Season_TA"
-    , "TAGame.Car_TA"
-    , "TAGame.GRI_TA"
-    , "TAGame.GameEvent_Season_TA"
-    , "TAGame.GameEvent_SoccarPrivate_TA"
-    , "TAGame.GameEvent_SoccarSplitscreen_TA"
-    , "TAGame.GameEvent_Soccar_TA"
-    , "TAGame.PRI_TA"
-    , "TAGame.Team_TA"
-    ] & map Text.pack & Set.fromList
-
--- | These classes have an initial rotation vector.
-classesWithRotation :: Set.Set Text.Text
-classesWithRotation =
-    [ "TAGame.Ball_TA"
-    , "TAGame.Car_Season_TA"
-    , "TAGame.Car_TA"
-    ] & map Text.pack & Set.fromList
diff --git a/library/Octane/Type/Actor.hs b/library/Octane/Type/Actor.hs
--- a/library/Octane/Type/Actor.hs
+++ b/library/Octane/Type/Actor.hs
@@ -10,8 +10,8 @@
 import qualified Octane.Type.Primitive.PCString as PCString
 import qualified Octane.Type.Primitive.Word32LE as Word32LE
 
--- | A class (like "Core.Object") and it's associated ID in the net stream
--- | (like 0).
+-- | A class (like @Core.Object@) and it's associated ID in the net stream
+-- (like 0).
 data Actor = Actor
     { actorName :: !PCString.PCString
     , actorStreamId :: !Word32LE.Word32LE
diff --git a/library/Octane/Type/KeyFrame.hs b/library/Octane/Type/KeyFrame.hs
--- a/library/Octane/Type/KeyFrame.hs
+++ b/library/Octane/Type/KeyFrame.hs
@@ -11,8 +11,8 @@
 import qualified Octane.Type.Primitive.Word32LE as Word32LE
 
 -- | A key frame. Each key frame has the time since the beginning of the match,
--- | the frame it corresponds to, and that frame's bit position in the network
--- | stream.
+-- the frame it corresponds to, and that frame's bit position in the network
+-- stream.
 data KeyFrame = KeyFrame
     { keyFrameTime :: !Float32LE.Float32LE
     , keyFrameFrame :: !Word32LE.Word32LE
diff --git a/library/Octane/Type/Primitive/Boolean.hs b/library/Octane/Type/Primitive/Boolean.hs
--- a/library/Octane/Type/Primitive/Boolean.hs
+++ b/library/Octane/Type/Primitive/Boolean.hs
@@ -3,27 +3,24 @@
 module Octane.Type.Primitive.Boolean (Boolean(..)) where
 
 import qualified Control.DeepSeq as DeepSeq
-import qualified Control.Newtype as Newtype
 import qualified Data.Aeson as Aeson
 import qualified Data.Binary as Binary
 import Data.Function ((&))
 import qualified GHC.Generics as Generics
 
 -- | A boolean value, stored in the first bit of a byte.
-newtype Boolean =
-    Boolean Bool
-    deriving (Eq,Generics.Generic,Show)
+newtype Boolean = Boolean
+    { unpackBoolean :: Bool
+    } deriving (Eq,Generics.Generic,Show)
 
 instance Binary.Binary Boolean where
     get = do
         boolean <- Binary.getWord8
         if boolean > 1
             then fail ("invalid Boolean value " ++ show boolean)
-            else boolean & fromIntegral & toEnum & Newtype.pack & return
+            else boolean & fromIntegral & toEnum & Boolean & return
     put boolean = do
-        boolean & Newtype.unpack & fromEnum & fromIntegral & Binary.putWord8
-
-instance Newtype.Newtype Boolean
+        boolean & unpackBoolean & fromEnum & fromIntegral & Binary.putWord8
 
 instance DeepSeq.NFData Boolean
 
diff --git a/library/Octane/Type/Primitive/Dictionary.hs b/library/Octane/Type/Primitive/Dictionary.hs
--- a/library/Octane/Type/Primitive/Dictionary.hs
+++ b/library/Octane/Type/Primitive/Dictionary.hs
@@ -4,7 +4,6 @@
 module Octane.Type.Primitive.Dictionary (Dictionary(..)) where
 
 import qualified Control.DeepSeq as DeepSeq
-import qualified Control.Newtype as Newtype
 import qualified Data.Aeson as Aeson
 import qualified Data.Binary as Binary
 import Data.Function ((&))
@@ -13,31 +12,29 @@
 import qualified Octane.Type.Primitive.PCString as PCString
 
 -- | A dictionary that maps strings to values. The dictionary is terminated by
--- | the key "None".
-newtype Dictionary a =
-    Dictionary (Map.Map PCString.PCString a)
-    deriving (Eq,Generics.Generic,Show)
+-- the key "None".
+newtype Dictionary a = Dictionary
+    { unpackDictionary :: (Map.Map PCString.PCString a)
+    } deriving (Eq,Generics.Generic,Show)
 
 instance (Binary.Binary a) => Binary.Binary (Dictionary a) where
     get = do
         element <- getElement
         if Map.null element
             then do
-                element & Newtype.pack & return
+                element & Dictionary & return
             else do
                 Dictionary elements <- Binary.get
-                elements & Map.union element & Newtype.pack & return
+                elements & Map.union element & Dictionary & return
     put dictionary = do
-        dictionary & Newtype.unpack & Map.assocs & mapM_ putElement
+        dictionary & unpackDictionary & Map.assocs & mapM_ putElement
         noneKey & Binary.put
 
-instance Newtype.Newtype (Dictionary a)
-
 instance (DeepSeq.NFData a) => DeepSeq.NFData (Dictionary a)
 
 instance (Aeson.ToJSON a) => Aeson.ToJSON (Dictionary a) where
     toJSON dictionary =
-        dictionary & Newtype.unpack & Map.mapKeys Newtype.unpack & Aeson.toJSON
+        dictionary & unpackDictionary & Map.mapKeys PCString.unpackPCString & Aeson.toJSON
 
 getElement
     :: (Binary.Binary a)
diff --git a/library/Octane/Type/Primitive/Float32LE.hs b/library/Octane/Type/Primitive/Float32LE.hs
--- a/library/Octane/Type/Primitive/Float32LE.hs
+++ b/library/Octane/Type/Primitive/Float32LE.hs
@@ -3,7 +3,6 @@
 module Octane.Type.Primitive.Float32LE (Float32LE(..)) where
 
 import qualified Control.DeepSeq as DeepSeq
-import qualified Control.Newtype as Newtype
 import qualified Data.Aeson as Aeson
 import qualified Data.Binary as Binary
 import qualified Data.Binary.IEEE754 as IEEE754
@@ -11,18 +10,16 @@
 import qualified GHC.Generics as Generics
 
 -- | A 32-bit little-endian float.
-newtype Float32LE =
-    Float32LE Float
-    deriving (Eq,Generics.Generic,Show)
+newtype Float32LE = Float32LE
+    { unpackFloat32LE :: Float
+    } deriving (Eq,Generics.Generic,Show)
 
 instance Binary.Binary Float32LE where
     get = do
         float <- IEEE754.getFloat32le
-        float & Newtype.pack & return
+        float & Float32LE & return
     put float = do
-        float & Newtype.unpack & IEEE754.putFloat32le
-
-instance Newtype.Newtype Float32LE
+        float & unpackFloat32LE & IEEE754.putFloat32le
 
 instance DeepSeq.NFData Float32LE
 
diff --git a/library/Octane/Type/Primitive/List.hs b/library/Octane/Type/Primitive/List.hs
--- a/library/Octane/Type/Primitive/List.hs
+++ b/library/Octane/Type/Primitive/List.hs
@@ -4,7 +4,6 @@
 
 import qualified Control.DeepSeq as DeepSeq
 import qualified Control.Monad as Monad
-import qualified Control.Newtype as Newtype
 import qualified Data.Aeson as Aeson
 import qualified Data.Binary as Binary
 import Data.Function ((&))
@@ -12,21 +11,19 @@
 import qualified Octane.Type.Primitive.Word32LE as Word32LE
 
 -- | A length-prefixed list.
-newtype List a =
-    List [a]
-    deriving (Eq,Generics.Generic,Show)
+newtype List a = List
+    { unpackList :: [a]
+    } deriving (Eq,Generics.Generic,Show)
 
 instance (Binary.Binary a) => Binary.Binary (List a) where
     get = do
         (Word32LE.Word32LE size) <- Binary.get
         elements <- Monad.replicateM (fromIntegral size) Binary.get
-        elements & Newtype.pack & return
+        elements & List & return
     put list = do
-        list & Newtype.unpack & length & fromIntegral & Word32LE.Word32LE &
+        list & unpackList & length & fromIntegral & Word32LE.Word32LE &
             Binary.put
-        list & Newtype.unpack & mapM_ Binary.put
-
-instance Newtype.Newtype (List a)
+        list & unpackList & mapM_ Binary.put
 
 instance (DeepSeq.NFData a) => DeepSeq.NFData (List a)
 
diff --git a/library/Octane/Type/Primitive/PCString.hs b/library/Octane/Type/Primitive/PCString.hs
--- a/library/Octane/Type/Primitive/PCString.hs
+++ b/library/Octane/Type/Primitive/PCString.hs
@@ -3,7 +3,6 @@
 module Octane.Type.Primitive.PCString (PCString(..)) where
 
 import qualified Control.DeepSeq as DeepSeq
-import qualified Control.Newtype as Newtype
 import qualified Data.Aeson as Aeson
 import qualified Data.Binary as Binary
 import qualified Data.Binary.Get as Binary
@@ -19,9 +18,9 @@
 import qualified Octane.Type.Primitive.Word32LE as Word32LE
 
 -- | A length-prefixed null-terminated string.
-newtype PCString =
-    PCString Text.Text
-    deriving (Eq,Generics.Generic,Ord,Show)
+newtype PCString = PCString
+    { unpackPCString :: Text.Text
+    } deriving (Eq,Generics.Generic,Ord,Show)
 
 instance Binary.Binary PCString where
     get = do
@@ -57,9 +56,9 @@
                          else do
                              bytes <- Binary.getByteString (fromIntegral size)
                              bytes & Encoding.decodeLatin1 & return
-        string & Text.dropEnd 1 & Newtype.pack & return
+        string & Text.dropEnd 1 & PCString & return
     put string = do
-        let cString = string & Newtype.unpack & flip Text.snoc '\NUL'
+        let cString = string & unpackPCString & flip Text.snoc '\NUL'
         let size = cString & Text.length & fromIntegral
         if Text.all Char.isLatin1 cString
             then do
@@ -70,9 +69,7 @@
                 cString & Encoding.encodeUtf16LE & Binary.putByteString
 
 instance String.IsString PCString where
-    fromString string = string & Text.pack & Newtype.pack
-
-instance Newtype.Newtype PCString
+    fromString string = string & Text.pack & PCString
 
 instance DeepSeq.NFData PCString
 
diff --git a/library/Octane/Type/Primitive/Stream.hs b/library/Octane/Type/Primitive/Stream.hs
--- a/library/Octane/Type/Primitive/Stream.hs
+++ b/library/Octane/Type/Primitive/Stream.hs
@@ -3,7 +3,6 @@
 module Octane.Type.Primitive.Stream (Stream(..), reverseBits) where
 
 import qualified Control.DeepSeq as DeepSeq
-import qualified Control.Newtype as Newtype
 import qualified Data.Aeson as Aeson
 import qualified Data.Binary as Binary
 import qualified Data.Binary.Get as Binary
@@ -16,28 +15,26 @@
 import qualified Octane.Type.Primitive.Word32LE as Word32LE
 
 -- | A length-prefixed stream of bits. The length is given in bytes. Each byte
--- | is reversed such that 0b01234567 is actually 0b76543210.
-newtype Stream =
-    Stream BS.ByteString
-    deriving (Eq,Generics.Generic,Show)
+-- is reversed such that 0b01234567 is actually 0b76543210.
+newtype Stream = Stream
+    { unpackStream :: BS.ByteString
+    } deriving (Eq,Generics.Generic,Show)
 
 instance Binary.Binary Stream where
     get = do
         Word32LE.Word32LE size <- Binary.get
         content <- size & fromIntegral & Binary.getByteString
-        content & BS.map reverseBits & Newtype.pack & return
+        content & BS.map reverseBits & Stream & return
     put stream = do
-        let content = Newtype.unpack stream
+        let content = unpackStream stream
         content & BS.length & fromIntegral & Word32LE.Word32LE & Binary.put
         content & BS.map reverseBits & Binary.putByteString
 
-instance Newtype.Newtype Stream
-
 instance DeepSeq.NFData Stream
 
 instance Aeson.ToJSON Stream where
     toJSON stream =
-        let size = stream & Newtype.unpack & BS.length
+        let size = stream & unpackStream & BS.length
             bytes =
                 if size == 1
                     then "byte"
diff --git a/library/Octane/Type/Primitive/Word32LE.hs b/library/Octane/Type/Primitive/Word32LE.hs
--- a/library/Octane/Type/Primitive/Word32LE.hs
+++ b/library/Octane/Type/Primitive/Word32LE.hs
@@ -3,7 +3,6 @@
 module Octane.Type.Primitive.Word32LE (Word32LE(..)) where
 
 import qualified Control.DeepSeq as DeepSeq
-import qualified Control.Newtype as Newtype
 import qualified Data.Aeson as Aeson
 import qualified Data.Binary as Binary
 import qualified Data.Binary.Get as Binary
@@ -13,18 +12,16 @@
 import qualified GHC.Generics as Generics
 
 -- | A 32-bit little-endian integer.
-newtype Word32LE =
-    Word32LE Int.Int32
-    deriving (Eq,Generics.Generic,Show)
+newtype Word32LE = Word32LE
+    { unpackWord32LE :: Int.Int32
+    } deriving (Eq,Generics.Generic,Show)
 
 instance Binary.Binary Word32LE where
     get = do
         word <- Binary.getWord32le
-        word & fromIntegral & Newtype.pack & return
+        word & fromIntegral & Word32LE & return
     put word = do
-        word & Newtype.unpack & fromIntegral & Binary.putWord32le
-
-instance Newtype.Newtype Word32LE
+        word & unpackWord32LE & fromIntegral & Binary.putWord32le
 
 instance DeepSeq.NFData Word32LE
 
diff --git a/library/Octane/Type/Primitive/Word64LE.hs b/library/Octane/Type/Primitive/Word64LE.hs
--- a/library/Octane/Type/Primitive/Word64LE.hs
+++ b/library/Octane/Type/Primitive/Word64LE.hs
@@ -3,7 +3,6 @@
 module Octane.Type.Primitive.Word64LE (Word64LE(..)) where
 
 import qualified Control.DeepSeq as DeepSeq
-import qualified Control.Newtype as Newtype
 import qualified Data.Aeson as Aeson
 import qualified Data.Binary as Binary
 import qualified Data.Binary.Get as Binary
@@ -13,18 +12,16 @@
 import qualified GHC.Generics as Generics
 
 -- | A 64-bit little-endian integer.
-newtype Word64LE =
-    Word64LE Int.Int64
-    deriving (Eq,Generics.Generic,Show)
+newtype Word64LE = Word64LE
+    { unpackWord64LE :: Int.Int64
+    } deriving (Eq,Generics.Generic,Show)
 
 instance Binary.Binary Word64LE where
     get = do
         word <- Binary.getWord64le
-        word & fromIntegral & Newtype.pack & return
+        word & fromIntegral & Word64LE & return
     put word = do
-        word & Newtype.unpack & fromIntegral & Binary.putWord64le
-
-instance Newtype.Newtype Word64LE
+        word & unpackWord64LE & fromIntegral & Binary.putWord64le
 
 instance DeepSeq.NFData Word64LE
 
diff --git a/library/Octane/Type/Property.hs b/library/Octane/Type/Property.hs
--- a/library/Octane/Type/Property.hs
+++ b/library/Octane/Type/Property.hs
@@ -4,7 +4,6 @@
 module Octane.Type.Property (Property(..)) where
 
 import qualified Control.DeepSeq as DeepSeq
-import qualified Control.Newtype as Newtype
 import qualified Data.Aeson.Types as Aeson
 import qualified Data.Binary as Binary
 import Data.Function ((&))
@@ -18,8 +17,8 @@
 import qualified Octane.Type.Primitive.Word64LE as Word64LE
 
 -- | A metadata property. All properties have a size, but only some actually
--- | use it. The value stored in the property can be an array, a boolean, and
--- | so on.
+-- use it. The value stored in the property can be an array, a boolean, and
+-- so on.
 data Property
     = ArrayProperty !Word64LE.Word64LE
                     !(List.List (Dictionary.Dictionary Property))
@@ -62,14 +61,14 @@
             _ | kind == floatProperty -> do
                 size <- Binary.get
                 value <-
-                    case Newtype.unpack size of
+                    case Word64LE.unpackWord64LE size of
                         4 -> Binary.get
                         x -> fail ("unknown FloatProperty size " ++ show x)
                 value & FloatProperty size & return
             _ | kind == intProperty -> do
                 size <- Binary.get
                 value <-
-                    case Newtype.unpack size of
+                    case Word64LE.unpackWord64LE size of
                         4 -> Binary.get
                         x -> fail ("unknown IntProperty size " ++ show x)
                 value & IntProperty size & return
@@ -80,7 +79,7 @@
             _ | kind == qWordProperty -> do
                 size <- Binary.get
                 value <-
-                    case Newtype.unpack size of
+                    case Word64LE.unpackWord64LE size of
                         8 -> Binary.get
                         x -> fail ("unknown QWordProperty size " ++ show x)
                 value & QWordProperty size & return
@@ -88,7 +87,7 @@
                 size <- Binary.get
                 value <- Binary.get
                 value & StrProperty size & return
-            _ -> fail ("unknown property type " ++ show (Newtype.unpack kind))
+            _ -> fail ("unknown property type " ++ show (PCString.unpackPCString kind))
     put property =
         case property of
             ArrayProperty size value -> do
diff --git a/library/Octane/Type/Replay.hs b/library/Octane/Type/Replay.hs
--- a/library/Octane/Type/Replay.hs
+++ b/library/Octane/Type/Replay.hs
@@ -20,7 +20,7 @@
 import qualified Octane.Type.Property as Property
 
 -- | An entire replay. All of the metadata has been parsed, but the actual net
--- | stream has not.
+-- stream has not.
 data Replay = Replay
     -- Number of bytes in the first section.
     { replaySize1 :: !Word32LE.Word32LE
diff --git a/octane.cabal b/octane.cabal
--- a/octane.cabal
+++ b/octane.cabal
@@ -3,7 +3,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           octane
-version:        0.5.2
+version:        0.5.3
 synopsis:       Parse Rocket League replays.
 description:    Octane parses Rocket League replays.
 category:       Game
@@ -28,7 +28,7 @@
 library
   hs-source-dirs:
       library
-  ghc-options: -Wall
+  ghc-options: -O2 -Wall
   build-depends:
       aeson >=0.9 && <0.12
     , autoexporter ==0.2.*
@@ -39,10 +39,10 @@
     , containers ==0.5.*
     , data-binary-ieee754 ==0.4.*
     , deepseq ==1.4.*
-    , newtype-generics ==0.4.*
     , text ==1.2.*
   exposed-modules:
       Octane
+      Octane.Data
       Octane.Main
       Octane.Parser
       Octane.Parser.ClassPropertyMap
@@ -73,7 +73,7 @@
   main-is: Executable.hs
   hs-source-dirs:
       executable
-  ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
+  ghc-options: -O2 -Wall -rtsopts -threaded -with-rtsopts=-N
   build-depends:
       base
     , octane
@@ -84,7 +84,7 @@
   main-is: TestSuite.hs
   hs-source-dirs:
       test-suite
-  ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
+  ghc-options: -O2 -Wall -rtsopts -threaded -with-rtsopts=-N
   build-depends:
       base
     , binary
@@ -122,7 +122,7 @@
   main-is: Benchmark.hs
   hs-source-dirs:
       benchmark
-  ghc-options: -Wall -rtsopts -threaded -with-rtsopts=-N
+  ghc-options: -O2 -Wall -rtsopts -threaded -with-rtsopts=-N
   build-depends:
       base
     , binary
diff --git a/package.yaml b/package.yaml
--- a/package.yaml
+++ b/package.yaml
@@ -31,7 +31,9 @@
 - package.yaml
 - README.markdown
 - stack.yaml
-ghc-options: -Wall
+ghc-options:
+- -O2
+- -Wall
 github: tfausak/octane
 library:
   dependencies:
@@ -44,7 +46,6 @@
   - containers ==0.5.*
   - data-binary-ieee754 ==0.4.*
   - deepseq ==1.4.*
-  - newtype-generics ==0.4.*
   - text ==1.2.*
   other-modules: Paths_octane
   source-dirs: library
@@ -69,4 +70,4 @@
     - -with-rtsopts=-N
     main: TestSuite.hs
     source-dirs: test-suite
-version: '0.5.2'
+version: '0.5.3'
diff --git a/stack.yaml b/stack.yaml
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,1 +1,1 @@
-resolver: nightly-2016-05-12
+resolver: nightly-2016-05-30
