{-# 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.Bimap as Bimap 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.Data as Data import qualified Octane.Parser as Parser import qualified Octane.Parser.Garage as Garage 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.unpackInt32 & Prelude.fromIntegral , fullReplay & unpackFullReplay & Prelude.fst & Type.replayVersion2 & Type.unpackInt32 & 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.unpackText & Map.map Aeson.toJSON getLevels :: FullReplay -> [Text.Text] getLevels fullReplay = fullReplay & unpackFullReplay & Prelude.fst & Type.replayLevels & Type.unpackList & Prelude.map Type.unpackText getMessages :: FullReplay -> Map.Map Text.Text Type.Text getMessages fullReplay = fullReplay & unpackFullReplay & Prelude.fst & Type.replayMessages & Type.unpackList & Prelude.map (\ message -> ( message & Type.messageFrame & Type.unpackInt32 & Prelude.show & Text.pack , message & Type.messageContent )) & Map.fromList getTickMarks :: FullReplay -> Map.Map Text.Text Type.Text getTickMarks fullReplay = fullReplay & unpackFullReplay & Prelude.fst & Type.replayMarks & Type.unpackList & Prelude.map (\ mark -> ( mark & Type.markFrame & Type.unpackInt32 & 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 gameMode -> Aeson.object [ ("Id", Aeson.toJSON gameMode) , ("Name", Data.gameModes & Bimap.lookup (Prelude.fromIntegral gameMode) & (\ x -> x :: Prelude.Maybe Text.Text) & Aeson.toJSON) ] 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.object [ ("Id", Aeson.toJSON body) , ("Name", body & Garage.getBody & Aeson.toJSON) ]) , ("Decal", Aeson.object [ ("Id", Aeson.toJSON decal) , ("Name", decal & Garage.getDecal & Aeson.toJSON) ]) , ("Wheels", Aeson.object [ ("Id", Aeson.toJSON wheels) , ("Name", wheels & Garage.getWheels & Aeson.toJSON) ]) , ("RocketTrail", Aeson.object [ ("Id", Aeson.toJSON rocketTrail) , ("Name", rocketTrail & Garage.getRocketTrail & Aeson.toJSON) ]) , ("Antenna", Aeson.object [ ("Id", Aeson.toJSON antenna) , ("Name", antenna & Garage.getAntenna & Aeson.toJSON) ]) , ("Topper", Aeson.object [ ("Id", Aeson.toJSON topper) , ("Name", topper & Garage.getTopper & Aeson.toJSON) ]) , ("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.object [ ("Id", Aeson.toJSON finish1) , ("Name", finish1 & Garage.getFinish & Aeson.toJSON) ]) , ("AccentFinish", Aeson.object [ ("Id", Aeson.toJSON finish2) , ("Name", finish2 & Garage.getFinish & Aeson.toJSON) ]) ] 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)