bl_info = {
"name": "ConverterPIX Wrapper for conversion & import of SCS Game Models",
"description": "Wrapper add-on to use ConvPIX within the Blender and import SCS
game models with ease.",
"author": "Simon Lusenc (50keda)",
"version": (2, 1),
"blender": (2, 81, 0),
"location": "File > Import > SCS Models - ConverterPIX & BT (*.scs)",
"category": "Import-Export",
"support": "COMMUNITY"
}
import bpy
import os
import subprocess
import shutil
from sys import platform
from tempfile import mkdtemp
from threading import Thread
from time import time
from bpy.props import StringProperty, CollectionProperty, IntProperty,
BoolProperty, PointerProperty, FloatProperty
from bpy.types import AddonPreferences
from bpy_extras.io_utils import ImportHelper
# use blender configuration directories to save converter pix binary,
# this way we can avoid permission problems of saving exe file
CONVERTER_PIX_DIR = os.path.join(bpy.utils.resource_path('USER'),
"config/ConverterPIXWrapper")
if not os.path.isdir(CONVERTER_PIX_DIR):
os.makedirs(CONVERTER_PIX_DIR, exist_ok=True)
if platform == "linux":
CONVERTER_PIX_URL =
"https://github.com/simon50keda/ConverterPIX/raw/master/bin/linux/converter_pix"
CONVERTER_PIX_PATH = os.path.join(CONVERTER_PIX_DIR, "converter_pix")
LINE_SPLITTER = "\n"
elif platform == "darwin":
CONVERTER_PIX_URL =
"https://github.com/theHarven/ConverterPIX/raw/MacOS_binary/bin/macos/
converter_pix"
CONVERTER_PIX_PATH = os.path.join(CONVERTER_PIX_DIR, "converter_pix")
LINE_SPLITTER = "\n"
else:
CONVERTER_PIX_URL =
"https://github.com/mwl4/ConverterPIX/raw/master/bin/win_x86/converter_pix.exe"
CONVERTER_PIX_PATH = os.path.join(CONVERTER_PIX_DIR, "converter_pix.exe")
LINE_SPLITTER = "\r\n"
def path_join(path1, path2):
"""Joins path1 with path2 and replaces backslashes with forward ones.
Needed for proper navigation inside archive tree on Windows.
"""
return os.path.join(path1, path2).replace("\\", "/")
def update_converter_pix():
"""Downloads ConverterPIX from github and saves it to CONVERTER_PIX_PATH.
:returns: True if successfully updated; False otherwise
:rtype: bool
"""
print("Downloading ConverterPIX...")
try:
from urllib3 import disable_warnings
from requests import get
# disable urllib warnings so we don't get complains over unauthorized
converter pix download
disable_warnings()
# create unauthorized get request and download converter pix
result = get(CONVERTER_PIX_URL, verify=False)
with open(CONVERTER_PIX_PATH, "wb") as f:
f.write(result.content)
# make it executable on linux
if platform == "linux" or platform == "darwin":
from stat import S_IEXEC, S_IXGRP
st = os.stat(CONVERTER_PIX_PATH)
os.chmod(CONVERTER_PIX_PATH, st.st_mode | S_IEXEC | S_IXGRP)
except Exception as e:
from traceback import format_exc
trace_str = format_exc().replace("\n", "\n\t")
print("Unexpected %s error accured duing updating of ConverterPIX:\n\t%s\n"
% (type(e).__name__, trace_str))
return False
print("ConverterPix updated!")
return True
def run_converter_pix(args):
"""Runs ConverterPIX via CLI.
1. On linux run it trough wine
2. Mac OS X currently not supported
In case return code is different from 0, sth went wrong.
:param args: Arguments for converter pix
:type args: list[str]
:return: return code and stdout from converter pix devided into lines; empty
list on error or not supported OS
:rtype: tuple[int, list[str]]
"""
final_command = [CONVERTER_PIX_PATH]
final_command.extend(args)
print(final_command)
result = subprocess.run(final_command, stdout=subprocess.PIPE)
# if there was some problem running converter pix just return empty list
if result.returncode != 0:
return result.returncode, []
# also return non-zero code if converter pix alone is reporting errors in
output
if "<error>".encode("utf-8") in result.stdout:
return -1, result.stdout.decode("utf-8").split(LINE_SPLITTER)
decoded_result = result.stdout.decode("utf-8").split(LINE_SPLITTER)
for line in decoded_result:
print(line)
return result.returncode, decoded_result
def get_archive_listdir(file_paths, current_subpath):
"""Get archive directory listing for given subpath.
:param file_paths: list of paths that should be used as base archives for
converter pix
:type file_paths: list[str]
:param current_subpath: current subpath inside of archives
:type current_subpath: str
:return: returns two lists: directories and files
:rtype: (list[str], list[str])
"""
args = []
for file_path in file_paths:
args.extend(["-b", file_path])
args.extend(["-listdir", current_subpath])
retcode, stdout = run_converter_pix(args)
dirs = []
files = []
if retcode != 0:
print("Error getting archive directory listing output from ConverterPIX!")
for line in stdout:
if line.startswith("[D] "):
dirs.append(os.path.relpath(line[4:], current_subpath))
elif line.startswith("[F] "):
files.append(os.path.relpath(line[4:], current_subpath))
return sorted(dirs), sorted(files)
# ### PROPERTIES ###
class ConvPIXWrapperAddonPrefs(AddonPreferences):
bl_idname = __name__
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
col.operator("world.converter_pix_wrapper_update_exe", icon="URL")
class ConvPIXWrapperFileEntry(bpy.types.PropertyGroup):
"""Property group holding browser file entry data."""
do_import: BoolProperty(description="Proccess this entry for
conversion/import.")
name: StringProperty(description="Name of the entry represeting name of the
file or directory.")
is_dir: BoolProperty(description="Taging this entry as directory.")
class ConvPIXWrapperBrowserData(bpy.types.PropertyGroup):
"""Property group representing file browser data."""
def is_subpath_valid(self):
"""Checks if current set subpath is valid for currently set archives.
:return: True if any dirs and files is returned; False otherwise
:rtype: bool
"""
archive_paths = [archive_path.name for archive_path in self.archive_paths]
dirs, files = get_archive_listdir(archive_paths, self.current_subpath)
if len(dirs) > 0 or len(files) > 0:
return True
return False
def update_active_entry(self, context):
"""Update function for navigation trough tree of archive:
1. When directory is selected it advances to it and refreshes the list.
2. When parent directory '..' is selected it returns one level up and
refreshes the list.
3. When file is selected nothing happens.
"""
# update current subpath only if proper active entry selected
if 0 <= self.active_entry < len(self.file_entries):
active_file_entry = self.file_entries[self.active_entry]
print("New active item:", active_file_entry.name)
# only advance in tree if active entry is directory
if active_file_entry.is_dir:
if active_file_entry.name == "..":
if self.current_subpath != "/":
self.current_subpath =
os.path.dirname(self.current_subpath)
else:
self.current_subpath = path_join(self.current_subpath,
active_file_entry.name)
self.active_entry = -1
# abort execution here, as setting active entry to -1 will anyway
trigger another update
return
elif active_file_entry.name != "..": # file was selected just return
and do nothing with selection
return
# remove old entries
while len(self.file_entries) > 0:
self.file_entries.remove(0)
# add entry for navigating to parent directory
entry = self.file_entries.add()
entry.name = ".."
entry.is_dir = True
# add actual entries from archives
archive_paths = [archive_path.name for archive_path in self.archive_paths]
dirs, files = get_archive_listdir(archive_paths, self.current_subpath)
for dir_name in dirs:
entry = self.file_entries.add()
entry.name = dir_name
entry.is_dir = True
for file_name in files:
# ignore file that don't match prescribed extension, if asteriks then
everything should be displayed
if not file_name.endswith(self.file_extension) and
self.file_extension != "*":
continue
entry = self.file_entries.add()
entry.name = file_name
entry.is_dir = False
multi_select: BoolProperty(
description="Can multiple files be selected?"
)
file_extension: StringProperty(
description="File extension for the files that should be listed in this
browser data.",
default="*",
)
archive_paths: CollectionProperty(
description="Paths to archives from which directories and files should be
listed.",
type=bpy.types.OperatorFileListElement,
)
current_subpath: StringProperty(
description="Current position in archive tree.",
default="/",
)
file_entries: CollectionProperty(
description="Collection of file entries for current position in archive
tree.",
type=ConvPIXWrapperFileEntry,
)
active_entry: IntProperty(
description="Currently selected directory/file in browser.",
default=-1,
update=update_active_entry
)
class ConvPIXWrapperArchiveToUse(bpy.types.PropertyGroup):
"""Property group holding entry data for archives to use."""
path: StringProperty(description="Path to archive.")
selected: BoolProperty(description="Marking this path as selected. Once
selected it can be deleted or moved in the list.")
# ### OPERATORS ###
class CONV_PIX_WRAPPER_UL_FileEntryItem(bpy.types.UIList):
"""Class for drawing archive browser file entry."""
def draw_item(self, context, layout, data, item, icon, active_data,
active_property, index):
if item.name == "..":
icon = "FILE_PARENT"
elif item.is_dir:
icon = "FILE_FOLDER"
else:
icon = "FILE_BLANK"
split_line = layout.split(factor=0.8)
split_line.prop(item, "name", text="", emboss=False, icon=icon)
if data.multi_select and not item.is_dir:
row = split_line.row()
row.alignment = "RIGHT"
row.prop(item, "do_import", text="")
class CONV_PIX_WRAPPER_OT_ListImport(bpy.types.Operator):
bl_idname = "converter_pix_wrapper.list_and_import"
bl_label = "Converter PIX Wrapper"
bl_options = {'UNDO', 'INTERNAL'}
__static_last_model_subpath = "/"
__static_last_anim_subpath = "/"
__static_browsers_slider = 0.5
archive_paths: CollectionProperty(
description="Paths to archives from which directories and files should be
listed.",
type=bpy.types.OperatorFileListElement,
)
only_convert: BoolProperty(
description="Use ConverterPIX only for conversion of resources into SCS
Project Base Path and import manually later?",
)
textures_to_base: BoolProperty(
description="Should textures be copied into the sibling 'base' directory,
so they won't be included in mod packing?",
)
import_animations: BoolProperty(
name="Use Animations",
description="Select animations for conversion and import?\n"
"Gives you ability to convert and import animations for
selected model (use it only if you are working with animated model)."
)
model_browser_data: PointerProperty(
description="Archive browser data for model selection.",
type=ConvPIXWrapperBrowserData
)
anim_browser_data: PointerProperty(
description="Archive browser data for animations selection.",
type=ConvPIXWrapperBrowserData
)
browsers_slider: FloatProperty(
name="Browsers Slider",
min=0.0,
max=1.0,
subtype='FACTOR',
default=0.5
)
def check(self, context):
return True # always trigger redraw to avoid problems of popup dialog
UIList not drawing properly
def invoke(self, context, event):
# prepare browsers data and forcly trigger update, to load up root archive
entries
for archive_path in self.archive_paths:
entry = self.model_browser_data.archive_paths.add()
entry.name = archive_path.name
entry = self.anim_browser_data.archive_paths.add()
entry.name = archive_path.name
self.model_browser_data.current_subpath =
CONV_PIX_WRAPPER_OT_ListImport.__static_last_model_subpath
if not self.model_browser_data.is_subpath_valid():
self.model_browser_data.current_subpath = "/"
self.anim_browser_data.current_subpath =
CONV_PIX_WRAPPER_OT_ListImport.__static_last_anim_subpath
if not self.anim_browser_data.is_subpath_valid():
self.anim_browser_data.current_subpath = "/"
self.model_browser_data.file_extension = ".pmg"
self.anim_browser_data.file_extension = ".pma"
self.anim_browser_data.multi_select = True
self.model_browser_data.update_active_entry(context)
self.anim_browser_data.update_active_entry(context)
self.browsers_slider =
CONV_PIX_WRAPPER_OT_ListImport.__static_browsers_slider
return context.window_manager.invoke_props_dialog(self, width=500)
def execute(self, context):
from io_scs_tools.utils import get_scs_globals
self.save_current_operator_settings()
if self.model_browser_data.active_entry == -1:
self.report({'WARNING'}, "No active model selected, aborting import!")
return {'CANCELLED'}
model_file_entry_name =
self.model_browser_data.file_entries[self.model_browser_data.active_entry].name
model_archive_subpath = path_join(self.model_browser_data.current_subpath,
model_file_entry_name)
# collect all selected animations from animations browser
anim_archive_subpaths = []
if self.import_animations:
for anim_file_entry in self.anim_browser_data.file_entries:
if anim_file_entry.do_import:
anim_archive_subpaths.append(path_join(self.anim_browser_data.current_subpath,
anim_file_entry.name[:-4]))
# temporarly convert to temp directory to be able to extract textures and
models separately
export_path = mkdtemp()
# put together arguments for converter pix
args = []
for archive_path in self.archive_paths:
args.extend(["-b", archive_path.name])
args.extend(["-m", model_archive_subpath[:-4]])
args.extend(anim_archive_subpaths)
args.extend(["-e", export_path])
# execute conversion
retcode, stdout = run_converter_pix(args)
if retcode != 0:
msg = "ConverterPIX crashed or encountered error! Standard output
returned:"
print(msg)
self.report({'ERROR'}, msg)
for line in stdout:
if line != "":
print(line)
self.report({'ERROR'}, line)
return {'CANCELLED'}
# calculate models & textures project path
models_project_path = textures_project_path =
get_scs_globals().scs_project_path
if self.textures_to_base:
textures_project_path = os.path.join(models_project_path, os.pardir,
"base")
# distribute converted data to appropriate folder
for root, dirs, files in os.walk(export_path, topdown=False):
file_subdir = os.path.relpath(root, export_path)
for file in files:
if file.endswith(".tobj") or file.endswith(".dds") or
file.endswith(".png"):
file_dstdir = os.path.join(textures_project_path, file_subdir)
else:
file_dstdir = os.path.join(models_project_path, file_subdir)
os.makedirs(file_dstdir, exist_ok=True)
src_path = os.path.join(root, file)
dst_path = os.path.join(file_dstdir, file)
shutil.move(src_path, dst_path)
# dirs cleanup - with the help of disabled topdown listing, remove
empty dirs here including topmost export path
os.rmdir(root)
# now do actual import with BT
if not self.only_convert:
pim_import_file = model_file_entry_name[:-4] + ".pim"
pim_import_dir = path_join(get_scs_globals().scs_project_path,
self.model_browser_data.current_subpath[1:])
bpy.ops.scs_tools.import_pim(files=[{"name": pim_import_file}],
directory=pim_import_dir)
return {'FINISHED'}
def save_current_operator_settings(self):
# backup last sub-paths to return to them eventually
CONV_PIX_WRAPPER_OT_ListImport.__static_last_model_subpath =
self.model_browser_data.current_subpath
CONV_PIX_WRAPPER_OT_ListImport.__static_last_anim_subpath =
self.anim_browser_data.current_subpath
# backup ui browsers divider value
CONV_PIX_WRAPPER_OT_ListImport.__static_browsers_slider =
self.browsers_slider
print("Saving current settings for ConverterPIXWrapper import operator...")
def cancel(self, context):
self.save_current_operator_settings()
def draw(self, context):
layout = self.layout
# clip browsers slider to proper upper and lower values to avoid UI
problems with split
if self.browsers_slider < 0.1 or self.browsers_slider > 0.85:
self.browsers_slider = min(0.85, max(0.1, self.browsers_slider))
archive_names = [os.path.basename(archive_path.name) for archive_path in
self.archive_paths]
layout.label(text="Currently working upon: %r." % archive_names)
# browsers slider ratio
layout.prop(self, "browsers_slider")
browser_layout = layout.split(factor=self.browsers_slider)
left_column = browser_layout.column(align=True)
right_column = browser_layout.column(align=True)
usage_type = "convert" if self.only_convert else "convert & import"
# left browser
left_column.label(text="Model to %s:" % usage_type)
subpath_row = left_column.row(align=True)
subpath_row.enabled = False
subpath_row.prop(self.model_browser_data, "current_subpath", text="")
left_column.template_list(
'CONV_PIX_WRAPPER_UL_FileEntryItem',
list_id="ModelBrowser",
dataptr=self.model_browser_data,
propname="file_entries",
active_dataptr=self.model_browser_data,
active_propname="active_entry",
rows=20,
maxrows=20,
)
# right browser
import_anim_row = right_column.row()
import_anim_row.label(text="Animations to %s:" % usage_type)
import_anim_row = import_anim_row.row()
import_anim_row.alignment = "RIGHT"
import_anim_row.prop(self, "import_animations", text="")
subpath_row = right_column.row(align=True)
subpath_row.enabled = False
subpath_row.prop(self.anim_browser_data, "current_subpath", text="")
browser_row = right_column.row(align=True)
browser_row.enabled = self.import_animations
browser_row.template_list(
'CONV_PIX_WRAPPER_UL_FileEntryItem',
list_id="AnimBrowser",
dataptr=self.anim_browser_data,
propname="file_entries",
active_dataptr=self.anim_browser_data,
active_propname="active_entry",
rows=20,
maxrows=20,
)
class CONV_PIX_WRAPPER_OT_Import(bpy.types.Operator, ImportHelper):
bl_idname = "converter_pix_wrapper.import"
bl_label = "Import SCS Models - ConverterPIX & BT (*.scs)"
bl_description = "Converts and imports selected SCS model with the help of
ConvPIX and SCS Blender Tools."
bl_options = {'UNDO', 'PRESET'}
directory: StringProperty()
files: CollectionProperty(name="Selected Files",
description="File paths used for importing the SCS
files",
type=bpy.types.OperatorFileListElement)
ordered_files = [] # stores ordered list of currently selected files, first
selected is first, last selected is last
archives_to_use: CollectionProperty(name="Archives to Use",
description="Archives that should be used
on conversion/import.",
type=ConvPIXWrapperArchiveToUse)
archives_to_use_mode: BoolProperty(
default=False,
description="Add currently selected files to list of archives to be used
with ConverterPIX as bases."
)
delete_selected_archives_mode: BoolProperty(
default=False,
description="Delete selected archives from list."
)
move_up_selected_archives_mode: BoolProperty(
default=False,
description="Move selected archives up in the list."
)
move_down_selected_archives_mode: BoolProperty(
default=False,
description="Move selected archives down in the list."
)
scs_project_path_mode: BoolProperty(
default=False,
description="Set currently selected directory as SCS Project Path"
)
only_convert: BoolProperty(
name="Only convert?",
description="Use ConverterPIX only for conversion of resources into SCS
Project Base Path and import manually later?",
default=False
)
textures_to_base: BoolProperty(
name="Textures to Base?",
description="Should textures be copied into the sibling 'base' directory,
so they won't be included in mod packing?",
default=False
)
filter_glob: StringProperty(default="*.scs;*.zip;", options={'HIDDEN'})
def check(self, context):
# create/update ordered list of currently selected files
current_file_names = set([file.name for file in self.files])
# we can put together ordering by copying names into extra array
for file_name in current_file_names:
if file_name not in self.ordered_files:
self.ordered_files.append(file_name)
# similarly as adding we have to take care of removing items which are not
selected anymore
for file_name in self.ordered_files.copy(): # work upon copy as we are
removing items in this for
if file_name not in current_file_names:
self.ordered_files.remove(file_name)
# handle different actions depending on boolean modes variables
if self.scs_project_path_mode: # set SCS Project Base Path
from io_scs_tools.utils import get_scs_globals
get_scs_globals().scs_project_path = os.path.dirname(self.filepath)
self.scs_project_path_mode = False
elif self.archives_to_use_mode: # add selected archives from browser to
archives list
curr_archives_to_use = [archive.path for archive in
self.archives_to_use]
for file_name in self.ordered_files:
curr_filepath = path_join(self.directory, file_name)
# avoid duplicates
if curr_filepath in curr_archives_to_use:
continue
new_archive_to_use = self.archives_to_use.add()
new_archive_to_use.path = curr_filepath
self.archives_to_use_mode = False
elif self.delete_selected_archives_mode: # delete selected archives from
list
i = 0
while i < len(self.archives_to_use):
if self.archives_to_use[i].selected:
self.archives_to_use.remove(i)
i -= 1
i += 1
self.delete_selected_archives_mode = False
elif self.move_up_selected_archives_mode: # move up selected archives in
the list
i = 0
while i < len(self.archives_to_use):
if self.archives_to_use[i].selected:
if i - 1 >= 0 and not self.archives_to_use[i - 1].selected:
self.archives_to_use.move(i, i - 1)
i += 1
self.move_up_selected_archives_mode = False
elif self.move_down_selected_archives_mode: # move down selected archives
in the list
i = len(self.archives_to_use) - 1
while i >= 0:
if self.archives_to_use[i].selected:
if i + 1 < len(self.archives_to_use) and not
self.archives_to_use[i + 1].selected:
self.archives_to_use.move(i, i + 1)
i -= 1
self.move_down_selected_archives_mode = False
def execute(self, context):
archive_paths = [{"name": archive.path} for archive in
self.archives_to_use]
# additionally add currently selected archives
for file_name in self.ordered_files:
archive_paths.append({"name": os.path.join(self.directory, file_name)})
bpy.ops.converter_pix_wrapper.list_and_import("INVOKE_DEFAULT",
archive_paths=archive_paths,
only_convert=self.only_convert,
textures_to_base=self.textures_to_base)
return {'FINISHED'}
def invoke(self, context, event):
# quick check if BT are installed
if "io_scs_tools" not in context.preferences.addons:
self.report({"ERROR"}, "Can't run Converter PIX Wrapper! Please install
SCS Blender Tools add-on first & enable it!")
return {'CANCELLED'}
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def draw(self, context):
from io_scs_tools import SCS_TOOLS_OT_Import
from io_scs_tools.internals.containers.config import AsyncPathsInit
from io_scs_tools.utils import get_scs_globals
files_box = self.layout.box()
files_box.row().label(text="Extra Archives to Use:")
is_any_archive_selected = False
files_list_col = files_box.column(align=True)
if len(self.archives_to_use) > 0:
for archive in self.archives_to_use:
row = files_list_col.row(align=True)
path_col = row.column(align=True)
path_col.enabled = False
path_col.prop(archive, "path", text="")
row.prop(archive, "selected", text="", icon_only=True,
icon="CHECKMARK" if archive.selected else "BLANK1")
is_any_archive_selected |= archive.selected
else:
files_list_col.label(text="No extra archives!", icon="INFO")
# show controls of list only if sth is selected
if is_any_archive_selected:
row = files_list_col.row(align=True)
row.prop(self, "delete_selected_archives_mode", text="Remove",
icon="PANEL_CLOSE")
row.prop(self, "move_up_selected_archives_mode", text="Up",
icon="TRIA_UP")
row.prop(self, "move_down_selected_archives_mode", text="Down",
icon="TRIA_DOWN")
files_list_col.prop(self, "archives_to_use_mode", toggle=True, text="Add
Archives to List", icon='PASTEDOWN')
settings_col = self.layout.box().column(align=True)
settings_col.prop(self, "only_convert")
settings_col.prop(self, "textures_to_base")
if self.only_convert:
scs_globals = get_scs_globals()
# importer_version = round(import_pix.version(), 2)
layout = self.layout
# SCS Project Path
box1 = layout.box()
layout_box_col = box1.column(align=True)
layout_box_col.label(text='SCS Project Base Path:', icon='FILE_FOLDER')
layout_box_col.separator()
layout_box_row = layout_box_col.row(align=True)
layout_box_row.alert = not os.path.isdir(scs_globals.scs_project_path)
layout_box_row.prop(scs_globals, 'scs_project_path', text='')
layout_box_row = layout_box_col.row(align=True)
layout_box_row.prop(self, "scs_project_path_mode", toggle=True,
text="Set Current Dir as Project Base", icon='PASTEDOWN')
if AsyncPathsInit.is_running(): # report running path initialization
operator
layout_box_row = layout_box_col.row(align=True)
layout_box_row.label(text="Paths initialization in progress...")
layout_box_row.label(text="", icon='TIME')
else:
SCS_TOOLS_OT_Import.draw(self, context)
class CONV_PIX_WRAPPER_OT_UpdateEXE(bpy.types.Operator):
bl_idname = "world.converter_pix_wrapper_update_exe"
bl_label = "Update ConverterPIX Executable"
bl_description = "Not sure if your ConverterPIX is up-to date? Use this button
to download & update it!"
bl_options = {'INTERNAL'}
def execute(self, context):
if update_converter_pix():
self.report({"INFO"}, "ConverterPIX file updated!")
else:
self.report({"ERROR"}, "Problem updating ConverterPIX! Try again
later.")
return {'FINISHED'}
def menu_func_import(self, context):
self.layout.operator(CONV_PIX_WRAPPER_OT_Import.bl_idname, text="SCS Models -
ConverterPIX & BT (*.scs)")
classes = (
ConvPIXWrapperAddonPrefs,
ConvPIXWrapperFileEntry,
ConvPIXWrapperBrowserData,
ConvPIXWrapperArchiveToUse,
CONV_PIX_WRAPPER_UL_FileEntryItem,
CONV_PIX_WRAPPER_OT_ListImport,
CONV_PIX_WRAPPER_OT_Import,
CONV_PIX_WRAPPER_OT_UpdateEXE,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
# check if converter pix exists or it's not to more than 1 day old, otherwise
redownload it!
if not os.path.isfile(CONVERTER_PIX_PATH) or time() -
os.path.getmtime(CONVERTER_PIX_PATH) > 60 * 60 * 24:
t = Thread(name="update converterpix", target=update_converter_pix)
t.setDaemon(True)
t.start()
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
if __name__ == '__main__':
register()