packages feed

octane-0.6.2: library/Octane/Analyzer.hs

module Octane.Analyzer where

import Data.Function ((&))

import qualified Data.Binary as Binary
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.Parser as Parser
import qualified System.Environment as Environment

type Point = (Int, Int, Int)
type Points = [Point]
type Frames = [Parser.Frame]
type ActorId = Int

-- { car actor id => player actor id }
getCarActorIds :: Frames -> Map.Map ActorId ActorId
getCarActorIds frames = frames
    & concatMap Parser.frameReplications
    & filter (\ replication -> replication
        & Parser.replicationClassName
        & flip Set.member carClassNames)
    & Maybe.mapMaybe (\ replication -> let
        carActorId = replication
            & Parser.replicationActorId
        maybeProperty = replication
            & Parser.replicationProperties
            & Map.lookup playerReplicationInfoPropertyName
        in case maybeProperty of
            Nothing -> Nothing
            Just property -> case property of
                Parser.PFlaggedInt _ playerActorId -> Just (carActorId, playerActorId)
                _ -> Nothing)
    & Map.fromList

getBallActorIds :: Frames -> Set.Set ActorId
getBallActorIds frames = frames
    & concatMap Parser.frameReplications
    & filter (\ replication -> replication
        & Parser.replicationClassName
        & (== ballClassName))
    & map (\ replication -> replication
        & Parser.replicationActorId)
    & Set.fromList

-- { player actor id => player name }
getPlayerActorIds :: Frames -> Map.Map ActorId Text.Text
getPlayerActorIds frames = frames
    & concatMap Parser.frameReplications
    & filter (\ replication -> replication
        & Parser.replicationClassName
        & (== playerClassName))
    & Maybe.mapMaybe (\ replication -> let
        actorId = replication
            & Parser.replicationActorId
        maybeProperty = replication
            & Parser.replicationProperties
            & Map.lookup playerNamePropertyName
        in case maybeProperty of
            Nothing -> Nothing
            Just property -> case property of
                Parser.PString name -> Just (actorId, name)
                _ -> Nothing)
    & Map.fromList

getRigidBodyStatesForActorId :: Frames -> ActorId -> [(Parser.Time, Point)]
getRigidBodyStatesForActorId frames actorId = frames
    & concatMap (\ frame -> let
        time = Parser.frameTime frame
        in frame
            & Parser.frameReplications
            & filter (\ replication -> replication
                & Parser.replicationActorId
                & (== actorId))
            & map Parser.replicationProperties
            & Maybe.mapMaybe (\ properties -> properties
                & Map.lookup rbsPropertyName)
            & Maybe.mapMaybe (\ property -> case property of
                Parser.PRigidBodyState _ location _ _ _ -> Just location
                _ -> Nothing)
            & map (\ (Parser.Vector x y z) -> (x, y, z))
            & map (\ point -> (time, point)))

getBallRigidBodyStates :: Frames -> [[(Parser.Time, Point)]]
getBallRigidBodyStates frames = frames
    & getBallActorIds
    & Set.toAscList
    & map (\ actorId -> actorId
        & getRigidBodyStatesForActorId frames)

getBallLocations :: Frames -> Points
getBallLocations frames = frames
    & getBallRigidBodyStates
    & concat
    & map snd

carClassNames :: Set.Set Text.Text
carClassNames =
    [ "TAGame.Car_Season_TA"
    , "TAGame.Car_TA"
    ] & map Text.pack & Set.fromList

ballClassName :: Text.Text
ballClassName = Text.pack "TAGame.Ball_TA"

playerClassName :: Text.Text
playerClassName = Text.pack "TAGame.PRI_TA"

playerReplicationInfoPropertyName :: Text.Text
playerReplicationInfoPropertyName = Text.pack "Engine.Pawn:PlayerReplicationInfo"

rbsPropertyName :: Text.Text
rbsPropertyName = Text.pack "TAGame.RBActor_TA:ReplicatedRBState"

playerNamePropertyName :: Text.Text
playerNamePropertyName = Text.pack "Engine.PlayerReplicationInfo:PlayerName"

