Init
Init
Init
"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
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 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
st = os.stat(CONVERTER_PIX_PATH)
os.chmod(CONVERTER_PIX_PATH, st.st_mode | S_IEXEC | S_IXGRP)
except Exception as e:
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
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, []
decoded_result = result.stdout.decode("utf-8").split(LINE_SPLITTER)
: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 = []
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!")
class ConvPIXWrapperAddonPrefs(AddonPreferences):
bl_idname = __name__
class ConvPIXWrapperFileEntry(bpy.types.PropertyGroup):
"""Property group holding browser file entry data."""
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 False
active_file_entry = self.file_entries[self.active_entry]
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
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."""
class CONV_PIX_WRAPPER_UL_FileEntryItem(bpy.types.UIList):
"""Class for drawing archive browser file entry."""
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)
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
)
# prepare browsers data and forcly trigger update, to load up root archive
entries
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
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)
anim_archive_subpaths.append(path_join(self.anim_browser_data.current_subpath,
anim_file_entry.name[:-4]))
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)
return {'CANCELLED'}
os.makedirs(file_dstdir, exist_ok=True)
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
browser_layout = layout.split(factor=self.browsers_slider)
left_column = browser_layout.column(align=True)
right_column = browser_layout.column(align=True)
# 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,
)
directory: StringProperty()
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
)
# 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)
get_scs_globals().scs_project_path = os.path.dirname(self.filepath)
self.scs_project_path_mode = False
# 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
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
i = 0
while i < len(self.archives_to_use):
if self.archives_to_use[i].selected:
i += 1
self.move_up_selected_archives_mode = False
i = len(self.archives_to_use) - 1
while i >= 0:
if self.archives_to_use[i].selected:
i -= 1
self.move_down_selected_archives_mode = False
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'}
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
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:
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")
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
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')
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'}
if update_converter_pix():
self.report({"INFO"}, "ConverterPIX file updated!")
else:
self.report({"ERROR"}, "Problem updating ConverterPIX! Try again
later.")
return {'FINISHED'}
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:
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()