diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.markdown
@@ -0,0 +1,7 @@
+# Change log
+
+Octane uses [Semantic Versioning][].
+The change log is available through the [releases on GitHub][].
+
+[Semantic Versioning]: http://semver.org/spec/v2.0.0.html
+[releases on GitHub]: https://github.com/tfausak/octane/releases
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Change log
-
-Octane uses [Semantic Versioning][].
-The change log is available through the [releases on GitHub][].
-
-[Semantic Versioning]: http://semver.org/spec/v2.0.0.html
-[releases on GitHub]: https://github.com/tfausak/octane/releases
diff --git a/LICENSE.md b/LICENSE.md
deleted file mode 100644
--- a/LICENSE.md
+++ /dev/null
@@ -1,23 +0,0 @@
-[The MIT License (MIT)][]
-
-Copyright (c) 2016 Taylor Fausak <taylor@fausak.me>
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-[The MIT License (MIT)]: http://opensource.org/licenses/MIT
diff --git a/README.markdown b/README.markdown
new file mode 100644
--- /dev/null
+++ b/README.markdown
@@ -0,0 +1,24 @@
+# [Octane][]
+
+[![Version badge][]][version]
+[![Windows build badge][]][windows build]
+[![Build badge][]][build]
+
+Octane parses [Rocket League][] replays.
+
+To use Octane, first download and unpack the [latest release][] for your
+operating system. Then run `octane path-to/the.replay`.
+
+The [Rocket League Replays wiki][] has links to other Rocket League replay
+parsers.
+
+[Octane]: https://github.com/tfausak/octane
+[Version badge]: https://www.stackage.org/package/octane/badge/nightly?label=version
+[version]: https://www.stackage.org/package/octane
+[Windows build badge]: https://ci.appveyor.com/api/projects/status/github/tfausak/octane?branch=main&svg=true
+[windows build]: https://ci.appveyor.com/project/TaylorFausak/octane
+[Build badge]: https://travis-ci.org/tfausak/octane.svg?branch=main
+[build]: https://travis-ci.org/tfausak/octane
+[Rocket League]: http://www.rocketleaguegame.com
+[latest release]: https://github.com/tfausak/octane/releases/latest
+[Rocket League Replays wiki]: https://github.com/rocket-league-replays/rocket-league-replays/wiki/Rocket-League-Replay-Parsers
diff --git a/README.md b/README.md
deleted file mode 100644
--- a/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-## The Haskell Tool Stack
-
-[![Build Status](https://travis-ci.org/commercialhaskell/stack.svg?branch=master)](https://travis-ci.org/commercialhaskell/stack)
-[![Windows build status](https://ci.appveyor.com/api/projects/status/c1c7uvmw6x1dupcl?svg=true)](https://ci.appveyor.com/project/snoyberg/stack)
-[![Release](https://img.shields.io/github/release/commercialhaskell/stack.svg)](https://github.com/commercialhaskell/stack/releases)
-
-`stack` is a cross-platform program for developing Haskell projects. It is aimed
-at Haskellers both new and experienced.
-
-See [haskellstack.org](http://haskellstack.org) or the `doc` directory for more
-information.
diff --git a/library/Octane/Parser.hs b/library/Octane/Parser.hs
--- a/library/Octane/Parser.hs
+++ b/library/Octane/Parser.hs
@@ -7,36 +7,125 @@
 import qualified Data.ByteString as BS
 import qualified Data.ByteString.Lazy as BSL
 import Data.Function ((&))
+import qualified Data.IntMap as IntMap
+import qualified Data.Maybe as Maybe
+import qualified Data.Text as Text
 import qualified Octane.Type as Type
 
 parseFrames :: Type.Replay -> [Type.Frame]
 parseFrames replay =
-    Binary.runGet
-        (replay & extractContext & getFrames & Bits.runBitGet)
-        (replay & Type.replayStream & Newtype.unpack & BSL.fromStrict)
+    let get = replay & extractContext & getFrames & Bits.runBitGet
+        stream = replay & Type.replayStream & Newtype.unpack & BSL.fromStrict
+    in Binary.runGet get stream
 
--- TODO: This will need at least the actors and cache items.
+type ObjectMap = IntMap.IntMap Text.Text
+
+buildObjectMap :: Type.Replay -> ObjectMap
+buildObjectMap replay =
+    replay & Type.replayObjects & Newtype.unpack & map Newtype.unpack &
+    zip [0 ..] &
+    IntMap.fromAscList
+
+type ClassMap = IntMap.IntMap Text.Text
+
+buildClassMap :: Type.Replay -> ClassMap
+buildClassMap replay =
+    replay & Type.replayActors & Newtype.unpack &
+    map
+        (\x ->
+              ( x & Type.actorTag & Newtype.unpack & fromIntegral
+              , x & Type.actorName & Newtype.unpack)) &
+    IntMap.fromList
+
+type Cache = IntMap.IntMap Type.CacheItem
+
+buildCache :: Type.Replay -> Cache
+buildCache replay =
+    replay & Type.replayCacheItems & Newtype.unpack &
+    map
+        (\x ->
+              (x & Type.cacheItemTag & Newtype.unpack & fromIntegral, x)) &
+    IntMap.fromList
+
+type PropertyMap = IntMap.IntMap Text.Text
+
+buildPropertyMap :: ObjectMap -> Cache -> Int -> PropertyMap
+buildPropertyMap objectMap cache key =
+    case IntMap.lookup key cache of
+        Nothing -> IntMap.empty
+        Just cacheItem ->
+            let parentId =
+                    cacheItem & Type.cacheItemStart & Newtype.unpack &
+                    fromIntegral
+                properties =
+                    cacheItem & Type.cacheItemCacheProperties & Newtype.unpack &
+                    map
+                        (\x ->
+                              ( x & Type.cachePropertyIndex & Newtype.unpack &
+                                fromIntegral
+                              , x & Type.cachePropertyTag & Newtype.unpack &
+                                fromIntegral)) &
+                    Maybe.mapMaybe
+                        (\(k,v) ->
+                              case IntMap.lookup v objectMap of
+                                  Nothing -> Nothing
+                                  Just name -> Just (k, name)) &
+                    IntMap.fromList
+            in if key == parentId
+                   then properties
+                   else IntMap.union
+                            properties
+                            (buildPropertyMap objectMap cache parentId)
+
+type ClassPropertyMap = IntMap.IntMap (IntMap.IntMap Text.Text)
+
+buildClassPropertyMap :: Type.Replay -> ClassPropertyMap
+buildClassPropertyMap replay =
+    let objectMap = buildObjectMap replay
+        classMap = buildClassMap replay
+        cache = buildCache replay
+        f k _ m =
+            case IntMap.lookup k cache of
+                Nothing -> m
+                Just cacheItem ->
+                    let x =
+                            cacheItem & Type.cacheItemTag & Newtype.unpack &
+                            fromIntegral
+                        v = buildPropertyMap objectMap cache x
+                    in IntMap.insert k v m
+    in IntMap.foldrWithKey f IntMap.empty classMap
+
+getClass :: ObjectMap -> Int -> Maybe (Int, Text.Text)
+getClass objectMap objectId =
+    case IntMap.lookup objectId objectMap of
+        Nothing -> Nothing
+        Just name ->
+            if name == Text.pack "TAGame.Default__PRI_TA" ||
+               Text.isInfixOf (Text.pack "Archetype") name
+                then getClass objectMap (objectId - 1)
+                else Just (objectId, name)
+
 data Context = Context
-    {
+    { contextObjectMap :: ObjectMap
     }
 
 extractContext :: Type.Replay -> Context
-extractContext _replay =
+extractContext replay =
     Context
-    {
+    { contextObjectMap = buildObjectMap replay
     }
 
 getFrames :: Context -> Bits.BitGet [Type.Frame]
 getFrames context = do
-    maybeFrame <- getFrame context
+    maybeFrame <- getMaybeFrame context
     case maybeFrame of
         Nothing -> return []
         Just frame -> do
             frames <- getFrames context
             return (frame : frames)
 
-getFrame :: Context -> Bits.BitGet (Maybe Type.Frame)
-getFrame context = do
+getMaybeFrame :: Context -> Bits.BitGet (Maybe Type.Frame)
+getMaybeFrame context = do
     -- TODO: Convert time bytes into a float.
     time <- Bits.getByteString 32
     -- TODO: Convert delta bytes into a float.
@@ -44,72 +133,110 @@
     if BS.all (== 0) time && BS.all (== 0) delta
         then return Nothing
         else do
-            replications <- getReplications context
-            let frame =
-                    Type.Frame
-                    { Type.frameTime = time
-                    , Type.frameDelta = delta
-                    , Type.frameReplications = replications
-                    }
+            frame <- getFrame context time delta
             return (Just frame)
 
+type Time = BS.ByteString
+
+type Delta = BS.ByteString
+
+getFrame :: Context -> Time -> Delta -> Bits.BitGet Type.Frame
+getFrame context time delta = do
+    replications <- getReplications context
+    let frame =
+            Type.Frame
+            { Type.frameTime = time
+            , Type.frameDelta = delta
+            , Type.frameReplications = replications
+            }
+    return frame
+
 getReplications :: Context -> Bits.BitGet [Type.Replication]
 getReplications context = do
-    (context',maybeReplication) <- getReplication context
+    (context',maybeReplication) <- getMaybeReplication context
     case maybeReplication of
         Nothing -> return []
         Just replication -> do
             replications <- getReplications context'
             return (replication : replications)
 
-getReplication :: Context -> Bits.BitGet (Context, Maybe Type.Replication)
-getReplication context = do
+getMaybeReplication :: Context -> Bits.BitGet (Context, Maybe Type.Replication)
+getMaybeReplication context = do
     hasReplication <- Bits.getBool
     if not hasReplication
         then return (context, Nothing)
         else do
-            -- TODO: Convert actor ID into an integer.
-            actorId <- Bits.getByteString (bitSize maxChannels)
-            isOpen <- Bits.getBool
+            (newContext,replication) <- getReplication context
+            return (newContext, Just replication)
+
+getReplication :: Context -> Bits.BitGet (Context, Type.Replication)
+getReplication context = do
+    actorId <- getInt maxChannels
+    isOpen <- Bits.getBool
+    let go =
             if isOpen
-                then do
-                    isNew <- Bits.getBool
-                    if isNew
-                        then do
-                            _unknownFlag <- Bits.getBool
-                            _objectId <- getInt 32
-                            -- TODO: Parse new actor.
-                            return
-                                ( context
-                                , Just
-                                      (Type.Replication
-                                       { Type.replicationActorId = actorId
-                                       , Type.replicationIsOpen = isOpen
-                                       , Type.replicationIsNew = Just isNew
-                                       }))
-                        else do
-                            let maybeClassId = getClassId context actorId
-                            case maybeClassId of
-                                Nothing -> fail "TODO: Could not get class ID."
-                                Just _classId ->
-                                    -- TODO: Parse existing actor.
-                                    return
-                                        ( context
-                                        , Just
-                                              (Type.Replication
-                                               { Type.replicationActorId = actorId
-                                               , Type.replicationIsOpen = isOpen
-                                               , Type.replicationIsNew = Just isNew
-                                               }))
-                else return
-                         ( context
-                         , Just
-                               (Type.Replication
-                                { Type.replicationActorId = actorId
-                                , Type.replicationIsOpen = isOpen
-                                , Type.replicationIsNew = Nothing
-                                }))
+                then getOpenReplication
+                else getClosedReplication
+    go context actorId
 
+type ActorId = Int
+
+getOpenReplication :: Context
+                   -> ActorId
+                   -> Bits.BitGet (Context, Type.Replication)
+getOpenReplication context actorId = do
+    isNew <- Bits.getBool
+    let go =
+            if isNew
+                then getNewReplication
+                else getExistingReplication
+    go context actorId
+
+getNewReplication :: Context
+                  -> ActorId
+                  -> Bits.BitGet (Context, Type.Replication)
+getNewReplication context actorId = do
+    _unknownFlag <- Bits.getBool
+    _objectId <- getInt 32
+    -- TODO: Parse new actor.
+    return
+        ( context
+        , Type.Replication
+          { Type.replicationActorId = actorId
+          , Type.replicationIsOpen = True
+          , Type.replicationIsNew = Just True
+          })
+
+getExistingReplication :: Context
+                       -> ActorId
+                       -> Bits.BitGet (Context, Type.Replication)
+getExistingReplication context actorId = do
+    let maybeClass = getClass (contextObjectMap context) actorId
+    case maybeClass of
+        Nothing ->
+            fail ("TODO: Could not get class for object " ++ show actorId)
+        Just (_classId,_className) ->
+            -- TODO: Parse existing actor.
+            return
+                ( context
+                , Type.Replication
+                  { Type.replicationActorId = actorId
+                  , Type.replicationIsOpen = True
+                  , Type.replicationIsNew = Just True
+                  })
+
+getClosedReplication :: Context
+                     -> ActorId
+                     -> Bits.BitGet (Context, Type.Replication)
+getClosedReplication context actorId = do
+    return
+        ( context
+        , Type.Replication
+          { Type.replicationActorId = actorId
+          , Type.replicationIsOpen = False
+          , Type.replicationIsNew = Nothing
+          })
+
 maxChannels
     :: (Integral a)
     => a
@@ -145,7 +272,3 @@
                     go (i + 1) newValue
                 else return value
     go 0 0
-
--- TODO: Actually implement this.
-getClassId :: Context -> BS.ByteString -> Maybe ()
-getClassId _context _actorId = Nothing
diff --git a/library/Octane/Type/Replication.hs b/library/Octane/Type/Replication.hs
--- a/library/Octane/Type/Replication.hs
+++ b/library/Octane/Type/Replication.hs
@@ -3,11 +3,10 @@
 module Octane.Type.Replication (Replication(..)) where
 
 import qualified Control.DeepSeq as DeepSeq
-import qualified Data.ByteString as BS
 import qualified GHC.Generics as Generics
 
 data Replication = Replication
-    { replicationActorId :: BS.ByteString
+    { replicationActorId :: Int
     , replicationIsOpen :: Bool
     , replicationIsNew :: Maybe Bool
     } deriving (Eq,Generics.Generic,Show)
diff --git a/library/Octane/Version.hs b/library/Octane/Version.hs
--- a/library/Octane/Version.hs
+++ b/library/Octane/Version.hs
@@ -1,3 +1,7 @@
-module Octane.Version (module Paths_octane) where
+module Octane.Version where
 
-import Paths_octane (version)
+import qualified Data.Version as Version
+import qualified Paths_octane as This
+
+version :: Version.Version
+version = This.version
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.4.18
+version:        0.4.19
 synopsis:       Parse Rocket League replays.
 description:    Octane parses Rocket League replays.
 category:       Game
@@ -16,10 +16,9 @@
 cabal-version:  >= 1.10
 
 extra-source-files:
-    CHANGELOG.md
-    LICENSE.md
+    CHANGELOG.markdown
     package.yaml
-    README.md
+    README.markdown
     stack.yaml
 
 source-repository head
@@ -33,8 +32,8 @@
   build-depends:
       aeson ==0.11.*
     , aeson-pretty ==0.7.*
-    , autoexporter >=0.1 && <0.3
-    , base >=4.8 && <4.10
+    , autoexporter ==0.2.*
+    , base ==4.*
     , binary ==0.7.*
     , binary-bits ==0.5.*
     , bytestring ==0.10.*
@@ -116,6 +115,7 @@
       Octane.Type.PropertySpec
       Octane.Type.ReplaySpec
       Octane.TypeSpec
+      Octane.VersionSpec
       OctaneSpec
   default-language: Haskell2010
 
diff --git a/package.yaml b/package.yaml
--- a/package.yaml
+++ b/package.yaml
@@ -27,10 +27,9 @@
     main: Executable.hs
     source-dirs: executable
 extra-source-files:
-- CHANGELOG.md
-- LICENSE.md
+- CHANGELOG.markdown
 - package.yaml
-- README.md
+- README.markdown
 - stack.yaml
 ghc-options: -Wall
 github: tfausak/octane
@@ -38,8 +37,8 @@
   dependencies:
   - aeson ==0.11.*
   - aeson-pretty ==0.7.*
-  - autoexporter >=0.1 && <0.3
-  - base >=4.8 && <4.10
+  - autoexporter ==0.2.*
+  - base ==4.*
   - binary ==0.7.*
   - binary-bits ==0.5.*
   - bytestring ==0.10.*
@@ -51,6 +50,7 @@
   other-modules: Paths_octane
   source-dirs: library
 license: MIT
+license-file: LICENSE.markdown
 maintainer: Taylor Fausak
 name: octane
 synopsis: Parse Rocket League replays.
@@ -70,4 +70,4 @@
     - -with-rtsopts=-N
     main: TestSuite.hs
     source-dirs: test-suite
-version: '0.4.18'
+version: '0.4.19'
diff --git a/stack.yaml b/stack.yaml
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,3 +1,1 @@
-install-ghc: true
-require-stack-version: ! '>=1.0.4'
-resolver: nightly-2016-04-14
+resolver: nightly-2016-05-02
diff --git a/test-suite/Octane/VersionSpec.hs b/test-suite/Octane/VersionSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Octane/VersionSpec.hs
@@ -0,0 +1,11 @@
+module Octane.VersionSpec (spec) where
+
+import Octane
+import Test.Tasty.Hspec
+
+import qualified Data.Version as Version
+
+spec :: Spec
+spec = describe "Version" $ do
+    it "has some parts" $ do
+        shouldBe (null (Version.versionBranch version)) False
diff --git a/test-suite/OctaneSpec.hs b/test-suite/OctaneSpec.hs
--- a/test-suite/OctaneSpec.hs
+++ b/test-suite/OctaneSpec.hs
@@ -4,8 +4,10 @@
 
 import qualified Octane.MainSpec
 import qualified Octane.TypeSpec
+import qualified Octane.VersionSpec
 
 spec :: Spec
 spec = describe "Octane" $ do
     Octane.MainSpec.spec
     Octane.TypeSpec.spec
+    Octane.VersionSpec.spec
