octane-0.5.0: library/Octane/Parser.hs
{-# LANGUAGE DeriveGeneric #-}
module Octane.Parser (parseFrames) where
import Data.Function ((&))
import qualified Control.Newtype as Newtype
import qualified Data.Binary.Bits.Get as Bits
import qualified Data.Binary.IEEE754 as IEEE754
import qualified Data.Binary.Get as Binary
import qualified Data.Bits as Bits
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import qualified Data.IntMap as IntMap
import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import qualified Data.Set as Set
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Encoding
import qualified Data.Word as Word
import qualified GHC.Generics as Generics
import qualified Octane.Type as Type
import qualified Text.Regex as Regex
parseFrames :: Type.Replay -> [Frame]
parseFrames replay = let
get = replay & extractContext & getFrames & Bits.runBitGet
stream = replay & Type.replayStream & Newtype.unpack & BSL.fromStrict
(_context, frames) = Binary.runGet get stream
in frames
getFrames :: Context -> Bits.BitGet (Context, [Frame])
getFrames context = do
maybeFrame <- getMaybeFrame context
case maybeFrame of
Nothing -> return (context, [])
Just (newContext, frame) -> do
(newerContext, frames) <- getFrames newContext
return (newerContext, (frame : frames))
getMaybeFrame :: Context -> Bits.BitGet (Maybe (Context, Frame))
getMaybeFrame context = do
time <- getFloat32
delta <- getFloat32
if time == 0 && delta == 0
then return Nothing
else do
(newContext, frame) <- getFrame context time delta
return (Just (newContext, frame))
getFrame :: Context -> Time -> Delta -> Bits.BitGet (Context, Frame)
getFrame context time delta = do
(newContext, replications) <- getReplications context
let frame =
Frame
{ frameTime = time
, frameDelta = delta
, frameReplications = replications
}
return (newContext, frame)
getReplications :: Context -> Bits.BitGet (Context, [Replication])
getReplications context = do
maybeReplication <- getMaybeReplication context
case maybeReplication of
Nothing -> return (context, [])
Just (newContext, replication) -> do
(newerContext, replications) <- getReplications newContext
return (newerContext, replication : replications)
getMaybeReplication :: Context -> Bits.BitGet (Maybe (Context, Replication))
getMaybeReplication context = do
hasReplication <- Bits.getBool
if not hasReplication
then return Nothing
else do
(newContext,replication) <- getReplication context
return (Just (newContext, replication))
getReplication :: Context -> Bits.BitGet (Context, Replication)
getReplication context = do
actorId <- getInt maxChannels
isOpen <- Bits.getBool
let go =
if isOpen
then getOpenReplication
else getClosedReplication
go context actorId
getOpenReplication :: Context
-> ActorId
-> Bits.BitGet (Context, 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, Replication)
getNewReplication context actorId = do
unknownFlag <- Bits.getBool
objectId <- getInt32
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 getClass context objectId of
Nothing -> error ("could not find class for object id " ++ show objectId)
Just x -> x
classInit <- getClassInit className
let thing = Thing
{ thingFlag = unknownFlag
, thingObjectId = objectId
, thingObjectName = objectName
, thingClassId = classId
, thingClassName = className
, thingClassInit = classInit
}
let things = contextThings context
let newThings = IntMap.insert actorId thing things
let newContext = context { contextThings = newThings }
return
( newContext
, Replication
{ replicationActorId = actorId
, replicationIsOpen = True
, replicationIsNew = Just True
, replicationProps = []
})
getExistingReplication :: Context
-> ActorId
-> Bits.BitGet (Context, Replication)
getExistingReplication 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
props <- getProps context thing
return (context, Replication
{ replicationActorId = actorId
, replicationIsOpen = True
, replicationIsNew = Just False
, replicationProps = props
})
getClosedReplication :: Context
-> ActorId
-> Bits.BitGet (Context, Replication)
getClosedReplication context actorId = do
let newThings = context & contextThings & IntMap.delete actorId
let newContext = context { contextThings = newThings }
return
( newContext
, Replication
{ replicationActorId = actorId
, replicationIsOpen = False
, replicationIsNew = Nothing
, replicationProps = []
})
getProps :: Context -> Thing -> Bits.BitGet [Prop]
getProps context thing = do
maybeProp <- getMaybeProp context thing
case maybeProp of
Nothing -> return []
Just prop -> do
props <- getProps context thing
return (prop : props)
getMaybeProp :: Context -> Thing -> Bits.BitGet (Maybe Prop)
getMaybeProp context thing = do
hasProp <- Bits.getBool
if hasProp
then do
prop <- getProp context thing
return (Just prop)
else return Nothing
getProp :: Context -> Thing -> Bits.BitGet Prop
getProp context thing = do
let classId = thing & thingClassId
let props = case context & contextClassPropertyMap & IntMap.lookup classId of
Nothing -> error ("could not find property map for class id " ++ show classId)
Just x -> x
let maxId = props & IntMap.keys & maximum
pid <- getInt maxId
let propName = case props & IntMap.lookup pid of
Nothing -> error ("could not find property name for property id " ++ show pid)
Just x -> x
value <- getPropValue propName
return (Prop { propId = pid, 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
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 propsWithUniqueId -> do
(systemId, remoteId, localId) <- getUniqueId
return (PUniqueId systemId remoteId localId)
_ | 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)
"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 8, which
-- would be a full 4x4 game.
number <- getInt 8
(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)
-- TODO: Parse other prop types.
_ -> fail ("don't know how to read property " ++ show name)
getFloat32 :: Bits.BitGet Float
getFloat32 = do
bytes <- Bits.getByteString 4
bytes & byteStringToFloat & return
-- TODO: This has a lot of overlap with PCString.
getString :: Bits.BitGet Text.Text
getString = do
rawSize <- getInt32
rawText <- if rawSize < 0
then do
let size = -2 * rawSize
bytes <- Bits.getByteString size
bytes & BS.map Type.reverseBits & Encoding.decodeUtf16LE & return
else do
bytes <- Bits.getByteString rawSize
bytes & BS.map Type.reverseBits & Encoding.decodeLatin1 & return
rawText & Text.dropEnd 1 & return
getUniqueId :: Bits.BitGet (SystemId, RemoteId, LocalId)
getUniqueId = do
byte <- Bits.getWord8 8
let systemId = Type.reverseBits byte
case systemId of
0 -> error "don't know how to parse splitscreen ids"
1 -> do
remoteId <- Bits.getByteString 8
localId <- Bits.getWord8 8
return (systemId, SteamId remoteId, localId)
2 -> do
remoteId <- Bits.getByteString 32
localId <- Bits.getWord8 8
return (systemId, PlayStationId remoteId, localId)
_ -> error ("unknown system id " ++ show systemId)
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.Pawn:PlayerReplicationInfo"
, "Engine.PlayerReplicationInfo:Team"
, "TAGame.Ball_TA:GameEvent"
, "TAGame.CameraSettingsActor_TA:PRI"
, "TAGame.CarComponent_TA:Vehicle"
, "TAGame.PRI_TA:PersistentCamera"
, "TAGame.PRI_TA:ReplicatedGameEvent"
, "TAGame.Team_TA:GameEvent"
] & map Text.pack & Set.fromList
propsWithString :: Set.Set Text.Text
propsWithString =
[ "Engine.GameReplicationInfo:ServerName"
, "Engine.PlayerReplicationInfo:PlayerName"
] & map Text.pack & Set.fromList
propsWithBoolean :: Set.Set Text.Text
propsWithBoolean =
[ "Engine.PlayerReplicationInfo:bReadyToPlay"
, "ProjectX.GRI_X:bGameStarted"
, "TAGame.CameraSettingsActor_TA:bUsingSecondaryCamera"
, "TAGame.GameEvent_TA:bHasLeaveMatchPenalty"
, "TAGame.GameEvent_Team_TA:bDisableMutingOtherTeam"
, "TAGame.PRI_TA:bOnlineLoadoutSet"
, "TAGame.Vehicle_TA:bDriving"
, "TAGame.GameEvent_Soccar_TA:bBallHasBeenHit"
, "TAGame.Vehicle_TA:bReplicatedHandbrake"
, "Engine.Actor:bCollideActors"
, "Engine.Actor:bBlockActors"
, "TAGame.CameraSettingsActor_TA:bUsingBehindView"
] & 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"
, "ProjectX.GRI_X:ReplicatedGamePlaylist"
, "TAGame.GameEvent_Soccar_TA:SecondsRemaining"
, "TAGame.GameEvent_TA:BotSkill"
, "TAGame.GameEvent_TA:ReplicatedGameStateTimeRemaining"
, "TAGame.GameEvent_Soccar_TA:RoundNum"
, "TAGame.GameEvent_TA:ReplicatedStateName"
, "TAGame.GameEvent_Team_TA:MaxTeamSize"
, "TAGame.PRI_TA:Title"
, "TAGame.PRI_TA:TotalXP"
, "TAGame.PRI_TA:MatchScore"
, "TAGame.PRI_TA:MatchShots"
, "TAGame.PRI_TA:MatchSaves"
, "Engine.TeamInfo:Score"
, "Engine.PlayerReplicationInfo:Score"
, "TAGame.PRI_TA:MatchGoals"
, "TAGame.PRI_TA:MatchAssists"
] & map Text.pack & Set.fromList
propsWithByte :: Set.Set Text.Text
propsWithByte =
[ "Engine.PlayerReplicationInfo:Ping"
, "TAGame.CarComponent_Boost_TA:ReplicatedBoostAmount"
, "TAGame.Vehicle_TA:ReplicatedThrottle"
, "TAGame.CarComponent_TA:ReplicatedActive"
, "TAGame.Vehicle_TA:ReplicatedSteer"
, "TAGame.Ball_TA:HitTeamNum"
, "TAGame.GameEvent_Soccar_TA:ReplicatedScoredOnTeam"
, "TAGame.CameraSettingsActor_TA:CameraYaw"
, "TAGame.CameraSettingsActor_TA:CameraPitch"
] & map Text.pack & Set.fromList
propsWithUniqueId :: Set.Set Text.Text
propsWithUniqueId =
[ "Engine.PlayerReplicationInfo:UniqueId"
, "TAGame.PRI_TA:PartyLeader"
] & map Text.pack & Set.fromList
propsWithCamSettings :: Set.Set Text.Text
propsWithCamSettings =
[ "TAGame.CameraSettingsActor_TA:ProfileSettings"
] & map Text.pack & Set.fromList
propsWithLocation :: Set.Set Text.Text
propsWithLocation =
[ "TAGame.CarComponent_Dodge_TA:DodgeTorque"
] & map Text.pack & Set.fromList
type SystemId = Word.Word8
-- This is the number associated with a splitscreen player. So the first player
-- is 0, the second is 1, and so on.
-- - 0 "Someone"
-- - 1 "Someone (1)"
type LocalId = Word.Word8
data RemoteId
= SteamId BS.ByteString -- TODO: This is an integer.
| PlayStationId BS.ByteString -- TODO: I think this is a string?
deriving (Eq, Show)
data Prop = Prop
{ propId :: Int
, propValue :: PropValue
} deriving (Eq, Show)
data PropValue
= PRigidBodyState Bool (Vector Int) (Vector Float) (Maybe (Vector Int)) (Maybe (Vector Int))
| PFlaggedInt Bool Int
| PString Text.Text
| PBoolean Bool
| PQWord Int Int
| PReservation Int SystemId RemoteId LocalId (Maybe Text.Text) Bool Bool
| 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
| PLocation (Vector Int)
| PPickup Bool (Maybe Int) Bool
| PEnum Word.Word16 -- TODO: This isn't the right data type.
| PExplosion Bool (Maybe Int) (Vector Int)
| PMusicStinger Bool Int Int
deriving (Eq, Show)
-- | 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
, frameDelta :: Float
, frameReplications :: [Replication]
} deriving (Eq,Generics.Generic,Show)
-- | Replication information about an actor in the net stream.
data Replication = Replication
{ replicationActorId :: Int
, replicationIsOpen :: Bool
, replicationIsNew :: Maybe Bool
, replicationProps :: [Prop]
} deriving (Eq,Generics.Generic,Show)
data Thing = Thing
{ thingFlag :: Bool
, thingObjectId :: Int
, thingObjectName :: Text.Text
, thingClassId :: Int
, thingClassName :: Text.Text
, thingClassInit :: ClassInit
} deriving (Show)
type Time = Float
type Delta = Float
type ActorId = Int
data Vector a = Vector
{ vectorX :: a
, vectorY :: a
, vectorZ :: a
} deriving (Eq, Show)
data ClassInit = ClassInit
{ classInitLocation :: Maybe (Vector Int)
, classInitRotation :: Maybe (Vector Int)
} deriving (Show)
-- { class stream id => { property stream id => name } }
type ClassPropertyMap = IntMap.IntMap (IntMap.IntMap Text.Text)
-- { stream id => object name }
type ObjectMap = IntMap.IntMap Text.Text
-- { archetype (object) name => class name }
type ArchetypeMap = Map.Map Text.Text Text.Text
-- { class name => class id }
type ClassMap = Map.Map Text.Text Int
data Context = Context
{ contextObjectMap :: ObjectMap
, contextClassPropertyMap :: ClassPropertyMap
, contextThings :: IntMap.IntMap Thing
, contextArchetypeMap :: ArchetypeMap
, contextClassMap :: ClassMap
} deriving (Show)
buildObjectMap :: Type.Replay -> ObjectMap
buildObjectMap replay =
replay & Type.replayObjects & Newtype.unpack & map Newtype.unpack &
zip [0 ..] &
IntMap.fromAscList
buildArchetypeMap :: Type.Replay -> ArchetypeMap
buildArchetypeMap replay
= replay
& Type.replayObjects
& Newtype.unpack
& map Newtype.unpack
& map (\ archetype -> let
k = archetype
v = archetypeToClass archetype
in (k, v))
& Map.fromList
& Map.union specialArchetypes
specialArchetypes :: ArchetypeMap
specialArchetypes =
[ ("GameInfo_Soccar.GameInfo.GameInfo_Soccar:GameReplicationInfoArchetype", "GRI")
] & map (\ (k, v) -> (Text.pack k, Text.pack v)) & Map.fromList
buildClassMap :: Type.Replay -> ClassMap
buildClassMap replay
= replay
& Type.replayObjects
& Newtype.unpack
& map Newtype.unpack
& zip [0 ..]
& map (\ (objectId, objectName) -> let
k = archetypeToClass objectName
v = objectId
in (k, v))
& reverse
& Map.fromList
getClass :: Context -> Int -> Maybe (Int, Text.Text)
getClass context objectId = let
objectMap = contextObjectMap context
archetypeMap = contextArchetypeMap context
classMap = contextClassMap context
in case IntMap.lookup objectId objectMap of
Nothing -> Nothing
Just archetypeName -> case Map.lookup archetypeName archetypeMap of
Nothing -> Nothing
Just className -> case Map.lookup className classMap of
Nothing -> Nothing
Just classId -> Just (classId, className)
archetypeToClass :: Text.Text -> Text.Text
archetypeToClass text
= text
& Text.splitOn (Text.pack ".")
& last
& Text.splitOn (Text.pack ":")
& last
& Text.unpack
& substitute "_?[0-9]+$" ""
& substitute "^Default__" ""
& substitute "_TA$" ""
& substitute "_Default$" ""
& substitute "Archetype$" ""
& Text.pack
substitute :: String -> String -> String -> String
substitute pattern replacement input =
Regex.subRegex (Regex.mkRegex pattern) input replacement
extractContext :: Type.Replay -> Context
extractContext replay =
Context
{ contextObjectMap = buildObjectMap replay
, contextClassPropertyMap = buildClassPropertyMap replay
, contextThings = IntMap.empty
, contextArchetypeMap = buildArchetypeMap replay
, contextClassMap = buildClassMap replay
}
classesWithLocation :: Set.Set Text.Text
classesWithLocation =
[ "Ball"
, "CameraSettingsActor"
, "Car"
, "CarComponent_Boost"
, "CarComponent_Dodge"
, "CarComponent_DoubleJump"
, "CarComponent_FlipCar"
, "CarComponent_Jump"
, "GRI"
, "GameEvent_Season"
, "GameEvent_Soccar"
, "GameEvent_SoccarPrivate"
, "GameEvent_SoccarSplitscreen"
, "GameReplicationInfo"
, "PRI"
, "Team"
, "Team_Soccar"
] & map Text.pack & Set.fromList
classesWithRotation :: Set.Set Text.Text
classesWithRotation =
[ "Ball"
, "Car_Season"
, "Car"
] & map Text.pack & Set.fromList
maxVectorValue :: Int
maxVectorValue = 19
byteStringToFloat :: BS.ByteString -> Float
byteStringToFloat bytes = Binary.runGet
IEEE754.getFloat32le
(bytes & BSL.fromStrict & BSL.map Type.reverseBits)
getVector :: Bits.BitGet (Vector Int)
getVector = do
numBits <- getInt maxVectorValue
let bias = Bits.shiftL 1 (numBits + 1)
let maxBits = numBits + 2
let maxValue = 2 ^ maxBits
dx <- getInt maxValue
dy <- getInt maxValue
dz <- getInt maxValue
return
Vector
{ vectorX = dx - bias
, vectorY = dy - bias
, vectorZ = dz - bias
}
getVectorBytewise
:: Bits.BitGet (Vector Int)
getVectorBytewise = do
hasX <- Bits.getBool
x <-
if hasX
then do
word <- Bits.getWord8 8
word & Type.reverseBits & fromIntegral & return
else return 0
hasY <- Bits.getBool
y <-
if hasY
then do
word <- Bits.getWord8 8
word & Type.reverseBits & fromIntegral & return
else return 0
hasZ <- Bits.getBool
z <-
if hasZ
then do
word <- Bits.getWord8 8
word & Type.reverseBits & fromIntegral & return
else return 0
return
Vector
{ vectorX = x
, vectorY = y
, vectorZ = z
}
getFloatVector :: Bits.BitGet (Vector Float)
getFloatVector = do
let maxValue = 1
let numBits = 16
x <- getFloat maxValue numBits
y <- getFloat maxValue numBits
z <- getFloat maxValue numBits
return Vector { vectorX = x, vectorY = y, vectorZ = z }
getFloat :: Int -> Int -> Bits.BitGet Float
getFloat maxValue numBits = do
let maxBitValue = (Bits.shiftL 1 (numBits - 1)) - 1
let bias = Bits.shiftL 1 (numBits - 1)
let serIntMax = Bits.shiftL 1 numBits
delta <- getInt serIntMax
let unscaledValue = delta - bias
if maxValue > maxBitValue
then do
let invScale = fromIntegral maxValue / fromIntegral maxBitValue
return (fromIntegral unscaledValue * invScale)
else do
let scale = fromIntegral maxBitValue / fromIntegral maxValue
let invScale = 1.0 / scale
return (fromIntegral unscaledValue * invScale)
getClassInit :: Text.Text -> Bits.BitGet ClassInit
getClassInit className = do
location <-
if Set.member className classesWithLocation
then do
vector <- getVector
return (Just vector)
else return Nothing
rotation <-
if Set.member className classesWithRotation
then do
vector <- getVectorBytewise
return (Just vector)
else return Nothing
return
ClassInit
{ classInitLocation = location
, classInitRotation = rotation
}
maxChannels
:: (Integral a)
=> a
maxChannels = 1024
bitSize
:: (Integral a)
=> a -> a
bitSize x = x & fromIntegral & logBase (2 :: Double) & ceiling
-- Reads an integer bitwise. The bits of the integer are backwards, so the
-- least significant bit is first. The argument is the maximum value this
-- integer can have. Bits will be read until the next bit would be greater than
-- the maximum value, or the number of bits necessary to reach the maximum
-- value has been reached, whichever comes first.
--
-- For example, if the maximum value is 4 and "11" has been read already,
-- nothing more will be read because another "1" would put the value over the
-- maximum.
getInt
:: Int -> Bits.BitGet Int
getInt maxValue = do
let maxBits = bitSize maxValue
go i value = do
let x = Bits.shiftL 1 i
if i < maxBits && value + x <= maxValue
then do
bit <- Bits.getBool
let newValue =
if bit
then value + x
else value
go (i + 1) newValue
else return value
go 0 0
getInt32 :: Bits.BitGet Int
getInt32 = getInt (2 ^ (32 :: Int))
getInt8 :: Bits.BitGet Int
getInt8 = getInt (2 ^ (8 :: Int))
-- builds a map from property ids in the stream to property names
buildPropertyMap :: Type.Replay -> IntMap.IntMap Text.Text
buildPropertyMap replay
= replay
& Type.replayObjects
& Newtype.unpack
& map Newtype.unpack
& zip [0 ..]
& IntMap.fromDistinctAscList
buildClassPropertyMap :: Type.Replay -> IntMap.IntMap (IntMap.IntMap Text.Text)
buildClassPropertyMap replay = let
propertyMap = buildPropertyMap replay
g x items = case items of
[] -> IntMap.empty
_ -> case dropWhile (\ (_, cacheId, _, _) -> cacheId /= x) items of
[] -> g (x - 1) items
(_, _, parentCacheId, properties) : others -> IntMap.union
properties
(g parentCacheId others)
f x items = case items of
(classId, _cacheId, parentCacheId, properties) : others -> IntMap.insert
classId
(IntMap.union
properties
(g parentCacheId others))
x
[] -> x
in replay
& Type.replayCacheItems
& Newtype.unpack
& map (\ item ->
( item & Type.cacheItemClassId & Newtype.unpack & fromIntegral
, item & Type.cacheItemCacheId & Newtype.unpack & fromIntegral & (\ x -> x :: Int)
, item & Type.cacheItemParentCacheId & Newtype.unpack & fromIntegral
, item & Type.cacheItemCacheProperties & Newtype.unpack & map (\ x ->
( x & Type.cachePropertyStreamId & Newtype.unpack & fromIntegral
, x & Type.cachePropertyObjectId & Newtype.unpack & fromIntegral & flip IntMap.lookup propertyMap & Maybe.fromJust
)) & IntMap.fromList
))
& reverse
& List.tails
& foldl f IntMap.empty