Advertisement
Kitomas

_parse_scene.py as of 2024-05-16

May 17th, 2024
481
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 13.75 KB | None | 0 0
  1. if __name__ == "__main__":
  2.     print("_parse_scene.py should be used as a module only!")
  3.     exit(0)
  4.    
  5. _DEBUG = False
  6. _PRINT_LAYERS = False #_DEBUG must be True as well
  7.  
  8. from os.path import isfile, exists
  9. from pprint import pprint
  10. import xml.etree.ElementTree as ET
  11.  
  12.  
  13.  
  14.  
  15. obj_data_types = {
  16.      "u8",  "s8",
  17.     "u16", "s16",
  18.     "u24", "s24",
  19.     "u32", "s32",
  20.     "u64", "s64",
  21.     "f32", "f64",
  22.     "rgb", "argb"
  23. }
  24.  
  25.  
  26.  
  27.  
  28. def printError(s, fatal=True):
  29.     if _DEBUG or fatal: print("Error: {}".format(s))
  30.     if fatal: exit(-1)
  31.     else    : return False
  32.  
  33. def sceneIdToPath(scene_id):
  34.     return "../scene_{}.tmx".format(scene_id)
  35.    
  36. #poor man's regex lol
  37. def extractTilesetName(path):
  38.     #will turn something like "../foo/tileset_collision.tsx" into "collision"
  39.     name = path.split("/")[-1]
  40.     return (("_").join(name.split("."))).split("_")[1]
  41.    
  42. def tryInt(string):
  43.     try:
  44.         return int(string)
  45.     except ValueError:
  46.         return None
  47.     except TypeError:
  48.         return None
  49.        
  50. def tryDict(dict, key):
  51.     try:
  52.         return dict[key]
  53.     except KeyError:
  54.         return None
  55.  
  56. def keyWarn(properties, name, fatal=False, cat="property"):
  57.     if tryDict(properties, name) == None:
  58.         #intentionally indented twice
  59.         if    fatal: print("  Error: {} \"{}\" doesn't exist!".format(cat, name)); exit(-1)
  60.         elif _DEBUG: print("  Warning: {} \"{}\" doesn't exist!".format(cat, name))
  61.         return True #key does not exist
  62.     else:
  63.         return False #key exists
  64.  
  65. #value is a hex quad string of "#AARRGGBB"
  66. def toRGBA(value):
  67.     alpha = int(value[1:3], 16)
  68.     red   = int(value[3:5], 16)
  69.     blue  = int(value[5:7], 16)
  70.     green = int(value[7:9], 16)
  71.     return (red, green, blue, alpha)
  72.  
  73. #dataValue should always be of type str
  74. def convertType(dataType, dataValue):
  75.     #(i wish python had switch statements...)
  76.     if   dataType == "int"    : return int(float(dataValue))
  77.     elif dataType == "bool"   : return eval(dataValue.capitalize())
  78.     elif dataType == "float"  : return float(dataValue)
  79.     elif dataType == "color"  : return toRGBA(dataValue)
  80.     elif dataType == "object" : return int(float(dataValue))
  81.     #elif dataType == 'file'  : return dataValue #(redundant)
  82.     else                      : return dataValue
  83.  
  84. def tryTypeWarn(type_str):
  85.     type_num = tryInt(type_str.split("_")[0])
  86.     if type_num != None:
  87.         return type_num
  88.     else:
  89.         if _DEBUG: print("Warning: type \"{}\" is invalid".format(type_str))
  90.         return 0
  91.        
  92. #(mostly) yoinked from stackoverflow lol
  93. def checkForDuplicateIndexes(thelist):
  94.   seen = set()
  95.   for x in thelist:
  96.     if x[0][0] in seen:
  97.         printError("found duplicate obj data index \"{}\"".format(x[0][0]))
  98.     seen.add(x[0][0])
  99.  
  100.  
  101.  
  102.  
  103. def parseObject(obj_in):
  104.     obj_out = { "data":[] }
  105.     props_a = obj_in.attrib
  106.     op = "object property"
  107.    
  108.     if _DEBUG: print(" parsing object \"{}\"...".format(tryDict(props_a, "name") or "NAMELESS"))
  109.     if keyWarn(props_a, "name"  , cat=op): obj_out["name"  ] = "NAMELESS"
  110.     else                                 : obj_out["name"  ] = props_a["name"]
  111.     if keyWarn(props_a, "x"     , cat=op): obj_out["x"     ] = 0
  112.     else                                 : obj_out["x"     ] = convertType("int", props_a["x"])
  113.     if keyWarn(props_a, "y"     , cat=op): obj_out["y"     ] = 0
  114.     else                                 : obj_out["y"     ] = convertType("int", props_a["y"])
  115.     if keyWarn(props_a, "width" , cat=op): obj_out["width" ] = 0
  116.     else                                 : obj_out["width" ] = convertType("int", props_a["width"])
  117.     if keyWarn(props_a, "height", cat=op): obj_out["height"] = 0
  118.     else                                 : obj_out["height"] = convertType("int", props_a["height"])
  119.     if keyWarn(props_a, "type"  , cat=op): obj_out["type"  ] = 0
  120.     else                                 : obj_out["type"  ] = tryTypeWarn(props_a["type"])
  121.    
  122.     #this should occur whether or not _DEBUG is True
  123.     if obj_out["type"] == 0: print("  Warning: object \"{}\"'s type is equal to 0".format(obj_out["name"]))
  124.    
  125.     #hitbox offset is not an intrinsic object property,
  126.      #so they need to be specified with a custom property
  127.     obj_out["hb_offset_x"] = 0
  128.     obj_out["hb_offset_y"] = 0
  129.  
  130.  
  131.     _props_b = obj_in.find("properties")
  132.     props_b  = []
  133.    
  134.     if _props_b != None:
  135.         for prop in _props_b.findall("property"):
  136.             pName  = prop.attrib["name" ].split("_")
  137.             pType  = prop.attrib["type" ]
  138.             pValue = prop.attrib["value"]
  139.             if len(pName) < 2: printError("\"{}\" isn't a valid obj data property name".format(pName[0]))
  140.             pIndex = tryInt(pName[0])
  141.             if pIndex == None: printError("\"{}\" has an invalid obj data property index".format(pName[0]))
  142.             pName[0] = pIndex
  143.             props_b.append(  (pName, convertType(pType, pValue))  )
  144.    
  145.         checkForDuplicateIndexes(props_b) #properties can't share the same index
  146.         props_b = sorted(props_b,  key = lambda x: x[0][0]  ) #sort by their indexes
  147.    
  148.     else:
  149.         return obj_out
  150.    
  151.    
  152.     data = []
  153.     for prop in props_b:
  154.         pType  = prop[0][1].lower()
  155.         pName  = ("_").join(prop[0][2:])
  156.         pValue = prop[1]
  157.         if pType not in obj_data_types:
  158.             printError("\"{}\" is not a valid obj data type".format(pType))
  159.         if pName[0:10] == "hb_offset_":
  160.             if pType != "u8":
  161.                 printError("hb_offset_<x/y> is not of type u8")
  162.             if pName != "hb_offset_x"  and  pName != "hb_offset_y":
  163.                 printError("malformed offset name \"{}\"".format(pName))
  164.             obj_out[pName] = pValue
  165.         else:
  166.             data.append(  (pType, pValue, pName)  )
  167.    
  168.    
  169.     obj_out["data"] = data
  170.  
  171.     return obj_out
  172.  
  173.  
  174.  
  175.  
  176. def printLayer(layers, name):
  177.     if tryDict(layers, name) == None: return
  178.     count = 0
  179.     print("  {}: [".format(name), end="")
  180.     for tile in layers[name]:
  181.         if count%32 == 0: print("\n    ", end="")
  182.         print(str(tile).rjust(3), end=",")
  183.         count += 1
  184.     print("\n  ],")
  185.    
  186. def printScene(scene, printLayers=_PRINT_LAYERS):
  187.     print("  --PROPERTIES--:")
  188.     pprint(scene["properties"], indent=4)
  189.    
  190.     if printLayers:
  191.         print("  --LAYERS--")
  192.         printLayer(scene["layers"], "collision")
  193.         printLayer(scene["layers"], "fg"       )
  194.         printLayer(scene["layers"], "mg"       )
  195.        
  196.     print("  --OBJECTS--")
  197.     pprint(scene["objs"])
  198.  
  199. def parseSceneMap(scene_id, announce=True):
  200.     filename = sceneIdToPath(scene_id)
  201.     if not exists(filename): printError("scene \"{}\" does not exist".format(filename))
  202.     if not isfile(filename): printError("scene \"{}\" is not a file".format(filename))
  203.     if _DEBUG or announce: print("PARSING \"{}\":".format(filename))
  204.     tree = ET.parse(filename)
  205.     map  = tree.getroot()
  206.    
  207.     #get map's intrinsic properties
  208.     width       = int(map.attrib["width"     ]) #should be 32
  209.     height      = int(map.attrib["height"    ]) #should be 18
  210.     tileWidth  = int(map.attrib["tilewidth" ]) #should be 24 (unused)
  211.     tileHeight = int(map.attrib["tileheight"]) #should be 24 (unused)
  212.     mapLength   = width*height
  213.    
  214.     if width      != 32: printError("map width is not 32")
  215.     if height     != 18: printError("map height is not 18")
  216.     if tileWidth  != 24: printError("tile width is not 24")
  217.     if tileHeight != 24: printError("tile height is not 24")
  218.    
  219.    
  220.    
  221.     #get map's custom properties
  222.     props = {}
  223.     for prop in map.find("properties"):
  224.         pName  = tryDict(prop.attrib, "name" ) or "NAMELESS"
  225.         pType  = tryDict(prop.attrib, "type" )
  226.         pValue = tryDict(prop.attrib, "value") or "NOVALUE"
  227.         props[ pName ] = convertType(pType, pValue)
  228.  
  229.     if  keyWarn(props, "bmp_bg"    ):  props["bmp_bg"    ] = 0
  230.     if  keyWarn(props, "repeat_bg" ):  props["repeat_bg" ] = False #lol
  231.     #if keyWarn(props, "objs_len"  ):  props["objs_len"  ] = 0 #(calculated later)
  232.     #if keyWarn(props, "tileset_a" ):  props["tileset_a" ] = 0 #(calculated later)
  233.     #if keyWarn(props, "tileset_b" ):  props["tileset_b" ] = 0 #(calculated later)
  234.     if  keyWarn(props, "edge_n"    ):  props["edge_n"    ] = 0
  235.     if  keyWarn(props, "edge_s"    ):  props["edge_s"    ] = 0
  236.     if  keyWarn(props, "edge_w"    ):  props["edge_w"    ] = 0
  237.     if  keyWarn(props, "edge_e"    ):  props["edge_e"    ] = 0
  238.     #if keyWarn(props, "scene"     ):  props["scene"     ] = 0 #(calculated later)
  239.     if  keyWarn(props, "music"     ):  props["music"     ] = 0
  240.     if  keyWarn(props, "ambience_a"):  props["ambience_a"] = 0
  241.     if  keyWarn(props, "ambience_b"):  props["ambience_b"] = 0
  242.    
  243.    
  244.    
  245.     #calculate tileset boundaries
  246.     _tsets = []
  247.     for tset in map.findall("tileset"):
  248.         tFirstGID = int(tryDict(tset.attrib, "firstgid")) - 1
  249.         tName     = extractTilesetName(tryDict(tset.attrib, "source"))
  250.         _tsets.append(  ( tFirstGID, tryInt(tName) or 0 )  )
  251.     tsets = sorted(_tsets,  key = lambda x: x[0]  ) #sorted by first element
  252.    
  253.     if   len(tsets) < 3: printError("map cannot have less than 3 tilesets (including collision)")
  254.     elif len(tsets) > 3: printError("map cannot have more than 3 tilesets (including collision)")
  255.        
  256.      #there should only be 1 tileset that registers as null (the collision's tileset)
  257.     tset_offsets = [0,] * 3
  258.     tset_nulls, tset_valids  =  0, 0
  259.     tset_a,     tset_b       =  0, 0
  260.     for i in range(len(tsets)):
  261.         #collision tileset
  262.         if    tsets[i][1] == 0:  tset_offsets[i] = -1;  tset_nulls  += 1
  263.         #tileset_a
  264.         elif  tset_valids == 0:  tset_a = tsets[i][1];  tset_valids += 1
  265.         #tileset_b
  266.         else                  :  tset_b = tsets[i][1];  tset_offsets[i] = 128
  267.    
  268.     if tset_nulls != 1: printError("map must have exactly 1 null tileset, reserved for collider map")
  269.  
  270.    
  271.    
  272.     #get map's layer data
  273.     layers = {}
  274.     for layer in map.findall("layer"):
  275.         lName   =      tryDict(layer.attrib,"name"  ) or "NAMELESS"
  276.         lWidth  = int( tryDict(layer.attrib,"width" ) or 0 )
  277.         lHeight = int( tryDict(layer.attrib,"height") or 0 )
  278.         lLength = lWidth*lHeight
  279.         # n-1 to make sure both tile 0 and 1 are treated as transparent
  280.         lData   = [ max(int(n)-1, 0) for n in layer.find("data").text.split(",") ] #csv to list
  281.         if lLength != mapLength : valid = printError("layer dims inconsistent with map dimensions")
  282.         if lLength != len(lData): valid = printError("layer dims inconsistent with its attributes")
  283.         layers[ lName ] = lData
  284.    
  285.     valid = True
  286.     if "collision" not in layers: valid = printError("layer \"collision\" doesn't exist", quit=False)
  287.     if "fg"        not in layers: valid = printError("layer \"fg\" doesn't exist",        quit=False)
  288.     if "mg"        not in layers: valid = printError("layer \"mg\" doesn't exist",        quit=False)
  289.     if not valid: exit(-1)
  290.    
  291.     for i in range(len(layers["collision"])):
  292.         #collider info is 6 bits (2^6 = 64),
  293.          #which is a factor of a normal tileset's 7 (2^7 = 128) anyway
  294.         layers["collision"][i] %= 64
  295.        
  296.     for i in range(len(layers["fg"])): #(fg & mg should be the same length)
  297.         #fg
  298.         tile = layers["fg"][i]
  299.         offset = tset_offsets[tile//128] #tile//128 should be between 0 -> 2
  300.         if offset == -1  and  (tile%128) != 0:
  301.             printError("fg cannot contain collision map tiles")
  302.         layers["fg"][i] = (tile%128) + max(offset, 0)
  303.         if layers["fg"][i] == 128: layers["fg"][i] = 0 #128 is also transparent
  304.        
  305.         #mg
  306.         tile = layers["mg"][i]
  307.         offset = tset_offsets[tile//128]
  308.         if offset == -1  and  (tile%128) != 0:
  309.             printError("mg cannot contain collision map tiles")
  310.         layers["mg"][i] = (tile%128) + max(offset, 0)
  311.         if layers["mg"][i] == 128: layers["mg"][i] = 0 #128 is also transparent
  312.        
  313.     #check if a given layer's data should be omitted from scene descriptor file
  314.     fg_nonzero = False
  315.     mg_nonzero = False
  316.     for i in range(len(layers["fg"])): #(fg & mg should be the same length)
  317.         if (layers["fg"][i]%128) != 0: fg_nonzero = True
  318.         if (layers["mg"][i]%128) != 0: mg_nonzero = True
  319.        
  320.     if not fg_nonzero  and  not fg_nonzero:
  321.         printError("fg and mg cannot both be empty")
  322.    
  323.    
  324.    
  325.     props["objs_len"  ] = 0
  326.     props["tileset_a" ] = tset_a
  327.     props["tileset_b" ] = tset_b
  328.     props["scene"     ] = scene_id
  329.     props["fg_nonzero"] = fg_nonzero
  330.     props["mg_nonzero"] = mg_nonzero
  331.    
  332.     #get scene objects
  333.     obj_groups = map.findall("objectgroup")
  334.     objs = []
  335.     if len(obj_groups) >  1: printError("scene cannot have more than 1 object group")
  336.     if len(obj_groups) == 1:
  337.         #(this loop will not run at all if there are no objects)
  338.         for obj in obj_groups[0].findall("object"):
  339.             props["objs_len" ] += 1
  340.             objs.append(parseObject(obj))
  341.  
  342.  
  343.  
  344.     #automatically mark edge as a loop if that edge is null
  345.     if props["edge_n"] == 0: props["edge_n"] = scene_id
  346.     if props["edge_s"] == 0: props["edge_s"] = scene_id
  347.     if props["edge_w"] == 0: props["edge_w"] = scene_id
  348.     if props["edge_e"] == 0: props["edge_e"] = scene_id
  349.    
  350.     #error if all 4 edges make the scene loop
  351.     edges = (scene_id, props["edge_n"], props["edge_s"],
  352.                        props["edge_w"], props["edge_e"])
  353.     if all(i==edges[0] for i in edges):
  354.         printError("all 4 edges make scene loop")
  355.  
  356.  
  357.     scene = {}
  358.     scene["properties"] = props
  359.     scene["layers"    ] = layers
  360.     scene["objs"      ] = objs
  361.    
  362.     if _DEBUG: printScene(scene)
  363.    
  364.     return scene
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement