packages feed

rattletrap 11.2.1 → 11.2.3

raw patch · 6 files changed

+244/−215 lines, 6 filesdep −HUnitdep ~aesondep ~aeson-prettydep ~array

Dependencies removed: HUnit

Dependency ranges changed: aeson, aeson-pretty, array, base, bytestring, containers, filepath, http-client, http-client-tls, text

Files

rattletrap.cabal view
@@ -1,7 +1,7 @@-cabal-version: 2.0+cabal-version: 2.2  name: rattletrap-version: 11.2.1+version: 11.2.3 synopsis: Parse and generate Rocket League replays. description: Rattletrap parses and generates Rocket League replays. @@ -16,21 +16,50 @@   type: git   location: https://github.com/tfausak/rattletrap +flag pedantic+  default: False+  manual: True++common library+  build-depends: base ^>= 4.15.0+  default-extensions: NamedFieldPuns+  default-language: Haskell2010+  ghc-options:+    -Weverything+    -Wno-all-missed-specialisations+    -Wno-implicit-prelude+    -Wno-missing-deriving-strategies+    -Wno-missing-export-lists+    -Wno-missing-exported-signatures+    -Wno-missing-import-lists+    -Wno-missing-safe-haskell-mode+    -Wno-prepositive-qualified-module+    -Wno-safe+    -Wno-unsafe++  if flag(pedantic)+    ghc-options: -Werror++common executable+  import: library++  build-depends: rattletrap+  ghc-options: -rtsopts -threaded+ library+  import: library+   autogen-modules: Paths_rattletrap   build-depends:-    base >= 4.15.0 && < 4.16-    , aeson >= 1.5.6 && < 1.6-    , aeson-pretty >= 0.8.8 && < 0.9-    , array >= 0.5.4 && < 0.6-    , bytestring >= 0.10.12 && < 0.12-    , containers >= 0.6.4 && < 0.7-    , filepath >= 1.4.2 && < 1.5-    , http-client >= 0.7.8 && < 0.8-    , http-client-tls >= 0.3.5 && < 0.4-    , text >= 1.2.4 && < 1.3-  default-extensions: NamedFieldPuns-  default-language: Haskell2010+    , aeson ^>= 1.5.6 || ^>= 2.0.0+    , aeson-pretty ^>= 0.8.8+    , array ^>= 0.5.4+    , bytestring ^>= 0.10.12+    , containers ^>= 0.6.4+    , filepath ^>= 1.4.2+    , http-client ^>= 0.7.8+    , http-client-tls ^>= 0.3.5+    , text ^>= 1.2.4   exposed-modules:     Paths_rattletrap     Rattletrap@@ -167,36 +196,18 @@     Rattletrap.Utility.Json     Rattletrap.Utility.Monad     Rattletrap.Version-  ghc-options:-    -Weverything-    -Wno-all-missed-specialisations-    -Wno-implicit-prelude-    -Wno-missing-deriving-strategies-    -Wno-missing-export-lists-    -Wno-missing-exported-signatures-    -Wno-missing-import-lists-    -Wno-missing-safe-haskell-mode-    -Wno-prepositive-qualified-module-    -Wno-safe-    -Wno-unsafe   hs-source-dirs: src/lib  executable rattletrap-  build-depends: base, rattletrap-  default-language: Haskell2010-  ghc-options: -rtsopts -threaded+  import: executable+   hs-source-dirs: src/exe   main-is: Main.hs  test-suite test-  build-depends:-    base-    , bytestring-    , filepath-    , HUnit >= 1.6.2 && < 1.7-    , rattletrap-  default-language: Haskell2010-  ghc-options: -rtsopts -threaded+  import: executable++  build-depends: bytestring, filepath   hs-source-dirs: src/test   main-is: Main.hs   type: exitcode-stdio-1.0
src/lib/Rattletrap/Console/Main.hs view
@@ -144,6 +144,7 @@         [ "ERROR: " <> Exception.displayException e         , "-- Context: " <> List.intercalate ", " ls         , "-- You are using Rattletrap version " <> Version.string+        , "-- " <> show config         , "-- Please report this problem at https://github.com/tfausak/rattletrap/issues/new"         ]       Exit.exitFailure
src/lib/Rattletrap/Schema.hs view
@@ -15,10 +15,10 @@ ref :: Schema -> Json.Value ref s = Json.object [Json.pair "$ref" $ Text.pack "#/definitions/" <> name s] -object :: [((Text.Text, Json.Value), Bool)] -> Json.Value+object :: [((String, Json.Value), Bool)] -> Json.Value object xs = Json.object   [ Json.pair "type" "object"-  , Json.pair "properties" . Json.object $ fmap fst xs+  , Json.pair "properties" . Json.object $ fmap (uncurry Json.pair . fst) xs   , Json.pair "required" . fmap (fst . fst) $ filter snd xs   ] 
src/lib/Rattletrap/Type/RemoteId/PsyNet.hs view
@@ -1,5 +1,6 @@ module Rattletrap.Type.RemoteId.PsyNet where +import qualified Control.Applicative as Applicative import qualified Rattletrap.BitGet as BitGet import qualified Rattletrap.BitPut as BitPut import qualified Rattletrap.Schema as Schema@@ -7,21 +8,24 @@ import qualified Rattletrap.Type.Version as Version import qualified Rattletrap.Utility.Json as Json -newtype PsyNet-  = PsyNet (Either U64.U64 (U64.U64, U64.U64, U64.U64, U64.U64))+data PsyNet+  = New U64.U64+  | Old U64.U64 U64.U64 U64.U64 U64.U64   deriving (Eq, Show)  instance Json.FromJSON PsyNet where-  parseJSON = fmap fromEither . Json.parseJSON+  parseJSON = Json.withObject "PsyNet" $ \object -> do+    let+      new = fmap New $ Json.required object "Left"+      old = do+        (a, b, c, d) <- Json.required object "Right"+        pure $ Old a b c d+    new Applicative.<|> old  instance Json.ToJSON PsyNet where-  toJSON = Json.toJSON . toEither--fromEither :: Either U64.U64 (U64.U64, U64.U64, U64.U64, U64.U64) -> PsyNet-fromEither = PsyNet--toEither :: PsyNet -> Either U64.U64 (U64.U64, U64.U64, U64.U64, U64.U64)-toEither (PsyNet x) = x+  toJSON x = case x of+    New a -> Json.object [Json.pair "Left" a]+    Old a b c d -> Json.object [Json.pair "Right" (a, b, c, d)]  schema :: Schema.Schema schema = Schema.named "remote-id-psy-net" $ Schema.oneOf@@ -34,20 +38,16 @@   ]  bitPut :: PsyNet -> BitPut.BitPut-bitPut x = case toEither x of-  Left l -> U64.bitPut l-  Right (a, b, c, d) ->-    U64.bitPut a <> U64.bitPut b <> U64.bitPut c <> U64.bitPut d+bitPut x = case x of+  New l -> U64.bitPut l+  Old a b c d -> U64.bitPut a <> U64.bitPut b <> U64.bitPut c <> U64.bitPut d  bitGet :: Version.Version -> BitGet.BitGet PsyNet-bitGet version =-  BitGet.label "PsyNet"-    . fmap fromEither-    $ if Version.atLeast 868 24 10 version-        then BitGet.label "new" $ fmap Left U64.bitGet-        else BitGet.label "old" . fmap Right $ do-          a <- U64.bitGet-          b <- U64.bitGet-          c <- U64.bitGet-          d <- U64.bitGet-          pure (a, b, c, d)+bitGet version = BitGet.label "PsyNet" $ if Version.atLeast 868 24 10 version+  then BitGet.label "New" $ fmap New U64.bitGet+  else BitGet.label "Old" $ do+    a <- U64.bitGet+    b <- U64.bitGet+    c <- U64.bitGet+    d <- U64.bitGet+    pure $ Old a b c d
src/lib/Rattletrap/Utility/Json.hs view
@@ -1,3 +1,10 @@+{- hlint ignore "Avoid restricted flags" -}+{-# OPTIONS_GHC -Wno-orphans #-}++{- hlint ignore "Avoid restricted extensions" -}+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleInstances #-}+ module Rattletrap.Utility.Json   ( module Rattletrap.Utility.Json   , Aeson.FromJSON(parseJSON)@@ -14,21 +21,45 @@ import qualified Data.Aeson.Types as Aeson import qualified Data.ByteString as ByteString import qualified Data.ByteString.Lazy as LazyByteString++# if MIN_VERSION_aeson(2, 0, 0)++import qualified Data.Aeson.Key as Key++toKey :: String -> Key.Key+toKey = Key.fromString++fromKey :: Key.Key -> String+fromKey = Key.toString++# else+ import qualified Data.Text as Text +toKey :: String -> Text.Text+toKey = Text.pack++fromKey :: Text.Text -> String+fromKey = Text.unpack++# endif++instance Aeson.KeyValue (String, Aeson.Value) where+  k .= v = (fromKey k, Aeson.toJSON v)+ required   :: Aeson.FromJSON value => Aeson.Object -> String -> Aeson.Parser value-required object key = object Aeson..: Text.pack key+required object key = object Aeson..: toKey key  optional   :: Aeson.FromJSON value   => Aeson.Object   -> String   -> Aeson.Parser (Maybe value)-optional object key = object Aeson..:? Text.pack key+optional object key = object Aeson..:? toKey key  pair :: (Aeson.ToJSON value, Aeson.KeyValue pair) => String -> value -> pair-pair key value = Text.pack key Aeson..= value+pair key value = toKey key Aeson..= value  decode :: Aeson.FromJSON a => ByteString.ByteString -> Either String a decode = Aeson.eitherDecodeStrict'
src/test/Main.hs view
@@ -2,48 +2,44 @@ import qualified Data.ByteString as ByteString import qualified GHC.Clock as Clock import qualified Rattletrap-import qualified System.Exit as Exit import qualified System.FilePath as FilePath-import qualified Test.HUnit as Test import qualified Text.Printf as Printf  main :: IO ()-main = runTests makeTests--runTests :: Test.Test -> IO ()-runTests test = do-  Rattletrap.rattletrap-    ""-    ["--schema", "--output", FilePath.combine directory "schema.json"]-  (result, elapsed) <- withElapsed $ Test.runTestTT test-  Printf.printf "Total time: %.3f seconds\n" elapsed-  Monad.when-    (Test.errors result > 0 || Test.failures result > 0)-    Exit.exitFailure--makeTests :: Test.Test-makeTests = Test.TestList $ fmap toTest replays--toTest :: (String, String) -> Test.Test-toTest (uuid, name) =-  Test.TestLabel (toLabel uuid name) . Test.TestCase $ toAssertion uuid+main = do+  let directory = "output"+  generateSchema directory+  mapM_ (testReplay directory) replays -toLabel :: String -> String -> String-toLabel uuid name = uuid <> ": " <> name+generateSchema :: FilePath -> IO ()+generateSchema directory = Rattletrap.rattletrap+  ""+  ["--schema", "--output", FilePath.combine directory "schema.json"] -toAssertion :: String -> Test.Assertion-toAssertion uuid = do-  let-    inputFile = FilePath.combine "replays" $ uuid <> ".replay"-    jsonFile = FilePath.combine directory $ uuid <> ".json"-    outputFile = FilePath.combine directory $ uuid <> ".replay"+testReplay :: FilePath -> (FilePath, String) -> IO ()+testReplay directory (uuid, name) = do+  let inputFile = FilePath.combine "replays" $ uuid <> ".replay"   input <- ByteString.readFile inputFile-  decode inputFile jsonFile-  encode jsonFile outputFile+  let mb = fromIntegral (ByteString.length input) / (1024 * 1024 :: Double)+  Printf.printf "- replay: %s %s (%.3f mb)\n" uuid name mb+  let jsonFile = FilePath.combine directory $ uuid <> ".json"+  do+    (s, ()) <- withDuration $ decode inputFile jsonFile+    Printf.printf "  decode: %.3f s @ %.3f mb/s\n" s (mb / s)+  let outputFile = FilePath.combine directory $ uuid <> ".replay"+  do+    (s, ()) <- withDuration $ encode jsonFile outputFile+    Printf.printf "  encode: %.3f s @ %.3f mb/s\n" s (mb / s)   output <- ByteString.readFile outputFile-  Monad.when (output /= input)-    $ Test.assertFailure "output does not match input"+  Monad.when (output /= input) $ fail "output does not match input" +withDuration :: IO a -> IO (Double, a)+withDuration action = do+  before <- Clock.getMonotonicTime+  result <- action+  after <- Clock.getMonotonicTime+  pure (after - before, result)+ decode :: FilePath -> FilePath -> IO () decode input output =   Rattletrap.rattletrap "" ["--compact", "--input", input, "--output", output]@@ -52,121 +48,111 @@ encode input output =   Rattletrap.rattletrap "" ["--input", input, "--output", output] -withElapsed :: IO a -> IO (a, Double)-withElapsed action = do-  before <- Clock.getMonotonicTime-  result <- action-  after <- Clock.getMonotonicTime-  pure (result, after - before)--directory :: FilePath-directory = "output"- replays :: [(String, String)] replays =-  [ ("0008", "a flip time")-  , ("000b", "nintendo switch")-  , ("0416", "v1.78 demolition")-  , ("07e9", "a game mode before Neo Tokyo")-  , ("0ad2", "some Latin-1 text")-  , ("0e76", "v1.95 rumble")-  , ("1205", "rumble mode")-  , ("160c", "a dedicated server IP")-  , ("16d5", "new property types")-  , ("18d6", "an online loadout attribute")-  , ("1a12", "overtime")-  , ("1ae4", "a game time")-  , ("1bc2", "no padding after the frames")-  , ("1d1d", "a camera pitch")-  , ("1ec9", "a V1.63 match")-  , ("1ef9", "a private hoops match")-  , ("1f37", "splitscreen players")-  , ("2114", "a match save")-  , ("21a8", "v1.66")-  , ("2266", "dropshot")-  , ("22ba", "a vote to forfeit")-  , ("27b6", "some UTF-16 text")-  , ("29f5", "frames")-  , ("2cf8", "demo v2.01")-  , ("2cfe", "a new playstation id")-  , ("3381", "patch 1.37")-  , ("35f6", "heatseeker v2.01")-  , ("372d", "a camera yaw attribute")-  , ("383e", "older unknown content field")-  , ("387f", "a frozen attribute")-  , ("3abd", "rlcs")-  , ("3ea1", "a custom team name")-  , ("4126", "a game mode after Neo Tokyo")-  , ("419a", "a club match")-  , ("42f0", "reservations after Neo Tokyo")-  , ("42f2", "anniversary ball")-  , ("4bc3", "with timed out attribute")-  , ("504e", "some messages")-  , ("520e", "no pickup attribute")-  , ("524f", "quat edge case")-  , ("52aa", "a match-ending attribute")-  , ("540d", "a demolish attribute")-  , ("551c", "private match settings")-  , ("5a06", "esports items")-  , ("6210", "different player history key")-  , ("6320", "a forfeit attribute")-  , ("6688", "a malformed byte property")-  , ("6b0d", "patch 1.37")-  , ("6d1b", "a flip right")-  , ("6f7c", "a map with numbers")-  , ("7083", "weird basketball capitalization")-  , ("7109", "a boost modifier")-  , ("7256", "special edition")-  , ("75ce", "primary and secondary titles")-  , ("7bf6", "an online loadouts attribute")-  , ("81d1", "gridiron")-  , ("89cb", "remote user data")-  , ("8ae5", "new painted items")-  , ("92a6", "with server performance state")-  , ("946f", "patch 1.43")-  , ("9704", "a batarang")-  , ("98e5", "a player using behind view")-  , ("9a2c", "ghost hunt")-  , ("9e35", "spike rush")-  , ("9eaa", "newer replay without trailing bytes")-  , ("a09e", "a tournament")-  , ("a128", "a round count down")-  , ("a184", "max score")-  , ("a1c0", "epic system id")-  , ("a52f", "some more mutators")-  , ("a558", "extended explosion data")-  , ("a671", "a waiting player")-  , ("a676", "new user color")-  , ("a7f0", "a ready attribute")-  , ("a9df", "salty shores patch 1.45")-  , ("aa70", "patch 1.50 - TitleID attribute")-  , ("afb1", "patch 1.37")-  , ("b9f9", "a party leader")-  , ("c14f", "some mutators")-  , ("c23b", "new psynet id")-  , ("c837", "a spectator")-  , ("cc4c", "after Starbase ARC")-  , ("d044", "hoops mutators")-  , ("d1d5", "v1.68")-  , ("d236", "rlcs s2")-  , ("d428", "a private hockey match")-  , ("d44c", "ranked tournament")-  , ("d52e", "psynet system id")-  , ("d7fb", "an explosion attribute")-  , ("d818", "heatseeker")-  , ("db70", "new lag indicator")-  , ("dcab", "weird ball attribute value")-  , ("dcb3", "a pawn type attribute")-  , ("dd14", "v1.88")-  , ("de56", "a problematic product attribute")-  , ("e80d", "unlimited time")-  , ("e978", "distracted")-  , ("eae3", "an actor/object ID collision")-  , ("eae8", "custom team colors")-  , ("edbb", "remote role")-  , ("f299", "a location attribute")-  , ("f7b9", "a hockey game event")-  , ("f811", "no frames")-  , ("fdc7", "an MVP")-  , ("ffb7", "with difficulty")+  [ ("0008", "a flip time") -- https://github.com/tfausak/rattletrap/commit/ee7afa0+  , ("000b", "nintendo switch") -- https://github.com/tfausak/rattletrap/pull/60+  , ("0416", "v1.78 demolition") -- https://github.com/tfausak/rattletrap/pull/164+  , ("07e9", "a game mode before Neo Tokyo") -- https://github.com/tfausak/rattletrap/commit/b806f9b+  , ("0ad2", "some Latin-1 text") -- https://github.com/tfausak/rattletrap/commit/13a8b2d+  , ("0e76", "v1.95 rumble") -- https://github.com/tfausak/rattletrap/pull/237+  , ("1205", "rumble mode") -- https://github.com/tfausak/rattletrap/commit/5256500+  , ("160c", "a dedicated server IP") -- https://github.com/tfausak/rattletrap/commit/5c64a6d+  , ("16d5", "new property types") -- https://github.com/tfausak/rattletrap/pull/41+  , ("18d6", "an online loadout attribute") -- https://github.com/tfausak/rattletrap/commit/a9900e7+  , ("1a12", "overtime") -- https://github.com/tfausak/rattletrap/commit/ada6053+  , ("1ae4", "a game time") -- https://github.com/tfausak/rattletrap/commit/d08176e+  , ("1bc2", "no padding after the frames") -- https://github.com/tfausak/rattletrap/commit/c9a2dd8+  , ("1d1d", "a camera pitch") -- https://github.com/tfausak/rattletrap/commit/ee7afa0+  , ("1ec9", "a V1.63 match") -- https://github.com/tfausak/rattletrap/pull/132+  , ("1ef9", "a private hoops match") -- https://github.com/tfausak/rattletrap/commit/5570839+  , ("1f37", "splitscreen players") -- https://github.com/tfausak/rattletrap/commit/c4d2f32+  , ("2114", "a match save") -- https://github.com/tfausak/rattletrap/commit/ee7afa0+  , ("21a8", "v1.66") -- https://github.com/tfausak/rattletrap/pull/142+  , ("2266", "dropshot") -- https://github.com/tfausak/rattletrap/pull/36+  , ("22ba", "a vote to forfeit") -- https://github.com/tfausak/rattletrap/commit/86656bb+  , ("27b6", "some UTF-16 text") -- https://github.com/tfausak/rattletrap/commit/c4d2f32+  , ("29f5", "frames") -- https://github.com/tfausak/rattletrap/commit/bf4b6af+  , ("2cf8", "demo v2.01") -- https://github.com/tfausak/rattletrap/pull/243+  , ("2cfe", "a new playstation id") -- https://github.com/tfausak/rattletrap/issues/51+  , ("3381", "patch 1.37") -- https://github.com/tfausak/rattletrap/pull/48+  , ("35f6", "heatseeker v2.01") -- https://github.com/tfausak/rattletrap/commit/57d86c2+  , ("372d", "a camera yaw attribute") -- https://github.com/tfausak/rattletrap/commit/9c1516c+  , ("383e", "older unknown content field") -- https://github.com/tfausak/rattletrap/pull/123+  , ("387f", "a frozen attribute") -- https://github.com/tfausak/rattletrap/commit/93ce196+  , ("3abd", "rlcs") -- https://github.com/tfausak/rattletrap/pull/86+  , ("3ea1", "a custom team name") -- https://github.com/tfausak/rattletrap/commit/cf4d145+  , ("4126", "a game mode after Neo Tokyo") -- https://github.com/tfausak/rattletrap/commit/a1cf21e+  , ("419a", "a club match") -- https://github.com/tfausak/rattletrap/commit/8e35043+  , ("42f0", "reservations after Neo Tokyo") -- https://github.com/tfausak/rattletrap/commit/163684f+  , ("42f2", "anniversary ball") -- https://github.com/tfausak/rattletrap/issues/147+  , ("4bc3", "with timed out attribute") -- https://github.com/tfausak/rattletrap/pull/98+  , ("504e", "some messages") -- https://github.com/tfausak/rattletrap/commit/1d4a538+  , ("520e", "no pickup attribute") -- https://github.com/tfausak/rattletrap/pull/38+  , ("524f", "quat edge case") -- https://github.com/tfausak/rattletrap/pull/87+  , ("52aa", "a match-ending attribute") -- https://github.com/tfausak/rattletrap/commit/5c64a6d+  , ("540d", "a demolish attribute") -- https://github.com/tfausak/rattletrap/commit/65ce033+  , ("551c", "private match settings") -- https://github.com/tfausak/rattletrap/commit/5c9ebfc+  , ("5a06", "esports items") -- https://github.com/tfausak/rattletrap/pull/114+  , ("6210", "different player history key") -- https://github.com/tfausak/rattletrap/pull/63+  , ("6320", "a forfeit attribute") -- https://github.com/tfausak/rattletrap/pull/20+  , ("6688", "a malformed byte property") -- https://github.com/tfausak/rattletrap/commit/b1ec18b+  , ("6b0d", "patch 1.37") -- https://github.com/tfausak/rattletrap/pull/48+  , ("6d1b", "a flip right") -- https://github.com/tfausak/rattletrap/commit/ee7afa0+  , ("6f7c", "a map with numbers") -- https://github.com/tfausak/rattletrap/commit/2629511+  , ("7083", "weird basketball capitalization") -- https://github.com/tfausak/rattletrap/pull/63+  , ("7109", "a boost modifier") -- https://github.com/tfausak/rattletrap/commit/ee7afa0+  , ("7256", "special edition") -- https://github.com/tfausak/rattletrap/pull/103+  , ("75ce", "primary and secondary titles") -- https://github.com/tfausak/rattletrap/pull/69+  , ("7bf6", "an online loadouts attribute") -- https://github.com/tfausak/rattletrap/commit/89d02f7+  , ("81d1", "gridiron") -- https://github.com/tfausak/rattletrap/pull/180+  , ("89cb", "remote user data") -- https://github.com/tfausak/rattletrap/commit/163684f+  , ("8ae5", "new painted items") -- https://github.com/tfausak/rattletrap/pull/43+  , ("92a6", "with server performance state") -- https://github.com/tfausak/rattletrap/pull/93+  , ("946f", "patch 1.43") -- https://github.com/tfausak/rattletrap/pull/69+  , ("9704", "a batarang") -- https://github.com/tfausak/rattletrap/commit/5958e5c+  , ("98e5", "a player using behind view") -- https://github.com/tfausak/rattletrap/commit/163684f+  , ("9a2c", "ghost hunt") -- https://github.com/tfausak/rattletrap/pull/160+  , ("9e35", "spike rush") -- https://github.com/tfausak/rattletrap/pull/160+  , ("9eaa", "newer replay without trailing bytes") -- https://github.com/tfausak/rattletrap/pull/126+  , ("a09e", "a tournament") -- https://github.com/tfausak/rattletrap/pull/69+  , ("a128", "a round count down") -- https://github.com/tfausak/rattletrap/commit/8bb778d+  , ("a184", "max score") -- https://github.com/tfausak/rattletrap/pull/158+  , ("a1c0", "epic system id") -- https://github.com/tfausak/rattletrap/pull/167+  , ("a52f", "some more mutators") -- https://github.com/tfausak/rattletrap/commit/ee7afa0+  , ("a558", "extended explosion data") -- https://github.com/tfausak/rattletrap/pull/44+  , ("a671", "a waiting player") -- https://github.com/tfausak/rattletrap/commit/163684f+  , ("a676", "new user color") -- https://github.com/tfausak/rattletrap/pull/93+  , ("a7f0", "a ready attribute") -- https://github.com/tfausak/rattletrap/commit/78af1fd+  , ("a9df", "salty shores patch 1.45") -- https://github.com/tfausak/rattletrap/pull/78+  , ("aa70", "patch 1.50 - TitleID attribute") -- https://github.com/tfausak/rattletrap/pull/93+  , ("afb1", "patch 1.37") -- https://github.com/tfausak/rattletrap/pull/48+  , ("b9f9", "a party leader") -- https://github.com/tfausak/rattletrap/commit/bba2cfd+  , ("c14f", "some mutators") -- https://github.com/tfausak/rattletrap/commit/bba2cfd+  , ("c23b", "new psynet id") -- https://github.com/tfausak/rattletrap/pull/118+  , ("c837", "a spectator") -- https://github.com/tfausak/rattletrap/commit/bba2cfd+  , ("cc4c", "after Starbase ARC") -- https://github.com/tfausak/rattletrap/pull/20+  , ("d044", "hoops mutators") -- https://github.com/tfausak/rattletrap/pull/34+  , ("d1d5", "v1.68") -- https://github.com/tfausak/rattletrap/pull/146+  , ("d236", "rlcs s2") -- https://github.com/tfausak/rattletrap/pull/88+  , ("d428", "a private hockey match") -- https://github.com/tfausak/rattletrap/commit/4c104b2+  , ("d44c", "ranked tournament") -- https://github.com/tfausak/rattletrap/pull/167+  , ("d52e", "psynet system id") -- https://github.com/tfausak/rattletrap/pull/99+  , ("d7fb", "an explosion attribute") -- https://github.com/tfausak/rattletrap/commit/c554e3e+  , ("d818", "heatseeker") -- https://github.com/tfausak/rattletrap/pull/160+  , ("db70", "new lag indicator") -- https://github.com/tfausak/rattletrap/pull/69+  , ("dcab", "weird ball attribute value") -- https://github.com/tfausak/rattletrap/issues/149+  , ("dcb3", "a pawn type attribute") -- https://github.com/tfausak/rattletrap/commit/7d7f438+  , ("dd14", "v1.88") -- https://github.com/tfausak/rattletrap/pull/170+  , ("de56", "a problematic product attribute") -- https://github.com/tfausak/rattletrap/issues/51+  , ("e80d", "unlimited time") -- https://github.com/tfausak/rattletrap/pull/76+  , ("e978", "distracted") -- https://github.com/tfausak/rattletrap/issues/156+  , ("eae3", "an actor/object ID collision") -- https://github.com/tfausak/rattletrap/commit/d8fad06+  , ("eae8", "custom team colors") -- https://github.com/tfausak/rattletrap/commit/809240f+  , ("edbb", "remote role") -- https://github.com/tfausak/rattletrap/pull/106+  , ("f299", "a location attribute") -- https://github.com/tfausak/rattletrap/commit/21b09c5+  , ("f7b9", "a hockey game event") -- https://github.com/tfausak/rattletrap/commit/3e16d7f+  , ("f811", "no frames") -- https://github.com/tfausak/rattletrap/commit/bf4b6af+  , ("fdc7", "an MVP") -- https://github.com/tfausak/rattletrap/commit/65019d2+  , ("ffb7", "with difficulty") -- https://github.com/tfausak/rattletrap/pull/167   ]