# file defining the structures and types # going to be used to display BCSV data in blender # add descriptions/names to all properties declared import bpy, bpy_extras, bpy_types import sys, random, io from . import blender_funcs from . import bcsv_funcs from . import file_ops # https://blenderartists.org/t/property-speed/606970/8 # Blender's properties are slow to access/modify, # need a faster approach to store the BCSV table # # - Seems like I must store the BCSV table serialized in a custom # property as a bytes object (can be saved on the BLEND file). # - "Open" the stored table (deserialize it) and store the # resulting structure in a defined object property. # - Pull a section of the table data into a Blender's # properties structure to be able to modify it through UI. # - On UI modifications, store the updated values into the deserialized structure. # - Serialize ("Save") structure into the custom property. This save option will have to # be triggered manually by the user (inneficient if this happens for every little modification). # When saving the BLEND file a "Save" will be triggered on all "open" BCSV tables. # - "Close" option to release the memory used by the deserialized structure. # - Export/Import will also be defined. # - A undo/redo buffer for each table will also be defined # constants DEFAULT_COL_COUNT = 7 DEFAULT_ROW_COUNT = 10 DEFAULT_VISIBLE_ROWS = 15 DEFAULT_VISIBLE_COLS = 6 # global variables ALLOW_UPDATE_CALLBACKS = True # set the properties area to be redrawn def redraw_properties_area(context): for area in context.screen.areas: if (area.type == "PROPERTIES"): area.tag_redraw() # called when the type value of a col_info is changed def cols_info_type_interface_upd(self, context): global ALLOW_UPDATE_CALLBACKS if (ALLOW_UPDATE_CALLBACKS == False): return table = context.object.smg_bcsv.get_table_from_address() interf = context.object.smg_bcsv.interface # type does not need to be checked but the rows_data # columns will need to be updated on type change interf_col_index = None # get the column index whose type was changed for i in range(interf.visible_number_of_cols): if (self == interf.cols_info[i]): interf_col_index = i break # convert the interface col index to the real table column index table_col_index = interf.col_slider + interf_col_index # in the table check if all the elements on the column are good # if one is not good, reset the whole column values # LONG, LONG_2, SHORT, CHAR python_type = int; def_value = 0 if (self.type == "FLOAT"): python_type = float; def_value = 0.0 elif (self.type in ["STRING", "STRING_OFFSET"]): python_type = str; def_value = "default" for i in range(table.row_count): if (type(table.rows_data[i][table_col_index]) != python_type): for j in range(table.row_count): table.rows_data[j][table_col_index] = def_value break # change the table col_info type manually table.cols_info[table_col_index].type = self.type # load the table section load_section_table(context) # update the col_info values when modified def cols_info_data_interface_upd(self, context): global ALLOW_UPDATE_CALLBACKS if (ALLOW_UPDATE_CALLBACKS == False): return interf = context.object.smg_bcsv.interface # ensure the correct values are stored in the structure # name or hash try: self.name_or_hash.encode("cp932") if (self.name_or_hash.lower().startswith("0x")): try: num = int(self.name_or_hash, 16) if (num < 0): col_info["name_or_hash"] = "0x00000000" elif (num > 0xFFFFFFFF): col_info["name_or_hash"] = "0xFFFFFFFF" except: blender_funcs.disp_msg("Hash \"%s\" cannot be interpreted as hex string" % self.name_or_hash[ : 9]) except: blender_funcs.disp_msg("Name or hash \"%s\" is not CP932 encodable" % self.name_or_hash[ : 9]) col_info["name_or_hash"] = "default" # bitmask try: int(self.bitmask, 16) if (self.bitmask.lower().startswith("0x")): col_info["bitmask"] = self.bitmask[2 : ].upper() except: blender_funcs.disp_msg("Bitmask \"%s\" cannot be interpreted as hex string" % self.bitmask) col_info["bitmask"] = "FFFFFFFF" # rshift does not need to be checked # type will be checked with another function # load the changes into the bcsv table apply_interf_to_table(context) bpy.ops.ed.undo_push() # interface for cols_info data class smg_cols_info_interface(bpy.types.PropertyGroup): name_or_hash = bpy.props.StringProperty( name = "Name or hash", description = "BCSVs hash their column name identifier. If the name is known, it will be printed, if not, the hash value (as a string) will be displayed", default = "default", update = cols_info_data_interface_upd ) bitmask = bpy.props.StringProperty( name = "Bitmask", description = "Bitmask used when pulling the encoded column data from a BCSV. After this bitmask, a right shift is done.", default = "FFFFFFFF", maxlen = 8, update = cols_info_data_interface_upd ) rshift = bpy.props.IntProperty( name = "Right shift", description = "Right shift used after bitmasking the encoded value from a BCSV. The value after the right shift will be the actual value wanted to be stored", min = 0, max = 255, default = 0, update = cols_info_data_interface_upd ) type = bpy.props.EnumProperty( name = "Data type", description = "BCSV column data type", items = ( ("LONG", "Long", "Long integer type (4 bytes long)."), ("STRING", "String", "String type (32 bytes long)."), ("FLOAT", "Float", "Float type (4 bytes long)"), ("LONG_2", "Long 2", "Long 2 integer type (4 bytes long)."), ("SHORT", "Short", "Short integer type (2 bytes long)."), ("CHAR", "Char", "Char integer type (1 byte long)."), ("STRING_OFFSET", "String offset", "String offset type (4 bytes long)."), ), update = cols_info_type_interface_upd ) # update string/string_offset values when modified def rows_data_cell_interface_upd(self, context): global ALLOW_UPDATE_CALLBACKS if (ALLOW_UPDATE_CALLBACKS == False): return # STRING/STRING_OFFSET must be CP932 encodable # STRING encoded size must be 32 bytes or less try: enc = self.string.encode("CP932") if (len(enc) >= 32): blender_funcs.disp_msg("STRING type \"%s\" encoded type is larger than 31 bytes" % (self.string[ : 9])) self["string"] = "default" except: blender_funcs.disp_msg("STRING type \"%s\" is not CP932 encodable" % (self.string[ : 9])) self["string"] = "default" try: self.string_offset.encode("CP932") except: blender_funcs.disp_msg("STRING_OFFSET type \"%s\" is not CP932 encodable" % (self.string_offset[ : 9])) self["string_offset"] = "default" # load the changes into the bcsv table apply_interf_to_table(context) bpy.ops.ed.undo_push() # interface for rows_data rows class smg_rows_data_cell_interface(bpy.types.PropertyGroup): char = bpy.props.IntProperty( name = "CHAR", description = "Value to be stored in a BCSV table if the column type is a CHAR type", default = 0, min = -128, max = 127, update = rows_data_cell_interface_upd ) short = bpy.props.IntProperty( name = "SHORT", description = "Value to be stored in a BCSV table if the column type is a SHORT type", default = 0, min = -32768, max = 32767, update = rows_data_cell_interface_upd ) long = bpy.props.IntProperty( name = "LONG/LONG_2", description = "Value to be stored in a BCSV table if the column type is a LONG/LONG_2 type", default = 0, min = -2147483648, max = 2147483647, update = rows_data_cell_interface_upd ) float = bpy.props.FloatProperty( name = "FLOAT", description = "Value to be stored in a BCSV table if the column type is a FLOAT type", default = 0.0, update = rows_data_cell_interface_upd ) string = bpy.props.StringProperty( name = "STRING", description = "Value to be stored in a BCSV table if the column type is a STRING type", default = "default", update = rows_data_cell_interface_upd ) string_offset = bpy.props.StringProperty( name = "STRING_OFFSET", description = "Value to be stored in a BCSV table if the column type is a STRING_OFFSET type", default = "default", update = rows_data_cell_interface_upd ) # interface for rows_data class smg_rows_data_interface(bpy.types.PropertyGroup): cells = bpy.props.CollectionProperty( name = "Row cells", description = "The cells in a row of a BCSV table", type = smg_rows_data_cell_interface ) # function to load a section of the table into the table interface structure # assumes the interface data and the bcsv table are compatible def load_section_table(context): # check params interf = context.object.smg_bcsv.interface table = context.object.smg_bcsv.get_table_from_address() # disable some function callbacks global ALLOW_UPDATE_CALLBACKS; ALLOW_UPDATE_CALLBACKS = False # cols_info interf.cols_info.clear() for i in range(interf.col_slider, interf.col_slider + interf.visible_number_of_cols): interf.cols_info.add() interf.cols_info[-1].name_or_hash = table.cols_info[i].name_or_hash interf.cols_info[-1].bitmask = hex(table.cols_info[i].bitmask)[2 : ].upper() interf.cols_info[-1].rshift = table.cols_info[i].rshift interf.cols_info[-1].type = table.cols_info[i].type # rows_data interf.rows_data.clear() for i in range(interf.visible_number_of_rows): interf.rows_data.add() for i in range(interf.col_slider, interf.col_slider + interf.visible_number_of_cols): attr = None if (table.cols_info[i].type in ["LONG", "LONG_2"]): attr = "long" elif (table.cols_info[i].type == "SHORT"): attr = "short" elif (table.cols_info[i].type == "CHAR"): attr = "char" elif (table.cols_info[i].type == "FLOAT"): attr = "float" elif (table.cols_info[i].type == "STRING"): attr = "string" elif (table.cols_info[i].type == "STRING_OFFSET"): attr = "string_offset" for j in range(interf.row_slider, interf.row_slider + interf.visible_number_of_rows): interf.rows_data[j - interf.row_slider].cells.add() setattr(interf.rows_data[j - interf.row_slider].cells[-1], attr, table.rows_data[j][i]) # enable the disabled function callbacks ALLOW_UPDATE_CALLBACKS = True bpy.ops.ed.undo_push() # function to be called manually to apply the bcsv # interface data to the deserialized bcsv data table def apply_interf_to_table(context): # save the section of the table shown in UI into the table in the bcsv table table = context.object.smg_bcsv.get_table_from_address() interf = context.object.smg_bcsv.interface # cols_info for i in range(interf.col_slider, interf.col_slider + interf.visible_number_of_cols): self_index = i - interf.col_slider table.cols_info[i].name_or_hash = interf.cols_info[self_index].name_or_hash table.cols_info[i].bitmask = int(interf.cols_info[self_index].bitmask, 16) table.cols_info[i].rshift = interf.cols_info[self_index].rshift # check if the type changed (need to update the whole table column) if (table.cols_info[i].type != interf.cols_info[self_index].type): def_value = 0 # LONG, LONG_2, SHORT, CHAR if (interf.cols_info[self_index] == "FLOAT"): def_value = 0.0 elif (interf.cols_info[self_index] in ["STRING", "STRING_OFFSET"]): def_value = "default" # update the table column for j in range(table.row_count): table.rows_data[j][i] = def_value # update the type table.cols_info[i].type = interf.cols_info[self_index].type # rows_data (update all UI visible values) for i in range(interf.col_slider, interf.col_slider + interf.visible_number_of_cols): to_eval = "interf.rows_data[%d].cells[%d]" self_col_index = i - interf.col_slider if (interf.cols_info[self_col_index].type in ["LONG", "LONG_2"]): to_eval += ".long" elif (interf.cols_info[self_col_index].type == "SHORT"): to_eval += ".short" elif (interf.cols_info[self_col_index].type == "CHAR"): to_eval += ".char" elif (interf.cols_info[self_col_index].type == "FLOAT"): to_eval += ".float" elif (interf.cols_info[self_col_index].type == "STRING"): to_eval += ".string" elif (interf.cols_info[self_col_index].type == "STRING_OFFSET"): to_eval += ".string_offset" for j in range(interf.row_slider, interf.row_slider + interf.visible_number_of_rows): table.rows_data[j][i] = eval(to_eval % (j - interf.row_slider, self_col_index)) # push an undo bpy.ops.ed.undo_push() # assign a custom property directly or assign the attribute # function to help the function assignments below def try_assign_custom_prop(obj, data_path, value): if (data_path in obj): obj[data_path] = value else: setattr(obj, data_path, value) # sync all the row/col related values from the bcsv interface with the bcsv table def interf_row_col_data_upd(self, context, data_path): global ALLOW_UPDATE_CALLBACKS if (ALLOW_UPDATE_CALLBACKS == False): return # table and interface table = context.object.smg_bcsv.get_table_from_address() interf = context.object.smg_bcsv.interface # decide whether to update or not the BCSV interface update_interf = False # row/column count if (data_path in ["row_count", "col_count", "row_slider", "visible_number_of_rows", "col_slider", "visible_number_of_cols"]): try_assign_custom_prop(self, "row_count", table.row_count) try_assign_custom_prop(self, "col_count", table.col_count) # row slider, visible number of rows if (data_path in ["row_count", "row_slider", "visible_number_of_rows"]): if (self.row_slider + self.visible_number_of_rows > table.row_count): row_slider_tmp = self.row_slider visible_number_of_rows_tmp = self.visible_number_of_rows while (row_slider_tmp + visible_number_of_rows_tmp > table.row_count): if (row_slider_tmp > 0): row_slider_tmp -= 1 elif (visible_number_of_rows_tmp > 0): visible_number_of_rows_tmp -= 1 try_assign_custom_prop(self, "row_slider", row_slider_tmp) try_assign_custom_prop(self, "visible_number_of_rows", visible_number_of_rows_tmp) # interface must update now update_interf = True # column slider, visible number of columns elif (data_path in ["col_count", "col_slider", "visible_number_of_cols"]): if (self.col_slider + self.visible_number_of_cols > table.col_count): col_slider_tmp = self.col_slider visible_number_of_cols_tmp = self.visible_number_of_cols while (col_slider_tmp + visible_number_of_cols_tmp > table.col_count): if (col_slider_tmp > 0): col_slider_tmp -= 1 elif (visible_number_of_cols_tmp > 0): visible_number_of_cols_tmp -= 1 try_assign_custom_prop(self, "col_slider", col_slider_tmp) try_assign_custom_prop(self, "visible_number_of_cols", visible_number_of_cols_tmp) # interface must update now update_interf = True # active row index elif (data_path == "active_row_index"): if (self.active_row_index[0] > table.row_count): self["active_row_index"][0] = table.row_count if (self.active_row_index[1] >= table.row_count): self["active_row_index"][1] = table.row_count - 1 # active column index elif (data_path == "active_col_index"): if (self.active_col_index[0] > table.col_count): self["active_col_index"][0] = table.col_count if (self.active_col_index[1] >= table.col_count): self["active_col_index"][1] = table.col_count - 1 # update the bcsv interface? if (update_interf): load_section_table(context) bpy.ops.ed.undo_push() # structure to serve as an interface to the actual bcsv table data stored in a custom property class smg_bcsv_table_interface(bpy.types.PropertyGroup): # smg bcsv table interface row_count = bpy.props.IntProperty( name = "Number of rows", description = "Number of rows in the BCSV table", min = 0, default = 0, update = (lambda self, context: interf_row_col_data_upd(self, context, "row_count")) ) col_count = bpy.props.IntProperty( name = "Number of columns", description = "Number of columns in the BCSV table", min = 0, default = 0, update = (lambda self, context: interf_row_col_data_upd(self, context, "col_count")) ) cols_info = bpy.props.CollectionProperty( name = "Columns information", description = "Array with the variables that characterize the columns of a BCSV", type = smg_cols_info_interface ) rows_data = bpy.props.CollectionProperty( name = "Rows data", description = "2D array with the data going to be stored into a BCSV table.", type = smg_rows_data_interface ) # blender specific data show_col_info = bpy.props.BoolProperty( name = "Show column info", description = "Show the BCSV column information in the UI", default = False ) visible_number_of_rows = bpy.props.IntProperty( name = "Visible number of rows", description = "Max number of BCSV rows to be drawn in the UI", min = 0, default = 0, update = (lambda self, context: interf_row_col_data_upd(self, context, "visible_number_of_rows")) ) visible_number_of_cols = bpy.props.IntProperty( name = "Visible number of columns", description = "Max number of BCSV columns to be drawn in the UI", min = 0, default = 0, update = (lambda self, context: interf_row_col_data_upd(self, context, "visible_number_of_cols")) ) row_slider = bpy.props.IntProperty( name = "Row slider", description = "Slider to be used as a scroller for rows to be able to navigate the BCSV table (Ctrl + scroll over the property)", min = 0, default = 0, update = (lambda self, context: interf_row_col_data_upd(self, context, "row_slider")) ) col_slider = bpy.props.IntProperty( name = "Column slider", description = "Slider to be used as a scroller for columns to be able to navigate the BCSV table (Ctrl + scroll over the property)", min = 0, default = 0, update = (lambda self, context: interf_row_col_data_upd(self, context, "col_slider")) ) active_row_index = bpy.props.IntVectorProperty( name = "Active row index", description = "Value used to specify an operation with the row associated with the index (insert / move / remove). Value at index 0 will be used for inserting/removing a row at the specified index. On moving, value at index 0 represents the current index row to move and value at index 1 represents the new row index position", default = (0, 0), size = 2, min = 0, update = (lambda self, context: interf_row_col_data_upd(self, context, "active_row_index")) ) active_col_index = bpy.props.IntVectorProperty( name = "Active column index", description = "Value used to specify an operation with the column associated with the index (insert / move / remove). Value at index 0 will be used for inserting/removing a column at the specified index. On moving, value at index 0 represents the current index column to move and value at index 1 represents the new column index position", default = (0, 0), size = 2, min = 0, update = (lambda self, context: interf_row_col_data_upd(self, context, "active_col_index")) ) hash_generator_str = bpy.props.StringProperty( name = "Hash generator string", description = "String variable to be used with the hash generator", default = "", update = (lambda self, context: undo_push_wrapper()) ) # ~ # undo/redo list # ~ # single operations: # ~ # insert/move/remove a row/col at a certain index # ~ # change a cell value rows_data/cols_info # ~ # change a cols_info[index].type value (can change all values of the respective column) # ~ # # ~ # check bcsv_funcs.py to see the operation definitions. # ~ # # ~ # when doing undos, the redos will be kept only if no new operations are added # ~ # doing something different will erase the following redos (it is the most logical thing to do) # ~ # (imagine keeping the undo/redo branches) # ~ DEFAULT_UNDO_REDO_BUFFER_SIZE = 25 # BCSV table buffer class table_info: def __init__(self): self.table = None self.undo_redo_list = None BCSV_TABLE_BUFFER_LENGTH = 25 BCSV_TABLE_BUFFER = [] # add a table to the buffer def add_table_to_buffer(table): if ("all good" not in bcsv_funcs.check_smg_bcsv_table(table)): return global BCSV_TABLE_BUFFER new_table_info = table_info() new_table_info.table = table new_table_info.undo_redo_list = [] BCSV_TABLE_BUFFER.append(new_table_info) while (len(BCSV_TABLE_BUFFER) > BCSV_TABLE_BUFFER_LENGTH): BCSV_TABLE_BUFFER.remove(BCSV_TABLE_BUFFER[0]) # function to get the "open" bcsv table in buffer def get_table_from_address(self): # check params if (type(self) != smg_bcsv): return None global BCSV_TABLE_BUFFER for table_info in BCSV_TABLE_BUFFER: if (id(table_info.table) == int(self.table_address, 16)): return table_info.table # nothing was found return None # function to get the "open" bcsv table in buffer def remove_table_from_buffer(address): global BCSV_TABLE_BUFFER for table_info in BCSV_TABLE_BUFFER: if (id(table_info.table) == address): BCSV_TABLE_BUFFER.remove(table_info) break # when the table address is modified def table_address_upd(self, context): try: int(self.table_address, 16) except: self["table_address"] = "0xABCDEF" # wrapper for undo_push() def undo_push_wrapper(): bpy.ops.ed.undo_push() # structure to store all the information needed for # BCSV table to be useful into blender class smg_bcsv(bpy.types.PropertyGroup): # the table interface interface = bpy.props.PointerProperty( name = "SMG BCSV Table Interface", description = "Interface used to modify the SMG BCSV table through Blender's UI", type = smg_bcsv_table_interface ) # hex string of the address of the table table_address = bpy.props.StringProperty( name = "SMG BCSV table address string", description = "Hex string address of the deserialized SMG BCSV table", default = "0xABCDEF", update = table_address_upd ) # storage for when serializing the table table_raw = bpy.props.StringProperty( name = "Raw SMG BCSV table", description = "Serialized BCSV table storage", subtype = "BYTE_STRING", default = "", update = (lambda self, context: undo_push_wrapper()) ) # function to get the "opened" table of an object get_table_from_address = (lambda self: get_table_from_address(self)) # undo/redo buffer size bcsv_table_buffer_size = bpy.props.IntProperty( name = "BCSV table buffer size", description = "Size of the buffer with \"open\" smg BCSV tables", default = BCSV_TABLE_BUFFER_LENGTH, min = 1 ) # Save a BCSV table into an object class DATA_OT_smg_bcsv_table_save(bpy.types.Operator): """Save BCSV table into an object""" bl_idname = "object.smg_bcsv_table_save" bl_label = "Save BCSV table" # save if an object is active/selected # and it has valid deserialized table data @classmethod def poll(cls, context): obj = context.object if (obj == None): return False if (obj.smg_bcsv.get_table_from_address() == None): return False return True # what the operator does def execute(self, context): # apply interface values to table apply_interf_to_table(context) # serialize the table (big endian) and store it into the object's custom property slot obj = context.object raw = bcsv_funcs.create_smg_bcsv_raw(obj.smg_bcsv.get_table_from_address(), ">", False) if (raw != None): obj.smg_bcsv.table_raw = bcsv_funcs.write_smg_bcsv_raw(raw, None) return {"FINISHED"} # Open a BCSV table stored in an object class DATA_OT_smg_bcsv_table_open(bpy.types.Operator): """Open a BCSV table stored in an object""" bl_idname = "object.smg_bcsv_table_open" bl_label = "Open BCSV table" # if an object is active/selected # it has a smg_bcsv_table custom property # it has a table loaded in the buffer @classmethod def poll(cls, context): obj = context.object if (obj == None): return False stream = io.BytesIO(obj.smg_bcsv.table_raw) if ("all good" not in bcsv_funcs.check_bcsv_file(stream, ">")): return False if (obj.smg_bcsv.get_table_from_address() != None): return False return True # what the operator does def execute(self, context): # check if the serialized struct is valid obj = context.object table = bcsv_funcs.read_bcsv_file(io.BytesIO(obj.smg_bcsv.table_raw), "BIG") if (type(table) != bcsv_funcs.smg_bcsv_table): blender_funcs.disp_msg("Cannot open BCSV table, it is malformed. Removing it.") obj.smg_bcsv.table_raw = bytes() return {"FINISHED"} # load the deserialized table data into the buffer add_table_to_buffer(table) obj.smg_bcsv.table_address = hex(id(table)) # assign the respective values to the smg_bcsv_table_interface # to be able to see the table through the UI interf = obj.smg_bcsv.interface interf.update_buffer_table = False # disable table updating interf.row_count = table.row_count interf.col_count = table.col_count interf.show_col_info = False interf.visible_number_of_rows = (DEFAULT_VISIBLE_ROWS if (table.row_count > DEFAULT_VISIBLE_ROWS) else table.row_count) interf.visible_number_of_cols = (DEFAULT_VISIBLE_COLS if (table.col_count > DEFAULT_VISIBLE_COLS) else table.col_count) interf.row_slider = 0 interf.col_slider = 0 interf.active_row_index = (0, 0) interf.active_col_index = (0, 0) load_section_table(context) return {"FINISHED"} # Close a BCSV table from an object (does not save it) class DATA_OT_smg_bcsv_table_close(bpy.types.Operator): """Close a BCSV table from an object (does not save it)""" bl_idname = "object.smg_bcsv_table_close" bl_label = "Close BCSV table" # if an object is active/selected # it has a valid deserialized smg bcsv table @classmethod def poll(cls, context): obj = context.object if (obj == None): return False if (obj.smg_bcsv.get_table_from_address() == None): return False return True # what the operator does def execute(self, context): obj = context.object remove_table_from_buffer(int(obj.smg_bcsv.table_address, 16)) return {"FINISHED"} # create a BCSV table for an object in blender (overrides previously stored table) class DATA_OT_smg_bcsv_table_create(bpy.types.Operator): """Create a new BCSV table from scratch""" bl_idname = "object.smg_bcsv_table_create" bl_label = "Create BCSV table" # variables exclusive to the operator row_count = bpy.props.IntProperty( name = "Number of rows", description = "Number of rows in the BCSV table", default = DEFAULT_ROW_COUNT, min = 0 ) col_count = bpy.props.IntProperty( name = "Number of columns", description = "Number of columns in the BCSV table", default = DEFAULT_COL_COUNT, min = 0 ) # create if an object is active/selected @classmethod def poll(cls, context): if (context.object == None): return False return True # what will be draw by the operator on the floating window def draw(self, context): layout = self.layout layout.prop(self, "row_count", text = "Row count") layout.prop(self, "col_count", text = "Column count") # don't exactly know why this is needed but it is needed def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) # what the operator does def execute(self, context): # create save and open a "default" BCSV table new = bcsv_funcs.smg_bcsv_table() new.row_count = self.row_count new.col_count = self.col_count for i in range(new.col_count): new.cols_info.append(bcsv_funcs.smg_bcsv_table.cols_info()) new.cols_info[-1].name_or_hash = "default" new.cols_info[-1].bitmask = 0xFFFFFFFF new.cols_info[-1].rshift = 0 new.cols_info[-1].type = "LONG" for i in range(new.row_count): new.rows_data.append([]) for j in range(new.col_count): new.rows_data[-1].append(0) # load the serialized/deserialized data add_table_to_buffer(new) context.object.smg_bcsv.table_address = hex(id(new)) raw = bcsv_funcs.create_smg_bcsv_raw(new, ">", False) context.object.smg_bcsv.table_raw = bcsv_funcs.write_smg_bcsv_raw(raw, None) bpy.ops.ed.undo_push() # disable table updating global ALLOW_UPDATE_CALLBACKS; ALLOW_UPDATE_CALLBACKS = False interf = context.object.smg_bcsv.interface interf.row_count = new.row_count interf.col_count = new.col_count interf.visible_number_of_rows = (DEFAULT_VISIBLE_ROWS if (new.row_count > DEFAULT_VISIBLE_ROWS) else new.row_count) interf.visible_number_of_cols = (DEFAULT_VISIBLE_COLS if (new.col_count > DEFAULT_VISIBLE_COLS) else new.col_count) interf.active_row_index = (0, 0) interf.active_col_index = (0, 0) # enable table updating ALLOW_UPDATE_CALLBACKS = True load_section_table(context) return {"FINISHED"} # Remove all BCSV table data from an object class DATA_OT_smg_bcsv_table_remove(bpy.types.Operator): """Remove a BCSV table from an object""" bl_idname = "object.smg_bcsv_table_remove" bl_label = "Remove BCSV table" # if an object is active/selected # it has a smg_bcsv_table custom property or # it has valid deserialized table data loaded @classmethod def poll(cls, context): obj = context.object if (obj == None): return False return True # what the operator does def execute(self, context): # remove whatever the object has completely obj = context.object obj.smg_bcsv.table_raw = bytes() remove_table_from_buffer(int(obj.smg_bcsv.table_address, 16)) return {"FINISHED"} # import a BCSV table data class DATA_OT_smg_bcsv_table_import(bpy.types.Operator, bpy_extras.io_utils.ImportHelper): """Import a BCSV table into the object's data""" bl_idname = "object.smg_bcsv_table_import" bl_label = "Import BCSV table" # importer options endian_mode = bpy.props.EnumProperty( name = "Endian mode", description = "Specify the Endianness of the file", default = "BIG", items = ( ("BIG", "Big", "Read the file as Big Endian"), ("LITTLE", "Little", "Read the file as Big Endian"), ("AUTO", "Auto-detect", "Read the file as Big endian first and if it fails, try reading it as Little Endian") ) ) # create if an object is active/selected @classmethod def poll(cls, context): if (context.object == None): return False return True # what the operator does def execute(self, context): # try importing the BCSV table result = bcsv_funcs.read_bcsv_file(self.properties.filepath, self.endian_mode) # check how the reading went if (type(result) == str): blender_funcs.disp_msg(result) return {"FINISHED"} # append the table to the buffer and save it obj = context.object add_table_to_buffer(result) obj.smg_bcsv.table_address = hex(id(result)) # assign the respective values to the smg_bcsv_table_interface # to be able to see the table through the UI interf = obj.smg_bcsv.interface # disable some callbacks global ALLOW_UPDATE_CALLBACKS; ALLOW_UPDATE_CALLBACKS = False interf.row_slider = 0 interf.col_slider = 0 interf.row_count = result.row_count interf.col_count = result.col_count interf.show_col_info = False interf.visible_number_of_rows = (DEFAULT_VISIBLE_ROWS if (result.row_count > DEFAULT_VISIBLE_ROWS) else result.row_count) interf.visible_number_of_cols = (DEFAULT_VISIBLE_COLS if (result.col_count > DEFAULT_VISIBLE_COLS) else result.col_count) interf.active_row_index = (0, 0) interf.active_col_index = (0, 0) # table can update now ALLOW_UPDATE_CALLBACKS = True # load the default section of the table load_section_table(context) # save the table bpy.ops.object.smg_bcsv_table_save() return {'FINISHED'} # export the data from a deserialized bcsv table class DATA_OT_smg_bcsv_table_export(bpy.types.Operator, bpy_extras.io_utils.ExportHelper): """Save the BCSV table from the object's data into a file (auto-saves the table)""" bl_idname = "object.smg_bcsv_table_export" bl_label = "Export BCSV table" filename_ext = "" filter_glob = bpy.props.StringProperty(default = "*", options = {"HIDDEN"}, maxlen = 255) # exporter options endian = bpy.props.EnumProperty( name = "Endian", description = "Way in which the table data will be written", default = "BIG", items = ( ("BIG", "Big", "Write data in the big endian byte ordering"), ("LITTLE", "Little", "Write data in the little endian byte ordering") ) ) use_std_pad_size = bpy.props.BoolProperty( name = "Use standard padding sizes", description = "Use the usual padding sizes when making the BCSV file", default = True ) # object is selected # it has a valid deserialized BCSV table @classmethod def poll(cls, context): obj = context.object if (obj == None): return False if (obj.smg_bcsv.get_table_from_address() == None): return False return True # what the importer actually does def execute(self, context): # save the table first bpy.ops.object.smg_bcsv_table_save() # create a raw BCSV struct with bcsv_funcs.create_smg_bcsv_raw() obj = context.object endian_ch = ">" if (self.endian == "BIG") else "<" raw = bcsv_funcs.create_smg_bcsv_raw(obj.smg_bcsv.get_table_from_address(), endian_ch, self.use_std_pad_size) # then write it to a real file with bcsv_funcs.write_smg_bcsv_raw(() bcsv_funcs.write_smg_bcsv_raw(raw, self.filepath) blender_funcs.disp_msg("BCSV file \"%s\" written." % (file_ops.get_file_name(self.filepath))) return {"FINISHED"} # insert a row in a BCSV table class DATA_OT_smg_bcsv_table_insert_row(bpy.types.Operator): """Insert a row in a BCSV table""" bl_idname = "object.smg_bcsv_table_insert_row" bl_label = "Insert row at active index" # object is selected # it has a valid deserialized BCSV table @classmethod def poll(cls, context): obj = context.object if (obj == None): return False if (obj.smg_bcsv.get_table_from_address() == None): return False return True # what the operator does def execute(self, context): # get all the variables obj = context.object table = obj.smg_bcsv.get_table_from_address() interf = obj.smg_bcsv.interface # insert a new row on the table at the specified index row_to_insert_values = [] for i in range(table.col_count): value = 0 # LONG, LONG_2, SHORT, CHAR if (table.cols_info[i].type == "FLOAT"): value = 0.0 elif (table.cols_info[i].type in ["STRING", "STRING_OFFSET"]): value = "default" row_to_insert_values.append(value) bcsv_funcs.exec_table_cmd(table, "INSERT", "ROW", [interf.active_row_index[0], row_to_insert_values]) # trigger a load section table interf.row_count = table.row_count # done! print("Row inserted at index %s" % (interf.active_row_index[0])) return {"FINISHED"} # remove a row in a BCSV table class DATA_OT_smg_bcsv_table_remove_row(bpy.types.Operator): """Remove a row from a BCSV table""" bl_idname = "object.smg_bcsv_table_remove_row" bl_label = "Remove row at active index" # object is selected # it has a valid deserialized BCSV table @classmethod def poll(cls, context): obj = context.object if (obj == None): return False if (obj.smg_bcsv.get_table_from_address() == None): return False return True # what the operator does def execute(self, context): obj = context.object table = obj.smg_bcsv.get_table_from_address() interf = obj.smg_bcsv.interface if (interf.row_count == 0): blender_funcs.disp_msg("Cannot remove more rows on a 0 row BCSV table") return {"FINISHED"} # get the real index to be removed row_to_remove_index = interf.active_row_index[0] if (row_to_remove_index >= interf.row_count): row_to_remove_index = interf.row_count - 1 interf.active_row_index[0] = row_to_remove_index # remove the row at the index row_to_remove_values = table.rows_data[row_to_remove_index] bcsv_funcs.exec_table_cmd(table, "REMOVE", "ROW", [row_to_remove_index, row_to_remove_values]) # trigger a load table interf.row_slider = interf.row_slider # done! print("Row at index %s removed" % (row_to_remove_index)) return {"FINISHED"} # move a row in a BCSV table class DATA_OT_smg_bcsv_table_move_row(bpy.types.Operator): """Move a row in a BCSV table to another position""" bl_idname = "object.smg_bcsv_table_move_row" bl_label = "Move row at active index" # object is selected # it has a valid deserialized BCSV table @classmethod def poll(cls, context): obj = context.object if (obj == None): return False if (obj.smg_bcsv.get_table_from_address() == None): return False return True # what the operator does def execute(self, context): obj = context.object table = obj.smg_bcsv.get_table_from_address() interf = obj.smg_bcsv.interface if (interf.row_count == 0): blender_funcs.disp_msg("Cannot move rows on a 0 row BCSV table") return {"FINISHED"} # get the indexes old_index = interf.active_row_index[0] new_index = interf.active_row_index[1] if (old_index >= interf.row_count): old_index = interf.row_count - 1 interf.active_row_index[0] = old_index # move the specified row bcsv_funcs.exec_table_cmd(table, "MOVE", "ROW", [old_index, new_index]) # trigger a load table load_section_table(context) # done! print("Row at index %s moved to index %s" % (old_index, new_index)) return {"FINISHED"} # insert a column in a BCSV table class DATA_OT_smg_bcsv_table_insert_col(bpy.types.Operator): """Insert a column in a BCSV table""" bl_idname = "object.smg_bcsv_table_insert_col" bl_label = "Insert column at active index" # object is selected # it has a valid deserialized BCSV table @classmethod def poll(cls, context): obj = context.object if (obj == None): return False if (obj.smg_bcsv.get_table_from_address() == None): return False return True # what the operator does def execute(self, context): # get all the variables obj = context.object table = obj.smg_bcsv.get_table_from_address() interf = obj.smg_bcsv.interface # insert a new col on the table at the specified index insert_index = interf.active_col_index[0] col_info_values = ["default", 0xFFFFFFFF, 0, "LONG"] col_to_insert_values = [0 for i in range(table.row_count)] bcsv_funcs.exec_table_cmd(table, "INSERT", "COLUMN", [insert_index, col_info_values, col_to_insert_values]) # trigger a load section table interf.col_count = table.col_count # done! print("Column inserted at index %s" % (interf.active_col_index[0])) return {"FINISHED"} # remove a column in a BCSV table class DATA_OT_smg_bcsv_table_remove_col(bpy.types.Operator): """Remove a column from a BCSV table""" bl_idname = "object.smg_bcsv_table_remove_col" bl_label = "Remove column at active index" # object is selected # it has a valid deserialized BCSV table @classmethod def poll(cls, context): obj = context.object if (obj == None): return False if (obj.smg_bcsv.get_table_from_address() == None): return False return True # what the operator does def execute(self, context): obj = context.object table = obj.smg_bcsv.get_table_from_address() interf = obj.smg_bcsv.interface if (interf.col_count == 1): blender_funcs.disp_msg("Cannot remove more columns on a 1 column BCSV table") return {"FINISHED"} # get the real index to be removed col_to_remove_index = interf.active_col_index[0] if (col_to_remove_index >= interf.col_count): col_to_remove_index = interf.col_count - 1 interf.active_col_index[0] = col_to_remove_index # remove the column at the index col_info = table.cols_info[col_to_remove_index] col_info_values = [col_info.name_or_hash, col_info.bitmask, col_info.rshift, col_info.type] col_to_remove_values = [] for i in range(table.row_count): col_to_remove_values.append(table.rows_data[i][col_to_remove_index]) bcsv_funcs.exec_table_cmd(table, "REMOVE", "COLUMN", [col_to_remove_index, col_info_values, col_to_remove_values]) # trigger a load table interf.col_slider = interf.col_slider # done! print("Column at index %s removed" % (col_to_remove_index)) return {"FINISHED"} # move a column in a BCSV table class DATA_OT_smg_bcsv_table_move_col(bpy.types.Operator): """Move a column in a BCSV table to another position""" bl_idname = "object.smg_bcsv_table_move_col" bl_label = "Move column at active index" # object is selected # it has a valid deserialized BCSV table @classmethod def poll(cls, context): obj = context.object if (obj == None): return False if (obj.smg_bcsv.get_table_from_address() == None): return False return True # what the operator does def execute(self, context): obj = context.object table = obj.smg_bcsv.get_table_from_address() interf = obj.smg_bcsv.interface # get the indexes old_index = interf.active_col_index[0] new_index = interf.active_col_index[1] if (old_index >= interf.col_count): old_index = interf.col_count - 1 interf.active_col_index[0] = old_index # move the specified column bcsv_funcs.exec_table_cmd(table, "MOVE", "COLUMN", [old_index, new_index]) # trigger a load table load_section_table(context) # done! print("Column at index %s moved to index %s" % (old_index, new_index)) return {"FINISHED"} # add a hash string as a known hash string class DATA_OT_smg_bcsv_table_interface_add_new_known_hash(bpy.types.Operator): """Add the current string as a new known hash string""" bl_idname = "object.smg_bcsv_table_interface_add_new_known_hash" bl_label = "Add new known string hash" # only if the string is CP932 encodable @classmethod def poll(cls, context): # ensure correct encoding try: context.object.smg_bcsv.interface.hash_generator_str.encode("cp932") return True except: return False # what the operator does def execute(self, context): interf = context.object.smg_bcsv.interface enc = interf.hash_generator_str.encode("cp932") bcsv_funcs.add_new_known_hash(enc) print("Hash string \"%s\" added as a known hash!" % (interf.hash_generator_str)) return {"FINISHED"} # copy the hash of a valid string class DATA_OT_smg_bcsv_table_interface_copy_str_hash(bpy.types.Operator): """Copy the generated hash of a string""" bl_idname = "object.smg_bcsv_table_interface_copy_str_hash" bl_label = "Copy string hash" # only if the string is CP932 encodable @classmethod def poll(cls, context): # ensure correct encoding try: context.object.smg_bcsv.interface.hash_generator_str.encode("cp932") return True except: return False # what the operator does def execute(self, context): string = context.object.smg_bcsv.interface.hash_generator_str hash_string = bcsv_funcs.calc_bytes_hash(string.encode("cp932")) hash_string = "0x%08X" % (hash_string) context.window_manager.clipboard = hash_string print("Hash string \"%s\" copied!" % (hash_string)) return {"FINISHED"} # panel to be drawn on an object's properties class DATA_PT_smg_bcsv_table_interface(bpy.types.Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "data" bl_label = "BCSV table" # panel will be drawn only when # the object selected in in context @classmethod def poll(cls, context): obj = context.object if (obj == None): return False return True # draw the panel def draw(self, context): # layout stuff layout = self.layout obj = context.object # draw these buttons row = layout.row() row.operator("object.smg_bcsv_table_create") row.operator("object.smg_bcsv_table_remove") row.operator("object.smg_bcsv_table_open") row.operator("object.smg_bcsv_table_close") row = layout.row() row.operator("object.smg_bcsv_table_save") row.operator("object.smg_bcsv_table_import") row.operator("object.smg_bcsv_table_export") # draw table table = obj.smg_bcsv.get_table_from_address() interf = obj.smg_bcsv.interface # cols_info if (table != None): if (interf.show_col_info): box = layout.box() row = box.row() row.label("Columns information: [Name or hash / Bitmask / Right shift / Data Type]") # display the columns information if (interf.visible_number_of_cols != 0): for i in range(interf.visible_number_of_cols): # first UI row if (i == 0): row = box.row(align = True) # display column data col = row.column(align = True) col.label("C%s" % (interf.col_slider + i)) col.prop(interf.cols_info[i], "name_or_hash", text = "") col.prop(interf.cols_info[i], "bitmask", text = "") col.prop(interf.cols_info[i], "rshift", text = "") col.prop(interf.cols_info[i], "type", text = "") # show the column slider row = box.row() row.prop(interf, "col_slider", icon_only = True) else: row.label("No columns displayed.") # show column info, visible cols per row and visible rows buttons box = layout.box() row = box.row() row.label("Table display") row = box.row() row.prop(interf, "show_col_info", text = "Display columns information") row = box.row() row.prop(interf, "visible_number_of_rows", text = "Visible numbers of rows") row.prop(interf, "visible_number_of_cols", text = "Visible numbers of columns") # show the number of rows/cols # show the active row/column property # show the insert/remove row/column buttons box = layout.box() row = box.row() row.label("Table operations") row = box.row() col = row.column(align = True) col.label("Row count: %s" % (interf.row_count)) row_tmp = col.row(align = False) row_tmp.prop(interf, "active_row_index", text = "Active row index") col.operator("object.smg_bcsv_table_insert_row") col.operator("object.smg_bcsv_table_move_row") col.operator("object.smg_bcsv_table_remove_row") col = row.column(align = True) col.label("Column count: %s" % (interf.col_count)) row_tmp = col.row(align = False) row_tmp.prop(interf, "active_col_index", text = "Active column index") col.operator("object.smg_bcsv_table_insert_col") col.operator("object.smg_bcsv_table_move_col") col.operator("object.smg_bcsv_table_remove_col") # box containing the rows data box = layout.box() # draw the table row = box.row(align = True) row.label("Rows information:") row = box.row(align = True) # check if there are rows to display if (interf.row_count == 0): row.label("BCSV table has no rows.") elif (interf.visible_number_of_rows == 0): row.label("No rows displayed.") # ready to start else: # draw the column slider row = box.row(align = True) row.prop(interf, "col_slider", icon_only = True) row.scale_x = 10 row.scale_y = 1 # draw the container for the rows (part for the rows, part for the row scroller) table_row = box.row(align = True) rows_data = table_row.column(align = True) # draw each table column for i in range(interf.visible_number_of_cols): # first UI row if (i == 0): row = rows_data.row(align = True) col = row.column(align = True) col.scale_x = 1 / 2.4 col.label("R/C") for j in range(interf.visible_number_of_rows): col.label("R%s" % (interf.row_slider + j)) # create the column data col = row.column(align = True) data_path = "long" if (interf.cols_info[i].type == "SHORT"): data_path = "short" elif (interf.cols_info[i].type == "CHAR"): data_path = "char" elif (interf.cols_info[i].type == "FLOAT"): data_path = "float" elif (interf.cols_info[i].type == "STRING"): data_path = "string" elif (interf.cols_info[i].type == "STRING_OFFSET"): data_path = "string_offset" for j in range(interf.visible_number_of_rows): if (j == 0): col.label("C%s: %s" % (interf.col_slider + i, interf.cols_info[i].name_or_hash)) col.prop(interf.rows_data[j].cells[i], data_path, text = "") # draw the row slider col = table_row.column(align = True) col.label("") col.scale_x = 1 / 18 col = table_row.column(align = True) col.prop(interf, "row_slider", icon_only = True) col.scale_x = 1 / 9 col.scale_y = interf.visible_number_of_rows + 1 # draw the column slider row = box.row(align = True) row.prop(interf, "col_slider", icon_only = True) row.scale_x = 10 row.scale_y = 1 # hash generator (just like the one in whitehole >:]) box = layout.box() row = box.row() row.label("Hash generator:") row_ops = box.row() row_ops.operator("object.smg_bcsv_table_interface_add_new_known_hash", icon = "NEW") row_ops.operator("object.smg_bcsv_table_interface_copy_str_hash", icon = "COPY_ID") row = box.row() row.prop(interf, "hash_generator_str", text = "Input") try: enc = interf.hash_generator_str.encode("cp932") row.label("Hash: 0x%08X" % (bcsv_funcs.calc_bytes_hash(enc))) except: row.label("Hash: String not CP932 encodable.") # add some extra padding to be able to "center" the table for i in range(8): layout.label("") # new classes to register classes = ( smg_cols_info_interface, smg_rows_data_cell_interface, smg_rows_data_interface, smg_bcsv_table_interface, smg_bcsv, DATA_OT_smg_bcsv_table_save, DATA_OT_smg_bcsv_table_open, DATA_OT_smg_bcsv_table_close, DATA_OT_smg_bcsv_table_create, DATA_OT_smg_bcsv_table_remove, DATA_OT_smg_bcsv_table_import, DATA_OT_smg_bcsv_table_export, DATA_OT_smg_bcsv_table_insert_row, DATA_OT_smg_bcsv_table_remove_row, DATA_OT_smg_bcsv_table_move_row, DATA_OT_smg_bcsv_table_insert_col, DATA_OT_smg_bcsv_table_remove_col, DATA_OT_smg_bcsv_table_move_col, DATA_OT_smg_bcsv_table_interface_add_new_known_hash, DATA_OT_smg_bcsv_table_interface_copy_str_hash, DATA_PT_smg_bcsv_table_interface, ) # function to be executed: # - before a Blender undo/redo # - before saving the BLEND # the undo_post executes undo_pre so I am doing a little trick # https://projects.blender.org/blender/blender/issues/60247 WAS_UNDO_PRE_EXECUTED = False @bpy.app.handlers.persistent def smg_bcsv_table_undo_redo_save_pre_post(dummy): override = bpy.context.copy() global WAS_UNDO_PRE_EXECUTED # save all the open tables and close them if (WAS_UNDO_PRE_EXECUTED == False): for scene in bpy.data.scenes: for obj in scene.objects: if (obj.smg_bcsv.get_table_from_address() == None): continue override["object"] = obj bpy.ops.object.smg_bcsv_table_save(override) bpy.ops.object.smg_bcsv_table_close(override) WAS_UNDO_PRE_EXECUTED = True # close the tables and open them again else: for scene in bpy.data.scenes: for obj in scene.objects: stream = io.BytesIO(obj.smg_bcsv.table_raw) if ("all good" not in bcsv_funcs.check_bcsv_file(stream, ">")): continue override["object"] = obj bpy.ops.object.smg_bcsv_table_open(override) WAS_UNDO_PRE_EXECUTED = False # register and unregister functions @bpy.app.handlers.persistent def register(dummy): try: for c in classes: bpy.utils.register_class(c) bpy.types.Object.smg_bcsv = bpy.props.PointerProperty(type = smg_bcsv) # ~ bpy.app.handlers.undo_pre.append(smg_bcsv_table_undo_redo_save_pre_post) # ~ bpy.app.handlers.redo_pre.append(smg_bcsv_table_undo_redo_save_pre_post) # ~ bpy.app.handlers.save_pre.append(smg_bcsv_table_undo_redo_save_pre_post) except: return def unregister(): try: for c in classes: bpy.utils.unregister_class(c) del bpy.types.Object.smg_bcsv # ~ bpy.app.handlers.undo_pre.remove(smg_bcsv_table_undo_redo_save_pre_post) # ~ bpy.app.handlers.redo_pre.remove(smg_bcsv_table_undo_redo_save_pre_post) # ~ bpy.app.handlers.save_pre.remove(smg_bcsv_table_undo_redo_save_pre_post) except: return