Repository for mbEditorPro 2.0
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

644 lines
27 KiB

# • ▌ ▄ ·. ▄▄▄▄· ▄▄▄ .·▄▄▄▄ ▪ ▄▄▄▄▄ ▄▄▄ ▄▄▄·▄▄▄
# ·██ ▐███▪▐█ ▀█▪ ▀▄.▀·██▪ ██ ██ •██ ▪ ▀▄ █· ▐█ ▄█▀▄ █·▪
# ▐█ ▌▐▌▐█·▐█▀▀█▄ ▐▀▀▪▄▐█· ▐█▌▐█· ▐█.▪ ▄█▀▄ ▐▀▀▄ ██▀·▐▀▀▄ ▄█▀▄
# ██ ██▌▐█▌██▄▪▐█ ▐█▄▄▌██. ██ ▐█▌ ▐█▌·▐█▌.▐▌▐█•█▌ ▐█▪·•▐█•█▌▐█▌.▐▌
# ▀▀ █▪▀▀▀·▀▀▀▀ ▀▀▀ ▀▀▀▀▀• ▀▀▀ ▀▀▀ ▀█▄▀▪.▀ ▀ .▀ .▀ ▀ ▀█▄▀▪
# Magicbane Emulator Project © 2013 - 2022
# www.magicbane.com
from collections import OrderedDict
from arcane.util import ResStream
TRACKER_TO_STRING = {
0: 'NONE',
1: 'XY',
2: 'Y',
}
STRING_TO_TRACKER = {value: key for key, value in TRACKER_TO_STRING.items()}
TRANSPARENT_TO_STRING = {
0: 'NONE',
1: 'PINK',
2: 'BLACK',
3: 'WHITE',
4: 'SEMI',
6: 'ALPHA',
}
STRING_TO_TRANSPARENT = {value: key for key, value in TRANSPARENT_TO_STRING.items()}
TEXTURE_TO_STRING = {
0: 'SINGLE_TEXTURE',
1: 'COLOR_TEXTURE',
3: 'ANIMATED_TEXTURE',
}
STRING_TO_TEXTURE = {value: key for key, value in TEXTURE_TO_STRING.items()}
LIGHT_TYPE_TO_STRING = {
0xb6787258: 'ArcLightPoint',
0x54e8ff1d: 'ArcLightAffectorAttach',
0xa73bd9d4: 'ArcLightAffectorFlicker',
}
STRING_TO_LIGHT_TYPE = {value: key for key, value in LIGHT_TYPE_TO_STRING.items()}
class ArcSinglePolyMesh:
def load_binary(self, stream: ResStream):
self.polymesh_id = stream.read_qword()
self.polymesh_decal = stream.read_bool()
self.polymesh_double_sided = stream.read_bool()
def save_binary(self, stream: ResStream):
stream.write_qword(self.polymesh_id)
stream.write_bool(self.polymesh_decal)
stream.write_bool(self.polymesh_double_sided)
def load_json(self, data):
self.polymesh_id = data['polymesh_id']
self.polymesh_decal = data['polymesh_decal']
self.polymesh_double_sided = data['polymesh_double_sided']
def save_json(self):
data = OrderedDict()
data['polymesh_id'] = self.polymesh_id
data['polymesh_decal'] = self.polymesh_decal
data['polymesh_double_sided'] = self.polymesh_double_sided
return data
class ArcMeshSet:
def load_binary(self, stream: ResStream):
num = stream.read_dword()
self.mesh_set = [ArcSinglePolyMesh() for _ in range(num)]
for mesh in self.mesh_set:
mesh.load_binary(stream)
def save_binary(self, stream: ResStream):
stream.write_dword(len(self.mesh_set))
for mesh in self.mesh_set:
mesh.save_binary(stream)
def load_json(self, data):
self.mesh_set = []
for mesh_data in data['mesh_set']:
mesh = ArcSinglePolyMesh()
mesh.load_json(mesh_data)
self.mesh_set.append(mesh)
def save_json(self):
data = OrderedDict()
data['mesh_set'] = []
for mesh in self.mesh_set:
data['mesh_set'].append(mesh.save_json())
return data
class ArcRenderTemplate:
def load_binary(self, stream: ResStream):
self.template_object_can_fade = stream.read_bool()
self.template_tracker = stream.read_dword()
self.template_illuminated = stream.read_bool()
self.template_bone_length = stream.read_float()
self.template_clip_map = stream.read_dword()
self.template_light_two_side = stream.read_dword()
self.template_cull_face = stream.read_dword()
self.template_specular_map = stream.read_qword()
self.template_shininess = stream.read_float()
self.template_has_mesh = stream.read_bool()
if self.template_has_mesh:
self.template_mesh = ArcMeshSet()
self.template_mesh.load_binary(stream)
def save_binary(self, stream: ResStream):
stream.write_bool(self.template_object_can_fade)
stream.write_dword(self.template_tracker)
stream.write_bool(self.template_illuminated)
stream.write_float(self.template_bone_length)
stream.write_dword(self.template_clip_map)
stream.write_dword(self.template_light_two_side)
stream.write_dword(self.template_cull_face)
stream.write_qword(self.template_specular_map)
stream.write_float(self.template_shininess)
stream.write_bool(self.template_has_mesh)
if self.template_has_mesh:
self.template_mesh.save_binary(stream)
def load_json(self, data):
self.template_object_can_fade = data['template_object_can_fade']
self.template_tracker = STRING_TO_TRACKER[data['template_tracker']]
self.template_illuminated = data['template_illuminated']
self.template_bone_length = data['template_bone_length']
self.template_clip_map = data['template_clip_map']
self.template_light_two_side = data['template_light_two_side']
self.template_cull_face = data['template_cull_face']
self.template_specular_map = data['template_specular_map']
self.template_shininess = data['template_shininess']
self.template_has_mesh = data['template_has_mesh']
if self.template_has_mesh:
self.template_mesh = ArcMeshSet()
self.template_mesh.load_json(data['template_mesh'])
def save_json(self):
data = OrderedDict()
data['template_object_can_fade'] = self.template_object_can_fade
data['template_tracker'] = TRACKER_TO_STRING[self.template_tracker]
data['template_illuminated'] = self.template_illuminated
data['template_bone_length'] = self.template_bone_length
data['template_clip_map'] = self.template_clip_map
data['template_light_two_side'] = self.template_light_two_side
data['template_cull_face'] = self.template_cull_face
data['template_specular_map'] = self.template_specular_map
data['template_shininess'] = self.template_shininess
data['template_has_mesh'] = self.template_has_mesh
if self.template_has_mesh:
data['template_mesh'] = self.template_mesh.save_json()
return data
class ArcSingleTexture:
def load_binary(self, stream: ResStream):
self.texture_id = stream.read_qword()
self.texture_transparent = stream.read_dword()
self.texture_compress = stream.read_bool()
self.texture_normal_map = stream.read_bool()
self.texture_detail_normal_map = stream.read_bool()
self.texture_create_mip_maps = stream.read_bool()
self.texture_x0 = stream.read_string()
self.texture_x1 = stream.read_string()
self.texture_x2 = stream.read_dword()
self.texture_x3 = stream.read_dword()
self.texture_x4 = stream.read_bool()
self.texture_wrap = stream.read_bool()
def save_binary(self, stream: ResStream):
stream.write_qword(self.texture_id)
stream.write_dword(self.texture_transparent)
stream.write_bool(self.texture_compress)
stream.write_bool(self.texture_normal_map)
stream.write_bool(self.texture_detail_normal_map)
stream.write_bool(self.texture_create_mip_maps)
stream.write_string(self.texture_x0)
stream.write_string(self.texture_x1)
stream.write_dword(self.texture_x2)
stream.write_dword(self.texture_x3)
stream.write_bool(self.texture_x4)
stream.write_bool(self.texture_wrap)
def load_json(self, data):
self.texture_id = data['texture_id']
self.texture_transparent = STRING_TO_TRANSPARENT[data['texture_transparent']]
self.texture_compress = data['texture_compress']
self.texture_normal_map = data['texture_normal_map']
self.texture_detail_normal_map = data['texture_detail_normal_map']
self.texture_create_mip_maps = data['texture_create_mip_maps']
self.texture_x0 = ''
self.texture_x1 = ''
self.texture_x2 = 255
self.texture_x3 = 0
self.texture_x4 = False
self.texture_wrap = data['texture_wrap']
def save_json(self):
data = OrderedDict()
data['texture_id'] = self.texture_id
data['texture_transparent'] = TRANSPARENT_TO_STRING[self.texture_transparent]
data['texture_compress'] = self.texture_compress
data['texture_normal_map'] = self.texture_normal_map
data['texture_detail_normal_map'] = self.texture_detail_normal_map
data['texture_create_mip_maps'] = self.texture_create_mip_maps
data['texture_wrap'] = self.texture_wrap
return data
class ArcColorTexture(ArcSingleTexture):
pass
class ArcAnimatedTexture:
def load_binary(self, stream: ResStream):
self.animated_texture_id = stream.read_qword()
self.animated_texture_transparent = stream.read_dword()
self.animated_texture_compress = stream.read_bool()
self.animated_texture_normal_map = stream.read_bool()
self.animated_texture_detail_normal_map = stream.read_bool()
self.animated_texture_create_mip_maps = stream.read_bool()
self.animated_texture_frame_timer = stream.read_float()
self.animated_texture_x0 = stream.read_float()
self.animated_texture_frame_rand = stream.read_dword()
num = stream.read_dword()
self.animated_texture_sets = [ArcTextureSet() for _ in range(num)]
for texture in self.animated_texture_sets:
texture.load_binary(stream)
def save_binary(self, stream: ResStream):
stream.write_qword(self.animated_texture_id)
stream.write_dword(self.animated_texture_transparent)
stream.write_bool(self.animated_texture_compress)
stream.write_bool(self.animated_texture_normal_map)
stream.write_bool(self.animated_texture_detail_normal_map)
stream.write_bool(self.animated_texture_create_mip_maps)
stream.write_float(self.animated_texture_frame_timer)
stream.write_float(self.animated_texture_x0)
stream.write_dword(self.animated_texture_frame_rand)
stream.write_dword(len(self.animated_texture_sets))
for texture in self.animated_texture_sets:
texture.save_binary(stream)
def load_json(self, data):
self.animated_texture_id = data['animated_texture_id']
self.animated_texture_transparent = STRING_TO_TRANSPARENT[data['animated_texture_transparent']]
self.animated_texture_compress = data['animated_texture_compress']
self.animated_texture_normal_map = data['animated_texture_normal_map']
self.animated_texture_detail_normal_map = data['animated_texture_detail_normal_map']
self.animated_texture_create_mip_maps = data['animated_texture_create_mip_maps']
self.animated_texture_frame_timer = data['animated_texture_frame_timer']
self.animated_texture_x0 = 0.0
self.animated_texture_frame_rand = data['animated_texture_frame_rand']
self.animated_texture_sets = []
for texture_data in data['animated_texture_sets']:
texture = ArcTextureSet()
texture.load_json(texture_data)
self.animated_texture_sets.append(texture)
def save_json(self):
data = OrderedDict()
data['animated_texture_id'] = self.animated_texture_id
data['animated_texture_transparent'] = TRANSPARENT_TO_STRING[self.animated_texture_transparent]
data['animated_texture_compress'] = self.animated_texture_compress
data['animated_texture_normal_map'] = self.animated_texture_normal_map
data['animated_texture_detail_normal_map'] = self.animated_texture_detail_normal_map
data['animated_texture_create_mip_maps'] = self.animated_texture_create_mip_maps
data['animated_texture_frame_timer'] = self.animated_texture_frame_timer
data['animated_texture_frame_rand'] = self.animated_texture_frame_rand
data['animated_texture_sets'] = []
for texture in self.animated_texture_sets:
data['animated_texture_sets'].append(texture.save_json())
return data
class ArcTextureSet:
def load_binary(self, stream: ResStream):
self.texture_type = stream.read_dword()
if self.texture_type == 0:
self.texture_data = ArcSingleTexture()
elif self.texture_type == 1:
self.texture_data = ArcColorTexture()
elif self.texture_type == 3:
self.texture_data = ArcAnimatedTexture()
self.texture_data.load_binary(stream)
def save_binary(self, stream: ResStream):
stream.write_dword(self.texture_type)
self.texture_data.save_binary(stream)
def load_json(self, data):
self.texture_type = STRING_TO_TEXTURE[data['texture_type']]
if self.texture_type == 0:
self.texture_data = ArcSingleTexture()
elif self.texture_type == 1:
self.texture_data = ArcColorTexture()
elif self.texture_type == 3:
self.texture_data = ArcAnimatedTexture()
self.texture_data.load_json(data['texture_data'])
def save_json(self):
data = OrderedDict()
data['texture_type'] = TEXTURE_TO_STRING[self.texture_type]
data['texture_data'] = self.texture_data.save_json()
return data
class ArcLightPoint:
def load_binary(self, stream: ResStream):
self.lightpoint_x0 = stream.read_dword()
self.lightpoint_x1 = stream.read_bool()
self.lightpoint_shader = stream.read_bool()
self.lightpoint_update_offscreen = stream.read_bool()
self.lightpoint_radius = stream.read_float()
self.lightpoint_position = stream.read_tuple()
self.lightpoint_diffuse_color = [stream.read_float() for _ in range(4)]
self.lightpoint_x2 = stream.read_dword()
self.lightpoint_orientation = [stream.read_float() for _ in range(4)]
self.lightpoint_cubemap = stream.read_dword()
self.lightpoint_x3 = stream.read_bool()
def save_binary(self, stream: ResStream):
stream.write_dword(self.lightpoint_x0)
stream.write_bool(self.lightpoint_x1)
stream.write_bool(self.lightpoint_shader)
stream.write_bool(self.lightpoint_update_offscreen)
stream.write_float(self.lightpoint_radius)
stream.write_tuple(self.lightpoint_position)
for i in range(4):
stream.write_float(self.lightpoint_diffuse_color[i])
stream.write_dword(self.lightpoint_x2)
for i in range(4):
stream.write_float(self.lightpoint_orientation[i])
stream.write_dword(self.lightpoint_cubemap)
stream.write_bool(self.lightpoint_x3)
def load_json(self, data):
self.lightpoint_x0 = 1
self.lightpoint_x1 = True
self.lightpoint_shader = data['lightpoint_shader']
self.lightpoint_update_offscreen = data['lightpoint_update_offscreen']
self.lightpoint_radius = data['lightpoint_radius']
self.lightpoint_position = data['lightpoint_position']
self.lightpoint_diffuse_color = data['lightpoint_diffuse_color']
self.lightpoint_x2 = 1
self.lightpoint_orientation = data['lightpoint_orientation']
self.lightpoint_cubemap = data['lightpoint_cubemap']
self.lightpoint_x3 = False
def save_json(self):
data = OrderedDict()
data['lightpoint_shader'] = self.lightpoint_shader
data['lightpoint_update_offscreen'] = self.lightpoint_update_offscreen
data['lightpoint_radius'] = self.lightpoint_radius
data['lightpoint_position'] = self.lightpoint_position
data['lightpoint_diffuse_color'] = self.lightpoint_diffuse_color
data['lightpoint_orientation'] = self.lightpoint_orientation
data['lightpoint_cubemap'] = self.lightpoint_cubemap
return data
class ArcLightAffectorAttach:
def load_binary(self, stream: ResStream):
self.attach_x0 = stream.read_dword()
self.attach_offset = stream.read_tuple()
def save_binary(self, stream: ResStream):
stream.write_dword(self.attach_x0)
stream.write_tuple(self.attach_offset)
def load_json(self, data):
self.attach_x0 = 1
self.attach_offset = data['attach_offset']
def save_json(self):
data = OrderedDict()
data['attach_offset'] = self.attach_offset
return data
class ArcLightAffectorFlicker:
def load_binary(self, stream: ResStream):
self.flicker_x0 = stream.read_dword()
self.flicker_avg_period = stream.read_float()
self.flicker_std_dev_radius = stream.read_float()
self.flicker_std_dev_period = stream.read_float()
self.flicker_falloff = stream.read_float()
def save_binary(self, stream: ResStream):
stream.write_dword(self.flicker_x0)
stream.write_float(self.flicker_avg_period)
stream.write_float(self.flicker_std_dev_radius)
stream.write_float(self.flicker_std_dev_period)
stream.write_float(self.flicker_falloff)
def load_json(self, data):
self.flicker_x0 = 1
self.flicker_avg_period = data['flicker_avg_period']
self.flicker_std_dev_radius = data['flicker_std_dev_radius']
self.flicker_std_dev_period = data['flicker_std_dev_period']
self.flicker_falloff = data['flicker_falloff']
def save_json(self):
data = OrderedDict()
data['flicker_avg_period'] = self.flicker_avg_period
data['flicker_std_dev_radius'] = self.flicker_std_dev_radius
data['flicker_std_dev_period'] = self.flicker_std_dev_period
data['flicker_falloff'] = self.flicker_falloff
return data
class ArcLightAffectors:
def load_binary(self, stream: ResStream):
self.light_affector_type = stream.read_dword()
if self.light_affector_type == 0x54e8ff1d:
self.light_affector_data = ArcLightAffectorAttach()
elif self.light_affector_type == 0xa73bd9d4:
self.light_affector_data = ArcLightAffectorFlicker()
self.light_affector_data.load_binary(stream)
self.light_affector_0xdaed = stream.read_dword()
def save_binary(self, stream: ResStream):
stream.write_dword(self.light_affector_type)
self.light_affector_data.save_binary(stream)
stream.write_dword(self.light_affector_0xdaed)
def load_json(self, data):
self.light_affector_type = STRING_TO_LIGHT_TYPE[data['light_affector_type']]
if self.light_affector_type == 0x54e8ff1d:
self.light_affector_data = ArcLightAffectorAttach()
elif self.light_affector_type == 0xa73bd9d4:
self.light_affector_data = ArcLightAffectorFlicker()
self.light_affector_data.load_json(data['light_affector_data'])
self.light_affector_0xdaed = 0xddaaeedd
def save_json(self):
data = OrderedDict()
data['light_affector_type'] = LIGHT_TYPE_TO_STRING[self.light_affector_type]
data['light_affector_data'] = self.light_affector_data.save_json()
return data
class ArcLight:
def load_binary(self, stream: ResStream):
self.light_x0 = stream.read_dword()
self.light_x1 = stream.read_bool()
self.light_type = stream.read_dword()
if self.light_type == 0xb6787258:
self.light_data = ArcLightPoint()
self.light_data.load_binary(stream)
self.light_0xdaed = stream.read_dword()
num = stream.read_dword()
self.light_affectors = [ArcLightAffectors() for _ in range(num)]
for extra in self.light_affectors:
extra.load_binary(stream)
def save_binary(self, stream: ResStream):
stream.write_dword(self.light_x0)
stream.write_bool(self.light_x1)
stream.write_dword(self.light_type)
self.light_data.save_binary(stream)
stream.write_dword(self.light_0xdaed)
stream.write_dword(len(self.light_affectors))
for extra in self.light_affectors:
extra.save_binary(stream)
def load_json(self, data):
self.light_x0 = 1
self.light_x1 = True
self.light_type = STRING_TO_LIGHT_TYPE[data['light_type']]
if self.light_type == 0xb6787258:
self.light_data = ArcLightPoint()
self.light_data.load_json(data['light_data'])
self.light_0xdaed = 0xddaaeedd
self.light_affectors = []
for extra_data in data['light_affectors']:
extra = ArcLightAffectors()
extra.load_json(extra_data)
self.light_affectors.append(extra)
def save_json(self):
data = OrderedDict()
data['light_type'] = LIGHT_TYPE_TO_STRING[self.light_type]
data['light_data'] = self.light_data.save_json()
data['light_affectors'] = []
for extra in self.light_affectors:
data['light_affectors'].append(extra.save_json())
return data
class ArcRender:
def load_binary(self, stream: ResStream):
self.render_template = ArcRenderTemplate()
self.render_template.load_binary(stream)
self.render_target_bone = stream.read_string()
self.render_scale = stream.read_tuple()
self.render_has_loc = stream.read_dword()
if self.render_has_loc:
self.render_loc = stream.read_tuple()
num_children = stream.read_dword()
self.render_children = [stream.read_qword() for _ in range(num_children)]
self.render_has_texture_set = stream.read_bool()
if self.render_has_texture_set:
num = stream.read_dword()
self.render_texture_set = [ArcTextureSet() for _ in range(num)]
for texture in self.render_texture_set:
texture.load_binary(stream)
self.render_collides = stream.read_bool()
self.render_calculate_bounding_box = stream.read_bool()
self.render_nation_crest = stream.read_bool()
self.render_guild_crest = stream.read_bool()
self.render_bumped = stream.read_bool()
self.render_vp_active = stream.read_bool()
if self.render_vp_active:
self.render_vp_name = stream.read_string()
num_params = stream.read_dword()
self.render_vp_params = [
[
stream.read_dword(),
stream.read_float(),
stream.read_float(),
stream.read_float(),
stream.read_float(),
] for _ in range(num_params)
]
self.render_has_light_effects = stream.read_bool()
if self.render_has_light_effects:
num_effects = stream.read_dword()
self.render_light_effects = [ArcLight() for _ in range(num_effects)]
for effect in self.render_light_effects:
effect.load_binary(stream)
def save_binary(self, stream: ResStream):
self.render_template.save_binary(stream)
stream.write_string(self.render_target_bone)
stream.write_tuple(self.render_scale)
stream.write_dword(self.render_has_loc)
if self.render_has_loc:
stream.write_tuple(self.render_loc)
stream.write_dword(len(self.render_children))
for child in self.render_children:
stream.write_qword(child)
stream.write_bool(self.render_has_texture_set)
if self.render_has_texture_set:
stream.write_dword(len(self.render_texture_set))
for texture in self.render_texture_set:
texture.save_binary(stream)
stream.write_bool(self.render_collides)
stream.write_bool(self.render_calculate_bounding_box)
stream.write_bool(self.render_nation_crest)
stream.write_bool(self.render_guild_crest)
stream.write_bool(self.render_bumped)
stream.write_bool(self.render_vp_active)
if self.render_vp_active:
stream.write_string(self.render_vp_name)
stream.write_dword(len(self.render_vp_params))
for param in self.render_vp_params:
stream.write_dword(param[0])
stream.write_float(param[1])
stream.write_float(param[2])
stream.write_float(param[3])
stream.write_float(param[4])
stream.write_bool(self.render_has_light_effects)
if self.render_has_light_effects:
stream.write_dword(len(self.render_light_effects))
for effect in self.render_light_effects:
effect.save_binary(stream)
def load_json(self, data):
self.render_template = ArcRenderTemplate()
self.render_template.load_json(data['render_template'])
self.render_target_bone = data['render_target_bone']
self.render_scale = data['render_scale']
self.render_has_loc = data['render_has_loc']
if self.render_has_loc:
self.render_loc = data['render_loc']
self.render_children = data['render_children']
self.render_has_texture_set = data['render_has_texture_set']
if self.render_has_texture_set:
self.render_texture_set = []
for texture_data in data['render_texture_set']:
texture = ArcTextureSet()
texture.load_json(texture_data)
self.render_texture_set.append(texture)
self.render_collides = data['render_collides']
self.render_calculate_bounding_box = data['render_calculate_bounding_box']
self.render_nation_crest = data['render_nation_crest']
self.render_guild_crest = data['render_guild_crest']
self.render_bumped = data['render_bumped']
self.render_vp_active = data['render_vp_active']
if self.render_vp_active:
self.render_vp_name = data['render_vp_name']
self.render_vp_params = data['render_vp_params']
self.render_has_light_effects = data['render_has_light_effects']
if self.render_has_light_effects:
self.render_light_effects = []
for effect_data in data['render_light_effects']:
effect = ArcLight()
effect.load_json(effect_data)
self.render_light_effects.append(effect)
def save_json(self):
data = OrderedDict()
data['render_template'] = self.render_template.save_json()
data['render_target_bone'] = self.render_target_bone
data['render_scale'] = self.render_scale
data['render_has_loc'] = self.render_has_loc
if self.render_has_loc:
data['render_loc'] = self.render_loc
data['render_children'] = self.render_children
data['render_has_texture_set'] = self.render_has_texture_set
if self.render_has_texture_set:
data['render_texture_set'] = []
for texture in self.render_texture_set:
data['render_texture_set'].append(texture.save_json())
data['render_collides'] = self.render_collides
data['render_calculate_bounding_box'] = self.render_calculate_bounding_box
data['render_nation_crest'] = self.render_nation_crest
data['render_guild_crest'] = self.render_guild_crest
data['render_bumped'] = self.render_bumped
data['render_vp_active'] = self.render_vp_active
if self.render_vp_active:
data['render_vp_name'] = self.render_vp_name
data['render_vp_params'] = self.render_vp_params
data['render_has_light_effects'] = self.render_has_light_effects
if self.render_has_light_effects:
data['render_light_effects'] = []
for effect in self.render_light_effects:
data['render_light_effects'].append(effect.save_json())
return data