Correct pose-relative transforms for impure skeletons
This commit is contained in:
parent
49f89a1fde
commit
b749e47536
@ -33,7 +33,7 @@ def read_scene(input_file, anim_only=False) -> Scene:
|
||||
|
||||
next_header = hedr.peak_next_header()
|
||||
|
||||
if "MSH2" in next_header:
|
||||
if next_header == "MSH2":
|
||||
|
||||
with hedr.read_child() as msh2:
|
||||
|
||||
@ -44,30 +44,29 @@ def read_scene(input_file, anim_only=False) -> Scene:
|
||||
|
||||
next_header = msh2.peak_next_header()
|
||||
|
||||
if "SINF" in next_header:
|
||||
if next_header == "SINF":
|
||||
with msh2.read_child() as sinf:
|
||||
pass
|
||||
|
||||
elif "MATL" in next_header:
|
||||
elif next_header == "MATL":
|
||||
with msh2.read_child() as matl:
|
||||
materials_list += _read_matl_and_get_materials_list(matl)
|
||||
for i,mat in enumerate(materials_list):
|
||||
scene.materials[mat.name] = mat
|
||||
|
||||
elif "MODL" in next_header:
|
||||
while ("MODL" in msh2.peak_next_header()):
|
||||
with msh2.read_child() as modl:
|
||||
scene.models.append(_read_modl(modl, materials_list))
|
||||
elif next_header == "MODL":
|
||||
with msh2.read_child() as modl:
|
||||
scene.models.append(_read_modl(modl, materials_list))
|
||||
|
||||
else:
|
||||
msh2.skip_bytes(1)
|
||||
|
||||
elif "SKL2" in next_header:
|
||||
elif next_header == "SKL2":
|
||||
with hedr.read_child() as skl2:
|
||||
num_bones = skl2.read_u32()
|
||||
scene.skeleton = [skl2.read_u32(5)[0] for i in range(num_bones)]
|
||||
|
||||
elif "ANM2" in next_header:
|
||||
elif next_header == "ANM2":
|
||||
with hedr.read_child() as anm2:
|
||||
scene.animation = _read_anm2(anm2)
|
||||
|
||||
@ -118,36 +117,36 @@ def _read_matd(matd: Reader) -> Material:
|
||||
|
||||
next_header = matd.peak_next_header()
|
||||
|
||||
if "NAME" in next_header:
|
||||
if next_header == "NAME":
|
||||
with matd.read_child() as name:
|
||||
mat.name = name.read_string()
|
||||
|
||||
elif "DATA" in next_header:
|
||||
elif next_header == "DATA":
|
||||
with matd.read_child() as data:
|
||||
data.read_f32(4) # Diffuse Color (Seams to get ignored by modelmunge)
|
||||
mat.specular_color = data.read_f32(4)
|
||||
data.read_f32(4) # Ambient Color (Seams to get ignored by modelmunge and Zero(?))
|
||||
data.read_f32() # Specular Exponent/Decay (Gets ignored by RedEngine in SWBFII for all known materials)
|
||||
|
||||
elif "ATRB" in next_header:
|
||||
elif next_header == "ATRB":
|
||||
with matd.read_child() as atrb:
|
||||
mat.flags = atrb.read_u8()
|
||||
mat.rendertype = atrb.read_u8()
|
||||
mat.data = atrb.read_u8(2)
|
||||
|
||||
elif "TX0D" in next_header:
|
||||
elif next_header == "TX0D":
|
||||
with matd.read_child() as tx0d:
|
||||
mat.texture0 = tx0d.read_string()
|
||||
|
||||
elif "TX1D" in next_header:
|
||||
elif next_header == "TX1D":
|
||||
with matd.read_child() as tx1d:
|
||||
mat.texture1 = tx1d.read_string()
|
||||
|
||||
elif "TX2D" in next_header:
|
||||
elif next_header == "TX2D":
|
||||
with matd.read_child() as tx2d:
|
||||
mat.texture2 = tx2d.read_string()
|
||||
|
||||
elif "TX3D" in next_header:
|
||||
elif next_header == "TX3D":
|
||||
with matd.read_child() as tx3d:
|
||||
mat.texture3 = tx3d.read_string()
|
||||
|
||||
@ -165,11 +164,11 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model:
|
||||
|
||||
next_header = modl.peak_next_header()
|
||||
|
||||
if "MTYP" in next_header:
|
||||
if next_header == "MTYP":
|
||||
with modl.read_child() as mtyp:
|
||||
model.model_type = ModelType(mtyp.read_u32())
|
||||
|
||||
elif "MNDX" in next_header:
|
||||
elif next_header == "MNDX":
|
||||
with modl.read_child() as mndx:
|
||||
index = mndx.read_u32()
|
||||
|
||||
@ -181,23 +180,23 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model:
|
||||
|
||||
model_counter += 1
|
||||
|
||||
elif "NAME" in next_header:
|
||||
elif next_header == "NAME":
|
||||
with modl.read_child() as name:
|
||||
model.name = name.read_string()
|
||||
|
||||
elif "PRNT" in next_header:
|
||||
elif next_header == "PRNT":
|
||||
with modl.read_child() as prnt:
|
||||
model.parent = prnt.read_string()
|
||||
|
||||
elif "FLGS" in next_header:
|
||||
elif next_header == "FLGS":
|
||||
with modl.read_child() as flgs:
|
||||
model.hidden = flgs.read_u32()
|
||||
|
||||
elif "TRAN" in next_header:
|
||||
elif next_header == "TRAN":
|
||||
with modl.read_child() as tran:
|
||||
model.transform = _read_tran(tran)
|
||||
|
||||
elif "GEOM" in next_header:
|
||||
elif next_header == "GEOM":
|
||||
model.geometry = []
|
||||
envelope = []
|
||||
|
||||
@ -206,11 +205,11 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model:
|
||||
while geom.could_have_child():
|
||||
next_header_geom = geom.peak_next_header()
|
||||
|
||||
if "SEGM" in next_header_geom:
|
||||
if next_header_geom == "SEGM":
|
||||
with geom.read_child() as segm:
|
||||
model.geometry.append(_read_segm(segm, materials_list))
|
||||
|
||||
elif "ENVL" in next_header_geom:
|
||||
elif next_header_geom == "ENVL":
|
||||
with geom.read_child() as envl:
|
||||
num_indicies = envl.read_u32()
|
||||
envelope += [envl.read_u32() for _ in range(num_indicies)]
|
||||
@ -228,7 +227,7 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model:
|
||||
index = vertex_weight.bone
|
||||
weight_set[i] = VertexWeight(vertex_weight.weight, envelope[vertex_weight.bone])
|
||||
|
||||
elif "SWCI" in next_header:
|
||||
elif next_header == "SWCI":
|
||||
prim = CollisionPrimitive()
|
||||
with modl.read_child() as swci:
|
||||
prim.shape = CollisionPrimitiveShape(swci.read_u32())
|
||||
@ -267,25 +266,25 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment:
|
||||
|
||||
next_header = segm.peak_next_header()
|
||||
|
||||
if "MATI" in next_header:
|
||||
if next_header == "MATI":
|
||||
with segm.read_child() as mati:
|
||||
geometry_seg.material_name = materials_list[mati.read_u32()].name
|
||||
|
||||
elif "POSL" in next_header:
|
||||
elif next_header == "POSL":
|
||||
with segm.read_child() as posl:
|
||||
num_positions = posl.read_u32()
|
||||
|
||||
for _ in range(num_positions):
|
||||
geometry_seg.positions.append(Vector(posl.read_f32(3)))
|
||||
|
||||
elif "NRML" in next_header:
|
||||
elif next_header == "NRML":
|
||||
with segm.read_child() as nrml:
|
||||
num_normals = nrml.read_u32()
|
||||
|
||||
for _ in range(num_positions):
|
||||
geometry_seg.normals.append(Vector(nrml.read_f32(3)))
|
||||
|
||||
elif "CLRL" in next_header:
|
||||
elif next_header == "CLRL":
|
||||
geometry_seg.colors = []
|
||||
|
||||
with segm.read_child() as clrl:
|
||||
@ -294,14 +293,14 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment:
|
||||
for _ in range(num_colors):
|
||||
geometry_seg.colors += unpack_color(clrl.read_u32())
|
||||
|
||||
elif "UV0L" in next_header:
|
||||
elif next_header == "UV0L":
|
||||
with segm.read_child() as uv0l:
|
||||
num_texcoords = uv0l.read_u32()
|
||||
|
||||
for _ in range(num_texcoords):
|
||||
geometry_seg.texcoords.append(Vector(uv0l.read_f32(2)))
|
||||
|
||||
elif "NDXL" in next_header:
|
||||
elif next_header == "NDXL":
|
||||
with segm.read_child() as ndxl:
|
||||
num_polygons = ndxl.read_u32()
|
||||
|
||||
@ -309,14 +308,14 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment:
|
||||
polygon = ndxl.read_u16(ndxl.read_u16())
|
||||
geometry_seg.polygons.append(polygon)
|
||||
|
||||
elif "NDXT" in next_header:
|
||||
elif next_header == "NDXT":
|
||||
with segm.read_child() as ndxt:
|
||||
num_tris = ndxt.read_u32()
|
||||
|
||||
for _ in range(num_tris):
|
||||
geometry_seg.triangles.append(ndxt.read_u16(3))
|
||||
|
||||
elif "STRP" in next_header:
|
||||
elif next_header == "STRP":
|
||||
strips : List[List[int]] = []
|
||||
|
||||
with segm.read_child() as strp:
|
||||
@ -357,7 +356,7 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment:
|
||||
#if segm.read_u16 != 0: #trailing 0 bug https://schlechtwetterfront.github.io/ze_filetypes/msh.html#STRP
|
||||
# segm.skip_bytes(-2)
|
||||
|
||||
elif "WGHT" in next_header:
|
||||
elif next_header == "WGHT":
|
||||
with segm.read_child() as wght:
|
||||
|
||||
geometry_seg.weights = []
|
||||
@ -389,11 +388,19 @@ def _read_anm2(anm2: Reader) -> Animation:
|
||||
|
||||
next_header = anm2.peak_next_header()
|
||||
|
||||
if "CYCL" in next_header:
|
||||
if next_header == "CYCL":
|
||||
with anm2.read_child() as cycl:
|
||||
pass
|
||||
|
||||
elif "KFR3" in next_header:
|
||||
'''
|
||||
num_anims = cycl.read_u32()
|
||||
|
||||
for _ in range(num_anims):
|
||||
cycl.skip_bytes(64)
|
||||
print("CYCL play style {}".format(cycl.read_u32(4)[1]))
|
||||
'''
|
||||
|
||||
elif next_header == "KFR3":
|
||||
with anm2.read_child() as kfr3:
|
||||
|
||||
num_bones = kfr3.read_u32()
|
||||
|
@ -20,75 +20,94 @@ import os
|
||||
|
||||
def extract_and_apply_anim(filename, scene):
|
||||
|
||||
arma = bpy.context.view_layer.objects.active
|
||||
arma = bpy.context.view_layer.objects.active
|
||||
|
||||
if arma.type != 'ARMATURE':
|
||||
raise Exception("Select an armature to attach the imported animation to!")
|
||||
if arma.type != 'ARMATURE':
|
||||
raise Exception("Select an armature to attach the imported animation to!")
|
||||
|
||||
if scene.animation is None:
|
||||
raise Exception("No animation found in msh file")
|
||||
|
||||
else:
|
||||
head, tail = os.path.split(filename)
|
||||
anim_name = tail.split(".")[0]
|
||||
action = bpy.data.actions.new(anim_name)
|
||||
if scene.animation is None:
|
||||
raise Exception("No animation found in msh file!")
|
||||
|
||||
else:
|
||||
head, tail = os.path.split(filename)
|
||||
anim_name = tail.split(".")[0]
|
||||
action = bpy.data.actions.new(anim_name)
|
||||
|
||||
if not arma.animation_data:
|
||||
arma.animation_data_create()
|
||||
|
||||
bone_bind_poses = {}
|
||||
for bone in arma.data.bones:
|
||||
local_mat = bone.matrix_local
|
||||
if bone.parent:
|
||||
local_mat = bone.parent.matrix_local.inverted() @ local_mat
|
||||
bone_bind_poses[bone.name] = local_mat
|
||||
if not arma.animation_data:
|
||||
arma.animation_data_create()
|
||||
|
||||
|
||||
for bone in arma.pose.bones:
|
||||
if crc(bone.name) in scene.animation.bone_frames:
|
||||
#print("Inserting anim data for bone: {}".format(bone.name))
|
||||
|
||||
bone_bind_poses = {}
|
||||
bone_stack_mats = {}
|
||||
|
||||
'''
|
||||
for bone in arma.data.bones:
|
||||
local_mat = bone.matrix_local
|
||||
if bone.parent:
|
||||
local_mat = bone.parent.matrix_local.inverted() @ local_mat
|
||||
bone_bind_poses[bone.name] = local_mat
|
||||
'''
|
||||
|
||||
for bone in arma.data.bones:
|
||||
bone_obj = bpy.data.objects[bone.name]
|
||||
bone_obj_parent = bone_obj.parent
|
||||
|
||||
bind_mat = bone_obj.matrix_local
|
||||
stack_mat = Matrix.Identity(4)
|
||||
|
||||
|
||||
bone_local_mat = bone_bind_poses[bone.name]
|
||||
while(True):
|
||||
if bone_obj_parent is None or bone_obj_parent.name in arma.data.bones:
|
||||
break
|
||||
bind_mat = bone_obj_parent.matrix_local @ bind_mat
|
||||
stack_mat = bone_obj_parent.matrix_local @ stack_mat
|
||||
bone_obj_parent = bone_obj_parent.parent
|
||||
|
||||
translation_frames, rotation_frames = scene.animation.bone_frames[crc(bone.name)]
|
||||
|
||||
loc_data_path = "pose.bones[\"{}\"].location".format(bone.name)
|
||||
rot_data_path = "pose.bones[\"{}\"].rotation_quaternion".format(bone.name)
|
||||
|
||||
fcurve_rot_w = action.fcurves.new(rot_data_path, index=0)
|
||||
fcurve_rot_x = action.fcurves.new(rot_data_path, index=1)
|
||||
fcurve_rot_y = action.fcurves.new(rot_data_path, index=2)
|
||||
fcurve_rot_z = action.fcurves.new(rot_data_path, index=3)
|
||||
|
||||
print("\nBone name: " + bone.name)
|
||||
print("\tRot: {}".format(quat_to_str(rotation_frames[0].rotation)))
|
||||
|
||||
for frame in rotation_frames:
|
||||
i = frame.index
|
||||
q = (bone_local_mat.inverted() @ convert_rotation_space(frame.rotation).to_matrix().to_4x4()).to_quaternion()
|
||||
|
||||
fcurve_rot_w.keyframe_points.insert(i,q.w)
|
||||
fcurve_rot_x.keyframe_points.insert(i,q.x)
|
||||
fcurve_rot_y.keyframe_points.insert(i,q.y)
|
||||
fcurve_rot_z.keyframe_points.insert(i,q.z)
|
||||
|
||||
print("\tLoc: {}".format(vec_to_str(translation_frames[0].translation)))
|
||||
bone_bind_poses[bone.name] = bind_mat.inverted() @ stack_mat
|
||||
|
||||
|
||||
fcurve_loc_x = action.fcurves.new(loc_data_path, index=0)
|
||||
fcurve_loc_y = action.fcurves.new(loc_data_path, index=1)
|
||||
fcurve_loc_z = action.fcurves.new(loc_data_path, index=2)
|
||||
|
||||
for frame in translation_frames:
|
||||
i = frame.index
|
||||
t = convert_vector_space(frame.translation) - bone_local_mat.translation
|
||||
for bone in arma.pose.bones:
|
||||
if crc(bone.name) in scene.animation.bone_frames:
|
||||
#print("Inserting anim data for bone: {}".format(bone.name))
|
||||
|
||||
fcurve_loc_x.keyframe_points.insert(i,t.x)
|
||||
fcurve_loc_y.keyframe_points.insert(i,t.y)
|
||||
fcurve_loc_z.keyframe_points.insert(i,t.z)
|
||||
bind_mat = bone_bind_poses[bone.name]
|
||||
|
||||
arma.animation_data.action = action
|
||||
translation_frames, rotation_frames = scene.animation.bone_frames[crc(bone.name)]
|
||||
|
||||
loc_data_path = "pose.bones[\"{}\"].location".format(bone.name)
|
||||
rot_data_path = "pose.bones[\"{}\"].rotation_quaternion".format(bone.name)
|
||||
|
||||
|
||||
fcurve_rot_w = action.fcurves.new(rot_data_path, index=0)
|
||||
fcurve_rot_x = action.fcurves.new(rot_data_path, index=1)
|
||||
fcurve_rot_y = action.fcurves.new(rot_data_path, index=2)
|
||||
fcurve_rot_z = action.fcurves.new(rot_data_path, index=3)
|
||||
|
||||
for frame in rotation_frames:
|
||||
i = frame.index
|
||||
q = (bind_mat @ convert_rotation_space(frame.rotation).to_matrix().to_4x4()).to_quaternion()
|
||||
|
||||
fcurve_rot_w.keyframe_points.insert(i,q.w)
|
||||
fcurve_rot_x.keyframe_points.insert(i,q.x)
|
||||
fcurve_rot_y.keyframe_points.insert(i,q.y)
|
||||
fcurve_rot_z.keyframe_points.insert(i,q.z)
|
||||
|
||||
|
||||
fcurve_loc_x = action.fcurves.new(loc_data_path, index=0)
|
||||
fcurve_loc_y = action.fcurves.new(loc_data_path, index=1)
|
||||
fcurve_loc_z = action.fcurves.new(loc_data_path, index=2)
|
||||
|
||||
for frame in translation_frames:
|
||||
i = frame.index
|
||||
t = (bind_mat @ Matrix.Translation(convert_vector_space(frame.translation))).translation
|
||||
|
||||
fcurve_loc_x.keyframe_points.insert(i,t.x)
|
||||
fcurve_loc_y.keyframe_points.insert(i,t.y)
|
||||
fcurve_loc_z.keyframe_points.insert(i,t.z)
|
||||
|
||||
arma.animation_data.action = action
|
||||
|
||||
|
||||
|
||||
@ -187,6 +206,14 @@ def extract_refined_skeleton(scene: Scene):
|
||||
|
||||
refined_skeleton_models = []
|
||||
|
||||
'''
|
||||
for bone in skeleton_models:
|
||||
|
||||
if bone.parent:
|
||||
if
|
||||
'''
|
||||
|
||||
|
||||
for bone in skeleton_models:
|
||||
|
||||
if bone.parent:
|
||||
@ -213,7 +240,7 @@ def extract_refined_skeleton(scene: Scene):
|
||||
else:
|
||||
curr_ancestor = model_dict[curr_ancestor.parent]
|
||||
stacked_transform = model_transform_to_matrix(curr_ancestor.transform) @ stacked_transform
|
||||
|
||||
|
||||
return sort_by_parent(refined_skeleton_models)
|
||||
|
||||
|
||||
@ -245,30 +272,30 @@ def extract_models(scene: Scene, materials_map):
|
||||
weights_offsets = {}
|
||||
|
||||
if model.geometry:
|
||||
for i,seg in enumerate(model.geometry):
|
||||
for i,seg in enumerate(model.geometry):
|
||||
|
||||
if i == 0:
|
||||
mat_name = seg.material_name
|
||||
if i == 0:
|
||||
mat_name = seg.material_name
|
||||
|
||||
verts += [tuple(convert_vector_space(v)) for v in seg.positions]
|
||||
verts += [tuple(convert_vector_space(v)) for v in seg.positions]
|
||||
|
||||
if seg.weights:
|
||||
weights_offsets[offset] = seg.weights
|
||||
if seg.weights:
|
||||
weights_offsets[offset] = seg.weights
|
||||
|
||||
if seg.texcoords is not None:
|
||||
full_texcoords += seg.texcoords
|
||||
else:
|
||||
full_texcoords += [(0.0,0.0) for _ in range(len(seg.positions))]
|
||||
if seg.texcoords is not None:
|
||||
full_texcoords += seg.texcoords
|
||||
else:
|
||||
full_texcoords += [(0.0,0.0) for _ in range(len(seg.positions))]
|
||||
|
||||
if seg.triangles:
|
||||
faces += [tuple([ind + offset for ind in tri]) for tri in seg.triangles]
|
||||
else:
|
||||
for strip in seg.triangle_strips:
|
||||
for i in range(len(strip) - 2):
|
||||
face = tuple([offset + strip[j] for j in range(i,i+3)])
|
||||
faces.append(face)
|
||||
if seg.triangles:
|
||||
faces += [tuple([ind + offset for ind in tri]) for tri in seg.triangles]
|
||||
else:
|
||||
for strip in seg.triangle_strips:
|
||||
for i in range(len(strip) - 2):
|
||||
face = tuple([offset + strip[j] for j in range(i,i+3)])
|
||||
faces.append(face)
|
||||
|
||||
offset += len(seg.positions)
|
||||
offset += len(seg.positions)
|
||||
|
||||
new_mesh.from_pydata(verts, [], faces)
|
||||
new_mesh.update()
|
||||
@ -379,6 +406,36 @@ def extract_scene(filepath: str, scene: Scene):
|
||||
skel = extract_refined_skeleton(scene)
|
||||
armature = refined_skeleton_to_armature(skel, model_map)
|
||||
|
||||
|
||||
for bone in armature.data.bones:
|
||||
bone_local = bone.matrix_local
|
||||
if bone.parent:
|
||||
bone_local = bone.parent.matrix_local.inverted() @ bone_local
|
||||
|
||||
bone_obj_local = bpy.data.objects[bone.name].matrix_local
|
||||
obj_loc, obj_rot, _ = bone_obj_local.decompose()
|
||||
|
||||
loc, rot, _ = bone_local.decompose()
|
||||
|
||||
locdiff = obj_loc - loc
|
||||
quatdiff = obj_rot - rot
|
||||
|
||||
if quatdiff.magnitude > .01:
|
||||
print("Big quat diff here")
|
||||
print("\t{}: obj quat: {} bone quat: {}".format(bone.name, quat_to_str(obj_rot), quat_to_str(rot)))
|
||||
|
||||
|
||||
#if locdiff.magnitude > .01:
|
||||
# print("Big loc diff here")
|
||||
# print("\t{}: obj loc: {} bone loc: {}".format(bone.name, vec_to_str(obj_loc), vec_to_str(loc)))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
reparent_obj = None
|
||||
for model in scene.models:
|
||||
if model.model_type == ModelType.SKIN:
|
||||
@ -398,15 +455,12 @@ def extract_scene(filepath: str, scene: Scene):
|
||||
armature.select_set(False)
|
||||
bpy.context.view_layer.objects.active = None
|
||||
|
||||
print("About to parent to bones....")
|
||||
|
||||
if armature is not None:
|
||||
for bone in armature.data.bones:
|
||||
for model in scene.models:
|
||||
if model.parent in armature.data.bones and model.name not in armature.data.bones:
|
||||
parent_object_to_bone(model_map[model.name], armature, model.parent)
|
||||
if model.parent in armature.data.bones and model.model_type != ModelType.NULL:
|
||||
pass#parent_object_to_bone(model_map[model.name], armature, model.parent)
|
||||
|
||||
print("Done parenting to bones")
|
||||
'''
|
||||
if reparent_obj is not None and armature.name != reparent_obj.name:
|
||||
|
||||
@ -418,14 +472,14 @@ def extract_scene(filepath: str, scene: Scene):
|
||||
armature.select_set(False)
|
||||
reparent_obj.select_set(False)
|
||||
bpy.context.view_layer.objects.active = None
|
||||
|
||||
'''
|
||||
|
||||
for model in scene.models:
|
||||
if model.name in bpy.data.objects:
|
||||
obj = bpy.data.objects[model.name]
|
||||
if get_is_model_hidden(obj) and len(obj.children) == 0:
|
||||
if get_is_model_hidden(obj) and len(obj.children) == 0 and model.model_type != ModelType.NULL:
|
||||
obj.hide_set(True)
|
||||
'''
|
||||
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user