├── .github └── workflows │ └── release.yml ├── LICENSE ├── README.md ├── cmd ├── rbxfile-dcomp │ ├── README.md │ └── main.go ├── rbxfile-dump │ ├── README.md │ └── main.go └── rbxfile-stat │ ├── README.md │ └── main.go ├── declare ├── declare.go ├── declare_test.go └── type.go ├── errors └── errors.go ├── file.go ├── file_test.go ├── go.mod ├── go.sum ├── json └── json.go ├── rbxl ├── README.md ├── arrays.go ├── cframe.go ├── codec.go ├── decoder.go ├── dump.go ├── encoder.go ├── errors.go ├── format.go ├── format_test.go ├── model.go ├── model_test.go └── values.go ├── rbxlx ├── codec.go ├── document.go └── format.go ├── ref.go ├── value_checklist.md ├── values.go └── values_test.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create draft release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | env: 9 | PROJECT : 'rbxfile' # Name of project. 10 | COMMANDS : './cmd' # Root location of programs. 11 | GOVERSION : 1.18.4 # Version of Go to compile with. 12 | DIST : './dist' # Scratch directory for building executables. 13 | 14 | jobs: 15 | 16 | build: 17 | name: Build executables 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | include: 22 | - { os: 'windows' , arch: 'amd64' , command: 'rbxfile-dump' , output: './dist/rbxfile-dump.exe' } 23 | - { os: 'windows' , arch: '386' , command: 'rbxfile-dump' , output: './dist/rbxfile-dump.exe' } 24 | - { os: 'darwin' , arch: 'amd64' , command: 'rbxfile-dump' , output: './dist/rbxfile-dump' } 25 | - { os: 'linux' , arch: '386' , command: 'rbxfile-dump' , output: './dist/rbxfile-dump' } 26 | - { os: 'linux' , arch: 'amd64' , command: 'rbxfile-dump' , output: './dist/rbxfile-dump' } 27 | - { os: 'windows' , arch: 'amd64' , command: 'rbxfile-dcomp' , output: './dist/rbxfile-dcomp.exe' } 28 | - { os: 'windows' , arch: '386' , command: 'rbxfile-dcomp' , output: './dist/rbxfile-dcomp.exe' } 29 | - { os: 'darwin' , arch: 'amd64' , command: 'rbxfile-dcomp' , output: './dist/rbxfile-dcomp' } 30 | - { os: 'linux' , arch: '386' , command: 'rbxfile-dcomp' , output: './dist/rbxfile-dcomp' } 31 | - { os: 'linux' , arch: 'amd64' , command: 'rbxfile-dcomp' , output: './dist/rbxfile-dcomp' } 32 | - { os: 'windows' , arch: 'amd64' , command: 'rbxfile-stat' , output: './dist/rbxfile-stat.exe' } 33 | - { os: 'windows' , arch: '386' , command: 'rbxfile-stat' , output: './dist/rbxfile-stat.exe' } 34 | - { os: 'darwin' , arch: 'amd64' , command: 'rbxfile-stat' , output: './dist/rbxfile-stat' } 35 | - { os: 'linux' , arch: '386' , command: 'rbxfile-stat' , output: './dist/rbxfile-stat' } 36 | - { os: 'linux' , arch: 'amd64' , command: 'rbxfile-stat' , output: './dist/rbxfile-stat' } 37 | steps: 38 | - name: Checkout code 39 | uses: actions/checkout@v3 40 | 41 | - name: Set up Go 42 | uses: actions/setup-go@v3 43 | with: 44 | go-version: ${{env.GOVERSION}} 45 | 46 | - name: Set version variable 47 | run: echo VERSION=${GITHUB_REF#refs/tags/} >> $GITHUB_ENV 48 | 49 | - name: Make build directory 50 | run: mkdir ${{env.DIST}} 51 | 52 | - name: Build executable 53 | env: 54 | GOOS: ${{matrix.os}} 55 | GOARCH: ${{matrix.arch}} 56 | OUTPUT: ${{matrix.output}} 57 | run: go build -v -trimpath -tags="release" -o $OUTPUT ${{env.COMMANDS}}/${{matrix.command}} 58 | 59 | - name: Create archive 60 | id: archive 61 | env: 62 | GOOS: ${{matrix.os}} 63 | GOARCH: ${{matrix.arch}} 64 | OUTPUT: ${{matrix.output}} 65 | run: | 66 | NAME=${{matrix.command}}-$VERSION-$GOOS-$GOARCH 67 | ARCHIVE=${{env.DIST}}/$NAME.zip 68 | zip --junk-paths $ARCHIVE $OUTPUT 69 | echo ::set-output name=name::$NAME 70 | echo ::set-output name=path::$ARCHIVE 71 | 72 | - name: Upload executable 73 | uses: actions/upload-artifact@v3 74 | with: 75 | name: ${{steps.archive.outputs.name}} 76 | path: ${{steps.archive.outputs.path}} 77 | if-no-files-found: error 78 | retention-days: 1 79 | 80 | release: 81 | name: Create release 82 | needs: [build] 83 | runs-on: ubuntu-latest 84 | steps: 85 | - name: Set version variable 86 | run: echo VERSION=${GITHUB_REF#refs/tags/} >> $GITHUB_ENV 87 | 88 | - name: Download all archives 89 | id: download 90 | uses: actions/download-artifact@v3 91 | 92 | - name: Move files 93 | run: | 94 | mkdir files 95 | mv $(find ${{steps.download.outputs.download-path}} -iname *.zip) files 96 | 97 | - name: Check files 98 | run: find . 99 | 100 | - name: Checkout code 101 | uses: actions/checkout@v3 102 | with: 103 | path: repo 104 | 105 | - name: Create release 106 | uses: softprops/action-gh-release@v1 107 | env: 108 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 109 | with: 110 | name: ${{env.PROJECT}} ${{env.VERSION}} 111 | draft: true 112 | files: files/*.zip 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014 Anaminus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/robloxapi/rbxfile?status.png)](https://godoc.org/github.com/robloxapi/rbxfile) 2 | 3 | # rbxfile 4 | 5 | The rbxfile package handles the decoding, encoding, and manipulation of Roblox 6 | instance data structures. 7 | 8 | This package can be used to manipulate Roblox instance trees outside of the 9 | Roblox client. Such data structures begin with a [Root][root] struct. A Root 10 | contains a list of child [Instances][inst], which in turn contain more child 11 | Instances, and so on, forming a tree of Instances. These Instances can be 12 | accessed and manipulated using an API similar to that of Roblox. 13 | 14 | Each Instance also has a set of "properties". Each property has a specific 15 | value of a certain [type][type]. Every available type implements the 16 | [Value][value] interface, and is prefixed with "Value". 17 | 18 | Root structures can be decoded from and encoded to various formats, including 19 | Roblox's native file formats. The two sub-packages [rbxl][rbxl] and 20 | [rbxlx][rbxlx] provide formats for Roblox's binary and XML formats. Root 21 | structures can also be encoded and decoded with the [json][json] package. 22 | 23 | Besides decoding from a format, root structures can also be created manually. 24 | The best way to do this is through the [declare][declare] sub-package, which 25 | provides an easy way to generate root structures. 26 | 27 | [root]: https://godoc.org/github.com/robloxapi/rbxfile#Root 28 | [inst]: https://godoc.org/github.com/robloxapi/rbxfile#Instance 29 | [type]: https://godoc.org/github.com/robloxapi/rbxfile#Type 30 | [value]: https://godoc.org/github.com/robloxapi/rbxfile#Value 31 | [rbxl]: https://godoc.org/github.com/robloxapi/rbxfile/rbxl 32 | [rbxlx]: https://godoc.org/github.com/robloxapi/rbxfile/rbxlx 33 | [json]: https://godoc.org/encoding/json 34 | [declare]: https://godoc.org/github.com/robloxapi/rbxfile/declare 35 | 36 | ## Related 37 | The implementation of the binary file format is based largely on the 38 | [RobloxFileSpec][spec] document, a reverse-engineered specification by Gregory 39 | Comer. 40 | 41 | Other projects that involve decoding and encoding Roblox files: 42 | 43 | - [rbx-fmt](https://github.com/stravant/rbx-fmt): An implementation in C. 44 | - [LibRbxl](https://github.com/GregoryComer/LibRbxl): An implementation in C#. 45 | - [rbx-dom](https://github.com/LPGhatguy/rbx-dom): An implementation in Rust. 46 | - [Roblox-File-Format](https://github.com/CloneTrooper1019/Roblox-File-Format): 47 | An implementation in C#. 48 | 49 | [spec]: https://www.classy-studios.com/Downloads/RobloxFileSpec.pdf 50 | -------------------------------------------------------------------------------- /cmd/rbxfile-dcomp/README.md: -------------------------------------------------------------------------------- 1 | # rbxfile-dcomp 2 | The **rbxfile-dcomp** command rewrites the content of binary files (`.rbxl`, 3 | `.rbxm`) with decompressed chunks, allowing the content of such files to be 4 | analyzed more easily. 5 | 6 | ## Usage 7 | ```bash 8 | rbxfile-dcomp [INPUT] [OUTPUT] 9 | ``` 10 | 11 | Reads a binary RBXL or RBXM file from `INPUT`, and writes to `OUTPUT` the same 12 | file, but with uncompressed chunks. 13 | 14 | `INPUT` and `OUTPUT` are paths to files. If `INPUT` is "-" or unspecified, then 15 | stdin is used. If `OUTPUT` is "-" or unspecified, then stdout is used. Warnings 16 | and errors are written to stderr. 17 | -------------------------------------------------------------------------------- /cmd/rbxfile-dcomp/main.go: -------------------------------------------------------------------------------- 1 | // The rbxfile-dcomp command rewrites a rbxl/rbxm file with decompressed 2 | // chunks. 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "io" 9 | "os" 10 | 11 | "github.com/robloxapi/rbxfile/rbxl" 12 | ) 13 | 14 | const usage = `usage: rbxfile-dcomp [INPUT] [OUTPUT] 15 | 16 | Reads a binary RBXL or RBXM file from INPUT, and writes to OUTPUT the same file, 17 | but with uncompressed chunks. 18 | 19 | INPUT and OUTPUT are paths to files. If INPUT is "-" or unspecified, then stdin 20 | is used. If OUTPUT is "-" or unspecified, then stdout is used. Warnings and 21 | errors are written to stderr. 22 | ` 23 | 24 | func main() { 25 | var input io.Reader = os.Stdin 26 | var output io.Writer = os.Stdout 27 | 28 | flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), usage) } 29 | flag.Parse() 30 | args := flag.Args() 31 | if len(args) >= 1 && args[0] != "-" { 32 | in, err := os.Open(args[0]) 33 | if err != nil { 34 | fmt.Fprintln(os.Stderr, fmt.Errorf("open input: %w", err)) 35 | return 36 | } 37 | input = in 38 | defer in.Close() 39 | } 40 | if len(args) >= 2 && args[1] != "-" { 41 | out, err := os.Create(args[1]) 42 | if err != nil { 43 | fmt.Fprintln(os.Stderr, fmt.Errorf("create output: %w", err)) 44 | return 45 | } 46 | defer out.Close() 47 | defer func() { 48 | err := out.Sync() 49 | if err != nil { 50 | fmt.Fprintln(os.Stderr, fmt.Errorf("sync output: %w", err)) 51 | return 52 | } 53 | }() 54 | output = out 55 | } 56 | 57 | warn, err := rbxl.Decoder{}.Decompress(output, input) 58 | if warn != nil { 59 | fmt.Fprintln(os.Stderr, fmt.Errorf("warning: %w", warn)) 60 | } 61 | if err != nil { 62 | fmt.Fprintln(os.Stderr, fmt.Errorf("error: %w", warn)) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /cmd/rbxfile-dump/README.md: -------------------------------------------------------------------------------- 1 | # rbxfile-dump 2 | The **rbxfile-dump** command dumps the content of binary files (`.rbxl`, 3 | `.rbxm`) in a readable format. 4 | 5 | ## Usage 6 | ```bash 7 | rbxfile-dump [INPUT] [OUTPUT] 8 | ``` 9 | 10 | Reads a binary RBXL or RBXM file from `INPUT`, and dumps a human-readable 11 | representation of the binary format to `OUTPUT`. 12 | 13 | `INPUT` and `OUTPUT` are paths to files. If `INPUT` is "-" or unspecified, then 14 | stdin is used. If `OUTPUT` is "-" or unspecified, then stdout is used. Warnings 15 | and errors are written to stderr. 16 | 17 | If the command failed to parse a chunk of the input, then the raw bytes of the 18 | chunk will be dumped for further analysis by the user. 19 | -------------------------------------------------------------------------------- /cmd/rbxfile-dump/main.go: -------------------------------------------------------------------------------- 1 | // The rbxfile-dump command dumps the content of a rbxl/rbxm file. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | 10 | "github.com/robloxapi/rbxfile/rbxl" 11 | ) 12 | 13 | const usage = `usage: rbxfile-dump [INPUT] [OUTPUT] 14 | 15 | Reads a binary RBXL or RBXM file from INPUT, and dumps a human-readable 16 | representation of the binary format to OUTPUT. 17 | 18 | INPUT and OUTPUT are paths to files. If INPUT is "-" or unspecified, then stdin 19 | is used. If OUTPUT is "-" or unspecified, then stdout is used. Warnings and 20 | errors are written to stderr. 21 | ` 22 | 23 | func main() { 24 | var input io.Reader = os.Stdin 25 | var output io.Writer = os.Stdout 26 | 27 | flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), usage) } 28 | flag.Parse() 29 | args := flag.Args() 30 | if len(args) >= 1 && args[0] != "-" { 31 | in, err := os.Open(args[0]) 32 | if err != nil { 33 | fmt.Fprintln(os.Stderr, fmt.Errorf("open input: %w", err)) 34 | return 35 | } 36 | input = in 37 | defer in.Close() 38 | } 39 | if len(args) >= 2 && args[1] != "-" { 40 | out, err := os.Create(args[1]) 41 | if err != nil { 42 | fmt.Fprintln(os.Stderr, fmt.Errorf("create output: %w", err)) 43 | return 44 | } 45 | defer out.Close() 46 | defer func() { 47 | err := out.Sync() 48 | if err != nil { 49 | fmt.Fprintln(os.Stderr, fmt.Errorf("sync output: %w", err)) 50 | return 51 | } 52 | }() 53 | output = out 54 | } 55 | 56 | warn, err := rbxl.Decoder{}.Dump(output, input) 57 | if warn != nil { 58 | fmt.Fprintln(os.Stderr, fmt.Errorf("warning: %w", warn)) 59 | } 60 | if err != nil { 61 | fmt.Fprintln(os.Stderr, fmt.Errorf("error: %w", warn)) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /cmd/rbxfile-stat/README.md: -------------------------------------------------------------------------------- 1 | # rbxfile-stat 2 | The **rbxfile-stat** command returns statistics for a roblox file. The following 3 | formats are supported: 4 | - rbxl 5 | - rbxm 6 | - rbxlx 7 | - rbxmx 8 | 9 | ## Usage 10 | ```bash 11 | rbxfile-stat [INPUT] [OUTPUT] 12 | ``` 13 | 14 | Reads a RBXL, RBXM, RBXLX, or RBXMX file from `INPUT`, and writes to `OUTPUT` 15 | statistics for the file. 16 | 17 | `INPUT` and `OUTPUT` are paths to files. If `INPUT` is "-" or unspecified, then 18 | stdin is used. If `OUTPUT` is "-" or unspecified, then stdout is used. Warnings 19 | and errors are written to stderr. 20 | 21 | ## Output 22 | The output is in JSON format with the following structure: 23 | 24 | Field | Type | Description 25 | ------------------|----------------------------------------|------------ 26 | Format | [Format](#format) | Low-level stats for the file format. 27 | InstanceCount | int | Actual number of instances. 28 | PropertyCount | int | Number of individual properties across all instances. 29 | ClassCount | class -> int | Number of instances, per class. 30 | TypeCount | type -> int | Number of properties, per type. 31 | OptionalTypeCount | type -> int | Number of properties of the optional type, per inner type. 32 | LargestProperties | array of [PropertyStat](#propertystat) | List of top 20 longest properties. Counts string-like and sequence types. 33 | 34 | ### Format 35 | 36 | Field | Type | Description 37 | --------------|--------|------------ 38 | XML | bool | True if the format is XML, otherwise binary. 39 | Version | int | Version of the binary format. 40 | ClassCount | int | Number of classes reported by the binary format header. 41 | InstanceCount | int | Number of instances reported by the binary format header. 42 | Chunks | int | Total number of chunks in the binary format. 43 | Chunks | Chunks | Number of chunks per signature in the binary format. 44 | 45 | ### PropertyStat 46 | 47 | Field | Type | Description 48 | --------------|--------|------------ 49 | Class | string | The class name of the instance having this property. 50 | Property | string | The name of this property. 51 | Type | string | The type of this property. 52 | Length | int | The length of this property. 53 | -------------------------------------------------------------------------------- /cmd/rbxfile-stat/main.go: -------------------------------------------------------------------------------- 1 | // The rbxfile-stat command displays stats for a roblox file. 2 | package main 3 | 4 | import ( 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "os" 10 | "sort" 11 | 12 | "github.com/robloxapi/rbxfile" 13 | "github.com/robloxapi/rbxfile/rbxl" 14 | ) 15 | 16 | const usage = `usage: rbxfile-stat [INPUT] [OUTPUT] 17 | 18 | Reads a RBXL, RBXM, RBXLX, or RBXMX file from INPUT, and writes to OUTPUT 19 | statistics for the file. 20 | 21 | INPUT and OUTPUT are paths to files. If INPUT is "-" or unspecified, then stdin 22 | is used. If OUTPUT is "-" or unspecified, then stdout is used. Warnings and 23 | errors are written to stderr. 24 | ` 25 | 26 | type PropLen struct { 27 | Class string 28 | Property string 29 | Type string 30 | Length int 31 | } 32 | 33 | func (p PropLen) String() string { 34 | return fmt.Sprintf("%s.%s:%s(%d)", p.Class, p.Property, p.Type, p.Length) 35 | } 36 | 37 | type PropLenCount map[PropLen]int 38 | 39 | func (p PropLenCount) MarshalJSON() ([]byte, error) { 40 | list := []PropLen{} 41 | for k := range p { 42 | list = append(list, k) 43 | } 44 | sort.Slice(list, func(i, j int) bool { 45 | return list[i].Length > list[j].Length 46 | }) 47 | if len(list) > 20 { 48 | list = list[:20] 49 | } 50 | return json.Marshal(list) 51 | } 52 | 53 | type Stats struct { 54 | // Binary format data. 55 | Format rbxl.DecoderStats 56 | 57 | // Number of instances overall. 58 | InstanceCount int 59 | 60 | // Number of properties overall. 61 | PropertyCount int 62 | 63 | // Number of instances per class. 64 | ClassCount map[string]int 65 | 66 | // Number of properties per type. 67 | TypeCount map[string]int 68 | 69 | OptionalTypeCount map[string]int `json:",omitempty"` 70 | 71 | LargestProperties PropLenCount `json:",omitempty"` 72 | } 73 | 74 | const Okay = 0 75 | const ( 76 | Exit = 1 << iota 77 | SkipProperties 78 | SkipChildren 79 | ) 80 | 81 | func walk(instances []*rbxfile.Instance, cb func(inst *rbxfile.Instance, property string, value rbxfile.Value) int) (ok bool) { 82 | for _, inst := range instances { 83 | status := cb(inst, "", nil) 84 | if status&Exit != 0 { 85 | return false 86 | } 87 | if status&SkipProperties == 0 { 88 | for property, value := range inst.Properties { 89 | status := cb(inst, property, value) 90 | if status&Exit != 0 { 91 | return false 92 | } 93 | if status&SkipProperties != 0 { 94 | break 95 | } 96 | } 97 | } 98 | if status&SkipChildren == 0 { 99 | if ok := walk(inst.Children, cb); !ok { 100 | return false 101 | } 102 | } 103 | } 104 | return true 105 | } 106 | 107 | func (s *Stats) Fill(root *rbxfile.Root) { 108 | if root == nil { 109 | return 110 | } 111 | 112 | s.PropertyCount = 0 113 | walk(root.Instances, func(inst *rbxfile.Instance, property string, value rbxfile.Value) int { 114 | if value == nil { 115 | return Okay 116 | } 117 | s.PropertyCount++ 118 | return Okay 119 | }) 120 | 121 | s.InstanceCount = 0 122 | s.ClassCount = map[string]int{} 123 | walk(root.Instances, func(inst *rbxfile.Instance, property string, value rbxfile.Value) int { 124 | s.InstanceCount++ 125 | s.ClassCount[inst.ClassName]++ 126 | return SkipProperties 127 | }) 128 | 129 | s.TypeCount = map[string]int{} 130 | walk(root.Instances, func(inst *rbxfile.Instance, property string, value rbxfile.Value) int { 131 | if value == nil { 132 | return Okay 133 | } 134 | s.TypeCount[value.Type().String()]++ 135 | return Okay 136 | }) 137 | 138 | s.OptionalTypeCount = map[string]int{} 139 | walk(root.Instances, func(inst *rbxfile.Instance, property string, value rbxfile.Value) int { 140 | if value == nil { 141 | return Okay 142 | } 143 | opt, ok := value.(rbxfile.ValueOptional) 144 | if !ok { 145 | return Okay 146 | } 147 | s.OptionalTypeCount[opt.ValueType().String()]++ 148 | return Okay 149 | }) 150 | 151 | s.LargestProperties = PropLenCount{} 152 | walk(root.Instances, func(inst *rbxfile.Instance, property string, value rbxfile.Value) int { 153 | if value == nil { 154 | return Okay 155 | } 156 | var n int 157 | switch value := value.(type) { 158 | case rbxfile.ValueBinaryString: 159 | n = len(value) 160 | case rbxfile.ValueColorSequence: 161 | n = len(value) 162 | case rbxfile.ValueContent: 163 | n = len(value) 164 | case rbxfile.ValueNumberSequence: 165 | n = len(value) 166 | case rbxfile.ValueProtectedString: 167 | n = len(value) 168 | case rbxfile.ValueSharedString: 169 | n = len(value) 170 | case rbxfile.ValueString: 171 | n = len(value) 172 | default: 173 | return Okay 174 | } 175 | s.LargestProperties[PropLen{ 176 | Class: inst.ClassName, 177 | Property: property, 178 | Type: value.Type().String(), 179 | Length: n}]++ 180 | return Okay 181 | }) 182 | } 183 | 184 | func main() { 185 | var input io.Reader = os.Stdin 186 | var output io.Writer = os.Stdout 187 | 188 | flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), usage) } 189 | flag.Parse() 190 | args := flag.Args() 191 | if len(args) >= 1 && args[0] != "-" { 192 | in, err := os.Open(args[0]) 193 | if err != nil { 194 | fmt.Fprintln(os.Stderr, fmt.Errorf("open input: %w", err)) 195 | return 196 | } 197 | input = in 198 | defer in.Close() 199 | } 200 | if len(args) >= 2 && args[1] != "-" { 201 | out, err := os.Create(args[1]) 202 | if err != nil { 203 | fmt.Fprintln(os.Stderr, fmt.Errorf("create output: %w", err)) 204 | return 205 | } 206 | defer out.Close() 207 | defer func() { 208 | err := out.Sync() 209 | if err != nil { 210 | fmt.Fprintln(os.Stderr, fmt.Errorf("sync output: %w", err)) 211 | return 212 | } 213 | }() 214 | output = out 215 | } 216 | 217 | var stats Stats 218 | root, warn, err := rbxl.Decoder{Stats: &stats.Format}.Decode(input) 219 | if warn != nil { 220 | fmt.Fprintln(os.Stderr, fmt.Errorf("decode warning: %w", warn)) 221 | } 222 | if err != nil { 223 | fmt.Fprintln(os.Stderr, fmt.Errorf("decode error: %w", warn)) 224 | } 225 | 226 | stats.Fill(root) 227 | 228 | je := json.NewEncoder(output) 229 | je.SetEscapeHTML(false) 230 | je.SetIndent("", "\t") 231 | if err := je.Encode(stats); err != nil { 232 | fmt.Fprintln(os.Stderr, fmt.Errorf("write error: %w", err)) 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /declare/declare.go: -------------------------------------------------------------------------------- 1 | // The declare package is used to generate rbxfile structures in a declarative 2 | // style. 3 | // 4 | // Most items have a Declare method, which returns a new rbxfile structure 5 | // corresponding to the declared item. 6 | // 7 | // The easiest way to use this package is to import it directly into the 8 | // current package: 9 | // 10 | // import . "github.com/robloxapi/rbxfile/declare" 11 | // 12 | // This allows the package's identifiers to be used directly without a 13 | // qualifier. 14 | package declare 15 | 16 | import ( 17 | "github.com/robloxapi/rbxfile" 18 | ) 19 | 20 | // primary is implemented by declarations that can be directly within a Root 21 | // declaration. 22 | type primary interface { 23 | primary() 24 | } 25 | 26 | // Root declares a rbxfile.Root. It is a list that contains Instance and 27 | // Metadata declarations. 28 | type Root []primary 29 | 30 | // build recursively resolves instance declarations. 31 | func build(dinst instance, refs rbxfile.References, props map[*rbxfile.Instance][]property) *rbxfile.Instance { 32 | inst := rbxfile.NewInstance(dinst.className) 33 | 34 | if dinst.reference != "" { 35 | refs[dinst.reference] = inst 36 | inst.Reference = dinst.reference 37 | } 38 | 39 | inst.Properties = make(map[string]rbxfile.Value, len(dinst.properties)) 40 | props[inst] = dinst.properties 41 | 42 | inst.Children = make([]*rbxfile.Instance, len(dinst.children)) 43 | for i, dchild := range dinst.children { 44 | child := build(dchild, refs, props) 45 | inst.Children[i] = child 46 | } 47 | 48 | return inst 49 | } 50 | 51 | // Declare evaluates the Root declaration, generating instances, metadata, and 52 | // property values, setting up the instance hierarchy, and resolving references. 53 | // 54 | // Elements are evaluated in order; if two metadata declarations have the same 55 | // key, the latter takes precedence. 56 | func (droot Root) Declare() *rbxfile.Root { 57 | lenInst := 0 58 | lenMeta := 0 59 | for _, p := range droot { 60 | switch p.(type) { 61 | case instance: 62 | lenInst++ 63 | case metadata: 64 | lenMeta++ 65 | } 66 | } 67 | 68 | root := rbxfile.Root{ 69 | Instances: make([]*rbxfile.Instance, 0, lenInst), 70 | Metadata: make(map[string]string, lenMeta), 71 | } 72 | 73 | refs := rbxfile.References{} 74 | props := map[*rbxfile.Instance][]property{} 75 | 76 | for _, p := range droot { 77 | switch p := p.(type) { 78 | case instance: 79 | root.Instances = append(root.Instances, build(p, refs, props)) 80 | case metadata: 81 | root.Metadata[p[0]] = p[1] 82 | } 83 | } 84 | 85 | for inst, properties := range props { 86 | for _, prop := range properties { 87 | inst.Properties[prop.name] = prop.typ.value(refs, prop.value) 88 | } 89 | } 90 | 91 | return &root 92 | } 93 | 94 | // metadata represents the declaration of metadata. 95 | type metadata [2]string 96 | 97 | func (metadata) primary() {} 98 | 99 | // Metadata declares key-value pair to be applied to the root's Metadata field. 100 | func Metadata(key, value string) metadata { 101 | return metadata{key, value} 102 | } 103 | 104 | // element is implemented by declarations that can be within an instance 105 | // declaration. 106 | type element interface { 107 | element() 108 | } 109 | 110 | // instance represents the declaration of a rbxfile.Instance. 111 | type instance struct { 112 | className string 113 | reference string 114 | properties []property 115 | children []instance 116 | } 117 | 118 | func (instance) primary() {} 119 | func (instance) element() {} 120 | 121 | // Declare evaluates the Instance declaration, generating the instance, 122 | // descendants, and property values, setting up the instance hierarchy, and 123 | // resolving references. 124 | func (dinst instance) Declare() *rbxfile.Instance { 125 | inst := rbxfile.NewInstance(dinst.className) 126 | 127 | refs := rbxfile.References{} 128 | props := map[*rbxfile.Instance][]property{} 129 | 130 | if dinst.reference != "" { 131 | refs[dinst.reference] = inst 132 | inst.Reference = dinst.reference 133 | } 134 | 135 | inst.Properties = make(map[string]rbxfile.Value, len(dinst.properties)) 136 | props[inst] = dinst.properties 137 | 138 | inst.Children = make([]*rbxfile.Instance, len(dinst.children)) 139 | for i, dchild := range dinst.children { 140 | child := build(dchild, refs, props) 141 | inst.Children[i] = child 142 | } 143 | 144 | for inst, properties := range props { 145 | for _, prop := range properties { 146 | inst.Properties[prop.name] = prop.typ.value(refs, prop.value) 147 | } 148 | } 149 | 150 | return inst 151 | } 152 | 153 | // Instance declares a rbxfile.Instance. It defines an instance with a class 154 | // name, and a series of "elements". An element can be a Property declaration, 155 | // which defines a property for the instance. An element can also be another 156 | // Instance declaration, which becomes a child of the instance. 157 | // 158 | // An element can also be a "Ref" declaration, which defines a string that can 159 | // be used to refer to the instance by properties with the Reference value 160 | // type. This also sets the instance's Reference field. 161 | func Instance(className string, elements ...element) instance { 162 | inst := instance{ 163 | className: className, 164 | } 165 | 166 | for _, e := range elements { 167 | switch e := e.(type) { 168 | case Ref: 169 | inst.reference = string(e) 170 | case property: 171 | inst.properties = append(inst.properties, e) 172 | case instance: 173 | inst.children = append(inst.children, e) 174 | } 175 | } 176 | 177 | return inst 178 | } 179 | 180 | type property struct { 181 | name string 182 | typ Type 183 | value []interface{} 184 | } 185 | 186 | func (property) element() {} 187 | 188 | // Property declares a property of a rbxfile.Instance. It defines the name of 189 | // the property, a type corresponding to a rbxfile.Value, and the value of the 190 | // property. 191 | // 192 | // The value argument may be one or more values of any type, which are 193 | // asserted to a rbxfile.Value corresponding to the given type. If the 194 | // value(s) cannot be asserted, then the zero value for the given type is 195 | // returned instead. 196 | // 197 | // When the given type or a field of the given type is a number, any number 198 | // type except for complex numbers may be given as the value. 199 | // 200 | // The value may be a single rbxfile.Value that corresponds to the given type 201 | // (e.g. rbxfile.ValueString for String), in which case the value itself is 202 | // returned. 203 | // 204 | // Otherwise, for a given type, values must be the following: 205 | // 206 | // String, BinaryString, ProtectedString, Content, SharedString: 207 | // A single string or []byte. Extra values are ignored. 208 | // 209 | // Bool: 210 | // A single bool. Extra values are ignored. 211 | // 212 | // Int, Float, Double, BrickColor, Token, Int64: 213 | // A single number. Extra values are ignored. 214 | // 215 | // UDim: 216 | // 2 numbers, corresponding to the Scale and Offset fields. 217 | // 218 | // UDim2: 219 | // 1) 2 rbxfile.ValueUDims, corresponding to the X and Y fields. 220 | // 2) 4 numbers, corresponding to the X.Scale, X.Offset, Y.Scale, and 221 | // Y.Offset fields. 222 | // 223 | // Ray: 224 | // 1) 2 rbxfile.ValueVector3s, corresponding to the Origin and 225 | // Direction fields. 226 | // 2) 6 numbers, corresponding to the X, Y, and Z fields of Origin, 227 | // then of Direction. 228 | // 229 | // Faces: 230 | // 6 bools, corresponding to the Right, Top, Back, Left, Bottom, and 231 | // Front fields. 232 | // 233 | // Axes: 234 | // 3 bools, corresponding to the X, Y, and Z fields. 235 | // 236 | // Color3: 237 | // 3 numbers, corresponding to the R, G, and B fields. 238 | // 239 | // Vector2, Vector2int16: 240 | // 2 numbers, corresponding to the X and Y fields. 241 | // 242 | // Vector3, Vector3int16: 243 | // 3 numbers, corresponding to the X, Y, and Z fields. 244 | // 245 | // CFrame: 246 | // 1) 10 values. The first value must be a rbxfile.ValueVector3, which 247 | // corresponds to the Position field. The remaining 9 values must 248 | // be numbers, which correspond to the components of the Rotation 249 | // field. 250 | // 2) 12 numbers. The first 3 correspond to the X, Y, and Z fields of 251 | // the Position field. The remaining 9 numbers correspond to the 252 | // Rotation field. 253 | // 254 | // Reference: 255 | // A single string, []byte or *rbxfile.Instance. Extra values are 256 | // ignored. When the value is a string or []byte, the reference is 257 | // resolved by looking for an instance whose "Ref" declaration is 258 | // equal to the value. 259 | // 260 | // NumberSequence: 261 | // 1) 2 or more rbxfile.ValueNumberSequenceKeypoints, which correspond 262 | // to keypoints in the sequence. 263 | // 2) 2 or more groups of 3 numbers. Each group corresponds to the 264 | // fields Time, Value, and Envelope of a single keypoint in the 265 | // sequence. 266 | // 267 | // ColorSequence: 268 | // 1) 2 or more rbxfile.ValueColorSequenceKeypoints, which correspond 269 | // to keypoints in the sequence. 270 | // 2) 2 or more groups of 3 values: A number, a rbxfile.ValueColor3, 271 | // and a number. Each group corresponds to the Time, Value and 272 | // Envelope fields of a single keypoint in the sequence. 273 | // 3) 2 or more groups of 5 numbers. Each group corresponds to the 274 | // fields Time, Value.R, Value.G, Value.B, and Envelope of a single 275 | // keypoint in the sequence. 276 | // 277 | // NumberRange: 278 | // 2 numbers, corresponding to the Min and Max fields. 279 | // 280 | // Rect: 281 | // 1) 2 rbxfile.ValueVector2s, corresponding to the Min and Max 282 | // fields. 283 | // 2) 4 numbers, corresponding to the Min.X, Min.Y, Max.X, and Max.Y 284 | // fields. 285 | // 286 | // PhysicalProperties: 287 | // 1) No values, indicating PhysicalProperties with CustomPhysics set 288 | // to false. 289 | // 2) 3 numbers, corresponding to the Density, Friction, and 290 | // Elasticity fields (CustomPhysics is set to true). 291 | // 3) 5 numbers, corresponding to the Density, Friction, and 292 | // Elasticity, FrictionWeight, and ElasticityWeight fields 293 | // (CustomPhysics is set to true). 294 | // 295 | // Color3uint8: 296 | // 3 numbers, corresponding to the R, G, and B fields. 297 | func Property(name string, typ Type, value ...interface{}) property { 298 | return property{name: name, typ: typ, value: value} 299 | } 300 | 301 | // Declare evaluates the Property declaration. Since the property does not 302 | // belong to any instance, the name is ignored, and only the value is 303 | // generated. 304 | func (prop property) Declare() rbxfile.Value { 305 | var refs rbxfile.References 306 | return prop.typ.value(refs, prop.value) 307 | } 308 | 309 | // Ref declares a string that can be used to refer to the Instance under which 310 | // it was declared. This will also set the instance's Reference field. 311 | type Ref string 312 | 313 | func (Ref) element() {} 314 | -------------------------------------------------------------------------------- /declare/declare_test.go: -------------------------------------------------------------------------------- 1 | package declare_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | . "github.com/robloxapi/rbxfile/declare" 7 | ) 8 | 9 | func Example() { 10 | root := Root{ 11 | Instance("Part", Ref("RBX12345678"), 12 | Property("Name", String, "BasePlate"), 13 | Property("CanCollide", Bool, true), 14 | Property("Position", Vector3, 0, 10, 0), 15 | Property("Size", Vector3, 2, 1.2, 4), 16 | Instance("CFrameValue", 17 | Property("Name", String, "Value"), 18 | Property("Value", CFrame, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1), 19 | ), 20 | Instance("ObjectValue", 21 | Property("Name", String, "Value"), 22 | Property("Value", Reference, "RBX12345678"), 23 | ), 24 | ), 25 | }.Declare() 26 | fmt.Println(root) 27 | } 28 | -------------------------------------------------------------------------------- /declare/type.go: -------------------------------------------------------------------------------- 1 | package declare 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/robloxapi/rbxfile" 7 | ) 8 | 9 | // Type corresponds to a rbxfile.Type. 10 | type Type byte 11 | 12 | // String returns a string representation of the type. If the type is not 13 | // valid, then the returned value will be "Invalid". 14 | func (t Type) String() string { 15 | s, ok := typeStrings[t] 16 | if !ok { 17 | return "Invalid" 18 | } 19 | return s 20 | } 21 | 22 | const ( 23 | _ Type = iota 24 | String 25 | BinaryString 26 | ProtectedString 27 | Content 28 | Bool 29 | Int 30 | Float 31 | Double 32 | UDim 33 | UDim2 34 | Ray 35 | Faces 36 | Axes 37 | BrickColor 38 | Color3 39 | Vector2 40 | Vector3 41 | CFrame 42 | Token 43 | Reference 44 | Vector3int16 45 | Vector2int16 46 | NumberSequence 47 | ColorSequence 48 | NumberRange 49 | Rect 50 | PhysicalProperties 51 | Color3uint8 52 | Int64 53 | SharedString 54 | ) 55 | 56 | // TypeFromString returns a Type from its string representation. Type(0) is 57 | // returned if the string does not represent an existing Type. 58 | func TypeFromString(s string) Type { 59 | s = strings.ToLower(s) 60 | for typ, str := range typeStrings { 61 | if s == strings.ToLower(str) { 62 | return typ 63 | } 64 | } 65 | return 0 66 | } 67 | 68 | var typeStrings = map[Type]string{ 69 | String: "String", 70 | BinaryString: "BinaryString", 71 | ProtectedString: "ProtectedString", 72 | Content: "Content", 73 | Bool: "Bool", 74 | Int: "Int", 75 | Float: "Float", 76 | Double: "Double", 77 | UDim: "UDim", 78 | UDim2: "UDim2", 79 | Ray: "Ray", 80 | Faces: "Faces", 81 | Axes: "Axes", 82 | BrickColor: "BrickColor", 83 | Color3: "Color3", 84 | Vector2: "Vector2", 85 | Vector3: "Vector3", 86 | CFrame: "CFrame", 87 | Token: "Token", 88 | Reference: "Reference", 89 | Vector3int16: "Vector3int16", 90 | Vector2int16: "Vector2int16", 91 | NumberSequence: "NumberSequence", 92 | ColorSequence: "ColorSequence", 93 | NumberRange: "NumberRange", 94 | Rect: "Rect", 95 | PhysicalProperties: "PhysicalProperties", 96 | Color3uint8: "Color3uint8", 97 | Int64: "Int64", 98 | SharedString: "SharedString", 99 | } 100 | 101 | func normUint8(v interface{}) uint8 { 102 | switch v := v.(type) { 103 | case int: 104 | return uint8(v) 105 | case uint: 106 | return uint8(v) 107 | case uint8: 108 | return uint8(v) 109 | case uint16: 110 | return uint8(v) 111 | case uint32: 112 | return uint8(v) 113 | case uint64: 114 | return uint8(v) 115 | case int8: 116 | return uint8(v) 117 | case int16: 118 | return uint8(v) 119 | case int32: 120 | return uint8(v) 121 | case int64: 122 | return uint8(v) 123 | case float32: 124 | return uint8(v) 125 | case float64: 126 | return uint8(v) 127 | } 128 | 129 | return 0 130 | } 131 | 132 | func normInt16(v interface{}) int16 { 133 | switch v := v.(type) { 134 | case int: 135 | return int16(v) 136 | case uint: 137 | return int16(v) 138 | case uint8: 139 | return int16(v) 140 | case uint16: 141 | return int16(v) 142 | case uint32: 143 | return int16(v) 144 | case uint64: 145 | return int16(v) 146 | case int8: 147 | return int16(v) 148 | case int16: 149 | return int16(v) 150 | case int32: 151 | return int16(v) 152 | case int64: 153 | return int16(v) 154 | case float32: 155 | return int16(v) 156 | case float64: 157 | return int16(v) 158 | } 159 | 160 | return 0 161 | } 162 | 163 | func normInt32(v interface{}) int32 { 164 | switch v := v.(type) { 165 | case int: 166 | return int32(v) 167 | case uint: 168 | return int32(v) 169 | case uint8: 170 | return int32(v) 171 | case uint16: 172 | return int32(v) 173 | case uint32: 174 | return int32(v) 175 | case uint64: 176 | return int32(v) 177 | case int8: 178 | return int32(v) 179 | case int16: 180 | return int32(v) 181 | case int32: 182 | return int32(v) 183 | case int64: 184 | return int32(v) 185 | case float32: 186 | return int32(v) 187 | case float64: 188 | return int32(v) 189 | } 190 | 191 | return 0 192 | } 193 | 194 | func normInt64(v interface{}) int64 { 195 | switch v := v.(type) { 196 | case int: 197 | return int64(v) 198 | case uint: 199 | return int64(v) 200 | case uint8: 201 | return int64(v) 202 | case uint16: 203 | return int64(v) 204 | case uint32: 205 | return int64(v) 206 | case uint64: 207 | return int64(v) 208 | case int8: 209 | return int64(v) 210 | case int16: 211 | return int64(v) 212 | case int32: 213 | return int64(v) 214 | case int64: 215 | return int64(v) 216 | case float32: 217 | return int64(v) 218 | case float64: 219 | return int64(v) 220 | } 221 | 222 | return 0 223 | } 224 | 225 | func normUint32(v interface{}) uint32 { 226 | switch v := v.(type) { 227 | case int: 228 | return uint32(v) 229 | case uint: 230 | return uint32(v) 231 | case uint8: 232 | return uint32(v) 233 | case uint16: 234 | return uint32(v) 235 | case uint32: 236 | return uint32(v) 237 | case uint64: 238 | return uint32(v) 239 | case int8: 240 | return uint32(v) 241 | case int16: 242 | return uint32(v) 243 | case int32: 244 | return uint32(v) 245 | case int64: 246 | return uint32(v) 247 | case float32: 248 | return uint32(v) 249 | case float64: 250 | return uint32(v) 251 | } 252 | 253 | return 0 254 | } 255 | 256 | func normFloat32(v interface{}) float32 { 257 | switch v := v.(type) { 258 | case int: 259 | return float32(v) 260 | case uint: 261 | return float32(v) 262 | case uint8: 263 | return float32(v) 264 | case uint16: 265 | return float32(v) 266 | case uint32: 267 | return float32(v) 268 | case uint64: 269 | return float32(v) 270 | case int8: 271 | return float32(v) 272 | case int16: 273 | return float32(v) 274 | case int32: 275 | return float32(v) 276 | case int64: 277 | return float32(v) 278 | case float32: 279 | return float32(v) 280 | case float64: 281 | return float32(v) 282 | } 283 | 284 | return 0 285 | } 286 | 287 | func normFloat64(v interface{}) float64 { 288 | switch v := v.(type) { 289 | case int: 290 | return float64(v) 291 | case uint: 292 | return float64(v) 293 | case uint8: 294 | return float64(v) 295 | case uint16: 296 | return float64(v) 297 | case uint32: 298 | return float64(v) 299 | case uint64: 300 | return float64(v) 301 | case int8: 302 | return float64(v) 303 | case int16: 304 | return float64(v) 305 | case int32: 306 | return float64(v) 307 | case int64: 308 | return float64(v) 309 | case float32: 310 | return float64(v) 311 | case float64: 312 | return float64(v) 313 | } 314 | 315 | return 0 316 | } 317 | 318 | func normBool(v interface{}) bool { 319 | vv, _ := v.(bool) 320 | return vv 321 | } 322 | 323 | func assertValue(t Type, v interface{}) (value rbxfile.Value, ok bool) { 324 | switch t { 325 | case String: 326 | value, ok = v.(rbxfile.ValueString) 327 | case BinaryString: 328 | value, ok = v.(rbxfile.ValueBinaryString) 329 | case ProtectedString: 330 | value, ok = v.(rbxfile.ValueProtectedString) 331 | case Content: 332 | value, ok = v.(rbxfile.ValueContent) 333 | case Bool: 334 | value, ok = v.(rbxfile.ValueBool) 335 | case Int: 336 | value, ok = v.(rbxfile.ValueInt) 337 | case Float: 338 | value, ok = v.(rbxfile.ValueFloat) 339 | case Double: 340 | value, ok = v.(rbxfile.ValueDouble) 341 | case UDim: 342 | value, ok = v.(rbxfile.ValueUDim) 343 | case UDim2: 344 | value, ok = v.(rbxfile.ValueUDim2) 345 | case Ray: 346 | value, ok = v.(rbxfile.ValueRay) 347 | case Faces: 348 | value, ok = v.(rbxfile.ValueFaces) 349 | case Axes: 350 | value, ok = v.(rbxfile.ValueAxes) 351 | case BrickColor: 352 | value, ok = v.(rbxfile.ValueBrickColor) 353 | case Color3: 354 | value, ok = v.(rbxfile.ValueColor3) 355 | case Vector2: 356 | value, ok = v.(rbxfile.ValueVector2) 357 | case Vector3: 358 | value, ok = v.(rbxfile.ValueVector3) 359 | case CFrame: 360 | value, ok = v.(rbxfile.ValueCFrame) 361 | case Token: 362 | value, ok = v.(rbxfile.ValueToken) 363 | case Reference: 364 | value, ok = v.(rbxfile.ValueReference) 365 | case Vector3int16: 366 | value, ok = v.(rbxfile.ValueVector3int16) 367 | case Vector2int16: 368 | value, ok = v.(rbxfile.ValueVector2int16) 369 | case NumberSequence: 370 | value, ok = v.(rbxfile.ValueNumberSequence) 371 | case ColorSequence: 372 | value, ok = v.(rbxfile.ValueColorSequence) 373 | case NumberRange: 374 | value, ok = v.(rbxfile.ValueNumberRange) 375 | case Rect: 376 | value, ok = v.(rbxfile.ValueRect) 377 | case PhysicalProperties: 378 | value, ok = v.(rbxfile.ValuePhysicalProperties) 379 | case Color3uint8: 380 | value, ok = v.(rbxfile.ValueColor3uint8) 381 | case Int64: 382 | value, ok = v.(rbxfile.ValueInt64) 383 | case SharedString: 384 | value, ok = v.(rbxfile.ValueSharedString) 385 | } 386 | return 387 | } 388 | 389 | func (t Type) value(refs rbxfile.References, v []interface{}) rbxfile.Value { 390 | if len(v) == 0 { 391 | goto zero 392 | } 393 | 394 | if v, ok := assertValue(t, v[0]); ok { 395 | return v 396 | } 397 | 398 | switch t { 399 | case String: 400 | switch v := v[0].(type) { 401 | case string: 402 | return rbxfile.ValueString(v) 403 | case []byte: 404 | return rbxfile.ValueString(v) 405 | } 406 | case BinaryString: 407 | switch v := v[0].(type) { 408 | case string: 409 | return rbxfile.ValueString(v) 410 | case []byte: 411 | return rbxfile.ValueString(v) 412 | } 413 | case ProtectedString: 414 | switch v := v[0].(type) { 415 | case string: 416 | return rbxfile.ValueString(v) 417 | case []byte: 418 | return rbxfile.ValueString(v) 419 | } 420 | case Content: 421 | switch v := v[0].(type) { 422 | case string: 423 | return rbxfile.ValueString(v) 424 | case []byte: 425 | return rbxfile.ValueString(v) 426 | } 427 | case Bool: 428 | switch v := v[0].(type) { 429 | case bool: 430 | return rbxfile.ValueBool(v) 431 | } 432 | case Int: 433 | return rbxfile.ValueInt(normInt32(v[0])) 434 | case Float: 435 | return rbxfile.ValueFloat(normFloat32(v[0])) 436 | case Double: 437 | return rbxfile.ValueFloat(normFloat64(v[0])) 438 | case UDim: 439 | if len(v) == 2 { 440 | return rbxfile.ValueUDim{ 441 | Scale: normFloat32(v[0]), 442 | Offset: normInt32(v[1]), 443 | } 444 | } 445 | case UDim2: 446 | switch len(v) { 447 | case 2: 448 | x, _ := v[0].(rbxfile.ValueUDim) 449 | y, _ := v[1].(rbxfile.ValueUDim) 450 | return rbxfile.ValueUDim2{ 451 | X: x, 452 | Y: y, 453 | } 454 | case 4: 455 | return rbxfile.ValueUDim2{ 456 | X: rbxfile.ValueUDim{ 457 | Scale: normFloat32(v[0]), 458 | Offset: normInt32(v[1]), 459 | }, 460 | Y: rbxfile.ValueUDim{ 461 | Scale: normFloat32(v[2]), 462 | Offset: normInt32(v[3]), 463 | }, 464 | } 465 | } 466 | case Ray: 467 | switch len(v) { 468 | case 2: 469 | origin, _ := v[0].(rbxfile.ValueVector3) 470 | direction, _ := v[1].(rbxfile.ValueVector3) 471 | return rbxfile.ValueRay{ 472 | Origin: origin, 473 | Direction: direction, 474 | } 475 | case 6: 476 | return rbxfile.ValueRay{ 477 | Origin: rbxfile.ValueVector3{ 478 | X: normFloat32(v[0]), 479 | Y: normFloat32(v[1]), 480 | Z: normFloat32(v[2]), 481 | }, 482 | Direction: rbxfile.ValueVector3{ 483 | X: normFloat32(v[3]), 484 | Y: normFloat32(v[4]), 485 | Z: normFloat32(v[5]), 486 | }, 487 | } 488 | } 489 | case Faces: 490 | if len(v) == 6 { 491 | return rbxfile.ValueFaces{ 492 | Right: normBool(v[0]), 493 | Top: normBool(v[1]), 494 | Back: normBool(v[2]), 495 | Left: normBool(v[3]), 496 | Bottom: normBool(v[4]), 497 | Front: normBool(v[5]), 498 | } 499 | } 500 | case Axes: 501 | if len(v) == 3 { 502 | return rbxfile.ValueAxes{ 503 | X: normBool(v[0]), 504 | Y: normBool(v[1]), 505 | Z: normBool(v[2]), 506 | } 507 | } 508 | case BrickColor: 509 | return rbxfile.ValueBrickColor(normUint32(v[0])) 510 | case Color3: 511 | if len(v) == 3 { 512 | return rbxfile.ValueColor3{ 513 | R: normFloat32(v[0]), 514 | G: normFloat32(v[1]), 515 | B: normFloat32(v[2]), 516 | } 517 | } 518 | case Vector2: 519 | if len(v) == 2 { 520 | return rbxfile.ValueVector2{ 521 | X: normFloat32(v[0]), 522 | Y: normFloat32(v[1]), 523 | } 524 | } 525 | case Vector3: 526 | if len(v) == 3 { 527 | return rbxfile.ValueVector3{ 528 | X: normFloat32(v[0]), 529 | Y: normFloat32(v[1]), 530 | Z: normFloat32(v[2]), 531 | } 532 | } 533 | case CFrame: 534 | switch len(v) { 535 | case 10: 536 | p, _ := v[0].(rbxfile.ValueVector3) 537 | return rbxfile.ValueCFrame{ 538 | Position: p, 539 | Rotation: [9]float32{ 540 | normFloat32(v[0]), 541 | normFloat32(v[1]), 542 | normFloat32(v[2]), 543 | normFloat32(v[3]), 544 | normFloat32(v[4]), 545 | normFloat32(v[5]), 546 | normFloat32(v[6]), 547 | normFloat32(v[7]), 548 | normFloat32(v[8]), 549 | }, 550 | } 551 | case 12: 552 | return rbxfile.ValueCFrame{ 553 | Position: rbxfile.ValueVector3{ 554 | X: normFloat32(v[0]), 555 | Y: normFloat32(v[1]), 556 | Z: normFloat32(v[2]), 557 | }, 558 | Rotation: [9]float32{ 559 | normFloat32(v[3]), 560 | normFloat32(v[4]), 561 | normFloat32(v[5]), 562 | normFloat32(v[6]), 563 | normFloat32(v[7]), 564 | normFloat32(v[8]), 565 | normFloat32(v[9]), 566 | normFloat32(v[10]), 567 | normFloat32(v[11]), 568 | }, 569 | } 570 | } 571 | case Token: 572 | return rbxfile.ValueToken(normUint32(v[0])) 573 | case Reference: 574 | switch v := v[0].(type) { 575 | case string: 576 | return rbxfile.ValueReference{ 577 | Instance: refs[v], 578 | } 579 | case []byte: 580 | return rbxfile.ValueReference{ 581 | Instance: refs[string(v)], 582 | } 583 | case *rbxfile.Instance: 584 | return rbxfile.ValueReference{ 585 | Instance: v, 586 | } 587 | } 588 | case Vector3int16: 589 | if len(v) == 3 { 590 | return rbxfile.ValueVector3int16{ 591 | X: normInt16(v[0]), 592 | Y: normInt16(v[1]), 593 | Z: normInt16(v[2]), 594 | } 595 | } 596 | case Vector2int16: 597 | if len(v) == 2 { 598 | return rbxfile.ValueVector2int16{ 599 | X: normInt16(v[0]), 600 | Y: normInt16(v[1]), 601 | } 602 | } 603 | case NumberSequence: 604 | if len(v) > 0 { 605 | if _, ok := v[0].(rbxfile.ValueNumberSequenceKeypoint); ok && len(v) >= 2 { 606 | ns := make(rbxfile.ValueNumberSequence, len(v)) 607 | for i, k := range v { 608 | k, _ := k.(rbxfile.ValueNumberSequenceKeypoint) 609 | ns[i] = k 610 | } 611 | return ns 612 | } else if len(v)%3 == 0 && len(v) >= 6 { 613 | ns := make(rbxfile.ValueNumberSequence, len(v)/3) 614 | for i := 0; i < len(v); i += 3 { 615 | ns[i/3] = rbxfile.ValueNumberSequenceKeypoint{ 616 | Time: normFloat32(v[i+0]), 617 | Value: normFloat32(v[i+1]), 618 | Envelope: normFloat32(v[i+2]), 619 | } 620 | } 621 | } 622 | } 623 | case ColorSequence: 624 | if len(v) > 0 { 625 | if _, ok := v[0].(rbxfile.ValueColorSequenceKeypoint); ok && len(v) >= 2 { 626 | cs := make(rbxfile.ValueColorSequence, len(v)) 627 | for i, k := range v { 628 | k, _ := k.(rbxfile.ValueColorSequenceKeypoint) 629 | cs[i] = k 630 | } 631 | return cs 632 | } else if _, ok := v[1].(rbxfile.ValueColor3); ok && len(v)%3 == 0 && len(v) >= 6 { 633 | cs := make(rbxfile.ValueColorSequence, len(v)/3) 634 | for i := 0; i < len(v); i += 3 { 635 | kval, _ := v[i+1].(rbxfile.ValueColor3) 636 | cs[i/3] = rbxfile.ValueColorSequenceKeypoint{ 637 | Time: normFloat32(v[i+0]), 638 | Value: kval, 639 | Envelope: normFloat32(v[i+2]), 640 | } 641 | } 642 | } else if len(v)%5 == 0 && len(v) >= 10 { 643 | cs := make(rbxfile.ValueColorSequence, len(v)/5) 644 | for i := 0; i < len(v); i += 5 { 645 | cs[i/5] = rbxfile.ValueColorSequenceKeypoint{ 646 | Time: normFloat32(v[i+0]), 647 | Value: rbxfile.ValueColor3{ 648 | R: normFloat32(v[i+1]), 649 | G: normFloat32(v[i+2]), 650 | B: normFloat32(v[i+3]), 651 | }, 652 | Envelope: normFloat32(v[i+4]), 653 | } 654 | } 655 | } 656 | } 657 | case NumberRange: 658 | if len(v) == 2 { 659 | return rbxfile.ValueNumberRange{ 660 | Min: normFloat32(v[0]), 661 | Max: normFloat32(v[1]), 662 | } 663 | } 664 | case Rect: 665 | switch len(v) { 666 | case 2: 667 | min, _ := v[0].(rbxfile.ValueVector2) 668 | max, _ := v[0].(rbxfile.ValueVector2) 669 | return rbxfile.ValueRect{ 670 | Min: min, 671 | Max: max, 672 | } 673 | case 4: 674 | return rbxfile.ValueRect{ 675 | Min: rbxfile.ValueVector2{ 676 | X: normFloat32(v[0]), 677 | Y: normFloat32(v[1]), 678 | }, 679 | Max: rbxfile.ValueVector2{ 680 | X: normFloat32(v[2]), 681 | Y: normFloat32(v[3]), 682 | }, 683 | } 684 | } 685 | case PhysicalProperties: 686 | switch len(v) { 687 | case 0: 688 | return rbxfile.ValuePhysicalProperties{} 689 | case 3: 690 | return rbxfile.ValuePhysicalProperties{ 691 | CustomPhysics: true, 692 | Density: normFloat32(v[0]), 693 | Friction: normFloat32(v[1]), 694 | Elasticity: normFloat32(v[2]), 695 | } 696 | case 5: 697 | return rbxfile.ValuePhysicalProperties{ 698 | CustomPhysics: true, 699 | Density: normFloat32(v[0]), 700 | Friction: normFloat32(v[1]), 701 | Elasticity: normFloat32(v[2]), 702 | FrictionWeight: normFloat32(v[3]), 703 | ElasticityWeight: normFloat32(v[4]), 704 | } 705 | } 706 | case Color3uint8: 707 | if len(v) == 3 { 708 | return rbxfile.ValueColor3uint8{ 709 | R: normUint8(v[0]), 710 | G: normUint8(v[1]), 711 | B: normUint8(v[2]), 712 | } 713 | } 714 | case Int64: 715 | return rbxfile.ValueInt64(normInt64(v[0])) 716 | case SharedString: 717 | switch v := v[0].(type) { 718 | case string: 719 | return rbxfile.ValueSharedString(v) 720 | case []byte: 721 | return rbxfile.ValueSharedString(v) 722 | } 723 | } 724 | 725 | zero: 726 | return rbxfile.NewValue(rbxfile.Type(t)) 727 | } 728 | -------------------------------------------------------------------------------- /errors/errors.go: -------------------------------------------------------------------------------- 1 | // The errors package provides additional error primitives. 2 | package errors 3 | 4 | import ( 5 | "errors" 6 | "strings" 7 | ) 8 | 9 | func New(text string) error { 10 | return errors.New(text) 11 | } 12 | 13 | func Unwrap(err error) error { 14 | return errors.Unwrap(err) 15 | } 16 | 17 | func Is(err, target error) bool { 18 | return errors.Is(err, target) 19 | } 20 | 21 | func As(err error, target interface{}) bool { 22 | return errors.As(err, target) 23 | } 24 | 25 | // Errors is a list of errors. 26 | type Errors []error 27 | 28 | // Errors formats the list by separating each message with a newline. Each 29 | // produced line, including lines within messages, is prefixed with a tab. 30 | func (errs Errors) Error() string { 31 | switch len(errs) { 32 | case 0: 33 | return "no errors" 34 | case 1: 35 | return errs[0].Error() 36 | default: 37 | var buf strings.Builder 38 | buf.WriteString("multiple errors:") 39 | for _, err := range errs { 40 | buf.WriteString("\n\t") 41 | msg := err.Error() 42 | msg = strings.ReplaceAll(msg, "\n", "\n\t") 43 | buf.WriteString(msg) 44 | } 45 | return buf.String() 46 | } 47 | } 48 | 49 | // Append returns errs with each err appended to it. Arguments that are nil are 50 | // skipped. 51 | func (errs Errors) Append(err ...error) Errors { 52 | for _, err := range err { 53 | if err != nil { 54 | errs = append(errs, err) 55 | } 56 | } 57 | return errs 58 | } 59 | 60 | // Return prepares errs to be returned by a function by returning nil if errs is 61 | // empty. 62 | func (errs Errors) Return() error { 63 | if len(errs) == 0 { 64 | return nil 65 | } 66 | return errs 67 | } 68 | 69 | // Union receives a number of errors and combines them into one Errors. Any errs 70 | // that are Errors are concatenated directly. Returns nil if all errs are nil or 71 | // empty. 72 | func Union(errs ...error) error { 73 | var e Errors 74 | for _, err := range errs { 75 | switch err := err.(type) { 76 | case nil: 77 | continue 78 | case Errors: 79 | for _, err := range err { 80 | if err != nil { 81 | e = append(e, err) 82 | } 83 | } 84 | default: 85 | e = append(e, err) 86 | } 87 | } 88 | return e.Return() 89 | } 90 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | // The rbxfile package handles the decoding, encoding, and manipulation of 2 | // Roblox instance data structures. 3 | // 4 | // This package can be used to manipulate Roblox instance trees outside of the 5 | // Roblox client. Such data structures begin with a Root struct. A Root 6 | // contains a list of child Instances, which in turn contain more child 7 | // Instances, and so on, forming a tree of Instances. These Instances can be 8 | // accessed and manipulated using an API similar to that of Roblox. 9 | // 10 | // Each Instance also has a set of "properties". Each property has a specific 11 | // value of a certain type. Every available type implements the Value 12 | // interface, and is prefixed with "Value". 13 | // 14 | // Root structures can be decoded from and encoded to various formats, 15 | // including Roblox's native file formats. The two sub-packages "rbxl" and 16 | // "rbxlx" provide formats for Roblox's binary and XML formats. Root structures 17 | // can also be encoded and decoded with the "json" package. 18 | // 19 | // Besides decoding from a format, root structures can also be created 20 | // manually. The best way to do this is through the "declare" sub-package, 21 | // which provides an easy way to generate root structures. 22 | package rbxfile 23 | 24 | // Root represents the root of an instance tree. Root is not itself an 25 | // instance, but a container for multiple root instances. 26 | type Root struct { 27 | // Instances contains root instances contained in the tree. 28 | Instances []*Instance 29 | 30 | // Metadata contains metadata about the tree. 31 | Metadata map[string]string 32 | } 33 | 34 | // NewRoot returns a new initialized Root. 35 | func NewRoot() *Root { 36 | return &Root{ 37 | Instances: []*Instance{}, 38 | Metadata: map[string]string{}, 39 | } 40 | } 41 | 42 | // Copy creates a copy of the root and its contents. 43 | // 44 | // A copied reference within the tree is resolved so that it points to the 45 | // corresponding copy of the original referent. Copied references that point 46 | // to an instance which isn't being copied will still point to the same 47 | // instance. 48 | func (root *Root) Copy() *Root { 49 | clone := &Root{ 50 | Instances: make([]*Instance, len(root.Instances)), 51 | } 52 | 53 | refs := make(References) 54 | crefs := make(References) 55 | propRefs := make([]PropRef, 0, 8) 56 | for i, inst := range root.Instances { 57 | clone.Instances[i] = inst.copy(refs, crefs, &propRefs) 58 | } 59 | for _, propRef := range propRefs { 60 | if !crefs.Resolve(propRef) { 61 | // Refers to an instance outside the tree, try getting the 62 | // original referent. 63 | refs.Resolve(propRef) 64 | } 65 | } 66 | return clone 67 | } 68 | 69 | // Instance represents a single Roblox instance. 70 | type Instance struct { 71 | // ClassName indicates the instance's type. 72 | ClassName string 73 | 74 | // Reference is a unique string used to refer to the instance from 75 | // elsewhere in the tree. 76 | Reference string 77 | 78 | // IsService indicates whether the instance should be treated as a 79 | // service. 80 | IsService bool 81 | 82 | // Properties is a map of properties of the instance. It maps the name of 83 | // the property to its current value. 84 | Properties map[string]Value 85 | 86 | // Children contains instances that are the children of the current 87 | // instance. The user must take care not to introduce circular references. 88 | Children []*Instance 89 | } 90 | 91 | // NewInstance creates a new Instance of a given class, and an optional 92 | // parent. 93 | func NewInstance(className string) *Instance { 94 | inst := &Instance{ 95 | ClassName: className, 96 | Properties: make(map[string]Value, 0), 97 | } 98 | return inst 99 | } 100 | 101 | // copy returns a deep copy of the instance while managing references. 102 | func (inst *Instance) copy(refs, crefs References, propRefs *[]PropRef) *Instance { 103 | clone := &Instance{ 104 | ClassName: inst.ClassName, 105 | Reference: refs.Get(inst), 106 | IsService: inst.IsService, 107 | Children: make([]*Instance, len(inst.Children)), 108 | Properties: make(map[string]Value, len(inst.Properties)), 109 | } 110 | crefs[clone.Reference] = clone 111 | for name, value := range inst.Properties { 112 | if value, ok := value.(ValueReference); ok { 113 | *propRefs = append(*propRefs, PropRef{ 114 | Instance: clone, 115 | Property: name, 116 | Reference: refs.Get(value.Instance), 117 | }) 118 | continue 119 | } 120 | clone.Properties[name] = value.Copy() 121 | } 122 | for i, child := range inst.Children { 123 | c := child.copy(refs, crefs, propRefs) 124 | clone.Children[i] = c 125 | } 126 | return clone 127 | } 128 | 129 | // Copy returns a deep copy of the instance. Each property and all descendants 130 | // are copied. 131 | // 132 | // A copied reference within the tree is resolved so that it points to the 133 | // corresponding copy of the original referent. Copied references that point 134 | // to an instance which isn't being copied will still point to the same 135 | // instance. 136 | func (inst *Instance) Copy() *Instance { 137 | refs := make(References) 138 | crefs := make(References) 139 | propRefs := make([]PropRef, 0, 8) 140 | clone := inst.copy(refs, crefs, &propRefs) 141 | for _, propRef := range propRefs { 142 | if !crefs.Resolve(propRef) { 143 | // Refers to an instance outside the tree, try getting the 144 | // original referent. 145 | refs.Resolve(propRef) 146 | } 147 | } 148 | return clone 149 | } 150 | -------------------------------------------------------------------------------- /file_test.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package rbxfile 4 | 5 | import ( 6 | "bytes" 7 | "regexp" 8 | "strconv" 9 | "testing" 10 | ) 11 | 12 | // Root Tests 13 | 14 | func TestRootCopy(t *testing.T) { 15 | r := &Root{ 16 | Instances: []*Instance{ 17 | NewInstance("ReferToSelf", nil), 18 | NewInstance("ReferToSibling", nil), 19 | NewInstance("ReferToOutside", nil), 20 | NewInstance("HasChild", nil), 21 | }, 22 | } 23 | child := NewInstance("Child", r.Instances[3]) 24 | child.Set("TestByteCopy", ValueString("hello world")) 25 | outside := NewInstance("Outside", nil) 26 | r.Instances[0].Set("Reference", ValueReference{Instance: r.Instances[0]}) 27 | r.Instances[1].Set("Reference", ValueReference{Instance: r.Instances[0]}) 28 | r.Instances[2].Set("Reference", ValueReference{Instance: outside}) 29 | 30 | rc := r.Copy() 31 | 32 | // Test number of instances. 33 | if i, j := len(r.Instances), len(rc.Instances); i != j { 34 | t.Errorf("mismatched number of instances (expected %d, got %d)", i, j) 35 | } 36 | for i := 0; i < len(r.Instances); i++ { 37 | if a, b := r.Instances[i].ClassName, rc.Instances[i].ClassName; a != b { 38 | t.Errorf("mismatched instance %d (expected %s, got %s)", a, b) 39 | } 40 | if r.Instances[i] == rc.Instances[i] { 41 | t.Errorf("instance %d in copy equals instance in root", i) 42 | } 43 | } 44 | // Test refer to self. 45 | if v, ok := rc.Instances[0].Get("Reference").(ValueReference); !ok || v.Instance != rc.Instances[0] { 46 | str := "" 47 | if v.Instance != nil { 48 | str = v.Instance.ClassName 49 | } 50 | t.Errorf("ReferToSelf failed (expected ReferToSelf, got %s)", str) 51 | } 52 | // Test refer to instance in tree. 53 | if v, ok := rc.Instances[1].Get("Reference").(ValueReference); !ok || v.Instance != rc.Instances[0] { 54 | str := "" 55 | if v.Instance != nil { 56 | str = v.Instance.ClassName 57 | } 58 | t.Errorf("ReferToSibling failed (expected ReferToSelf, got %s)", str) 59 | } 60 | // Test refer to instance outside tree. 61 | if v, _ := rc.Instances[2].Get("Reference").(ValueReference); v.Instance != outside { 62 | t.Errorf("ReferToOutside referent in copy does not equal referent in root") 63 | } 64 | // Test number of children. 65 | if i, j := len(r.Instances[3].Children), len(rc.Instances[3].Children); i != j { 66 | t.Errorf("mismatched number of children (expected %d, got %d)", i, j) 67 | } 68 | // Test children. 69 | if a, b := r.Instances[3].Children[0], rc.Instances[3].Children[0]; a == b { 70 | t.Errorf("child in copy equals child in root") 71 | } else { 72 | av := a.Get("TestByteCopy").(ValueString) 73 | bv, ok := b.Get("TestByteCopy").(ValueString) 74 | if !ok { 75 | t.Errorf("TestByteCopy: property failed to copy") 76 | } 77 | if !bytes.Equal([]byte(av), []byte(bv)) { 78 | t.Errorf("TestByteCopy: content of bytes not equal (got %v)", bv) 79 | } 80 | if av[0] = av[0] + 1; bv[0] == av[0] { 81 | t.Errorf("TestByteCopy: slices not copied") 82 | } 83 | } 84 | 85 | // Test parent of root instance. 86 | r = &Root{Instances: []*Instance{child}} 87 | rc = r.Copy() 88 | if rc.Instances[0].Parent() != nil { 89 | t.Errorf("instance has non-nil parent") 90 | } 91 | } 92 | 93 | // Instance Tests 94 | 95 | func TestNewInstance(t *testing.T) { 96 | inst := NewInstance("Part", nil) 97 | if inst.ClassName != "Part" { 98 | t.Errorf("got ClassName %q, expected %q", inst.ClassName, "Part") 99 | } 100 | if ok, _ := regexp.MatchString("^RBX[0-9A-F]{32}$", inst.Reference); !ok { 101 | t.Errorf("unexpected value for generated reference") 102 | } 103 | 104 | child := NewInstance("IntValue", inst) 105 | if child.ClassName != "IntValue" { 106 | t.Errorf("got ClassName %q, expected %q", child.ClassName, "IntValue") 107 | } 108 | if ok, _ := regexp.MatchString("^RBX[0-9A-F]{32}$", child.Reference); !ok { 109 | t.Errorf("unexpected value for generated reference") 110 | } 111 | if child.Parent() != inst { 112 | t.Errorf("parent of child is not inst") 113 | } 114 | for _, c := range inst.Children { 115 | if c == child { 116 | goto foundChild 117 | } 118 | } 119 | t.Errorf("child not found in parent") 120 | foundChild: 121 | } 122 | 123 | func namedInst(className string, parent *Instance) *Instance { 124 | inst := NewInstance(className, parent) 125 | inst.SetName(className) 126 | return inst 127 | } 128 | 129 | type treeTest struct { 130 | // Compare a to b. 131 | a, b *Instance 132 | // Expected results. 133 | ar, dr bool 134 | } 135 | 136 | func testAncestry(t *testing.T, groups ...treeTest) { 137 | for _, g := range groups { 138 | if r := g.a.IsAncestorOf(g.b); r != g.ar { 139 | t.Errorf("%s.IsAncestorOf(%s) returned %t when %t was expected", g.a, g.b, r, g.ar) 140 | } 141 | if r := g.a.IsDescendantOf(g.b); r != g.dr { 142 | t.Errorf("%s.IsDescendantOf(%s) returned %t when %t was expected", g.a, g.b, r, g.dr) 143 | } 144 | } 145 | } 146 | 147 | func TestInstanceHierarchy(t *testing.T) { 148 | parent := namedInst("Parent", nil) 149 | inst := namedInst("Instance", nil) 150 | sibling := namedInst("Sibling", parent) 151 | child := namedInst("Child", inst) 152 | desc := namedInst("Descendant", child) 153 | 154 | if inst.Parent() != nil { 155 | t.Error("expected nil parent") 156 | } 157 | 158 | if err := inst.SetParent(inst); err == nil { 159 | t.Error("no error on setting parent to self") 160 | } 161 | 162 | if err := inst.SetParent(child); err == nil { 163 | t.Error("no error on setting parent to child") 164 | } 165 | 166 | if err := inst.SetParent(desc); err == nil { 167 | t.Error("no error on setting parent to descendant") 168 | } 169 | 170 | if err := inst.SetParent(parent); err != nil { 171 | t.Error("failed to set parent:", err) 172 | } 173 | 174 | if inst.Parent() != parent { 175 | t.Error("unexpected parent") 176 | } 177 | 178 | if err := inst.SetParent(parent); err != nil { 179 | t.Error("error on setting same parent:", err) 180 | } 181 | 182 | testAncestry(t, 183 | treeTest{parent, nil, false, false}, 184 | treeTest{parent, parent, false, false}, 185 | treeTest{parent, sibling, true, false}, 186 | treeTest{parent, inst, true, false}, 187 | treeTest{parent, child, true, false}, 188 | treeTest{parent, desc, true, false}, 189 | 190 | treeTest{sibling, nil, false, false}, 191 | treeTest{sibling, parent, false, true}, 192 | treeTest{sibling, sibling, false, false}, 193 | treeTest{sibling, inst, false, false}, 194 | treeTest{sibling, child, false, false}, 195 | treeTest{sibling, desc, false, false}, 196 | 197 | treeTest{inst, nil, false, false}, 198 | treeTest{inst, parent, false, true}, 199 | treeTest{inst, sibling, false, false}, 200 | treeTest{inst, inst, false, false}, 201 | treeTest{inst, child, true, false}, 202 | treeTest{inst, desc, true, false}, 203 | 204 | treeTest{child, nil, false, false}, 205 | treeTest{child, parent, false, true}, 206 | treeTest{child, sibling, false, false}, 207 | treeTest{child, inst, false, true}, 208 | treeTest{child, child, false, false}, 209 | treeTest{child, desc, true, false}, 210 | 211 | treeTest{desc, nil, false, false}, 212 | treeTest{desc, parent, false, true}, 213 | treeTest{desc, sibling, false, false}, 214 | treeTest{desc, inst, false, true}, 215 | treeTest{desc, child, false, true}, 216 | treeTest{desc, desc, false, false}, 217 | ) 218 | 219 | if err := sibling.SetParent(nil); err != nil { 220 | t.Error("failed to set parent:", err) 221 | } 222 | 223 | if sibling.Parent() != nil { 224 | t.Error("expected nil parent") 225 | } 226 | 227 | if err := sibling.SetParent(parent); err != nil { 228 | t.Error("failed to set parent:", err) 229 | } 230 | 231 | if sibling.Parent() != parent { 232 | t.Error("unexpected parent") 233 | } 234 | } 235 | 236 | func TestInstance_AddChild(t *testing.T) { 237 | parent := namedInst("Parent", nil) 238 | inst := namedInst("Instance", nil) 239 | sibling := namedInst("Sibling", parent) 240 | child := namedInst("Child", inst) 241 | desc := namedInst("Descendant", child) 242 | 243 | if inst.Parent() != nil { 244 | t.Error("expected nil parent") 245 | } 246 | if err := inst.AddChild(inst); err == nil { 247 | t.Error("no error on adding self") 248 | } 249 | if err := child.AddChild(inst); err == nil { 250 | t.Error("no error on adding parent to child") 251 | } 252 | if err := desc.AddChild(inst); err == nil { 253 | t.Error("no error on adding parent to descendant") 254 | } 255 | if err := parent.AddChild(inst); err != nil { 256 | t.Error("failed add child:", err) 257 | } 258 | if inst.Parent() != parent { 259 | t.Error("unexpected parent") 260 | } 261 | if n := len(parent.Children); n != 2 { 262 | t.Error("unexpected length of children (expected 2, got %d):", n) 263 | } 264 | if parent.Children[0] != sibling { 265 | t.Error("unexpected sibling") 266 | } 267 | if parent.Children[1] != inst { 268 | t.Error("unexpected order of child") 269 | } 270 | if err := parent.AddChild(inst); err != nil { 271 | t.Error("error on adding same child:", err) 272 | } 273 | parent.AddChild(sibling) 274 | if parent.Children[0] != inst { 275 | t.Error("unexpected order of child") 276 | } 277 | if parent.Children[1] != sibling { 278 | t.Error("unexpected order of child") 279 | } 280 | } 281 | 282 | func TestInstance_AddChildAt(t *testing.T) { 283 | parent := namedInst("Parent", nil) 284 | inst := namedInst("Instance", nil) 285 | sibling := namedInst("Sibling", parent) 286 | child := namedInst("Child", inst) 287 | desc := namedInst("Descendant", child) 288 | 289 | if inst.Parent() != nil { 290 | t.Error("expected nil parent") 291 | } 292 | if err := inst.AddChildAt(100, inst); err == nil { 293 | t.Error("no error on adding self") 294 | } 295 | if err := child.AddChildAt(100, inst); err == nil { 296 | t.Error("no error on adding parent to child") 297 | } 298 | if err := desc.AddChildAt(100, inst); err == nil { 299 | t.Error("no error on adding parent to descendant") 300 | } 301 | if err := parent.AddChildAt(100, inst); err != nil { 302 | t.Error("failed add child:", err) 303 | } 304 | if inst.Parent() != parent { 305 | t.Error("unexpected parent") 306 | } 307 | if n := len(parent.Children); n != 2 { 308 | t.Error("unexpected length of children (expected 2, got %d):", n) 309 | } 310 | if parent.Children[0] != sibling { 311 | t.Error("unexpected sibling") 312 | } 313 | if parent.Children[1] != inst { 314 | t.Error("unexpected order of child") 315 | } 316 | if err := parent.AddChildAt(100, inst); err != nil { 317 | t.Error("error on adding same child:", err) 318 | } 319 | parent.AddChildAt(100, sibling) 320 | if parent.Children[0] != inst { 321 | t.Error("unexpected order of child") 322 | } 323 | if parent.Children[1] != sibling { 324 | t.Error("unexpected order of child") 325 | } 326 | 327 | parent = NewInstance("Parent", nil) 328 | child0 := NewInstance("Child0", nil) 329 | child1 := NewInstance("Child1", nil) 330 | child2 := NewInstance("Child2", nil) 331 | assertOrder := func(children ...*Instance) { 332 | if i, j := len(children), len(parent.Children); i != j { 333 | t.Error("unexpected number of children (expected %d, got %d)", i, j) 334 | } 335 | for i := 0; i < len(children); i++ { 336 | if parent.Children[i] != children[i] { 337 | t.Error("unexpected child %d (expected %s, got %s)", children[i].ClassName, parent.Children[i].ClassName) 338 | } 339 | } 340 | } 341 | clear := func() { 342 | child0.SetParent(nil) 343 | child1.SetParent(nil) 344 | child2.SetParent(nil) 345 | } 346 | 347 | assertOrder() 348 | 349 | parent.AddChildAt(0, child0) 350 | assertOrder(child0) 351 | parent.AddChildAt(1, child1) 352 | assertOrder(child0, child1) 353 | parent.AddChildAt(2, child2) 354 | assertOrder(child0, child1, child2) 355 | clear() 356 | 357 | parent.AddChildAt(-1, child0) 358 | assertOrder(child0) 359 | parent.AddChildAt(0, child1) 360 | assertOrder(child1, child0) 361 | parent.AddChildAt(1, child2) 362 | assertOrder(child1, child2, child0) 363 | parent.AddChildAt(0, child0) 364 | assertOrder(child0, child1, child2) 365 | parent.AddChildAt(1, child0) 366 | assertOrder(child1, child0, child2) 367 | } 368 | 369 | func testRemoveOrder(t *testing.T, at bool, parent *Instance, child []*Instance, remove, children string) { 370 | for _, c := range child { 371 | c.SetParent(nil) 372 | } 373 | for _, c := range child { 374 | c.SetParent(parent) 375 | } 376 | if at { 377 | for _, r := range remove { 378 | parent.RemoveChildAt(int(r - '0')) 379 | } 380 | } else { 381 | for _, r := range remove { 382 | parent.RemoveChild(child[int(r-'0')]) 383 | } 384 | } 385 | if i, j := len(children), len(parent.Children); i != j { 386 | t.Errorf("%s:%s: unexpected number of children (expected %d, got %d)", remove, children, i, j) 387 | } 388 | for i, r := range children { 389 | c := child[int(r-'0')] 390 | if parent.Children[i] != c { 391 | t.Errorf("%s:%s: unexpected child %d (expected %s, got %s)", remove, children, i, c.ClassName, parent.Children[i].ClassName) 392 | } 393 | } 394 | for i, ch := range child { 395 | if bytes.Contains([]byte(children), []byte{byte(i + '0')}) { 396 | if ch.Parent() != parent { 397 | t.Errorf("%s:%s: expected parent of %s", remove, children, ch) 398 | } 399 | } else { 400 | if ch.Parent() != nil { 401 | t.Errorf("%s:%s: expected nil parent of %s", remove, children, ch) 402 | } 403 | } 404 | } 405 | } 406 | 407 | func TestInstance_RemoveChild(t *testing.T) { 408 | parent := NewInstance("Parent", nil) 409 | child := make([]*Instance, 3) 410 | for i := range child { 411 | child[i] = NewInstance("Child"+strconv.Itoa(i), nil) 412 | } 413 | // Remove child n | Verify child n 414 | testRemoveOrder(t, false, parent, child, "", "012") 415 | testRemoveOrder(t, false, parent, child, "0", "12") 416 | testRemoveOrder(t, false, parent, child, "1", "02") 417 | testRemoveOrder(t, false, parent, child, "2", "01") 418 | testRemoveOrder(t, false, parent, child, "00", "12") 419 | testRemoveOrder(t, false, parent, child, "10", "2") 420 | testRemoveOrder(t, false, parent, child, "20", "1") 421 | testRemoveOrder(t, false, parent, child, "01", "2") 422 | testRemoveOrder(t, false, parent, child, "11", "02") 423 | testRemoveOrder(t, false, parent, child, "21", "0") 424 | testRemoveOrder(t, false, parent, child, "02", "1") 425 | testRemoveOrder(t, false, parent, child, "12", "0") 426 | testRemoveOrder(t, false, parent, child, "22", "01") 427 | testRemoveOrder(t, false, parent, child, "000", "12") 428 | testRemoveOrder(t, false, parent, child, "100", "2") 429 | testRemoveOrder(t, false, parent, child, "200", "1") 430 | testRemoveOrder(t, false, parent, child, "010", "2") 431 | testRemoveOrder(t, false, parent, child, "110", "2") 432 | testRemoveOrder(t, false, parent, child, "210", "") 433 | testRemoveOrder(t, false, parent, child, "020", "1") 434 | testRemoveOrder(t, false, parent, child, "120", "") 435 | testRemoveOrder(t, false, parent, child, "220", "1") 436 | testRemoveOrder(t, false, parent, child, "001", "2") 437 | testRemoveOrder(t, false, parent, child, "101", "2") 438 | testRemoveOrder(t, false, parent, child, "201", "") 439 | testRemoveOrder(t, false, parent, child, "011", "2") 440 | testRemoveOrder(t, false, parent, child, "111", "02") 441 | testRemoveOrder(t, false, parent, child, "211", "0") 442 | testRemoveOrder(t, false, parent, child, "021", "") 443 | testRemoveOrder(t, false, parent, child, "121", "0") 444 | testRemoveOrder(t, false, parent, child, "221", "0") 445 | testRemoveOrder(t, false, parent, child, "002", "1") 446 | testRemoveOrder(t, false, parent, child, "102", "") 447 | testRemoveOrder(t, false, parent, child, "202", "1") 448 | testRemoveOrder(t, false, parent, child, "012", "") 449 | testRemoveOrder(t, false, parent, child, "112", "0") 450 | testRemoveOrder(t, false, parent, child, "212", "0") 451 | testRemoveOrder(t, false, parent, child, "022", "1") 452 | testRemoveOrder(t, false, parent, child, "122", "0") 453 | testRemoveOrder(t, false, parent, child, "222", "01") 454 | } 455 | 456 | func TestInstance_RemoveChildAt(t *testing.T) { 457 | parent := NewInstance("Parent", nil) 458 | child := make([]*Instance, 3) 459 | for i := range child { 460 | child[i] = NewInstance("Child"+strconv.Itoa(i), nil) 461 | } 462 | // Remove child at n | Verify child n 463 | testRemoveOrder(t, true, parent, child, "", "012") 464 | testRemoveOrder(t, true, parent, child, "0", "12") 465 | testRemoveOrder(t, true, parent, child, "1", "02") 466 | testRemoveOrder(t, true, parent, child, "2", "01") 467 | testRemoveOrder(t, true, parent, child, "00", "2") 468 | testRemoveOrder(t, true, parent, child, "10", "2") 469 | testRemoveOrder(t, true, parent, child, "20", "1") 470 | testRemoveOrder(t, true, parent, child, "01", "1") 471 | testRemoveOrder(t, true, parent, child, "11", "0") 472 | testRemoveOrder(t, true, parent, child, "21", "0") 473 | testRemoveOrder(t, true, parent, child, "02", "12") 474 | testRemoveOrder(t, true, parent, child, "12", "02") 475 | testRemoveOrder(t, true, parent, child, "22", "01") 476 | testRemoveOrder(t, true, parent, child, "000", "") 477 | testRemoveOrder(t, true, parent, child, "100", "") 478 | testRemoveOrder(t, true, parent, child, "200", "") 479 | testRemoveOrder(t, true, parent, child, "010", "") 480 | testRemoveOrder(t, true, parent, child, "110", "") 481 | testRemoveOrder(t, true, parent, child, "210", "") 482 | testRemoveOrder(t, true, parent, child, "020", "2") 483 | testRemoveOrder(t, true, parent, child, "120", "2") 484 | testRemoveOrder(t, true, parent, child, "220", "1") 485 | testRemoveOrder(t, true, parent, child, "001", "2") 486 | testRemoveOrder(t, true, parent, child, "101", "2") 487 | testRemoveOrder(t, true, parent, child, "201", "1") 488 | testRemoveOrder(t, true, parent, child, "011", "1") 489 | testRemoveOrder(t, true, parent, child, "111", "0") 490 | testRemoveOrder(t, true, parent, child, "211", "0") 491 | testRemoveOrder(t, true, parent, child, "021", "1") 492 | testRemoveOrder(t, true, parent, child, "121", "0") 493 | testRemoveOrder(t, true, parent, child, "221", "0") 494 | testRemoveOrder(t, true, parent, child, "002", "2") 495 | testRemoveOrder(t, true, parent, child, "102", "2") 496 | testRemoveOrder(t, true, parent, child, "202", "1") 497 | testRemoveOrder(t, true, parent, child, "012", "1") 498 | testRemoveOrder(t, true, parent, child, "112", "0") 499 | testRemoveOrder(t, true, parent, child, "212", "0") 500 | testRemoveOrder(t, true, parent, child, "022", "12") 501 | testRemoveOrder(t, true, parent, child, "122", "02") 502 | testRemoveOrder(t, true, parent, child, "222", "01") 503 | } 504 | 505 | func TestInstance_RemoveAll(t *testing.T) { 506 | parent := NewInstance("Parent", nil) 507 | children := make([]*Instance, 100) 508 | for i := range children { 509 | children[i] = NewInstance("Child", parent) 510 | } 511 | 512 | parent.RemoveAll() 513 | if i := len(parent.Children); i != 0 { 514 | t.Error("expected Children length of 0 (got %d)", i) 515 | } 516 | for i, child := range children { 517 | if child.Parent() != nil { 518 | t.Error("expected nil parent on child %d", i) 519 | } 520 | } 521 | } 522 | 523 | func TestInstance_Clone(t *testing.T) { 524 | inst := NewInstance("Instance", nil) 525 | inst.SetName("InstanceName") 526 | inst.Properties["Position"] = ValueVector3{X: 1, Y: 2, Z: 3} 527 | 528 | child := NewInstance("Child", inst) 529 | child.SetName("ChildName") 530 | child.Properties["Size"] = ValueVector3{X: 4, Y: 5, Z: 6} 531 | 532 | outside := NewInstance("Outside", nil) 533 | inst.Set("Reference", ValueReference{Instance: outside}) 534 | 535 | cinst := inst.Clone() 536 | 537 | if cinst.ClassName != inst.ClassName { 538 | t.Error("cloned ClassName does not equal original") 539 | } 540 | if cinst.Parent() != nil { 541 | t.Error("expected nil clone parent") 542 | } 543 | 544 | if cinst.Name() != inst.Name() { 545 | t.Error("cloned Name property does not equal original") 546 | } 547 | a, b := inst.Get("Name").(ValueString), cinst.Get("Name").(ValueString) 548 | if a[0] = a[0] + 1; b[0] == a[0] { 549 | t.Error("slice of cloned Name poonts to same array as original") 550 | } 551 | if cinst.Properties["Position"] != inst.Properties["Position"] { 552 | t.Error("cloned Position property does not equal original") 553 | } 554 | if v, _ := cinst.Properties["Reference"].(ValueReference); v.Instance != outside { 555 | t.Error("cloned Reference property does not equal original") 556 | } 557 | 558 | var cchild *Instance 559 | if len(cinst.Children) != 1 { 560 | t.Fatalf("expected 1 child, got %d", len(cinst.Children)) 561 | } else { 562 | cchild = cinst.Children[0] 563 | } 564 | 565 | if cchild.ClassName != child.ClassName { 566 | t.Error("cloned child ClassName does not equal original") 567 | } 568 | if cchild.Parent() != cinst { 569 | t.Error("clone child parent is not cloned inst") 570 | } 571 | 572 | if cchild.Name() != child.Name() { 573 | t.Error("cloned child Name property does not equal original") 574 | } 575 | if cchild.Properties["Size"] != child.Properties["Size"] { 576 | t.Error("cloned child Size property does not equal original") 577 | } 578 | } 579 | 580 | func TestInstance_FindFirstChild(t *testing.T) { 581 | inst := namedInst("Instance", nil) 582 | child0 := namedInst("Child", inst) 583 | desc00 := namedInst("Desc", child0) 584 | namedInst("Desc", child0) 585 | child1 := namedInst("Child", inst) 586 | namedInst("Desc", child1) 587 | desc11 := namedInst("Desc1", child1) 588 | namedInst("Desc", child1) 589 | 590 | if c := inst.FindFirstChild("DoesNotExist", false); c != nil { 591 | t.Error("found child that does not exist") 592 | } 593 | 594 | if c := inst.FindFirstChild("DoesNotExist", true); c != nil { 595 | t.Error("found descendant that does not exist (recursive)") 596 | } 597 | 598 | if c := inst.FindFirstChild("Child", false); c != child0 { 599 | t.Error("failed to get first child") 600 | } 601 | 602 | if c := inst.FindFirstChild("Child", true); c != child0 { 603 | t.Error("failed to get first child (recursive)") 604 | } 605 | 606 | if c := inst.FindFirstChild("Desc", false); c != nil { 607 | t.Error("expected nil result") 608 | } 609 | 610 | if c := inst.FindFirstChild("Desc", true); c != desc00 { 611 | t.Error("failed to get first descendant (recursive)") 612 | } 613 | 614 | if c := inst.FindFirstChild("Desc1", true); c != desc11 { 615 | t.Error("failed to get selected descendant (recursive)") 616 | } 617 | } 618 | 619 | func TestInstance_GetFullName(t *testing.T) { 620 | inst0 := namedInst("Grandparent", nil) 621 | inst1 := namedInst("Parent", inst0) 622 | inst2 := namedInst("Entity", inst1) 623 | inst3 := namedInst("Child", inst2) 624 | inst4 := namedInst("Grandchild", inst3) 625 | 626 | if name := inst4.GetFullName(); name != `Grandparent.Parent.Entity.Child.Grandchild` { 627 | t.Errorf("unexpected full name %q", name) 628 | } 629 | } 630 | 631 | func TestInstance_Name(t *testing.T) { 632 | inst := NewInstance("Instance", nil) 633 | 634 | if inst.Name() != "" { 635 | t.Error("unexpected value returned from Name") 636 | } 637 | 638 | inst.SetName("Instance") 639 | 640 | if v, ok := inst.Properties["Name"]; !ok { 641 | t.Error("failed to set Name property") 642 | } else if v, ok := v.(ValueString); !ok { 643 | t.Error("expected ValueString type for Name property") 644 | } else if string(v) != "Instance" { 645 | t.Error("unexpected value of Name property") 646 | } 647 | 648 | if inst.Name() != "Instance" { 649 | t.Error("unexpected value returned from Name") 650 | } 651 | 652 | inst.SetName("") 653 | 654 | if v, ok := inst.Properties["Name"]; !ok { 655 | t.Error("expected Name property") 656 | } else if v, ok := v.(ValueString); !ok { 657 | t.Error("expected ValueString type for Name property") 658 | } else if string(v) != "" { 659 | t.Error("unexpected value of Name property") 660 | } 661 | 662 | if inst.Name() != "" { 663 | t.Error("unexpected value returned from Name") 664 | } 665 | } 666 | 667 | func TestInstance_String(t *testing.T) { 668 | inst := NewInstance("Instance", nil) 669 | 670 | if inst.String() != "Instance" { 671 | t.Error("unexpected value returned from String") 672 | } 673 | 674 | inst.SetName("InstanceName") 675 | 676 | if inst.String() != "InstanceName" { 677 | t.Error("unexpected value returned from String") 678 | } 679 | 680 | inst.SetName("") 681 | 682 | if inst.String() != "Instance" { 683 | t.Error("unexpected value returned from String") 684 | } 685 | } 686 | 687 | func TestInstance_GetSet(t *testing.T) { 688 | inst := NewInstance("Instance", nil) 689 | 690 | if inst.Get("Property") != nil { 691 | t.Error("unexpected value returned from Get") 692 | } 693 | 694 | inst.Set("Property", ValueString("Value")) 695 | 696 | if v, ok := inst.Properties["Property"]; !ok { 697 | t.Error("expected property") 698 | } else if v, ok := v.(ValueString); !ok { 699 | t.Error("expected ValueString type for property") 700 | } else if string(v) != "Value" { 701 | t.Error("unexpected value of property") 702 | } 703 | 704 | if v := inst.Get("Property"); v == nil { 705 | t.Error("expected property") 706 | } else if v, ok := v.(ValueString); !ok { 707 | t.Error("expected ValueString type for property") 708 | } else if string(v) != "Value" { 709 | t.Error("unexpected value of property") 710 | } 711 | 712 | inst.Set("Property", nil) 713 | 714 | if inst.Properties["Property"] != nil { 715 | t.Error("unexpected property") 716 | } 717 | 718 | if inst.Get("Property") != nil { 719 | t.Error("unexpected value returned from Get") 720 | } 721 | } 722 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/robloxapi/rbxfile 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/anaminus/parse v0.2.0 7 | github.com/bkaradzic/go-lz4 v1.0.0 8 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a 9 | ) 10 | 11 | require golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/anaminus/parse v0.2.0 h1:a8IEzp/INmUHVfZp4zaeggLvgqIjHrBFrBhkM/qdKUM= 2 | github.com/anaminus/parse v0.2.0/go.mod h1:5EP2T2CqY4EDBxl2S/qhFSSb2VjW+iIqG+8Scy3mZN8= 3 | github.com/bkaradzic/go-lz4 v1.0.0 h1:RXc4wYsyz985CkXXeX04y4VnZFGG8Rd43pRaHsOXAKk= 4 | github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= 5 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= 6 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 7 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= 8 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 9 | -------------------------------------------------------------------------------- /json/json.go: -------------------------------------------------------------------------------- 1 | // The json package is used to encode and decode rbxfile objects to the JSON 2 | // format. 3 | package json 4 | 5 | import ( 6 | "bytes" 7 | "encoding/base64" 8 | "encoding/json" 9 | "errors" 10 | "io" 11 | 12 | "github.com/robloxapi/rbxfile" 13 | ) 14 | 15 | func Encode(root *rbxfile.Root) (b []byte, err error) { 16 | return json.Marshal(RootToJSONInterface(root)) 17 | } 18 | 19 | func Decode(b []byte) (root *rbxfile.Root, err error) { 20 | var v interface{} 21 | err = json.Unmarshal(b, &v) 22 | if err != nil { 23 | return nil, err 24 | } 25 | root, ok := RootFromJSONInterface(v) 26 | if !ok { 27 | return nil, errors.New("invalid JSON Root object") 28 | } 29 | return root, nil 30 | } 31 | 32 | // The current version of the schema. 33 | const jsonVersion = 0 34 | 35 | func indexJSON(v, i, p interface{}) bool { 36 | var value interface{} 37 | var okay = false 38 | switch object := v.(type) { 39 | case map[string]interface{}: 40 | index, ok := i.(string) 41 | if !ok { 42 | return false 43 | } 44 | value, ok = object[index] 45 | if !ok { 46 | return false 47 | } 48 | okay = true 49 | case []interface{}: 50 | index, ok := i.(int) 51 | if !ok { 52 | return false 53 | } 54 | if index >= len(object) || index < 0 { 55 | return false 56 | } 57 | value = object[index] 58 | okay = true 59 | default: 60 | return false 61 | } 62 | if !okay { 63 | return false 64 | } 65 | switch p := p.(type) { 66 | case *bool: 67 | value, ok := value.(bool) 68 | if !ok { 69 | return false 70 | } 71 | *p = value 72 | case *float64: 73 | value, ok := value.(float64) 74 | if !ok { 75 | return false 76 | } 77 | *p = value 78 | case *string: 79 | value, ok := value.(string) 80 | if !ok { 81 | return false 82 | } 83 | *p = value 84 | case *[]interface{}: 85 | value, ok := value.([]interface{}) 86 | if !ok { 87 | return false 88 | } 89 | *p = value 90 | case *map[string]interface{}: 91 | value, ok := value.(map[string]interface{}) 92 | if !ok { 93 | return false 94 | } 95 | *p = value 96 | case *interface{}: 97 | *p = value 98 | } 99 | return true 100 | } 101 | 102 | // RootToJSONInterface converts a rbxfile.Root to a generic interface that can 103 | // be read by json.Marshal. 104 | func RootToJSONInterface(root *rbxfile.Root) interface{} { 105 | refs := rbxfile.References{} 106 | iroot := make(map[string]interface{}, 2) 107 | iroot["rbxfile_version"] = float64(jsonVersion) 108 | instances := make([]interface{}, len(root.Instances)) 109 | for i, inst := range root.Instances { 110 | instances[i] = InstanceToJSONInterface(inst, refs) 111 | } 112 | iroot["instances"] = instances 113 | return iroot 114 | } 115 | 116 | // RootToJSONInterface converts a generic interface produced json.Unmarshal to 117 | // a rbxfile.Root. 118 | func RootFromJSONInterface(iroot interface{}) (root *rbxfile.Root, ok bool) { 119 | var version float64 120 | if !indexJSON(iroot, "rbxfile_version", &version) { 121 | return nil, false 122 | } 123 | 124 | root = new(rbxfile.Root) 125 | 126 | switch int(version) { 127 | case 0: 128 | refs := rbxfile.References{} 129 | propRefs := []rbxfile.PropRef{} 130 | root.Instances = make([]*rbxfile.Instance, 0, 8) 131 | var instances []interface{} 132 | if !indexJSON(iroot, "instances", &instances) { 133 | return nil, false 134 | } 135 | for _, iinst := range instances { 136 | inst, ok := InstanceFromJSONInterface(iinst, refs, &propRefs) 137 | if !ok { 138 | continue 139 | } 140 | root.Instances = append(root.Instances, inst) 141 | } 142 | for _, pr := range propRefs { 143 | pr.Instance.Properties[pr.Property] = rbxfile.ValueReference{ 144 | Instance: refs[pr.Reference], 145 | } 146 | } 147 | default: 148 | return nil, false 149 | } 150 | return root, true 151 | } 152 | 153 | //////////////////////////////////////////////////////////////// 154 | 155 | // InstanceToJSONInterface converts a rbxfile.Instance to a generic interface 156 | // that can be read by json.Marshal. 157 | // 158 | // The refs argument is used by to keep track of instance references. 159 | func InstanceToJSONInterface(inst *rbxfile.Instance, refs rbxfile.References) interface{} { 160 | iinst := make(map[string]interface{}, 5) 161 | iinst["class_name"] = inst.ClassName 162 | iinst["reference"] = refs.Get(inst) 163 | iinst["is_service"] = inst.IsService 164 | properties := make(map[string]interface{}, len(inst.Properties)) 165 | for name, prop := range inst.Properties { 166 | iprop := make(map[string]interface{}, 2) 167 | iprop["type"] = prop.Type().String() 168 | iprop["value"] = ValueToJSONInterface(prop, refs) 169 | properties[name] = iprop 170 | } 171 | iinst["properties"] = properties 172 | children := make([]interface{}, len(inst.Children)) 173 | for i, child := range inst.Children { 174 | children[i] = InstanceToJSONInterface(child, refs) 175 | } 176 | iinst["children"] = children 177 | return iinst 178 | } 179 | 180 | // InstanceFromJSONInterface converts a generic interface produced by 181 | // json.Unmarshal into a rbxfile.Instance. 182 | // 183 | // The refs argument is used to keep track of instance references. 184 | // 185 | // The propRefs argument is populated with a list of PropRefs, specifying 186 | // properties of descendant instances that are references. This should be used 187 | // in combination with refs to set each property after all instance have been 188 | // processed. 189 | func InstanceFromJSONInterface(iinst interface{}, refs rbxfile.References, propRefs *[]rbxfile.PropRef) (inst *rbxfile.Instance, ok bool) { 190 | var ref string 191 | indexJSON(iinst, "reference", &ref) 192 | if rbxfile.IsEmptyReference(ref) { 193 | inst = rbxfile.NewInstance("") 194 | } else { 195 | var exists bool 196 | if inst, exists = refs[ref]; !exists { 197 | inst = rbxfile.NewInstance("") 198 | inst.Reference = ref 199 | refs[ref] = inst 200 | } 201 | } 202 | 203 | if !indexJSON(iinst, "class_name", &inst.ClassName) { 204 | return nil, false 205 | } 206 | 207 | indexJSON(iinst, "is_service", &inst.IsService) 208 | 209 | var properties map[string]interface{} 210 | indexJSON(iinst, "properties", &properties) 211 | for name, iprop := range properties { 212 | var typ string 213 | if !indexJSON(iprop, "type", &typ) { 214 | continue 215 | } 216 | 217 | var ivalue interface{} 218 | if !indexJSON(iprop, "value", &ivalue) { 219 | continue 220 | } 221 | 222 | t := rbxfile.TypeFromString(typ) 223 | value := ValueFromJSONInterface(t, ivalue) 224 | if value == nil { 225 | continue 226 | } 227 | if t == rbxfile.TypeReference { 228 | *propRefs = append(*propRefs, rbxfile.PropRef{ 229 | Instance: inst, 230 | Property: name, 231 | Reference: string(value.(rbxfile.ValueString)), 232 | }) 233 | } else { 234 | inst.Properties[name] = value 235 | } 236 | } 237 | 238 | var children []interface{} 239 | indexJSON(iinst, "children", &children) 240 | inst.Children = make([]*rbxfile.Instance, 0, len(children)) 241 | for i, ichild := range children { 242 | child, ok := InstanceFromJSONInterface(ichild, refs, propRefs) 243 | if !ok { 244 | continue 245 | } 246 | inst.Children[i] = child 247 | } 248 | 249 | return inst, true 250 | } 251 | 252 | //////////////////////////////////////////////////////////////// 253 | 254 | // ValueToJSONInterface converts a value to a generic interface that can be 255 | // read by json.Marshal. 256 | // 257 | // The refs argument is used when converting a rbxfile.ValueReference to a 258 | // string. 259 | func ValueToJSONInterface(value rbxfile.Value, refs rbxfile.References) interface{} { 260 | switch value := value.(type) { 261 | case rbxfile.ValueString: 262 | return string(value) 263 | case rbxfile.ValueBinaryString: 264 | var buf bytes.Buffer 265 | bw := base64.NewEncoder(base64.StdEncoding, &buf) 266 | bw.Write([]byte(value)) 267 | return buf.String() 268 | case rbxfile.ValueProtectedString: 269 | return string(value) 270 | case rbxfile.ValueContent: 271 | if len(value) == 0 { 272 | return nil 273 | } 274 | return string(value) 275 | case rbxfile.ValueBool: 276 | return bool(value) 277 | case rbxfile.ValueInt: 278 | return float64(value) 279 | case rbxfile.ValueFloat: 280 | return float64(value) 281 | case rbxfile.ValueDouble: 282 | return float64(value) 283 | case rbxfile.ValueUDim: 284 | return map[string]interface{}{ 285 | "scale": float64(value.Scale), 286 | "offset": float64(value.Offset), 287 | } 288 | case rbxfile.ValueUDim2: 289 | return map[string]interface{}{ 290 | "x": ValueToJSONInterface(value.X, refs), 291 | "y": ValueToJSONInterface(value.Y, refs), 292 | } 293 | case rbxfile.ValueRay: 294 | return map[string]interface{}{ 295 | "origin": ValueToJSONInterface(value.Origin, refs), 296 | "direction": ValueToJSONInterface(value.Direction, refs), 297 | } 298 | case rbxfile.ValueFaces: 299 | return map[string]interface{}{ 300 | "right": value.Right, 301 | "top": value.Top, 302 | "back": value.Back, 303 | "left": value.Left, 304 | "bottom": value.Bottom, 305 | "front": value.Front, 306 | } 307 | case rbxfile.ValueAxes: 308 | return map[string]interface{}{ 309 | "x": value.X, 310 | "y": value.Y, 311 | "z": value.Z, 312 | } 313 | case rbxfile.ValueBrickColor: 314 | return float64(value) 315 | case rbxfile.ValueColor3: 316 | return map[string]interface{}{ 317 | "r": float64(value.R), 318 | "g": float64(value.G), 319 | "b": float64(value.B), 320 | } 321 | case rbxfile.ValueVector2: 322 | return map[string]interface{}{ 323 | "x": float64(value.X), 324 | "y": float64(value.Y), 325 | } 326 | case rbxfile.ValueVector3: 327 | return map[string]interface{}{ 328 | "x": float64(value.X), 329 | "y": float64(value.Y), 330 | "z": float64(value.Z), 331 | } 332 | case rbxfile.ValueCFrame: 333 | ivalue := make(map[string]interface{}, 2) 334 | ivalue["position"] = ValueToJSONInterface(value.Position, refs) 335 | rotation := make([]interface{}, len(value.Rotation)) 336 | for i, r := range value.Rotation { 337 | rotation[i] = float64(r) 338 | } 339 | ivalue["rotation"] = rotation 340 | return ivalue 341 | case rbxfile.ValueToken: 342 | return float64(value) 343 | case rbxfile.ValueReference: 344 | return refs.Get(value.Instance) 345 | case rbxfile.ValueVector3int16: 346 | return map[string]interface{}{ 347 | "x": float64(value.X), 348 | "y": float64(value.Y), 349 | "z": float64(value.Z), 350 | } 351 | case rbxfile.ValueVector2int16: 352 | return map[string]interface{}{ 353 | "x": float64(value.X), 354 | "y": float64(value.Y), 355 | } 356 | case rbxfile.ValueNumberSequence: 357 | ivalue := make([]interface{}, len(value)) 358 | for i, nsk := range value { 359 | ivalue[i] = map[string]interface{}{ 360 | "time": float64(nsk.Time), 361 | "value": float64(nsk.Value), 362 | "envelope": float64(nsk.Envelope), 363 | } 364 | } 365 | return ivalue 366 | case rbxfile.ValueColorSequence: 367 | ivalue := make([]interface{}, len(value)) 368 | for i, csk := range value { 369 | ivalue[i] = map[string]interface{}{ 370 | "time": float64(csk.Time), 371 | "value": ValueToJSONInterface(csk.Value, refs), 372 | "envelope": float64(csk.Envelope), 373 | } 374 | } 375 | return ivalue 376 | case rbxfile.ValueNumberRange: 377 | return map[string]interface{}{ 378 | "min": float64(value.Min), 379 | "max": float64(value.Max), 380 | } 381 | case rbxfile.ValueRect: 382 | return map[string]interface{}{ 383 | "min": ValueToJSONInterface(value.Min, refs), 384 | "max": ValueToJSONInterface(value.Max, refs), 385 | } 386 | case rbxfile.ValuePhysicalProperties: 387 | return map[string]interface{}{ 388 | "custom_physics": value.CustomPhysics, 389 | "density": float64(value.Density), 390 | "friction": float64(value.Friction), 391 | "elasticity": float64(value.Elasticity), 392 | "friction_weight": float64(value.FrictionWeight), 393 | "elasticity_weight": float64(value.ElasticityWeight), 394 | } 395 | case rbxfile.ValueColor3uint8: 396 | return map[string]interface{}{ 397 | "r": float64(value.R), 398 | "g": float64(value.G), 399 | "b": float64(value.B), 400 | } 401 | case rbxfile.ValueInt64: 402 | return float64(value) 403 | case rbxfile.ValueSharedString: 404 | // TODO: Implement as shared data. 405 | var buf bytes.Buffer 406 | bw := base64.NewEncoder(base64.StdEncoding, &buf) 407 | bw.Write([]byte(value)) 408 | return buf.String() 409 | case rbxfile.ValueOptional: 410 | return map[string]interface{}{ 411 | "type": value.ValueType(), 412 | "value": ValueToJSONInterface(value.Value(), refs), 413 | } 414 | } 415 | return nil 416 | } 417 | 418 | // ValueFromJSONInterface converts a generic interface produced by 419 | // json.Unmarshal to a rbxfile.Value. 420 | // 421 | // When the value is rbxfile.TypeReference, the result is a 422 | // rbxfile.ValueString containing the raw reference string, expected to be 423 | // dereferenced at a later time. 424 | func ValueFromJSONInterface(typ rbxfile.Type, ivalue interface{}) (value rbxfile.Value) { 425 | switch typ { 426 | case rbxfile.TypeString: 427 | v, ok := ivalue.(string) 428 | if !ok { 429 | return nil 430 | } 431 | return rbxfile.ValueString(v) 432 | case rbxfile.TypeBinaryString: 433 | v, ok := ivalue.(string) 434 | if !ok { 435 | return nil 436 | } 437 | buf := bytes.NewReader([]byte(v)) 438 | b, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding, buf)) 439 | if err != nil { 440 | return rbxfile.ValueBinaryString(v) 441 | } 442 | return rbxfile.ValueBinaryString(b) 443 | case rbxfile.TypeProtectedString: 444 | v, ok := ivalue.(string) 445 | if !ok { 446 | return nil 447 | } 448 | return rbxfile.ValueProtectedString(v) 449 | case rbxfile.TypeContent: 450 | if ivalue == nil { 451 | return rbxfile.ValueContent(nil) 452 | } 453 | v, ok := ivalue.(string) 454 | if !ok { 455 | return nil 456 | } 457 | return rbxfile.ValueContent(v) 458 | case rbxfile.TypeBool: 459 | v, ok := ivalue.(bool) 460 | if !ok { 461 | return nil 462 | } 463 | return rbxfile.ValueBool(v) 464 | case rbxfile.TypeInt: 465 | v, ok := ivalue.(float64) 466 | if !ok { 467 | return nil 468 | } 469 | return rbxfile.ValueInt(int32(v)) 470 | case rbxfile.TypeFloat: 471 | v, ok := ivalue.(float64) 472 | if !ok { 473 | return nil 474 | } 475 | return rbxfile.ValueFloat(float32(v)) 476 | case rbxfile.TypeDouble: 477 | v, ok := ivalue.(float64) 478 | if !ok { 479 | return nil 480 | } 481 | return rbxfile.ValueDouble(v) 482 | case rbxfile.TypeUDim: 483 | v, ok := ivalue.(map[string]interface{}) 484 | if !ok { 485 | return nil 486 | } 487 | return rbxfile.ValueUDim{ 488 | Scale: float32(v["scale"].(float64)), 489 | Offset: int32(v["offset"].(float64)), 490 | } 491 | case rbxfile.TypeUDim2: 492 | v, ok := ivalue.(map[string]interface{}) 493 | if !ok { 494 | return nil 495 | } 496 | return rbxfile.ValueUDim2{ 497 | X: ValueFromJSONInterface(rbxfile.TypeUDim, v["x"]).(rbxfile.ValueUDim), 498 | Y: ValueFromJSONInterface(rbxfile.TypeUDim, v["y"]).(rbxfile.ValueUDim), 499 | } 500 | case rbxfile.TypeRay: 501 | v, ok := ivalue.(map[string]interface{}) 502 | if !ok { 503 | return nil 504 | } 505 | return rbxfile.ValueRay{ 506 | Origin: ValueFromJSONInterface(rbxfile.TypeVector3, v["origin"]).(rbxfile.ValueVector3), 507 | Direction: ValueFromJSONInterface(rbxfile.TypeVector3, v["direction"]).(rbxfile.ValueVector3), 508 | } 509 | case rbxfile.TypeFaces: 510 | v, ok := ivalue.(map[string]interface{}) 511 | if !ok { 512 | return nil 513 | } 514 | return rbxfile.ValueFaces{ 515 | Right: v["right"].(bool), 516 | Top: v["top"].(bool), 517 | Back: v["back"].(bool), 518 | Left: v["left"].(bool), 519 | Bottom: v["bottom"].(bool), 520 | Front: v["front"].(bool), 521 | } 522 | case rbxfile.TypeAxes: 523 | v, ok := ivalue.(map[string]interface{}) 524 | if !ok { 525 | return nil 526 | } 527 | return rbxfile.ValueAxes{ 528 | X: v["x"].(bool), 529 | Y: v["y"].(bool), 530 | Z: v["z"].(bool), 531 | } 532 | case rbxfile.TypeBrickColor: 533 | v, ok := ivalue.(float64) 534 | if !ok { 535 | return nil 536 | } 537 | return rbxfile.ValueBrickColor(uint32(v)) 538 | case rbxfile.TypeColor3: 539 | v, ok := ivalue.(map[string]interface{}) 540 | if !ok { 541 | return nil 542 | } 543 | return rbxfile.ValueColor3{ 544 | R: float32(v["r"].(float64)), 545 | G: float32(v["g"].(float64)), 546 | B: float32(v["b"].(float64)), 547 | } 548 | case rbxfile.TypeVector2: 549 | v, ok := ivalue.(map[string]interface{}) 550 | if !ok { 551 | return nil 552 | } 553 | return rbxfile.ValueVector2{ 554 | X: float32(v["x"].(float64)), 555 | Y: float32(v["y"].(float64)), 556 | } 557 | case rbxfile.TypeVector3: 558 | v, ok := ivalue.(map[string]interface{}) 559 | if !ok { 560 | return nil 561 | } 562 | return rbxfile.ValueVector3{ 563 | X: float32(v["x"].(float64)), 564 | Y: float32(v["y"].(float64)), 565 | Z: float32(v["z"].(float64)), 566 | } 567 | case rbxfile.TypeCFrame: 568 | v, ok := ivalue.(map[string]interface{}) 569 | if !ok { 570 | return nil 571 | } 572 | value := rbxfile.ValueCFrame{ 573 | Position: ValueFromJSONInterface(rbxfile.TypeVector3, v["position"]).(rbxfile.ValueVector3), 574 | } 575 | irotation, ok := v["rotation"].([]interface{}) 576 | if !ok { 577 | return value 578 | } 579 | for i, irot := range irotation { 580 | if i >= len(value.Rotation) { 581 | break 582 | } 583 | value.Rotation[i] = float32(irot.(float64)) 584 | } 585 | return value 586 | case rbxfile.TypeToken: 587 | v, ok := ivalue.(float64) 588 | if !ok { 589 | return nil 590 | } 591 | return rbxfile.ValueToken(uint32(v)) 592 | case rbxfile.TypeReference: 593 | v, ok := ivalue.(string) 594 | if !ok { 595 | return nil 596 | } 597 | // ValueReference is handled as a special case, so return as a 598 | // ValueString. 599 | return rbxfile.ValueString(v) 600 | case rbxfile.TypeVector3int16: 601 | v, ok := ivalue.(map[string]interface{}) 602 | if !ok { 603 | return nil 604 | } 605 | return rbxfile.ValueVector3int16{ 606 | X: int16(v["x"].(float64)), 607 | Y: int16(v["y"].(float64)), 608 | Z: int16(v["z"].(float64)), 609 | } 610 | case rbxfile.TypeVector2int16: 611 | v, ok := ivalue.(map[string]interface{}) 612 | if !ok { 613 | return nil 614 | } 615 | return rbxfile.ValueVector2int16{ 616 | X: int16(v["x"].(float64)), 617 | Y: int16(v["y"].(float64)), 618 | } 619 | case rbxfile.TypeNumberSequence: 620 | v, ok := ivalue.([]interface{}) 621 | if !ok { 622 | return nil 623 | } 624 | value := make(rbxfile.ValueNumberSequence, len(v)) 625 | for i, insk := range v { 626 | insk, ok := insk.(map[string]interface{}) 627 | if !ok { 628 | continue 629 | } 630 | value[i] = rbxfile.ValueNumberSequenceKeypoint{ 631 | Time: float32(insk["time"].(float64)), 632 | Value: float32(insk["value"].(float64)), 633 | Envelope: float32(insk["envelope"].(float64)), 634 | } 635 | } 636 | return value 637 | case rbxfile.TypeColorSequence: 638 | v, ok := ivalue.([]interface{}) 639 | if !ok { 640 | return nil 641 | } 642 | value := make(rbxfile.ValueColorSequence, len(v)) 643 | for i, icsk := range v { 644 | icsk, ok := icsk.(map[string]interface{}) 645 | if !ok { 646 | continue 647 | } 648 | value[i] = rbxfile.ValueColorSequenceKeypoint{ 649 | Time: float32(icsk["time"].(float64)), 650 | Value: ValueFromJSONInterface(rbxfile.TypeColor3, icsk["value"]).(rbxfile.ValueColor3), 651 | Envelope: float32(icsk["envelope"].(float64)), 652 | } 653 | } 654 | return value 655 | case rbxfile.TypeNumberRange: 656 | v, ok := ivalue.(map[string]interface{}) 657 | if !ok { 658 | return nil 659 | } 660 | return rbxfile.ValueNumberRange{ 661 | Min: float32(v["min"].(float64)), 662 | Max: float32(v["max"].(float64)), 663 | } 664 | case rbxfile.TypeRect: 665 | v, ok := ivalue.(map[string]interface{}) 666 | if !ok { 667 | return nil 668 | } 669 | return rbxfile.ValueRect{ 670 | Min: ValueFromJSONInterface(rbxfile.TypeVector2, v["min"]).(rbxfile.ValueVector2), 671 | Max: ValueFromJSONInterface(rbxfile.TypeVector2, v["max"]).(rbxfile.ValueVector2), 672 | } 673 | case rbxfile.TypePhysicalProperties: 674 | v, ok := ivalue.(map[string]interface{}) 675 | if !ok { 676 | return nil 677 | } 678 | return rbxfile.ValuePhysicalProperties{ 679 | CustomPhysics: v["custom_physics"].(bool), 680 | Density: float32(v["density"].(float64)), 681 | Friction: float32(v["friction"].(float64)), 682 | Elasticity: float32(v["elasticity"].(float64)), 683 | FrictionWeight: float32(v["friction_weight"].(float64)), 684 | ElasticityWeight: float32(v["elasticity_weight"].(float64)), 685 | } 686 | case rbxfile.TypeColor3uint8: 687 | v, ok := ivalue.(map[string]interface{}) 688 | if !ok { 689 | return nil 690 | } 691 | return rbxfile.ValueColor3uint8{ 692 | R: byte(v["r"].(float64)), 693 | G: byte(v["g"].(float64)), 694 | B: byte(v["b"].(float64)), 695 | } 696 | case rbxfile.TypeInt64: 697 | v, ok := ivalue.(float64) 698 | if !ok { 699 | return nil 700 | } 701 | return rbxfile.ValueInt64(int64(v)) 702 | case rbxfile.TypeSharedString: 703 | // TODO: Implement as shared data. 704 | v, ok := ivalue.(string) 705 | if !ok { 706 | return nil 707 | } 708 | buf := bytes.NewReader([]byte(v)) 709 | b, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding, buf)) 710 | if err != nil { 711 | return rbxfile.ValueSharedString(v) 712 | } 713 | return rbxfile.ValueSharedString(b) 714 | case rbxfile.TypeOptional: 715 | v, ok := ivalue.(map[string]interface{}) 716 | if !ok { 717 | return nil 718 | } 719 | t := rbxfile.TypeFromString(v["type"].(string)) 720 | if v["value"] == nil { 721 | return rbxfile.None(t) 722 | } 723 | return rbxfile.Some(ValueFromJSONInterface(t, v["value"])) 724 | } 725 | return nil 726 | } 727 | -------------------------------------------------------------------------------- /rbxl/README.md: -------------------------------------------------------------------------------- 1 | # rbxfile/rbxl 2 | 3 | [![GoDoc](https://godoc.org/github.com/robloxapi/rbxfile/rbxl?status.png)](https://godoc.org/github.com/robloxapi/rbxfile/rbxl) 4 | 5 | Package rbxl implements a decoder and encoder for Roblox's binary file format. 6 | 7 | This package registers the formats "rbxl" and "rbxm" to the rbxfile package. 8 | 9 | The easiest way to decode and encode files is through the functions 10 | [DeserializePlace][dserp], [SerializePlace][serp], [DeserializeModel][dserm], 11 | and [SerializeModel][serm]. These decode and encode directly between byte 12 | streams and Root structures specified by the rbxfile package. For most 13 | purposes, this is all that is required to read and write Roblox binary files. 14 | Further documentation gives an overview of how the package works internally. 15 | 16 | ## Overview 17 | 18 | A [Serializer][serzr] is used to transform data from byte streams to Root 19 | structures and back. A serializer specifies a decoder and encoder. Both a 20 | decoder and encoder combined is referred to as a "codec". 21 | 22 | Codecs transform data between a generic rbxfile.Root structure, and this 23 | package's "format model" structure. Custom codecs can be implemented. For 24 | example, you might wish to decode files normally, but encode them in an 25 | alternative way: 26 | 27 | ```go 28 | serializer := NewSerializer(nil, CustomEncoder) 29 | ``` 30 | 31 | Custom codecs can be used with a Serializer by implementing the 32 | [Decoder][decr] and [Encoder][encr] interfaces. Both do not need to be 33 | implemented. In the example above, passing nil as an argument causes the 34 | serializer to use the default "[RobloxCodec][roco]", which implements both a 35 | default decoder and encoder. This codec attempts to emulate how Roblox decodes 36 | and encodes its files. 37 | 38 | A [FormatModel][fmtm] is the representation of the file format itself, rather 39 | than the data it contains. The FormatModel is like a buffer between the byte 40 | stream and the Root structure. FormatModels can be encoded (and rarely, 41 | decoded) to and from Root structures in multiple ways, which is specified by 42 | codecs. However, there is only one way to encode and decode to and from a byte 43 | stream, which is handled by the FormatModel. 44 | 45 | [dserp]: https://godoc.org/github.com/robloxapi/rbxfile/rbxl#DeserializePlace 46 | [serp]: https://godoc.org/github.com/robloxapi/rbxfile/rbxl#SerializePlace 47 | [dserm]: https://godoc.org/github.com/robloxapi/rbxfile/rbxl#DeserializeModel 48 | [serm]: https://godoc.org/github.com/robloxapi/rbxfile/rbxl#SerializeModel 49 | 50 | [rbxfile]: https://godoc.org/github.com/robloxapi/rbxfile 51 | [root]: https://godoc.org/github.com/robloxapi/rbxfile#Root 52 | [serzr]: https://godoc.org/github.com/robloxapi/rbxfile/rbxl#Serializer 53 | [decr]: https://godoc.org/github.com/robloxapi/rbxfile/rbxl#Decoder 54 | [encr]: https://godoc.org/github.com/robloxapi/rbxfile/rbxl#Encoder 55 | [roco]: https://godoc.org/github.com/robloxapi/rbxfile/rbxl#RobloxCodec 56 | [fmtm]: https://godoc.org/github.com/robloxapi/rbxfile/rbxl#FormatModel 57 | -------------------------------------------------------------------------------- /rbxl/cframe.go: -------------------------------------------------------------------------------- 1 | package rbxl 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | func matrixFromID(i uint8) (m [9]float32) { 8 | i-- 9 | // Ignore IDs that produce invalid matrices. 0, which is wrapped to 255, 10 | // indicates a non-special CFrame, so it must also be ignored. 11 | if i >= 35 || i/6%3 == i%3 { 12 | return m 13 | } 14 | // Set directions of X and Y axes. 15 | m[i/6%3*3] = 1 - float32(i/18*2) 16 | m[i%6%3*3+1] = 1 - float32(i%6/3*2) 17 | // Set Z axis to cross product of X and Y. 18 | m[2] = m[3]*m[7] - m[4]*m[6] 19 | m[5] = m[6]*m[1] - m[7]*m[0] 20 | m[8] = m[0]*m[4] - m[1]*m[3] 21 | return m 22 | } 23 | 24 | var _0 = float32(math.Copysign(0, -1)) 25 | 26 | // DIFF: Unspecified IDs produce either invalid matrices or garbage values, so 27 | // these are assumed to be undefined. 28 | var cframeSpecialMatrix = map[uint8][9]float32{ 29 | // i: m if m := matrixFromID(i); m != [9]float32{} 30 | 0x02: {+1, +0, +0, +0, +1, +0, +0, +0, +1}, 31 | 0x03: {+1, +0, +0, +0, +0, -1, +0, +1, +0}, 32 | 0x05: {+1, +0, +0, +0, -1, +0, +0, +0, -1}, 33 | 0x06: {+1, +0, _0, +0, +0, +1, +0, -1, +0}, 34 | 0x07: {+0, +1, +0, +1, +0, +0, +0, +0, -1}, 35 | 0x09: {+0, +0, +1, +1, +0, +0, +0, +1, +0}, 36 | 0x0A: {+0, -1, +0, +1, +0, _0, +0, +0, +1}, 37 | 0x0C: {+0, +0, -1, +1, +0, +0, +0, -1, +0}, 38 | 0x0D: {+0, +1, +0, +0, +0, +1, +1, +0, +0}, 39 | 0x0E: {+0, +0, -1, +0, +1, +0, +1, +0, +0}, 40 | 0x10: {+0, -1, +0, +0, +0, -1, +1, +0, +0}, 41 | 0x11: {+0, +0, +1, +0, -1, +0, +1, +0, _0}, 42 | 0x14: {-1, +0, +0, +0, +1, +0, +0, +0, -1}, 43 | 0x15: {-1, +0, +0, +0, +0, +1, +0, +1, _0}, 44 | 0x17: {-1, +0, +0, +0, -1, +0, +0, +0, +1}, 45 | 0x18: {-1, +0, _0, +0, +0, -1, +0, -1, _0}, 46 | 0x19: {+0, +1, _0, -1, +0, +0, +0, +0, +1}, 47 | 0x1B: {+0, +0, -1, -1, +0, +0, +0, +1, +0}, 48 | 0x1C: {+0, -1, _0, -1, +0, _0, +0, +0, -1}, 49 | 0x1E: {+0, +0, +1, -1, +0, +0, +0, -1, +0}, 50 | 0x1F: {+0, +1, +0, +0, +0, -1, -1, +0, +0}, 51 | 0x20: {+0, +0, +1, +0, +1, _0, -1, +0, +0}, 52 | 0x22: {+0, -1, +0, +0, +0, +1, -1, +0, +0}, 53 | 0x23: {+0, +0, -1, +0, -1, _0, -1, +0, _0}, 54 | } 55 | 56 | var cframeSpecialNumber = map[[9]float32]uint8{ 57 | cframeSpecialMatrix[0x02]: 0x02, 58 | cframeSpecialMatrix[0x03]: 0x03, 59 | cframeSpecialMatrix[0x05]: 0x05, 60 | cframeSpecialMatrix[0x06]: 0x06, 61 | cframeSpecialMatrix[0x07]: 0x07, 62 | cframeSpecialMatrix[0x09]: 0x09, 63 | cframeSpecialMatrix[0x0A]: 0x0A, 64 | cframeSpecialMatrix[0x0C]: 0x0C, 65 | cframeSpecialMatrix[0x0D]: 0x0D, 66 | cframeSpecialMatrix[0x0E]: 0x0E, 67 | cframeSpecialMatrix[0x10]: 0x10, 68 | cframeSpecialMatrix[0x11]: 0x11, 69 | cframeSpecialMatrix[0x14]: 0x14, 70 | cframeSpecialMatrix[0x15]: 0x15, 71 | cframeSpecialMatrix[0x17]: 0x17, 72 | cframeSpecialMatrix[0x18]: 0x18, 73 | cframeSpecialMatrix[0x19]: 0x19, 74 | cframeSpecialMatrix[0x1B]: 0x1B, 75 | cframeSpecialMatrix[0x1C]: 0x1C, 76 | cframeSpecialMatrix[0x1E]: 0x1E, 77 | cframeSpecialMatrix[0x1F]: 0x1F, 78 | cframeSpecialMatrix[0x20]: 0x20, 79 | cframeSpecialMatrix[0x22]: 0x22, 80 | cframeSpecialMatrix[0x23]: 0x23, 81 | } 82 | -------------------------------------------------------------------------------- /rbxl/decoder.go: -------------------------------------------------------------------------------- 1 | package rbxl 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | 7 | "github.com/anaminus/parse" 8 | "github.com/robloxapi/rbxfile" 9 | "github.com/robloxapi/rbxfile/errors" 10 | "github.com/robloxapi/rbxfile/rbxlx" 11 | ) 12 | 13 | // DecoderStats contains statistics generated while decoding the format. 14 | type DecoderStats struct { 15 | XML bool // Whether the format is XML. 16 | Version uint16 // Version of the format. 17 | ClassCount uint32 // Number of classes reported by the header. 18 | InstanceCount uint32 // Number of instances reported by the header. 19 | Chunks int // Total number of chunks. 20 | ChunkTypes map[string]int // Number of chunks per signature. 21 | } 22 | 23 | // Decoder decodes a stream of bytes into an rbxfile.Root. 24 | type Decoder struct { 25 | // Mode indicates which type of format is decoded. 26 | Mode Mode 27 | 28 | // If NoXML is true, then the decoder will not attempt to decode the legacy 29 | // XML format for backward compatibility. 30 | NoXML bool 31 | 32 | // If not nil, stats will be set while decoding. 33 | Stats *DecoderStats 34 | } 35 | 36 | // Decode reads data from r and decodes it into root according to the rbxl 37 | // format. 38 | func (d Decoder) Decode(r io.Reader) (root *rbxfile.Root, warn, err error) { 39 | if r == nil { 40 | return nil, nil, errors.New("nil reader") 41 | } 42 | 43 | f, buf, w, err := d.decode(r, false) 44 | warn = errors.Union(warn, w) 45 | if err != nil { 46 | return nil, warn, err 47 | } 48 | if buf != nil { 49 | root, warn, err = rbxlx.Decoder{}.Decode(buf) 50 | if err != nil { 51 | return nil, warn, XMLError{Cause: err} 52 | } 53 | return root, warn, nil 54 | } 55 | 56 | // Run codec. 57 | codec := robloxCodec{Mode: d.Mode} 58 | root, w, err = codec.Decode(f) 59 | warn = errors.Union(warn, w) 60 | if err != nil { 61 | return nil, warn, err 62 | } 63 | return root, warn, nil 64 | } 65 | 66 | // Decompress reencodes the compressed chunks of the binary format as 67 | // uncompressed. The format is decoded from r, then encoded to w. 68 | // 69 | // Returns ErrXML if the the data is in the legacy XML format. 70 | func (d Decoder) Decompress(w io.Writer, r io.Reader) (warn, err error) { 71 | if r == nil { 72 | return nil, errors.New("nil reader") 73 | } 74 | 75 | f, buf, ws, err := d.decode(r, true) 76 | warn = errors.Union(warn, ws) 77 | if err != nil { 78 | return warn, err 79 | } 80 | if buf != nil { 81 | return warn, ErrXML 82 | } 83 | 84 | ws, err = Encoder{Mode: d.Mode, Uncompressed: true}.encode(w, f, true) 85 | warn = errors.Union(warn, ws) 86 | return warn, err 87 | } 88 | 89 | func decodeError(r *parse.BinaryReader, err error) error { 90 | r.Add(0, err) 91 | err = r.Err() 92 | if err != nil { 93 | return DataError{Offset: r.N(), Cause: err} 94 | } 95 | return nil 96 | } 97 | 98 | // decode parses the format. If the XML format is detected, then decode returns 99 | // a non-nil Reader with the original content, ready to be parsed by an XML 100 | // format decoder. 101 | func (d Decoder) decode(r io.Reader, dcomp bool) (f *formatModel, o io.Reader, warn, err error) { 102 | f = &formatModel{} 103 | fr := parse.NewBinaryReader(r) 104 | 105 | // Check signature. 106 | signature := make([]byte, len(robloxSig+binaryMarker)) 107 | if fr.Bytes(signature) { 108 | return f, nil, nil, decodeError(fr, nil) 109 | } 110 | if !bytes.Equal(signature[:len(robloxSig)], []byte(robloxSig)) { 111 | return f, nil, nil, decodeError(fr, errInvalidSig) 112 | } 113 | 114 | // Check for legacy XML. 115 | if !bytes.Equal(signature[len(robloxSig):], []byte(binaryMarker)) { 116 | if d.Stats != nil { 117 | d.Stats.XML = true 118 | } 119 | if d.NoXML { 120 | return nil, nil, nil, decodeError(fr, errInvalidSig) 121 | } else { 122 | // Reconstruct original reader. 123 | return nil, io.MultiReader(bytes.NewReader(signature), r), nil, nil 124 | } 125 | } 126 | 127 | // Check header magic. 128 | header := make([]byte, len(binaryHeader)) 129 | if fr.Bytes(header) { 130 | return nil, nil, nil, decodeError(fr, nil) 131 | } 132 | if !bytes.Equal(header, []byte(binaryHeader)) { 133 | return nil, nil, nil, decodeError(fr, errors.New("the file header is corrupted")) 134 | } 135 | 136 | // Check version. 137 | if fr.Number(&f.Version) { 138 | return nil, nil, nil, decodeError(fr, nil) 139 | } 140 | if d.Stats != nil { 141 | d.Stats.Version = f.Version 142 | } 143 | if f.Version != 0 { 144 | return nil, nil, nil, decodeError(fr, errUnrecognizedVersion(f.Version)) 145 | } 146 | 147 | // Get Class count. 148 | if fr.Number(&f.ClassCount) { 149 | return nil, nil, nil, decodeError(fr, nil) 150 | } 151 | if d.Stats != nil { 152 | d.Stats.ClassCount = f.ClassCount 153 | } 154 | f.groupLookup = make(map[int32]*chunkInstance, f.ClassCount) 155 | 156 | // Get Instance count. 157 | if fr.Number(&f.InstanceCount) { 158 | return nil, nil, nil, decodeError(fr, nil) 159 | } 160 | if d.Stats != nil { 161 | d.Stats.InstanceCount = f.InstanceCount 162 | } 163 | 164 | // Check reserved bytes. 165 | var reserved [8]byte 166 | if fr.Bytes(reserved[:]) { 167 | return nil, nil, nil, decodeError(fr, nil) 168 | } 169 | var warns errors.Errors 170 | if reserved != [8]byte{} { 171 | warns = append(warns, errReserve{Offset: fr.N() - int64(len(reserved)), Bytes: reserved[:]}) 172 | } 173 | 174 | // Decode chunks. 175 | if dcomp { 176 | if err = d.decompressChunks(f, fr); err != nil { 177 | return nil, nil, warns.Return(), err 178 | } 179 | } else { 180 | if err = d.decodeChunks(f, fr, &warns); err != nil { 181 | return nil, nil, warns.Return(), err 182 | } 183 | } 184 | 185 | // Handle trailing content. 186 | f.Trailing, _ = fr.All() 187 | 188 | if err = decodeError(fr, nil); err != nil { 189 | return nil, nil, warns.Return(), err 190 | } 191 | return f, nil, warns.Return(), nil 192 | } 193 | 194 | func (d Decoder) decodeChunks(f *formatModel, fr *parse.BinaryReader, warns *errors.Errors) (err error) { 195 | for i := 0; ; i++ { 196 | rawChunk := new(rawChunk) 197 | if rawChunk.Decode(fr) { 198 | return decodeError(fr, nil) 199 | } 200 | if d.Stats != nil { 201 | if d.Stats.ChunkTypes == nil { 202 | d.Stats.ChunkTypes = map[string]int{} 203 | } 204 | d.Stats.ChunkTypes[sig(rawChunk.signature).String()]++ 205 | } 206 | 207 | var n int64 208 | var err error 209 | var chunk chunk 210 | payload := bytes.NewReader(rawChunk.payload) 211 | switch rawChunk.signature { 212 | case sigMETA: 213 | ch := chunkMeta{} 214 | n, err = ch.Decode(payload) 215 | chunk = &ch 216 | case sigSSTR: 217 | ch := chunkSharedStrings{} 218 | n, err = ch.Decode(payload) 219 | chunk = &ch 220 | case sigINST: 221 | ch := chunkInstance{} 222 | n, err = ch.Decode(payload) 223 | chunk = &ch 224 | if err == nil { 225 | f.groupLookup[ch.ClassID] = &ch 226 | } 227 | case sigPROP: 228 | ch := chunkProperty{} 229 | n, err = ch.Decode(payload, f.groupLookup) 230 | chunk = &ch 231 | case sigPRNT: 232 | ch := chunkParent{} 233 | n, err = ch.Decode(payload) 234 | chunk = &ch 235 | case sigEND: 236 | ch := chunkEnd{} 237 | n, err = ch.Decode(payload) 238 | chunk = &ch 239 | default: 240 | chunk = &chunkUnknown{rawChunk: *rawChunk} 241 | *warns = warns.Append(ChunkError{Index: i, Sig: sig(rawChunk.signature), Cause: errUnknownChunkSig}) 242 | } 243 | 244 | chunk.SetCompressed(bool(rawChunk.compressed)) 245 | 246 | if err != nil { 247 | *warns = warns.Append(ChunkError{Index: i, Sig: sig(rawChunk.signature), Cause: err}) 248 | f.Chunks = append(f.Chunks, &chunkErrored{ 249 | chunk: chunk, 250 | Offset: n, 251 | Cause: err, 252 | Bytes: rawChunk.payload, 253 | }) 254 | continue 255 | } 256 | 257 | f.Chunks = append(f.Chunks, chunk) 258 | if d.Stats != nil { 259 | d.Stats.Chunks++ 260 | } 261 | 262 | if chunk, ok := chunk.(*chunkEnd); ok { 263 | if chunk.Compressed() { 264 | *warns = warns.Append(errEndChunkCompressed) 265 | } 266 | if !bytes.Equal(chunk.Content, []byte("")) { 267 | *warns = warns.Append(errEndChunkContent) 268 | } 269 | break 270 | } 271 | } 272 | 273 | return nil 274 | } 275 | 276 | func (d Decoder) decompressChunks(f *formatModel, fr *parse.BinaryReader) (err error) { 277 | for i := 0; ; i++ { 278 | rawChunk := new(rawChunk) 279 | if rawChunk.Decode(fr) { 280 | return decodeError(fr, nil) 281 | } 282 | if d.Stats != nil { 283 | if d.Stats.ChunkTypes == nil { 284 | d.Stats.ChunkTypes = map[string]int{} 285 | } 286 | d.Stats.ChunkTypes[sig(rawChunk.signature).String()]++ 287 | } 288 | 289 | chunk := &chunkUnknown{rawChunk: *rawChunk} 290 | chunk.SetCompressed(bool(rawChunk.compressed)) 291 | f.Chunks = append(f.Chunks, chunk) 292 | if d.Stats != nil { 293 | d.Stats.Chunks++ 294 | } 295 | 296 | if rawChunk.signature == sigEND { 297 | break 298 | } 299 | } 300 | return nil 301 | } 302 | -------------------------------------------------------------------------------- /rbxl/dump.go: -------------------------------------------------------------------------------- 1 | package rbxl 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "strconv" 9 | "unicode" 10 | 11 | "github.com/robloxapi/rbxfile/errors" 12 | ) 13 | 14 | // Dump writes to w a readable representation of the binary format decoded from 15 | // r. 16 | // 17 | // Returns ErrXML if the the data is in the legacy XML format. 18 | func (d Decoder) Dump(w io.Writer, r io.Reader) (warn, err error) { 19 | if r == nil { 20 | return nil, errors.New("nil reader") 21 | } 22 | if w == nil { 23 | return nil, errors.New("nil writer") 24 | } 25 | 26 | f, buf, ws, err := d.decode(r, false) 27 | warn = errors.Union(warn, ws) 28 | if err != nil { 29 | return warn, err 30 | } 31 | if buf != nil { 32 | return warn, ErrXML 33 | } 34 | 35 | classes := map[int32]*chunkInstance{} 36 | 37 | bw := bufio.NewWriter(w) 38 | fmt.Fprintf(bw, "Version: %d", f.Version) 39 | fmt.Fprintf(bw, "\nClasses: %d", f.ClassCount) 40 | fmt.Fprintf(bw, "\nInstances: %d", f.InstanceCount) 41 | fmt.Fprint(bw, "\nChunks: {") 42 | for i, chunk := range f.Chunks { 43 | dumpChunk(bw, 1, i, chunk, classes) 44 | } 45 | fmt.Fprint(bw, "\n}") 46 | 47 | bw.Flush() 48 | return warn, nil 49 | } 50 | 51 | func dumpChunk(w *bufio.Writer, indent, i int, chunk chunk, classes map[int32]*chunkInstance) { 52 | dumpNewline(w, indent) 53 | if i >= 0 { 54 | fmt.Fprintf(w, "#%d: ", i) 55 | } 56 | dumpSig(w, chunk.Signature()) 57 | if chunk.Compressed() { 58 | w.WriteString(" (compressed) {") 59 | } else { 60 | w.WriteString(" (uncompressed) {") 61 | } 62 | switch chunk := chunk.(type) { 63 | case *chunkMeta: 64 | dumpNewline(w, indent+1) 65 | fmt.Fprintf(w, "Count: %d", len(chunk.Values)) 66 | for _, p := range chunk.Values { 67 | dumpNewline(w, indent+1) 68 | w.WriteByte('{') 69 | dumpNewline(w, indent+2) 70 | w.WriteString("Key: ") 71 | dumpString(w, indent+2, p[0]) 72 | dumpNewline(w, indent+2) 73 | w.WriteString("Value: ") 74 | dumpString(w, indent+2, p[1]) 75 | dumpNewline(w, indent+1) 76 | w.WriteByte('}') 77 | } 78 | case *chunkSharedStrings: 79 | dumpNewline(w, indent+1) 80 | fmt.Fprintf(w, "Version: %d", chunk.Version) 81 | dumpNewline(w, indent+1) 82 | fmt.Fprintf(w, "Values: (count:%d) {", len(chunk.Values)) 83 | for i, s := range chunk.Values { 84 | dumpNewline(w, indent+2) 85 | fmt.Fprintf(w, "%d: {", i) 86 | dumpNewline(w, indent+3) 87 | w.WriteString("Hash: ") 88 | dumpBytes(w, indent+3, s.Hash[:]) 89 | dumpNewline(w, indent+3) 90 | w.WriteString("Value: ") 91 | dumpBytes(w, indent+3, s.Value) 92 | dumpNewline(w, indent+2) 93 | w.WriteByte('}') 94 | } 95 | dumpNewline(w, indent+1) 96 | w.WriteByte('}') 97 | case *chunkInstance: 98 | classes[chunk.ClassID] = chunk 99 | dumpNewline(w, indent+1) 100 | fmt.Fprintf(w, "ClassID: %d", chunk.ClassID) 101 | dumpNewline(w, indent+1) 102 | w.WriteString("ClassName: ") 103 | dumpString(w, indent+1, chunk.ClassName) 104 | if chunk.IsService { 105 | dumpNewline(w, indent+1) 106 | fmt.Fprintf(w, "InstanceIDs: (count:%d) (service) {", len(chunk.InstanceIDs)) 107 | for i, id := range chunk.InstanceIDs { 108 | dumpNewline(w, indent+2) 109 | if i >= len(chunk.GetService) { 110 | fmt.Fprintf(w, "%d: %d (invalid)", i, id) 111 | } else { 112 | fmt.Fprintf(w, "%d: %d (%d)", i, id, chunk.GetService[i]) 113 | } 114 | } 115 | dumpNewline(w, indent+1) 116 | w.WriteByte('}') 117 | } else { 118 | dumpNewline(w, indent+1) 119 | fmt.Fprintf(w, "InstanceIDs: (count:%d) {", len(chunk.InstanceIDs)) 120 | for i, id := range chunk.InstanceIDs { 121 | dumpNewline(w, indent+2) 122 | fmt.Fprintf(w, "%d: %d", i, id) 123 | } 124 | dumpNewline(w, indent+1) 125 | w.WriteByte('}') 126 | } 127 | case *chunkProperty: 128 | dumpNewline(w, indent+1) 129 | fmt.Fprintf(w, "ClassID: %d", chunk.ClassID) 130 | if inst, ok := classes[chunk.ClassID]; ok { 131 | fmt.Fprintf(w, " (count:%d, ", len(inst.InstanceIDs)) 132 | dumpString(w, indent+1, inst.ClassName) 133 | w.WriteByte(')') 134 | } 135 | dumpNewline(w, indent+1) 136 | w.WriteString("PropertyName: ") 137 | dumpString(w, indent+1, chunk.PropertyName) 138 | if chunk.Properties != nil { 139 | t := chunk.Properties.Type() 140 | length := chunk.Properties.Len() 141 | dumpNewline(w, indent+1) 142 | fmt.Fprintf(w, "Properties: (count:%d, (type:%d) %s) ", length, t, t.String()) 143 | if a, ok := chunk.Properties.(arrayDumper); ok { 144 | a.Dump(w, indent+1) 145 | } else { 146 | length := chunk.Properties.Len() 147 | w.WriteByte('{') 148 | for i := 0; i < length; i++ { 149 | v := chunk.Properties.Get(i) 150 | dumpNewline(w, indent+2) 151 | fmt.Fprintf(w, "%d: ", i) 152 | v.Dump(w, indent+2) 153 | } 154 | dumpNewline(w, indent+1) 155 | w.WriteByte('}') 156 | } 157 | } 158 | case *chunkParent: 159 | dumpNewline(w, indent+1) 160 | fmt.Fprintf(w, "Version: %d", chunk.Version) 161 | dumpNewline(w, indent+1) 162 | w.WriteString("Values (child : parent)") 163 | c := len(chunk.Children) 164 | p := len(chunk.Parents) 165 | n := c 166 | if p > c { 167 | n = p 168 | } 169 | for i := 0; i < n; i++ { 170 | dumpNewline(w, indent+2) 171 | if i >= c { 172 | w.WriteString("invalid") 173 | } else { 174 | fmt.Fprintf(w, "%d", chunk.Children[i]) 175 | } 176 | w.WriteString(" : ") 177 | if i >= p { 178 | w.WriteString("invalid") 179 | } else { 180 | fmt.Fprintf(w, "%d", chunk.Parents[i]) 181 | } 182 | } 183 | case *chunkEnd: 184 | dumpNewline(w, indent+1) 185 | w.WriteString("Content: ") 186 | dumpString(w, indent+1, string(chunk.Content)) 187 | case *chunkUnknown: 188 | dumpNewline(w, indent+1) 189 | w.WriteString("\n\t\tBytes: ") 190 | dumpBytes(w, indent+1, chunk.payload) 191 | case *chunkErrored: 192 | dumpNewline(w, indent+1) 193 | w.WriteString("") 194 | dumpNewline(w, indent+1) 195 | fmt.Fprintf(w, "Offset: %d", chunk.Offset) 196 | dumpNewline(w, indent+1) 197 | fmt.Fprintf(w, "Error: %s", chunk.Cause) 198 | dumpNewline(w, indent+1) 199 | w.WriteString("Chunk:") 200 | dumpChunk(w, indent+2, -1, chunk.chunk, classes) 201 | dumpNewline(w, indent+1) 202 | w.WriteString("Bytes: ") 203 | dumpBytes(w, indent+1, chunk.Bytes) 204 | } 205 | dumpNewline(w, indent) 206 | fmt.Fprint(w, "}") 207 | } 208 | 209 | func dumpNewline(w *bufio.Writer, indent int) { 210 | w.WriteByte('\n') 211 | for i := 0; i < indent; i++ { 212 | w.WriteByte('\t') 213 | } 214 | } 215 | 216 | func dumpSig(w *bufio.Writer, sig sig) { 217 | var b [4]byte 218 | binary.LittleEndian.PutUint32(b[:], uint32(sig)) 219 | for _, c := range b { 220 | if unicode.IsPrint(rune(c)) { 221 | w.WriteByte(c) 222 | } else { 223 | w.WriteByte('.') 224 | } 225 | } 226 | fmt.Fprintf(w, " (% 02X)", b) 227 | } 228 | 229 | func dumpString(w *bufio.Writer, indent int, s string) { 230 | for _, r := range s { 231 | if !unicode.IsGraphic(r) { 232 | dumpBytes(w, indent, []byte(s)) 233 | return 234 | } 235 | } 236 | fmt.Fprintf(w, "(len:%d) ", len(s)) 237 | w.WriteString(strconv.Quote(s)) 238 | } 239 | 240 | func dumpBytes(w *bufio.Writer, indent int, b []byte) { 241 | fmt.Fprintf(w, "(len:%d)", len(b)) 242 | const width = 16 243 | for j := 0; j < len(b); j += width { 244 | dumpNewline(w, indent+1) 245 | w.WriteString("| ") 246 | for i := j; i < j+width; { 247 | if i < len(b) { 248 | s := strconv.FormatUint(uint64(b[i]), 16) 249 | if len(s) == 1 { 250 | w.WriteString("0") 251 | } 252 | w.WriteString(s) 253 | } else if len(b) < width { 254 | break 255 | } else { 256 | w.WriteString(" ") 257 | } 258 | i++ 259 | if i%8 == 0 && i < j+width { 260 | w.WriteString(" ") 261 | } else { 262 | w.WriteString(" ") 263 | } 264 | } 265 | w.WriteString("|") 266 | n := len(b) 267 | if j+width < n { 268 | n = j + width 269 | } 270 | for i := j; i < n; i++ { 271 | if 32 <= b[i] && b[i] <= 126 { 272 | w.WriteRune(rune(b[i])) 273 | } else { 274 | w.WriteByte('.') 275 | } 276 | } 277 | w.WriteByte('|') 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /rbxl/encoder.go: -------------------------------------------------------------------------------- 1 | package rbxl 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | 7 | "github.com/anaminus/parse" 8 | "github.com/robloxapi/rbxfile" 9 | "github.com/robloxapi/rbxfile/errors" 10 | ) 11 | 12 | // Encoder encodes a rbxfile.Root into a stream of bytes. 13 | type Encoder struct { 14 | // Mode indicates which type of format is encoded. 15 | Mode Mode 16 | 17 | // Uncompressed sets whether compression is forcibly disabled for all 18 | // chunks. 19 | Uncompressed bool 20 | } 21 | 22 | // Encode formats root according to the rbxl format, and writers it to w. 23 | func (e Encoder) Encode(w io.Writer, root *rbxfile.Root) (warn, err error) { 24 | if w == nil { 25 | return nil, errors.New("nil writer") 26 | } 27 | 28 | codec := robloxCodec{Mode: e.Mode} 29 | f, ws, err := codec.Encode(root) 30 | warn = errors.Union(warn, ws) 31 | if err != nil { 32 | return warn, CodecError{Cause: err} 33 | } 34 | 35 | return e.encode(w, f, false) 36 | } 37 | 38 | func encodeError(w *parse.BinaryWriter, err error) error { 39 | w.Add(0, err) 40 | err = w.Err() 41 | if errs, ok := err.(errors.Errors); ok && len(errs) == 0 { 42 | err = nil 43 | } 44 | if err != nil { 45 | return DataError{Offset: w.N(), Cause: err} 46 | } 47 | return nil 48 | } 49 | 50 | func (e Encoder) encode(w io.Writer, f *formatModel, dcomp bool) (warn, err error) { 51 | var warns errors.Errors 52 | 53 | fw := parse.NewBinaryWriter(w) 54 | 55 | if fw.Bytes([]byte(robloxSig + binaryMarker + binaryHeader)) { 56 | return warns.Return(), encodeError(fw, nil) 57 | } 58 | 59 | if fw.Number(f.Version) { 60 | return warns.Return(), encodeError(fw, nil) 61 | } 62 | 63 | if fw.Number(f.ClassCount) { 64 | return warns.Return(), encodeError(fw, nil) 65 | } 66 | 67 | if fw.Number(f.InstanceCount) { 68 | return warns.Return(), encodeError(fw, nil) 69 | } 70 | 71 | // reserved 72 | if fw.Number(uint64(0)) { 73 | return warns.Return(), encodeError(fw, nil) 74 | } 75 | 76 | for i, chunk := range f.Chunks { 77 | if !validChunk(chunk.Signature()) && !dcomp { 78 | warns = append(warns, ChunkError{Index: i, Sig: chunk.Signature(), Cause: errUnknownChunkSig}) 79 | } 80 | if endChunk, ok := chunk.(*chunkEnd); ok { 81 | if !e.Uncompressed && endChunk.Compressed() { 82 | warns = append(warns, errEndChunkCompressed) 83 | } 84 | 85 | if !bytes.Equal(endChunk.Content, []byte("")) && !dcomp { 86 | warns = append(warns, errEndChunkContent) 87 | } 88 | 89 | if i != len(f.Chunks)-1 && !dcomp { 90 | warns = append(warns, errEndChunkNotLast) 91 | } 92 | } 93 | 94 | rawChunk := new(rawChunk) 95 | rawChunk.signature = uint32(chunk.Signature()) 96 | if !e.Uncompressed { 97 | rawChunk.compressed = compressed(chunk.Compressed()) 98 | } 99 | 100 | buf := new(bytes.Buffer) 101 | if fw.Add(chunk.WriteTo(buf)) { 102 | return warns.Return(), encodeError(fw, nil) 103 | } 104 | 105 | rawChunk.payload = buf.Bytes() 106 | 107 | if rawChunk.WriteTo(fw) { 108 | return warns.Return(), encodeError(fw, nil) 109 | } 110 | } 111 | 112 | fw.Bytes(f.Trailing) 113 | 114 | return warns.Return(), encodeError(fw, nil) 115 | } 116 | -------------------------------------------------------------------------------- /rbxl/errors.go: -------------------------------------------------------------------------------- 1 | package rbxl 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | // Indicates an unexpected file signature. 12 | errInvalidSig = errors.New("invalid signature") 13 | // Indicates a chunk signature not known by the codec. 14 | errUnknownChunkSig = errors.New("unknown chunk signature") 15 | // Indicates that the end chunk is compressed, where it is expected to be 16 | // uncompressed. 17 | errEndChunkCompressed = errors.New("end chunk is compressed") 18 | // Indicates unexpected content within the end chunk. 19 | errEndChunkContent = errors.New("end chunk content is not ``") 20 | // Indicates that there are additional chunks that follow the end chunk. 21 | errEndChunkNotLast = errors.New("end chunk is not the last chunk") 22 | ) 23 | 24 | // errUnrecognizedVersion indicates a format version not recognized by the 25 | // codec. 26 | type errUnrecognizedVersion uint16 27 | 28 | func (err errUnrecognizedVersion) Error() string { 29 | return fmt.Sprintf("unrecognized version %d", err) 30 | } 31 | 32 | // errUnknownType indicates a property data type not known by the codec. 33 | type errUnknownType typeID 34 | 35 | func (err errUnknownType) Error() string { 36 | return fmt.Sprintf("unknown data type 0x%X", byte(err)) 37 | } 38 | 39 | // errReserve indicates an unexpected value for bytes that are presumed to be 40 | // reserved. 41 | type errReserve struct { 42 | // Offset marks the location of the reserved bytes. 43 | Offset int64 44 | // Bytes is the unexpected content of the reserved bytes. 45 | Bytes []byte 46 | } 47 | 48 | func (err errReserve) Error() string { 49 | return fmt.Sprintf("unexpected content for reserved bytes near %d: % 02X", err.Offset, err.Bytes) 50 | } 51 | 52 | type errParentArray struct { 53 | Children int 54 | Parent int 55 | } 56 | 57 | func (err errParentArray) Error() string { 58 | return fmt.Sprintf("length of parents array (%d) does not match length of children array (%d)", err.Parent, err.Children) 59 | } 60 | 61 | // ErrXML indicates the unexpected detection of the legacy XML format. 62 | var ErrXML = errors.New("unexpected XML format") 63 | 64 | // ValueError is an error that is produced by a Value of a certain Type. 65 | type ValueError struct { 66 | Type byte 67 | 68 | Cause error 69 | } 70 | 71 | func (err ValueError) Error() string { 72 | return fmt.Sprintf("type %s (0x%X): %s", typeID(err.Type).String(), err.Type, err.Cause.Error()) 73 | } 74 | 75 | func (err ValueError) Unwrap() error { 76 | return err.Cause 77 | } 78 | 79 | // XMLError wraps an error that occurred while parsing the legacy XML format. 80 | type XMLError struct { 81 | Cause error 82 | } 83 | 84 | func (err XMLError) Error() string { 85 | if err.Cause == nil { 86 | return "decoding XML" 87 | } 88 | return "decoding XML: " + err.Cause.Error() 89 | } 90 | 91 | func (err XMLError) Unwrap() error { 92 | return err.Cause 93 | } 94 | 95 | // CodecError wraps an error that occurred while encoding or decoding a binary 96 | // format model. 97 | type CodecError struct { 98 | Cause error 99 | } 100 | 101 | func (err CodecError) Error() string { 102 | if err.Cause == nil { 103 | return "codec error" 104 | } 105 | return "codec error: " + err.Cause.Error() 106 | } 107 | 108 | func (err CodecError) Unwrap() error { 109 | return err.Cause 110 | } 111 | 112 | // DataError wraps an error that occurred while encoding or decoding byte data. 113 | type DataError struct { 114 | // Offset is the byte offset where the error occurred. 115 | Offset int64 116 | 117 | Cause error 118 | } 119 | 120 | func (err DataError) Error() string { 121 | var s strings.Builder 122 | s.WriteString("data error") 123 | if err.Offset >= 0 { 124 | s.WriteString(" at ") 125 | s.Write(strconv.AppendInt(nil, err.Offset, 10)) 126 | } 127 | if err.Cause != nil { 128 | s.WriteString(": ") 129 | s.WriteString(err.Cause.Error()) 130 | } 131 | return s.String() 132 | } 133 | 134 | func (err DataError) Unwrap() error { 135 | return err.Cause 136 | } 137 | 138 | // ChunkError indicates an error that occurred within a chunk. 139 | type ChunkError struct { 140 | // Index is the position of the chunk within the file. 141 | Index int 142 | // Sig is the signature of the chunk. 143 | Sig sig 144 | 145 | Cause error 146 | } 147 | 148 | func (err ChunkError) Error() string { 149 | if err.Index < 0 { 150 | return fmt.Sprintf("%q chunk: %s", err.Sig.String(), err.Cause.Error()) 151 | } 152 | return fmt.Sprintf("#%d %q chunk: %s", err.Index, err.Sig.String(), err.Cause.Error()) 153 | } 154 | 155 | func (err ChunkError) Unwrap() error { 156 | return err.Cause 157 | } 158 | -------------------------------------------------------------------------------- /rbxl/format.go: -------------------------------------------------------------------------------- 1 | // Package rbxl implements a decoder and encoder for Roblox's binary file 2 | // format. 3 | package rbxl 4 | 5 | // Mode indicates how the codec formats data. 6 | type Mode uint8 7 | 8 | const ( 9 | Place Mode = iota // Data is handled as a Roblox place (RBXL) file. 10 | Model // Data is handled as a Roblox model (RBXM) file. 11 | ) 12 | -------------------------------------------------------------------------------- /rbxl/format_test.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package rbxl 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "testing" 9 | 10 | "github.com/robloxapi/rbxfile" 11 | ) 12 | 13 | const goodfile = " 0 { 389 | raw := make([]byte, groupLength*4) 390 | if fr.Bytes(raw) { 391 | return fr.End() 392 | } 393 | 394 | values, err := refArrayFromBytes(raw, int(groupLength)) 395 | if fr.Add(0, err) { 396 | return fr.End() 397 | } 398 | 399 | for i, v := range values { 400 | c.InstanceIDs[i] = int32(v) 401 | } 402 | } 403 | 404 | if c.IsService { 405 | c.GetService = make([]byte, groupLength) 406 | if fr.Bytes(c.GetService) { 407 | return fr.End() 408 | } 409 | } 410 | 411 | return fr.End() 412 | } 413 | 414 | func (c *chunkInstance) WriteTo(w io.Writer) (n int64, err error) { 415 | fw := parse.NewBinaryWriter(w) 416 | 417 | if fw.Number(c.ClassID) { 418 | return fw.End() 419 | } 420 | 421 | if writeString(fw, c.ClassName) { 422 | return fw.End() 423 | } 424 | 425 | var isService uint8 = 0 426 | if c.IsService { 427 | isService = 1 428 | } 429 | if fw.Number(isService) { 430 | return fw.End() 431 | } 432 | 433 | if fw.Number(uint32(len(c.InstanceIDs))) { 434 | return fw.End() 435 | } 436 | 437 | if len(c.InstanceIDs) > 0 { 438 | if fw.Bytes(refArrayToBytes(c.InstanceIDs)) { 439 | return fw.End() 440 | } 441 | } 442 | 443 | if c.IsService { 444 | if fw.Bytes(c.GetService) { 445 | return fw.End() 446 | } 447 | } 448 | 449 | return fw.End() 450 | } 451 | 452 | //////////////////////////////////////////////////////////////// 453 | 454 | const sigEND = 0x00_44_4E_45 // \0DNE 455 | 456 | // chunkEnd is a Chunk that signals the end of the file. It causes the decoder 457 | // to stop reading chunks, so it should be the last chunk. 458 | type chunkEnd struct { 459 | compressed 460 | 461 | // The raw decompressed content of the chunk. For maximum compatibility, 462 | // the content should be "", and the chunk should be 463 | // uncompressed. The decoder will emit warnings indicating such, if this 464 | // is not the case. 465 | Content []byte 466 | } 467 | 468 | func (chunkEnd) Signature() sig { 469 | return sigEND 470 | } 471 | 472 | func (c *chunkEnd) Decode(r io.Reader) (n int64, err error) { 473 | fr := parse.NewBinaryReader(r) 474 | 475 | c.Content, _ = fr.All() 476 | 477 | return fr.End() 478 | } 479 | 480 | func (c *chunkEnd) WriteTo(w io.Writer) (n int64, err error) { 481 | fw := parse.NewBinaryWriter(w) 482 | 483 | fw.Bytes(c.Content) 484 | 485 | return fw.End() 486 | } 487 | 488 | //////////////////////////////////////////////////////////////// 489 | 490 | const sigPRNT = 0x54_4E_52_50 // TNRP 491 | 492 | // chunkParent is a Chunk that contains information about the parent-child 493 | // relationships between instances in the model. 494 | type chunkParent struct { 495 | compressed 496 | 497 | // Version is the version of the chunk. Reserved so that the format of the 498 | // parent chunk can be changed without changing the version of the entire 499 | // file format. 500 | Version uint8 501 | 502 | // Children is a list of instances referred to by instance ID. The length 503 | // of this array should be equal to InstanceCount. 504 | Children []int32 505 | 506 | // Parents is a list of instances, referred to by instance ID, that 507 | // indicate the Parent of the corresponding instance in the Children 508 | // array. The length of this array should be equal to the length of 509 | // Children. 510 | Parents []int32 511 | } 512 | 513 | func (chunkParent) Signature() sig { 514 | return sigPRNT 515 | } 516 | 517 | func (c *chunkParent) Decode(r io.Reader) (n int64, err error) { 518 | fr := parse.NewBinaryReader(r) 519 | 520 | if fr.Number(&c.Version) { 521 | return fr.End() 522 | } 523 | 524 | var instanceCount uint32 525 | if fr.Number(&instanceCount) { 526 | return fr.End() 527 | } 528 | 529 | c.Children = make([]int32, instanceCount) 530 | if instanceCount > 0 { 531 | raw := make([]byte, instanceCount*4) 532 | if fr.Bytes(raw) { 533 | return fr.End() 534 | } 535 | 536 | values, err := refArrayFromBytes(raw, int(instanceCount)) 537 | if fr.Add(0, err) { 538 | return fr.End() 539 | } 540 | 541 | for i, v := range values { 542 | c.Children[i] = int32(v) 543 | } 544 | } 545 | 546 | c.Parents = make([]int32, instanceCount) 547 | if instanceCount > 0 { 548 | raw := make([]byte, instanceCount*4) 549 | if fr.Bytes(raw) { 550 | return fr.End() 551 | } 552 | 553 | values, err := refArrayFromBytes(raw, int(instanceCount)) 554 | if fr.Add(0, err) { 555 | return fr.End() 556 | } 557 | 558 | for i, v := range values { 559 | c.Parents[i] = int32(v) 560 | } 561 | } 562 | 563 | return fr.End() 564 | } 565 | 566 | func (c *chunkParent) WriteTo(w io.Writer) (n int64, err error) { 567 | fw := parse.NewBinaryWriter(w) 568 | 569 | if fw.Number(c.Version) { 570 | return fw.End() 571 | } 572 | 573 | var instanceCount = len(c.Children) 574 | if len(c.Parents) != instanceCount { 575 | fw.Add(0, errParentArray{Children: instanceCount, Parent: len(c.Parents)}) 576 | return fw.End() 577 | } 578 | if fw.Number(uint32(instanceCount)) { 579 | return fw.End() 580 | } 581 | if instanceCount > 0 { 582 | if fw.Bytes(refArrayToBytes(c.Children)) { 583 | return fw.End() 584 | } 585 | if fw.Bytes(refArrayToBytes(c.Parents)) { 586 | return fw.End() 587 | } 588 | } 589 | 590 | return fw.End() 591 | } 592 | 593 | //////////////////////////////////////////////////////////////// 594 | 595 | const sigPROP = 0x50_4F_52_50 // PORP 596 | 597 | // chunkProperty is a Chunk that contains information about the properties of 598 | // a group of instances. 599 | type chunkProperty struct { 600 | compressed 601 | 602 | // ClassID is the ID of an instance group contained in a ChunkInstance. 603 | ClassID int32 604 | 605 | // PropertyName is the name of a valid property in each instance of the 606 | // corresponding instance group. 607 | PropertyName string 608 | 609 | // Properties is a list of Values of the given DataType. Each value in the 610 | // array corresponds to the property of an instance in the specified 611 | // group. 612 | Properties array 613 | } 614 | 615 | func (chunkProperty) Signature() sig { 616 | return sigPROP 617 | } 618 | 619 | func (c *chunkProperty) Decode(r io.Reader, groupLookup map[int32]*chunkInstance) (n int64, err error) { 620 | fr := parse.NewBinaryReader(r) 621 | 622 | if fr.Number(&c.ClassID) { 623 | return fr.End() 624 | } 625 | inst, ok := groupLookup[c.ClassID] 626 | if !ok { 627 | fr.Add(0, fmt.Errorf("unknown ID `%d`", c.ClassID)) 628 | return fr.End() 629 | } 630 | 631 | if readString(fr, &c.PropertyName) { 632 | return fr.End() 633 | } 634 | 635 | rawBytes, failed := fr.All() 636 | if failed { 637 | return fr.End() 638 | } 639 | 640 | if len(rawBytes) == 0 { 641 | // No value data. 642 | c.Properties = nil 643 | return fr.End() 644 | } 645 | 646 | if c.Properties, _, err = typeArrayFromBytes(rawBytes, len(inst.InstanceIDs)); err != nil { 647 | if c.Properties != nil { 648 | fr.Add(0, ValueError{Type: byte(c.Properties.Type()), Cause: err}) 649 | } else { 650 | fr.Add(0, err) 651 | } 652 | return fr.End() 653 | } 654 | 655 | return fr.End() 656 | } 657 | 658 | func (c *chunkProperty) WriteTo(w io.Writer) (n int64, err error) { 659 | fw := parse.NewBinaryWriter(w) 660 | 661 | if fw.Number(c.ClassID) { 662 | return fw.End() 663 | } 664 | 665 | if writeString(fw, c.PropertyName) { 666 | return fw.End() 667 | } 668 | 669 | if c.Properties == nil { 670 | // No value data. 671 | return fw.End() 672 | } 673 | 674 | rawBytes := make([]byte, 0, zb+c.Properties.BytesLen()) 675 | rawBytes, err = typeArrayToBytes(rawBytes, c.Properties) 676 | if err != nil { 677 | fw.Add(0, err) 678 | return fw.End() 679 | } 680 | fw.Bytes(rawBytes) 681 | 682 | return fw.End() 683 | } 684 | 685 | //////////////////////////////////////////////////////////////// 686 | 687 | const sigMETA = 0x41_54_45_4D // ATEM 688 | 689 | // chunkMeta is a Chunk that contains file metadata. 690 | type chunkMeta struct { 691 | compressed 692 | 693 | Values [][2]string 694 | } 695 | 696 | func (chunkMeta) Signature() sig { 697 | return sigMETA 698 | } 699 | 700 | func (c *chunkMeta) Decode(r io.Reader) (n int64, err error) { 701 | fr := parse.NewBinaryReader(r) 702 | 703 | var size uint32 704 | if fr.Number(&size) { 705 | return fr.End() 706 | } 707 | c.Values = make([][2]string, int(size)) 708 | 709 | for i := range c.Values { 710 | if readString(fr, &c.Values[i][0]) { 711 | return fr.End() 712 | } 713 | if readString(fr, &c.Values[i][1]) { 714 | return fr.End() 715 | } 716 | } 717 | 718 | return fr.End() 719 | } 720 | 721 | func (c *chunkMeta) WriteTo(w io.Writer) (n int64, err error) { 722 | fw := parse.NewBinaryWriter(w) 723 | 724 | if fw.Number(uint32(len(c.Values))) { 725 | return fw.End() 726 | } 727 | 728 | for _, pair := range c.Values { 729 | if writeString(fw, pair[0]) { 730 | return fw.End() 731 | } 732 | if writeString(fw, pair[1]) { 733 | return fw.End() 734 | } 735 | } 736 | 737 | return fw.End() 738 | } 739 | 740 | //////////////////////////////////////////////////////////////// 741 | 742 | const sigSSTR = 0x52_54_53_53 // RTSS 743 | 744 | // chunkSharedStrings is a Chunk that contains shared strings. 745 | type chunkSharedStrings struct { 746 | compressed 747 | 748 | Version uint32 749 | Values []sharedString 750 | } 751 | 752 | type sharedString struct { 753 | Hash [16]byte 754 | Value []byte 755 | } 756 | 757 | func (chunkSharedStrings) Signature() sig { 758 | return sigSSTR 759 | } 760 | 761 | func (c *chunkSharedStrings) Decode(r io.Reader) (n int64, err error) { 762 | fr := parse.NewBinaryReader(r) 763 | 764 | if fr.Number(&c.Version) { 765 | return fr.End() 766 | } 767 | // TODO: validate version? 768 | 769 | var length uint32 770 | if fr.Number(&length) { 771 | return fr.End() 772 | } 773 | c.Values = make([]sharedString, int(length)) 774 | 775 | for i := range c.Values { 776 | if fr.Bytes(c.Values[i].Hash[:]) { 777 | fr.End() 778 | } 779 | var value string 780 | if readString(fr, &value) { 781 | return fr.End() 782 | } 783 | c.Values[i].Value = []byte(value) 784 | // TODO: validate hash? 785 | } 786 | 787 | return fr.End() 788 | } 789 | 790 | func (c *chunkSharedStrings) WriteTo(w io.Writer) (n int64, err error) { 791 | fw := parse.NewBinaryWriter(w) 792 | 793 | if fw.Number(c.Version) { 794 | return fw.End() 795 | } 796 | 797 | if fw.Number(uint32(len(c.Values))) { 798 | return fw.End() 799 | } 800 | 801 | for _, ss := range c.Values { 802 | if fw.Bytes(ss.Hash[:]) { 803 | fw.End() 804 | } 805 | if writeString(fw, string(ss.Value)) { 806 | fw.End() 807 | } 808 | } 809 | 810 | return fw.End() 811 | } 812 | 813 | //////////////////////////////////////////////////////////////// 814 | -------------------------------------------------------------------------------- /rbxl/model_test.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package rbxl 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "io" 9 | "testing" 10 | "unicode/utf8" 11 | ) 12 | 13 | func readFrom(f *FormatModel, b ...interface{}) (err error) { 14 | _, err = f.ReadFrom(bytes.NewReader(app(b...))) 15 | return 16 | } 17 | 18 | func writeTo(f *FormatModel, w *writer) (err error) { 19 | _, err = f.WriteTo(w) 20 | return 21 | } 22 | 23 | func app(bs ...interface{}) []byte { 24 | n := 0 25 | for _, b := range bs { 26 | switch b := b.(type) { 27 | case string: 28 | n += len(b) 29 | case []byte: 30 | n += len(b) 31 | case rune: 32 | n += utf8.RuneLen(b) 33 | case byte: 34 | n++ 35 | case int: 36 | n++ 37 | } 38 | } 39 | 40 | s := make([]byte, 0, n) 41 | for _, b := range bs { 42 | switch b := b.(type) { 43 | case string: 44 | s = append(s, []byte(b)...) 45 | case []byte: 46 | s = append(s, b...) 47 | case rune: 48 | s = append(s, []byte(string(b))...) 49 | case byte: 50 | s = append(s, b) 51 | case int: 52 | s = append(s, byte(b)) 53 | } 54 | } 55 | 56 | return s 57 | } 58 | 59 | type writer int 60 | 61 | func (w writer) set(n int) *writer { 62 | v := writer(n) 63 | return &v 64 | } 65 | 66 | func (w writer) add(n int) *writer { 67 | v := w + writer(n) 68 | return &v 69 | } 70 | 71 | func (w writer) copy() *writer { 72 | v := w 73 | return &v 74 | } 75 | 76 | func (w *writer) Write(b []byte) (n int, err error) { 77 | if *w <= 0 { 78 | return 0, io.EOF 79 | } 80 | 81 | *(*int)(w) -= len(b) 82 | 83 | if *w < 0 { 84 | return len(b) + *(*int)(w), io.ErrUnexpectedEOF 85 | } 86 | 87 | return len(b), nil 88 | } 89 | 90 | func pstr(s string) string { 91 | v := s 92 | return v 93 | } 94 | 95 | func initFormatModel() *FormatModel { 96 | f := new(FormatModel) 97 | f.Version = 0 98 | f.TypeCount = 3 99 | f.InstanceCount = 6 100 | 101 | names := []valueString{ 102 | valueString("intValue0"), 103 | valueString("intValue1"), 104 | valueString("Vector3Value0"), 105 | valueString("Vector3Value1"), 106 | valueString("Vector3Value2"), 107 | valueString("Workspace0"), 108 | } 109 | 110 | values := []valueInt{ 111 | valueInt(42), 112 | valueInt(-37), 113 | } 114 | 115 | f.Chunks = []Chunk{ 116 | &ChunkInstance{ 117 | IsCompressed: false, 118 | TypeID: 0, 119 | ClassName: "IntValue", 120 | InstanceIDs: []int32{0, 1}, 121 | IsService: false, 122 | GetService: []byte{}, 123 | }, 124 | &ChunkInstance{ 125 | IsCompressed: false, 126 | TypeID: 1, 127 | ClassName: "Vector3Value", 128 | InstanceIDs: []int32{2, 3, 4}, 129 | IsService: false, 130 | GetService: []byte{}, 131 | }, 132 | &ChunkInstance{ 133 | IsCompressed: false, 134 | TypeID: 2, 135 | ClassName: "Workspace", 136 | InstanceIDs: []int32{5}, 137 | IsService: true, 138 | GetService: []byte{1}, 139 | }, 140 | &ChunkProperty{ 141 | IsCompressed: false, 142 | TypeID: 0, 143 | PropertyName: "Name", 144 | DataType: typeString, 145 | Properties: []Value{ 146 | &names[0], 147 | &names[1], 148 | }, 149 | }, 150 | &ChunkProperty{ 151 | IsCompressed: false, 152 | TypeID: 0, 153 | PropertyName: "Value", 154 | DataType: typeInt, 155 | Properties: []Value{ 156 | &values[0], 157 | &values[1], 158 | }, 159 | }, 160 | &ChunkProperty{ 161 | IsCompressed: false, 162 | TypeID: 1, 163 | PropertyName: "Name", 164 | DataType: typeString, 165 | Properties: []Value{ 166 | &names[2], 167 | &names[3], 168 | &names[4], 169 | }, 170 | }, 171 | &ChunkProperty{ 172 | IsCompressed: false, 173 | TypeID: 1, 174 | PropertyName: "Value", 175 | DataType: typeVector3, 176 | Properties: []Value{ 177 | &valueVector3{X: 1, Y: 2, Z: 3}, 178 | &valueVector3{X: 4, Y: 5, Z: 6}, 179 | &valueVector3{X: 7, Y: 8, Z: 9}, 180 | }, 181 | }, 182 | &ChunkProperty{ 183 | IsCompressed: false, 184 | TypeID: 2, 185 | PropertyName: "Name", 186 | DataType: typeString, 187 | Properties: []Value{ 188 | &names[5], 189 | }, 190 | }, 191 | &ChunkParent{ 192 | Version: 0, 193 | Children: []int32{0, 1, 2, 3, 4, 5}, 194 | Parents: []int32{5, 5, 0, 0, 1, -1}, 195 | IsCompressed: false, 196 | }, 197 | &ChunkEnd{ 198 | IsCompressed: false, 199 | Content: []byte(""), 200 | }, 201 | } 202 | 203 | return f 204 | } 205 | 206 | type testChunk bool 207 | 208 | func (c testChunk) Signature() [4]byte { 209 | b := [4]byte{} 210 | copy(b[:], []byte("TEST")) 211 | return b 212 | } 213 | 214 | func (c testChunk) Compressed() bool { 215 | return false 216 | } 217 | 218 | func (c testChunk) SetCompressed(bool) {} 219 | 220 | func (c testChunk) ReadFrom(r io.Reader) (n int64, err error) { 221 | return 0, nil 222 | } 223 | 224 | func (c testChunk) WriteTo(w io.Writer) (n int64, err error) { 225 | if c { 226 | return 0, nil 227 | } 228 | return 0, errors.New("test write success") 229 | } 230 | 231 | func hasWarning(f *FormatModel, warning error) bool { 232 | for _, w := range f.Warnings { 233 | if w == warning { 234 | return true 235 | } 236 | } 237 | return false 238 | } 239 | 240 | func TestFormatModel_ReadFrom(t *testing.T) { 241 | f := new(FormatModel) 242 | var b []byte 243 | 244 | if _, err := f.ReadFrom(nil); err == nil || err.Error() != "reader is nil" { 245 | t.Error("expected error (nil reader), got:", err) 246 | } 247 | 248 | if err := readFrom(f); err != io.EOF { 249 | t.Error("expected error (no sig), got:", err) 250 | } 251 | if err := readFrom(f, RobloxSig); err != io.ErrUnexpectedEOF { 252 | t.Error("expected error (short sig), got:", err) 253 | } 254 | if err := readFrom(f, RobloxSig, "@"); err != ErrInvalidSig { 255 | t.Error("expected error (bad sig), got:", err) 256 | } 257 | b = app(b, RobloxSig, BinaryMarker) 258 | 259 | if err := readFrom(f, b); err != io.EOF { 260 | t.Error("expected error (no header), got:", err) 261 | } 262 | if err := readFrom(f, b, BinaryHeader[:1]); err != io.ErrUnexpectedEOF { 263 | t.Error("expected error (short header), got:", err) 264 | } 265 | if err := readFrom(f, b, make([]byte, len(BinaryHeader))); err != ErrCorruptHeader { 266 | t.Error("expected error (bad header), got:", err) 267 | } 268 | b = app(b, BinaryHeader) 269 | 270 | if err := readFrom(f, b); err != io.EOF { 271 | t.Error("expected error (no version), got:", err) 272 | } 273 | if err := readFrom(f, b, 0); err != io.ErrUnexpectedEOF { 274 | t.Error("expected error (short version), got:", err) 275 | } 276 | if err, ok := readFrom(f, b, 255, 1).(ErrUnrecognizedVersion); !ok { 277 | t.Error("expected error (short version), got:", err) 278 | } else if uint16(err) != 511 { 279 | t.Error("incorrect version error (expected 511), got:", uint16(err)) 280 | } 281 | b = app(b, 0, 0) 282 | 283 | if err := readFrom(f, b); err != io.EOF { 284 | t.Error("expected error (no type count), got:", err) 285 | } 286 | if err := readFrom(f, b, 0, 0); err != io.ErrUnexpectedEOF { 287 | t.Error("expected error (short type count), got:", err) 288 | } 289 | b = app(b, 0, 0, 0, 0) 290 | 291 | if err := readFrom(f, b); err != io.EOF { 292 | t.Error("expected error (no instance count), got:", err) 293 | } 294 | if err := readFrom(f, b, 0, 0); err != io.ErrUnexpectedEOF { 295 | t.Error("expected error (short instance count), got:", err) 296 | } 297 | b = app(b, 0, 0, 0, 0) 298 | 299 | if err := readFrom(f, b); err != io.EOF { 300 | t.Error("expected error (no reserve), got:", err) 301 | } 302 | if err := readFrom(f, b, 0, 0, 0, 0); err != io.ErrUnexpectedEOF { 303 | t.Error("expected error (short reserve), got:", err) 304 | } 305 | if err := readFrom(f, b, 1, 0, 0, 0, 0, 0, 0, 0); err != io.EOF { 306 | t.Error("expected error (no chunks), got:", err) 307 | } 308 | if len(f.Warnings) == 0 { 309 | t.Error("expected warning (non-zero reserve)") 310 | } else if f.Warnings[0] != WarnReserveNonZero { 311 | t.Error("expected warning (non-zero reserve), got:", f.Warnings[0]) 312 | } 313 | b = app(b, 0, 0, 0, 0, 0, 0, 0, 0) 314 | 315 | if err := readFrom(f, b, "TEST"); err != io.EOF { 316 | t.Error("expected error (bad raw chunk), got:", err) 317 | } 318 | 319 | if err := readFrom(f, b, "TEST", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); err != io.EOF { 320 | t.Error("expected error (no extra chunk), got:", err) 321 | } 322 | if len(f.Warnings) == 0 { 323 | t.Error("expected warning (bad chunk sig)") 324 | } else if _, ok := f.Warnings[0].(WarnUnknownChunk); !ok { 325 | t.Error("expected warning (bad chunk sig), got:", f.Warnings[0]) 326 | } 327 | 328 | if err := readFrom(f, b, "END\x00", 18, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, []byte{240, 1, 101, 110, 100, 32, 116, 101, 115, 116, 32, 99, 111, 110, 116, 101, 110, 116}); err != nil { 329 | t.Error("unexpected error:", err) 330 | } 331 | if len(f.Warnings) == 0 { 332 | t.Error("expected warning (compressed end chunk)") 333 | } else if f.Warnings[0] != WarnEndChunkCompressed { 334 | t.Error("expected warning (compressed end chunk), got:", f.Warnings[0]) 335 | } 336 | if len(f.Chunks) == 0 { 337 | t.Error("expected chunk") 338 | } else if chunk, ok := f.Chunks[0].(*ChunkEnd); !ok { 339 | t.Error("expected end chunk") 340 | } else { 341 | if string(chunk.Content) != "end test content" { 342 | t.Error("unexpected chunk payload, got:", string(chunk.Content)) 343 | } 344 | if !chunk.IsCompressed { 345 | t.Error("unexpected chunk chunk compression") 346 | } 347 | } 348 | 349 | if err, ok := readFrom(f, b, "INST", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0).(ErrChunk); !ok { 350 | t.Error("expected error (empty inst chunk), got:", err) 351 | } else { 352 | if string(err.Sig[:]) != "INST" { 353 | t.Error("expected INST chunk error, got:", string(err.Sig[:])) 354 | } 355 | if err.Err != io.EOF { 356 | t.Error("expected EOF chunk error, got:", err.Err) 357 | } 358 | } 359 | } 360 | 361 | func TestFormatModel_WriteTo(t *testing.T) { 362 | w := new(writer) 363 | f := initFormatModel() 364 | 365 | if _, err := f.WriteTo(nil); err == nil || err.Error() != "writer is nil" { 366 | t.Error("expected error (nil writer), got:", err) 367 | } 368 | 369 | // Signature 370 | if err := writeTo(f, w.copy()); err == nil || err != io.EOF { 371 | t.Error("expected error (no sig space), got:", err) 372 | } 373 | if err := writeTo(f, w.add(len(RobloxSig))); err == nil || err != io.ErrUnexpectedEOF { 374 | t.Error("expected error (short sig space), got:", err) 375 | } 376 | w = w.add(len(RobloxSig + BinaryMarker + BinaryHeader)) 377 | 378 | // Version 379 | if err := writeTo(f, w.copy()); err == nil || err != io.EOF { 380 | t.Error("expected error (no version space), got:", err) 381 | } 382 | if err := writeTo(f, w.add(1)); err == nil || err != io.ErrUnexpectedEOF { 383 | t.Error("expected error (short version space), got:", err) 384 | } 385 | w = w.add(2) 386 | 387 | // TypeCount 388 | if err := writeTo(f, w.copy()); err == nil || err != io.EOF { 389 | t.Error("expected error (no type count space), got:", err) 390 | } 391 | if err := writeTo(f, w.add(2)); err == nil || err != io.ErrUnexpectedEOF { 392 | t.Error("expected error (short type count space), got:", err) 393 | } 394 | w = w.add(4) 395 | 396 | // InstanceCount 397 | if err := writeTo(f, w.copy()); err == nil || err != io.EOF { 398 | t.Error("expected error (no instance count space), got:", err) 399 | } 400 | if err := writeTo(f, w.add(2)); err == nil || err != io.ErrUnexpectedEOF { 401 | t.Error("expected error (short instance count space), got:", err) 402 | } 403 | w = w.add(4) 404 | 405 | // Reserve 406 | if err := writeTo(f, w.copy()); err == nil || err != io.EOF { 407 | t.Error("expected error (no reserve space), got:", err) 408 | } 409 | if err := writeTo(f, w.add(4)); err == nil || err != io.ErrUnexpectedEOF { 410 | t.Error("expected error (short reserve space), got:", err) 411 | } 412 | w = w.add(8) 413 | 414 | // Chunks 415 | if err := writeTo(f, w.copy()); err == nil || err != io.EOF { 416 | t.Error("expected error (no chunk space), got:", err) 417 | } 418 | if err := writeTo(f, w.add(1)); err == nil || err != io.ErrUnexpectedEOF { 419 | t.Error("expected error (short chunk space), got:", err) 420 | } 421 | w = w.add(1 << 10) 422 | 423 | chunk, _ := f.Chunks[len(f.Chunks)-1].(*ChunkEnd) 424 | chunk.IsCompressed = true 425 | chunk.Content = []byte("test content") 426 | 427 | f.Chunks = append(f.Chunks, testChunk(true)) 428 | 429 | if err := writeTo(f, w.copy()); err != nil { 430 | t.Error("unexpected error (chunk warnings):", err) 431 | } else { 432 | if !hasWarning(f, WarnEndChunkCompressed) { 433 | t.Error("expected warning (compressed end chunk)") 434 | } 435 | if !hasWarning(f, WarnEndChunkContent) { 436 | t.Error("expected warning (bad end chunk content)") 437 | } 438 | if !hasWarning(f, WarnEndChunkNotLast) { 439 | t.Error("expected warning (end chunk not last)") 440 | } 441 | for _, warning := range f.Warnings { 442 | if warning, ok := warning.(WarnUnknownChunk); ok { 443 | if string(warning[:]) != "TEST" { 444 | t.Error("unexpected signature (unknown chunk)", [4]byte(warning)) 445 | } 446 | goto okay 447 | } 448 | } 449 | t.Error("expected warning (unknown chunk)") 450 | okay: 451 | } 452 | 453 | f.Chunks[8].(*ChunkParent).Parents = []int32{} 454 | if err := writeTo(f, w.copy()); err == nil || err != ErrChunkParentArray { 455 | t.Error("expected error (chunk write), got:", err) 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /rbxlx/format.go: -------------------------------------------------------------------------------- 1 | package rbxlx 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/robloxapi/rbxfile" 8 | ) 9 | 10 | // Decoder decodes a stream of bytes into a rbxfile.Root according to the rbxlx 11 | // format. 12 | type Decoder struct { 13 | // DiscardInvalidProperties determines how invalid properties are decoded. 14 | // If true, when the parser successfully decodes a property, but fails to 15 | // decode its value or a component, then the entire property is discarded. 16 | // If false, then as much information as possible is retained; any value or 17 | // component that fails will be emitted as the zero value for the type. 18 | DiscardInvalidProperties bool 19 | } 20 | 21 | // Decode reads data from r and decodes it into root. 22 | func (d Decoder) Decode(r io.Reader) (root *rbxfile.Root, warn, err error) { 23 | document := new(documentRoot) 24 | if _, err = document.ReadFrom(r); err != nil { 25 | return nil, document.Warnings.Return(), fmt.Errorf("error parsing document: %w", err) 26 | } 27 | codec := robloxCodec{ 28 | DiscardInvalidProperties: d.DiscardInvalidProperties, 29 | } 30 | root, err = codec.Decode(document) 31 | if err != nil { 32 | return nil, document.Warnings.Return(), fmt.Errorf("error decoding data: %w", err) 33 | } 34 | return root, document.Warnings.Return(), nil 35 | } 36 | 37 | // Encoder encodes a rbxfile.Root into a stream of bytes according to the rbxlx 38 | // format. 39 | type Encoder struct { 40 | // ExcludeReferent determines whether the "referent" attribute should be 41 | // excluded from Item tags when encoding. 42 | ExcludeReferent bool 43 | 44 | // ExcludeExternal determines whether standard tags should be 45 | // excluded from the root tag when encoding. 46 | ExcludeExternal bool 47 | 48 | // ExcludeMetadata determines whether tags should be excluded while 49 | // encoding. 50 | ExcludeMetadata bool 51 | 52 | // Prefix is a string that appears at the start of each line in the 53 | // document. The prefix is added after each newline. Newlines are added 54 | // automatically when either Prefix or Indent is not empty. 55 | Prefix string 56 | 57 | // Indent is a string that indicates one level of indentation. A sequence of 58 | // indents will appear after the Prefix, an amount equal to the current 59 | // nesting depth in the markup. 60 | Indent string 61 | 62 | // NoDefaultIndent sets how Indent is interpreted when Indent is an empty 63 | // string. If false, an empty Indent will be interpreted as "\t". 64 | NoDefaultIndent bool 65 | 66 | // Suffix is a string that appears at the very end of the document. This 67 | // string is appended to the end of the file, after the root tag. 68 | Suffix string 69 | 70 | // ExcludeRoot determines whether the root tag should be excluded when 71 | // encoding. This can be combined with Prefix to write documents in-line. 72 | ExcludeRoot bool 73 | } 74 | 75 | // Encode formats root, writing the result to w. 76 | func (e Encoder) Encode(w io.Writer, root *rbxfile.Root) (warn, err error) { 77 | codec := robloxCodec{ 78 | ExcludeReferent: e.ExcludeReferent, 79 | ExcludeExternal: e.ExcludeExternal, 80 | ExcludeMetadata: e.ExcludeMetadata, 81 | } 82 | document, err := codec.Encode(root) 83 | if err != nil { 84 | return document.Warnings.Return(), fmt.Errorf("error encoding data: %w", err) 85 | } 86 | document.Prefix = e.Prefix 87 | if e.Indent == "" && !e.NoDefaultIndent { 88 | document.Indent = "\t" 89 | } else { 90 | document.Indent = e.Indent 91 | } 92 | document.Suffix = e.Suffix 93 | document.ExcludeRoot = e.ExcludeRoot 94 | if _, err = document.WriteTo(w); err != nil { 95 | return document.Warnings.Return(), fmt.Errorf("error encoding format: %w", err) 96 | } 97 | return document.Warnings.Return(), nil 98 | } 99 | -------------------------------------------------------------------------------- /ref.go: -------------------------------------------------------------------------------- 1 | package rbxfile 2 | 3 | import ( 4 | "crypto/rand" 5 | "io" 6 | ) 7 | 8 | // PropRef specifies the property of an instance that is a reference, which is 9 | // to be resolved into its referent at a later time. 10 | type PropRef struct { 11 | Instance *Instance 12 | Property string 13 | Reference string 14 | } 15 | 16 | // References is a mapping of reference strings to Instances. 17 | type References map[string]*Instance 18 | 19 | // Resolve resolves a PropRef and sets the value of the property using 20 | // References. If the referent does not exist, and the reference is not empty, 21 | // then false is returned. True is returned otherwise. 22 | func (refs References) Resolve(propRef PropRef) bool { 23 | if refs == nil { 24 | return false 25 | } 26 | if propRef.Instance == nil { 27 | return false 28 | } 29 | referent := refs[propRef.Reference] 30 | propRef.Instance.Properties[propRef.Property] = ValueReference{ 31 | Instance: referent, 32 | } 33 | return referent != nil && !IsEmptyReference(propRef.Reference) 34 | } 35 | 36 | // Get gets a reference from an Instance, using References to check for 37 | // duplicates. If the instance's reference already exists in References, then 38 | // a new reference is generated and applied to the instance. The instance's 39 | // reference is then added to References. 40 | func (refs References) Get(instance *Instance) (ref string) { 41 | if instance == nil { 42 | return "" 43 | } 44 | 45 | ref = instance.Reference 46 | if refs == nil { 47 | return ref 48 | } 49 | // If the reference is not empty, or if the reference is not marked, or 50 | // the marked reference already refers to the current instance, then do 51 | // nothing. 52 | if IsEmptyReference(ref) || refs[ref] != nil && refs[ref] != instance { 53 | // Otherwise, regenerate the reference until it is not a duplicate. 54 | for { 55 | // If a generated reference matches a reference that was not yet 56 | // traversed, then the latter reference will be regenerated, which 57 | // may not match Roblox's implementation. It is difficult to 58 | // discern whether this is correct because it is extremely 59 | // unlikely that a duplicate will be generated. 60 | ref = GenerateReference() 61 | if _, ok := refs[ref]; !ok { 62 | instance.Reference = ref 63 | break 64 | } 65 | } 66 | } 67 | // Mark reference as taken. 68 | refs[ref] = instance 69 | return ref 70 | } 71 | 72 | // IsEmptyReference returns whether a reference string is considered "empty", 73 | // and therefore does not have a referent. 74 | func IsEmptyReference(ref string) bool { 75 | switch ref { 76 | case "", "null", "nil": 77 | return true 78 | default: 79 | return false 80 | } 81 | } 82 | 83 | func generateUUID() string { 84 | var buf [32]byte 85 | if _, err := io.ReadFull(rand.Reader, buf[:16]); err != nil { 86 | panic(err) 87 | } 88 | buf[6] = (buf[6] & 0x0F) | 0x40 // Version 4 ; 0100XXXX 89 | buf[8] = (buf[8] & 0x3F) | 0x80 // Variant RFC4122 ; 10XXXXXX 90 | const hextable = "0123456789ABCDEF" 91 | for i := len(buf)/2 - 1; i >= 0; i-- { 92 | buf[i*2+1] = hextable[buf[i]&0x0f] 93 | buf[i*2] = hextable[buf[i]>>4] 94 | } 95 | return string(buf[:]) 96 | } 97 | 98 | // GenerateReference generates a unique string that can be used as a reference 99 | // to an Instance. 100 | func GenerateReference() string { 101 | return "RBX" + generateUUID() 102 | } 103 | -------------------------------------------------------------------------------- /value_checklist.md: -------------------------------------------------------------------------------- 1 | To add value type `Foobar`: 2 | 3 | - rbxfile 4 | - `values.go` 5 | - [ ] Add `TypeFoobar` to Type constants. 6 | - [ ] In `typeStrings`, map `TypeFoobar` to string `"Foobar"`. 7 | - [ ] In `valueGenerators`, map `TypeFoobar` to function 8 | `newValueFoobar`. 9 | - [ ] Create `ValueFoobar` type. 10 | - [ ] Add `ValueFoobar` type with appropriate underlying type. 11 | - [ ] Implement `newValueFoobar` function (`func() Value`) 12 | - [ ] Implement `Type() Type` method. 13 | - Return `TypeFoobar`. 14 | - [ ] Implement `String() string` method. 15 | - Return string representation of value that is similar to the 16 | results of Roblox's `tostring` function. 17 | - [ ] Implement `Copy() Value` method. 18 | - Must return a deep copy of the underlying value. 19 | - `values_test.go` 20 | - ... 21 | - declare 22 | - `declare/type.go` 23 | - [ ] Add `Foobar` to type constants. 24 | - Ensure `Foobar` does not conflict with existing identifiers. 25 | - [ ] In `typeStrings`, map `Foobar` to string `"Foobar"`. 26 | - [ ] In function `assertValue`, add case `Foobar`. 27 | - Assert `v` as `rbxfile.ValueFoobar`. 28 | - [ ] In method `Type.value`, add case `Foobar`. 29 | - Convert slice of arbitrary values to a `rbxfile.ValueFoobar`. 30 | - `declare/declare.go` 31 | - [ ] In function `Property`, document behavior of `Foobar` case in 32 | `Type.value` method. 33 | - `declare/declare_test.go` 34 | - ... 35 | - json 36 | - `json/json.go` 37 | - [ ] In function `ValueToJSONInterface`, add case 38 | `rbxfile.ValueFoobar`. 39 | - Convert `rbxfile.ValueFoobar` to generic JSON interface. 40 | - [ ] In function `ValueFromJSONInterface`, add case 41 | `rbxfile.TypeFoobar`. 42 | - Convert generic JSON interface to `rbxfile.ValueFoobar`. 43 | - rbxlx 44 | - `rbxlx/codec.go` 45 | - [ ] In function `GetCanonType` add case `"foobar"` (lowercase). 46 | - Returns `"Foobar"` 47 | - [ ] In method `rdecoder.getValue`, add case `"Foobar"`. 48 | - Receives `tag *Tag`, must return `rbxfile.ValueFoobar`. 49 | - `components` can be used to map subtags to value fields. 50 | - [ ] In method `rencoder.encodeProperty`, add case 51 | `rbxfile.ValueFoobar`. 52 | - Returns `*Tag` that is decodable by `rdecoder.getValue`. 53 | - [ ] In function `isCanonType`, add case `rbxfile.ValueFoobar`. 54 | - rbxl 55 | - `rbxl/values.go` 56 | - [ ] Add `TypeFoobar` to type constants. 57 | - [ ] In `String` method, add case `TypeFoobar` that returns `"Foobar"`. 58 | - [ ] In `ValueType` method, add case `TypeFoobar` that returns 59 | `rbxfile.TypeFoobar`. 60 | - [ ] In `FromValueType` function, add case `rbxfile.TypeFoobar` that 61 | returns `TypeFoobar`. 62 | - [ ] In `NewValue`, add case `TypeFoobar` that returns 63 | `new(ValueFoobar)`. 64 | - [ ] Create `ValueFoobar` type. 65 | - [ ] Add `ValueFoobar` with appropriate underlying type. 66 | - [ ] Implement `Type() Type` method. 67 | - Returns `TypeFoobar`. 68 | - [ ] Implement `Bytes`. 69 | - Converts a single `ValueFoobar` to a slice of bytes. 70 | - [ ] Implement `FromBytes`. 71 | - Converts a slice of bytes to a single `ValueFoobar`. 72 | - [ ] If fields of `ValueFoobar` must be interleaved, implement 73 | `fielder` interface. 74 | - [ ] Implement `fieldLen`. 75 | - Returns the byte size of each field. 76 | - Update maxFieldLen if the length of the returned slice is 77 | greater. 78 | - [ ] Implement `fieldSet`. 79 | - Sets field number `i` using bytes from `b`. 80 | - [ ] Implement `fieldGet`. 81 | - Returns field number `i` as a slice of bytes. 82 | - `rbxl/codec.go` 83 | - [ ] In function `decodeValue`, add case `*ValueFoobar`. 84 | - Converts `*ValueFoobar` to `rbxfile.ValueFoobar`. 85 | - [ ] In function `encodeValue`, add case `rbxfile.ValueFoobar`. 86 | - Converts `rbxfile.ValueFoobar` to `*ValueFoobar`. 87 | - `rbxl/arrays.go` 88 | - [ ] In function `ValuesToBytes`, add case `TypeFoobar`. 89 | - Converts a slice of `ValueFoobar` to a slice of bytes. 90 | - If fields `ValueFoobar` must be interleaved, use 91 | `interleaveFields`. 92 | - [ ] In function `ValuesFromBytes`, add case `TypeFoobar`. 93 | - Converts a slice of bytes to a slice of `ValueFoobar`. 94 | - If fields of ValueFoobar` are interleaved, use 95 | `deinterleaveFields`. 96 | -------------------------------------------------------------------------------- /values.go: -------------------------------------------------------------------------------- 1 | package rbxfile 2 | 3 | import ( 4 | "encoding/binary" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // Type represents a Roblox type. 10 | type Type byte 11 | 12 | // String returns a string representation of the type. If the type is not 13 | // valid, then the returned value will be "Invalid". 14 | func (t Type) String() string { 15 | s, ok := typeStrings[t] 16 | if !ok { 17 | return "Invalid" 18 | } 19 | return s 20 | } 21 | 22 | const ( 23 | TypeInvalid Type = iota 24 | TypeString 25 | TypeBinaryString 26 | TypeProtectedString 27 | TypeContent 28 | TypeBool 29 | TypeInt 30 | TypeFloat 31 | TypeDouble 32 | TypeUDim 33 | TypeUDim2 34 | TypeRay 35 | TypeFaces 36 | TypeAxes 37 | TypeBrickColor 38 | TypeColor3 39 | TypeVector2 40 | TypeVector3 41 | TypeCFrame 42 | TypeToken 43 | TypeReference 44 | TypeVector3int16 45 | TypeVector2int16 46 | TypeNumberSequence 47 | TypeColorSequence 48 | TypeNumberRange 49 | TypeRect 50 | TypePhysicalProperties 51 | TypeColor3uint8 52 | TypeInt64 53 | TypeSharedString 54 | TypeOptional 55 | TypeUniqueId 56 | TypeFont 57 | ) 58 | 59 | // TypeFromString returns a Type from its string representation. TypeInvalid 60 | // is returned if the string does not represent an existing Type. 61 | func TypeFromString(s string) Type { 62 | for typ, str := range typeStrings { 63 | if s == str { 64 | return typ 65 | } 66 | } 67 | return TypeInvalid 68 | } 69 | 70 | var typeStrings = map[Type]string{ 71 | TypeString: "String", 72 | TypeBinaryString: "BinaryString", 73 | TypeProtectedString: "ProtectedString", 74 | TypeContent: "Content", 75 | TypeBool: "Bool", 76 | TypeInt: "Int", 77 | TypeFloat: "Float", 78 | TypeDouble: "Double", 79 | TypeUDim: "UDim", 80 | TypeUDim2: "UDim2", 81 | TypeRay: "Ray", 82 | TypeFaces: "Faces", 83 | TypeAxes: "Axes", 84 | TypeBrickColor: "BrickColor", 85 | TypeColor3: "Color3", 86 | TypeVector2: "Vector2", 87 | TypeVector3: "Vector3", 88 | TypeCFrame: "CFrame", 89 | TypeToken: "Token", 90 | TypeReference: "Reference", 91 | TypeVector3int16: "Vector3int16", 92 | TypeVector2int16: "Vector2int16", 93 | TypeNumberSequence: "NumberSequence", 94 | TypeColorSequence: "ColorSequence", 95 | TypeNumberRange: "NumberRange", 96 | TypeRect: "Rect", 97 | TypePhysicalProperties: "PhysicalProperties", 98 | TypeColor3uint8: "Color3uint8", 99 | TypeInt64: "Int64", 100 | TypeSharedString: "SharedString", 101 | TypeOptional: "Optional", 102 | TypeUniqueId: "UniqueId", 103 | TypeFont: "Font", 104 | } 105 | 106 | // Value holds a value of a particular Type. 107 | type Value interface { 108 | // Type returns an identifier indicating the type. 109 | Type() Type 110 | 111 | // String returns a string representation of the current value. 112 | String() string 113 | 114 | // Copy returns a copy of the value, which can be safely modified. 115 | Copy() Value 116 | } 117 | 118 | // NewValue returns new Value of the given Type. The initial value will not 119 | // necessarily be the zero for the type. If the given type is invalid, then a 120 | // nil value is returned. 121 | func NewValue(typ Type) Value { 122 | newValue, ok := valueGenerators[typ] 123 | if !ok { 124 | return nil 125 | } 126 | return newValue() 127 | } 128 | 129 | type valueGenerator func() Value 130 | 131 | var valueGenerators = map[Type]valueGenerator{ 132 | TypeString: newValueString, 133 | TypeBinaryString: newValueBinaryString, 134 | TypeProtectedString: newValueProtectedString, 135 | TypeContent: newValueContent, 136 | TypeBool: newValueBool, 137 | TypeInt: newValueInt, 138 | TypeFloat: newValueFloat, 139 | TypeDouble: newValueDouble, 140 | TypeUDim: newValueUDim, 141 | TypeUDim2: newValueUDim2, 142 | TypeRay: newValueRay, 143 | TypeFaces: newValueFaces, 144 | TypeAxes: newValueAxes, 145 | TypeBrickColor: newValueBrickColor, 146 | TypeColor3: newValueColor3, 147 | TypeVector2: newValueVector2, 148 | TypeVector3: newValueVector3, 149 | TypeCFrame: newValueCFrame, 150 | TypeToken: newValueToken, 151 | TypeReference: newValueReference, 152 | TypeVector3int16: newValueVector3int16, 153 | TypeVector2int16: newValueVector2int16, 154 | TypeNumberSequence: newValueNumberSequence, 155 | TypeColorSequence: newValueColorSequence, 156 | TypeNumberRange: newValueNumberRange, 157 | TypeRect: newValueRect, 158 | TypePhysicalProperties: newValuePhysicalProperties, 159 | TypeColor3uint8: newValueColor3uint8, 160 | TypeInt64: newValueInt64, 161 | TypeSharedString: newValueSharedString, 162 | TypeOptional: newValueOptional, 163 | TypeUniqueId: newValueUniqueId, 164 | TypeFont: newValueFont, 165 | } 166 | 167 | func joinstr(a ...string) string { 168 | n := 0 169 | for i := 0; i < len(a); i++ { 170 | n += len(a[i]) 171 | } 172 | 173 | b := make([]byte, n) 174 | bp := 0 175 | for _, s := range a { 176 | bp += copy(b[bp:], s) 177 | } 178 | return string(b) 179 | } 180 | 181 | //////////////////////////////////////////////////////////////// 182 | // Values 183 | 184 | type ValueString []byte 185 | 186 | func newValueString() Value { 187 | return make(ValueString, 0) 188 | } 189 | 190 | func (ValueString) Type() Type { 191 | return TypeString 192 | } 193 | func (t ValueString) String() string { 194 | return string(t) 195 | } 196 | func (t ValueString) Copy() Value { 197 | c := make(ValueString, len(t)) 198 | copy(c, t) 199 | return c 200 | } 201 | 202 | //////////////// 203 | 204 | type ValueBinaryString []byte 205 | 206 | func newValueBinaryString() Value { 207 | return make(ValueBinaryString, 0) 208 | } 209 | 210 | func (ValueBinaryString) Type() Type { 211 | return TypeBinaryString 212 | } 213 | func (t ValueBinaryString) String() string { 214 | return string(t) 215 | } 216 | func (t ValueBinaryString) Copy() Value { 217 | c := make(ValueBinaryString, len(t)) 218 | copy(c, t) 219 | return c 220 | } 221 | 222 | //////////////// 223 | 224 | type ValueProtectedString []byte 225 | 226 | func newValueProtectedString() Value { 227 | return make(ValueProtectedString, 0) 228 | } 229 | 230 | func (ValueProtectedString) Type() Type { 231 | return TypeProtectedString 232 | } 233 | func (t ValueProtectedString) String() string { 234 | return string(t) 235 | } 236 | func (t ValueProtectedString) Copy() Value { 237 | c := make(ValueProtectedString, len(t)) 238 | copy(c, t) 239 | return c 240 | } 241 | 242 | //////////////// 243 | 244 | type ValueContent []byte 245 | 246 | func newValueContent() Value { 247 | return make(ValueContent, 0) 248 | } 249 | 250 | func (ValueContent) Type() Type { 251 | return TypeContent 252 | } 253 | func (t ValueContent) String() string { 254 | return string(t) 255 | } 256 | func (t ValueContent) Copy() Value { 257 | c := make(ValueContent, len(t)) 258 | copy(c, t) 259 | return c 260 | } 261 | 262 | //////////////// 263 | 264 | type ValueBool bool 265 | 266 | func newValueBool() Value { 267 | return *new(ValueBool) 268 | } 269 | 270 | func (ValueBool) Type() Type { 271 | return TypeBool 272 | } 273 | func (t ValueBool) String() string { 274 | if t { 275 | return "true" 276 | } else { 277 | return "false" 278 | } 279 | } 280 | func (t ValueBool) Copy() Value { 281 | return t 282 | } 283 | 284 | //////////////// 285 | 286 | type ValueInt int32 287 | 288 | func newValueInt() Value { 289 | return *new(ValueInt) 290 | } 291 | 292 | func (ValueInt) Type() Type { 293 | return TypeInt 294 | } 295 | func (t ValueInt) String() string { 296 | return strconv.FormatInt(int64(t), 10) 297 | } 298 | func (t ValueInt) Copy() Value { 299 | return t 300 | } 301 | 302 | //////////////// 303 | 304 | type ValueFloat float32 305 | 306 | func newValueFloat() Value { 307 | return *new(ValueFloat) 308 | } 309 | 310 | func (ValueFloat) Type() Type { 311 | return TypeFloat 312 | } 313 | func (t ValueFloat) String() string { 314 | return strconv.FormatFloat(float64(t), 'f', -1, 32) 315 | } 316 | func (t ValueFloat) Copy() Value { 317 | return t 318 | } 319 | 320 | //////////////// 321 | 322 | type ValueDouble float64 323 | 324 | func newValueDouble() Value { 325 | return *new(ValueDouble) 326 | } 327 | 328 | func (ValueDouble) Type() Type { 329 | return TypeDouble 330 | } 331 | func (t ValueDouble) String() string { 332 | return strconv.FormatFloat(float64(t), 'f', -1, 64) 333 | } 334 | func (t ValueDouble) Copy() Value { 335 | return t 336 | } 337 | 338 | //////////////// 339 | 340 | type ValueUDim struct { 341 | Scale float32 342 | Offset int32 343 | } 344 | 345 | func newValueUDim() Value { 346 | return *new(ValueUDim) 347 | } 348 | 349 | func (ValueUDim) Type() Type { 350 | return TypeUDim 351 | } 352 | func (t ValueUDim) String() string { 353 | return joinstr( 354 | strconv.FormatFloat(float64(t.Scale), 'f', -1, 32), 355 | ", ", 356 | strconv.FormatInt(int64(t.Offset), 10), 357 | ) 358 | } 359 | func (t ValueUDim) Copy() Value { 360 | return t 361 | } 362 | 363 | //////////////// 364 | 365 | type ValueUDim2 struct { 366 | X, Y ValueUDim 367 | } 368 | 369 | func newValueUDim2() Value { 370 | return *new(ValueUDim2) 371 | } 372 | 373 | func (ValueUDim2) Type() Type { 374 | return TypeUDim2 375 | } 376 | func (t ValueUDim2) String() string { 377 | return joinstr( 378 | "{", 379 | t.X.String(), 380 | "}, {", 381 | t.Y.String(), 382 | "}", 383 | ) 384 | } 385 | func (t ValueUDim2) Copy() Value { 386 | return t 387 | } 388 | 389 | //////////////// 390 | 391 | type ValueRay struct { 392 | Origin, Direction ValueVector3 393 | } 394 | 395 | func newValueRay() Value { 396 | return *new(ValueRay) 397 | } 398 | 399 | func (ValueRay) Type() Type { 400 | return TypeRay 401 | } 402 | func (t ValueRay) String() string { 403 | return joinstr( 404 | "{", 405 | t.Origin.String(), 406 | "}, {", 407 | t.Direction.String(), 408 | "}", 409 | ) 410 | } 411 | func (t ValueRay) Copy() Value { 412 | return t 413 | } 414 | 415 | //////////////// 416 | 417 | type ValueFaces struct { 418 | Right, Top, Back, Left, Bottom, Front bool 419 | } 420 | 421 | func newValueFaces() Value { 422 | return *new(ValueFaces) 423 | } 424 | 425 | func (ValueFaces) Type() Type { 426 | return TypeFaces 427 | } 428 | func (t ValueFaces) String() string { 429 | s := make([]string, 0, 6) 430 | if t.Front { 431 | s = append(s, "Front") 432 | } 433 | if t.Bottom { 434 | s = append(s, "Bottom") 435 | } 436 | if t.Left { 437 | s = append(s, "Left") 438 | } 439 | if t.Back { 440 | s = append(s, "Back") 441 | } 442 | if t.Top { 443 | s = append(s, "Top") 444 | } 445 | if t.Right { 446 | s = append(s, "Right") 447 | } 448 | 449 | return strings.Join(s, ", ") 450 | } 451 | func (t ValueFaces) Copy() Value { 452 | return t 453 | } 454 | 455 | //////////////// 456 | 457 | type ValueAxes struct { 458 | X, Y, Z bool 459 | } 460 | 461 | func newValueAxes() Value { 462 | return *new(ValueAxes) 463 | } 464 | 465 | func (ValueAxes) Type() Type { 466 | return TypeAxes 467 | } 468 | func (t ValueAxes) String() string { 469 | s := make([]string, 0, 3) 470 | if t.X { 471 | s = append(s, "X") 472 | } 473 | if t.Y { 474 | s = append(s, "Y") 475 | } 476 | if t.Z { 477 | s = append(s, "Z") 478 | } 479 | 480 | return strings.Join(s, ", ") 481 | } 482 | func (t ValueAxes) Copy() Value { 483 | return t 484 | } 485 | 486 | //////////////// 487 | 488 | type ValueBrickColor uint32 489 | 490 | func newValueBrickColor() Value { 491 | return *new(ValueBrickColor) 492 | } 493 | 494 | func (ValueBrickColor) Type() Type { 495 | return TypeBrickColor 496 | } 497 | func (t ValueBrickColor) String() string { 498 | return strconv.FormatUint(uint64(t), 10) 499 | } 500 | func (t ValueBrickColor) Copy() Value { 501 | return t 502 | } 503 | 504 | //////////////// 505 | 506 | type ValueColor3 struct { 507 | R, G, B float32 508 | } 509 | 510 | func newValueColor3() Value { 511 | return *new(ValueColor3) 512 | } 513 | 514 | func (ValueColor3) Type() Type { 515 | return TypeColor3 516 | } 517 | func (t ValueColor3) String() string { 518 | return joinstr( 519 | strconv.FormatFloat(float64(t.R), 'f', -1, 32), 520 | ", ", 521 | strconv.FormatFloat(float64(t.G), 'f', -1, 32), 522 | ", ", 523 | strconv.FormatFloat(float64(t.B), 'f', -1, 32), 524 | ) 525 | } 526 | func (t ValueColor3) Copy() Value { 527 | return t 528 | } 529 | 530 | //////////////// 531 | 532 | type ValueVector2 struct { 533 | X, Y float32 534 | } 535 | 536 | func newValueVector2() Value { 537 | return *new(ValueVector2) 538 | } 539 | 540 | func (ValueVector2) Type() Type { 541 | return TypeVector2 542 | } 543 | func (t ValueVector2) String() string { 544 | return joinstr( 545 | strconv.FormatFloat(float64(t.X), 'f', -1, 32), 546 | ", ", 547 | strconv.FormatFloat(float64(t.Y), 'f', -1, 32), 548 | ) 549 | } 550 | func (t ValueVector2) Copy() Value { 551 | return t 552 | } 553 | 554 | //////////////// 555 | 556 | type ValueVector3 struct { 557 | X, Y, Z float32 558 | } 559 | 560 | func newValueVector3() Value { 561 | return *new(ValueVector3) 562 | } 563 | 564 | func (ValueVector3) Type() Type { 565 | return TypeVector3 566 | } 567 | func (t ValueVector3) String() string { 568 | return joinstr( 569 | strconv.FormatFloat(float64(t.X), 'f', -1, 32), 570 | ", ", 571 | strconv.FormatFloat(float64(t.Y), 'f', -1, 32), 572 | ", ", 573 | strconv.FormatFloat(float64(t.Z), 'f', -1, 32), 574 | ) 575 | } 576 | func (t ValueVector3) Copy() Value { 577 | return t 578 | } 579 | 580 | //////////////// 581 | 582 | type ValueCFrame struct { 583 | Position ValueVector3 584 | Rotation [9]float32 585 | } 586 | 587 | func newValueCFrame() Value { 588 | return ValueCFrame{ 589 | Position: ValueVector3{0, 0, 0}, 590 | Rotation: [9]float32{1, 0, 0, 0, 1, 0, 0, 0, 1}, 591 | } 592 | } 593 | 594 | func (ValueCFrame) Type() Type { 595 | return TypeCFrame 596 | } 597 | func (t ValueCFrame) String() string { 598 | s := make([]string, 12) 599 | s[0] = strconv.FormatFloat(float64(t.Position.X), 'f', -1, 32) 600 | s[1] = strconv.FormatFloat(float64(t.Position.Y), 'f', -1, 32) 601 | s[2] = strconv.FormatFloat(float64(t.Position.Z), 'f', -1, 32) 602 | for i, f := range t.Rotation { 603 | s[i+3] = strconv.FormatFloat(float64(f), 'f', -1, 32) 604 | } 605 | return strings.Join(s, ", ") 606 | } 607 | func (t ValueCFrame) Copy() Value { 608 | return t 609 | } 610 | 611 | //////////////// 612 | 613 | type ValueToken uint32 614 | 615 | func newValueToken() Value { 616 | return *new(ValueToken) 617 | } 618 | 619 | func (ValueToken) Type() Type { 620 | return TypeToken 621 | } 622 | func (t ValueToken) String() string { 623 | return strconv.FormatInt(int64(t), 10) 624 | } 625 | func (t ValueToken) Copy() Value { 626 | return t 627 | } 628 | 629 | //////////////// 630 | 631 | type ValueReference struct { 632 | *Instance 633 | } 634 | 635 | func newValueReference() Value { 636 | return *new(ValueReference) 637 | } 638 | 639 | func (ValueReference) Type() Type { 640 | return TypeReference 641 | } 642 | func (t ValueReference) String() string { 643 | if t.Instance == nil { 644 | return "" 645 | } 646 | return t.Reference 647 | } 648 | func (t ValueReference) Copy() Value { 649 | return t 650 | } 651 | 652 | //////////////// 653 | 654 | type ValueVector3int16 struct { 655 | X, Y, Z int16 656 | } 657 | 658 | func newValueVector3int16() Value { 659 | return *new(ValueVector3int16) 660 | } 661 | 662 | func (ValueVector3int16) Type() Type { 663 | return TypeVector3int16 664 | } 665 | func (t ValueVector3int16) String() string { 666 | return joinstr( 667 | strconv.FormatInt(int64(t.X), 10), 668 | ", ", 669 | strconv.FormatInt(int64(t.Y), 10), 670 | ", ", 671 | strconv.FormatInt(int64(t.Z), 10), 672 | ) 673 | } 674 | func (t ValueVector3int16) Copy() Value { 675 | return t 676 | } 677 | 678 | //////////////// 679 | 680 | type ValueVector2int16 struct { 681 | X, Y int16 682 | } 683 | 684 | func newValueVector2int16() Value { 685 | return *new(ValueVector2int16) 686 | } 687 | 688 | func (ValueVector2int16) Type() Type { 689 | return TypeVector2int16 690 | } 691 | func (t ValueVector2int16) String() string { 692 | return joinstr( 693 | strconv.FormatInt(int64(t.X), 10), 694 | ", ", 695 | strconv.FormatInt(int64(t.Y), 10), 696 | ) 697 | } 698 | func (t ValueVector2int16) Copy() Value { 699 | return t 700 | } 701 | 702 | //////////////// 703 | 704 | type ValueNumberSequenceKeypoint struct { 705 | Time, Value, Envelope float32 706 | } 707 | 708 | func (t ValueNumberSequenceKeypoint) String() string { 709 | return joinstr( 710 | strconv.FormatFloat(float64(t.Time), 'f', -1, 32), 711 | " ", 712 | strconv.FormatFloat(float64(t.Value), 'f', -1, 32), 713 | " ", 714 | strconv.FormatFloat(float64(t.Envelope), 'f', -1, 32), 715 | ) 716 | } 717 | 718 | type ValueNumberSequence []ValueNumberSequenceKeypoint 719 | 720 | func newValueNumberSequence() Value { 721 | return make(ValueNumberSequence, 0, 8) 722 | } 723 | 724 | func (ValueNumberSequence) Type() Type { 725 | return TypeNumberSequence 726 | } 727 | func (t ValueNumberSequence) String() string { 728 | b := make([]byte, 0, 64) 729 | for _, v := range t { 730 | b = append(b, []byte(v.String())...) 731 | b = append(b, ' ') 732 | } 733 | return string(b) 734 | } 735 | func (t ValueNumberSequence) Copy() Value { 736 | c := make(ValueNumberSequence, len(t)) 737 | copy(c, t) 738 | return c 739 | } 740 | 741 | //////////////// 742 | 743 | type ValueColorSequenceKeypoint struct { 744 | Time float32 745 | Value ValueColor3 746 | Envelope float32 747 | } 748 | 749 | func (t ValueColorSequenceKeypoint) String() string { 750 | return joinstr( 751 | strconv.FormatFloat(float64(t.Time), 'f', -1, 32), 752 | " ", 753 | strconv.FormatFloat(float64(t.Value.R), 'f', -1, 32), 754 | " ", 755 | strconv.FormatFloat(float64(t.Value.G), 'f', -1, 32), 756 | " ", 757 | strconv.FormatFloat(float64(t.Value.B), 'f', -1, 32), 758 | " ", 759 | strconv.FormatFloat(float64(t.Envelope), 'f', -1, 32), 760 | ) 761 | } 762 | 763 | type ValueColorSequence []ValueColorSequenceKeypoint 764 | 765 | func newValueColorSequence() Value { 766 | return make(ValueColorSequence, 0, 8) 767 | } 768 | 769 | func (ValueColorSequence) Type() Type { 770 | return TypeColorSequence 771 | } 772 | func (t ValueColorSequence) String() string { 773 | b := make([]byte, 0, 64) 774 | for _, v := range t { 775 | b = append(b, []byte(v.String())...) 776 | b = append(b, ' ') 777 | } 778 | return string(b) 779 | } 780 | func (t ValueColorSequence) Copy() Value { 781 | c := make(ValueColorSequence, len(t)) 782 | copy(c, t) 783 | return c 784 | } 785 | 786 | //////////////// 787 | 788 | type ValueNumberRange struct { 789 | Min, Max float32 790 | } 791 | 792 | func newValueNumberRange() Value { 793 | return *new(ValueNumberRange) 794 | } 795 | 796 | func (ValueNumberRange) Type() Type { 797 | return TypeNumberRange 798 | } 799 | func (t ValueNumberRange) String() string { 800 | return joinstr( 801 | strconv.FormatFloat(float64(t.Min), 'f', -1, 32), 802 | " ", 803 | strconv.FormatFloat(float64(t.Max), 'f', -1, 32), 804 | ) 805 | } 806 | func (t ValueNumberRange) Copy() Value { 807 | return t 808 | } 809 | 810 | //////////////// 811 | 812 | type ValueRect struct { 813 | Min, Max ValueVector2 814 | } 815 | 816 | func newValueRect() Value { 817 | return *new(ValueRect) 818 | } 819 | 820 | func (ValueRect) Type() Type { 821 | return TypeRect 822 | } 823 | func (t ValueRect) String() string { 824 | return joinstr( 825 | strconv.FormatFloat(float64(t.Min.X), 'f', -1, 32), 826 | ", ", 827 | strconv.FormatFloat(float64(t.Min.Y), 'f', -1, 32), 828 | ", ", 829 | strconv.FormatFloat(float64(t.Max.X), 'f', -1, 32), 830 | ", ", 831 | strconv.FormatFloat(float64(t.Max.Y), 'f', -1, 32), 832 | ) 833 | } 834 | func (t ValueRect) Copy() Value { 835 | return t 836 | } 837 | 838 | //////////////// 839 | 840 | type ValuePhysicalProperties struct { 841 | CustomPhysics bool 842 | Density float32 843 | Friction float32 844 | Elasticity float32 845 | FrictionWeight float32 846 | ElasticityWeight float32 847 | } 848 | 849 | func newValuePhysicalProperties() Value { 850 | return *new(ValuePhysicalProperties) 851 | } 852 | 853 | func (ValuePhysicalProperties) Type() Type { 854 | return TypePhysicalProperties 855 | } 856 | func (t ValuePhysicalProperties) String() string { 857 | if t.CustomPhysics { 858 | return joinstr( 859 | strconv.FormatFloat(float64(t.Density), 'f', -1, 32), ", ", 860 | strconv.FormatFloat(float64(t.Friction), 'f', -1, 32), ", ", 861 | strconv.FormatFloat(float64(t.Elasticity), 'f', -1, 32), ", ", 862 | strconv.FormatFloat(float64(t.FrictionWeight), 'f', -1, 32), ", ", 863 | strconv.FormatFloat(float64(t.ElasticityWeight), 'f', -1, 32), 864 | ) 865 | } 866 | return "nil" 867 | } 868 | func (t ValuePhysicalProperties) Copy() Value { 869 | return t 870 | } 871 | 872 | //////////////// 873 | 874 | type ValueColor3uint8 struct { 875 | R, G, B byte 876 | } 877 | 878 | func newValueColor3uint8() Value { 879 | return *new(ValueColor3uint8) 880 | } 881 | 882 | func (ValueColor3uint8) Type() Type { 883 | return TypeColor3uint8 884 | } 885 | func (t ValueColor3uint8) String() string { 886 | return joinstr( 887 | strconv.FormatUint(uint64(t.R), 10), 888 | ", ", 889 | strconv.FormatUint(uint64(t.G), 10), 890 | ", ", 891 | strconv.FormatUint(uint64(t.B), 10), 892 | ) 893 | } 894 | func (t ValueColor3uint8) Copy() Value { 895 | return t 896 | } 897 | 898 | //////////////// 899 | 900 | type ValueInt64 int64 901 | 902 | func newValueInt64() Value { 903 | return *new(ValueInt64) 904 | } 905 | 906 | func (ValueInt64) Type() Type { 907 | return TypeInt64 908 | } 909 | 910 | func (t ValueInt64) String() string { 911 | return strconv.FormatInt(int64(t), 10) 912 | } 913 | 914 | func (t ValueInt64) Copy() Value { 915 | return t 916 | } 917 | 918 | //////////////// 919 | 920 | type ValueSharedString []byte 921 | 922 | func newValueSharedString() Value { 923 | return make(ValueSharedString, 0) 924 | } 925 | 926 | func (ValueSharedString) Type() Type { 927 | return TypeSharedString 928 | } 929 | func (t ValueSharedString) String() string { 930 | return string(t) 931 | } 932 | func (t ValueSharedString) Copy() Value { 933 | c := make(ValueSharedString, len(t)) 934 | copy(c, t) 935 | return c 936 | } 937 | 938 | //////////////// 939 | 940 | type ValueOptional struct { 941 | typ Type 942 | value Value 943 | } 944 | 945 | func newValueOptional() Value { 946 | return ValueOptional{} 947 | } 948 | 949 | // Some returns a ValueOptional with the given value and value's type. 950 | func Some(value Value) ValueOptional { 951 | if value == nil { 952 | panic("option value cannot be nil") 953 | } 954 | return ValueOptional{ 955 | typ: value.Type(), 956 | value: value, 957 | } 958 | } 959 | 960 | // None returns a ValueOptional with type t and no value. 961 | func None(t Type) ValueOptional { 962 | return ValueOptional{ 963 | typ: t, 964 | value: nil, 965 | } 966 | } 967 | 968 | func (ValueOptional) Type() Type { 969 | return TypeOptional 970 | } 971 | func (t ValueOptional) String() string { 972 | if t.value == nil { 973 | return "nil" 974 | } 975 | return t.value.String() 976 | } 977 | func (t ValueOptional) Copy() Value { 978 | if t.value != nil { 979 | t.value = t.value.Copy() 980 | } 981 | return t 982 | } 983 | 984 | // Some sets the option to have the given value and value's type. 985 | func (v *ValueOptional) Some(value Value) ValueOptional { 986 | if value == nil { 987 | panic("option value cannot be nil") 988 | } 989 | v.typ = value.Type() 990 | v.value = value 991 | return *v 992 | } 993 | 994 | // None sets the option to have type t with no value. 995 | func (v *ValueOptional) None(t Type) ValueOptional { 996 | v.typ = t 997 | v.value = nil 998 | return *v 999 | } 1000 | 1001 | // Value returns the value of the option, or nil if the option has no value. 1002 | func (v ValueOptional) Value() Value { 1003 | return v.value 1004 | } 1005 | 1006 | // ValueType returns the the value type of the option. 1007 | func (v ValueOptional) ValueType() Type { 1008 | return v.typ 1009 | } 1010 | 1011 | //////////////// 1012 | 1013 | // ValueUniqueId represents the value of a unique identifier. 1014 | // 1015 | // In Roblox's implementation, it would appear that there is a base value held 1016 | // in memory, which is updated by certain conditions, then copied to a new 1017 | // instance. 1018 | // 1019 | // When a session starts, Random is initialized to a random value, apparently 1020 | // always positive. Time is initialized to the current time. Index seems to be 1021 | // initialized to 0x10000. 1022 | // 1023 | // Index is incremented every time an instance is created. If Index rolls over 1024 | // back to 0, Random and Time are updated. 1025 | type ValueUniqueId struct { 1026 | // A pseudo-randomly generated value. 1027 | Random int64 1028 | // The number of seconds after 2021-01-01 00:00:00. 1029 | Time uint32 1030 | // A sequential value. 1031 | Index uint32 1032 | } 1033 | 1034 | func newValueUniqueId() Value { 1035 | return *new(ValueUniqueId) 1036 | } 1037 | 1038 | func (ValueUniqueId) Type() Type { 1039 | return TypeUniqueId 1040 | } 1041 | 1042 | func (v ValueUniqueId) String() string { 1043 | var b [32]byte 1044 | binary.BigEndian.PutUint64(b[0:8], uint64(v.Random)) 1045 | binary.BigEndian.PutUint32(b[8:12], v.Time) 1046 | binary.BigEndian.PutUint32(b[12:16], v.Index) 1047 | const hextable = "0123456789abcdef" 1048 | for i := len(b)/2 - 1; i >= 0; i-- { 1049 | b[i*2+1] = hextable[b[i]&0x0f] 1050 | b[i*2] = hextable[b[i]>>4] 1051 | } 1052 | return string(b[:]) 1053 | } 1054 | 1055 | func (t ValueUniqueId) Copy() Value { 1056 | return t 1057 | } 1058 | 1059 | //////////////// 1060 | 1061 | type FontStyle uint8 1062 | 1063 | const ( 1064 | FontStyleNormal = 0 1065 | FontStyleItalic = 1 1066 | ) 1067 | 1068 | func FontStyleFromString(s string) (fs FontStyle, ok bool) { 1069 | switch s { 1070 | case "Normal": 1071 | return FontStyleNormal, true 1072 | case "Italic": 1073 | return FontStyleItalic, true 1074 | default: 1075 | return 0, false 1076 | } 1077 | } 1078 | 1079 | func (f FontStyle) String() string { 1080 | switch f { 1081 | case FontStyleNormal: 1082 | return "Normal" 1083 | case FontStyleItalic: 1084 | return "Italic" 1085 | default: 1086 | return "" 1087 | } 1088 | } 1089 | 1090 | type FontWeight uint16 1091 | 1092 | const ( 1093 | FontWeightThin = 100 1094 | FontWeightExtraLight = 200 1095 | FontWeightLight = 300 1096 | FontWeightRegular = 400 1097 | FontWeightMedium = 500 1098 | FontWeightSemiBold = 600 1099 | FontWeightBold = 700 1100 | FontWeightExtraBold = 800 1101 | FontWeightHeavy = 900 1102 | ) 1103 | 1104 | func FontWeightFromString(s string) (fs FontWeight, ok bool) { 1105 | switch s { 1106 | case "Thin": 1107 | return FontWeightThin, true 1108 | case "ExtraLight": 1109 | return FontWeightExtraLight, true 1110 | case "Light": 1111 | return FontWeightLight, true 1112 | case "Regular": 1113 | return FontWeightRegular, true 1114 | case "Medium": 1115 | return FontWeightMedium, true 1116 | case "SemiBold": 1117 | return FontWeightSemiBold, true 1118 | case "Bold": 1119 | return FontWeightBold, true 1120 | case "ExtraBold": 1121 | return FontWeightExtraBold, true 1122 | case "Heavy": 1123 | return FontWeightHeavy, true 1124 | default: 1125 | return 0, false 1126 | } 1127 | } 1128 | 1129 | func (f FontWeight) String() string { 1130 | switch f { 1131 | case FontWeightThin: 1132 | return "Thin" 1133 | case FontWeightExtraLight: 1134 | return "ExtraLight" 1135 | case FontWeightLight: 1136 | return "Light" 1137 | case FontWeightRegular: 1138 | return "Regular" 1139 | case FontWeightMedium: 1140 | return "Medium" 1141 | case FontWeightSemiBold: 1142 | return "SemiBold" 1143 | case FontWeightBold: 1144 | return "Bold" 1145 | case FontWeightExtraBold: 1146 | return "ExtraBold" 1147 | case FontWeightHeavy: 1148 | return "Heavy" 1149 | default: 1150 | return "" 1151 | } 1152 | } 1153 | 1154 | type ValueFont struct { 1155 | Family ValueContent 1156 | Weight FontWeight 1157 | Style FontStyle 1158 | CachedFaceId ValueContent 1159 | } 1160 | 1161 | func newValueFont() Value { 1162 | return *new(ValueFont) 1163 | } 1164 | 1165 | func (ValueFont) Type() Type { 1166 | return TypeFont 1167 | } 1168 | 1169 | func (v ValueFont) String() string { 1170 | var s strings.Builder 1171 | s.WriteString("Font { Family = ") 1172 | s.Write(v.Family) 1173 | s.WriteString(", Weight = ") 1174 | s.WriteString(v.Weight.String()) 1175 | s.WriteString(", Style = ") 1176 | s.WriteString(v.Style.String()) 1177 | s.WriteString(" }") 1178 | return s.String() 1179 | } 1180 | 1181 | func (t ValueFont) Copy() Value { 1182 | return ValueFont{ 1183 | Family: t.Family.Copy().(ValueContent), 1184 | Weight: t.Weight, 1185 | Style: t.Style, 1186 | CachedFaceId: t.Family.Copy().(ValueContent), 1187 | } 1188 | } 1189 | -------------------------------------------------------------------------------- /values_test.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package rbxfile 4 | 5 | import ( 6 | "math" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | var testTypes = []Type{} 13 | 14 | func init() { 15 | testTypes = make([]Type, len(typeStrings)) 16 | for i := range testTypes { 17 | testTypes[i] = Type(i + 1) 18 | } 19 | } 20 | 21 | func TestType_String(t *testing.T) { 22 | if TypeString.String() != "String" { 23 | t.Error("unexpected result") 24 | } 25 | 26 | if Type(0).String() != "Invalid" || Type(len(testTypes)+1).String() != "Invalid" { 27 | t.Error("expected Invalid string") 28 | } 29 | } 30 | 31 | func TestTypeFromString(t *testing.T) { 32 | for _, typ := range testTypes { 33 | if st := TypeFromString(typ.String()); st != typ { 34 | t.Errorf("expected type %s from TypeFromString (got %s)", typ, st) 35 | } 36 | } 37 | 38 | if TypeFromString("String") != TypeString { 39 | t.Error("unexpected result from TypeFromString") 40 | } 41 | 42 | if TypeFromString("UnknownType") != TypeInvalid { 43 | t.Error("unexpected result from TypeFromString") 44 | } 45 | } 46 | 47 | func TestNewValue(t *testing.T) { 48 | for _, typ := range testTypes { 49 | name := reflect.ValueOf(NewValue(typ)).Type().Name() 50 | if strings.TrimPrefix(name, "Value") != typ.String() { 51 | t.Errorf("type %s does not match Type%s", name, typ) 52 | } 53 | } 54 | if NewValue(TypeInvalid) != nil { 55 | t.Error("expected nil value for invalid type") 56 | } 57 | } 58 | 59 | func TestValueCopy(t *testing.T) { 60 | for _, typ := range testTypes { 61 | v := NewValue(typ) 62 | c := v.Copy() 63 | if !reflect.DeepEqual(v, c) { 64 | t.Errorf("copy of value %q is not equal to original", v.Type().String()) 65 | } 66 | } 67 | } 68 | 69 | type testCompareString struct { 70 | v Value 71 | s string 72 | } 73 | 74 | func testCompareStrings(t *testing.T, vts []testCompareString) { 75 | for _, vt := range vts { 76 | if vt.v.String() != vt.s { 77 | t.Errorf("unexpected result from String method of value %q (%q expected, got %q)", vt.v.Type().String(), vt.s, vt.v.String()) 78 | } 79 | } 80 | } 81 | 82 | func TestValueString(t *testing.T) { 83 | testCompareStrings(t, []testCompareString{ 84 | {ValueString("test\000string"), "test\000string"}, 85 | {ValueBinaryString("test\000string"), "test\000string"}, 86 | {ValueProtectedString("test\000string"), "test\000string"}, 87 | {ValueContent("test\000string"), "test\000string"}, 88 | 89 | {ValueBool(true), "true"}, 90 | {ValueBool(false), "false"}, 91 | 92 | {ValueInt(42), "42"}, 93 | {ValueInt(-42), "-42"}, 94 | 95 | {ValueFloat(8388607.314159), "8388607.5"}, 96 | {ValueFloat(math.Pi), "3.1415927"}, 97 | {ValueFloat(-math.Phi), "-1.618034"}, 98 | {ValueFloat(math.Inf(1)), "+Inf"}, 99 | {ValueFloat(math.Inf(-1)), "-Inf"}, 100 | {ValueFloat(math.NaN()), "NaN"}, 101 | 102 | {ValueDouble(8388607.314159), "8388607.314159"}, 103 | {ValueDouble(math.Pi), "3.141592653589793"}, 104 | {ValueDouble(-math.Phi), "-1.618033988749895"}, 105 | {ValueDouble(math.Inf(1)), "+Inf"}, 106 | {ValueDouble(math.Inf(-1)), "-Inf"}, 107 | {ValueDouble(math.NaN()), "NaN"}, 108 | 109 | {ValueUDim{ 110 | Scale: math.Pi, 111 | Offset: 12345, 112 | }, "3.1415927, 12345"}, 113 | 114 | {ValueUDim2{ 115 | X: ValueUDim{ 116 | Scale: 1, 117 | Offset: 2, 118 | }, 119 | Y: ValueUDim{ 120 | Scale: 3, 121 | Offset: 4, 122 | }, 123 | }, "{1, 2}, {3, 4}"}, 124 | 125 | {ValueRay{ 126 | Origin: ValueVector3{X: 1, Y: 2, Z: 3}, 127 | Direction: ValueVector3{X: 4, Y: 5, Z: 6}, 128 | }, "{1, 2, 3}, {4, 5, 6}"}, 129 | 130 | {ValueFaces{ 131 | Front: true, 132 | Bottom: true, 133 | Left: true, 134 | Back: true, 135 | Top: true, 136 | Right: true, 137 | }, "Front, Bottom, Left, Back, Top, Right"}, 138 | {ValueFaces{ 139 | Front: true, 140 | Bottom: false, 141 | Left: true, 142 | Back: false, 143 | Top: true, 144 | Right: false, 145 | }, "Front, Left, Top"}, 146 | 147 | {ValueAxes{X: true, Y: true, Z: true}, "X, Y, Z"}, 148 | {ValueAxes{X: true, Y: false, Z: true}, "X, Z"}, 149 | 150 | {ValueBrickColor(194), "194"}, 151 | 152 | {ValueColor3{R: 0.5, G: 0.25, B: 0.75}, "0.5, 0.25, 0.75"}, 153 | 154 | {ValueVector2{X: 1, Y: 2}, "1, 2"}, 155 | 156 | {ValueVector3{X: 1, Y: 2, Z: 3}, "1, 2, 3"}, 157 | 158 | {ValueCFrame{ 159 | Position: ValueVector3{X: 1, Y: 2, Z: 3}, 160 | Rotation: [9]float32{4, 5, 6, 7, 8, 9, 10, 11, 12}, 161 | }, "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12"}, 162 | 163 | {ValueToken(42), "42"}, 164 | 165 | {ValueReference{}, ""}, 166 | {ValueReference{Instance: namedInst("Instance", nil)}, "Instance"}, 167 | 168 | {ValueVector3int16{X: 1, Y: 2, Z: 3}, "1, 2, 3"}, 169 | 170 | {ValueVector2int16{X: 1, Y: 2}, "1, 2"}, 171 | }, 172 | ) 173 | } 174 | --------------------------------------------------------------------------------