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 +48/−37
- src/lib/Rattletrap/Console/Main.hs +1/−0
- src/lib/Rattletrap/Schema.hs +2/−2
- src/lib/Rattletrap/Type/RemoteId/PsyNet.hs +25/−25
- src/lib/Rattletrap/Utility/Json.hs +34/−3
- src/test/Main.hs +134/−148
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 ]