Skip to content
Snippets Groups Projects
Commit df27bb5b authored by Jesse Kaukonen's avatar Jesse Kaukonen
Browse files

- Enabling Cycles uploading

- Enabling settings for defining things for Cycles rendering
- Adding some file format checking
- Adding some code for defining a few session settings via xmlrpc
- Making the UI more responsive
parent 538d6dda
Branches
Tags
No related merge requests found
...@@ -21,8 +21,8 @@ DEV = False ...@@ -21,8 +21,8 @@ DEV = False
bl_info = { bl_info = {
"name": "Renderfarm.fi", "name": "Renderfarm.fi",
"author": "Nathan Letwory <nathan@letworyinteractive.com>, Jesse Kaukonen <jesse.kaukonen@gmail.com>", "author": "Nathan Letwory <nathan@letworyinteractive.com>, Jesse Kaukonen <jesse.kaukonen@gmail.com>",
"version": (15,), "version": (16,),
"blender": (2, 6, 1), "blender": (2, 6, 2),
"location": "Render > Engine > Renderfarm.fi", "location": "Render > Engine > Renderfarm.fi",
"description": "Send .blend as session to http://www.renderfarm.fi to render", "description": "Send .blend as session to http://www.renderfarm.fi to render",
"warning": "", "warning": "",
...@@ -85,6 +85,7 @@ bpy.originalFileName = bpy.path.display_name_from_filepath(bpy.data.filepath) ...@@ -85,6 +85,7 @@ bpy.originalFileName = bpy.path.display_name_from_filepath(bpy.data.filepath)
bpy.particleBakeWarning = False bpy.particleBakeWarning = False
bpy.childParticleWarning = False bpy.childParticleWarning = False
bpy.simulationWarning = False bpy.simulationWarning = False
bpy.file_format_warning = False
bpy.ready = False bpy.ready = False
if False and DEV: if False and DEV:
...@@ -123,6 +124,9 @@ class ORESettings(bpy.types.PropertyGroup): ...@@ -123,6 +124,9 @@ class ORESettings(bpy.types.PropertyGroup):
longdesc = StringProperty(name='Description', description='Description of the scene (2k)', maxlen=2048, default='') longdesc = StringProperty(name='Description', description='Description of the scene (2k)', maxlen=2048, default='')
title = StringProperty(name='Title', description='Title for this session (128 characters)', maxlen=128, default='') title = StringProperty(name='Title', description='Title for this session (128 characters)', maxlen=128, default='')
url = StringProperty(name='Project URL', description='Project URL. Leave empty if not applicable', maxlen=256, default='') url = StringProperty(name='Project URL', description='Project URL. Leave empty if not applicable', maxlen=256, default='')
engine = StringProperty(name='Engine', description='The rendering engine that is used for rendering', maxlen=64, default='blender')
samples = IntProperty(name='Samples', description='Number of samples that is used (Cycles only)', min=1, max=1000000, soft_min=1, soft_max=100000, default=100)
file_format = StringProperty(name='File format', description='File format used for the rendering', maxlen=20, default='PNG_FORMAT')
parts = IntProperty(name='Parts/Frame', description='', min=1, max=1000, soft_min=1, soft_max=64, default=1) parts = IntProperty(name='Parts/Frame', description='', min=1, max=1000, soft_min=1, soft_max=64, default=1)
resox = IntProperty(name='Resolution X', description='X of render', min=1, max=10000, soft_min=1, soft_max=10000, default=1920) resox = IntProperty(name='Resolution X', description='X of render', min=1, max=10000, soft_min=1, soft_max=10000, default=1920)
...@@ -130,7 +134,7 @@ class ORESettings(bpy.types.PropertyGroup): ...@@ -130,7 +134,7 @@ class ORESettings(bpy.types.PropertyGroup):
memusage = IntProperty(name='Memory Usage', description='Estimated maximum memory usage during rendering in MB', min=1, max=6*1024, soft_min=1, soft_max=3*1024, default=256) memusage = IntProperty(name='Memory Usage', description='Estimated maximum memory usage during rendering in MB', min=1, max=6*1024, soft_min=1, soft_max=3*1024, default=256)
start = IntProperty(name='Start Frame', description='Start Frame', default=1) start = IntProperty(name='Start Frame', description='Start Frame', default=1)
end = IntProperty(name='End Frame', description='End Frame', default=250) end = IntProperty(name='End Frame', description='End Frame', default=250)
fps = IntProperty(name='FPS', description='FPS', min=1, max=256, default=25) fps = IntProperty(name='FPS', description='FPS', min=1, max=120, default=25)
prepared = BoolProperty(name='Prepared', description='Set to True if preparation has been run', default=False) prepared = BoolProperty(name='Prepared', description='Set to True if preparation has been run', default=False)
loginInserted = BoolProperty(name='LoginInserted', description='Set to True if user has logged in', default=False) loginInserted = BoolProperty(name='LoginInserted', description='Set to True if user has logged in', default=False)
...@@ -139,8 +143,8 @@ class ORESettings(bpy.types.PropertyGroup): ...@@ -139,8 +143,8 @@ class ORESettings(bpy.types.PropertyGroup):
selected_session = IntProperty(name='Selected Session', description='The selected session', default=0) selected_session = IntProperty(name='Selected Session', description='The selected session', default=0)
hasUnsupportedSimulation = BoolProperty(name='HasSimulation', description='Set to True if therea re unsupported simulations', default=False) hasUnsupportedSimulation = BoolProperty(name='HasSimulation', description='Set to True if therea re unsupported simulations', default=False)
inlicense = EnumProperty(items=licenses, name='source license', description='license speficied for the source files', default='1') inlicense = EnumProperty(items=licenses, name='Scene license', description='License speficied for the source files', default='1')
outlicense = EnumProperty(items=licenses, name='output license', description='license speficied for the output files', default='1') outlicense = EnumProperty(items=licenses, name='Product license', description='License speficied for the output files', default='1')
sessions = CollectionProperty(type=ORESession, name='Sessions', description='Sessions on Renderfarm.fi') sessions = CollectionProperty(type=ORESession, name='Sessions', description='Sessions on Renderfarm.fi')
completed_sessions = CollectionProperty(type=ORESession, name='Completed sessions', description='Sessions that have been already rendered') completed_sessions = CollectionProperty(type=ORESession, name='Completed sessions', description='Sessions that have been already rendered')
rejected_sessions = CollectionProperty(type=ORESession, name='Rejected sessions', description='Sessions that have been rejected') rejected_sessions = CollectionProperty(type=ORESession, name='Rejected sessions', description='Sessions that have been rejected')
...@@ -244,6 +248,31 @@ def changeSettings(): ...@@ -244,6 +248,31 @@ def changeSettings():
ore.end = sce.frame_end ore.end = sce.frame_end
ore.fps = rd.fps ore.fps = rd.fps
bpy.file_format_warning = False
bpy.simulationWarning = False
bpy.texturePackError = False
bpy.particleBakeWarning = False
bpy.childParticleWarning = False
if (rd.image_settings.file_format == 'HDR'):
rd.image_settings.file_format = 'PNG'
bpy.file_format_warning = True
# Convert between Blender's image format and BURP's formats
if (rd.image_settings.file_format == 'PNG'):
ore.file_format = 'PNG_FORMAT'
elif (rd.image_settings.file_format == 'OPEN_EXR'):
ore.file_format = 'EXR_FORMAT'
elif (rd.image_settings.file_format == 'OPEN_EXR_MULTILAYER'):
ore.file_format = 'EXR_MULTILAYER_FORMAT'
elif (rd.image_settings.file_format == 'HDR'):
ore.file_format = 'PNG_FORMAT'
else:
ore.file_format = 'PNG_FORMAT'
if (ore.engine == 'cycles'):
ore.samples = bpy.context.scene.cycles.samples
# Multipart support doesn' work if SSS is used # Multipart support doesn' work if SSS is used
if ((rd.use_sss == True and hasSSSMaterial()) and ore.parts > 1): if ((rd.use_sss == True and hasSSSMaterial()) and ore.parts > 1):
ore.parts = 1; ore.parts = 1;
...@@ -266,8 +295,13 @@ def prepareScene(): ...@@ -266,8 +295,13 @@ def prepareScene():
changeSettings() changeSettings()
ore.resox = rd.resolution_x
ore.resoy = rd.resolution_y
ore.fps = rd.fps
ore.start = sce.frame_start
ore.end = sce.frame_end
print("Packing external textures...") print("Packing external textures...")
# Pack all external textures
try: try:
bpy.ops.file.pack_all() bpy.ops.file.pack_all()
bpy.texturePackError = False bpy.texturePackError = False
...@@ -315,20 +349,34 @@ class OpSwitchRenderfarm(bpy.types.Operator): ...@@ -315,20 +349,34 @@ class OpSwitchRenderfarm(bpy.types.Operator):
def execute(self, context): def execute(self, context):
changeSettings() changeSettings()
if (bpy.context.scene.render.engine == 'CYCLES'):
bpy.context.scene.ore_render.engine = 'cycles'
else:
bpy.context.scene.ore_render.engine = 'blender'
bpy.context.scene.render.engine = 'RENDERFARMFI_RENDER' bpy.context.scene.render.engine = 'RENDERFARMFI_RENDER'
return {'FINISHED'} return {'FINISHED'}
class OpSwitchBlenderRender(bpy.types.Operator): class OpSwitchBlenderRender(bpy.types.Operator):
bl_label = "Switch to Blender Render" bl_label = "Switch to local render"
bl_idname = "ore.switch_to_blender_render" bl_idname = "ore.switch_to_local_render"
def execute(self, context): def execute(self, context):
bpy.context.scene.render.engine = 'BLENDER_RENDER' rd = bpy.context.scene.render
return {'FINISHED'} ore = bpy.context.scene.ore_render
rd.resolution_x = ore.resox
rd.resolution_y = ore.resoy
rd.fps = ore.fps
bpy.context.scene.frame_start = ore.start
bpy.context.scene.frame_end = ore.end
if (bpy.context.scene.ore_render.engine == 'cycles'):
bpy.context.scene.render.engine = 'CYCLES'
else:
bpy.context.scene.render.engine = 'BLENDER_RENDER'
return {'FINISHED'}
# Copies start & end frame + others from render settings to ore settings # Copies start & end frame + others from render settings to ore settings
class OpCopySettings(bpy.types.Operator): class OpCopySettings(bpy.types.Operator):
bl_label = "Copy from Blender Render settings" bl_label = "Copy settings from current scene"
bl_idname = "ore.copy_settings" bl_idname = "ore.copy_settings"
def execute(self, context): def execute(self, context):
...@@ -342,31 +390,6 @@ class OpCopySettings(bpy.types.Operator): ...@@ -342,31 +390,6 @@ class OpCopySettings(bpy.types.Operator):
ore.fps = rd.fps ore.fps = rd.fps
return {'FINISHED'} return {'FINISHED'}
# We re-write the default render panel (not enabled, breaks Cycles)
'''class RENDER_PT_render(RenderButtonsPanel, bpy.types.Panel):
bl_label = "Render"
COMPAT_ENGINES = {'BLENDER_RENDER'}
def draw(self, context):
layout = self.layout
rd = context.scene.render
row = layout.row()
row.operator("ore.switch_to_renderfarm_render", text="Renderfarm.fi", icon='WORLD')
row.operator("ore.switch_to_blender_render", text="Blender Render", icon='BLENDER')
row = layout.row()
if (bpy.context.scene.render.engine == 'BLENDER_RENDER'):
row.operator("render.render", text="Image", icon='RENDER_STILL')
row.operator("render.render", text="Animation", icon='RENDER_ANIMATION').animation = True
layout.prop(rd, "display_mode", text="Display")
else:
if bpy.found_newer_version == True:
layout.operator('ore.open_download_location')
else:
if bpy.up_to_date == True:
layout.label(text='You have the latest version')
layout.operator('ore.check_update')
'''
class EngineSelectPanel(bpy.types.Panel): class EngineSelectPanel(bpy.types.Panel):
bl_idname = "OBJECT_PT_engineSelectPanel" bl_idname = "OBJECT_PT_engineSelectPanel"
bl_label = "Choose rendering mode" bl_label = "Choose rendering mode"
...@@ -379,7 +402,7 @@ class EngineSelectPanel(bpy.types.Panel): ...@@ -379,7 +402,7 @@ class EngineSelectPanel(bpy.types.Panel):
rd = context.scene.render rd = context.scene.render
row = layout.row() row = layout.row()
row.operator("ore.switch_to_renderfarm_render", text="Renderfarm.fi", icon='WORLD') row.operator("ore.switch_to_renderfarm_render", text="Renderfarm.fi", icon='WORLD')
row.operator("ore.switch_to_blender_render", text="Blender Render", icon='BLENDER') row.operator("ore.switch_to_local_render", text="Local computer", icon='BLENDER')
row = layout.row() row = layout.row()
if (bpy.context.scene.render.engine == 'RENDERFARMFI_RENDER'): if (bpy.context.scene.render.engine == 'RENDERFARMFI_RENDER'):
if bpy.found_newer_version == True: if bpy.found_newer_version == True:
...@@ -489,6 +512,50 @@ class RENDER_PT_RenderfarmFi(RenderButtonsPanel, bpy.types.Panel): ...@@ -489,6 +512,50 @@ class RENDER_PT_RenderfarmFi(RenderButtonsPanel, bpy.types.Panel):
layout.label(text="Example: blue skies hero castle flowers grass particles") layout.label(text="Example: blue skies hero castle flowers grass particles")
layout.prop(ore, 'url') layout.prop(ore, 'url')
layout.label(text="Example: www.sintel.org") layout.label(text="Example: www.sintel.org")
layout.label(text="Please verify your settings", icon='MODIFIER')
row = layout.row()
#row.operator('ore.copy_settings')
#row = layout.row()
layout.label(text="Rendering engine")
row = layout.row()
if (ore.engine == 'blender'):
row.operator('ore.use_blender_render', icon='FILE_TICK')
row.operator('ore.use_cycles_render')
elif (ore.engine == 'cycles' ):
row.operator('ore.use_blender_render')
row.operator('ore.use_cycles_render', icon='FILE_TICK')
else:
row.operator('ore.use_blender_render', icon='FILE_TICK')
row.operator('ore.use_cycles_render')
row = layout.row()
layout.separator()
row = layout.row()
row.prop(ore, 'resox')
row.prop(ore, 'resoy')
row = layout.row()
row.prop(ore, 'start')
row.prop(ore, 'end')
row = layout.row()
row.prop(ore, 'fps')
row = layout.row()
if (ore.engine == 'cycles'):
row.prop(ore, 'samples')
row = layout.row()
row.prop(ore, 'memusage')
#row.prop(ore, 'parts')
layout.separator()
row = layout.row()
layout.label(text="Licenses", icon='FILE_REFRESH')
row = layout.row()
row.prop(ore, 'inlicense')
row = layout.row()
row.prop(ore, 'outlicense')
checkStatus(ore) checkStatus(ore)
if (len(bpy.errors) > 0): if (len(bpy.errors) > 0):
bpy.ready = False bpy.ready = False
...@@ -496,7 +563,7 @@ class RENDER_PT_RenderfarmFi(RenderButtonsPanel, bpy.types.Panel): ...@@ -496,7 +563,7 @@ class RENDER_PT_RenderfarmFi(RenderButtonsPanel, bpy.types.Panel):
bpy.ready = True bpy.ready = True
class UPLOAD_PT_RenderfarmFi(RenderButtonsPanel, bpy.types.Panel): class UPLOAD_PT_RenderfarmFi(RenderButtonsPanel, bpy.types.Panel):
bl_label = "Upload" bl_label = "Upload to www.renderfarm.fi"
COMPAT_ENGINES = set(['RENDERFARMFI_RENDER']) COMPAT_ENGINES = set(['RENDERFARMFI_RENDER'])
@classmethod @classmethod
...@@ -525,48 +592,24 @@ class UPLOAD_PT_RenderfarmFi(RenderButtonsPanel, bpy.types.Panel): ...@@ -525,48 +592,24 @@ class UPLOAD_PT_RenderfarmFi(RenderButtonsPanel, bpy.types.Panel):
layout.separator() layout.separator()
layout.label(text="Please verify your settings", icon='MODIFIER')
row = layout.row()
row.operator('ore.copy_settings')
row = layout.row()
row.label(text="Resolution: " + str(ore.resox) + "x" + str(ore.resoy))
row = layout.row()
row.label(text="Frames: " + str(ore.start) + " - " + str(ore.end))
row = layout.row()
if (ore.start == ore.end):
row.label(text="You have selected only 1 frame to be rendered", icon='ERROR')
row = layout.row()
row.label(text="Renderfarm.fi does not render stills - only animations")
row = layout.row()
row.label(text="Frame rate: " + str(ore.fps))
row = layout.row()
layout.separator()
layout.label(text="Optional advanced settings", icon='MODIFIER')
row = layout.row()
row.prop(ore, 'memusage')
#row.prop(ore, 'parts')
layout.separator()
row = layout.row()
layout.label(text="Licenses", icon='FILE_REFRESH')
row = layout.row()
row.prop(ore, 'inlicense')
row.prop(ore, 'outlicense')
row = layout.row() row = layout.row()
if (bpy.uploadInProgress == True): if (bpy.uploadInProgress == True):
layout.label(text="Attempting upload...") layout.label(text="------------------------")
layout.label(text="- Attempting upload... -")
layout.label(text="------------------------")
if (bpy.file_format_warning == True):
layout.label(text="Your output format is HDR", icon='ERROR')
layout.label(text="Right now we don't support this file format")
layout.label(text="File format will be changed to PNG")
if (bpy.texturePackError): if (bpy.texturePackError):
layout.label(text="There was an error in packing external textures", icon='ERROR') layout.label(text="There was an error in packing external textures", icon='ERROR')
layout.label(text="Make sure that all your textures exist on your computer") layout.label(text="Make sure that all your textures exist on your computer")
layout.label(text="The render will still work, but won't have the missing textures") layout.label(text="The render will still work, but won't have the missing textures")
layout.label(text="You may want to cancel your render above") layout.label(text="You may want to cancel your render above in \"My sessions\"")
if (bpy.linkedFileError): if (bpy.linkedFileError):
layout.label(text="There was an error in appending linked .blend files", icon='ERROR') layout.label(text="There was an error in appending linked .blend files", icon='ERROR')
layout.label(text="Your render might not have all the external content") layout.label(text="Your render might not have all the external content")
layout.label(text="You may want to cancel your render above") layout.label(text="You may want to cancel your render above in \"My sessions\"")
if (bpy.particleBakeWarning): if (bpy.particleBakeWarning):
layout.label(text="You have a particle simulation", icon='ERROR') layout.label(text="You have a particle simulation", icon='ERROR')
layout.label(text="All Emitter type particles must be baked") layout.label(text="All Emitter type particles must be baked")
...@@ -585,7 +628,8 @@ class UPLOAD_PT_RenderfarmFi(RenderButtonsPanel, bpy.types.Panel): ...@@ -585,7 +628,8 @@ class UPLOAD_PT_RenderfarmFi(RenderButtonsPanel, bpy.types.Panel):
if (errorTime > 4): if (errorTime > 4):
bpy.infoError = False bpy.infoError = False
bpy.errorStartTime = -1 bpy.errorStartTime = -1
layout.label(text="Blender may seem frozen during the upload!", icon='LAMP') layout.label(text="Warning:", icon='LAMP')
layout.label(text="Blender may seem frozen during the upload!")
row.operator('ore.reset', icon='FILE_REFRESH') row.operator('ore.reset', icon='FILE_REFRESH')
else: else:
layout.label(text="Fill the scene information first") layout.label(text="Fill the scene information first")
...@@ -608,10 +652,16 @@ def encode_multipart_data(data, files): ...@@ -608,10 +652,16 @@ def encode_multipart_data(data, files):
def encode_file(field_name): def encode_file(field_name):
filename = files [field_name] filename = files [field_name]
fcontent = None
print('encoding', field_name)
try:
fcontent = str(open(filename, 'rb').read(), encoding='iso-8859-1')
except Exception:
print('Trouble in paradise')
return ('--' + boundary, return ('--' + boundary,
'Content-Disposition: form-data; name="%s"; filename="%s"' % (field_name, filename), 'Content-Disposition: form-data; name="%s"; filename="%s"' % (field_name, filename),
'Content-Type: %s' % get_content_type(filename), 'Content-Type: %s' % get_content_type(filename),
'', str(open(filename, 'rb').read(), encoding='iso-8859-1')) '', fcontent)
lines = [] lines = []
for name in data: for name in data:
...@@ -619,10 +669,13 @@ def encode_multipart_data(data, files): ...@@ -619,10 +669,13 @@ def encode_multipart_data(data, files):
for name in files: for name in files:
lines.extend(encode_file(name)) lines.extend(encode_file(name))
lines.extend(('--%s--' % boundary, '')) lines.extend(('--%s--' % boundary, ''))
print("joining lines into body")
body = '\r\n'.join(lines) body = '\r\n'.join(lines)
headers = {'content-type': 'multipart/form-data; boundary=' + boundary, headers = {'content-type': 'multipart/form-data; boundary=' + boundary,
'content-length': str(len(body))} 'content-length': str(len(body))}
print("headers and body ready")
return body, headers return body, headers
...@@ -686,10 +739,10 @@ def ore_upload(op, context): ...@@ -686,10 +739,10 @@ def ore_upload(op, context):
key = res['key'] key = res['key']
userid = res['userId'] userid = res['userId']
print("Creating server proxy") print("Creating server proxy")
proxy = xmlrpc.client.ServerProxy(rffi_xmlrpc, verbose=DEV) proxy = xmlrpc.client.ServerProxy(rffi_xmlrpc, verbose=DEV) #r'http://xmlrpc.renderfarm.fi/session')
proxy._ServerProxy__transport.user_agent = 'Renderfarm.fi Uploader/%s' % (bpy.CURRENT_VERSION) proxy._ServerProxy__transport.user_agent = 'Renderfarm.fi Uploader/%s' % (bpy.CURRENT_VERSION)
print("Creating a new session") print("Creating a new session")
res = proxy.session.createSession(userid, key) res = proxy.session.createSession(userid, key) # This may use an existing, non-rendered session. Prevents spamming in case the upload fails for some reason
sessionid = res['sessionId'] sessionid = res['sessionId']
key = res['key'] key = res['key']
print("Session id is " + str(sessionid)) print("Session id is " + str(sessionid))
...@@ -709,7 +762,13 @@ def ore_upload(op, context): ...@@ -709,7 +762,13 @@ def ore_upload(op, context):
res = proxy.session.setXSize(userid, res['key'], sessionid, ore.resox) res = proxy.session.setXSize(userid, res['key'], sessionid, ore.resox)
res = proxy.session.setYSize(userid, res['key'], sessionid, ore.resoy) res = proxy.session.setYSize(userid, res['key'], sessionid, ore.resoy)
res = proxy.session.setFrameRate(userid, res['key'], sessionid, ore.fps) res = proxy.session.setFrameRate(userid, res['key'], sessionid, ore.fps)
res = proxy.session.setRenderer(userid, res['key'], sessionid, 'blender') res = proxy.session.setFrameFormat(userid, res['key'], sessionid, ore.file_format)
res = proxy.session.setRenderer(userid, res['key'], sessionid, ore.engine)
res = proxy.session.setSamples(userid, res['key'], sessionid, ore.samples)
if (ore.engine == 'cycles'):
res = proxy.session.setReplication(userid, res['key'], sessionid, 1)
else:
res = proxy.session.setReplication(userid, res['key'], sessionid, 3)
res = proxy.session.setOutputLicense(userid, res['key'], sessionid, int(ore.outlicense)) res = proxy.session.setOutputLicense(userid, res['key'], sessionid, int(ore.outlicense))
res = proxy.session.setInputLicense(userid, res['key'], sessionid, int(ore.inlicense)) res = proxy.session.setInputLicense(userid, res['key'], sessionid, int(ore.inlicense))
print("Setting primary input file") print("Setting primary input file")
...@@ -1087,6 +1146,22 @@ class ORE_UseBlenderReso(bpy.types.Operator): ...@@ -1087,6 +1146,22 @@ class ORE_UseBlenderReso(bpy.types.Operator):
ore.fps = rd.fps ore.fps = rd.fps
return {'FINISHED'} return {'FINISHED'}
class ORE_UseCyclesRender(bpy.types.Operator):
bl_idname = "ore.use_cycles_render"
bl_label = "Cycles"
def execute(self, context):
context.scene.ore_render.engine = 'cycles'
return {'FINISHED'}
class ORE_UseBlenderRender(bpy.types.Operator):
bl_idname = "ore.use_blender_render"
bl_label = "Blender Internal"
def execute(self, context):
context.scene.ore_render.engine = 'blender'
return {'FINISHED'}
class ORE_ChangeUser(bpy.types.Operator): class ORE_ChangeUser(bpy.types.Operator):
bl_idname = "ore.change_user" bl_idname = "ore.change_user"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment