反序列化 Minecraft 的更好方法 json

Better way to deserialize Minecraft json

我正在尝试使用来自 (1.8.json download) 的 Minecraft json。样本:

{
  "objects": {
    "realms/lang/de_DE.lang": {
      "hash": "729b2c09d5c588787b23127eeda2730f9c039194",
      "size": 7784
    },
    "realms/lang/cy_GB.lang": {
      "hash": "7b52463b2df4685d2d82c5d257fd5ec79843d618",
      "size": 7688
    },
    "minecraft/sounds/mob/blaze/breathe4.ogg": {
      "hash": "78d544a240d627005aaef6033fd646eafc66fe7a",
      "size": 22054
    },
    "minecraft/sounds/dig/sand4.ogg": {
      "hash": "37afa06f97d58767a1cd1382386db878be1532dd",
      "size": 5491
    }
  }
}

实际的 json 长得多,大约 2940 行。

我需要一种反序列化它的方法,它并不完全疯狂 - 使用 JSONUtils 我得到 4411 行代码,但相同的代码不能用于任何其他版本的 Minecraft。

The automated tools can be very useful, but they are not perfect - especially when it comes to dictionaries.

The first thing to note is that the structure is the same for all of them, that is they all consist of hash and size properties. This means rather than creating hundreds of identical classes we can use the same one over and over:

' the type that repeats from your robot:
Public Class MinecraftItem
    Public Property hash As String
    Public Property size As Int32
End Class

Since the automated tools are working on the fly (they do not look ahead...or back) they don't really know that they are all the same. The next thing is that the classes the robots generate wont work in this case:

Public Property minecraft/sounds/music/game/creative/creative3.ogg As _
            MinecraftSoundsMusicGameCreativeCreative3Ogg

That is illegal as a 属性 name. However, the names will work just fine as Dictionary keys. In addition to the above MinecraftItem class, we might want a container class:

Public Class MinecraftContainer
    Public objects As Dictionary(Of String, MinecraftItem)
End Class

There are (at least) 3 ways to get at the data:

Method 1: Pluck out a single item

Imports Newtonsoft.Json
Imports Newtonsoft.Json.Linq

Dim jstr As String = ...from whereever

' parse the json into a JObject
Dim js As JObject = JObject.Parse(jstr)

' if you somehow know the names, you can pluck out the data:
Dim mi = js("objects")("minecraft/sounds/mob/blaze/hit2.ogg")

Console.WriteLine(mi("hash").ToString & "    " & mi("size").ToString)

The first line parses the original json string into a JObject. This allows us to work with the contents in various ways. For instance, we can reference "objects" in the json and them the items by name, which is exactly what happens in the next line:

' drill into "objects", get the "...hit2.ogg" item
Dim mi = js("objects")("minecraft/sounds/mob/blaze/hit2.ogg")

This will work if you just need to fetch a particular item from the big file. The mi variable created is a "special" json token, so use the names to get the data bits you want

hash = mi("hash").ToString
size = mi("size").ToString

Method 2: Deserialize To a Dictionary

This would be my preferred method. Include the same Import statements as in the first example, then:

' parse the json string
Dim js As JObject = JObject.Parse(jstr)

' deserialize the inner "objects" to a NET Dictionary
Dim myItems = JsonConvert.DeserializeObject(Of Dictionary(Of String, _
                   MinecraftItem))(js("objects").ToString)

This will create myItems as a Net Dictionary(Of String, MincraftItem) from the json. Since the MinecraftObject class doesn't do anything but hold the dictionary, we skipped it.

The keys are the long names and each value is a MinecraftItem which allows you to reference them more conventionally:

' get one of the items into a variable
gravel3 = myItems("minecraft/sounds/mob/chicken/step2.ogg")
ConsoleWriteLine("Gravel3  hash: {0},  size: {1}",
                      gravel3.hash, gravel3.size.ToString)

If you are not familiar with the .Net Dictionary it is somewhat like an array or List except you access your items by a Key rather then index. To loop thru them all:

Dim n As Integer = 0
For Each kvp As KeyValuePair(Of String, MinecraftItem) In myItems
    Console.WriteLine("Name: {0}  Hash: {1}  size: {2}",
                      kvp.Key, 
                      kvp.Value.hash, 
                      kvp.Value.size.ToString)
    n += 1
    If n >= 2 Then Exit For           ' just print 3
Next                              

Output:

Name: realms/lang/de_DE.lang Hash: 10a54fc66c8f479bb65c8d39c3b62265ac82e742 size: 8112
Name: realms/lang/cy_GB.lang Hash: 14cfb2f24e7d91dbc22a2a0e3b880d9829320243 size: 7347
Name: minecraft/sounds/mob/chicken/step2.ogg Hash: bf7fadaf64945f6b31c803d086ac6a652aabef9b size: 3838

Just remember, the Key will always be the long path name, and each .Value is MinecraftItem object.

Method 3: Deserialize To a Container

You can skip the parsing step by using the MinecraftContainer class:

Dim jstr As String = ...from whereever
Dim myJItems = JsonConvert.DeserializeObject(Of MinecraftContainer)(jstr)

Note that this time, you pass the entire string you downloaded to JsonConvert. The result will be an extra outer object which contains a dictionary 属性 named "Objects". So, you reference the items using some leading references:

gravel3hash = myJItems.Object("minecraft/sounds/dig/gravel3.ogg").hash

gravel3 = myJItems.Object("minecraft/sounds/dig/gravel3.ogg")
    ConsoleWriteLine("Gravel3  hash: {0},  size: {1}",
                      gravel3.hash, gravel3.size.ToString)
'or:
ConsoleWriteLine("Gravel3  hash: {0},  size: {1}",
                   myJItems.Object("minecraft/sounds/dig/gravel3.ogg").hash, 
                   myJItems.Object("minecraft/sounds/dig/gravel3.ogg").size.ToString)   

This method is just one line of code to deserialize, but it means
a) I have to define the container class and
b) I have to use myJItems.Object to drill into the otherwise empty container to get at the Dictionary.

Personally, I would forego this and use method 2 - one extra line of code makes it a bit easier to work with. That said, you can also extract the dictionary collection from the container:

Dim myItems As Dictionary(Of String, MinecraftItem)= myJItems.Object