Image
-- **************************************************
-- Provide Moho with the name of this script object
-- **************************************************

ScriptName = "MR_MoveTargetedJoint"

-- **************************************************
-- General information about this script
-- **************************************************

MR_MoveTargetedJoint = {}

function MR_MoveTargetedJoint:Name()
	return self:Localize('UILabel')
end

function MR_MoveTargetedJoint:Version()
	return '1.2'
end

function MR_MoveTargetedJoint:UILabel()
	return self:Localize('UILabel')
end

function MR_MoveTargetedJoint:Creator()
	return 'Eugene Babich'
end

function MR_MoveTargetedJoint:Description()
	return self:Localize('Description')
end

-- **************************************************
-- Is Relevant / Is Enabled
-- **************************************************

function MR_MoveTargetedJoint:IsRelevant(moho)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return false
	end
	return true
end

function MR_MoveTargetedJoint:IsEnabled(moho)
	if (moho:CountBones() < 3) then
		return false
	end
	if moho.frame == 0 then 
		return false
	end
	return true
end

-- **************************************************
-- Recurring Values
-- **************************************************

MR_MoveTargetedJoint.hideBonesWhileMoving = false
MR_MoveTargetedJoint.leaveBonesSelected = true
MR_MoveTargetedJoint.bakeAdjacentFrames = false
MR_MoveTargetedJoint.maintainProportions = false
MR_MoveTargetedJoint.firstBoneVisibility = true
MR_MoveTargetedJoint.secondBoneVisibility = true
MR_MoveTargetedJoint.firstBone = -1
MR_MoveTargetedJoint.firstBonePos = LM.Vector2:new_local()
MR_MoveTargetedJoint.firstBoneScalePercent = 50
MR_MoveTargetedJoint.clickOffset = LM.Vector2:new_local()
MR_MoveTargetedJoint.secondBone = -1
MR_MoveTargetedJoint.targetBonePos = LM.Vector2:new_local()
MR_MoveTargetedJoint.targetBone = 0
MR_MoveTargetedJoint.boneAngle = 0
MR_MoveTargetedJoint.isFlipped = false
MR_MoveTargetedJoint.isParentfliped = false
MR_MoveTargetedJoint.isMore = false
MR_MoveTargetedJoint.isActive = true
MR_MoveTargetedJoint.childsList = {}
MR_MoveTargetedJoint.firstBoneOffset = 0
MR_MoveTargetedJoint.secondBoneOffset = 0

-- **************************************************
-- Prefs
-- **************************************************

function MR_MoveTargetedJoint:LoadPrefs(prefs)
	self.hideBonesWhileMoving = prefs:GetBool("MR_MoveTargetedJoint.hideBonesWhileMoving", false)
	self.leaveBonesSelected = prefs:GetBool("MR_MoveTargetedJoint.leaveBonesSelected", false)
	self.bakeAdjacentFrames = prefs:GetBool("MR_MoveTargetedJoint.bakeAdjacentFrames", false)
	self.maintainProportions = prefs:GetBool("MR_MoveTargetedJoint.maintainProportions", false)
end

function MR_MoveTargetedJoint:SavePrefs(prefs)
	prefs:SetBool("MR_MoveTargetedJoint.hideBonesWhileMoving", self.hideBonesWhileMoving)
	prefs:SetBool("MR_MoveTargetedJoint.leaveBonesSelected", self.leaveBonesSelected)
	prefs:SetBool("MR_MoveTargetedJoint.bakeAdjacentFrames", self.bakeAdjacentFrames)
	prefs:SetBool("MR_MoveTargetedJoint.maintainProportions", self.maintainProportions)
end

function MR_MoveTargetedJoint:ResetPrefs()
	self.hideBonesWhileMoving = false
	self.leaveBonesSelected = true
	self.bakeAdjacentFrames = false
	self.maintainProportions = false
end

-- **************************************************
-- Keyboard/Mouse Control
-- **************************************************

function MR_MoveTargetedJoint:OnMouseDown(moho, mouseEvent)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end
	
	if (moho:CountSelectedBones() > 0) then
		for i = 0, skel:CountBones() - 1 do
			skel:Bone(i).fSelected = false
		end
	end

	local id = mouseEvent.view:PickBone(mouseEvent.pt, mouseEvent.vec, moho.layer, false)
	local boneCandidate = skel:Bone(id)
	if not self:IsBoneIK(moho, boneCandidate) then
		self.isActive = false
		return
	end

	if boneCandidate.fFlipH:GetValue(moho.layerFrame) or boneCandidate.fFlipV:GetValue(moho.layerFrame) then
		self.isActive = false
		return
	end
	
	local boneCandidateTargetID = skel:TargetOfBone(id, moho.layerFrame) 
	if boneCandidateTargetID == -1 then
		local boneCandidateParentID = boneCandidate.fParent
		local isParentOk = true
		if boneCandidateParentID > -1 then
			local parentChilds = self:CountBoneChildren(moho, skel, boneCandidateParentID, true)
			if parentChilds == 1 then
				local bCandParent = skel:Bone(boneCandidateParentID)
				if self:IsBoneIK(moho, bCandParent) then
					self.isActive = false
					return
				end
			end	
		end
		
		local childs = self:CountBoneChildren(moho, skel, id, true)
		
		if childs ~= 1 then
			self.isActive = false
			return
		end
		
		local found = false
		for q in pairs(self.childsList) do
			local bone = skel:Bone(self.childsList[q])
			if self:IsBoneIK(moho, bone) then
				if skel:TargetOfBone(self.childsList[q], moho.layerFrame)  > -1 then
					local tBone = skel:Bone(self.childsList[q])
					if self:Round(tBone.fPos.y) == 0 then
						if not tBone.fFlipH:GetValue(moho.layerFrame) and not tBone.fFlipV:GetValue(moho.layerFrame) then
							skel:Bone(id).fSelected = true	
							tBone.fSelected = true	
							self.firstBone = id
							self.secondBone = self.childsList[q]
							found = true
						end	
					end
				end
			end
		end
		if found then
			self.isActive = true
		else
			self.isActive = false
			return
		end	
	else
		if self:Round(boneCandidate.fPos.y) ~= 0 then
			self.isActive = false
			return
		end
		local boneCandidateFirstID = boneCandidate.fParent
		if boneCandidateFirstID > -1 then
			local bCandFirst = skel:Bone(boneCandidateFirstID)
			if not self:IsBoneIK(moho, bCandFirst) then
				self.isActive = false
				return
			end
			local bCandFirstParentID = bCandFirst.fParent
			if bCandFirstParentID > -1 then
				local bCandFirstParent = skel:Bone(bCandFirstParentID)
				local parentFirstChilds = self:CountBoneChildren(moho, skel, bCandFirstParentID, true)
				if parentFirstChilds == 1 then
					if self:IsBoneIK(moho, bCandFirstParent) then
						self.isActive = false
						return
					end
				end
			end	

			local childs = self:CountBoneChildren(moho, skel, boneCandidateFirstID, true)
			
			if childs ~= 1 then
				self.isActive = false
				return
			end
			
			if bCandFirst.fFlipH:GetValue(moho.layerFrame) or bCandFirst.fFlipV:GetValue(moho.layerFrame) then
				self.isActive = false
				return
			end	
			
			if skel:TargetOfBone(boneCandidateFirstID, moho.layerFrame) > -1 then
				self.isActive = false
				return
			end
			
			bCandFirst.fSelected = true	
			skel:Bone(id).fSelected = true	
			self.firstBone = boneCandidateFirstID
			self.secondBone = id
			self.isActive = true
		else
			self.isActive = false
			return
		end	
	end
	
	if self.isActive == false then 
		return
	end
	
	moho.document:PrepUndo(moho.layer)
	moho.document:SetDirty()
	
	self.isFlipped = false
	
	local bone = skel:Bone(self.firstBone)
	local secondBone = skel:Bone(self.secondBone)
	
	if self.maintainProportions then
		local boneFirstLenght = secondBone.fPos.x
		local boneSecondLenght = secondBone.fLength
		local boneFirstScale = bone.fScale
		local boneSecondScale = secondBone.fScale
		self.firstBoneScalePercent = ((boneFirstLenght * boneFirstScale) / ((boneFirstLenght * boneFirstScale) + (boneSecondLenght * boneSecondScale))) * 100
	end
	
	local secondBonePos = LM.Vector2:new_local()
	secondBonePos:Set(secondBone.fPos)
	local firstBoneMatrix = LM.Matrix:new_local()
	firstBoneMatrix:Set(bone.fMovedMatrix)
	firstBoneMatrix:Transform(secondBonePos)
	
	local mousePos = mouseEvent.vec
	self.clickOffset = secondBonePos - mousePos
	
	if self.bakeAdjacentFrames then
		if moho.layerFrame - 1 > 0 then
			local boneFAngle = bone.fAnimAngle:GetValue(moho.layerFrame - 1)
			local boneFScale = bone.fAnimScale:GetValue(moho.layerFrame - 1)
			local boneSAngle = secondBone.fAnimAngle:GetValue(moho.layerFrame - 1)
			local boneSScale = secondBone.fAnimScale:GetValue(moho.layerFrame - 1)
			bone.fAnimAngle:SetValue(moho.layerFrame - 1, boneFAngle)
			secondBone.fAnimAngle:SetValue(moho.layerFrame - 1, boneSAngle)
			bone.fAnimScale:SetValue(moho.layerFrame - 1, boneFScale)
			secondBone.fAnimScale:SetValue(moho.layerFrame - 1, boneSScale)
		end
		
		if moho.layerFrame + 1 > 0 then
			local boneFAngle = bone.fAnimAngle:GetValue(moho.layerFrame + 1)
			local boneFScale = bone.fAnimScale:GetValue(moho.layerFrame + 1)
			local boneSAngle = secondBone.fAnimAngle:GetValue(moho.layerFrame + 1)
			local boneSScale = secondBone.fAnimScale:GetValue(moho.layerFrame + 1)
			bone.fAnimAngle:SetValue(moho.layerFrame + 1, boneFAngle)
			secondBone.fAnimAngle:SetValue(moho.layerFrame + 1, boneSAngle)
			bone.fAnimScale:SetValue(moho.layerFrame + 1, boneFScale)
			secondBone.fAnimScale:SetValue(moho.layerFrame + 1, boneSScale)
		end
	end
	
	if self.hideBonesWhileMoving then
		self.firstBoneVisibility = bone.fHidden
		self.secondBoneVisibility = secondBone.fHidden
		bone.fHidden = true
		secondBone.fHidden = true
	end
	
	local firstBoneStretching = bone.fMaxAutoScaling
	local secondBoneStretching = secondBone.fMaxAutoScaling
	bone.fMaxAutoScaling = 1
	secondBone.fMaxAutoScaling = 1
	
	moho.layer:UpdateCurFrame()
	
	self.firstBoneOffset = bone.fScale - bone.fAnimScale:GetValue(moho.layerFrame)
	self.secondBoneOffset = secondBone.fScale - secondBone.fAnimScale:GetValue(moho.layerFrame)
	bone.fMaxAutoScaling = firstBoneStretching
	secondBone.fMaxAutoScaling = secondBoneStretching
	
	local bonePose = LM.Vector2:new_local()
	bonePose:Set(bone.fPos)
	
	local boneTarget = skel:TargetOfBone(self.secondBone, moho.layerFrame) 
	self.targetBone = boneTarget
	local targetPos = LM.Vector2:new_local() 
	local target = skel:Bone(boneTarget)
	local targetParent = target.fParent
	targetPos:Set(target.fPos)
	
	if targetParent > -1 then
		local targetParentBone = skel:Bone(targetParent)
		local targetParentMatrix = LM.Matrix:new_local()
		targetParentMatrix:Set(targetParentBone.fMovedMatrix)
		targetParentMatrix:Transform(targetPos)
	end

	self.targetBonePos = targetPos
	local parentBoneID = bone.fParent
	if parentBoneID > -1 then
		local parentBone = skel:Bone(parentBoneID)
		local parentMatrix = LM.Matrix:new_local()
		parentMatrix:Set(parentBone.fMovedMatrix)
		parentMatrix:Transform(bonePose)
	end
	self.bonePose = bonePose
		
	local vectorBonePos = targetPos - bonePose
	local boneAngle = math.atan2(vectorBonePos.y, vectorBonePos.x)
	if boneAngle < 0 then
		boneAngle = (math.pi * 2) + boneAngle
	end
		
	self.boneAngle = boneAngle
	
	local secondBoneAngle = secondBone.fAnimAngle:GetValue(moho.layerFrame)
	if secondBoneAngle > math.pi * 2 then
		secondBoneAngle = secondBoneAngle - (math.pi * 2)
	elseif secondBoneAngle < 0 then
		secondBoneAngle = secondBoneAngle + (math.pi * 2)
	end
	
	isParentfliped = false
	if parentBoneID > -1 then
		local nextBone = skel:Bone(parentBoneID)
		repeat 
			local prevBone = nextBone
			if nextBone.fFlipH:GetValue(moho.layerFrame) then
				isParentfliped =  not isParentfliped
			end
			if nextBone.fFlipV:GetValue(moho.layerFrame) then
				isParentfliped = not isParentfliped
			end
			
			if nextBone.fParent > -1 then
				nextBone = skel:Bone(nextBone.fParent)
			end
		until nextBone == prevBone
	end
	
	if secondBoneAngle < math.pi then
		self.isMore = true
	else
		self.isMore = false
	end
	
	if isParentfliped then
		self.isMore = not self.isMore
	end
	
	self:OnMouseMoved(moho, mouseEvent)
end

function MR_MoveTargetedJoint:IsBoneIK(moho, bone)
	if (bone.fAngleControlParent >= 0 or bone.fPosControlParent >= 0 or bone.fScaleControlParent >= 0 or bone.fBoneDynamics.value or bone.fIgnoredByIK) then
		return false
	else
		return true
	end	
end

function MR_MoveTargetedJoint:OnMouseMoved(moho, mouseEvent)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end
	if self.isActive == false then
		return
	end
		
	local bone = skel:Bone(self.firstBone)
	local secondBone = skel:Bone(self.secondBone)
	local bonePose = LM.Vector2:new_local()
	bonePose = self.bonePose
	local targetPos = LM.Vector2:new_local()
	local boneLenght = secondBone.fPos.x
	local dist = self:GetDistance(bonePose, mouseEvent.vec + self.clickOffset)
	local boneNewScale = dist / boneLenght
	bone.fAnimScale:SetValue(moho.layerFrame, boneNewScale - self.firstBoneOffset)
	local secondDist = self:GetDistance(self.targetBonePos, mouseEvent.vec + self.clickOffset)
	local secondBoneLenght = secondBone.fLength
	local secondBoneNewScale = secondDist / secondBoneLenght
	secondBone.fAnimScale:SetValue(moho.layerFrame, secondBoneNewScale - self.secondBoneOffset)
	
	if self.maintainProportions then
		local totalDist = dist + secondDist
		local distP = (totalDist / 100) * self.firstBoneScalePercent
		distP = distP / boneLenght
		local secondDistP = (totalDist / 100) * (100 - self.firstBoneScalePercent)
		secondDistP = secondDistP / secondBoneLenght
		bone.fAnimScale:SetValue(moho.layerFrame, distP - self.firstBoneOffset)
		secondBone.fAnimScale:SetValue(moho.layerFrame, secondDistP - self.secondBoneOffset)
	end
	
	local vectorMousePos = (mouseEvent.vec + self.clickOffset) - bonePose
	local mouseAngle = math.atan2(vectorMousePos.y, vectorMousePos.x)
	
	if mouseAngle < 0 then
		mouseAngle = (math.pi * 2) + mouseAngle
	end
	
	mouseAngle = mouseAngle - self.boneAngle
	if mouseAngle < 0 then
		mouseAngle = (math.pi * 2) + mouseAngle
	end
	
	local turnToRight = 0.17
	local turnToLeft = 3.31
	if isParentfliped then
		turnToRight = 3.31
		turnToLeft = 0.17
	end
	
	if self.isMore then
		if mouseAngle < math.pi and not self.isFlipped then
			local boneAngle = bone.fAnimAngle:GetValue(moho.layerFrame)
			local secondBoneAngle = secondBone.fAnimAngle:GetValue(moho.layerFrame)
			bone.fAnimAngle:SetValue(moho.layerFrame, turnToLeft)
			secondBone.fAnimAngle:SetValue(moho.layerFrame, turnToLeft)
			self.isFlipped = true
		end
		if mouseAngle > math.pi and self.isFlipped then
			local boneAngle = bone.fAnimAngle:GetValue(moho.layerFrame)
			local secondBoneAngle = secondBone.fAnimAngle:GetValue(moho.layerFrame)
			bone.fAnimAngle:SetValue(moho.layerFrame, turnToRight)
			secondBone.fAnimAngle:SetValue(moho.layerFrame, turnToRight)
			self.isFlipped = false
		end
	else
		if mouseAngle < math.pi and self.isFlipped then
			local boneAngle = bone.fAnimAngle:GetValue(moho.layerFrame)
			local secondBoneAngle = secondBone.fAnimAngle:GetValue(moho.layerFrame)
			bone.fAnimAngle:SetValue(moho.layerFrame, turnToLeft)
			secondBone.fAnimAngle:SetValue(moho.layerFrame, turnToLeft)
			self.isFlipped = false
		end
		if mouseAngle > math.pi and not self.isFlipped then
			local boneAngle = bone.fAnimAngle:GetValue(moho.layerFrame)
			local secondBoneAngle = secondBone.fAnimAngle:GetValue(moho.layerFrame)
			bone.fAnimAngle:SetValue(moho.layerFrame, turnToRight)
			secondBone.fAnimAngle:SetValue(moho.layerFrame, turnToRight)
			self.isFlipped = true
		end
	end
	moho.layer:UpdateCurFrame()
	mouseEvent.view:DrawMe()
