├── .gitignore ├── LICENCE ├── README.md ├── burger ├── __init__.py ├── roundedfloats.py ├── toppings │ ├── __init__.py │ ├── biomes.py │ ├── blocks.py │ ├── blockstates.py │ ├── entities.py │ ├── entitymetadata.py │ ├── identify.py │ ├── items.py │ ├── language.py │ ├── objects.py │ ├── packetinstructions.py │ ├── packets.py │ ├── particletypes.py │ ├── recipes.py │ ├── sounds.py │ ├── stats.py │ ├── tags.py │ ├── tileentities.py │ ├── topping.py │ └── version.py ├── util.py └── website.py ├── munch.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.class 3 | *.jar 4 | *.swp 5 | test.py 6 | build/ 7 | 8 | *.java 9 | *.bat 10 | *.json -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2011 Tyler Kennedy 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Burger 2 | Burger is a "framework" for automatically extracting data 3 | from the Minecraft game for the purpose of writing the protocol 4 | specification, interoperability, and other neat uses. 5 | 6 | ## The Idea 7 | Burger is made up of *toppings*, which can provide and satisfy 8 | simple dependencies, and which can be run all-together or just 9 | a few specifically. Each topping is then aggregated by 10 | `munch.py` into the whole and output as a JSON dictionary. 11 | 12 | ## Usage 13 | The simplest way to use Burger is to pass the `-d` or `--download` 14 | flag, which will download the specified minecraft client for you. 15 | This option can be specified multiple times. The downloaded jar will be saved 16 | in the working directory, and if it already exists the existing verison will be used. 17 | 18 | $ python munch.py --download 1.13.2 19 | 20 | To download the latest snapshot, `-D` or `--download-latest` can be used. 21 | 22 | $ python munch.py -D 23 | 24 | Alternatively, you can specify the client JAR by passing it as an argument. 25 | 26 | $ python munch.py 1.8.jar 27 | 28 | You can redirect the output from the default `stdout` by passing 29 | `-o ` or `--output `. This is useful when combined with 30 | verbose output (`-v` or `--verbose`) so that the output doesn't go into the file. 31 | 32 | $ python munch.py -D --output output.json 33 | 34 | You can see what toppings are available by passing `-l` or `--list`. 35 | 36 | $ python munch.py --list 37 | 38 | You can also run specific toppings by passing a comma-delimited list 39 | to `-t` or `--toppings`. If a topping cannot be used because it's 40 | missing a dependency, it will output an error telling you what 41 | also needs to be included. Toppings will generally automatically load 42 | their dependencies, however. 43 | 44 | $ python munch.py -D --toppings language,stats 45 | 46 | The above example would only extract the language information, as 47 | well as the stats and achievements (both part of `stats`). 48 | -------------------------------------------------------------------------------- /burger/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TkTech/Burger/868fd008dbbedf5b88f31535f9329506e6d4bd94/burger/__init__.py -------------------------------------------------------------------------------- /burger/roundedfloats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | """ 4 | Copyright (c) 2011 Tyler Kenendy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import six 26 | 27 | def transform_floats(o): 28 | if isinstance(o, float): 29 | return round(o, 5) 30 | elif isinstance(o, dict): 31 | return {k: transform_floats(v) for k, v in six.iteritems(o)} 32 | elif isinstance(o, (list, tuple)): 33 | return [transform_floats(v) for v in o] 34 | return o 35 | 36 | -------------------------------------------------------------------------------- /burger/toppings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TkTech/Burger/868fd008dbbedf5b88f31535f9329506e6d4bd94/burger/toppings/__init__.py -------------------------------------------------------------------------------- /burger/toppings/biomes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | """ 4 | Copyright (c) 2011 Tyler Kenendy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import six 26 | from .topping import Topping 27 | 28 | from jawa.util.descriptor import method_descriptor 29 | 30 | from jawa.constants import * 31 | 32 | class BiomeTopping(Topping): 33 | """Gets most biome types.""" 34 | 35 | PROVIDES = [ 36 | "identify.biome.superclass", 37 | "biomes" 38 | ] 39 | 40 | DEPENDS = [ 41 | "identify.biome.register", 42 | "identify.biome.list", 43 | "version.data", 44 | "language" 45 | ] 46 | 47 | @staticmethod 48 | def act(aggregate, classloader, verbose=False): 49 | if "biome.register" not in aggregate["classes"]: 50 | return 51 | data_version = aggregate["version"]["data"] if "data" in aggregate["version"] else -1 52 | if data_version >= 1901: # 18w43a 53 | BiomeTopping._process_114(aggregate, classloader, verbose) 54 | elif data_version >= 1466: # snapshot 18w06a 55 | BiomeTopping._process_113(aggregate, classloader, verbose) 56 | elif data_version != -1: 57 | BiomeTopping._process_19(aggregate, classloader, verbose) 58 | else: 59 | BiomeTopping._process_18(aggregate, classloader, verbose) 60 | 61 | @staticmethod 62 | def _process_18(aggregate, classloader, verbose): 63 | # Processes biomes for Minecraft 1.8 and below 64 | biomes_base = aggregate.setdefault("biomes", {}) 65 | biomes = biomes_base.setdefault("biome", {}) 66 | biome_fields = biomes_base.setdefault("biome_fields", {}) 67 | 68 | superclass = aggregate["classes"]["biome.register"] 69 | aggregate["classes"]["biome.superclass"] = superclass 70 | cf = classloader[superclass] 71 | 72 | mutate_method_desc = None 73 | mutate_method_name = None 74 | void_methods = cf.methods.find(returns="L" + superclass + ";", args="", f=lambda m: m.access_flags.acc_protected and not m.access_flags.acc_static) 75 | for method in void_methods: 76 | for ins in method.code.disassemble(): 77 | if ins == "sipush" and ins.operands[0].value == 128: 78 | mutate_method_desc = method.descriptor.value 79 | mutate_method_name = method.name.value 80 | 81 | make_mutated_method_desc = None 82 | make_mutated_method_name = None 83 | int_methods = cf.methods.find(returns="L" + superclass + ";", args="I", f=lambda m: m.access_flags.acc_protected and not m.access_flags.acc_static) 84 | for method in int_methods: 85 | for ins in method.code.disassemble(): 86 | if ins == "new": 87 | make_mutated_method_desc = method.descriptor.value 88 | make_mutated_method_name = method.name.value 89 | 90 | method = cf.methods.find_one(name="") 91 | heights_by_field = {} 92 | tmp = None 93 | stack = None 94 | 95 | def store_biome_if_valid(biome): 96 | """Stores the given biome if it is a valid, complete biome.""" 97 | if biome is not None and "name" in biome and biome["name"] != " and ": 98 | biomes[biome["name"]] = biome 99 | if "field" in biome: 100 | biome_fields[biome["field"]] = biome["name"] 101 | 102 | # OK, start running through the initializer for biomes. 103 | for ins in method.code.disassemble(): 104 | if ins == "new": 105 | store_biome_if_valid(tmp) 106 | 107 | stack = [] 108 | const = ins.operands[0] 109 | tmp = { 110 | "rainfall": 0.5, 111 | "height": [0.1, 0.2], 112 | "temperature": 0.5, 113 | "class": const.name.value 114 | } 115 | elif tmp is None: 116 | continue 117 | elif ins == "invokespecial": 118 | const = ins.operands[0] 119 | name = const.name_and_type.name.value 120 | if len(stack) == 2 and (isinstance(stack[1], float) or isinstance(stack[0], float)): 121 | # Height constructor 122 | tmp["height"] = [stack[0], stack[1]] 123 | stack = [] 124 | elif len(stack) >= 1 and isinstance(stack[0], int): # 1, 2, 3-argument beginning with int = id 125 | tmp["id"] = stack[0] 126 | stack = [] 127 | elif name != "": 128 | tmp["rainfall"] = 0 129 | elif ins == "invokevirtual": 130 | const = ins.operands[0] 131 | name = const.name_and_type.name.value 132 | desc = const.name_and_type.descriptor.value 133 | if name == mutate_method_name and desc == mutate_method_desc: 134 | # New, separate biome 135 | tmp = tmp.copy() 136 | tmp["name"] += " M" 137 | tmp["id"] += 128 138 | if "field" in tmp: 139 | del tmp["field"] 140 | tmp["height"][0] += .1 141 | tmp["height"][1] += .2 142 | store_biome_if_valid(tmp) 143 | elif name == make_mutated_method_name and desc == make_mutated_method_desc: 144 | # New, separate biome, but with a custom ID 145 | tmp = tmp.copy() 146 | tmp["name"] += " M" 147 | tmp["id"] += stack.pop() 148 | if "field" in tmp: 149 | del tmp["field"] 150 | tmp["height"][0] += .1 151 | tmp["height"][1] += .2 152 | store_biome_if_valid(tmp) 153 | elif len(stack) == 1: 154 | stack.pop() 155 | elif len(stack) == 2: 156 | tmp["rainfall"] = stack.pop() 157 | tmp["temperature"] = stack.pop() 158 | elif ins == "putstatic": 159 | const = ins.operands[0] 160 | field = const.name_and_type.name.value 161 | if "height" in tmp and not "name" in tmp: 162 | # Actually creating a height 163 | heights_by_field[field] = tmp["height"] 164 | else: 165 | tmp["field"] = field 166 | elif ins == "getstatic": 167 | # Loading a height map or preparing to mutate a biome 168 | const = ins.operands[0] 169 | field = const.name_and_type.name.value 170 | if field in heights_by_field: 171 | # Heightmap 172 | tmp["height"] = heights_by_field[field] 173 | else: 174 | # Store the old one first 175 | store_biome_if_valid(tmp) 176 | if field in biome_fields: 177 | tmp = biomes[biome_fields[field]] 178 | # numeric values & constants 179 | elif ins in ("ldc", "ldc_w"): 180 | const = ins.operands[0] 181 | if isinstance(const, String): 182 | tmp["name"] = const.string.value 183 | if isinstance(const, (Integer, Float)): 184 | stack.append(const.value) 185 | 186 | elif ins.mnemonic.startswith("fconst"): 187 | stack.append(float(ins.mnemonic[-1])) 188 | elif ins in ("bipush", "sipush"): 189 | stack.append(ins.operands[0].value) 190 | 191 | store_biome_if_valid(tmp) 192 | 193 | @staticmethod 194 | def _process_19(aggregate, classloader, verbose): 195 | # Processes biomes for Minecraft 1.9 through 1.12 196 | biomes_base = aggregate.setdefault("biomes", {}) 197 | biomes = biomes_base.setdefault("biome", {}) 198 | biome_fields = biomes_base.setdefault("biome_fields", {}) 199 | 200 | superclass = aggregate["classes"]["biome.register"] 201 | aggregate["classes"]["biome.superclass"] = superclass 202 | cf = classloader[superclass] 203 | 204 | method = cf.methods.find_one(returns="V", args="", f=lambda m: m.access_flags.acc_public and m.access_flags.acc_static) 205 | heights_by_field = {} 206 | first_new = True 207 | biome = None 208 | stack = [] 209 | 210 | # OK, start running through the initializer for biomes. 211 | for ins in method.code.disassemble(): 212 | if ins == "anewarray": 213 | # End of biome initialization; now creating the list of biomes 214 | # for the explore all biomes achievement but we don't need 215 | # that info. 216 | break 217 | 218 | if ins == "new": 219 | if first_new: 220 | # There are two 'new's in biome initialization - the first 221 | # one is for the biome generator itself and the second one 222 | # is the biome properties. There's some info that is only 223 | # stored on the first new (well, actually, beforehand) 224 | # that we want to save. 225 | const = ins.operands[0] 226 | 227 | text_id = stack.pop() 228 | numeric_id = stack.pop() 229 | 230 | biome = { 231 | "id": numeric_id, 232 | "text_id": text_id, 233 | "rainfall": 0.5, 234 | "height": [0.1, 0.2], 235 | "temperature": 0.5, 236 | "class": const.name.value 237 | } 238 | stack = [] 239 | 240 | first_new = not(first_new) 241 | elif ins == "invokestatic": 242 | # Call to the static registration method 243 | # We already saved its parameters at the constructor, so we 244 | # only need to store the biome now. 245 | biomes[biome["text_id"]] = biome 246 | elif ins == "invokespecial": 247 | # Possibly the constructor for biome properties, which takes 248 | # the name as a string. 249 | if len(stack) > 0 and not "name" in biome: 250 | biome["name"] = stack.pop() 251 | 252 | stack = [] 253 | elif ins == "invokevirtual": 254 | const = ins.operands[0] 255 | name = const.name_and_type.name.value 256 | desc = method_descriptor(const.name_and_type.descriptor.value) 257 | 258 | if len(desc.args) == 1: 259 | if desc.args[0].name == "float": 260 | # Ugly portion - different methods with different names 261 | # Hopefully the order doesn't change 262 | if name == "a": 263 | biome["temperature"] = stack.pop() 264 | elif name == "b": 265 | biome["rainfall"] = stack.pop() 266 | elif name == "c": 267 | biome["height"][0] = stack.pop() 268 | elif name == "d": 269 | biome["height"][1] = stack.pop() 270 | elif desc.args[0].name == "java/lang/String": 271 | # setBaseBiome 272 | biome["mutated_from"] = stack.pop() 273 | # numeric values & constants 274 | elif ins in ("ldc", "ldc_w"): 275 | const = ins.operands[0] 276 | if isinstance(const, String): 277 | stack.append(const.string.value) 278 | if isinstance(const, (Integer, Float)): 279 | stack.append(const.value) 280 | 281 | elif ins.mnemonic.startswith("fconst"): 282 | stack.append(float(ins.mnemonic[-1])) 283 | elif ins in ("bipush", "sipush"): 284 | stack.append(ins.operands[0].value) 285 | 286 | # Go through the biome list and add the field info. 287 | list = aggregate["classes"]["biome.list"] 288 | lcf = classloader[list] 289 | 290 | # Find the static block, and load the fields for each. 291 | method = lcf.methods.find_one(name="") 292 | biome_name = "" 293 | for ins in method.code.disassemble(): 294 | if ins in ("ldc", "ldc_w"): 295 | const = ins.operands[0] 296 | if isinstance(const, String): 297 | biome_name = const.string.value 298 | elif ins == "putstatic": 299 | if biome_name is None or biome_name == "Accessed Biomes before Bootstrap!": 300 | continue 301 | const = ins.operands[0] 302 | field = const.name_and_type.name.value 303 | biomes[biome_name]["field"] = field 304 | biome_fields[field] = biome_name 305 | 306 | @staticmethod 307 | def _process_113(aggregate, classloader, verbose): 308 | # Processes biomes for Minecraft 1.13 and above 309 | biomes_base = aggregate.setdefault("biomes", {}) 310 | biomes = biomes_base.setdefault("biome", {}) 311 | biome_fields = biomes_base.setdefault("biome_fields", {}) 312 | 313 | superclass = aggregate["classes"]["biome.register"] 314 | aggregate["classes"]["biome.superclass"] = superclass 315 | cf = classloader[superclass] 316 | 317 | method = cf.methods.find_one(returns="V", args="", f=lambda m: m.access_flags.acc_public and m.access_flags.acc_static) 318 | 319 | # First pass: identify all the biomes. 320 | stack = [] 321 | for ins in method.code.disassemble(): 322 | if ins in ("bipush", "sipush"): 323 | stack.append(ins.operands[0].value) 324 | elif ins in ("ldc", "ldc_w"): 325 | const = ins.operands[0] 326 | if isinstance(const, String): 327 | stack.append(const.string.value) 328 | elif ins == "new": 329 | const = ins.operands[0] 330 | stack.append(const.name.value) 331 | elif ins == "invokestatic": 332 | # Registration 333 | assert len(stack) == 3 334 | # NOTE: the default values there aren't present 335 | # in the actual code 336 | biomes[stack[1]] = { 337 | "id": stack[0], 338 | "text_id": stack[1], 339 | "rainfall": 0.5, 340 | "height": [0.1, 0.2], 341 | "temperature": 0.5, 342 | "class": stack[2] 343 | } 344 | stack = [] 345 | elif ins == "anewarray": 346 | # End of biome initialization; now creating the list of biomes 347 | # for the explore all biomes achievement but we don't need 348 | # that info. 349 | break 350 | 351 | # Second pass: check the biome constructors and fill in data from there. 352 | if aggregate["version"]["data"] >= 1483: # 18w16a 353 | BiomeTopping._process_113_classes_new(aggregate, classloader, verbose) 354 | else: 355 | BiomeTopping._process_113_classes_old(aggregate, classloader, verbose) 356 | 357 | # 3rd pass: go through the biome list and add the field info. 358 | list = aggregate["classes"]["biome.list"] 359 | lcf = classloader[list] 360 | 361 | method = lcf.methods.find_one(name="") 362 | biome_name = "" 363 | for ins in method.code.disassemble(): 364 | if ins in ("ldc", "ldc_w"): 365 | const = ins.operands[0] 366 | if isinstance(const, String): 367 | biome_name = const.string.value 368 | elif ins == "putstatic": 369 | if biome_name is None or biome_name == "Accessed Biomes before Bootstrap!": 370 | continue 371 | const = ins.operands[0] 372 | field = const.name_and_type.name.value 373 | biomes[biome_name]["field"] = field 374 | biome_fields[field] = biome_name 375 | 376 | 377 | @staticmethod 378 | def _process_113_classes_old(aggregate, classloader, verbose): 379 | # Between 18w06a and 18w15a biomes set fields directly, instead of 380 | # using a builder (as was done before and after). 381 | for biome in six.itervalues(aggregate["biomes"]["biome"]): 382 | cf = classloader[biome["class"]] 383 | method = cf.methods.find_one(name="") 384 | 385 | # Assume a specific order. Also evil and may break if things change. 386 | str_count = 0 387 | float_count = 0 388 | last = None 389 | for ins in method.code.disassemble(): 390 | if ins in ("ldc", "ldc_w"): 391 | const = ins.operands[0] 392 | if isinstance(const, String): 393 | last = const.string.value 394 | else: 395 | last = const.value 396 | elif ins.mnemonic.startswith("fconst_"): 397 | last = float(ins.mnemonic[-1]) 398 | elif ins == "putfield" and last != None: 399 | if isinstance(last, float): 400 | if float_count == 0: 401 | biome["height"][0] = last 402 | elif float_count == 1: 403 | biome["height"][1] = last 404 | elif float_count == 2: 405 | biome["temperature"] = last 406 | elif float_count == 3: 407 | biome["rainfall"] = last 408 | float_count += 1 409 | elif isinstance(last, six.string_types): 410 | if str_count == 0: 411 | biome["name"] = last 412 | elif str_count == 1: 413 | biome["mutated_from"] = last 414 | str_count += 1 415 | last = None 416 | 417 | @staticmethod 418 | def _process_113_classes_new(aggregate, classloader, verbose): 419 | # After 18w16a, biomes used a builder again. The name is now also translatable. 420 | 421 | for biome in six.itervalues(aggregate["biomes"]["biome"]): 422 | biome["name"] = aggregate["language"]["biome"]["minecraft." + biome["text_id"]] 423 | 424 | cf = classloader[biome["class"]] 425 | method = cf.methods.find_one(name="") 426 | stack = [] 427 | for ins in method.code.disassemble(): 428 | if ins == "invokespecial": 429 | const = ins.operands[0] 430 | name = const.name_and_type.name.value 431 | if const.class_.name.value == cf.super_.name.value and name == "": 432 | # Calling biome init; we're done 433 | break 434 | elif ins == "invokevirtual": 435 | const = ins.operands[0] 436 | name = const.name_and_type.name.value 437 | desc = method_descriptor(const.name_and_type.descriptor.value) 438 | 439 | if len(desc.args) == 1: 440 | if desc.args[0].name == "float": 441 | # Ugly portion - different methods with different names 442 | # Hopefully the order doesn't change 443 | if name == "a": 444 | biome["height"][0] = stack.pop() 445 | elif name == "b": 446 | biome["height"][1] = stack.pop() 447 | elif name == "c": 448 | biome["temperature"] = stack.pop() 449 | elif name == "d": 450 | biome["rainfall"] = stack.pop() 451 | elif desc.args[0].name == "java/lang/String": 452 | val = stack.pop() 453 | if val is not None: 454 | biome["mutated_from"] = val 455 | 456 | stack = [] 457 | # Constants 458 | elif ins in ("ldc", "ldc_w"): 459 | const = ins.operands[0] 460 | if isinstance(const, String): 461 | stack.append(const.string.value) 462 | if isinstance(const, (Integer, Float)): 463 | stack.append(const.value) 464 | 465 | elif ins.mnemonic.startswith("fconst"): 466 | stack.append(float(ins.mnemonic[-1])) 467 | elif ins in ("bipush", "sipush"): 468 | stack.append(ins.operands[0].value) 469 | elif ins == "aconst_null": 470 | stack.append(None) 471 | 472 | @staticmethod 473 | def _process_114(aggregate, classloader, verbose): 474 | # Processes biomes for Minecraft 1.14 475 | listclass = aggregate["classes"]["biome.list"] 476 | lcf = classloader[listclass] 477 | superclass = next(lcf.fields.find()).type.name # The first field in the list is a biome 478 | aggregate["classes"]["biome.superclass"] = superclass 479 | 480 | biomes_base = aggregate.setdefault("biomes", {}) 481 | biomes = biomes_base.setdefault("biome", {}) 482 | biome_fields = biomes_base.setdefault("biome_fields", {}) 483 | 484 | method = lcf.methods.find_one(name="") 485 | 486 | # First pass: identify all the biomes. 487 | stack = [] 488 | for ins in method.code.disassemble(): 489 | if ins.mnemonic in ("bipush", "sipush"): 490 | stack.append(ins.operands[0].value) 491 | elif ins.mnemonic in ("ldc", "ldc_w"): 492 | const = ins.operands[0] 493 | if isinstance(const, String): 494 | stack.append(const.string.value) 495 | elif ins.mnemonic == "new": 496 | const = ins.operands[0] 497 | stack.append(const.name.value) 498 | elif ins.mnemonic == "invokestatic": 499 | # Registration 500 | assert len(stack) == 3 501 | # NOTE: the default values there aren't present 502 | # in the actual code 503 | tmp_biome = { 504 | "id": stack[0], 505 | "text_id": stack[1], 506 | "rainfall": 0.5, 507 | "height": [0.1, 0.2], 508 | "temperature": 0.5, 509 | "class": stack[2] 510 | } 511 | biomes[stack[1]] = tmp_biome 512 | stack = [tmp_biome] # Registration returns the biome 513 | elif ins.mnemonic == "anewarray": 514 | # End of biome initialization; now creating the list of biomes 515 | # for the explore all biomes achievement but we don't need 516 | # that info. 517 | break 518 | elif ins.mnemonic == "getstatic": 519 | const = ins.operands[0] 520 | if const.class_.name.value == listclass: 521 | stack.append(biomes[biome_fields[const.name_and_type.name.value]]) 522 | else: 523 | stack.append(object()) 524 | elif ins.mnemonic == "putstatic": 525 | const = ins.operands[0] 526 | field = const.name_and_type.name.value 527 | stack[0]["field"] = field 528 | biome_fields[field] = stack[0]["text_id"] 529 | stack.pop() 530 | 531 | # Second pass: check the biome constructors and fill in data from there. 532 | BiomeTopping._process_113_classes_new(aggregate, classloader, verbose) 533 | 534 | -------------------------------------------------------------------------------- /burger/toppings/entities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | """ 4 | Copyright (c) 2011 Tyler Kenendy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import six 26 | 27 | from .topping import Topping 28 | from burger.util import WalkerCallback, class_from_invokedynamic, walk_method 29 | 30 | from jawa.constants import * 31 | from jawa.util.descriptor import method_descriptor 32 | 33 | class EntityTopping(Topping): 34 | """Gets most entity types.""" 35 | 36 | PROVIDES = [ 37 | "entities.entity" 38 | ] 39 | 40 | DEPENDS = [ 41 | "identify.entity.list", 42 | "version.entity_format", 43 | "language" 44 | ] 45 | 46 | @staticmethod 47 | def act(aggregate, classloader, verbose=False): 48 | # Decide which type of entity logic should be used. 49 | 50 | handlers = { 51 | "1.10": EntityTopping._entities_1point10, 52 | "1.11": EntityTopping._entities_1point11, 53 | "1.13": EntityTopping._entities_1point13 54 | } 55 | entity_format = aggregate["version"]["entity_format"] 56 | if entity_format in handlers: 57 | handlers[entity_format](aggregate, classloader, verbose) 58 | else: 59 | if verbose: 60 | print("Unknown entity format %s" % entity_format) 61 | return 62 | 63 | entities = aggregate["entities"] 64 | 65 | entities["info"] = { 66 | "entity_count": len(entities["entity"]) 67 | } 68 | 69 | EntityTopping.abstract_entities(classloader, entities["entity"], verbose) 70 | EntityTopping.compute_sizes(classloader, aggregate, entities["entity"]) 71 | 72 | @staticmethod 73 | def _entities_1point13(aggregate, classloader, verbose): 74 | if verbose: 75 | print("Using 1.13 entity format") 76 | 77 | listclass = aggregate["classes"]["entity.list"] 78 | cf = classloader[listclass] 79 | 80 | entities = aggregate.setdefault("entities", {}) 81 | entity = entities.setdefault("entity", {}) 82 | 83 | # Find the inner builder class 84 | inner_classes = cf.attributes.find_one(name="InnerClasses").inner_classes 85 | builderclass = None 86 | funcclass = None # 19w08a+ - a functional interface for creating new entities 87 | for entry in inner_classes: 88 | if entry.outer_class_info_index == 0: 89 | # Ignore anonymous classes 90 | continue 91 | 92 | outer = cf.constants.get(entry.outer_class_info_index) 93 | if outer.name == listclass: 94 | inner = cf.constants.get(entry.inner_class_info_index) 95 | inner_cf = classloader[inner.name.value] 96 | if inner_cf.access_flags.acc_interface: 97 | if funcclass: 98 | raise Exception("Unexpected multiple inner interfaces") 99 | funcclass = inner.name.value 100 | else: 101 | if builderclass: 102 | raise Exception("Unexpected multiple inner classes") 103 | builderclass = inner.name.value 104 | 105 | if not builderclass: 106 | raise Exception("Failed to find inner class for builder in " + str(inner_classes)) 107 | # Note that funcclass might not be found since it didn't always exist 108 | 109 | method = cf.methods.find_one(name="") 110 | 111 | # Example of what's being parsed: 112 | # public static final EntityType AREA_EFFECT_CLOUD = register("area_effect_cloud", EntityType.Builder.create(EntityAreaEffectCloud::new, EntityCategory.MISC).setSize(6.0F, 0.5F)); // 19w05a+ 113 | # public static final EntityType AREA_EFFECT_CLOUD = register("area_effect_cloud", EntityType.Builder.create(EntityAreaEffectCloud.class, EntityAreaEffectCloud::new).setSize(6.0F, 0.5F)); // 19w03a+ 114 | # and in older versions: 115 | # public static final EntityType AREA_EFFECT_CLOUD = register("area_effect_cloud", EntityType.Builder.create(EntityAreaEffectCloud.class, EntityAreaEffectCloud::new)); // 18w06a-19w02a 116 | # and in even older versions: 117 | # public static final EntityType AREA_EFFECT_CLOUD = register("area_effect_cloud", EntityType.Builder.create(EntityAreaEffectCloud::new)); // through 18w05a 118 | 119 | class EntityContext(WalkerCallback): 120 | def __init__(self): 121 | self.cur_id = 0 122 | 123 | def on_invokedynamic(self, ins, const, args): 124 | # MC uses EntityZombie::new, similar; return the created class 125 | return class_from_invokedynamic(ins, cf) 126 | 127 | def on_invoke(self, ins, const, obj, args): 128 | if const.class_.name == listclass: 129 | assert len(args) == 2 130 | # Call to register 131 | name = args[0] 132 | new_entity = args[1] 133 | new_entity["name"] = name 134 | new_entity["id"] = self.cur_id 135 | if "minecraft." + name in aggregate["language"]["entity"]: 136 | new_entity["display_name"] = aggregate["language"]["entity"]["minecraft." + name] 137 | self.cur_id += 1 138 | 139 | entity[name] = new_entity 140 | return new_entity 141 | elif const.class_.name == builderclass: 142 | if ins.mnemonic != "invokestatic": 143 | if len(args) == 2 and const.name_and_type.descriptor.value.startswith("(FF)"): 144 | # Entity size in 19w03a and newer 145 | obj["width"] = args[0] 146 | obj["height"] = args[1] 147 | 148 | # There are other properties on the builder (related to whether the entity can be created) 149 | # We don't care about these 150 | return obj 151 | 152 | method_desc = const.name_and_type.descriptor.value 153 | desc = method_descriptor(method_desc) 154 | 155 | if len(args) == 2: 156 | if desc.args[0].name == "java/lang/Class" and desc.args[1].name == "java/util/function/Function": 157 | # Builder.create(Class, Function), 18w06a+ 158 | # In 18w06a, they added a parameter for the entity class; check consistency 159 | assert args[0] == args[1] + ".class" 160 | cls = args[1] 161 | elif desc.args[0].name == "java/util/function/Function" or desc.args[0].name == funcclass: 162 | # Builder.create(Function, EntityCategory), 19w05a+ 163 | cls = args[0] 164 | else: 165 | if verbose: 166 | print("Unknown entity type builder creation method", method_desc) 167 | cls = None 168 | elif len(args) == 1: 169 | # There is also a format that creates an entity that cannot be serialized. 170 | # This might be just with a single argument (its class), in 18w06a+. 171 | # Otherwise, in 18w05a and below, it's just the function to build. 172 | if desc.args[0].name == "java/lang/Function": 173 | # Builder.create(Function), 18w05a- 174 | # Just the function, which was converted into a class name earlier 175 | cls = args[0] 176 | elif desc.args[0].name == "java/lang/Class": 177 | # Builder.create(Class), 18w06a+ 178 | # The type that represents something that cannot be serialized 179 | cls = None 180 | else: 181 | # Assume Builder.create(EntityCategory) in 19w05a+, 182 | # though it could be hit for other unknown signatures 183 | cls = None 184 | else: 185 | # Assume Builder.create(), though this could be hit for other unknown signatures 186 | # In 18w05a and below, nonserializable entities 187 | cls = None 188 | 189 | return { "class": cls } if cls else { "serializable": "false" } 190 | 191 | def on_put_field(self, ins, const, obj, value): 192 | if isinstance(value, dict): 193 | # Keep track of the field in the entity list too. 194 | value["field"] = const.name_and_type.name.value 195 | # Also, if this isn't a serializable entity, get the class from the generic signature of the field 196 | if "class" not in value: 197 | field = cf.fields.find_one(name=const.name_and_type.name.value) 198 | sig = field.attributes.find_one(name="Signature").signature.value # Something like `Laev;` 199 | value["class"] = sig[sig.index("<") + 2 : sig.index(">") - 1] # Awful way of getting the actual type 200 | 201 | def on_new(self, ins, const): 202 | # Done once, for the registry, but we don't care 203 | return object() 204 | 205 | def on_get_field(self, ins, const, obj): 206 | # 19w05a+: used to set entity types. 207 | return object() 208 | 209 | walk_method(cf, method, EntityContext(), verbose) 210 | 211 | @staticmethod 212 | def _entities_1point11(aggregate, classloader, verbose): 213 | # 1.11 logic 214 | if verbose: 215 | print("Using 1.11 entity format") 216 | 217 | listclass = aggregate["classes"]["entity.list"] 218 | cf = classloader[listclass] 219 | 220 | entities = aggregate.setdefault("entities", {}) 221 | entity = entities.setdefault("entity", {}) 222 | 223 | method = cf.methods.find_one(args='', returns="V", f=lambda m: m.access_flags.acc_public and m.access_flags.acc_static) 224 | 225 | minecart_info = {} 226 | class EntityContext(WalkerCallback): 227 | def on_get_field(self, ins, const, obj): 228 | # Minecarts use an enum for their data - assume that this is that enum 229 | const = ins.operands[0] 230 | if not "types_by_field" in minecart_info: 231 | EntityTopping._load_minecart_enum(classloader, const.class_.name.value, minecart_info) 232 | minecart_name = minecart_info["types_by_field"][const.name_and_type.name.value] 233 | return minecart_info["types"][minecart_name] 234 | 235 | def on_invoke(self, ins, const, obj, args): 236 | if const.class_.name == listclass: 237 | if len(args) == 4: 238 | # Initial registration 239 | name = args[1] 240 | old_name = args[3] 241 | entity[name] = { 242 | "id": args[0], 243 | "name": name, 244 | "class": args[2][:-len(".class")], 245 | "old_name": old_name 246 | } 247 | 248 | if old_name + ".name" in aggregate["language"]["entity"]: 249 | entity[name]["display_name"] = aggregate["language"]["entity"][old_name + ".name"] 250 | elif len(args) == 3: 251 | # Spawn egg registration 252 | name = args[0] 253 | if name in entity: 254 | entity[name]["egg_primary"] = args[1] 255 | entity[name]["egg_secondary"] = args[2] 256 | elif verbose: 257 | print("Missing entity during egg registration: %s" % name) 258 | elif const.class_.name == minecart_info["class"]: 259 | # Assume that obj is the minecart info, and the method being called is the one that gets the name 260 | return obj["entitytype"] 261 | 262 | def on_new(self, ins, const): 263 | raise Exception("unexpected new: %s" % ins) 264 | 265 | def on_put_field(self, ins, const, obj, value): 266 | raise Exception("unexpected putfield: %s" % ins) 267 | 268 | walk_method(cf, method, EntityContext(), verbose) 269 | 270 | @staticmethod 271 | def _entities_1point10(aggregate, classloader, verbose): 272 | # 1.10 logic 273 | if verbose: 274 | print("Using 1.10 entity format") 275 | 276 | superclass = aggregate["classes"]["entity.list"] 277 | cf = classloader[superclass] 278 | 279 | method = cf.methods.find_one(name="") 280 | mode = "starting" 281 | 282 | superclass = aggregate["classes"]["entity.list"] 283 | cf = classloader[superclass] 284 | 285 | entities = aggregate.setdefault("entities", {}) 286 | entity = entities.setdefault("entity", {}) 287 | alias = None 288 | 289 | stack = [] 290 | tmp = {} 291 | minecart_info = {} 292 | 293 | for ins in method.code.disassemble(): 294 | if mode == "starting": 295 | # We don't care about the logger setup stuff at the beginning; 296 | # wait until an entity definition starts. 297 | if ins in ("ldc", "ldc_w"): 298 | mode = "entities" 299 | # elif is not used here because we need to handle modes changing 300 | if mode != "starting": 301 | if ins in ("ldc", "ldc_w"): 302 | const = ins.operands[0] 303 | if isinstance(const, ConstantClass): 304 | stack.append(const.name.value) 305 | elif isinstance(const, String): 306 | stack.append(const.string.value) 307 | else: 308 | stack.append(const.value) 309 | elif ins in ("bipush", "sipush"): 310 | stack.append(ins.operands[0].value) 311 | elif ins == "new": 312 | # Entity aliases (for lack of a better term) start with 'new's. 313 | # Switch modes (this operation will be processed there) 314 | mode = "aliases" 315 | const = ins.operands[0] 316 | stack.append(const.name.value) 317 | elif ins == "getstatic": 318 | # Minecarts use an enum for their data - assume that this is that enum 319 | const = ins.operands[0] 320 | if not "types_by_field" in minecart_info: 321 | EntityTopping._load_minecart_enum(classloader, const.class_.name.value, minecart_info) 322 | # This technically happens when invokevirtual is called, but do it like this for simplicity 323 | minecart_name = minecart_info["types_by_field"][const.name_and_type.name.value] 324 | stack.append(minecart_info["types"][minecart_name]["entitytype"]) 325 | elif ins == "invokestatic": # invokestatic 326 | if mode == "entities": 327 | tmp["class"] = stack[0] 328 | tmp["name"] = stack[1] 329 | tmp["id"] = stack[2] 330 | if (len(stack) >= 5): 331 | tmp["egg_primary"] = stack[3] 332 | tmp["egg_secondary"] = stack[4] 333 | if tmp["name"] + ".name" in aggregate["language"]["entity"]: 334 | tmp["display_name"] = aggregate["language"]["entity"][tmp["name"] + ".name"] 335 | entity[tmp["name"]] = tmp 336 | elif mode == "aliases": 337 | tmp["entity"] = stack[0] 338 | tmp["name"] = stack[1] 339 | if (len(stack) >= 5): 340 | tmp["egg_primary"] = stack[2] 341 | tmp["egg_secondary"] = stack[3] 342 | tmp["class"] = stack[-1] # last item, made by new. 343 | if alias is None: 344 | alias = entities.setdefault("alias", {}) 345 | alias[tmp["name"]] = tmp 346 | 347 | tmp = {} 348 | stack = [] 349 | 350 | @staticmethod 351 | def _load_minecart_enum(classloader, classname, minecart_info): 352 | """Stores data about the minecart enum in aggregate""" 353 | minecart_info["class"] = classname 354 | 355 | minecart_types = minecart_info.setdefault("types", {}) 356 | minecart_types_by_field = minecart_info.setdefault("types_by_field", {}) 357 | 358 | minecart_cf = classloader[classname] 359 | init_method = minecart_cf.methods.find_one(name="") 360 | 361 | already_has_minecart_name = False 362 | for ins in init_method.code.disassemble(): 363 | if ins == "new": 364 | const = ins.operands[0] 365 | minecart_class = const.name.value 366 | elif ins == "ldc": 367 | const = ins.operands[0] 368 | if isinstance(const, String): 369 | if already_has_minecart_name: 370 | minecart_type = const.string.value 371 | else: 372 | already_has_minecart_name = True 373 | minecart_name = const.string.value 374 | elif ins == "putstatic": 375 | const = ins.operands[0] 376 | if const.name_and_type.descriptor.value != "L" + classname + ";": 377 | # Other parts of the enum initializer (values array) that we don't care about 378 | continue 379 | 380 | minecart_field = const.name_and_type.name.value 381 | 382 | minecart_types[minecart_name] = { 383 | "class": minecart_class, 384 | "field": minecart_field, 385 | "name": minecart_name, 386 | "entitytype": minecart_type 387 | } 388 | minecart_types_by_field[minecart_field] = minecart_name 389 | 390 | already_has_minecart_name = False 391 | 392 | @staticmethod 393 | def compute_sizes(classloader, aggregate, entities): 394 | # Class -> size 395 | size_cache = {} 396 | 397 | # NOTE: Use aggregate["entities"] instead of the given entities list because 398 | # this method is re-used in the objects topping 399 | base_entity_cf = classloader[aggregate["entities"]["entity"]["~abstract_entity"]["class"]] 400 | 401 | # Note that there are additional methods matching this, used to set camera angle and such 402 | set_size = base_entity_cf.methods.find_one(args="FF", returns="V", f=lambda m: m.access_flags.acc_protected) 403 | 404 | set_size_name = set_size.name.value 405 | set_size_desc = set_size.descriptor.value 406 | 407 | def compute_size(class_name): 408 | if class_name == "java/lang/Object": 409 | return None 410 | 411 | if class_name in size_cache: 412 | return size_cache[class_name] 413 | 414 | cf = classloader[class_name] 415 | constructor = cf.methods.find_one(name="") 416 | 417 | tmp = [] 418 | for ins in constructor.code.disassemble(): 419 | if ins in ("ldc", "ldc_w"): 420 | const = ins.operands[0] 421 | if isinstance(const, Float): 422 | tmp.append(const.value) 423 | elif ins == "invokevirtual": 424 | const = ins.operands[0] 425 | if const.name_and_type.name == set_size_name and const.name_and_type.descriptor == set_size_desc: 426 | if len(tmp) == 2: 427 | result = tmp 428 | else: 429 | # There was a call to the method, but we couldn't parse it fully 430 | result = None 431 | break 432 | tmp = [] 433 | else: 434 | # We want only the simplest parse, so even things like multiplication should cause this to be reset 435 | tmp = [] 436 | else: 437 | # No result, so use the superclass 438 | result = compute_size(cf.super_.name.value) 439 | 440 | size_cache[class_name] = result 441 | return result 442 | 443 | for entity in six.itervalues(entities): 444 | if "width" not in entity: 445 | size = compute_size(entity["class"]) 446 | if size is not None: 447 | entity["width"] = size[0] 448 | entity["height"] = size[1] 449 | 450 | @staticmethod 451 | def abstract_entities(classloader, entities, verbose): 452 | entity_classes = {e["class"]: e["name"] for e in six.itervalues(entities)} 453 | 454 | # Add some abstract classes, to help with metadata, and for reference only; 455 | # these are not spawnable 456 | def abstract_entity(abstract_name, *subclass_names): 457 | for name in subclass_names: 458 | if name in entities: 459 | cf = classloader[entities[name]["class"]] 460 | parent = cf.super_.name.value 461 | if parent not in entity_classes: 462 | entities["~abstract_" + abstract_name] = { "class": parent, "name": "~abstract_" + abstract_name } 463 | elif verbose: 464 | print("Unexpected non-abstract class for parent of %s: %s" % (name, entity_classes[parent])) 465 | break 466 | else: 467 | if verbose: 468 | print("Failed to find abstract entity %s as a superclass of %s" % (abstract_name, subclass_names)) 469 | 470 | abstract_entity("entity", "item", "Item") 471 | abstract_entity("minecart", "minecart", "MinecartRideable") 472 | abstract_entity("living", "armor_stand", "ArmorStand") # EntityLivingBase 473 | abstract_entity("insentient", "ender_dragon", "EnderDragon") # EntityLiving 474 | abstract_entity("monster", "enderman", "Enderman") # EntityMob 475 | abstract_entity("tameable", "wolf", "Wolf") # EntityTameable 476 | abstract_entity("animal", "sheep", "Sheep") # EntityAnimal 477 | abstract_entity("ageable", "~abstract_animal") # EntityAgeable 478 | abstract_entity("creature", "~abstract_ageable") # EntityCreature 479 | -------------------------------------------------------------------------------- /burger/toppings/entitymetadata.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from .topping import Topping 4 | from burger.util import WalkerCallback, walk_method, string_from_invokedymanic 5 | 6 | from jawa.constants import * 7 | from jawa.util.descriptor import method_descriptor 8 | 9 | class EntityMetadataTopping(Topping): 10 | PROVIDES = [ 11 | "entities.metadata" 12 | ] 13 | 14 | DEPENDS = [ 15 | "entities.entity", 16 | "identify.metadata", 17 | # For serializers 18 | "packets.instructions", 19 | "identify.packet.packetbuffer", 20 | "identify.blockstate", 21 | "identify.chatcomponent", 22 | "identify.itemstack", 23 | "identify.nbtcompound", 24 | "identify.particle" 25 | ] 26 | 27 | @staticmethod 28 | def act(aggregate, classloader, verbose=False): 29 | # This approach works in 1.9 and later; before then metadata was different. 30 | entities = aggregate["entities"]["entity"] 31 | 32 | datamanager_class = aggregate["classes"]["metadata"] 33 | datamanager_cf = classloader[datamanager_class] 34 | 35 | create_key_method = datamanager_cf.methods.find_one(f=lambda m: len(m.args) == 2 and m.args[0].name == "java/lang/Class") 36 | dataparameter_class = create_key_method.returns.name 37 | dataserializer_class = create_key_method.args[1].name 38 | 39 | register_method = datamanager_cf.methods.find_one(f=lambda m: len(m.args) == 2 and m.args[0].name == dataparameter_class) 40 | 41 | dataserializers_class = None 42 | for ins in register_method.code.disassemble(): 43 | # The code loops up an ID and throws an exception if it's not registered 44 | # We want the class that it looks the ID up in 45 | if ins == "invokestatic": 46 | const = ins.operands[0] 47 | dataserializers_class = const.class_.name.value 48 | elif dataserializers_class and ins in ("ldc", "ldc_w"): 49 | const = ins.operands[0] 50 | if const == "Unregistered serializer ": 51 | break 52 | elif dataserializers_class and ins == "invokedynamic": 53 | text = string_from_invokedymanic(ins, datamanager_cf) 54 | if "Unregistered serializer " in text: 55 | break 56 | else: 57 | raise Exception("Failed to identify dataserializers") 58 | 59 | base_entity_class = entities["~abstract_entity"]["class"] 60 | base_entity_cf = classloader[base_entity_class] 61 | register_data_method_name = None 62 | register_data_method_desc = "()V" 63 | # The last call in the base entity constructor is to registerData() (formerly entityInit()) 64 | for ins in base_entity_cf.methods.find_one(name="").code.disassemble(): 65 | if ins.mnemonic == "invokevirtual": 66 | const = ins.operands[0] 67 | if const.name_and_type.descriptor == register_data_method_desc: 68 | register_data_method_name = const.name_and_type.name.value 69 | # Keep looping, to find the last call 70 | 71 | dataserializers = EntityMetadataTopping.identify_serializers(classloader, dataserializer_class, dataserializers_class, aggregate["classes"], verbose) 72 | aggregate["entities"]["dataserializers"] = dataserializers 73 | dataserializers_by_field = {serializer["field"]: serializer for serializer in six.itervalues(dataserializers)} 74 | 75 | entity_classes = {e["class"]: e["name"] for e in six.itervalues(entities)} 76 | parent_by_class = {} 77 | metadata_by_class = {} 78 | bitfields_by_class = {} 79 | 80 | # this flag is shared among all entities 81 | # getSharedFlag is currently the only method in Entity with those specific args and returns, this may change in the future! (hopefully not) 82 | shared_get_flag_method = base_entity_cf.methods.find_one(args="I", returns="Z").name.value 83 | 84 | def fill_class(cls): 85 | # Returns the starting index for metadata in subclasses of cls 86 | if cls == "java/lang/Object": 87 | return 0 88 | if cls in metadata_by_class: 89 | return len(metadata_by_class[cls]) + fill_class(parent_by_class[cls]) 90 | 91 | cf = classloader[cls] 92 | super = cf.super_.name.value 93 | parent_by_class[cls] = super 94 | index = fill_class(super) 95 | 96 | metadata = [] 97 | class MetadataFieldContext(WalkerCallback): 98 | def __init__(self): 99 | self.cur_index = index 100 | 101 | def on_invoke(self, ins, const, obj, args): 102 | if const.class_.name == datamanager_class and const.name_and_type.name == create_key_method.name and const.name_and_type.descriptor == create_key_method.descriptor: 103 | # Call to createKey. 104 | # Sanity check: entities should only register metadata for themselves 105 | if args[0] != cls + ".class": 106 | # ... but in some versions, mojang messed this up with potions... hence why the sanity check exists in vanilla now. 107 | if verbose: 108 | other_class = args[0][:-len(".class")] 109 | name = entity_classes.get(cls, "Unknown") 110 | other_name = entity_classes.get(other_class, "Unknown") 111 | print("An entity tried to register metadata for another entity: %s (%s) from %s (%s)" % (other_name, other_class, name, cls)) 112 | 113 | serializer = args[1] 114 | index = self.cur_index 115 | self.cur_index += 1 116 | 117 | metadata_entry = { 118 | "serializer_id": serializer["id"], 119 | "serializer": serializer["name"] if "name" in serializer else serializer["id"], 120 | "index": index 121 | } 122 | metadata.append(metadata_entry) 123 | return metadata_entry 124 | 125 | def on_put_field(self, ins, const, obj, value): 126 | if isinstance(value, dict): 127 | value["field"] = const.name_and_type.name.value 128 | 129 | def on_get_field(self, ins, const, obj): 130 | if const.class_.name == dataserializers_class: 131 | return dataserializers_by_field[const.name_and_type.name.value] 132 | 133 | def on_invokedynamic(self, ins, const, args): 134 | return object() 135 | 136 | def on_new(self, ins, const): 137 | return object() 138 | 139 | init = cf.methods.find_one(name="") 140 | if init: 141 | ctx = MetadataFieldContext() 142 | walk_method(cf, init, ctx, verbose) 143 | index = ctx.cur_index 144 | 145 | class MetadataDefaultsContext(WalkerCallback): 146 | def __init__(self, wait_for_putfield=False): 147 | self.textcomponentstring = None 148 | # True whlie waiting for "this.dataManager = new EntityDataManager(this);" when going through the entity constructor 149 | self.waiting_for_putfield = wait_for_putfield 150 | 151 | def on_invoke(self, ins, const, obj, args): 152 | if self.waiting_for_putfield: 153 | return 154 | 155 | if "Optional" in const.class_.name.value: 156 | if const.name_and_type.name in ("absent", "empty"): 157 | return "Empty" 158 | elif len(args) == 1: 159 | # Assume "of" or similar 160 | return args[0] 161 | elif const.name_and_type.name == "valueOf": 162 | # Boxing methods 163 | if const.class_.name == "java/lang/Boolean": 164 | return bool(args[0]) 165 | else: 166 | return args[0] 167 | elif const.name_and_type.name == "": 168 | if const.class_.name == self.textcomponentstring: 169 | obj["text"] = args[0] 170 | 171 | return 172 | elif const.class_.name == datamanager_class: 173 | assert const.name_and_type.name == register_method.name 174 | assert const.name_and_type.descriptor == register_method.descriptor 175 | 176 | # args[0] is the metadata entry, and args[1] is the default value 177 | if args[0] is not None and args[1] is not None: 178 | args[0]["default"] = args[1] 179 | 180 | return 181 | elif const.name_and_type.descriptor.value.endswith("L" + datamanager_class + ";"): 182 | # getDataManager, which doesn't really have a reason to exist given that the data manager field is accessible 183 | return None 184 | elif const.name_and_type.name == register_data_method_name and const.name_and_type.descriptor == register_data_method_desc: 185 | # Call to super.registerData() 186 | return 187 | 188 | def on_put_field(self, ins, const, obj, value): 189 | if const.name_and_type.descriptor == "L" + datamanager_class + ";": 190 | if not self.waiting_for_putfield: 191 | raise Exception("Unexpected putfield: %s" % (ins,)) 192 | self.waiting_for_putfield = False 193 | 194 | def on_get_field(self, ins, const, obj): 195 | if self.waiting_for_putfield: 196 | return 197 | 198 | if const.name_and_type.descriptor == "L" + dataparameter_class + ";": 199 | # Definitely shouldn't be registering something declared elsewhere 200 | assert const.class_.name == cls 201 | for metadata_entry in metadata: 202 | if const.name_and_type.name == metadata_entry.get("field"): 203 | return metadata_entry 204 | else: 205 | if verbose: 206 | print("Can't figure out metadata entry for field %s; default will not be set." % (const,)) 207 | return None 208 | 209 | if const.class_.name == aggregate["classes"]["position"]: 210 | # Assume BlockPos.ORIGIN 211 | return "(0, 0, 0)" 212 | elif const.class_.name == aggregate["classes"]["itemstack"]: 213 | # Assume ItemStack.EMPTY 214 | return "Empty" 215 | elif const.name_and_type.descriptor == "L" + datamanager_class + ";": 216 | return 217 | else: 218 | return None 219 | 220 | def on_new(self, ins, const): 221 | if self.waiting_for_putfield: 222 | return 223 | 224 | if self.textcomponentstring == None: 225 | # Check if this is TextComponentString 226 | temp_cf = classloader[const.name.value] 227 | for str in temp_cf.constants.find(type_=String): 228 | if "TextComponent{text=" in str.string.value: 229 | self.textcomponentstring = const.name.value 230 | break 231 | 232 | if const.name == aggregate["classes"]["nbtcompound"]: 233 | return "Empty" 234 | elif const.name == self.textcomponentstring: 235 | return {'text': None} 236 | 237 | register = cf.methods.find_one(name=register_data_method_name, f=lambda m: m.descriptor == register_data_method_desc) 238 | if register and not register.access_flags.acc_abstract: 239 | walk_method(cf, register, MetadataDefaultsContext(), verbose) 240 | elif cls == base_entity_class: 241 | walk_method(cf, cf.methods.find_one(name=""), MetadataDefaultsContext(True), verbose) 242 | 243 | get_flag_method = None 244 | 245 | # find if the class has a `boolean getFlag(int)` method 246 | for method in cf.methods.find(args="I", returns="Z"): 247 | previous_operators = [] 248 | for ins in method.code.disassemble(): 249 | if ins.mnemonic == "bipush": 250 | # check for a series of operators that looks something like this 251 | # `return ((Byte)this.R.a(bo) & var1) != 0;` 252 | operator_matcher = ["aload", "getfield", "getstatic", "invokevirtual", "checkcast", "invokevirtual", "iload", "iand", "ifeq", "bipush", "goto"] 253 | previous_operators_match = previous_operators == operator_matcher 254 | 255 | if previous_operators_match and ins.operands[0].value == 0: 256 | # store the method name as the result for later 257 | get_flag_method = method.name.value 258 | 259 | previous_operators.append(ins.mnemonic) 260 | 261 | bitfields = [] 262 | 263 | # find the methods that get bit fields 264 | for method in cf.methods.find(args="", returns="Z"): 265 | if method.code: 266 | bitmask_value = None 267 | stack = [] 268 | for ins in method.code.disassemble(): 269 | # the method calls getField() or getSharedField() 270 | if ins.mnemonic in ("invokevirtual", "invokespecial", "invokeinterface", "invokestatic"): 271 | calling_method = ins.operands[0].name_and_type.name.value 272 | 273 | has_correct_arguments = ins.operands[0].name_and_type.descriptor.value == "(I)Z" 274 | 275 | is_getflag_method = has_correct_arguments and calling_method == get_flag_method 276 | is_shared_getflag_method = has_correct_arguments and calling_method == shared_get_flag_method 277 | 278 | # if it's a shared flag, update the bitfields_by_class for abstract_entity 279 | if is_shared_getflag_method and stack: 280 | bitmask_value = stack.pop() 281 | if bitmask_value is not None: 282 | base_entity_cls = base_entity_cf.this.name.value 283 | if base_entity_cls not in bitfields_by_class: 284 | bitfields_by_class[base_entity_cls] = [] 285 | bitfields_by_class[base_entity_cls].append({ 286 | # we include the class here so it can be easily figured out from the mappings 287 | "class": cls, 288 | "method": method.name.value, 289 | "mask": 1 << bitmask_value 290 | }) 291 | bitmask_value = None 292 | elif is_getflag_method and stack: 293 | bitmask_value = stack.pop() 294 | break 295 | elif ins.mnemonic == "iand": 296 | # get the last item in the stack, since it's the bitmask 297 | bitmask_value = stack[-1] 298 | break 299 | elif ins.mnemonic == "bipush": 300 | stack.append(ins.operands[0].value) 301 | if bitmask_value: 302 | bitfields.append({ 303 | "method": method.name.value, 304 | "mask": bitmask_value 305 | }) 306 | 307 | 308 | metadata_by_class[cls] = metadata 309 | if cls not in bitfields_by_class: 310 | bitfields_by_class[cls] = bitfields 311 | else: 312 | bitfields_by_class[cls].extend(bitfields) 313 | return index 314 | 315 | for cls in six.iterkeys(entity_classes): 316 | fill_class(cls) 317 | 318 | for e in six.itervalues(entities): 319 | cls = e["class"] 320 | metadata = e["metadata"] = [] 321 | 322 | if metadata_by_class[cls]: 323 | metadata.append({ 324 | "class": cls, 325 | "data": metadata_by_class[cls], 326 | "bitfields": bitfields_by_class[cls] 327 | }) 328 | 329 | cls = parent_by_class[cls] 330 | while cls not in entity_classes and cls != "java/lang/Object" : 331 | # Add metadata from _abstract_ parent classes, at the start 332 | if metadata_by_class[cls]: 333 | metadata.insert(0, { 334 | "class": cls, 335 | "data": metadata_by_class[cls], 336 | "bitfields": bitfields_by_class[cls] 337 | }) 338 | cls = parent_by_class[cls] 339 | 340 | # And then, add a marker for the concrete parent class. 341 | if cls in entity_classes: 342 | # Always do this, even if the immediate concrete parent has no metadata 343 | metadata.insert(0, { 344 | "class": cls, 345 | "entity": entity_classes[cls] 346 | }) 347 | 348 | @staticmethod 349 | def identify_serializers(classloader, dataserializer_class, dataserializers_class, classes, verbose): 350 | serializers_by_field = {} 351 | serializers = {} 352 | id = 0 353 | dataserializers_cf = classloader[dataserializers_class] 354 | for ins in dataserializers_cf.methods.find_one(name="").code.disassemble(): 355 | #print(ins, serializers_by_field, serializers) 356 | # Setting up the serializers 357 | if ins.mnemonic == "new": 358 | const = ins.operands[0] 359 | last_cls = const.name.value 360 | elif ins.mnemonic == "putstatic": 361 | const = ins.operands[0] 362 | if const.name_and_type.descriptor.value != "L" + dataserializer_class + ";": 363 | # E.g. setting the registry. 364 | continue 365 | 366 | field = const.name_and_type.name.value 367 | serializer = EntityMetadataTopping.identify_serializer(classloader, last_cls, classes, verbose) 368 | 369 | serializer["class"] = last_cls 370 | serializer["field"] = field 371 | 372 | serializers_by_field[field] = serializer 373 | # Actually registering them 374 | elif ins.mnemonic == "getstatic": 375 | const = ins.operands[0] 376 | field = const.name_and_type.name.value 377 | 378 | serializer = serializers_by_field[field] 379 | serializer["id"] = id 380 | name = serializer.get("name") or str(id) 381 | if name not in serializers: 382 | serializers[name] = serializer 383 | else: 384 | if verbose: 385 | print("Duplicate serializer with identified name %s: original %s, new %s" % (name, serializers[name], serializer)) 386 | serializers[str(id)] = serializer # This hopefully will not clash but still shouldn't happen in the first place 387 | 388 | id += 1 389 | 390 | return serializers 391 | 392 | @staticmethod 393 | def identify_serializer(classloader, cls, classes, verbose): 394 | # In here because otherwise the import messes with finding the topping in this file 395 | from .packetinstructions import PacketInstructionsTopping as _PIT 396 | from .packetinstructions import PACKETBUF_NAME 397 | 398 | cf = classloader[cls] 399 | sig = cf.attributes.find_one(name="Signature").signature.value 400 | # Input: 401 | # Ljava/lang/Object;Los;>; 402 | # First, get the generic part only: 403 | # Ljava/util/Optional; 404 | # Then, get rid of the 'L' and ';' by removing the first and last chars 405 | # java/util/Optional 406 | # End result is still a bit awful, but it can be worked with... 407 | inner_type = sig[sig.index("<") + 1 : sig.rindex(">")][1:-1] 408 | serializer = { 409 | "type": inner_type 410 | } 411 | 412 | # Try to do some recognition of what it is: 413 | name = None 414 | name_prefix = "" 415 | if "Optional<" in inner_type: 416 | # NOTE: both java and guava optionals are used at different times 417 | name_prefix = "Opt" 418 | # Get rid of another parameter 419 | inner_type = inner_type[inner_type.index("<") + 1 : inner_type.rindex(">")][1:-1] 420 | 421 | if inner_type.startswith("java/lang/"): 422 | name = inner_type[len("java/lang/"):] 423 | if name == "Integer": 424 | name = "VarInt" 425 | elif inner_type == "java/util/UUID": 426 | name = "UUID" 427 | elif inner_type == "java/util/OptionalInt": 428 | name = "OptVarInt" 429 | elif inner_type == classes["nbtcompound"]: 430 | name = "NBT" 431 | elif inner_type == classes["itemstack"]: 432 | name = "Slot" 433 | elif inner_type == classes["chatcomponent"]: 434 | name = "Chat" 435 | elif inner_type == classes["position"]: 436 | name = "BlockPos" 437 | elif inner_type == classes["blockstate"]: 438 | name = "BlockState" 439 | elif inner_type == classes.get("particle"): # doesn't exist in all versions 440 | name = "Particle" 441 | else: 442 | # Try some more tests, based on the class itself: 443 | try: 444 | content_cf = classloader[inner_type] 445 | if len(list(content_cf.fields.find(type_="F"))) == 3: 446 | name = "Rotations" 447 | elif content_cf.constants.find_one(type_=String, f=lambda c: c == "down"): 448 | name = "Facing" 449 | elif content_cf.constants.find_one(type_=String, f=lambda c: c == "FALL_FLYING"): 450 | assert content_cf.access_flags.acc_enum 451 | name = "Pose" 452 | elif content_cf.constants.find_one(type_=String, f=lambda c: c == "profession"): 453 | name = "VillagerData" 454 | except: 455 | if verbose: 456 | print("Failed to determine name of metadata content type %s" % inner_type) 457 | import traceback 458 | traceback.print_exc() 459 | 460 | if name: 461 | serializer["name"] = name_prefix + name 462 | 463 | # Decompile the serialization code. 464 | # Note that we are using the bridge method that takes an object, and not the more find 465 | try: 466 | write_args = "L" + classes["packet.packetbuffer"] + ";Ljava/lang/Object;" 467 | methods = list(cf.methods.find(returns="V", args=write_args)) 468 | assert len(methods) == 1 469 | operations = _PIT.operations(classloader, cf, classes, verbose, 470 | methods[0], arg_names=("this", PACKETBUF_NAME, "value")) 471 | serializer.update(_PIT.format(operations)) 472 | except: 473 | if verbose: 474 | print("Failed to process operations for metadata serializer", serializer) 475 | import traceback 476 | traceback.print_exc() 477 | 478 | return serializer 479 | -------------------------------------------------------------------------------- /burger/toppings/identify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | """ 4 | Copyright (c) 2011 Tyler Kenendy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from .topping import Topping 26 | 27 | from jawa.constants import String 28 | 29 | import traceback 30 | 31 | # We can identify almost every class we need just by 32 | # looking for consistent strings. 33 | MATCHES = ( 34 | (['Fetching addPacket for removed entity', 'Fetching packet for removed entity'], 'entity.trackerentry'), 35 | (['#%04d/%d%s', 'attribute.modifier.equals.'], 'itemstack'), 36 | (['disconnect.lost'], 'nethandler.client'), 37 | (['Outdated server!', 'multiplayer.disconnect.outdated_client'], 38 | 'nethandler.server'), 39 | (['Corrupt NBT tag'], 'nbtcompound'), 40 | ([' is already assigned to protocol '], 'packet.connectionstate'), 41 | ( 42 | ['The received encoded string buffer length is ' \ 43 | 'less than zero! Weird string!'], 44 | 'packet.packetbuffer' 45 | ), 46 | (['Data value id is too big'], 'metadata'), 47 | (['X#X'], 'recipe.superclass'), 48 | (['Skipping BlockEntity with id '], 'tileentity.superclass'), 49 | ( 50 | ['ThreadedAnvilChunkStorage ({}): All chunks are saved'], 51 | 'anvilchunkloader' 52 | ), 53 | (['has invalidly named property'], 'blockstatecontainer'), 54 | ((['HORIZONTAL'], True), 'enumfacing.plane'), 55 | ((['bubble'], True), 'particletypes') 56 | ) 57 | 58 | # Enforce a lower priority on some matches, since some classes may match both 59 | # these and other strings, which we want to be grouped with the other string 60 | # if it exists, and with this if it doesn't 61 | MAYBE_MATCHES = ( 62 | (['Skipping Entity with id'], 'entity.list'), 63 | ) 64 | 65 | # In some cases there really isn't a good way to verify that it's a specific 66 | # class and we need to just depend on it coming first (bad!) 67 | # The biome class specifically is an issue because in 18w06a, the old name is 68 | # present in the biome's own class, but the ID is still in the register class. 69 | # This stops being an issue later into 1.13 when biome names become translatable. 70 | 71 | # Similarly, in 1.13, "bubble" is ambiguous between the particle class and 72 | # particle list, but the particletypes topping works with the first result in that case. 73 | 74 | # In 1.18-pre8, the "Getting block state" message now appears in both rendering 75 | # code and world code, but in both cases the return type is correct. 76 | IGNORE_DUPLICATES = [ "biome.register", "particletypes", "blockstate" ] 77 | 78 | def check_match(value, match_list): 79 | exact = False 80 | if isinstance(match_list, tuple): 81 | match_list, exact = match_list 82 | 83 | for match in match_list: 84 | if exact: 85 | if value != match: 86 | continue 87 | else: 88 | if match not in value: 89 | continue 90 | 91 | return True 92 | return False 93 | 94 | def identify(classloader, path, verbose): 95 | """ 96 | The first pass across the jar will identify all possible classes it 97 | can, maping them by the 'type' it implements. 98 | 99 | We have limited information available to us on this pass. We can only 100 | check for known signatures and predictable constants. In the next pass, 101 | we'll have the initial mapping from this pass available to us. 102 | """ 103 | possible_match = None 104 | 105 | for c in classloader.search_constant_pool(path=path, type_=String): 106 | value = c.string.value 107 | for match_list, match_name in MATCHES: 108 | if check_match(value, match_list): 109 | class_file = classloader[path] 110 | return match_name, class_file.this.name.value 111 | 112 | for match_list, match_name in MAYBE_MATCHES: 113 | if check_match(value, match_list): 114 | class_file = classloader[path] 115 | possible_match = (match_name, class_file.this.name.value) 116 | # Continue searching through the other constants in the class 117 | 118 | if 'BaseComponent' in value: 119 | class_file = classloader[path] 120 | # We want the interface for chat components, but it has no 121 | # string constants, so we need to use the abstract class and then 122 | # get its first implemented interface. 123 | 124 | # As of 20w17a, there is another interface in the middle that we don't 125 | # want, but the interface we do want extends Brigadier's Message interface. 126 | # So, loop up until a good-looking interface is present. 127 | # In other versions, the interface extends Iterable. In some versions, it extends both. 128 | while len(class_file.interfaces) in (1, 2): 129 | parent = class_file.interfaces[0].name.value 130 | if "com/mojang/brigadier" in parent or "java/lang/Iterable" == parent: 131 | break 132 | class_file = classloader[parent] 133 | else: 134 | # There wasn't the same number of interfaces, can't do anything really 135 | if verbose: 136 | print(class_file, "(parent of " + path + ", BaseComponent) has an unexpected number of interfaces:", class_file.interfaces) 137 | # Just hope for the best with the current class file 138 | 139 | return 'chatcomponent', class_file.this.name.value 140 | 141 | if value == 'ambient.cave': 142 | # This is found in both the sounds list class and sounds event class. 143 | # However, the sounds list class also has a constant specific to it. 144 | # Note that this method will not work in 1.8, but the list class doesn't exist then either. 145 | class_file = classloader[path] 146 | 147 | for c2 in class_file.constants.find(type_=String): 148 | if c2 == 'Accessed Sounds before Bootstrap!': 149 | return 'sounds.list', class_file.this.name.value 150 | else: 151 | return 'sounds.event', class_file.this.name.value 152 | 153 | if value == 'piston_head': 154 | # piston_head is a technical block, which is important as that means it has no item form. 155 | # This constant is found in both the block list class and the class containing block registrations. 156 | class_file = classloader[path] 157 | 158 | for c2 in class_file.constants.find(type_=String): 159 | if c2 == 'Accessed Blocks before Bootstrap!': 160 | return 'block.list', class_file.this.name.value 161 | else: 162 | return 'block.register', class_file.this.name.value 163 | 164 | if value == 'diamond_pickaxe': 165 | # Similarly, diamond_pickaxe is only an item. This exists in 3 classes, though: 166 | # - The actual item registration code 167 | # - The item list class 168 | # - The item renderer class (until 1.13), which we don't care about 169 | class_file = classloader[path] 170 | 171 | for c2 in class_file.constants.find(type_=String): 172 | if c2 == 'textures/misc/enchanted_item_glint.png': 173 | # Item renderer, which we don't care about 174 | return 175 | 176 | if c2 == 'Accessed Items before Bootstrap!': 177 | return 'item.list', class_file.this.name.value 178 | else: 179 | return 'item.register', class_file.this.name.value 180 | 181 | if value in ('Ice Plains', 'mutated_ice_flats', 'ice_spikes'): 182 | # Finally, biomes. There's several different names that were used for this one biome 183 | # Only classes are the list class and the one with registration. Note that the list didn't exist in 1.8. 184 | class_file = classloader[path] 185 | 186 | for c2 in class_file.constants.find(type_=String): 187 | if c2 == 'Accessed Biomes before Bootstrap!': 188 | return 'biome.list', class_file.this.name.value 189 | else: 190 | return 'biome.register', class_file.this.name.value 191 | 192 | if value == 'minecraft': 193 | class_file = classloader[path] 194 | 195 | # Look for two protected final strings 196 | def is_protected_final(m): 197 | return m.access_flags.acc_protected and m.access_flags.acc_final 198 | 199 | find_args = { 200 | "type_": "Ljava/lang/String;", 201 | "f": is_protected_final 202 | } 203 | fields = class_file.fields.find(**find_args) 204 | 205 | if len(list(fields)) == 2: 206 | return 'identifier', class_file.this.name.value 207 | 208 | if value == 'PooledMutableBlockPosition modified after it was released.': 209 | # Keep on going up the class hierarchy until we find a logger, 210 | # which is declared in the main BlockPos class 211 | # We can't hardcode a specific number of classes to go up, as 212 | # in some versions PooledMutableBlockPos extends BlockPos directly, 213 | # but in others have PooledMutableBlockPos extend MutableBlockPos. 214 | # Also, this is the _only_ string constant available to us. 215 | # Finally, note that PooledMutableBlockPos was introduced in 1.9. 216 | # This technique will not work in 1.8. 217 | cf = classloader[path] 218 | logger_type = "Lorg/apache/logging/log4j/Logger;" 219 | while not cf.fields.find_one(type_=logger_type): 220 | if cf.super_.name == "java/lang/Object": 221 | cf = None 222 | break 223 | cf = classloader[cf.super_.name.value] 224 | if cf: 225 | return 'position', cf.this.name.value 226 | 227 | if value == 'Getting block state': 228 | # This message is found in Chunk, in the method getBlockState. 229 | # We could also theoretically identify BlockPos from this method, 230 | # but currently identify only allows marking one class at a time. 231 | class_file = classloader[path] 232 | 233 | for method in class_file.methods: 234 | for ins in method.code.disassemble(): 235 | if ins.mnemonic in ("ldc", "ldc_w"): 236 | if ins.operands[0] == 'Getting block state': 237 | return 'blockstate', method.returns.name 238 | else: 239 | if verbose: 240 | print("Found chunk as %s, but didn't find the method that returns blockstate" % path) 241 | 242 | if value == 'particle.notFound': 243 | # This is in ParticleArgument, which is used for commands and 244 | # implements brigadier's ArgumentType. 245 | class_file = classloader[path] 246 | 247 | if len(class_file.interfaces) == 1 and class_file.interfaces[0].name == "com/mojang/brigadier/arguments/ArgumentType": 248 | sig = class_file.attributes.find_one(name="Signature").signature.value 249 | inner_type = sig[sig.index("<") + 1 : sig.rindex(">")][1:-1] 250 | return "particle", inner_type 251 | elif verbose: 252 | print("Found ParticleArgument as %s, but it didn't implement the expected interface" % path) 253 | 254 | # May (will usually) be None 255 | return possible_match 256 | 257 | 258 | class IdentifyTopping(Topping): 259 | """Finds important superclasses needed by other toppings.""" 260 | 261 | PROVIDES = [ 262 | "identify.anvilchunkloader", 263 | "identify.biome.list", 264 | "identify.biome.register", 265 | "identify.block.list", 266 | "identify.block.register", 267 | "identify.blockstatecontainer", 268 | "identify.blockstate", 269 | "identify.chatcomponent", 270 | "identify.entity.list", 271 | "identify.entity.trackerentry", 272 | "identify.enumfacing.plane", 273 | "identify.identifier", 274 | "identify.item.list", 275 | "identify.item.register", 276 | "identify.itemstack", 277 | "identify.metadata", 278 | "identify.nbtcompound", 279 | "identify.nethandler.client", 280 | "identify.nethandler.server", 281 | "identify.packet.connectionstate", 282 | "identify.packet.packetbuffer", 283 | "identify.particle", 284 | "identify.particletypes", 285 | "identify.position", 286 | "identify.recipe.superclass", 287 | "identify.resourcelocation", 288 | "identify.sounds.event", 289 | "identify.sounds.list", 290 | "identify.tileentity.superclass" 291 | ] 292 | 293 | DEPENDS = [] 294 | 295 | @staticmethod 296 | def act(aggregate, classloader, verbose=False): 297 | classes = aggregate.setdefault("classes", {}) 298 | for path in classloader.path_map.keys(): 299 | if not path.endswith(".class"): 300 | continue 301 | 302 | result = identify(classloader, path[:-len(".class")], verbose) 303 | if result: 304 | if result[0] in classes: 305 | if result[0] in IGNORE_DUPLICATES: 306 | continue 307 | raise Exception( 308 | "Already registered %(value)s to %(old_class)s! " 309 | "Can't overwrite it with %(new_class)s" % { 310 | "value": result[0], 311 | "old_class": classes[result[0]], 312 | "new_class": result[1] 313 | }) 314 | classes[result[0]] = result[1] 315 | if len(classes) == len(IdentifyTopping.PROVIDES): 316 | # If everything has been found, we don't need to keep 317 | # searching, so stop early for performance 318 | break 319 | 320 | # Add classes that might not be recognized in some versions 321 | # since the registration class is also the list class 322 | if "sounds.list" not in classes and "sounds.event" in classes: 323 | classes["sounds.list"] = classes["sounds.event"] 324 | if "block.list" not in classes and "block.register" in classes: 325 | classes["block.list"] = classes["block.register"] 326 | if "item.list" not in classes and "item.register" in classes: 327 | classes["item.list"] = classes["item.register"] 328 | if "biome.list" not in classes and "biome.register" in classes: 329 | classes["biome.list"] = classes["biome.register"] 330 | 331 | if verbose: 332 | print("identify classes: %s" % classes) 333 | -------------------------------------------------------------------------------- /burger/toppings/items.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | """ 4 | Copyright (c) 2011 Tyler Kenedy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | from .topping import Topping 25 | 26 | from jawa.constants import * 27 | from jawa.util.descriptor import method_descriptor 28 | 29 | from burger.util import WalkerCallback, walk_method 30 | 31 | import six 32 | 33 | class ItemsTopping(Topping): 34 | """Provides some information on most available items.""" 35 | PROVIDES = [ 36 | "identify.item.superclass", 37 | "items" 38 | ] 39 | 40 | DEPENDS = [ 41 | "identify.block.superclass", 42 | "identify.block.list", 43 | "identify.item.register", 44 | "identify.item.list", 45 | "language", 46 | "blocks", 47 | "version.protocol", 48 | "version.is_flattened" 49 | ] 50 | 51 | @staticmethod 52 | def act(aggregate, classloader, verbose=False): 53 | data_version = aggregate["version"]["data"] if "data" in aggregate["version"] else -1 54 | if data_version >= 1901: # 18w43a 55 | ItemsTopping._process_1point14(aggregate, classloader, verbose) 56 | return # This also adds classes 57 | elif data_version >= 1461: # 18w02a 58 | ItemsTopping._process_1point13(aggregate, classloader, verbose) 59 | else: 60 | ItemsTopping._process_1point12(aggregate, classloader, verbose) 61 | 62 | item_list = aggregate["items"]["item"] 63 | item_fields = aggregate["items"].setdefault("item_fields", {}) 64 | 65 | # Go through the item list and add the field info. 66 | list = aggregate["classes"]["item.list"] 67 | lcf = classloader[list] 68 | 69 | # Find the static block, and load the fields for each. 70 | method = lcf.methods.find_one(name="") 71 | item_name = "" 72 | for ins in method.code.disassemble(): 73 | if ins in ("ldc", "ldc_w"): 74 | const = ins.operands[0] 75 | if isinstance(const, String): 76 | item_name = const.string.value 77 | elif ins == "putstatic": 78 | const = ins.operands[0] 79 | field = const.name_and_type.name.value 80 | if item_name in item_list: 81 | item_list[item_name]["field"] = field 82 | elif verbose: 83 | print("Cannot find an item matching %s for field %s" % (item_name, field)) 84 | item_fields[field] = item_name 85 | 86 | @staticmethod 87 | def _process_1point14(aggregate, classloader, verbose): 88 | # Handles versions after 1.14 (specifically >= 18w43a) 89 | # All of the registration happens in the list class in this version. 90 | listclass = aggregate["classes"]["item.list"] 91 | lcf = classloader[listclass] 92 | superclass = next(lcf.fields.find()).type.name # The first field in the list class is an item 93 | cf = classloader[superclass] 94 | aggregate["classes"]["item.superclass"] = superclass 95 | blockclass = aggregate["classes"]["block.superclass"] 96 | blocklist = aggregate["classes"]["block.list"] 97 | 98 | cf = classloader[superclass] 99 | 100 | if "item" in aggregate["language"]: 101 | language = aggregate["language"]["item"] 102 | else: 103 | language = None 104 | 105 | # Figure out what the builder class is 106 | ctor = cf.methods.find_one(name="") 107 | builder_class = ctor.args[0].name 108 | builder_cf = classloader[builder_class] 109 | 110 | # Find the max stack size method 111 | max_stack_method = None 112 | for method in builder_cf.methods.find(args='I'): 113 | for ins in method.code.disassemble(): 114 | if ins.mnemonic in ("ldc", "ldc_w"): 115 | const = ins.operands[0] 116 | if isinstance(const, String) and const.string.value == "Unable to have damage AND stack.": 117 | max_stack_method = method 118 | break 119 | if max_stack_method: 120 | break 121 | if not max_stack_method: 122 | raise Exception("Couldn't find max stack size setter in " + builder_class) 123 | 124 | register_item_block_method = lcf.methods.find_one(args='L' + blockclass + ';', returns='L' + superclass + ';') 125 | item_block_class = None 126 | # Find the class used that represents an item that is a block 127 | for ins in register_item_block_method.code.disassemble(): 128 | if ins.mnemonic == "new": 129 | const = ins.operands[0] 130 | item_block_class = const.name.value 131 | break 132 | 133 | items = aggregate.setdefault("items", {}) 134 | item_list = items.setdefault("item", {}) 135 | item_fields = items.setdefault("item_fields", {}) 136 | 137 | is_item_class_cache = {superclass: True} 138 | def is_item_class(name): 139 | if name in is_item_class_cache: 140 | return is_item_class_cache 141 | elif name == 'java/lang/Object': 142 | return True 143 | elif '/' in name: 144 | return False 145 | 146 | cf = classloader[name] 147 | result = is_item_class(cf.super_.name.value) 148 | is_item_class_cache[name] = result 149 | return result 150 | # Find the static block registration method 151 | method = lcf.methods.find_one(name='') 152 | 153 | class Walker(WalkerCallback): 154 | def __init__(self): 155 | self.cur_id = 0 156 | 157 | def on_new(self, ins, const): 158 | class_name = const.name.value 159 | return {"class": class_name} 160 | 161 | def on_invoke(self, ins, const, obj, args): 162 | method_name = const.name_and_type.name.value 163 | method_desc = const.name_and_type.descriptor.value 164 | desc = method_descriptor(method_desc) 165 | 166 | if ins.mnemonic == "invokestatic": 167 | if const.class_.name.value == listclass: 168 | current_item = {} 169 | 170 | text_id = None 171 | for idx, arg in enumerate(desc.args): 172 | if arg.name == blockclass: 173 | if isinstance(args[idx], list): 174 | continue 175 | block = args[idx] 176 | text_id = block["text_id"] 177 | if "name" in block: 178 | current_item["name"] = block["name"] 179 | if "display_name" in block: 180 | current_item["display_name"] = block["display_name"] 181 | elif arg.name == superclass: 182 | current_item.update(args[idx]) 183 | elif arg.name == item_block_class: 184 | current_item.update(args[idx]) 185 | text_id = current_item["text_id"] 186 | elif arg.name == "java/lang/String": 187 | text_id = args[idx] 188 | 189 | if current_item == {} and not text_id: 190 | if verbose: 191 | print("Couldn't find any identifying information for the call to %s with %s" % (method_desc, args)) 192 | return 193 | 194 | if not text_id: 195 | if verbose: 196 | print("Could not find text_id for call to %s with %s" % (method_desc, args)) 197 | return 198 | 199 | # Call to the static register method. 200 | current_item["text_id"] = text_id 201 | current_item["numeric_id"] = self.cur_id 202 | self.cur_id += 1 203 | lang_key = "minecraft.%s" % text_id 204 | if language != None and lang_key in language: 205 | current_item["display_name"] = language[lang_key] 206 | if "max_stack_size" not in current_item: 207 | current_item["max_stack_size"] = 64 208 | item_list[text_id] = current_item 209 | 210 | return current_item 211 | else: 212 | if method_name == "": 213 | # Call to a constructor. Check if the builder is in the args, 214 | # and if so update the item with it 215 | idx = 0 216 | for arg in desc.args: 217 | if arg.name == builder_class: 218 | # Update from the builder 219 | if "max_stack_size" in args[idx]: 220 | obj["max_stack_size"] = args[idx]["max_stack_size"] 221 | elif arg.name == blockclass and "text_id" not in obj: 222 | block = args[idx] 223 | obj["text_id"] = block["text_id"] 224 | if "name" in block: 225 | obj["name"] = block["name"] 226 | if "display_name" in block: 227 | obj["display_name"] = block["display_name"] 228 | idx += 1 229 | elif method_name == max_stack_method.name.value and method_desc == max_stack_method.descriptor.value: 230 | obj["max_stack_size"] = args[0] 231 | 232 | if desc.returns.name != "void": 233 | if desc.returns.name == builder_class or is_item_class(desc.returns.name): 234 | if ins.mnemonic == "invokestatic": 235 | # Probably returning itself, but through a synthetic method 236 | return args[0] 237 | else: 238 | # Probably returning itself 239 | return obj 240 | else: 241 | return object() 242 | 243 | def on_get_field(self, ins, const, obj): 244 | if const.class_.name.value == blocklist: 245 | # Getting a block; put it on the stack. 246 | block_name = aggregate["blocks"]["block_fields"][const.name_and_type.name.value] 247 | if block_name not in aggregate["blocks"]["block"]: 248 | if verbose: 249 | print("No information available for item-block for %s/%s" % (const.name_and_type.name.value, block_name)) 250 | return {} 251 | else: 252 | return aggregate["blocks"]["block"][block_name] 253 | elif const.class_.name.value == listclass: 254 | return item_list[item_fields[const.name_and_type.name.value]] 255 | else: 256 | return const 257 | 258 | def on_put_field(self, ins, const, obj, value): 259 | if isinstance(value, dict): 260 | field = const.name_and_type.name.value 261 | value["field"] = field 262 | item_fields[const.name_and_type.name.value] = value["text_id"] 263 | 264 | walk_method(cf, method, Walker(), verbose) 265 | 266 | @staticmethod 267 | def _process_1point13(aggregate, classloader, verbose): 268 | # Handles versions after 1.13 (specifically >= 18w02a) 269 | superclass = aggregate["classes"]["item.register"] 270 | aggregate["classes"]["item.superclass"] = superclass 271 | blockclass = aggregate["classes"]["block.superclass"] 272 | blocklist = aggregate["classes"]["block.list"] 273 | 274 | cf = classloader[superclass] 275 | 276 | if "item" in aggregate["language"]: 277 | language = aggregate["language"]["item"] 278 | else: 279 | language = None 280 | 281 | # Figure out what the builder class is 282 | ctor = cf.methods.find_one(name="") 283 | builder_class = ctor.args[0].name 284 | builder_cf = classloader[builder_class] 285 | 286 | # Find the max stack size method 287 | max_stack_method = None 288 | for method in builder_cf.methods.find(args='I'): 289 | for ins in method.code.disassemble(): 290 | if ins in ("ldc", "ldc_w"): 291 | const = ins.operands[0] 292 | if isinstance(const, String) and const == "Unable to have damage AND stack.": 293 | max_stack_method = method 294 | break 295 | if not max_stack_method: 296 | raise Exception("Couldn't find max stack size setter in " + builder_class) 297 | 298 | register_item_block_method = cf.methods.find_one(args='L' + blockclass + ';', returns="V") 299 | item_block_class = None 300 | # Find the class used that represents an item that is a block 301 | for ins in register_item_block_method.code.disassemble(): 302 | if ins == "new": 303 | const = ins.operands[0] 304 | item_block_class = const.name.value 305 | break 306 | 307 | items = aggregate.setdefault("items", {}) 308 | item_list = items.setdefault("item", {}) 309 | 310 | is_item_class_cache = {superclass: True} 311 | def is_item_class(name): 312 | if name in is_item_class_cache: 313 | return is_item_class_cache 314 | elif name == 'java/lang/Object': 315 | return True 316 | elif '/' in name: 317 | return False 318 | 319 | cf = classloader[name] 320 | result = is_item_class(cf.super_.name.value) 321 | is_item_class_cache[name] = result 322 | return result 323 | # Find the static block registration method 324 | method = cf.methods.find_one(args='', returns="V", f=lambda m: m.access_flags.acc_public and m.access_flags.acc_static) 325 | 326 | class Walker(WalkerCallback): 327 | def __init__(self): 328 | self.cur_id = 0 329 | 330 | def on_new(self, ins, const): 331 | class_name = const.name.value 332 | return {"class": class_name} 333 | 334 | def on_invoke(self, ins, const, obj, args): 335 | method_name = const.name_and_type.name.value 336 | method_desc = const.name_and_type.descriptor.value 337 | desc = method_descriptor(method_desc) 338 | 339 | if ins == "invokestatic": 340 | if const.class_.name == superclass: 341 | current_item = {} 342 | 343 | text_id = None 344 | idx = 0 345 | for arg in desc.args: 346 | if arg.name == blockclass: 347 | block = args[idx] 348 | text_id = block["text_id"] 349 | if "name" in block: 350 | current_item["name"] = block["name"] 351 | if "display_name" in block: 352 | current_item["display_name"] = block["display_name"] 353 | elif arg.name == superclass: 354 | current_item.update(args[idx]) 355 | elif arg.name == item_block_class: 356 | current_item.update(args[idx]) 357 | text_id = current_item["text_id"] 358 | elif arg.name == "java/lang/String": 359 | text_id = args[idx] 360 | idx += 1 361 | 362 | if current_item == {} and not text_id: 363 | if verbose: 364 | print("Couldn't find any identifying information for the call to %s with %s" % (method_desc, args)) 365 | return 366 | 367 | if not text_id: 368 | if verbose: 369 | print("Could not find text_id for call to %s with %s" % (method_desc, args)) 370 | return 371 | 372 | # Call to the static register method. 373 | current_item["text_id"] = text_id 374 | current_item["numeric_id"] = self.cur_id 375 | self.cur_id += 1 376 | lang_key = "minecraft.%s" % text_id 377 | if language != None and lang_key in language: 378 | current_item["display_name"] = language[lang_key] 379 | if "max_stack_size" not in current_item: 380 | current_item["max_stack_size"] = 64 381 | item_list[text_id] = current_item 382 | else: 383 | if method_name == "": 384 | # Call to a constructor. Check if the builder is in the args, 385 | # and if so update the item with it 386 | idx = 0 387 | for arg in desc.args: 388 | if arg.name == builder_class: 389 | # Update from the builder 390 | if "max_stack_size" in args[idx]: 391 | obj["max_stack_size"] = args[idx]["max_stack_size"] 392 | elif arg.name == blockclass and "text_id" not in obj: 393 | block = args[idx] 394 | obj["text_id"] = block["text_id"] 395 | if "name" in block: 396 | obj["name"] = block["name"] 397 | if "display_name" in block: 398 | obj["display_name"] = block["display_name"] 399 | idx += 1 400 | elif method_name == max_stack_method.name.value and method_desc == max_stack_method.descriptor.value: 401 | obj["max_stack_size"] = args[0] 402 | 403 | if desc.returns.name != "void": 404 | if desc.returns.name == builder_class or is_item_class(desc.returns.name): 405 | if ins == "invokestatic": 406 | # Probably returning itself, but through a synthetic method 407 | return args[0] 408 | else: 409 | # Probably returning itself 410 | return obj 411 | else: 412 | return object() 413 | 414 | def on_get_field(self, ins, const, obj): 415 | if const.class_.name == blocklist: 416 | # Getting a block; put it on the stack. 417 | block_name = aggregate["blocks"]["block_fields"][const.name_and_type.name.value] 418 | if block_name not in aggregate["blocks"]["block"]: 419 | if verbose: 420 | print("No information available for item-block for %s/%s" % (const.name_and_type.name.value, block_name)) 421 | return {} 422 | else: 423 | return aggregate["blocks"]["block"][block_name] 424 | else: 425 | return const 426 | 427 | def on_put_field(self, ins, const, obj, value): 428 | raise Exception("unexpected putfield: %s" % ins) 429 | 430 | walk_method(cf, method, Walker(), verbose) 431 | 432 | @staticmethod 433 | def _process_1point12(aggregate, classloader, verbose): 434 | superclass = aggregate["classes"]["item.register"] 435 | aggregate["classes"]["item.superclass"] = superclass 436 | blockclass = aggregate["classes"]["block.superclass"] 437 | blocklist = aggregate["classes"]["block.list"] 438 | 439 | is_flattened = aggregate["version"]["is_flattened"] 440 | 441 | def add_block_info_to_item(field_info, item): 442 | """Adds data from the given field (should be in Blocks) to the given item""" 443 | assert field_info["class"] == blocklist 444 | 445 | block_name = aggregate["blocks"]["block_fields"][field_info["name"]] 446 | if block_name not in aggregate["blocks"]["block"]: 447 | if verbose: 448 | print("No information available for item-block for %s/%s" % (field_info["name"], block_name)) 449 | return 450 | block = aggregate["blocks"]["block"][block_name] 451 | 452 | if not is_flattened and "numeric_id" in block: 453 | item["numeric_id"] = block["numeric_id"] 454 | item["text_id"] = block["text_id"] 455 | if "name" in block: 456 | item["name"] = block["name"] 457 | if "display_name" in block: 458 | item["display_name"] = block["display_name"] 459 | 460 | cf = classloader[superclass] 461 | 462 | # Find the registration method 463 | method = cf.methods.find_one(args='', returns="V", f=lambda m: m.access_flags.acc_public and m.access_flags.acc_static) 464 | items = aggregate.setdefault("items", {}) 465 | item_list = items.setdefault("item", {}) 466 | 467 | if "item" in aggregate["language"]: 468 | language = aggregate["language"]["item"] 469 | else: 470 | language = None 471 | 472 | string_setter = cf.methods.find_one(returns="L" + superclass + ";", 473 | args="Ljava/lang/String;", 474 | f=lambda x: not x.access_flags.acc_static) 475 | int_setter = cf.methods.find_one(returns="L" + superclass + ";", 476 | args="I", f=lambda x: not x.access_flags.acc_static) 477 | 478 | if string_setter: 479 | # There will be 2, but the first one is the name setter 480 | name_setter = string_setter.name.value + string_setter.descriptor.value 481 | else: 482 | name_setter = None 483 | 484 | if int_setter: 485 | # There are multiple; the first one sets max stack size and another 486 | # sets max durability. However, durability is called in the constructor, 487 | # so we can't use it easily 488 | stack_size_setter = int_setter.name.value + int_setter.descriptor.value 489 | else: 490 | stack_size_setter = None 491 | 492 | register_item_block_method = cf.methods.find_one(args='L' + blockclass + ';', returns="V") 493 | register_item_block_method_custom = cf.methods.find_one(args='L' + blockclass + ';L' + superclass + ';', returns="V") 494 | register_item_method = cf.methods.find_one(args='ILjava/lang/String;L' + superclass + ';', returns="V") \ 495 | or cf.methods.find_one(args='Ljava/lang/String;L' + superclass + ';', returns="V") 496 | 497 | item_block_class = None 498 | # Find the class used that represents an item that is a block 499 | for ins in register_item_block_method.code.disassemble(): 500 | if ins == "new": 501 | const = ins.operands[0] 502 | item_block_class = const.name.value 503 | break 504 | 505 | stack = [] 506 | current_item = { 507 | "class": None, 508 | "calls": {} 509 | } 510 | tmp = [] 511 | 512 | for ins in method.code.disassemble(): 513 | if ins == "new": 514 | # The beginning of a new block definition 515 | const = ins.operands[0] 516 | class_name = const.name.value 517 | 518 | class_file = classloader[class_name] 519 | if class_file.super_.name == "java/lang/Object": 520 | # A function created for an item shouldn't be counted - we 521 | # only want items, not Functions. 522 | # I would check directly against the interface but I can't 523 | # seem to get that to work. 524 | if class_file.this.name.value != superclass: 525 | # If it's a call to 'new Item()' then it's still an item 526 | continue 527 | 528 | current_item = { 529 | "class": class_name, 530 | "calls": {} 531 | } 532 | 533 | if len(stack) == 2: 534 | # If the block is constructed in the registration method, 535 | # like `registerBlock(1, "stone", (new BlockStone()))`, then 536 | # the parameters are pushed onto the stack before the 537 | # constructor is called. 538 | current_item["numeric_id"] = stack[0] 539 | current_item["text_id"] = stack[1] 540 | elif len(stack) == 1: 541 | if isinstance(stack[0], six.string_types): 542 | current_item["text_id"] = stack[0] 543 | else: 544 | # Assuming this is a field set via getstatic 545 | add_block_info_to_item(stack[0], current_item) 546 | stack = [] 547 | elif ins.mnemonic.startswith("fconst"): 548 | stack.append(float(ins.mnemonic[-1])) 549 | elif ins in ("bipush", "sipush"): 550 | stack.append(ins.operands[0].value) 551 | elif ins in ("ldc", "ldc_w"): 552 | const = ins.operands[0] 553 | 554 | if isinstance(const, ConstantClass): 555 | stack.append("%s.class" % const.name.value) 556 | elif isinstance(const, String): 557 | stack.append(const.string.value) 558 | else: 559 | stack.append(const.value) 560 | elif ins in ("invokevirtual", "invokespecial"): 561 | # A method invocation 562 | const = ins.operands[0] 563 | method_name = const.name_and_type.name.value 564 | method_desc = const.name_and_type.descriptor.value 565 | current_item["calls"][method_name] = stack 566 | current_item["calls"][method_name + method_desc] = stack 567 | stack = [] 568 | elif ins == "getstatic": 569 | const = ins.operands[0] 570 | #TODO: Is this the right way to represent a field on the stack? 571 | stack.append({"class": const.class_.name.value, 572 | "name": const.name_and_type.name.value}) 573 | elif ins == "invokestatic": 574 | const = ins.operands[0] 575 | name_index = const.name_and_type.name.index 576 | descriptor_index = const.name_and_type.descriptor.index 577 | if name_index == register_item_block_method.name.index and descriptor_index == register_item_block_method.descriptor.index: 578 | current_item["class"] = item_block_class 579 | if len(stack) == 1: 580 | # Assuming this is a field set via getstatic 581 | add_block_info_to_item(stack[0], current_item) 582 | elif name_index == register_item_method.name.index and descriptor_index == register_item_method.descriptor.index: 583 | # Some items are constructed as a method variable rather 584 | # than directly in the registration method; thus the 585 | # paremters are set here. 586 | if len(stack) == 2: 587 | current_item["numeric_id"] = stack[0] 588 | current_item["text_id"] = stack[1] 589 | elif len(stack) == 1: 590 | current_item["text_id"] = stack[0] 591 | 592 | stack = [] 593 | tmp.append(current_item) 594 | current_item = { 595 | "class": None, 596 | "calls": {} 597 | } 598 | 599 | if is_flattened: 600 | # Current IDs are incremental, manually track them 601 | cur_id = 0 602 | 603 | for item in tmp: 604 | if not "text_id" in item: 605 | init_one_block = "(L" + blockclass + ";)V" 606 | init_two_blocks = "(L" + blockclass + ";L" + blockclass + ";)V" 607 | if init_one_block in item["calls"]: 608 | add_block_info_to_item(item["calls"][init_one_block][0], item) 609 | elif init_two_blocks in item["calls"]: 610 | # Skulls use this 611 | add_block_info_to_item(item["calls"][init_two_blocks][0], item) 612 | else: 613 | if verbose: 614 | print("Dropping nameless item, couldn't identify ctor for a block: %s" % item) 615 | continue 616 | 617 | if not "text_id" in item: 618 | if verbose: 619 | print("Even after item block handling, no name: %s" % item) 620 | continue 621 | 622 | final = {} 623 | 624 | if "numeric_id" in item: 625 | assert not is_flattened 626 | final["numeric_id"] = item["numeric_id"] 627 | else: 628 | assert is_flattened 629 | final["numeric_id"] = cur_id 630 | cur_id += 1 631 | 632 | if "text_id" in item: 633 | final["text_id"] = item["text_id"] 634 | final["class"] = item["class"] 635 | 636 | if "name" in item: 637 | final["name"] = item["name"] 638 | if "display_name" in item: 639 | final["display_name"] = item["display_name"] 640 | 641 | if name_setter in item["calls"]: 642 | final["name"] = item["calls"][name_setter][0] 643 | 644 | if stack_size_setter in item["calls"]: 645 | final["max_stack_size"] = item["calls"][stack_size_setter][0] 646 | else: 647 | final["max_stack_size"] = 64 648 | 649 | if "name" in final: 650 | lang_key = "%s.name" % final["name"] 651 | else: 652 | # 17w43a (1.13) and above - no specific translation string, only the id 653 | lang_key = "minecraft.%s" % final["text_id"] 654 | if language and lang_key in language: 655 | final["display_name"] = language[lang_key] 656 | 657 | item_list[final["text_id"]] = final 658 | -------------------------------------------------------------------------------- /burger/toppings/language.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | """ 4 | Copyright (c) 2011 Tyler Kenendy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | from .topping import Topping 25 | import six 26 | 27 | try: 28 | import json 29 | except ImportError: 30 | import simplejson as json 31 | 32 | class LanguageTopping(Topping): 33 | """Provides the contents of the English language files.""" 34 | 35 | PROVIDES = [ 36 | "language" 37 | ] 38 | 39 | DEPENDS = [] 40 | 41 | @staticmethod 42 | def act(aggregate, classloader, verbose=False): 43 | aggregate["language"] = {} 44 | LanguageTopping.load_language( 45 | aggregate, 46 | classloader, 47 | "lang/stats_US.lang", 48 | verbose 49 | ) 50 | LanguageTopping.load_language( 51 | aggregate, 52 | classloader, 53 | "lang/en_US.lang", 54 | verbose 55 | ) 56 | LanguageTopping.load_language( 57 | aggregate, 58 | classloader, 59 | "assets/minecraft/lang/en_US.lang", 60 | verbose 61 | ) 62 | LanguageTopping.load_language( 63 | aggregate, 64 | classloader, 65 | "assets/minecraft/lang/en_us.lang", 66 | verbose 67 | ) 68 | LanguageTopping.load_language( 69 | aggregate, 70 | classloader, 71 | "assets/minecraft/lang/en_us.json", 72 | verbose, 73 | True 74 | ) 75 | 76 | @staticmethod 77 | def load_language(aggregate, classloader, path, verbose=False, is_json=False): 78 | try: 79 | with classloader.open(path) as fin: 80 | contents = fin.read().decode("utf-8") 81 | except: 82 | if verbose: 83 | print("Can't find file %s in jar" % path) 84 | return 85 | 86 | for category, name, value in LanguageTopping.parse_lang(contents, verbose, is_json): 87 | cat = aggregate["language"].setdefault(category, {}) 88 | cat[name] = value 89 | 90 | @staticmethod 91 | def parse_lang(contents, verbose, is_json): 92 | if is_json: 93 | contents = json.loads(contents) 94 | for tag, value in six.iteritems(contents): 95 | category, name = tag.split(".", 1) 96 | 97 | yield (category, name, value) 98 | else: 99 | contents = contents.split("\n") 100 | lineno = 0 101 | for line in contents: 102 | lineno = lineno + 1 103 | line = line.strip() 104 | 105 | if not line: 106 | continue 107 | if line[0] == "#": 108 | continue 109 | 110 | if not "=" in line or not "." in line: 111 | if verbose: 112 | print("Language file line %s is malformed: %s" % (lineno, line)) 113 | continue 114 | 115 | tag, value = line.split("=", 1) 116 | category, name = tag.split(".", 1) 117 | 118 | yield (category, name, value) -------------------------------------------------------------------------------- /burger/toppings/objects.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | """ 4 | Copyright (c) 2011 Tyler Kenendy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import six 26 | from copy import copy 27 | 28 | from .topping import Topping 29 | 30 | from jawa.constants import * 31 | 32 | class ObjectTopping(Topping): 33 | """Gets most vehicle/object types.""" 34 | 35 | PROVIDES = [ 36 | "entities.object" 37 | ] 38 | 39 | DEPENDS = [ 40 | "identify.nethandler.client", 41 | "identify.entity.trackerentry", 42 | "version.data", 43 | "entities.entity", 44 | "packets.classes" 45 | ] 46 | 47 | @staticmethod 48 | def act(aggregate, classloader, verbose=False): 49 | if aggregate["version"]["data"] >= 1930: # 19w05a+ 50 | # Object IDs were removed in 19w05a, and entity IDs are now used instead. Skip this topping entirely. 51 | return 52 | if "entity.trackerentry" not in aggregate["classes"] or "nethandler.client" not in aggregate["classes"]: 53 | return 54 | 55 | entities = aggregate["entities"] 56 | 57 | # Find the spawn object packet ID using EntityTrackerEntry.createSpawnPacket 58 | # (which handles other spawn packets too, but the first item in it is correct) 59 | entitytrackerentry = aggregate["classes"]["entity.trackerentry"] 60 | entitytrackerentry_cf = classloader[entitytrackerentry] 61 | 62 | createspawnpacket_method = entitytrackerentry_cf.methods.find_one(args="", 63 | f=lambda x: x.access_flags.acc_private and not x.access_flags.acc_static and not x.returns.name == "void") 64 | 65 | packet_class_name = None 66 | 67 | # Handle capitalization changes from 1.11 68 | item_entity_class = entities["entity"]["item"]["class"] if "item" in entities["entity"] else entities["entity"]["Item"]["class"] 69 | 70 | will_be_spawn_object_packet = False 71 | for ins in createspawnpacket_method.code.disassemble(): 72 | if ins == "instanceof": 73 | # Check to make sure that it's a spawn packet for item entities 74 | const = ins.operands[0] 75 | if const.name == item_entity_class: 76 | will_be_spawn_object_packet = True 77 | elif ins == "new" and will_be_spawn_object_packet: 78 | const = ins.operands[0] 79 | packet_class_name = const.name.value 80 | break 81 | 82 | if packet_class_name is None: 83 | if verbose: 84 | print("Failed to find spawn object packet") 85 | return 86 | 87 | # Get the packet info for the spawn object packet - not required but it is helpful information 88 | for key, packet in six.iteritems(aggregate["packets"]["packet"]): 89 | if packet_class_name in packet["class"]: 90 | # "in" is used because packet["class"] would have ".class" at the end 91 | entities["info"]["spawn_object_packet"] = key 92 | break 93 | 94 | objects = entities.setdefault("object", {}) 95 | 96 | # Now find the spawn object packet handler and use it to figure out IDs 97 | nethandler = aggregate["classes"]["nethandler.client"] 98 | nethandler_cf = classloader[nethandler] 99 | method = nethandler_cf.methods.find_one(args="L" + packet_class_name + ";") 100 | 101 | potential_id = 0 102 | current_id = 0 103 | 104 | for ins in method.code.disassemble(): 105 | if ins == "if_icmpne": 106 | current_id = potential_id 107 | elif ins in ("bipush", "sipush"): 108 | potential_id = ins.operands[0].value 109 | elif ins == "new": 110 | const = ins.operands[0] 111 | tmp = {"id": current_id, "class": const.name.value} 112 | objects[tmp["id"]] = tmp 113 | 114 | entities_by_class = {entity["class"]: entity for entity in six.itervalues(entities["entity"])} 115 | 116 | from .entities import EntityTopping 117 | EntityTopping.compute_sizes(classloader, aggregate, objects) # Needed because some objects aren't in the entity list 118 | 119 | for o in six.itervalues(objects): 120 | if o["class"] in entities_by_class: 121 | # If this object corresponds to a known entity, copy data from that 122 | entity = entities_by_class[o["class"]] 123 | if "id" in entity: 124 | o["entity_id"] = entity["id"] 125 | if "name" in entity: 126 | o["name"] = entity["name"] 127 | if "width" in entity: 128 | o["width"] = entity["width"] 129 | if "height" in entity: 130 | o["height"] = entity["height"] 131 | if "texture" in entity: 132 | o["texture"] = entity["texture"] 133 | 134 | entities["info"]["object_count"] = len(objects) 135 | -------------------------------------------------------------------------------- /burger/toppings/packets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | """ 4 | Copyright (c) 2011 Tyler Kenendy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from .topping import Topping 26 | 27 | from jawa.constants import * 28 | 29 | from burger.util import * 30 | 31 | def packet_name(packet): 32 | return "%s_%s_%02X" % (packet["state"], packet["direction"], packet["id"]) 33 | 34 | class PacketsTopping(Topping): 35 | """Provides minimal information on all network packets.""" 36 | 37 | PROVIDES = [ 38 | "packets.ids", 39 | "packets.classes", 40 | "packets.directions" 41 | ] 42 | 43 | DEPENDS = [ 44 | "identify.packet.connectionstate", 45 | "identify.packet.packetbuffer" 46 | ] 47 | 48 | @staticmethod 49 | def act(aggregate, classloader, verbose=False): 50 | connectionstate = aggregate["classes"]["packet.connectionstate"] 51 | cf = classloader[connectionstate] 52 | 53 | # Find the static constructor 54 | method = cf.methods.find_one(name="") 55 | stack = [] 56 | 57 | packets = aggregate.setdefault("packets", {}) 58 | packet = packets.setdefault("packet", {}) 59 | states = packets.setdefault("states", {}) 60 | directions = packets.setdefault("directions", {}) 61 | 62 | # There are 3 (post-netty) formats that the registration code takes: 63 | # - The 1.7 format (13w41a through 1.7.10 and 14w21b) 64 | # - The 1.8 format (14w25a through 1.14.4) 65 | # - The 1.15 format (19w34a+) 66 | # These can be conveniently decided by the number of protected instance 67 | # methods that return ConnectionState itself. 68 | register_methods = list(cf.methods.find(returns="L" + connectionstate + ";", 69 | f=lambda x: x.access_flags.acc_protected and not x.access_flags.acc_static)) 70 | 71 | if len(register_methods) == 2: 72 | PacketsTopping.parse_17_format(classloader, connectionstate, register_methods, directions, states, packet, verbose) 73 | elif len(register_methods) == 1: 74 | PacketsTopping.parse_18_format(classloader, connectionstate, register_methods[0], directions, states, packet, verbose) 75 | elif len(register_methods) == 0: 76 | PacketsTopping.parse_115_format(classloader, connectionstate, directions, states, packet, verbose) 77 | 78 | info = packets.setdefault("info", {}) 79 | info["count"] = len(packet) 80 | 81 | @staticmethod 82 | def parse_17_format(classloader, connectionstate, register_methods, directions, states, packets, verbose): 83 | # The relevant code looks like this: 84 | """ 85 | enum EnumConnectionState { // eo in 1.7.10 86 | HANDSHAKING(-1) {{ // a (ep) 87 | this.registerServerbound(0, C00Handshake.class); 88 | }}, 89 | PLAY(0) {{ // b (eq) 90 | this.registerClientbound(0, S00PacketKeepAlive.class); 91 | this.registerClientbound(1, S01PacketJoinGame.class); 92 | this.registerClientbound(2, S02PacketChat.class); 93 | this.registerClientbound(3, S03PacketTimeUpdate.class); 94 | // ... 95 | this.registerServerbound(0, C00PacketKeepAlive.class); 96 | this.registerServerbound(1, C01PacketChatMessage.class); 97 | this.registerServerbound(2, C02PacketUseEntity.class); 98 | this.registerServerbound(3, C03PacketPlayer.class); 99 | // ... 100 | }}, 101 | STATUS(1) {{ // c (er) 102 | // ... 103 | }}, 104 | LOGIN(2) {{ // d (es) 105 | // ... 106 | }}; 107 | // ... 108 | private final com.google.common.collect.BiMap serverboundPackets; // h 109 | private final com.google.common.collect.BiMap clientboundPackets; // i 110 | // ... 111 | protected EnumConnectionState registerServerbound(int id, Class packetClass) { // a 112 | if (this.serverboundPackets.containsKey(Integer.valueOf(id))) { 113 | String error = "Serverbound packet ID " + id + " is already assigned to " + this.serverboundPackets.get(id) + "; cannot re-assign to " + packetClass; 114 | LogManager.getLogger().fatal(error); 115 | throw new IllegalArgumentException(error); 116 | } else if (this.serverboundPackets.containsValue(packetClass)) { 117 | String error = "Serverbound packet " + packetClass + " is already assigned to ID " + this.serverboundPackets.inverse().get(packetClass) + "; cannot re-assign to " + id; 118 | LogManager.getLogger().fatal(error); 119 | throw new IllegalArgumentException(error); 120 | } else { 121 | this.serverboundPackets.put(id, packetClass); 122 | return this; 123 | } 124 | } 125 | 126 | protected EnumConnectionState registerClientbound(int id, Class packetClass) { // b 127 | if (this.clientboundPackets.containsKey(Integer.valueOf(id))) { 128 | String error = "Clientbound packet ID " + id + " is already assigned to " + this.clientboundPackets.get(id) + "; cannot re-assign to " + packetClass; 129 | LogManager.getLogger().fatal(error); 130 | throw new IllegalArgumentException(error); 131 | } else if (this.clientboundPackets.containsValue(packetClass)) { 132 | String error = "Clientbound packet " + packetClass + " is already assigned to ID " + this.clientboundPackets.inverse().get(packetClass) + "; cannot re-assign to " + id; 133 | LogManager.getLogger().fatal(error); 134 | throw new IllegalArgumentException(error); 135 | } else { 136 | this.clientboundPackets.put(id, packetClass); 137 | return this; 138 | } 139 | } 140 | // ... 141 | } 142 | """ 143 | # We can identify the serverbound and clientbound methods by the string 144 | # in the error message. Packet IDs are manual in this version. 145 | cf = classloader[connectionstate] 146 | 147 | # First, figure out registerServerbound and registerClientbound by looking for the string constants: 148 | directions_by_method = {} 149 | for method in register_methods: 150 | for ins in method.code.disassemble(): 151 | if ins == "ldc": 152 | const = ins.operands[0] 153 | if isinstance(const, String): 154 | if "Clientbound" in const.string.value: 155 | directions["CLIENTBOUND"] = { 156 | "register_method": method.name.value, 157 | "name": "CLIENTBOUND" 158 | } 159 | directions_by_method[method.name.value] = directions["CLIENTBOUND"] 160 | break 161 | elif "Serverbound" in const.string.value: 162 | directions["SERVERBOUND"] = { 163 | "register_method": method.name.value, 164 | "name": "SERVERBOUND" 165 | } 166 | directions_by_method[method.name.value] = directions["SERVERBOUND"] 167 | break 168 | 169 | # Now identify the inner enum classes: 170 | states.update(get_enum_constants(cf, verbose)) 171 | # These are the states on this version, which shouldn't change 172 | assert states.keys() == set(("HANDSHAKING", "PLAY", "STATUS", "LOGIN")) 173 | 174 | # Now that we have states and directions, go through each state and 175 | # find its calls to register. This happens in the state's constructor. 176 | for state in states.values(): 177 | class StateHandlerCallback(WalkerCallback): 178 | def on_invoke(self, ins, const, obj, args): 179 | if const.name_and_type.name.value == "": 180 | # call to super 181 | return 182 | 183 | assert len(args) == 2 184 | id = args[0] 185 | cls = args[1] 186 | # Make sure this is one of the register methods 187 | assert const.name_and_type.name.value in directions_by_method 188 | dir = directions_by_method[const.name_and_type.name.value]["name"] 189 | from_client = (dir == "SERVERBOUND") 190 | from_server = (dir == "CLIENTBOUND") 191 | packet = { 192 | "id": id, 193 | "class": cls, 194 | "direction": dir, 195 | "from_client": from_client, 196 | "from_server": from_server, 197 | "state": state["name"] 198 | } 199 | packets[packet_name(packet)] = packet 200 | return obj 201 | 202 | def on_new(self, ins, const): 203 | raise Exception("Unexpected new: %s" % str(ins)) 204 | def on_put_field(self, ins, const, obj, value): 205 | raise Exception("Unexpected putfield: %s" % str(ins)) 206 | def on_get_field(self, ins, const, obj): 207 | raise Exception("Unexpected getfield: %s" % str(ins)) 208 | 209 | state_cf = classloader[state["class"]] 210 | walk_method(state_cf, state_cf.methods.find_one(name=""), StateHandlerCallback(), verbose) 211 | 212 | @staticmethod 213 | def parse_18_format(classloader, connectionstate, register_method, directions, states, packets, verbose): 214 | # The relevant code looks like this: 215 | """ 216 | public enum EnumConnectionState { // gy in 1.8 217 | HANDSHAKING(-1) {{ // a (gz) 218 | this.registerPacket(EnumPacketDirection.SERVERBOUND, C00Handshake.class); 219 | }}, 220 | PLAY(0) {{ // b (ha) 221 | this.registerPacket(EnumPacketDirection.CLIENTBOUND, S00PacketKeepAlive.class); 222 | this.registerPacket(EnumPacketDirection.CLIENTBOUND, S01PacketJoinGame.class); 223 | this.registerPacket(EnumPacketDirection.CLIENTBOUND, S02PacketChat.class); 224 | this.registerPacket(EnumPacketDirection.CLIENTBOUND, S03PacketTimeUpdate.class); 225 | // ... 226 | this.registerPacket(EnumPacketDirection.SERVERBOUND, C00PacketKeepAlive.class); 227 | this.registerPacket(EnumPacketDirection.SERVERBOUND, C01PacketChatMessage.class); 228 | this.registerPacket(EnumPacketDirection.SERVERBOUND, C02PacketUseEntity.class); 229 | this.registerPacket(EnumPacketDirection.SERVERBOUND, C03PacketPlayer.class); 230 | }}, 231 | STATUS(1) {{ // c (hb) 232 | // ... 233 | }}, 234 | LOGIN(2) {{ // d (hc) 235 | // ... 236 | }}; 237 | // ... 238 | } 239 | """ 240 | # Fortunately, we can figure out what EnumPacketDirection is 241 | # using the signature of the register method. 242 | cf = classloader[connectionstate] 243 | 244 | assert len(register_method.args) == 2 245 | assert register_method.args[1].name == "java/lang/Class" 246 | direction_class = register_method.args[0].name 247 | 248 | directions.update(get_enum_constants(classloader[direction_class], verbose)) 249 | directions_by_field = {direction["field"]: direction for direction in directions.values()} 250 | 251 | # The directions should be the ones we know and love: 252 | assert directions.keys() == set(("CLIENTBOUND", "SERVERBOUND")) 253 | 254 | # Now identify the inner enum classes: 255 | states.update(get_enum_constants(cf, verbose)) 256 | # These are the states on this version, which shouldn't change 257 | assert states.keys() == set(("HANDSHAKING", "PLAY", "STATUS", "LOGIN")) 258 | 259 | # Now that we have states and directions, go through each state and 260 | # find its calls to register. This happens in the state's constructor. 261 | for state in states.values(): 262 | # Packet IDs dynamically count up from 0 for each direction, 263 | # resetting to 0 for each state. 264 | cur_id = { dir_name: 0 for dir_name in directions.keys() } 265 | 266 | class StateHandlerCallback(WalkerCallback): 267 | def on_invoke(self, ins, const, obj, args): 268 | if const.name_and_type.name.value == "": 269 | # call to super 270 | return 271 | 272 | assert len(args) == 2 273 | direction = args[0]["name"] 274 | cls = args[1] 275 | 276 | id = cur_id[direction] 277 | cur_id[direction] += 1 278 | 279 | from_client = (direction == "SERVERBOUND") 280 | from_server = (direction == "CLIENTBOUND") 281 | packet = { 282 | "id": id, 283 | "class": cls, 284 | "direction": direction, 285 | "from_client": from_client, 286 | "from_server": from_server, 287 | "state": state["name"] 288 | } 289 | packets[packet_name(packet)] = packet 290 | return obj 291 | 292 | def on_get_field(self, ins, const, obj): 293 | if const.class_.name == direction_class: 294 | return directions_by_field[const.name_and_type.name.value] 295 | 296 | raise Exception("Unexpected getfield: %s" % str(ins)) 297 | 298 | def on_new(self, ins, const): 299 | raise Exception("Unexpected new: %s" % str(ins)) 300 | def on_put_field(self, ins, const, obj, value): 301 | raise Exception("Unexpected putfield: %s" % str(ins)) 302 | 303 | state_cf = classloader[state["class"]] 304 | walk_method(state_cf, state_cf.methods.find_one(name=""), StateHandlerCallback(), verbose) 305 | 306 | @staticmethod 307 | def parse_115_format(classloader, connectionstate, directions, states, packets, verbose): 308 | # The relevant code looks like this: 309 | """ 310 | public enum ProtocolType { 311 | HANDSHAKING(-1, builder() 312 | .registerDirection(PacketDirection.SERVERBOUND, (new ProtocolType.PacketList()) 313 | .registerPacket(CHandshakePacket.class, CHandshakePacket::new))), 314 | PLAY(0, builder() 315 | .registerDirection(PacketDirection.CLIENTBOUND, (new ProtocolType.PacketList()) 316 | .registerPacket(SSpawnObjectPacket.class, SSpawnObjectPacket::new) 317 | .registerPacket(SSpawnExperienceOrbPacket.class, SSpawnExperienceOrbPacket::new) 318 | .registerPacket(SSpawnGlobalEntityPacket.class, SSpawnGlobalEntityPacket::new) 319 | .registerPacket(SSpawnMobPacket.class, SSpawnMobPacket::new) 320 | // ... 321 | ).registerDirection(PacketDirection.SERVERBOUND, (new ProtocolType.PacketList()) 322 | .registerPacket(CConfirmTeleportPacket.class, CConfirmTeleportPacket::new) 323 | .registerPacket(CQueryTileEntityNBTPacket.class, CQueryTileEntityNBTPacket::new) 324 | .registerPacket(CSetDifficultyPacket.class, CSetDifficultyPacket::new) 325 | .registerPacket(CChatMessagePacket.class, CChatMessagePacket::new) 326 | //... 327 | )), 328 | STATUS(1, builder() 329 | .registerDirection(PacketDirection.SERVERBOUND, (new ProtocolType.PacketList()) 333 | .registerPacket(SServerInfoPacket.class, SServerInfoPacket::new) 334 | .registerPacket(SPongPacket.class, SPongPacket::new))), 335 | LOGIN(2, builder() 336 | .registerDirection(PacketDirection.CLIENTBOUND, (new ProtocolType.PacketList()) 337 | .registerPacket(SDisconnectLoginPacket.class, SDisconnectLoginPacket::new) 338 | .registerPacket(SEncryptionRequestPacket.class, SEncryptionRequestPacket::new) 339 | .registerPacket(SLoginSuccessPacket.class, SLoginSuccessPacket::new) 340 | .registerPacket(SEnableCompressionPacket.class, SEnableCompressionPacket::new) 341 | .registerPacket(SCustomPayloadLoginPacket.class, SCustomPayloadLoginPacket::new)) 342 | .registerDirection(PacketDirection.SERVERBOUND, (new ProtocolType.PacketList()) 343 | .registerPacket(CLoginStartPacket.class, CLoginStartPacket::new) 344 | .registerPacket(CEncryptionResponsePacket.class, CEncryptionResponsePacket::new) 345 | .registerPacket(CCustomPayloadLoginPacket.class, CCustomPayloadLoginPacket::new))); 346 | 347 | private ProtocolType(int id, Builder builder) { 348 | } 349 | 350 | private static ProtocolType.Builder builder() { 351 | return new ProtocolType.Builder(); 352 | } 353 | 354 | static class Builder { 355 | public ProtocolType.Builder registerDirection(PacketDirection direction, ProtocolType.PacketList packetList) { 356 | // ... 357 | return this; 358 | } 359 | } 360 | 361 | static class PacketList { 362 | private PacketList() { // Yes, this is private, though it's accessed externally... 363 | // ... 364 | } 365 | public

> ProtocolType.PacketList registerPacket(Class

packetClass, Supplier

constructor) { 366 | // ... 367 | return this; 368 | } 369 | } 370 | } 371 | """ 372 | # (This is using 1.14 MCP names and my own guesses, neither of which I'm completely happy with) 373 | cf = classloader[connectionstate] 374 | clinit = cf.methods.find_one(name="") 375 | 376 | # Identify the enum constants, though this skips over the rest of the initialization we care about: 377 | states.update(get_enum_constants(cf, verbose)) 378 | # These are the states on this version, which hopefully won't change 379 | assert states.keys() == set(("HANDSHAKING", "PLAY", "STATUS", "LOGIN")) 380 | 381 | # Identify the direction class, by first locating builder() as the first call... 382 | for ins in clinit.code.disassemble(): 383 | if ins.mnemonic == "invokestatic": 384 | const = ins.operands[0] 385 | assert const.class_.name == connectionstate 386 | builder_method = cf.methods.find_one(name=const.name_and_type.name, f=lambda m: m.descriptor == const.name_and_type.descriptor) 387 | break 388 | else: 389 | raise Exception("Needed to find an invokestatic instruction") 390 | 391 | # Now get the Builder class, and then PacketDirection and PacketList 392 | builder = builder_method.returns.name 393 | builder_cf = classloader[builder] 394 | # Assume that registerDirection is the only public method 395 | register_direction = builder_cf.methods.find_one(f=lambda m: m.access_flags.acc_public) 396 | 397 | direction_class = register_direction.args[0].name 398 | packet_list = register_direction.args[1].name 399 | 400 | directions.update(get_enum_constants(classloader[direction_class], verbose)) 401 | directions_by_field = {direction["field"]: direction for direction in directions.values()} 402 | 403 | # The directions should be the ones we know and love: 404 | assert directions.keys() == set(("CLIENTBOUND", "SERVERBOUND")) 405 | 406 | # Now go through the init code one last time, this time looking at all 407 | # the instructions: 408 | packets_by_state = {} 409 | class StateHandlerCallback(WalkerCallback): 410 | def on_invoke(self, ins, const, obj, args): 411 | if const.name_and_type.name == "": 412 | if const.class_.name == connectionstate: 413 | # Call to enum constructor. Store data now. 414 | packets_by_state[args[0]] = args[3] 415 | return 416 | 417 | if const.name_and_type.name == builder_method.name and \ 418 | const.name_and_type.descriptor == builder_method.descriptor: 419 | # Builder is represented by a dict that maps direction to 420 | # the packetlist 421 | return {} 422 | 423 | if const.class_.name == builder: 424 | # Assume call to registerDirection 425 | direction = args[0] 426 | packetlist = args[1] 427 | obj[direction] = packetlist 428 | return obj 429 | 430 | if const.class_.name == packet_list: 431 | cls = args[0] 432 | packet_lambda_class = args[1] + ".class" 433 | assert cls == packet_lambda_class 434 | obj.append(cls) 435 | return obj 436 | 437 | if const.name_and_type.descriptor == "()[L" + connectionstate + ";": 438 | # Calling $values() to construct the values array -- this is 439 | # is the endpoint for us in versions starting with 21w19a. 440 | # 21w19a updated to require Java 16. 441 | # This function was added by a Javac change from Java 15: 442 | # https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8241798 443 | raise StopIteration() 444 | 445 | def on_get_field(self, ins, const, obj): 446 | if const.class_.name == direction_class: 447 | return directions_by_field[const.name_and_type.name.value]["name"] 448 | 449 | if const.class_.name == connectionstate: 450 | # Getting the enum fields to create the values array -- this 451 | # is the endpoint for us in versions before 21w19a 452 | raise StopIteration() 453 | 454 | raise Exception("Unexpected getfield: %s" % str(ins)) 455 | 456 | def on_new(self, ins, const): 457 | if const.name == connectionstate: 458 | # Connection state doesn't need to be represented directly, 459 | # since we don't actually use the object once it's fully 460 | # constructed 461 | return object() 462 | if const.name == packet_list: 463 | # PacketList is just a list 464 | return [] 465 | 466 | raise Exception("Unexpected new: %s" % str(ins)) 467 | 468 | def on_invokedynamic(self, ins, const, args): 469 | return class_from_invokedynamic(ins, cf) 470 | 471 | def on_put_field(self, ins, const, obj, value): 472 | # Ignore putfields, since we're registering in the constructor 473 | # call. 474 | pass 475 | 476 | walk_method(cf, clinit, StateHandlerCallback(), verbose) 477 | # packets_by_state now looks like this (albeit with obfuscated names): 478 | # {'HANDSHAKING': {'SERVERBOUND': ['CHandshakePacket']}, 'PLAY': {'CLIENTBOUND': ['SSpawnObjectPacket', 'SSpawnExperienceOrbPacket', 'SSpawnGlobalEntityPacket', 'SSpawnMobPacket'], 'SERVERBOUND': ['CConfirmTeleportPacket', 'CQueryTileEntityNBTPacket', 'CSetDifficultyPacket', 'CChatMessagePacket']}, 'STATUS': {'SERVERBOUND': [], 'CLIENTBOUND': []}, 'LOGIN': {'CLIENTBOUND': [], 'SERVERBOUND': []}} 479 | # We need to transform this into something more like what's used in other versions. 480 | 481 | for state, directions in packets_by_state.items(): 482 | for direction, packetlist in directions.items(): 483 | for id, packet_class in enumerate(packetlist): 484 | from_client = (direction == "SERVERBOUND") 485 | from_server = (direction == "CLIENTBOUND") 486 | packet = { 487 | "id": id, 488 | "class": packet_class, 489 | "direction": direction, 490 | "from_client": from_client, 491 | "from_server": from_server, 492 | "state": state 493 | } 494 | packets[packet_name(packet)] = packet 495 | -------------------------------------------------------------------------------- /burger/toppings/particletypes.py: -------------------------------------------------------------------------------- 1 | from .topping import Topping 2 | 3 | 4 | class ParticleTypesTopping(Topping): 5 | """Provides a list of all particle types""" 6 | 7 | PROVIDES = ["particletypes"] 8 | DEPENDS = ["identify.particletypes"] 9 | 10 | @staticmethod 11 | def act(aggregate, classloader, verbose=False): 12 | particletypes = [] 13 | cf = classloader[aggregate["classes"]["particletypes"]] 14 | # Method is either or a void with no parameters, check both 15 | # until we find one that loads constants 16 | for meth in cf.methods.find(args='', returns='V'): 17 | ops = tuple(meth.code.disassemble()) 18 | if next(filter(lambda op: 'ldc' in op.name, ops), False): 19 | break 20 | 21 | for idx, op in enumerate(ops): 22 | if 'ldc' in op.name: 23 | str_val = op.operands[0].string.value 24 | 25 | # Enum identifiers in older version of MC are all uppercase, 26 | # these are distinct from the particletype strings we're 27 | # collecting here. 28 | if str_val.isupper(): 29 | continue 30 | 31 | # This instruction sequence is unique to particle type fields 32 | if ops[idx + 1].name in ('bipush', 'getstatic'): 33 | particletypes.append(str_val) 34 | 35 | aggregate['particletypes'] = particletypes 36 | -------------------------------------------------------------------------------- /burger/toppings/recipes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | """ 4 | Copyright (c) 2011 Tyler Kenendy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | from .topping import Topping 26 | 27 | from jawa.util.descriptor import method_descriptor 28 | from jawa.constants import * 29 | 30 | try: 31 | import json 32 | except ImportError: 33 | import simplejson as json 34 | 35 | import six 36 | import copy 37 | 38 | class RecipesTopping(Topping): 39 | """Provides a list of most possible crafting recipes.""" 40 | 41 | PROVIDES = [ 42 | "recipes" 43 | ] 44 | 45 | DEPENDS = [ 46 | "identify.recipe.superclass", 47 | "identify.block.list", 48 | "identify.item.list", 49 | "blocks", 50 | "items", 51 | "tags" 52 | ] 53 | 54 | @staticmethod 55 | def act(aggregate, classloader, verbose=False): 56 | if "assets/minecraft/recipes/stick.json" in classloader.path_map: 57 | recipe_list = RecipesTopping.find_from_json(aggregate, classloader, "assets/minecraft/recipes/", verbose) 58 | elif "data/minecraft/recipes/stick.json" in classloader.path_map: 59 | recipe_list = RecipesTopping.find_from_json(aggregate, classloader, "data/minecraft/recipes/", verbose) 60 | else: 61 | recipe_list = RecipesTopping.find_from_jar(aggregate, classloader, verbose) 62 | 63 | recipes = aggregate.setdefault("recipes", {}) 64 | 65 | for recipe in recipe_list: 66 | makes = recipe['makes']['name'] 67 | 68 | recipes_for_item = recipes.setdefault(makes, []) 69 | recipes_for_item.append(recipe) 70 | 71 | @staticmethod 72 | def find_from_json(aggregate, classloader, prefix, verbose): 73 | if verbose: 74 | print("Extracting recipes from JSON") 75 | 76 | recipes = [] 77 | 78 | def parse_item(blob, allow_lists=True): 79 | """ 80 | Converts a JSON item into a burger item. 81 | If the blob is a list, then a list is returned (if allowed). 82 | """ 83 | # If the key involves a list, then convert to a list and parse them all. 84 | if isinstance(blob, list): 85 | if allow_lists: 86 | return [parse_item(entry) for entry in blob] 87 | else: 88 | raise Exception("A list of items is not allowed in this context") 89 | elif "tag" in blob: 90 | if allow_lists: 91 | res = [] 92 | tag = blob["tag"] 93 | if tag.startswith("minecraft:"): 94 | tag = tag[len("minecraft:"):] 95 | for id in aggregate["tags"]["items/" + tag]["values"]: 96 | res.append(parse_item({"item": id})) 97 | return res 98 | else: 99 | raise Exception("A tag is not allowed in this context") 100 | # There's some wierd stuff regarding 0 or 32767 here; I'm not worrying about it though 101 | # Probably 0 is the default for results, and 32767 means "any" for ingredients 102 | assert "item" in blob 103 | result = { 104 | "type": "item" 105 | } 106 | 107 | id = blob["item"] 108 | if id.startswith("minecraft:"): 109 | id = id[len("minecraft:"):] # TODO: In the future, we don't want to strip namespaces 110 | 111 | if verbose and id not in aggregate["items"]["item"]: 112 | print("A recipe references item %s but that doesn't exist" % id) 113 | 114 | result["name"] = id 115 | 116 | if "data" in blob: 117 | result["metadata"] = blob["data"] 118 | if "count" in blob: 119 | result["count"] = blob["count"] 120 | 121 | return result 122 | 123 | for name in classloader.path_map.keys(): 124 | if name.startswith(prefix) and name.endswith(".json"): 125 | recipe_id = "minecraft:" + name[len(prefix):-len(".json")] 126 | try: 127 | with classloader.open(name) as fin: 128 | data = json.load(fin) 129 | 130 | assert "type" in data 131 | recipe_type = data["type"] 132 | if recipe_type.startswith("minecraft:"): 133 | recipe_type = recipe_type[len("minecraft:"):] 134 | 135 | if recipe_type not in ("crafting_shaped", "crafting_shapeless"): 136 | # We only care about regular recipes, not furnace/loom/whatever ones. 137 | continue 138 | 139 | recipe = {} 140 | recipe["id"] = recipe_id # new for 1.12, but used ingame 141 | 142 | if "group" in data: 143 | recipe["group"] = data["group"] 144 | 145 | 146 | assert "result" in data 147 | recipe["makes"] = parse_item(data["result"], False) 148 | if "count" not in recipe["makes"]: 149 | recipe["makes"]["count"] = 1 # default, TODO should we keep specifying this? 150 | 151 | matching_recipes = [recipe] 152 | 153 | if recipe_type == "crafting_shapeless": 154 | recipe["type"] = 'shapeless' 155 | 156 | assert "ingredients" in data 157 | 158 | recipe["ingredients"] = [] 159 | for ingredient in data["ingredients"]: 160 | item = parse_item(ingredient) 161 | if isinstance(item, list): 162 | tmp = [] 163 | for recipe_choice in matching_recipes: 164 | for real_item in item: 165 | recipe_choice_work = copy.deepcopy(recipe_choice) 166 | recipe_choice_work["ingredients"].append(real_item) 167 | tmp.append(recipe_choice_work) 168 | matching_recipes = tmp 169 | else: 170 | for recipe_choice in matching_recipes: 171 | recipe_choice["ingredients"].append(item) 172 | elif recipe_type == "crafting_shaped": 173 | recipe["type"] = 'shape' 174 | 175 | assert "pattern" in data 176 | assert "key" in data 177 | 178 | pattern = data["pattern"] 179 | recipe["raw"] = { 180 | "rows": pattern, 181 | "subs": {} 182 | } 183 | for (id, value) in six.iteritems(data["key"]): 184 | item = parse_item(value) 185 | if isinstance(item, list): 186 | tmp = [] 187 | for recipe_choice in matching_recipes: 188 | for real_item in item: 189 | recipe_choice_work = copy.deepcopy(recipe_choice) 190 | recipe_choice_work["raw"]["subs"][id] = real_item 191 | tmp.append(recipe_choice_work) 192 | matching_recipes = tmp 193 | else: 194 | for recipe_choice in matching_recipes: 195 | recipe_choice["raw"]["subs"][id] = item 196 | 197 | for recipe_choice in matching_recipes: 198 | shape = [] 199 | for row in recipe_choice["raw"]["rows"]: 200 | shape_row = [] 201 | for char in row: 202 | if not char.isspace(): 203 | shape_row.append(recipe_choice["raw"]["subs"][char]) 204 | else: 205 | shape_row.append(None) 206 | shape.append(shape_row) 207 | recipe_choice["shape"] = shape 208 | 209 | recipes.extend(matching_recipes) 210 | except Exception as e: 211 | print("Failed to parse %s: %s" % (recipe_id, e)) 212 | raise 213 | 214 | return recipes 215 | 216 | @staticmethod 217 | def find_from_jar(aggregate, classloader, verbose): 218 | superclass = aggregate["classes"]["recipe.superclass"] 219 | 220 | if verbose: 221 | print("Extracting recipes from %s" % superclass) 222 | 223 | cf = classloader[superclass] 224 | 225 | # Find the constructor 226 | method = cf.methods.find_one( 227 | name="" 228 | ) 229 | 230 | # Find the set function, so we can figure out what class defines 231 | # a recipe. 232 | # This method's second parameter is an array of objects. 233 | setters = list(cf.methods.find( 234 | f = lambda m: len(m.args) == 2 and m.args[1].dimensions == 1 and m.args[1].name == "java/lang/Object" 235 | )) 236 | 237 | itemstack = aggregate["classes"]["itemstack"] 238 | 239 | target_class = setters[0].args[0] 240 | setter_names = [x.name.value for x in setters] 241 | 242 | def get_material(clazz, field): 243 | """Converts a class name and field into a block or item.""" 244 | if clazz == aggregate["classes"]["block.list"]: 245 | if field in aggregate["blocks"]["block_fields"]: 246 | name = aggregate["blocks"]["block_fields"][field] 247 | return { 248 | 'type': 'block', 249 | 'name': name 250 | } 251 | else: 252 | raise Exception("Unknown block with field " + field) 253 | elif clazz == aggregate["classes"]["item.list"]: 254 | if field in aggregate["items"]["item_fields"]: 255 | name = aggregate["items"]["item_fields"][field] 256 | return { 257 | 'type': 'item', 258 | 'name': name 259 | } 260 | else: 261 | raise Exception("Unknown item with field " + field) 262 | else: 263 | raise Exception("Unknown list class " + clazz) 264 | 265 | def read_itemstack(itr): 266 | """Reads an itemstack from the given iterator of instructions""" 267 | stack = [] 268 | while True: 269 | ins = itr.next() 270 | if ins in ("bipush", "sipush"): 271 | stack.append(ins.operands[0].value) 272 | elif ins == "getstatic": 273 | const = ins.operands[0] 274 | clazz = const.class_.name.value 275 | name = const.name_and_type.name.value 276 | stack.append((clazz, name)) 277 | elif ins == "invokevirtual": 278 | # TODO: This is a _total_ hack... 279 | # We assume that this is an enum, used to get the data value 280 | # for the given block. We also assume that the return value 281 | # matches the enum constant's position... and do math from that. 282 | name = stack.pop()[1] 283 | # As I said... ugly. There's probably a way better way of doing this. 284 | dv = int(name, 36) - int('a', 36) 285 | stack.append(dv) 286 | elif ins == "iadd": 287 | # For whatever reason, there are a few cases where 4 is both 288 | # added and subtracted to the enum constant value. 289 | # So we need to handle that :/ 290 | i2 = stack.pop() 291 | i1 = stack.pop() 292 | stack.append(i1 + i2); 293 | elif ins == "isub": 294 | i2 = stack.pop() 295 | i1 = stack.pop() 296 | stack.append(i1 - i2); 297 | elif ins == "invokespecial": 298 | const = ins.operands[0] 299 | if const.name_and_type.name == "": 300 | break 301 | 302 | item = get_material(*stack[0]) 303 | if len(stack) == 3: 304 | item['count'] = stack[1] 305 | item['metadata'] = stack[2] 306 | elif len(stack) == 2: 307 | item['count'] = stack[1] 308 | return item 309 | 310 | def find_recipes(classloader, cf, method, target_class, setter_names): 311 | # Go through all instructions. 312 | itr = iter(method.code.disassemble()) 313 | recipes = [] 314 | try: 315 | while True: 316 | ins = itr.next() 317 | if ins.mnemonic != "new": 318 | # Wait until an item starts 319 | continue 320 | # Start of another recipe - the ending item. 321 | const = ins.operands[0] 322 | if const.name.value != itemstack: 323 | # Or it could be another type; irrelevant 324 | continue 325 | # The crafted item, first parameter 326 | crafted_item = read_itemstack(itr) 327 | 328 | ins = itr.next() 329 | # Size of the parameter array 330 | if ins in ("bipush", "sipush"): 331 | param_count = ins.operands[0].value 332 | else: 333 | raise Exception('Unexpected instruction: expected int constant, got ' + str(ins)) 334 | 335 | num_astore = 0 336 | data = None 337 | array = [] 338 | while num_astore < param_count: 339 | ins = itr.next() 340 | # Read through the array; some strangeness of types, 341 | # though. Also, note that the array index is pushed, 342 | # but we overwrite it with the second value and just 343 | # add in order instead. 344 | # 345 | # The weirdness here is because characters and strings are 346 | # mixed; for example jukebox looks like this: 347 | # new Object[] {"###", "#X#", "###", '#', Blocks.PLANKS, 'X', Items.DIAMOND} 348 | if ins == "aastore": 349 | num_astore += 1 350 | array.append(data) 351 | data = None 352 | elif ins in ("ldc", "ldc_w"): 353 | const = ins.operands[0] 354 | # Separate into a list of characters, to disambiguate (see below) 355 | data = list(const.string.value) 356 | if ins in ("bipush", "sipush"): 357 | data = ins.operands[0].value 358 | elif ins == "invokestatic": 359 | const = ins.operands[0] 360 | if const.class_.name == "java/lang/Character" and const.name_and_type.name == "valueOf": 361 | data = chr(data) 362 | else: 363 | raise Exception("Unknown method invocation: " + repr(const)) 364 | elif ins == "getstatic": 365 | const = ins.operands[0] 366 | clazz = const.class_.name.value 367 | field = const.name_and_type.name.value 368 | data = get_material(clazz, field) 369 | elif ins == "new": 370 | data = read_itemstack(itr) 371 | 372 | ins = itr.next() 373 | assert ins == "invokevirtual" 374 | const = ins.operands[0] 375 | 376 | recipe_data = {} 377 | if const.name_and_type.name == setter_names[0]: 378 | # Shaped 379 | recipe_data['type'] = 'shape' 380 | recipe_data['makes'] = crafted_item 381 | rows = [] 382 | subs = {} 383 | # Keys are a list while values are a single character; 384 | # we make the keys a list merely as a way to disambiguate 385 | # and rejoin it later (hacky :/) 386 | try: 387 | itr2 = iter(array) 388 | while True: 389 | obj = itr2.next() 390 | if isinstance(obj, list): 391 | # Pattern 392 | rows.append("".join(obj)) 393 | elif isinstance(obj, six.string_types): 394 | # Character 395 | item = itr2.next() 396 | subs[obj] = item 397 | except StopIteration: 398 | pass 399 | recipe_data['raw'] = { 400 | 'rows': rows, 401 | 'subs': subs 402 | } 403 | 404 | shape = [] 405 | for row in rows: 406 | shape_row = [] 407 | for char in row: 408 | if not char.isspace(): 409 | shape_row.append(subs[char]) 410 | else: 411 | shape_row.append(None) 412 | shape.append(shape_row) 413 | 414 | recipe_data['shape'] = shape 415 | else: 416 | # Shapeless 417 | recipe_data['type'] = 'shapeless' 418 | recipe_data['makes'] = crafted_item 419 | recipe_data['ingredients'] = array 420 | 421 | recipes.append(recipe_data) 422 | except StopIteration: 423 | pass 424 | return recipes 425 | 426 | return find_recipes(classloader, cf, method, target_class, setter_names) 427 | -------------------------------------------------------------------------------- /burger/toppings/sounds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | """ 4 | Copyright (c) 2011 Tyler Kenendy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | try: 26 | import json 27 | except ImportError: 28 | import simplejson as json 29 | 30 | import traceback 31 | 32 | import six 33 | import six.moves.urllib.request 34 | 35 | from burger import website 36 | from .topping import Topping 37 | 38 | from jawa.constants import * 39 | 40 | RESOURCES_SITE = "http://resources.download.minecraft.net/%(short_hash)s/%(hash)s" 41 | 42 | def get_sounds(asset_index, resources_site=RESOURCES_SITE): 43 | """Downloads the sounds.json file from the assets index""" 44 | hash = asset_index["objects"]["minecraft/sounds.json"]["hash"] 45 | short_hash = hash[0:2] 46 | sounds_url = resources_site % {'hash': hash, 'short_hash': short_hash} 47 | 48 | sounds_file = six.moves.urllib.request.urlopen(sounds_url) 49 | 50 | try: 51 | return json.load(sounds_file) 52 | finally: 53 | sounds_file.close() 54 | 55 | class SoundTopping(Topping): 56 | """Finds all named sound effects which are both used in the server and 57 | available for download.""" 58 | 59 | PROVIDES = [ 60 | "sounds" 61 | ] 62 | 63 | DEPENDS = [ 64 | "identify.sounds.list", 65 | "identify.sounds.event", 66 | "version.name", 67 | "language" 68 | ] 69 | 70 | @staticmethod 71 | def act(aggregate, classloader, verbose=False): 72 | sounds = aggregate.setdefault('sounds', {}) 73 | 74 | if 'sounds.event' not in aggregate["classes"]: 75 | # 1.8 - TODO implement this for 1.8 76 | if verbose: 77 | print("Not enough information to run sounds topping; missing sounds.event") 78 | return 79 | 80 | try: 81 | version_meta = website.get_version_meta(aggregate["version"]["id"], verbose) 82 | except Exception as e: 83 | if verbose: 84 | print("Error: Failed to download version meta for sounds: %s" % e) 85 | traceback.print_exc() 86 | return 87 | try: 88 | assets = website.get_asset_index(version_meta, verbose) 89 | except Exception as e: 90 | if verbose: 91 | print("Error: Failed to download asset index for sounds: %s" % e) 92 | traceback.print_exc() 93 | return 94 | try: 95 | sounds_json = get_sounds(assets) 96 | except Exception as e: 97 | if verbose: 98 | print("Error: Failed to download sound list: %s" % e) 99 | traceback.print_exc() 100 | return 101 | 102 | soundevent = aggregate["classes"]["sounds.event"] 103 | cf = classloader[soundevent] 104 | 105 | # Find the static sound registration method 106 | method = cf.methods.find_one(args='', returns="V", f=lambda m: m.access_flags.acc_static) 107 | 108 | sound_name = None 109 | sound_id = 0 110 | for ins in method.code.disassemble(): 111 | if ins in ('ldc', 'ldc_w'): 112 | const = ins.operands[0] 113 | sound_name = const.string.value 114 | elif ins == 'invokestatic': 115 | sound = { 116 | "name": sound_name, 117 | "id": sound_id 118 | } 119 | sound_id += 1 120 | 121 | if sound_name in sounds_json: 122 | json_sound = sounds_json[sound_name] 123 | if "sounds" in json_sound: 124 | sound["sounds"] = [] 125 | for value in json_sound["sounds"]: 126 | data = {} 127 | if isinstance(value, six.string_types): 128 | data["name"] = value 129 | path = value 130 | elif isinstance(value, dict): 131 | # Guardians use this to have a reduced volume 132 | data = value 133 | path = value["name"] 134 | asset_key = "minecraft/sounds/%s.ogg" % path 135 | if asset_key in assets["objects"]: 136 | data["hash"] = assets["objects"][asset_key]["hash"] 137 | sound["sounds"].append(data) 138 | if "subtitle" in json_sound: 139 | subtitle = json_sound["subtitle"] 140 | sound["subtitle_key"] = subtitle 141 | # Get rid of the starting key since the language topping 142 | # splits it off like that 143 | subtitle_trimmed = subtitle[len("subtitles."):] 144 | if subtitle_trimmed in aggregate["language"]["subtitles"]: 145 | sound["subtitle"] = aggregate["language"]["subtitles"][subtitle_trimmed] 146 | 147 | sounds[sound_name] = sound 148 | 149 | # Get fields now 150 | soundlist = aggregate["classes"]["sounds.list"] 151 | lcf = classloader[soundlist] 152 | 153 | method = lcf.methods.find_one(name="") 154 | for ins in method.code.disassemble(): 155 | if ins in ('ldc', 'ldc_w'): 156 | const = ins.operands[0] 157 | sound_name = const.string.value 158 | elif ins == "putstatic": 159 | if sound_name is None or sound_name == "Accessed Sounds before Bootstrap!": 160 | continue 161 | const = ins.operands[0] 162 | field = const.name_and_type.name.value 163 | sounds[sound_name]["field"] = field 164 | -------------------------------------------------------------------------------- /burger/toppings/stats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | """ 4 | Copyright (c) 2011 Tyler Kenendy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | from .topping import Topping 25 | import six 26 | 27 | class StatsTopping(Topping): 28 | """Gets all statistics and statistic related strings.""" 29 | 30 | PROVIDES = [ 31 | "stats.statistics", 32 | "stats.achievements" 33 | ] 34 | 35 | DEPENDS = [ 36 | "language" 37 | ] 38 | 39 | @staticmethod 40 | def act(aggregate, classloader, verbose=False): 41 | stats = aggregate.setdefault("stats", {}) 42 | if "stat" in aggregate["language"]: 43 | stat_lang = aggregate["language"]["stat"] 44 | 45 | for sk, sv in six.iteritems(stat_lang): 46 | item = stats.setdefault(sk, {}) 47 | item["desc"] = sv 48 | 49 | achievements = aggregate.setdefault("achievements", {}) 50 | if "achievement" in aggregate["language"]: 51 | achievement_lang = aggregate["language"]["achievement"] 52 | 53 | for ak, av in six.iteritems(achievement_lang): 54 | real_name = ak[:-5] if ak.endswith(".desc") else ak 55 | item = achievements.setdefault(real_name, {}) 56 | if ak.endswith(".desc"): 57 | item["desc"] = av 58 | else: 59 | item["name"] = av 60 | -------------------------------------------------------------------------------- /burger/toppings/tags.py: -------------------------------------------------------------------------------- 1 | from .topping import Topping 2 | 3 | try: 4 | import json 5 | except ImportError: 6 | import simplejson as json 7 | 8 | class TagsTopping(Topping): 9 | """Provides a list of all block and item tags""" 10 | 11 | PROVIDES = [ 12 | "tags" 13 | ] 14 | DEPENDS = [] 15 | 16 | @staticmethod 17 | def act(aggregate, classloader, verbose=False): 18 | tags = aggregate.setdefault("tags", {}) 19 | prefix = "data/minecraft/tags/" 20 | suffix = ".json" 21 | for path in classloader.path_map: 22 | if not path.startswith(prefix) or not path.endswith(suffix): 23 | continue 24 | key = path[len(prefix):-len(suffix)] 25 | idx = key.find("/") 26 | type, name = key[:idx], key[idx + 1:] 27 | with classloader.open(path) as fin: 28 | data = json.load(fin) 29 | data["type"] = type 30 | data["name"] = name 31 | tags[key] = data 32 | 33 | # Tags can reference other tags -- flatten that out. 34 | flattening = set() 35 | flattened = set() 36 | def flatten_tag(name): 37 | if name in flattening: 38 | if verbose: 39 | print("Already flattening " + name + " -- is there a cycle?", flattening) 40 | return 41 | if name in flattened: 42 | return 43 | 44 | flattening.add(name) 45 | 46 | tag = tags[name] 47 | values = tag["values"] 48 | new_values = [] 49 | for entry in values: 50 | if entry.startswith("#"): 51 | assert entry.startswith("#minecraft:") 52 | referenced_tag_name = tag["type"] + "/" + entry[len("#minecraft:"):] 53 | flatten_tag(referenced_tag_name) 54 | new_values.extend(tags[referenced_tag_name]["values"]) 55 | else: 56 | new_values.append(entry) 57 | tag["values"] = new_values 58 | 59 | flattening.discard(name) 60 | flattened.add(name) 61 | 62 | for name in tags: 63 | flatten_tag(name) 64 | -------------------------------------------------------------------------------- /burger/toppings/tileentities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | 4 | import six 5 | 6 | from .topping import Topping 7 | 8 | from jawa.constants import ConstantClass, String 9 | from burger.util import class_from_invokedynamic 10 | 11 | class TileEntityTopping(Topping): 12 | """Gets tile entity (block entity) types.""" 13 | 14 | PROVIDES = [ 15 | "identify.tileentity.list", 16 | "tileentities.list", 17 | "tileentities.tags", 18 | "tileentities.networkids" 19 | ] 20 | 21 | DEPENDS = [ 22 | "identify.tileentity.superclass", 23 | "identify.block.superclass", 24 | "packets.classes", 25 | "blocks" 26 | ] 27 | 28 | @staticmethod 29 | def act(aggregate, classloader, verbose=False): 30 | if "tileentity.superclass" not in aggregate["classes"]: 31 | if verbose: 32 | print("Missing tileentity.superclass") 33 | return 34 | 35 | TileEntityTopping.identify_block_entities(aggregate, classloader, verbose) 36 | TileEntityTopping.identify_associated_blocks(aggregate, classloader, verbose) 37 | TileEntityTopping.identify_network_ids(aggregate, classloader, verbose) 38 | 39 | @staticmethod 40 | def identify_block_entities(aggregate, classloader, verbose): 41 | te = aggregate.setdefault("tileentity", {}) 42 | 43 | superclass = aggregate["classes"]["tileentity.superclass"] 44 | cf = classloader[superclass] 45 | 46 | # First, figure out whether this is a version where the TE superclass 47 | # is also the TE list. 48 | if cf.constants.find_one(String, lambda c: c.string.value in ('daylight_detector', 'DLDetector')): 49 | # Yes, it is 50 | listclass = superclass 51 | else: 52 | # It isn't, but we can figure it out by looking at the constructor's first parameter. 53 | method = cf.methods.find_one(name="") 54 | listclass = method.args[0].name 55 | cf = classloader[listclass] 56 | 57 | aggregate["classes"]["tileentity.list"] = listclass 58 | 59 | method = cf.methods.find_one(name="") 60 | 61 | tileentities = te.setdefault("tileentities", {}) 62 | te_classes = te.setdefault("classes", {}) 63 | tmp = {} 64 | for ins in method.code.disassemble(): 65 | if ins in ("ldc", "ldc_w"): 66 | const = ins.operands[0] 67 | if isinstance(const, ConstantClass): 68 | # Used before 1.13 69 | tmp["class"] = const.name.value 70 | elif isinstance(const, String): 71 | tmp["name"] = const.string.value 72 | elif ins == "invokedynamic": 73 | # Used after 1.13 74 | tmp["class"] = class_from_invokedynamic(ins, cf) 75 | elif ins == "invokestatic": 76 | if "class" in tmp and "name" in tmp: 77 | tmp["blocks"] = [] 78 | tileentities[tmp["name"]] = tmp 79 | te_classes[tmp["class"]] = tmp["name"] 80 | tmp = {} 81 | 82 | @staticmethod 83 | def identify_associated_blocks(aggregate, classloader, verbose): 84 | te = aggregate["tileentity"] 85 | tileentities = te["tileentities"] 86 | te_classes = te["classes"] 87 | 88 | blocks = aggregate["blocks"]["block"] 89 | # Brewing stands are a fairly simple block entity with a clear hierarchy 90 | brewing_stand = blocks["brewing_stand"] 91 | cf = classloader[brewing_stand["class"]] 92 | 93 | blockcontainer = cf.super_.name.value 94 | cf = classloader[blockcontainer] 95 | assert len(cf.interfaces) == 1 96 | 97 | tileentityprovider = cf.interfaces[0].name.value 98 | cf = classloader[tileentityprovider] 99 | methods = list(cf.methods.find(returns="L" + aggregate["classes"]["tileentity.superclass"] + ";")) 100 | assert len(methods) == 1 101 | create_te_name = methods[0].name.value 102 | create_te_desc = methods[0].descriptor.value 103 | 104 | has_be_by_class = {} 105 | has_be_by_class[blockcontainer] = True 106 | has_be_by_class[aggregate["classes"]["block.superclass"]] = False 107 | 108 | def has_be(cls): 109 | if cls in has_be_by_class: 110 | return has_be_by_class[cls] 111 | 112 | cf = classloader[cls] 113 | 114 | if has_be(cf.super_.name.value): 115 | has_be_by_class[cls] = True 116 | return True 117 | 118 | for interface in cf.interfaces: 119 | # Final case: if it implements the interface but doesn't directly 120 | # extend BlockContainer, it's still a TE 121 | if interface.name.value == tileentityprovider: 122 | has_be_by_class[cls] = True 123 | return True 124 | 125 | return False 126 | 127 | blocks_with_be = [] 128 | 129 | for block in six.itervalues(blocks): 130 | if has_be(block["class"]): 131 | blocks_with_be.append(block) 132 | 133 | # OK, we've identified all blocks that have block entities... 134 | # now figure out which one each one actually has 135 | for block in blocks_with_be: 136 | # Find the createNewTileEntity method. 137 | # However, it might actually be in a parent class, so loop until it's found 138 | cls = block["class"] 139 | create_te = None 140 | while not create_te: 141 | cf = classloader[cls] 142 | cls = cf.super_.name.value 143 | create_te = cf.methods.find_one(f=lambda m: m.name == create_te_name and m.descriptor == create_te_desc) 144 | 145 | for ins in create_te.code.disassemble(): 146 | if ins.mnemonic == "new": 147 | const = ins.operands[0] 148 | te_name = te_classes[const.name.value] 149 | block["block_entity"] = te_name 150 | tileentities[te_name]["blocks"].append(block["text_id"]) 151 | break 152 | 153 | @staticmethod 154 | def identify_network_ids(aggregate, classloader, verbose): 155 | te = aggregate["tileentity"] 156 | tileentities = te["tileentities"] 157 | te_classes = te["classes"] 158 | 159 | nbt_tag_type = "L" + aggregate["classes"]["nbtcompound"] + ";" 160 | if "nethandler.client" in aggregate["classes"]: 161 | updatepacket = None 162 | for packet in six.itervalues(aggregate["packets"]["packet"]): 163 | if (packet["direction"] != "CLIENTBOUND" or 164 | packet["state"] != "PLAY"): 165 | continue 166 | 167 | packet_cf = classloader[packet["class"][:-len(".class")]] # XXX should we be including the .class sufix in the packet class if we just trim it everywhere we use it? 168 | # Check if the packet has the expected fields in the class file 169 | # for the update tile entity packet 170 | if (len(packet_cf.fields) >= 3 and 171 | # Tile entity type int, at least (maybe also position) 172 | len(list(packet_cf.fields.find(type_="I"))) >= 1 and 173 | # New NBT tag 174 | len(list(packet_cf.fields.find(type_=nbt_tag_type))) >= 1): 175 | # There are other fields, but they vary by version. 176 | updatepacket = packet 177 | break 178 | 179 | if not updatepacket: 180 | if verbose: 181 | print("Failed to identify update tile entity packet") 182 | return 183 | 184 | te["update_packet"] = updatepacket 185 | nethandler = aggregate["classes"]["nethandler.client"] 186 | nethandler_cf = classloader[nethandler] 187 | 188 | updatepacket_name = updatepacket["class"].replace(".class", "") 189 | 190 | method = nethandler_cf.methods.find_one( 191 | args="L" + updatepacket_name + ";") 192 | 193 | value = None 194 | for ins in method.code.disassemble(): 195 | if ins in ("bipush", "sipush"): 196 | value = ins.operands[0].value 197 | elif ins == "instanceof": 198 | if value is None: 199 | # Ensure the command block callback is not counted 200 | continue 201 | 202 | const = ins.operands[0] 203 | te_name = te_classes[const.name.value] 204 | tileentities[te_name]["network_id"] = value 205 | value = None 206 | -------------------------------------------------------------------------------- /burger/toppings/topping.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | """ 4 | Copyright (c) 2011 Tyler Kenendy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | 26 | class Topping(object): 27 | PROVIDES = None 28 | DEPENDS = None 29 | 30 | @staticmethod 31 | def act(aggregate, classloader, verbose=False): 32 | raise NotImplementedError() 33 | 34 | -------------------------------------------------------------------------------- /burger/toppings/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2011 Tyler Kenendy 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | """ 22 | 23 | from .topping import Topping 24 | 25 | from jawa.constants import * 26 | 27 | try: 28 | import json 29 | except ImportError: 30 | import simplejson as json 31 | 32 | class VersionTopping(Topping): 33 | """Provides the protocol version.""" 34 | 35 | PROVIDES = [ 36 | "version.protocol", 37 | "version.id", 38 | "version.name", 39 | "version.data", 40 | "version.is_flattened", 41 | "version.entity_format" 42 | ] 43 | 44 | DEPENDS = [ 45 | "identify.nethandler.server", 46 | "identify.anvilchunkloader" 47 | ] 48 | 49 | @staticmethod 50 | def act(aggregate, classloader, verbose=False): 51 | aggregate.setdefault("version", {}) 52 | 53 | try: 54 | # 18w47b+ has a file that just directly includes this info 55 | with classloader.open("version.json") as fin: 56 | version_json = json.load(fin) 57 | aggregate["version"]["data"] = version_json["world_version"] 58 | aggregate["version"]["protocol"] = version_json["protocol_version"] 59 | aggregate["version"]["name"] = version_json["name"] 60 | # Starting with 1.14.3-pre1, the "id" field began being used 61 | # for the id used on the downloads site. Prior to that, (1.14.2) 62 | # "name" was used, and "id" looked like 63 | # "1.14.2 / f647ba8dc371474797bee24b2b312ff4". 64 | # Our heuristic for this is whether the ID is shorter than the name. 65 | if len(version_json["id"]) <= len(version_json["name"]): 66 | if verbose: 67 | print("Using id '%s' over name '%s' for id as it is shorter" % (version_json["id"], version_json["name"])) 68 | aggregate["version"]["id"] = version_json["id"] 69 | else: 70 | if verbose: 71 | print("Using name '%s' over id '%s' for id as it is shorter" % (version_json["name"], version_json["id"])) 72 | aggregate["version"]["id"] = version_json["name"] 73 | except: 74 | # Find it manually 75 | VersionTopping.get_protocol_version(aggregate, classloader, verbose) 76 | VersionTopping.get_data_version(aggregate, classloader, verbose) 77 | 78 | if "data" in aggregate["version"]: 79 | data_version = aggregate["version"]["data"] 80 | # Versions after 17w46a (1449) are flattened 81 | aggregate["version"]["is_flattened"] = (data_version > 1449) 82 | if data_version >= 1461: 83 | # 1.13 (18w02a and above, 1461) uses yet another entity format 84 | aggregate["version"]["entity_format"] = "1.13" 85 | elif data_version >= 800: 86 | # 1.11 versions (16w32a and above, 800) use one entity format 87 | aggregate["version"]["entity_format"] = "1.11" 88 | else: 89 | # Old entity format 90 | aggregate["version"]["entity_format"] = "1.10" 91 | else: 92 | aggregate["version"]["is_flattened"] = False 93 | aggregate["version"]["entity_format"] = "1.10" 94 | 95 | @staticmethod 96 | def get_protocol_version(aggregate, classloader, verbose): 97 | versions = aggregate["version"] 98 | if "nethandler.server" in aggregate["classes"]: 99 | nethandler = aggregate["classes"]["nethandler.server"] 100 | cf = classloader[nethandler] 101 | version = None 102 | looking_for_version_name = False 103 | for method in cf.methods: 104 | for instr in method.code.disassemble(): 105 | if instr in ("bipush", "sipush"): 106 | version = instr.operands[0].value 107 | elif instr == "ldc" and version is not None: 108 | constant = instr.operands[0] 109 | if isinstance(constant, String): 110 | str = constant.string.value 111 | 112 | if "multiplayer.disconnect.outdated_client" in str: 113 | versions["protocol"] = version 114 | looking_for_version_name = True 115 | continue 116 | elif looking_for_version_name: 117 | versions["name"] = str 118 | versions["id"] = versions["name"] 119 | return 120 | elif "Outdated server!" in str: 121 | versions["protocol"] = version 122 | versions["name"] = \ 123 | str[len("Outdated server! I'm still on "):] 124 | versions["id"] = versions["name"] 125 | return 126 | elif verbose: 127 | print("Unable to determine protocol version") 128 | 129 | @staticmethod 130 | def get_data_version(aggregate, classloader, verbose): 131 | if "anvilchunkloader" in aggregate["classes"]: 132 | anvilchunkloader = aggregate["classes"]["anvilchunkloader"] 133 | cf = classloader[anvilchunkloader] 134 | 135 | for method in cf.methods: 136 | can_be_correct = True 137 | for ins in method.code.disassemble(): 138 | if ins in ("ldc", "ldc_w"): 139 | const = ins.operands[0] 140 | if isinstance(const, String) and const == "hasLegacyStructureData": 141 | # In 18w21a+, there are two places that reference DataVersion, 142 | # one which is querying it and one which is saving it. 143 | # We don't want the one that's querying it; 144 | # if "hasLegacyStructureData" is present then we're in the 145 | # querying one so break and try the next method 146 | can_be_correct = False 147 | break 148 | 149 | if not can_be_correct: 150 | continue 151 | 152 | next_ins_is_version = False 153 | found_version = None 154 | for ins in method.code.disassemble(): 155 | if ins in ("ldc", "ldc_w"): 156 | const = ins.operands[0] 157 | if isinstance(const, String) and const == "DataVersion": 158 | next_ins_is_version = True 159 | elif isinstance(const, Integer): 160 | if next_ins_is_version: 161 | found_version = const.value 162 | break 163 | elif not next_ins_is_version: 164 | pass 165 | elif ins in ("bipush", "sipush"): 166 | found_version = ins.operands[0].value 167 | break 168 | 169 | if found_version is not None: 170 | aggregate["version"]["data"] = found_version 171 | break 172 | elif verbose: 173 | print("Unable to determine data version") 174 | -------------------------------------------------------------------------------- /burger/website.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | """ 4 | Copyright (c) 2011 Tyler Kenendy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | import os 25 | import six.moves.urllib.request 26 | 27 | try: 28 | import json 29 | except ImportError: 30 | import simplejson as json 31 | 32 | VERSION_MANIFEST = "https://launchermeta.mojang.com/mc/game/version_manifest.json" 33 | LEGACY_VERSION_META = "https://s3.amazonaws.com/Minecraft.Download/versions/%(version)s/%(version)s.json" # DEPRECATED 34 | 35 | _cached_version_manifest = None 36 | _cached_version_metas = {} 37 | 38 | def _load_json(url): 39 | stream = six.moves.urllib.request.urlopen(url) 40 | try: 41 | return json.load(stream) 42 | finally: 43 | stream.close() 44 | 45 | def get_version_manifest(): 46 | global _cached_version_manifest 47 | if _cached_version_manifest: 48 | return _cached_version_manifest 49 | 50 | _cached_version_manifest = _load_json(VERSION_MANIFEST) 51 | return _cached_version_manifest 52 | 53 | def get_version_meta(version, verbose): 54 | """ 55 | Gets a version JSON file, first attempting the to use the version manifest 56 | and then falling back to the legacy site if that fails. 57 | Note that the main manifest should include all versions as of august 2018. 58 | """ 59 | if version == "20w14~": 60 | # April fools snapshot, labeled 20w14~ ingame but 20w14infinite in the launcher 61 | version = "20w14infinite" 62 | 63 | if version in _cached_version_metas: 64 | return _cached_version_metas[version] 65 | 66 | version_manifest = get_version_manifest() 67 | for version_info in version_manifest["versions"]: 68 | if version_info["id"] == version: 69 | address = version_info["url"] 70 | break 71 | else: 72 | if verbose: 73 | print("Failed to find %s in the main version manifest; using legacy site" % version) 74 | address = LEGACY_VERSION_META % {'version': version} 75 | if verbose: 76 | print("Loading version manifest for %s from %s" % (version, address)) 77 | meta = _load_json(address) 78 | 79 | _cached_version_metas[version] = meta 80 | return meta 81 | 82 | def get_asset_index(version_meta, verbose): 83 | """Downloads the Minecraft asset index""" 84 | if "assetIndex" not in version_meta: 85 | raise Exception("No asset index defined in the version meta") 86 | asset_index = version_meta["assetIndex"] 87 | if verbose: 88 | print("Assets: id %(id)s, url %(url)s" % asset_index) 89 | return _load_json(asset_index["url"]) 90 | 91 | 92 | def client_jar(version, verbose): 93 | """Downloads a specific version, by name""" 94 | filename = version + ".jar" 95 | if not os.path.exists(filename): 96 | meta = get_version_meta(version, verbose) 97 | if verbose: 98 | print("For version %s, the downloads section of the meta is %s" % (filename, meta["downloads"])) 99 | url = meta["downloads"]["client"]["url"] 100 | if verbose: 101 | print("Downloading %s from %s" % (version, url)) 102 | six.moves.urllib.request.urlretrieve(url, filename=filename) 103 | return filename 104 | 105 | def latest_client_jar(verbose): 106 | manifest = get_version_manifest() 107 | return client_jar(manifest["latest"]["snapshot"], verbose) 108 | -------------------------------------------------------------------------------- /munch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | """ 4 | Copyright (c) 2011 Tyler Kenendy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | import os 25 | import sys 26 | import getopt 27 | import urllib 28 | import traceback 29 | 30 | try: 31 | import json 32 | except ImportError: 33 | import simplejson as json 34 | 35 | from collections import deque 36 | 37 | from jawa.classloader import ClassLoader 38 | from jawa.transforms import simple_swap, expand_constants 39 | 40 | from burger import website 41 | from burger.roundedfloats import transform_floats 42 | 43 | 44 | def import_toppings(): 45 | """ 46 | Attempts to imports either a list of toppings or, if none were 47 | given, attempts to load all available toppings. 48 | """ 49 | this_dir = os.path.dirname(__file__) 50 | toppings_dir = os.path.join(this_dir, "burger", "toppings") 51 | from_list = [] 52 | 53 | # Traverse the toppings directory and import everything. 54 | for root, dirs, files in os.walk(toppings_dir): 55 | for file_ in files: 56 | if not file_.endswith(".py"): 57 | continue 58 | elif file_.startswith("__"): 59 | continue 60 | elif file_ == "topping.py": 61 | continue 62 | 63 | from_list.append(file_[:-3]) 64 | 65 | from burger.toppings.topping import Topping 66 | toppings = {} 67 | last = Topping.__subclasses__() 68 | 69 | for topping in from_list: 70 | __import__("burger.toppings.%s" % topping) 71 | current = Topping.__subclasses__() 72 | subclasses = list([o for o in current if o not in last]) 73 | last = Topping.__subclasses__() 74 | if len(subclasses) == 0: 75 | print("Topping '%s' contains no topping" % topping) 76 | elif len(subclasses) >= 2: 77 | print("Topping '%s' contains more than one topping" % topping) 78 | else: 79 | toppings[topping] = subclasses[0] 80 | 81 | return toppings 82 | 83 | if __name__ == "__main__": 84 | try: 85 | opts, args = getopt.gnu_getopt( 86 | sys.argv[1:], 87 | "t:o:vd:Dlc", 88 | [ 89 | "toppings=", 90 | "output=", 91 | "verbose", 92 | "download=", 93 | "download-latest", 94 | "list", 95 | "compact", 96 | "url=" 97 | ] 98 | ) 99 | except getopt.GetoptError as err: 100 | print(str(err)) 101 | sys.exit(1) 102 | 103 | # Default options 104 | toppings = None 105 | output = sys.stdout 106 | verbose = False 107 | download_jars = [] 108 | download_latest = False 109 | list_toppings = False 110 | compact = False 111 | url = None 112 | 113 | for o, a in opts: 114 | if o in ("-t", "--toppings"): 115 | toppings = a.split(",") 116 | elif o in ("-o", "--output"): 117 | output = open(a, "w") 118 | elif o in ("-v", "--verbose"): 119 | verbose = True 120 | elif o in ("-c", "--compact"): 121 | compact = True 122 | elif o in ("-d", "--download"): 123 | download_jars.append(a) 124 | elif o in ("-D", "--download-latest"): 125 | download_latest = True 126 | elif o in ("-l", "--list"): 127 | list_toppings = True 128 | elif o in ("-s", "--url"): 129 | url = a 130 | 131 | # Load all toppings 132 | all_toppings = import_toppings() 133 | 134 | # List all of the available toppings, 135 | # as well as their docstring if available. 136 | if list_toppings: 137 | for topping in all_toppings: 138 | print("%s" % topping) 139 | if all_toppings[topping].__doc__: 140 | print(" -- %s\n" % all_toppings[topping].__doc__) 141 | sys.exit(0) 142 | 143 | # Get the toppings we want 144 | if toppings is None: 145 | loaded_toppings = all_toppings.values() 146 | else: 147 | loaded_toppings = [] 148 | for topping in toppings: 149 | if topping not in all_toppings: 150 | print("Topping '%s' doesn't exist" % topping) 151 | else: 152 | loaded_toppings.append(all_toppings[topping]) 153 | 154 | class DependencyNode: 155 | def __init__(self, topping): 156 | self.topping = topping 157 | self.provides = topping.PROVIDES 158 | self.depends = topping.DEPENDS 159 | self.childs = [] 160 | 161 | def __repr__(self): 162 | return str(self.topping) 163 | 164 | # Order topping execution by building dependency tree 165 | topping_nodes = [] 166 | topping_provides = {} 167 | for topping in loaded_toppings: 168 | topping_node = DependencyNode(topping) 169 | topping_nodes.append(topping_node) 170 | for provides in topping_node.provides: 171 | topping_provides[provides] = topping_node 172 | 173 | # Include missing dependencies 174 | for topping in topping_nodes: 175 | for dependency in topping.depends: 176 | if not dependency in topping_provides: 177 | for other_topping in all_toppings.values(): 178 | if dependency in other_topping.PROVIDES: 179 | topping_node = DependencyNode(other_topping) 180 | topping_nodes.append(topping_node) 181 | for provides in topping_node.provides: 182 | topping_provides[provides] = topping_node 183 | 184 | # Find dependency childs 185 | for topping in topping_nodes: 186 | for dependency in topping.depends: 187 | if not dependency in topping_provides: 188 | print("(%s) requires (%s)" % (topping, dependency)) 189 | sys.exit(1) 190 | if not topping_provides[dependency] in topping.childs: 191 | topping.childs.append(topping_provides[dependency]) 192 | 193 | # Run leaves first 194 | to_be_run = [] 195 | while len(topping_nodes) > 0: 196 | stuck = True 197 | for topping in topping_nodes: 198 | if len(topping.childs) == 0: 199 | stuck = False 200 | for parent in topping_nodes: 201 | if topping in parent.childs: 202 | parent.childs.remove(topping) 203 | to_be_run.append(topping.topping) 204 | topping_nodes.remove(topping) 205 | if stuck: 206 | print("Can't resolve dependencies") 207 | sys.exit(1) 208 | 209 | jarlist = args 210 | 211 | # Download any jars that have already been specified 212 | for version in download_jars: 213 | client_path = website.client_jar(version, verbose) 214 | jarlist.append(client_path) 215 | 216 | # Download a copy of the latest snapshot jar 217 | if download_latest: 218 | client_path = website.latest_client_jar(verbose) 219 | jarlist.append(client_path) 220 | 221 | # Download a JAR from the given URL 222 | if url: 223 | url_path = urllib.urlretrieve(url)[0] 224 | jarlist.append(url_path) 225 | 226 | summary = [] 227 | 228 | for path in jarlist: 229 | classloader = ClassLoader(path, max_cache=0, bytecode_transforms=[simple_swap, expand_constants]) 230 | names = classloader.path_map.keys() 231 | num_classes = sum(1 for name in names if name.endswith(".class")) 232 | 233 | aggregate = { 234 | "source": { 235 | "file": path, 236 | "classes": num_classes, 237 | "other": len(names), 238 | "size": os.path.getsize(path) 239 | } 240 | } 241 | 242 | available = [] 243 | for topping in to_be_run: 244 | missing = [dep for dep in topping.DEPENDS if dep not in available] 245 | if len(missing) != 0: 246 | if verbose: 247 | print("Dependencies failed for %s: Missing %s" % (topping, missing)) 248 | continue 249 | 250 | orig_aggregate = aggregate.copy() 251 | try: 252 | topping.act(aggregate, classloader, verbose) 253 | available.extend(topping.PROVIDES) 254 | except: 255 | aggregate = orig_aggregate # If the topping failed, don't leave things in an incomplete state 256 | if verbose: 257 | print("Failed to run %s" % topping) 258 | traceback.print_exc() 259 | 260 | summary.append(aggregate) 261 | 262 | if not compact: 263 | json.dump(transform_floats(summary), output, sort_keys=True, indent=4) 264 | else: 265 | json.dump(transform_floats(summary), output) 266 | 267 | # Cleanup temporary downloads (the URL download is temporary) 268 | if url: 269 | os.remove(url_path) 270 | # Cleanup file output (if used) 271 | if output is not sys.stdout: 272 | output.close() 273 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | from setuptools import setup, find_packages 4 | 5 | setup( 6 | name="Burger", 7 | packages=find_packages(), 8 | version="0.3.0", 9 | description="Extract information from Minecraft bytecode.", 10 | author="Tyler Kennedy", 11 | author_email="tk@tkte.ch", 12 | url="http://github.com/mcdevs/Burger", 13 | keywords=["java", "minecraft"], 14 | install_requires=[ 15 | 'six>=1.4.0', 16 | 'Jawa>=2.2.0,<3' 17 | ], 18 | classifiers=[ 19 | "Programming Language :: Python", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent", 22 | "Development Status :: 3 - Alpha", 23 | "Intended Audience :: Developers", 24 | "Topic :: Software Development :: Disassemblers" 25 | ] 26 | ) 27 | --------------------------------------------------------------------------------