From 51bddf03acc81ef31bf666f4ec9a49850ed55f2e Mon Sep 17 00:00:00 2001 From: Taylor Fausak Date: Mon, 28 Oct 2024 20:30:31 -0500 Subject: [PATCH] Handle new struct property (#312) * Handle new struct property * Update README * Update schemas * Require newer `data-default-class` * Fix dictionary schema * Fix dictionary schema again --- README.md | 6 +-- cabal.project | 3 ++ rattletrap.cabal | 1 + replays/3bd0.replay | Bin 0 -> 48734 bytes src/lib/Rattletrap/Console/Main.hs | 3 ++ src/lib/Rattletrap/Type/Dictionary.hs | 50 +++++++-------------- src/lib/Rattletrap/Type/Property.hs | 20 ++++++--- src/lib/Rattletrap/Type/Property/Byte.hs | 22 ++++++--- src/lib/Rattletrap/Type/Property/Struct.hs | 46 +++++++++++++++++++ src/lib/Rattletrap/Type/PropertyValue.hs | 11 ++++- src/lib/Rattletrap/Type/Replay.hs | 4 +- src/test/Main.hs | 1 + 12 files changed, 113 insertions(+), 54 deletions(-) create mode 100644 replays/3bd0.replay create mode 100644 src/lib/Rattletrap/Type/Property/Struct.hs diff --git a/README.md b/README.md index 2d7cfcde..1a9c9a86 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ and points, or low-level details like positions and cameras. Generating replays can be used to modify replays in order to force everyone into the same car or change the map a game was played on. -Rattletrap supports every version of Rocket League up to [2.43][], which was -released on 2024-04-16. If a replay can be played by the Rocket League client, +Rattletrap supports every version of Rocket League up to [2.45][], which was +released on 2024-10-22. If a replay can be played by the Rocket League client, it can be parsed by Rattletrap. (If not, that's a bug. Please report it!) ## Install @@ -127,6 +127,6 @@ $ rattletrap -i input.replay | [Rattletrap]: https://github.com/tfausak/rattletrap [Rocket League]: https://www.rocketleague.com -[2.43]: https://www.rocketleague.com/en/news/patch-notes-v2-43 +[2.45]: https://www.rocketleague.com/en/news/patch-notes-v2-45 [Ball Chasing]: https://ballchasing.com [the latest release]: https://github.com/tfausak/rattletrap/releases/latest diff --git a/cabal.project b/cabal.project index e6fdbadb..33a84bbe 100644 --- a/cabal.project +++ b/cabal.project @@ -1 +1,4 @@ packages: . +constraints: + -- https://github.com/snoyberg/http-client/issues/547 + data-default-class >= 0.2 diff --git a/rattletrap.cabal b/rattletrap.cabal index d530b413..9e4ed0c3 100644 --- a/rattletrap.cabal +++ b/rattletrap.cabal @@ -178,6 +178,7 @@ library Rattletrap.Type.Property.Name Rattletrap.Type.Property.QWord Rattletrap.Type.Property.Str + Rattletrap.Type.Property.Struct Rattletrap.Type.PropertyValue Rattletrap.Type.Quaternion Rattletrap.Type.RemoteId diff --git a/replays/3bd0.replay b/replays/3bd0.replay new file mode 100644 index 0000000000000000000000000000000000000000..c3448a78f4c90acdb6024a829c7bd861b36d6507 GIT binary patch literal 48734 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 literal 0 HcmV?d00001 diff --git a/src/lib/Rattletrap/Console/Main.hs b/src/lib/Rattletrap/Console/Main.hs index 9edcf5dd..71c2e290 100644 --- a/src/lib/Rattletrap/Console/Main.hs +++ b/src/lib/Rattletrap/Console/Main.hs @@ -81,6 +81,7 @@ import qualified Rattletrap.Type.Message as Message import qualified Rattletrap.Type.Property as Property import qualified Rattletrap.Type.Property.Array as Property.Array import qualified Rattletrap.Type.Property.Byte as Property.Byte +import qualified Rattletrap.Type.Property.Struct as Property.Struct import qualified Rattletrap.Type.PropertyValue as PropertyValue import qualified Rattletrap.Type.Quaternion as Quaternion import qualified Rattletrap.Type.RemoteId as RemoteId @@ -223,6 +224,7 @@ schema = CompressedWord.schema, CompressedWordVector.schema, contentSchema, + Dictionary.elementSchema Property.schema, Dictionary.schema Property.schema, F32.schema, Frame.schema, @@ -239,6 +241,7 @@ schema = Property.schema, Property.Array.schema Property.schema, Property.Byte.schema, + Property.Struct.schema Property.schema, PropertyValue.schema Property.schema, Quaternion.schema, RemoteId.schema, diff --git a/src/lib/Rattletrap/Type/Dictionary.hs b/src/lib/Rattletrap/Type/Dictionary.hs index 44e681f0..80804baa 100644 --- a/src/lib/Rattletrap/Type/Dictionary.hs +++ b/src/lib/Rattletrap/Type/Dictionary.hs @@ -1,7 +1,5 @@ module Rattletrap.Type.Dictionary where -import qualified Data.Bifunctor as Bifunctor -import qualified Data.Map as Map import qualified Data.Text as Text import qualified Rattletrap.ByteGet as ByteGet import qualified Rattletrap.BytePut as BytePut @@ -18,49 +16,31 @@ data Dictionary a = Dictionary instance (Json.FromJSON a) => Json.FromJSON (Dictionary a) where parseJSON = Json.withObject "Dictionary" $ \o -> do - keys <- Json.required o "keys" - lastKey_ <- Json.required o "last_key" - value <- Json.required o "value" - let build :: - (MonadFail m) => - Map.Map Text.Text a -> - Int -> - [(Int, (Str.Str, a))] -> - [Text.Text] -> - m (RList.List (Str.Str, a)) - build m i xs ks = case ks of - [] -> pure . RList.fromList . reverse $ fmap snd xs - k : t -> case Map.lookup k m of - Nothing -> fail $ "missing required key " <> show k - Just v -> build m (i + 1) ((i, (Str.fromText k, v)) : xs) t - elements_ <- build value 0 [] keys - pure Dictionary {elements = elements_, lastKey = lastKey_} + elements <- Json.required o "elements" + lastKey <- Json.required o "last_key" + pure Dictionary {elements = elements, lastKey = lastKey} instance (Json.ToJSON a) => Json.ToJSON (Dictionary a) where toJSON x = Json.object - [ Json.pair "keys" . fmap fst . RList.toList $ elements x, - Json.pair "last_key" $ lastKey x, - Json.pair "value" - . Map.fromList - . fmap (Bifunctor.first Str.toText) - . RList.toList - $ elements x + [ Json.pair "elements" . RList.toList $ elements x, + Json.pair "last_key" $ lastKey x ] schema :: Schema.Schema -> Schema.Schema schema s = Schema.named ("dictionary-" <> Text.unpack (Schema.name s)) $ Schema.object - [ (Json.pair "keys" . Schema.json $ Schema.array Str.schema, True), - (Json.pair "last_key" $ Schema.ref Str.schema, True), - ( Json.pair "value" $ - Json.object - [ Json.pair "type" "object", - Json.pair "additionalProperties" $ Schema.ref s - ], - True - ) + [ (Json.pair "elements" . Schema.json . Schema.array $ elementSchema s, True), + (Json.pair "last_key" $ Schema.ref Str.schema, True) + ] + +elementSchema :: Schema.Schema -> Schema.Schema +elementSchema s = + Schema.named ("dictionary-element-" <> Text.unpack (Schema.name s)) $ + Schema.tuple + [ Schema.ref Str.schema, + Schema.ref s ] lookup :: Str.Str -> Dictionary a -> Maybe a diff --git a/src/lib/Rattletrap/Type/Property.hs b/src/lib/Rattletrap/Type/Property.hs index 4e007822..3ba90a8c 100644 --- a/src/lib/Rattletrap/Type/Property.hs +++ b/src/lib/Rattletrap/Type/Property.hs @@ -5,13 +5,14 @@ import qualified Rattletrap.BytePut as BytePut import qualified Rattletrap.Schema as Schema import qualified Rattletrap.Type.PropertyValue as PropertyValue import qualified Rattletrap.Type.Str as Str -import qualified Rattletrap.Type.U64 as U64 +import qualified Rattletrap.Type.U32 as U32 import qualified Rattletrap.Utility.Json as Json data Property = Property { kind :: Str.Str, -- | Not used. - size :: U64.U64, + size :: U32.U32, + index :: U32.U32, value :: PropertyValue.PropertyValue Property } deriving (Eq, Show) @@ -20,14 +21,16 @@ instance Json.FromJSON Property where parseJSON = Json.withObject "Property" $ \object -> do kind <- Json.required object "kind" size <- Json.required object "size" + index <- Json.required object "index" value <- Json.required object "value" - pure Property {kind, size, value} + pure Property {kind, size, index, value} instance Json.ToJSON Property where toJSON x = Json.object [ Json.pair "kind" $ kind x, Json.pair "size" $ size x, + Json.pair "index" $ index x, Json.pair "value" $ value x ] @@ -36,14 +39,16 @@ schema = Schema.named "property" $ Schema.object [ (Json.pair "kind" $ Schema.ref Str.schema, True), - (Json.pair "size" $ Schema.ref U64.schema, True), + (Json.pair "size" $ Schema.ref U32.schema, True), + (Json.pair "index" $ Schema.ref U32.schema, True), (Json.pair "value" . Schema.ref $ PropertyValue.schema schema, True) ] bytePut :: Property -> BytePut.BytePut bytePut x = Str.bytePut (kind x) - <> U64.bytePut (size x) + <> U32.bytePut (size x) + <> U32.bytePut (index x) <> PropertyValue.bytePut bytePut (value x) @@ -51,6 +56,7 @@ bytePut x = byteGet :: ByteGet.ByteGet Property byteGet = ByteGet.label "Property" $ do kind <- ByteGet.label "kind" Str.byteGet - size <- ByteGet.label "size" U64.byteGet + size <- ByteGet.label "size" U32.byteGet + index <- ByteGet.label "index" U32.byteGet value <- ByteGet.label "value" $ PropertyValue.byteGet byteGet kind - pure Property {kind, size, value} + pure Property {kind, size, index, value} diff --git a/src/lib/Rattletrap/Type/Property/Byte.hs b/src/lib/Rattletrap/Type/Property/Byte.hs index 5c837f15..17549951 100644 --- a/src/lib/Rattletrap/Type/Property/Byte.hs +++ b/src/lib/Rattletrap/Type/Property/Byte.hs @@ -4,12 +4,12 @@ import qualified Rattletrap.ByteGet as ByteGet import qualified Rattletrap.BytePut as BytePut import qualified Rattletrap.Schema as Schema import qualified Rattletrap.Type.Str as Str +import qualified Rattletrap.Type.U8 as U8 import qualified Rattletrap.Utility.Json as Json -import qualified Rattletrap.Utility.Monad as Monad data Byte = Byte { key :: Str.Str, - value :: Maybe Str.Str + value :: Maybe (Either U8.U8 Str.Str) } deriving (Eq, Show) @@ -25,17 +25,29 @@ schema :: Schema.Schema schema = Schema.named "property-byte" $ Schema.tuple - [Schema.ref Str.schema, Schema.json $ Schema.maybe Str.schema] + [ Schema.ref Str.schema, + Schema.oneOf + [ Schema.ref Schema.null, + Schema.object [(Json.pair "Left" $ Schema.ref U8.schema, True)], + Schema.object [(Json.pair "Right" $ Schema.ref Str.schema, True)] + ] + ] bytePut :: Byte -> BytePut.BytePut -bytePut byte = Str.bytePut (key byte) <> foldMap Str.bytePut (value byte) +bytePut byte = Str.bytePut (key byte) <> foldMap (either U8.bytePut Str.bytePut) (value byte) byteGet :: ByteGet.ByteGet Byte byteGet = ByteGet.label "Byte" $ do key <- ByteGet.label "key" Str.byteGet let isSteam = key == Str.fromString "OnlinePlatform_Steam" isPlayStation = key == Str.fromString "OnlinePlatform_PS4" + isNone = key == Str.fromString "None" value <- ByteGet.label "value" $ - Monad.whenMaybe (not $ isSteam || isPlayStation) Str.byteGet + if isSteam || isPlayStation + then pure Nothing + else + if isNone + then Just . Left <$> U8.byteGet + else Just . Right <$> Str.byteGet pure Byte {key, value} diff --git a/src/lib/Rattletrap/Type/Property/Struct.hs b/src/lib/Rattletrap/Type/Property/Struct.hs new file mode 100644 index 00000000..7bfcfb40 --- /dev/null +++ b/src/lib/Rattletrap/Type/Property/Struct.hs @@ -0,0 +1,46 @@ +module Rattletrap.Type.Property.Struct where + +import qualified Rattletrap.ByteGet as ByteGet +import qualified Rattletrap.BytePut as BytePut +import qualified Rattletrap.Schema as Schema +import qualified Rattletrap.Type.Dictionary as Dictionary +import qualified Rattletrap.Type.Str as Str +import qualified Rattletrap.Utility.Json as Json + +data Struct a = Struct + { name :: Str.Str, + fields :: Dictionary.Dictionary a + } + deriving (Eq, Show) + +instance (Json.FromJSON a) => Json.FromJSON (Struct a) where + parseJSON = Json.withObject "Struct" $ \o -> do + name <- Json.required o "name" + fields <- Json.required o "fields" + pure Struct {name, fields} + +instance (Json.ToJSON a) => Json.ToJSON (Struct a) where + toJSON x = + Json.object + [ Json.pair "name" $ name x, + Json.pair "fields" $ fields x + ] + +schema :: Schema.Schema -> Schema.Schema +schema s = + Schema.named "property-struct" $ + Schema.object + [ (Json.pair "name" $ Schema.ref Str.schema, True), + (Json.pair "fields" $ Schema.ref (Dictionary.schema s), True) + ] + +bytePut :: (a -> BytePut.BytePut) -> Struct a -> BytePut.BytePut +bytePut p x = + Str.bytePut (name x) + <> Dictionary.bytePut p (fields x) + +byteGet :: ByteGet.ByteGet a -> ByteGet.ByteGet (Struct a) +byteGet g = ByteGet.label "Struct" $ do + name <- ByteGet.label "name" Str.byteGet + fields <- ByteGet.label "fields" $ Dictionary.byteGet g + pure Struct {name, fields} diff --git a/src/lib/Rattletrap/Type/PropertyValue.hs b/src/lib/Rattletrap/Type/PropertyValue.hs index 0ca61fd4..4fac12fc 100644 --- a/src/lib/Rattletrap/Type/PropertyValue.hs +++ b/src/lib/Rattletrap/Type/PropertyValue.hs @@ -13,6 +13,7 @@ import qualified Rattletrap.Type.Property.Int as Property.Int import qualified Rattletrap.Type.Property.Name as Property.Name import qualified Rattletrap.Type.Property.QWord as Property.QWord import qualified Rattletrap.Type.Property.Str as Property.Str +import qualified Rattletrap.Type.Property.Struct as Property.Struct import qualified Rattletrap.Type.Str as Str import qualified Rattletrap.Utility.Json as Json @@ -29,6 +30,7 @@ data PropertyValue a Name Property.Name.Name | QWord Property.QWord.QWord | Str Property.Str.Str + | Struct (Property.Struct.Struct a) deriving (Eq, Show) instance (Json.FromJSON a) => Json.FromJSON (PropertyValue a) where @@ -41,7 +43,8 @@ instance (Json.FromJSON a) => Json.FromJSON (PropertyValue a) where fmap Int $ Json.required object "int", fmap Name $ Json.required object "name", fmap QWord $ Json.required object "q_word", - fmap Str $ Json.required object "str" + fmap Str $ Json.required object "str", + fmap Struct $ Json.required object "struct" ] instance (Json.ToJSON a) => Json.ToJSON (PropertyValue a) where @@ -54,6 +57,7 @@ instance (Json.ToJSON a) => Json.ToJSON (PropertyValue a) where Name y -> Json.object [Json.pair "name" y] QWord y -> Json.object [Json.pair "q_word" y] Str y -> Json.object [Json.pair "str" y] + Struct y -> Json.object [Json.pair "struct" y] schema :: Schema.Schema -> Schema.Schema schema s = @@ -67,7 +71,8 @@ schema s = ("int", Schema.ref Property.Int.schema), ("name", Schema.ref Property.Name.schema), ("q_word", Schema.ref Property.QWord.schema), - ("str", Schema.ref Property.Str.schema) + ("str", Schema.ref Property.Str.schema), + ("struct", Schema.ref $ Property.Struct.schema s) ] bytePut :: (a -> BytePut.BytePut) -> PropertyValue a -> BytePut.BytePut @@ -80,6 +85,7 @@ bytePut putProperty value = case value of Name x -> Property.Name.bytePut x QWord x -> Property.QWord.bytePut x Str x -> Property.Str.bytePut x + Struct x -> Property.Struct.bytePut putProperty x byteGet :: ByteGet.ByteGet a -> Str.Str -> ByteGet.ByteGet (PropertyValue a) byteGet getProperty kind = @@ -92,4 +98,5 @@ byteGet getProperty kind = "NameProperty" -> fmap Name Property.Name.byteGet "QWordProperty" -> fmap QWord Property.QWord.byteGet "StrProperty" -> fmap Str Property.Str.byteGet + "StructProperty" -> fmap Struct $ Property.Struct.byteGet getProperty x -> ByteGet.throw $ UnknownProperty.UnknownProperty x diff --git a/src/lib/Rattletrap/Type/Replay.hs b/src/lib/Rattletrap/Type/Replay.hs index d84eb22e..447c5659 100644 --- a/src/lib/Rattletrap/Type/Replay.hs +++ b/src/lib/Rattletrap/Type/Replay.hs @@ -110,7 +110,7 @@ getNumFrames header_ = case Dictionary.lookup (Str.fromString "NumFrames") (Header.properties header_) of - Just (Property.Property _ _ (PropertyValue.Int numFrames)) -> + Just (Property.Property _ _ _ (PropertyValue.Int numFrames)) -> fromIntegral (I32.toInt32 (Property.Int.toI32 numFrames)) _ -> 0 @@ -120,7 +120,7 @@ getMaxChannels header_ = case Dictionary.lookup (Str.fromString "MaxChannels") (Header.properties header_) of - Just (Property.Property _ _ (PropertyValue.Int maxChannels)) -> + Just (Property.Property _ _ _ (PropertyValue.Int maxChannels)) -> fromIntegral (I32.toInt32 (Property.Int.toI32 maxChannels)) _ -> 1023 diff --git a/src/test/Main.hs b/src/test/Main.hs index 09cd97fb..254bb705 100644 --- a/src/test/Main.hs +++ b/src/test/Main.hs @@ -87,6 +87,7 @@ replays = ("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 + ("3bd0", "v2.45"), -- https://github.com/tfausak/rattletrap/issues/311 ("3ea1", "a custom team name"), -- https://github.com/tfausak/rattletrap/commit/cf4d145 ("4050", "v2.08 dodge impulse"), -- https://github.com/tfausak/rattletrap/issues/247 ("4126", "a game mode after Neo Tokyo"), -- https://github.com/tfausak/rattletrap/commit/a1cf21e