end

function MR_MoveTargetedJoint:Round(x, n)
	n = math.pow(10, n or 3)
	x = x * n
	if x >= 0 then x = math.floor(x + 0.5) else x = math.ceil(x - 0.5) end
	return x / n
end

function MR_MoveTargetedJoint:GetDistance(Pos1, Pos2)
	return math.sqrt((Pos2.x-Pos1.x)^2+(Pos2.y-Pos1.y)^2)
end

function MR_MoveTargetedJoint:CountBoneChildren(moho, skel, boneID, ignoreControlledBones)
	for q in pairs(self.childsList) do
		self.childsList[q] = nil
	end

	local n = 0
	for i = 0, skel:CountBones() - 1 do
		local bone = skel:Bone(i)
		if (bone.fParent == boneID) then
			n = n + 1
			table.insert(self.childsList, i)
			if (ignoreControlledBones) then
				if (bone.fAngleControlParent >= 0 or bone.fPosControlParent >= 0 or bone.fScaleControlParent >= 0 or bone.fBoneDynamics.value or bone.fIgnoredByIK) then
					n = n - 1
				end
			end
		end
	end
	return n
end

function MR_MoveTargetedJoint:OnMouseUp(moho, mouseEvent)
	local skel = moho:Skeleton()
	if (skel == nil) then
		return
	end
	
	if self.isActive == false then
		return
	end
	
	local bone = skel:Bone(self.firstBone)
	local secondBone = skel:Bone(self.secondBone)
	
	if self.hideBonesWhileMoving then
		bone.fHidden = self.firstBoneVisibility
		secondBone.fHidden = self.secondBoneVisibility
	end
	
	bone.fAnimAngle:SetValue(moho.layerFrame, bone.fAngle)
	secondBone.fAnimAngle:SetValue(moho.layerFrame, secondBone.fAngle)
	
	moho:NewKeyframe(CHANNEL_BONE_S)
	moho:NewKeyframe(CHANNEL_BONE)
	
	if not self.leaveBonesSelected then
		bone.fSelected = false
		secondBone.fSelected = false
	end
	
	moho:UpdateUI()
	mouseEvent.view:DrawMe()
	moho:UpdateSelectedChannels()
end

-- **************************************************
-- Tool Panel Layout
-- **************************************************

MR_MoveTargetedJoint.HIDE_BONES_WHILE_MOVING = MOHO.MSG_BASE
MR_MoveTargetedJoint.LEAVE_BONES_SELECTED = MOHO.MSG_BASE +1
MR_MoveTargetedJoint.BAKE_ADJACENT_FRAMES = MOHO.MSG_BASE +2
MR_MoveTargetedJoint.MAINTAIN_PROPORTIONS = MOHO.MSG_BASE +3

function MR_MoveTargetedJoint:DoLayout(moho, layout)
	self.hideBonesWhileMovingCheckbox = LM.GUI.CheckBox(self:Localize('Hide bones while moving'), self.HIDE_BONES_WHILE_MOVING)
    layout:AddChild(self.hideBonesWhileMovingCheckbox, LM.GUI.ALIGN_LEFT, 0)
	
	self.leaveBonesSelectedCheckbox = LM.GUI.CheckBox(self:Localize('Leave Bones Selected'), self.LEAVE_BONES_SELECTED)
    layout:AddChild(self.leaveBonesSelectedCheckbox, LM.GUI.ALIGN_LEFT, 0)
	
	self.bakeAdjacentFramesCheckbox = LM.GUI.CheckBox(self:Localize('Bake Adjacent Frames'), self.BAKE_ADJACENT_FRAMES)
    layout:AddChild(self.bakeAdjacentFramesCheckbox, LM.GUI.ALIGN_LEFT, 0)
	
	self.maintainProportionsCheckbox = LM.GUI.CheckBox(self:Localize('Maintain Proportions'), self.MAINTAIN_PROPORTIONS)
    layout:AddChild(self.maintainProportionsCheckbox, LM.GUI.ALIGN_LEFT, 0)