getDistance :: Point -> Point -> Float
getDistance (x1, y1, z1) (x2, y2, z2) = let
    dx = fromIntegral (x2 - x1)
    dy = fromIntegral (y2 - y1)
    dz = fromIntegral (z2 - z1)
    in sqrt (dx ** 2 + dy ** 2 + dz ** 2)

getBallDistances :: Points -> [Float]
getBallDistances points = points
    & zip (drop 1 points)
    & map (\ (p2, p1) -> getDistance p1 p2)

getBallDistance :: Points -> Float
getBallDistance points = points
    & getBallDistances
    & sum

getBallSpeeds :: Points -> [Float]
getBallSpeeds points = points
    & getBallDistances
    & map (\ distance -> distance / 0.04 {- the average delta -})

getHisto :: (a -> Int) -> Int -> Int -> [a] -> (Int, Int, Int)
getHisto fromPoint bottom top points = let
    range = abs (top - bottom)
    third = quot range 3
    values = map fromPoint points
    in foldr
        ((\ value (low, mid, high) ->
            if value > top - third then (low, mid, high + 1)
            else if value < bottom + third then (low + 1, mid, high)
            else (low, mid + 1, high)))
        (0, 0, 0)
        values

getXHisto :: Points -> (Int, Int, Int) -- not sure of the direction... left to right?
getXHisto points = getHisto (\ (x, _, _) -> x) (-4500) 4500 points

getYHisto :: Points -> (Int, Int, Int) -- not sure of the direction... orange to blue?
getYHisto points = getHisto (\ (_, y, _) -> y) (-5200) 5200 points

getZHisto :: Points -> (Int, Int, Int) -- bottom to top
getZHisto points = getHisto (\ (_, _, z) -> z) 0 2000 points

getSpeedHisto :: [Float] -> (Int, Int, Int) -- slow to fast
getSpeedHisto speeds = getHisto round 0 2000 speeds

analyze :: FilePath -> IO ()
analyze file = do
    replay <- Binary.decodeFile file
    let frames = Parser.parseFrames replay

    putStr "Number of frames: "
    print (length frames)

    let ballLocations = getBallLocations frames
    putStr "Number of ball replications: "
    print (length ballLocations) -- 7702

    let ballDistance = getBallDistance ballLocations
    putStr "Total distance traveled by the ball: "
    print ballDistance -- 323759.94

    let (xs, ys, zs) = unzip3 ballLocations
    let minX = minimum xs
    let maxX = maximum xs
    putStr "(Minimum X value, maximum X value): "
    print (minX, maxX) -- (-4004,4003) [wasteland (-4451,4458)]

    let minY = minimum ys
    let maxY = maximum ys
    putStr "(Minimum Y value, maximum Y value): "
    print (minY, maxY) -- (-5215,5214)

    let minZ = minimum zs
    let maxZ = maximum zs
    putStr "(Minimum Z value, maximum Z value): "
    print (minZ, maxZ) -- (88,1943)

    let xHisto = getXHisto ballLocations
    putStr "(Left, middle, right): "
    print xHisto -- (2241,2237,3224)

    let yHisto = getYHisto ballLocations
    putStr "(Orange, middle, blue): "
    print yHisto -- (3731,1504,2467)

    let zHisto = getZHisto ballLocations
    putStr "(Bottom, middle, top): "
    print zHisto -- (6967,507,228)

    let ballSpeeds = getBallSpeeds ballLocations
    let minSpeed = minimum ballSpeeds
    let maxSpeed = maximum ballSpeeds
    putStr "(Minimum speed, maximum speed): "
    print (minSpeed, maxSpeed) -- (0.0,130894.555)

    let speedHisto = getSpeedHisto ballSpeeds
    putStr "(Slow, medium, fast): "
    print speedHisto -- (2250,3630,1821)

    let playerActorIds = getPlayerActorIds frames
    putStrLn "Players:"
    playerActorIds & Map.toList & mapM_ print

    let carActorIds = getCarActorIds frames
    putStrLn "Cars:"
    carActorIds & Map.toList & mapM_ print

main :: IO ()
main = do
    args <- Environment.getArgs
    mapM_ analyze args