packages feed

octane 0.7.0 → 0.8.0

raw patch · 7 files changed

+503/−94 lines, 7 files

Files

library/Octane.hs view
@@ -1,5 +1,6 @@ module Octane     ( module Octane.Data+    , module Octane.FullReplay     , module Octane.Main     , module Octane.Parser     , module Octane.Type@@ -7,6 +8,7 @@     ) where  import Octane.Data+import Octane.FullReplay import Octane.Main import Octane.Parser import Octane.Type
library/Octane/Data.hs view
@@ -1052,17 +1052,17 @@     , ("Washington Wizards", 1275)     -- Video Games     , ("Blacklight", 385)-    , ("Blacklight: Retribution", todo)-    , ("Chivalry - Agatha Knights", todo)-    , ("Chivalry - Mason Order", todo)-    , ("Edge Of Space", todo)-    , ("Euro Truck Simulator Rig", todo)-    , ("Fallout - Vault Boy", todo)-    , ("Fenix Rage", todo)-    , ("Goat Simulator - G2", todo)-    , ("Goat Simulator - Goatenna", todo)-    , ("Oddworld - Abe", todo)-    , ("Oddworld - Molluck", todo)+    , ("Blacklight: Retribution", 211)+    , ("Chivalry - Agatha Knights", 525)+    , ("Chivalry - Mason Order", 526)+    , ("Edge Of Space", 213)+    , ("Euro Truck Simulator Rig", 813)+    , ("Fallout - Vault Boy", 673)+    , ("Fenix Rage", 216)+    , ("Goat Simulator - G2", 927)+    , ("Goat Simulator - Goatenna", 867)+    , ("Oddworld - Abe", 678)+    , ("Oddworld - Molluck", 679)     , ("Oddworld - Necrum", todo)     , ("Oddworld - RuptureFarms", todo)     , ("Portal - Aperture Laboratories", todo)
+ library/Octane/FullReplay.hs view
@@ -0,0 +1,453 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}++module Octane.FullReplay+    ( FullReplay+    , parseReplay+    , parseReplayFile+    , unsafeParseReplay+    , unsafeParseReplayFile+    ) where++import Data.Aeson ((.=))+import Data.Function ((&))+import Data.Monoid ((<>))+import Prelude ((==), (/=), (&&))++import qualified Control.DeepSeq as DeepSeq+import qualified Control.Monad as Monad+import qualified Data.Aeson as Aeson+import qualified Data.Binary as Binary+import qualified Data.ByteString.Lazy as ByteString+import qualified Data.Foldable as Foldable+import qualified Data.IntMap.Strict as IntMap+import qualified Data.Map.Strict as Map+import qualified Data.Text as Text+import qualified Data.Version as Version+import qualified GHC.Generics as Generics+import qualified Octane.Parser as Parser+import qualified Octane.Type as Type+import qualified Prelude+++newtype FullReplay = FullReplay+    { unpackFullReplay :: (Type.Replay, [Parser.Frame])+    } deriving (Generics.Generic, Prelude.Show)++instance DeepSeq.NFData FullReplay++instance Aeson.ToJSON FullReplay where+    toJSON fullReplay = do+        Aeson.object+            [ "Version" .= getVersion fullReplay+            , "Metadata" .= getMetadata fullReplay+            , "Levels" .= getLevels fullReplay+            , "Messages" .= getMessages fullReplay+            , "TickMarks" .= getTickMarks fullReplay+            , "Frames" .= getFrames fullReplay+            ]+++newFullReplay :: Type.Replay -> [Parser.Frame] -> FullReplay+newFullReplay replay frames = FullReplay (replay, frames)+++getVersion :: FullReplay -> Prelude.String+getVersion fullReplay =+    [ fullReplay+        & unpackFullReplay+        & Prelude.fst+        & Type.replayVersion1+        & Type.unpackWord32LE+        & Prelude.fromIntegral+    , fullReplay+        & unpackFullReplay+        & Prelude.fst+        & Type.replayVersion2+        & Type.unpackWord32LE+        & Prelude.fromIntegral+    ] & Version.makeVersion & Version.showVersion+++getMetadata :: FullReplay -> Map.Map Text.Text Aeson.Value+getMetadata fullReplay = fullReplay+    & unpackFullReplay+    & Prelude.fst+    & Type.replayProperties+    & Type.unpackDictionary+    & Map.mapKeys Type.unpackPCString+    & Map.map Aeson.toJSON+++getLevels :: FullReplay -> [Text.Text]+getLevels fullReplay = fullReplay+    & unpackFullReplay+    & Prelude.fst+    & Type.replayLevels+    & Type.unpackList+    & Prelude.map Type.unpackPCString+++getMessages :: FullReplay -> Map.Map Text.Text Type.PCString+getMessages fullReplay = fullReplay+    & unpackFullReplay+    & Prelude.fst+    & Type.replayMessages+    & Type.unpackList+    & Prelude.map (\ message ->+        ( message+            & Type.messageFrame+            & Type.unpackWord32LE+            & Prelude.show+            & Text.pack+        , message & Type.messageContent+        ))+    & Map.fromList+++getTickMarks :: FullReplay -> Map.Map Text.Text Type.PCString+getTickMarks fullReplay = fullReplay+    & unpackFullReplay+    & Prelude.fst+    & Type.replayMarks+    & Type.unpackList+    & Prelude.map (\ mark ->+        ( mark+            & Type.markFrame+            & Type.unpackWord32LE+            & Prelude.show+            & Text.pack+        , mark & Type.markLabel+        ))+    & Map.fromList+++getFrames :: FullReplay -> [Map.Map Text.Text Aeson.Value]+getFrames fullReplay = fullReplay+    & unpackFullReplay+    & Prelude.snd+    & Foldable.foldl'+        (\ (state, frames) frame -> let+            newState = updateState frame state+            minimalFrame = getDelta state frame+            in (newState, minimalFrame : frames))+        (initialState, [])+    & Prelude.snd+    & Prelude.reverse+    & Prelude.map (\ frame -> Map.fromList+        [ ("IsKeyFrame", frame & Parser.frameIsKeyFrame & Aeson.toJSON)+        , ("Number", frame & Parser.frameNumber & Aeson.toJSON)+        , ("Time", frame & Parser.frameTime & Aeson.toJSON)+        , ("Delta", frame & Parser.frameDelta & Aeson.toJSON)+        , ("Spawned", getSpawnedActors frame)+        , ("Updated", getUpdatedActors frame)+        , ("Destroyed", getDestroyedActors frame)+        ])+++-- { actor id => (alive?, { property name => property value } ) }+type State = IntMap.IntMap (Prelude.Bool, Map.Map Text.Text Parser.PropValue)+++initialState :: State+initialState = IntMap.empty+++updateState :: Parser.Frame -> State -> State+updateState frame state1 = let+    spawned = frame+        & Parser.frameReplications+        & Prelude.filter (\ replication -> replication+            & Parser.replicationState+            & (== Parser.RSOpening))+        & Prelude.map Parser.replicationActorId+    state2 = spawned+        & Prelude.foldr+            (IntMap.alter (\ maybeValue -> Prelude.Just (case maybeValue of+                Prelude.Nothing -> (Prelude.True, Map.empty)+                Prelude.Just (_, properties) -> (Prelude.True, properties))))+            state1++    destroyed = frame+        & Parser.frameReplications+        & Prelude.filter (\ replication -> replication+            & Parser.replicationState+            & (== Parser.RSClosing))+        & Prelude.map Parser.replicationActorId+    state3 = destroyed+        & Prelude.foldr+            (IntMap.alter (\ maybeValue -> Prelude.Just (case maybeValue of+                Prelude.Nothing -> (Prelude.False, Map.empty)+                Prelude.Just (_, properties) -> (Prelude.False, properties))))+            state2++    updated = frame+        & Parser.frameReplications+        & Prelude.filter (\ replication -> replication+            & Parser.replicationState+            & (== Parser.RSExisting))+    state4 = updated+        & Prelude.foldr+            (\ replication -> IntMap.alter+                (\ maybeValue -> Prelude.Just (case maybeValue of+                    Prelude.Nothing ->+                        (Prelude.True, Parser.replicationProperties replication)+                    Prelude.Just (alive, properties) ->+                        ( alive+                        , Map.union+                            (Parser.replicationProperties replication)+                            properties+                        )))+                (Parser.replicationActorId replication))+            state3++    in state4+++getDelta :: State -> Parser.Frame -> Parser.Frame+getDelta state frame = let+    newReplications = frame+        & Parser.frameReplications+        -- Remove replications that aren't actually new.+        & reject (\ replication -> let+            isOpening = Parser.replicationState replication == Parser.RSOpening+            actorId = Parser.replicationActorId replication+            currentState = IntMap.lookup actorId state+            isAlive = Prelude.fmap Prelude.fst currentState+            wasAlreadyAlive = isAlive == Prelude.Just Prelude.True+            in isOpening && wasAlreadyAlive)+        -- Remove properties that haven't changed.+        & Prelude.map (\ replication ->+            if Parser.replicationState replication == Parser.RSExisting+            then let+                actorId = Parser.replicationActorId replication+                currentState = IntMap.findWithDefault+                    (Prelude.True, Map.empty) actorId state+                currentProperties = Prelude.snd currentState+                newProperties = Parser.replicationProperties replication+                changes = newProperties+                    & Map.filterWithKey (\ name newValue -> let+                        oldValue = Map.lookup name currentProperties+                        in Prelude.Just newValue /= oldValue)+                in replication { Parser.replicationProperties = changes }+            else replication)+    in frame { Parser.frameReplications = newReplications }+++reject :: (a -> Prelude.Bool) -> [a] -> [a]+reject p xs = Prelude.filter (\ x -> Prelude.not (p x)) xs+++getSpawnedActors :: Parser.Frame -> Aeson.Value+getSpawnedActors frame = frame+    & Parser.frameReplications+    & Prelude.filter (\ replication -> replication+        & Parser.replicationState+        & (== Parser.RSOpening))+    & Prelude.map (\ replication ->+        ( replication+            & Parser.replicationActorId+            & Prelude.show+            & Text.pack+        , Aeson.object+            [ ("Name", replication+                & Parser.replicationObjectName+                & Aeson.toJSON)+            , ("Class", replication+                & Parser.replicationClassName+                & Aeson.toJSON)+            , ("Position", replication+                & Parser.replicationInitialization+                & Prelude.fmap Parser.classInitLocation+                & Monad.join+                & Aeson.toJSON)+            , ("Rotation", replication+                & Parser.replicationInitialization+                & Prelude.fmap Parser.classInitRotation+                & Monad.join+                & Aeson.toJSON)+            ]+        ))+    & Aeson.object+++getUpdatedActors :: Parser.Frame -> Aeson.Value+getUpdatedActors frame = frame+    & Parser.frameReplications+    & Prelude.filter (\ replication -> replication+        & Parser.replicationState+        & (== Parser.RSExisting))+    & Prelude.map (\ replication ->+        ( replication+            & Parser.replicationActorId+            & Prelude.show+            & Text.pack+        , replication+            & Parser.replicationProperties+            & Map.map (\ property -> Aeson.object+                [ ("Type", getPropertyName property)+                , ("Value", getPropertyValue property)+                ])+        ))+    & reject (\ (_, properties) -> Map.null properties)+    & Prelude.map (\ (actorId, properties) ->+        (actorId, Aeson.toJSON properties))+    & Aeson.object+++getPropertyName :: Parser.PropValue -> Aeson.Value+getPropertyName property = case property of+    Parser.PBoolean _ -> "Boolean"+    Parser.PByte _ -> "Byte"+    Parser.PCamSettings _ _ _ _ _ _ -> "CameraSettings"+    Parser.PDemolish _ _ _ _ _ _ -> "Demolition"+    Parser.PEnum _ _ -> "Enum"+    Parser.PExplosion _ _ _ -> "Explosion"+    Parser.PFlaggedInt _ _ -> "FlaggedInt"+    Parser.PFloat _ -> "Float"+    Parser.PGameMode _ -> "GameMode"+    Parser.PInt _ -> "Int"+    Parser.PLoadout _ _ _ _ _ _ _ _ _ -> "Loadout"+    Parser.PLoadoutOnline _ _ _ _ -> "OnlineLoadout"+    Parser.PLocation _ -> "Position"+    Parser.PMusicStinger _ _ _ -> "MusicStinger"+    Parser.PPickup _ _ _ -> "Pickup"+    Parser.PPrivateMatchSettings _ _ _ _ _ _ -> "PrivateMatchSettings"+    Parser.PQWord _ _ -> "QWord"+    Parser.PRelativeRotation _ -> "RelativeRotation"+    Parser.PReservation _ _ _ _ _ _ _ -> "Reservation"+    Parser.PRigidBodyState _ _ _ _ _ -> "RigidBodyState"+    Parser.PString _ -> "String"+    Parser.PTeamPaint _ _ _ _ _ -> "Paint"+    Parser.PUniqueId _ _ _ -> "UniqueId"+++getPropertyValue :: Parser.PropValue -> Aeson.Value+getPropertyValue property = case property of+    Parser.PBoolean x -> Aeson.toJSON x+    Parser.PByte x -> Aeson.toJSON x+    Parser.PCamSettings fov height angle distance stiffness swivelSpeed -> Aeson.object+        [ ("FOV", Aeson.toJSON fov)+        , ("Height", Aeson.toJSON height)+        , ("Angle", Aeson.toJSON angle)+        , ("Distance", Aeson.toJSON distance)+        , ("Stiffness", Aeson.toJSON stiffness)+        , ("SwivelSpeed", Aeson.toJSON swivelSpeed)+        ]+    Parser.PDemolish a b c d e f -> Aeson.toJSON (a, b, c, d, e, f)+    Parser.PEnum x y -> Aeson.toJSON (x, y)+    Parser.PExplosion a b c -> Aeson.toJSON (a, b, c)+    Parser.PFlaggedInt x y -> Aeson.toJSON (x, y)+    Parser.PFloat x -> Aeson.toJSON x+    Parser.PGameMode x -> case x of+        1 -> "Hockey"+        2 -> "Hoops"+        _ -> Aeson.String ("Unknown game mode " <> Text.pack (Prelude.show x))+    Parser.PInt x -> Aeson.toJSON x+    Parser.PLoadout version body decal wheels rocketTrail antenna topper x y -> Aeson.object+        [ ("Version", Aeson.toJSON version)+        , ("Body", Aeson.toJSON body)+        , ("Decal", Aeson.toJSON decal)+        , ("Wheels", Aeson.toJSON wheels)+        , ("RocketTrail", Aeson.toJSON rocketTrail)+        , ("Antenna", Aeson.toJSON antenna)+        , ("Topper", Aeson.toJSON topper)+        , ("Unknown1", Aeson.toJSON x)+        , ("Unknown2", Aeson.toJSON y)+        ]+    Parser.PLoadoutOnline a b c d -> Aeson.toJSON (a, b, c, d)+    Parser.PLocation x -> Aeson.toJSON x+    Parser.PMusicStinger a b c -> Aeson.toJSON (a, b, c)+    Parser.PPickup a b c -> Aeson.toJSON (a, b, c)+    Parser.PPrivateMatchSettings mutators joinableBy maxPlayers name password x -> Aeson.object+        [ ("Mutators", Aeson.toJSON mutators)+        , ("JoinableBy", Aeson.toJSON joinableBy)+        , ("MaxPlayers", Aeson.toJSON maxPlayers)+        , ("Name", Aeson.toJSON name)+        , ("Password", Aeson.toJSON password)+        , ("Unknown", Aeson.toJSON x)+        ]+    Parser.PQWord a b -> Aeson.toJSON (a, b)+    Parser.PRelativeRotation x -> Aeson.toJSON x+    Parser.PReservation number systemId remoteId localId name x y -> Aeson.object+        [ ("Number", Aeson.toJSON number)+        , ("SystemId", Aeson.toJSON systemId)+        , ("RemoteId", Aeson.toJSON remoteId)+        , ("LocalId", Aeson.toJSON localId)+        , ("Name", Aeson.toJSON name)+        , ("Unknown1", Aeson.toJSON x)+        , ("Unknown2", Aeson.toJSON y)+        ]+    Parser.PRigidBodyState sleeping position rotation linear angular -> Aeson.object+        [ ("Sleeping", Aeson.toJSON sleeping)+        , ("Position", Aeson.toJSON position)+        , ("Rotation", Aeson.toJSON rotation)+        , ("LinearVelocity", Aeson.toJSON linear)+        , ("AngularVelocity", Aeson.toJSON angular)+        ]+    Parser.PString x -> Aeson.toJSON x+    Parser.PTeamPaint team color1 color2 finish1 finish2 -> Aeson.object+        [ ("Team", Aeson.toJSON team)+        , ("PrimaryColor", Aeson.toJSON color1)+        , ("AccentColor", Aeson.toJSON color2)+        , ("PrimaryFinish", Aeson.toJSON finish1)+        , ("AccentFinish", Aeson.toJSON finish2)+        ]+    Parser.PUniqueId systemId remoteId localId -> Aeson.object+        [ ("System", case systemId of+            0 -> "Local"+            1 -> "Steam"+            2 -> "PlayStation"+            4 -> "Xbox"+            _ -> Aeson.String ("Unknown system " <> Text.pack (Prelude.show systemId)))+        , ("Remote", case remoteId of+            Parser.SplitscreenId x -> Aeson.toJSON x+            Parser.SteamId x -> Aeson.toJSON x+            Parser.PlayStationId x -> Aeson.toJSON x+            Parser.XboxId x -> Aeson.toJSON x)+        , ("Local", Aeson.toJSON localId)+        ]++getDestroyedActors :: Parser.Frame -> Aeson.Value+getDestroyedActors frame = frame+    & Parser.frameReplications+    & Prelude.filter (\ replication -> replication+        & Parser.replicationState+        & (== Parser.RSClosing))+    & Prelude.map Parser.replicationActorId+    & Aeson.toJSON+++parseReplay :: ByteString.ByteString -> Prelude.Either Text.Text FullReplay+parseReplay bytes = do+    case Binary.decodeOrFail bytes of+        Prelude.Left (_, _, message) -> do+            Prelude.Left (Text.pack message)+        Prelude.Right (_, _, replay) -> do+            let frames = Parser.parseFrames replay+            Prelude.Right (newFullReplay replay frames)+++parseReplayFile :: Prelude.FilePath -> Prelude.IO (Prelude.Either Text.Text FullReplay)+parseReplayFile file = do+    result <- Binary.decodeFileOrFail file+    case result of+        Prelude.Left (_, message) -> do+            Prelude.pure (Prelude.Left (Text.pack message))+        Prelude.Right replay -> do+            let frames = Parser.parseFrames replay+            Prelude.pure (Prelude.Right (newFullReplay replay frames))+++unsafeParseReplay :: ByteString.ByteString -> FullReplay+unsafeParseReplay bytes = do+    let replay = Binary.decode bytes+    let frames = Parser.parseFrames replay+    newFullReplay replay frames+++unsafeParseReplayFile :: Prelude.FilePath -> Prelude.IO FullReplay+unsafeParseReplayFile file = do+    replay <- Binary.decodeFile file+    let frames = Parser.parseFrames replay+    Prelude.pure (newFullReplay replay frames)
library/Octane/Main.hs view
@@ -1,75 +1,16 @@ module Octane.Main (main) where  import qualified Control.DeepSeq as DeepSeq-import qualified Control.Monad as Monad import qualified Data.Aeson as Aeson-import qualified Data.Binary as Binary-import qualified Data.Binary.Get as Binary-import qualified Data.ByteString as BS-import qualified Data.ByteString.Lazy as BSL-import qualified Data.ByteString.Lazy.Char8 as BSL8-import Data.Function ((&))-import qualified Data.Map as Map-import qualified Data.Maybe as Maybe-import qualified Data.Text as Text-import qualified Octane.Parser as Parser-import qualified Octane.Type as Type+import qualified Data.ByteString.Lazy.Char8 as ByteString+import qualified Octane.FullReplay as FullReplay+import qualified Prelude import qualified System.Environment as Environment-import qualified System.IO as IO -main :: IO ()-main = do-    files <- Environment.getArgs-    contents <- mapM BS.readFile files-    results <- mapM Binary.decodeFileOrFail files-    mapM_ debug (zip3 files contents results) -debug :: (String, BS.ByteString, Either (Binary.ByteOffset, String) Type.Replay)-      -> IO ()-debug (file,contents,result) =-    case result of-        Left (offset,message) -> error-            ( file-            ++ " @ byte "-            ++ show offset-            ++ ": "-            ++ message-            )-        Right replay -> do-            let inputSize = contents & BS.length & fromIntegral-            let outputSize = replay-                    & Binary.encode-                    & BSL.length-                    & DeepSeq.deepseq replay-            Monad.when (inputSize /= outputSize) (IO.hPutStrLn IO.stderr-                ( "input size "-                ++ show inputSize-                ++ " not equal to output size "-                ++ show outputSize-                ))--            let frames = Parser.parseFrames replay-            let expectedFrames = replay-                    & Type.replayProperties-                    & Type.unpackDictionary-                    & Map.lookup ("NumFrames" & Text.pack & Type.PCString)-                    & Maybe.fromMaybe (Type.IntProperty (Type.Word64LE 4) (Type.Word32LE 0))-            let actualFrames = frames-                    & length-                    & fromIntegral-                    & Type.Word32LE-                    & Type.IntProperty (Type.Word64LE 4)-                    & DeepSeq.deepseq frames-            Monad.when (expectedFrames /= actualFrames) (error-                ( "expected "-                ++ show expectedFrames-                ++ " frames but found "-                ++ show actualFrames-                ++ " frames"-                ))--            putStr "{\"meta\":"-            replay & Aeson.encode & BSL8.putStrLn-            putStr ",\"frames\":"-            frames & Aeson.encode & BSL8.putStrLn-            putStrLn "}"+main :: Prelude.IO ()+main = do+    [file] <- Environment.getArgs+    fullReplay <- FullReplay.unsafeParseReplayFile file+    DeepSeq.deepseq fullReplay (Prelude.pure ())+    ByteString.putStrLn (Aeson.encode fullReplay)
library/Octane/Parser.hs view
@@ -36,29 +36,29 @@         & (\ property -> case property of             Just (Type.IntProperty _ x) -> x & Type.unpackWord32LE & fromIntegral             _ -> 0)-    get = replay & extractContext & getFrames numFrames & Bits.runBitGet+    get = replay & extractContext & getFrames 0 numFrames & Bits.runBitGet     stream = replay & Type.replayStream & Type.unpackStream & BSL.fromStrict     (_context, frames) = Binary.runGet get stream     in frames -getFrames :: Int -> Context -> Bits.BitGet (Context, [Frame])-getFrames numFrames context = do-    if numFrames < 1+getFrames :: Int -> Int -> Context -> Bits.BitGet (Context, [Frame])+getFrames number numFrames context = do+    if number >= numFrames     then return (context, [])     else do         isEmpty <- Bits.isEmpty         if isEmpty         then return (context, [])         else do-            maybeFrame <- getMaybeFrame context+            maybeFrame <- getMaybeFrame context number             case maybeFrame of                 Nothing -> return (context, [])                 Just (newContext, frame) -> do-                    (newerContext, frames) <- getFrames (numFrames - 1) newContext+                    (newerContext, frames) <- getFrames (number + 1) numFrames newContext                     return (newerContext, (frame : frames)) -getMaybeFrame :: Context -> Bits.BitGet (Maybe (Context, Frame))-getMaybeFrame context = do+getMaybeFrame :: Context -> Int -> Bits.BitGet (Maybe (Context, Frame))+getMaybeFrame context number = do     time <- getFloat32     delta <- getFloat32     if time == 0 && delta == 0@@ -66,15 +66,17 @@     else if time < 0.001 || delta < 0.001     then error ("parsing previous frame probably failed. time: " ++ show time ++ ", delta: " ++ show delta)     else do-        (newContext, frame) <- getFrame context time delta+        (newContext, frame) <- getFrame context number time delta         return (Just (newContext, frame)) -getFrame :: Context -> Time -> Delta -> Bits.BitGet (Context, Frame)-getFrame context time delta = do+getFrame :: Context -> Int -> Time -> Delta -> Bits.BitGet (Context, Frame)+getFrame context number time delta = do     (newContext, replications) <- getReplications context     let frame =             Frame-            { frameTime = time+            { frameNumber = number+            , frameIsKeyFrame = context & contextKeyFrames & Set.member number+            , frameTime = time             , frameDelta = delta             , frameReplications = replications             }@@ -606,7 +608,9 @@ -- | 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. data Frame = Frame-    { frameTime :: !Float+    { frameNumber :: !Int+    , frameIsKeyFrame :: !Bool+    , frameTime :: !Float     , frameDelta :: !Float     , frameReplications :: ![Replication]     } deriving (Eq,Generics.Generic,Show)@@ -696,6 +700,7 @@     , contextClassPropertyMap :: !ClassPropertyMap     , contextThings :: !(IntMap.IntMap Thing)     , contextClassMap :: !ClassMap+    , contextKeyFrames :: !(Set.Set Int)     } deriving (Eq, Generics.Generic, Show)  instance DeepSeq.NFData Context@@ -707,6 +712,13 @@     , contextClassPropertyMap = CPM.getClassPropertyMap replay     , contextThings = IntMap.empty     , contextClassMap = CPM.getActorMap replay+    , contextKeyFrames = replay+        & Type.replayKeyFrames+        & Type.unpackList+        & map Type.keyFrameFrame+        & map Type.unpackWord32LE+        & map fromIntegral+        & Set.fromList     }  byteStringToFloat :: BS.ByteString -> Float
octane.cabal view
@@ -3,7 +3,7 @@ -- see: https://github.com/sol/hpack  name:           octane-version:        0.7.0+version:        0.8.0 synopsis:       Parse Rocket League replays. description:    Octane parses Rocket League replays. category:       Game@@ -45,6 +45,7 @@       Octane       Octane.Analyzer       Octane.Data+      Octane.FullReplay       Octane.Json       Octane.Main       Octane.Parser
package.yaml view
@@ -69,4 +69,4 @@     - -with-rtsopts=-N     main: TestSuite.hs     source-dirs: test-suite-version: '0.7.0'+version: '0.8.0'