File: ObjectExporter.fs

package info (click to toggle)
openrct2-objects 1.7.6%2Bdfsg-1
  • links: PTS, VCS
  • area: contrib
  • in suites: sid
  • size: 28,024 kB
  • sloc: javascript: 316; python: 307; makefile: 25
file content (469 lines) | stat: -rw-r--r-- 18,568 bytes parent folder | download | duplicates (6)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
// objexport
// Exports objects from RCT2 to OpenRCT2 json files

namespace OpenRCT2.Legacy.ObjectExporter

module ResizeArray =
    let tryItem index (list: ResizeArray<'T>) =
        if index < 0 || index >= list.Count then None
        else Some list.[index]

module Dictionary =
    let tryItem key (dict: System.Collections.Generic.IDictionary<'TKey, 'TValue>) =
        match dict.TryGetValue key with
        | true, value -> Some value
        | _ -> None

module ObjectExporter =

    open System
    open System.Collections.Generic
    open System.ComponentModel
    open System.Diagnostics
    open System.IO
    open System.Text
    open JsonTypes
    open Microsoft.FSharp.Core
    open Newtonsoft.Json
    open Newtonsoft.Json.FSharp
    open PropertyExtractor
    open RCT2ObjectData.Objects
    open RCT2ObjectData.Objects.Types

    type ObjectExporterOptions =
        { id: string option
          authors: string list
          languageDirectory: string option
          objectType: string option
          multithreaded: bool
          splitFootpaths: bool
          storePng: bool
          outputParkobj: bool }

    let printWithColour (c: ConsoleColor) (s: string) =
        let oldColour = Console.ForegroundColor
        Console.ForegroundColor <- c
        Console.WriteLine(s)
        Console.ForegroundColor <- oldColour

    let UTF8NoBOM = new UTF8Encoding(false)

    let serializeToJson (value: 'a) =
        let sb = new StringBuilder(capacity = 256)
        let sw = new StringWriter(sb)
        let jsonSerializer = JsonSerializer.CreateDefault()
        use jsonWriter = new MyJsonWriter(sw)
        Seq.iter (jsonSerializer.Converters.Add) JsonFsharp.converters
        jsonSerializer.ContractResolver <- new ObjectContractResolver()
        jsonSerializer.Serialize(jsonWriter, value, typedefof<'a>)
        sw.ToString()

    let serializeToJsonFile path (value: 'a) =
        let json = serializeToJson value + Environment.NewLine
        File.WriteAllText(path, json, UTF8NoBOM)

    let createDirectory path =
        try
            if not (Directory.Exists path) then
                Directory.CreateDirectory(path) |> ignore
            true
        with
        | ex ->
            printfn "Unable to create '%s': %s" path ex.Message
            false

    let getObjId (groupPrefix: string) (suffix: string) (obj: ObjectData) =
        let sourcePrefix =
            match obj.Source with
            | SourceTypes.RCT2 -> "rct2"
            | SourceTypes.WW -> "rct2.ww"
            | SourceTypes.TT -> "rct2.tt"
            | _ -> "other"

        let name = obj.ObjectHeader.FileName.ToLower()

        [sourcePrefix; groupPrefix; name; suffix]
        |> List.filter (String.IsNullOrEmpty >> not)
        |> String.concat "."

    let getObjTypeName = function
        | ObjectTypes.Attraction -> "ride"
        | ObjectTypes.SceneryGroup -> "scenery_group"
        | ObjectTypes.SmallScenery -> "scenery_small"
        | ObjectTypes.LargeScenery -> "scenery_large"
        | ObjectTypes.Wall -> "scenery_wall"
        | ObjectTypes.Footpath -> "footpath"
        | ObjectTypes.PathAddition -> "footpath_item"
        | ObjectTypes.PathBanner -> "footpath_banner"
        | ObjectTypes.ParkEntrance -> "park_entrance"
        | ObjectTypes.Water -> "water"
        | _ -> "other"

    let getSourceDirectoryName = function
        | SourceTypes.RCT2 -> "rct2"
        | SourceTypes.WW -> "rct2ww"
        | SourceTypes.TT -> "rct2tt"
        | _ -> "other"

    let getLanguageName = function
        | 0 -> "en-GB"
        | 1 -> "en-US"
        | 2 -> "fr-FR"
        | 3 -> "de-DE"
        | 4 -> "es-ES"
        | 5 -> "it-IT"
        | 6 -> "nl-NL"
        | 7 -> "sv-SE"
        | 8 -> "ja-JP"
        | 9 -> "ko-KR"
        | 10 -> "zh-CN"
        | 11 -> "zh-TW"
        | 13 -> "pt-BR"
        | i -> i.ToString()

    let getOriginalObjectIdString (hdr: ObjectDataHeader) =
        String.Format("{0:X8}|{1,-8}|{2:X8}", hdr.Flags, hdr.FileName, hdr.Checksum)

    let getAuthorsForSource source =
        match source with
        | SourceTypes.RCT2 ->
            [|"Chris Sawyer"; "Simon Foster"|]
        | SourceTypes.WW
        | SourceTypes.TT ->
            [|"Frontier Studios"|]
        | _ ->
            [||]

    let getObjectStrings (obj: ObjectData) (ourStrings: IDictionary<string, IDictionary<string, string>>) =
        let getStrings index =
            let stringEntries = obj.StringTable.Entries
            match ResizeArray.tryItem index stringEntries with
            | None -> dict []
            | Some stringEntry ->
                let strings =
                    stringEntry.Strings
                    |> Seq.mapi(fun i str ->
                        let lang = getLanguageName i
                        let decoded = Localisation.decodeStringFromRCT2 lang str.Data
                        (lang, decoded.Trim()))
                    |> Seq.filter(fun (_, str) ->
                        // Decide whether the string is useful
                        match str with
                        | "" -> false
                        | s when s.StartsWith("#not translated", StringComparison.OrdinalIgnoreCase) -> false
                        | _ -> true)
                    |> Seq.toArray

                strings
                |> Seq.filter(fun (x, y) -> x = getLanguageName 0 || y <> snd strings.[0])
                |> dict

        let theirStrings =
            let entries =
                if obj.Type = ObjectTypes.Attraction then
                    [| ("name", getStrings 0);
                       ("description", getStrings 1);
                       ("capacity", getStrings 2)|]
                else
                    [| ("name", getStrings 0) |]

            entries
            |> Array.filter(fun (_, y) -> y.Count > 0)
            |> dict

        // Overlay our strings on top of the RCT2 strings
        let strings = Localisation.overlayStrings ourStrings theirStrings

        // Remove unwanted strings
        let validKeys =
            match obj.Type with
            | ObjectTypes.Attraction -> ["name"; "description"; "capacity"]
            | _ -> ["name"]
        for s in Seq.toArray strings.Keys do
            if not (List.contains s validKeys) then
                strings.Remove(s) |> ignore

        // Remove empty string collections
        strings
        |> Seq.where (fun kvp -> kvp.Value.Count = 0)
        |> Seq.toArray
        |> Seq.iter (fun kvp -> strings.Remove(kvp.Key) |> ignore)

        // Return
        strings

    let packageIntoParkobj path =
        let parkobjPath = path + ".parkobj"
        if File.Exists(parkobjPath) then
            File.Delete(parkobjPath)
        System.IO.Compression.ZipFile.CreateFromDirectory(path, parkobjPath)
        Directory.Delete(path, true)

    let gxcBuild outputPath inputPath =
        try
            let psi = new ProcessStartInfo()
            psi.FileName <- "gxc"
            psi.ArgumentList.Add("build")
            psi.ArgumentList.Add(outputPath)
            psi.ArgumentList.Add(inputPath)
            psi.RedirectStandardOutput <- true
            let proc = System.Diagnostics.Process.Start(psi)
            proc.BeginOutputReadLine() // Prevents the process from hanging
            proc.WaitForExit()
        with
        | :? Win32Exception as ex when ex.NativeErrorCode = 2 ->
            "Unable to find gxc on PATH (pass --png to skip gxc)"
            |> printWithColour ConsoleColor.Red

    let buildGxFile outputPath images =
        let lgxFilename = "images.dat"
        let lgxPath = Path.Combine(outputPath, lgxFilename)
        sprintf "Building %s" lgxFilename
        |> printWithColour ConsoleColor.DarkGray
        let imagePngPath = Path.Combine(outputPath, "images.png")
        let numImages = List.length images
        let imageJsonPath = Path.Combine(outputPath, "images.json")
        serializeToJsonFile imageJsonPath images
        gxcBuild lgxPath imageJsonPath
        File.Delete(imagePngPath)
        File.Delete(imageJsonPath)
        [sprintf "$LGX:%s[0..%d]" lgxFilename (numImages - 1)]

    let exportParkObject outputPath (ourStrings: IDictionary<string, IDictionary<string, string>>) (inputPath: string) (obj: ObjectData) (options: ObjectExporterOptions) =
        let exportSubObject splitKind objId =
            let outputPath = Path.Combine(outputPath, objId)
            let objName = obj.ObjectHeader.FileName.ToUpper()
            let outputJsonPath = Path.Combine(outputPath, "object.json")

            sprintf "Exporting %s to %s" objName (Path.GetFullPath(outputJsonPath))
            |> printWithColour ConsoleColor.DarkGray

            if createDirectory outputPath then
                let images =
                    let println = printWithColour ConsoleColor.DarkGray
                    let numImages = obj.GraphicsData.NumImages
                    let ids =
                        match splitKind with
                        | Some FootpathSurface -> 71 :: [0..50]
                        | Some FootpathQueue -> 72 :: [51..70]
                        | Some FootpathRailings ->
                            let footpathObj = obj :?> Footpath
                            let flags = footpathObj.Header.Flags
                            let hasPoleSupports = flags.HasFlag(FootpathFlags.PoleSupports)
                            let hasSupportImages = flags.HasFlag(FootpathFlags.PoleBase)
                            let hasElevatedPathImages = flags.HasFlag(FootpathFlags.OverlayPath)
                            if hasPoleSupports then
                                if hasSupportImages then
                                    71 :: [73..164]
                                else
                                    71 :: [73..145]
                            else
                                71 :: [73..167]
                        | None -> [0..numImages - 1]
                    obj |> ImageExporter.exportImages ids outputPath println

                let images =
                    match options.storePng with
                    | true -> images :> obj
                    | false -> buildGxFile outputPath images

                let properties =
                    match splitKind with
                    | Some kind -> obj :?> Footpath |> getFootpathSplit kind
                    | None -> obj |> getProperties

                let objectType =
                    match splitKind with
                    | Some FootpathSurface
                    | Some FootpathQueue -> "footpath_surface"
                    | Some FootpathRailings -> "footpath_railings"
                    | None -> getObjTypeName obj.Type

                let authors =
                    match options.authors with
                    | [] -> getAuthorsForSource obj.Source
                    | items -> items |> List.toArray

                let originalId =
                    match splitKind with
                    | Some _ -> null
                    | None -> getOriginalObjectIdString obj.ObjectHeader

                let jobj = { id = objId
                             authors = authors
                             version = "1.0"
                             originalId = originalId
                             objectType = objectType
                             properties = properties
                             images = images
                             strings = getObjectStrings obj ourStrings }

                let json = serializeToJson jobj + Environment.NewLine
                File.WriteAllText(outputJsonPath, json, UTF8NoBOM)

                if options.outputParkobj then
                    packageIntoParkobj outputPath

        if obj.ObjectHeader.Type = ObjectTypes.Footpath && options.splitFootpaths then
            let (surfaceId, queueId, railingsId) =
                match options.id with
                | Some id ->
                    let surfaceId = id + ".surface"
                    let queueId = id + ".queue"
                    let railingsId = id + ".railings"
                    (surfaceId, queueId, railingsId)
                | None ->
                    let surfaceId = obj |> getObjId "footpath_surface" ""
                    let queueId = obj |> getObjId "footpath_surface" "queue"
                    let railingsId = obj |> getObjId "footpath_railings" ""
                    (surfaceId, queueId, railingsId)

            exportSubObject (Some FootpathSurface) surfaceId
            exportSubObject (Some FootpathQueue) queueId
            exportSubObject (Some FootpathRailings) railingsId
        else
            let id =
                match options.id with
                | Some id -> id
                | None -> obj |> getObjId "" ""
            exportSubObject None id

    let exportObject outputPath (ourStrings: IDictionary<string, IDictionary<string, string>>) (inputPath: string) (obj: ObjectData) =
        let getOutputJsonPath basePath (obj: ObjectData) name =
            Path.Combine(basePath,
                         getSourceDirectoryName obj.Source,
                         getObjTypeName obj.Type,
                         name + ".json")

        let inputFileName = Path.GetFileNameWithoutExtension(inputPath).ToUpper()
        let objName = obj.ObjectHeader.FileName.ToUpper()
        let objId = obj |> getObjId "" ""
        let outputJsonPath = getOutputJsonPath outputPath obj objId

        sprintf "Exporting %s to %s" objName (Path.GetFullPath(outputJsonPath))
        |> printWithColour ConsoleColor.DarkGray

        // Get RCT2 images
        let numImages = obj.ImageDirectory.NumEntries
        let images =
            match obj.Type with
            | ObjectTypes.Water -> null
            | _ -> [| sprintf "$RCT2:OBJDATA/%s.DAT[%d..%d]" inputFileName 0 (numImages - 1) |]

        let properties = getProperties obj
        let jobj = { id = objId
                     authors = getAuthorsForSource obj.Source
                     version = "1.0"
                     originalId = getOriginalObjectIdString obj.ObjectHeader
                     objectType = getObjTypeName obj.Type
                     properties = properties
                     images = images
                     strings = getObjectStrings obj ourStrings }

        let json = serializeToJson jobj + Environment.NewLine
        Directory.CreateDirectory(Path.GetDirectoryName(outputJsonPath)) |> ignore
        File.WriteAllText(outputJsonPath, json, UTF8NoBOM)

    type FileTypeResult = Directory | File

    let exportObjects path outputPath options =
        // Load new object strings from OpenRCT2
        let objectStrings =
            match options.languageDirectory with
            | None -> dict []
            | Some dir ->
                sprintf "Reading object strings from '%s'" dir
                |> printWithColour ConsoleColor.Cyan
                Localisation.getOpenObjectStrings dir

        let shouldObjectBeProcessed =
            match options.objectType with
            | None ->
                (fun _ -> true)
            | Some includeType ->
                (fun (obj: ObjectData) -> includeType = getObjTypeName obj.Type)

        let getOverrideStringsForObject (obj: ObjectData) =
            match objectStrings.TryGetValue obj.ObjectHeader.FileName with
            | true, value -> value
            | _ -> dict []

        let getFileType path =
            if File.GetAttributes(path).HasFlag(FileAttributes.Directory) then
                Some Directory
            elif File.Exists(path) then
                Some File
            else
                None

        let measureTime fn =
            let sw = Stopwatch.StartNew()
            let result = fn ()
            sw.Stop()
            (result, sw.Elapsed.TotalSeconds)

        let tryReadObjectData path =
            try
                ObjectData.FromFile(path) |> Some
            with
            | _ ->
                None

        let exportSingleObject () =
            if not (createDirectory outputPath) then
                printfn "'%s' does not exist" path
                1
            else
                sprintf "Exporting object from '%s' to '%s'" path outputPath
                |> printWithColour ConsoleColor.Cyan
                let (_, time) = measureTime (fun () ->
                    let obj = ObjectData.FromFile(path)
                    let strings = getOverrideStringsForObject obj
                    exportParkObject outputPath strings path obj options)
                sprintf "Object exported in %.1fs" time
                |> printWithColour ConsoleColor.Green
                0

        let exportAllObjects () =
            if not (createDirectory outputPath) then
                printfn "'%s' does not exist" path
                1
            else
                let processObject path (obj: ObjectData) =
                    if not (isNull obj) && obj.Type <> ObjectTypes.ScenarioText && shouldObjectBeProcessed obj then
                        let strings = getOverrideStringsForObject obj
                        exportObject outputPath strings path obj
                        Some ()
                    else
                        None

                let readAndProcessObject path =
                    match tryReadObjectData path with
                    | Some objdata ->
                        processObject path objdata
                    | None ->
                        sprintf "Unable to read object data for %s" path
                        |> printWithColour ConsoleColor.Red
                        None

                sprintf "Exporting objects from '%s' to '%s'" path outputPath
                |> printWithColour ConsoleColor.Cyan
                let (numObj, time) = measureTime (fun () ->
                    Directory.GetFiles(path)
                    |> Array.where (fun x -> x.EndsWith(".dat", StringComparison.OrdinalIgnoreCase))
                    |> match options.multithreaded with
                       | true -> Array.Parallel.choose readAndProcessObject
                       | false -> Array.choose readAndProcessObject
                    |> Array.length)
                sprintf "%d objects exported in %.1fs" numObj time
                |> printWithColour ConsoleColor.Green
                0

        match getFileType path with
        | Some File ->
            exportSingleObject ()
        | Some Directory ->
            exportAllObjects ()
        | _ ->
            printfn "'%s' does not exist" path
            1