From 70e647076418d114111aa76b5d3639a5b4271e94 Mon Sep 17 00:00:00 2001 From: Owl Date: Fri, 26 Sep 2025 14:32:34 -0400 Subject: bcsv and other stuff --- bcsv_editing.py | 1391 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1391 insertions(+) create mode 100644 bcsv_editing.py (limited to 'bcsv_editing.py') diff --git a/bcsv_editing.py b/bcsv_editing.py new file mode 100644 index 0000000..8aad5e8 --- /dev/null +++ b/bcsv_editing.py @@ -0,0 +1,1391 @@ +# 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 -- cgit v1.2.3-70-g09d2