end

function MR_MoveTargetedJoint:UpdateWidgets(moho)
	MR_MoveTargetedJoint.hideBonesWhileMovingCheckbox:SetValue(self.hideBonesWhileMoving)
	MR_MoveTargetedJoint.leaveBonesSelectedCheckbox:SetValue(self.leaveBonesSelected)
	MR_MoveTargetedJoint.bakeAdjacentFramesCheckbox:SetValue(self.bakeAdjacentFrames)
	MR_MoveTargetedJoint.maintainProportionsCheckbox:SetValue(self.maintainProportions)
end

function MR_MoveTargetedJoint:HandleMessage(moho, view, msg)
	if msg == self.HIDE_BONES_WHILE_MOVING then
        self.hideBonesWhileMoving = self.hideBonesWhileMovingCheckbox:Value()
	elseif msg == self.LEAVE_BONES_SELECTED then
		self.leaveBonesSelected = self.leaveBonesSelectedCheckbox:Value()
	elseif msg == self.BAKE_ADJACENT_FRAMES then
		self.bakeAdjacentFrames = self.bakeAdjacentFramesCheckbox:Value()	
    elseif msg == self.MAINTAIN_PROPORTIONS then
		self.maintainProportions = self.maintainProportionsCheckbox:Value()	
    end
end

-- **************************************************
-- Localization
-- **************************************************

function MR_MoveTargetedJoint:Localize(text)
	local phrase = {}

	phrase['Description'] = 'Precisely control knees and elbows when using target bones.'
	phrase['UILabel'] = 'Move Targeted Joint'
	
	phrase['Hide bones while moving'] = 'Hide bones while moving'
	phrase['Leave Bones Selected'] = 'Leave bones selected'
	phrase['Bake Adjacent Frames'] = 'Bake adjacent frames'
	phrase['Maintain Proportions'] = 'Maintain proportions'

	local fileWord = MOHO.Localize("/Menus/File/File=File")
	if fileWord == "Файл" then
		phrase['Description'] = 'Тонко контролируйте колени и локти персонажей подконтрольные целевым костям.'
		phrase['UILabel'] = 'Контроль целевого сустава'
		
		phrase['Hide bones while moving'] = 'Прятать кости при движении'
		phrase['Leave Bones Selected'] = 'Оставить кости выделенными'
		phrase['Bake Adjacent Frames'] = 'Запечь соседние кадры'
		phrase['Maintain Proportions'] = 'Соблюдать пропорции'
	end

	return phrase[text]
end

Icon
Move Targeted Joint
Listed

Script type: Tool

Uploaded: Jan 08 2021, 09:07

Last modified: May 12 2021, 09:00

Script Version: 1.2

This tool allows you to easily fine-tune the elbows and knees of your character when target bones are in use.



To work correctly it requires a hierarchical pair of bones controlled by a target. The bones must not be flipped. The lower bone (the shin or the forearm) must not have any Y-axis translation.

This script is no longer supported, please download MR Pose Tool instead. https://mohoscripts.com/script/mr_pose_tool MR Pose Tool has the functionality of this tool and even more.



This script, and all other scripts on this site are distributed as free software under the GNU General Public License 3.0 or later.
Downloads count: 2761