589 lines
18 KiB
Plaintext
Executable File
589 lines
18 KiB
Plaintext
Executable File
--
|
|
-- File:: RsClothLiveLink.ms
|
|
-- Description:: RAGE Cloth Editor Live-Link Rollout
|
|
--
|
|
-- Author:: Michael Taschler <michael.taschler@rockstarnorth.com>
|
|
-- Date:: 02 February 2012
|
|
--
|
|
|
|
-----------------------------------------------------------------------------
|
|
-- Rollout
|
|
-----------------------------------------------------------------------------
|
|
rollout RageClothLiveLink "Cloth Live Link" width:280 height:100
|
|
(
|
|
-------------------------------------------------------------------------
|
|
-- Locals
|
|
-------------------------------------------------------------------------
|
|
local connectionTimer = undefined -- Timer used to periodically verify that a connection is up.
|
|
local connectionTimerInterval = 5000 -- Tick every 5 seconds
|
|
local restTimer = undefined -- Timer used to periodically verify that a connection is up.
|
|
local restTimerInterval = 1000 -- Tick every second
|
|
local liveClothEditingService
|
|
local curObjSel = undefined
|
|
|
|
-------------------------------------------------------------------------
|
|
-- UI Widgets
|
|
-------------------------------------------------------------------------
|
|
checkButton btnConnectToGame "Connect to Game" tooltip:"Toggle this to establish a connection with the game." width:250
|
|
edittext etLinkStatus "Link Status:" text:"Not connected to the game" enabled:false
|
|
button btnSpawnCharacter "Spawn Character in Game" width:250 enabled:false
|
|
checkButton btnLiveUpdate "Enable Live Updates" tooltip:"Toggle this for changes you make in max to be automatically sent to the game." width:250 enabled:false
|
|
edittext etRestStatus "REST Status:" text:"Not registered" enabled:false caption:"test" tooltip:"blah"
|
|
label liveLinkInfo "While Live-Editing is enabled only the pin radii can be modified" width:260 height:28 style_sunkenedge:true offset:[15,0] visible:false
|
|
|
|
------------------------------------------------------------------------------------
|
|
-- RAG Widgets:
|
|
------------------------------------------------------------------------------------
|
|
local CreatePedsWidgetName = "Peds/Create Peds widgets"
|
|
local RentaCrowdPed1WidgetName = "Peds/RentaCrowd(tm)/Crowd Ped 1"
|
|
local RentaCrowdPed2WidgetName = "Peds/RentaCrowd(tm)/Crowd Ped 2"
|
|
local RentaCrowdPed3WidgetName = "Peds/RentaCrowd(tm)/Crowd Ped 3"
|
|
local RentaCrowdSizeWidgetName = "Peds/RentaCrowd(tm)/Crowd size"
|
|
local RentaCrowdSpawnWidgetName = "Peds/RentaCrowd(tm)/Spawn crowd"
|
|
|
|
local CreateClothWidgetName = "Cloth Management/Create Widgets"
|
|
local ClothManagementBankName = "Cloth Management"
|
|
local ClothRestInterfaceWidgetName = "Rest Interface"
|
|
------------------------------------------------------------------------------------
|
|
|
|
-------------------------------------------------------------------------
|
|
-- Methods
|
|
-------------------------------------------------------------------------
|
|
|
|
-- Called whenever the selection changes
|
|
fn UpdateInterface sel =
|
|
(
|
|
curObjSel = sel
|
|
|
|
if(curObjSel != undefined) then
|
|
(
|
|
if (btnConnectToGame.state == on) then
|
|
(
|
|
btnLiveUpdate.enabled = true
|
|
)
|
|
)
|
|
else
|
|
(
|
|
if (btnLiveUpdate.state == on) then
|
|
(
|
|
btnLiveUpdate.state = off
|
|
RageClothLiveLink.OnLiveUpdateStateChanged false
|
|
btnLiveUpdate.enabled = false
|
|
)
|
|
)
|
|
)
|
|
|
|
fn GetSelectedVertCount =
|
|
(
|
|
count = 0
|
|
curObjSel = ::GetValidObjectSelection()
|
|
|
|
if (curObjSel != undefined) then
|
|
(
|
|
count = polyop.getNumVerts curObjSel
|
|
)
|
|
|
|
return count
|
|
)
|
|
|
|
fn GetRestInterfaceWidgetName =
|
|
(
|
|
local tokens = filterString maxFileName "."
|
|
local vertCount = GetSelectedVertCount()
|
|
local restInterfaceWidget = stringStream ""
|
|
format "%/% % vertices/%" ClothManagementBankName tokens[1] vertCount ClothRestInterfaceWidgetName to:restInterfaceWidget
|
|
--print (restInterfaceWidget as string)
|
|
return (restInterfaceWidget as string)
|
|
)
|
|
|
|
-- Create the cloth management and ped widgets
|
|
fn InitialiseRAGWidgets =
|
|
(
|
|
RemoteConnection.SendCommand("widget \"" + CreateClothWidgetName + "\"")
|
|
RemoteConnection.SendCommand("widget \"" + CreatePedsWidgetName + "\"");
|
|
RemoteConnection.SendSyncCommand()
|
|
)
|
|
|
|
fn GetMaxValuesFromChannel obj dataChannel =
|
|
(
|
|
local values = #()
|
|
|
|
if (GetChannelSupport obj dataChannel) then
|
|
(
|
|
for i in 1 to obj.numverts do
|
|
(
|
|
append values (GetChannelValue obj dataChannel i)
|
|
)
|
|
)
|
|
|
|
return values
|
|
)
|
|
|
|
fn SetMaxValuesForChannel obj dataChannel values =
|
|
(
|
|
if (GetChannelSupport obj dataChannel) then
|
|
(
|
|
for i in 1 to obj.numverts do
|
|
(
|
|
SetChannelValue obj dataChannel i values[i]
|
|
)
|
|
)
|
|
)
|
|
|
|
-- Verifies that max and the game have the same values set, prompting the user if this isn't the case
|
|
-- Returns whether the values are now in sync
|
|
fn CheckInGameValuesMatchMax =
|
|
(
|
|
try
|
|
(
|
|
--liveClothEditingService.UpdateVertexMapping()
|
|
|
|
-- Check that the number of verts in max match those in game
|
|
local inGamePinRadii = liveClothEditingService.GetAllPinRadii(0)
|
|
local maxPinRadii = (GetMaxValuesFromChannel curObjSel RAGEClothChannelTable[2].vDataChn)
|
|
local pinRadiiAreSame = true
|
|
/*
|
|
print ("InGame Radii: " + (inGamePinRadii.count as string))
|
|
for i in 1 to inGamePinRadii.count do
|
|
(
|
|
format "%, " inGamePinRadii[i]
|
|
)
|
|
format "\n"
|
|
print ("Max Radii: " + (maxPinRadii.count as string))
|
|
for i in 1 to maxPinRadii.count do
|
|
(
|
|
format "%, " maxPinRadii[i]
|
|
)
|
|
format "\n"
|
|
*/
|
|
for i in 1 to inGamePinRadii.count do
|
|
(
|
|
if (abs(inGamePinRadii[i] - maxPinRadii[i]) > 0.0001) then
|
|
(
|
|
pinRadiiAreSame = false
|
|
break
|
|
)
|
|
)
|
|
|
|
if (pinRadiiAreSame == false) then
|
|
(
|
|
local msg = "The pin radii values set for this cloth are different in max to those in game.\n" +
|
|
"Do you wish to copy the max values to game or do you wish to copy the game values to max?\n\n" +
|
|
"Select:\n" +
|
|
"\t'Yes' to send the max values to the game\n" +
|
|
"\t'No' to copy the game values into max\n" +
|
|
"\t'Cancel' to disable live editing."
|
|
|
|
local result = yesNoCancelBox msg title:"Pin Radii Mismatch"
|
|
|
|
if (result == #yes) then
|
|
(
|
|
print "Updating game pin radii"
|
|
--print maxPinRadii
|
|
liveClothEditingService.UpdateAllPinRadii 0 maxPinRadii
|
|
)
|
|
else if (result == #no) then
|
|
(
|
|
print "Updating max pin radii"
|
|
--print inGamePinRadii
|
|
SetMaxValuesForChannel curObjSel RAGEClothChannelTable[2].vDataChn inGamePinRadii
|
|
)
|
|
else
|
|
(
|
|
return false
|
|
)
|
|
)
|
|
|
|
return true
|
|
)
|
|
catch
|
|
(
|
|
messagebox liveClothEditingService.LastExceptionMessage title:liveClothEditingService.LastExceptionType
|
|
return false
|
|
)
|
|
)
|
|
|
|
fn OnConnectionStateChanged connected =
|
|
(
|
|
if( connected ) then
|
|
(
|
|
if ( not RemoteConnection.IsConnected() ) then
|
|
(
|
|
RemoteConnection.Connect()
|
|
)
|
|
|
|
ipAddress = RemoteConnection.GetGameIP()
|
|
--print ("IP: " + ipAddress)
|
|
|
|
if ( RemoteConnection.IsConnected() and (ipAddress != undefined) ) then
|
|
(
|
|
etLinkStatus.text = ("Connected to game at " + ipAddress)
|
|
liveClothEditingService = dotnetobject "RSG.RESTServices.Game.Cloth.LiveClothEditingService" ipAddress
|
|
connectionTimer.Start()
|
|
|
|
if (curObjSel != undefined) then
|
|
(
|
|
print "btnLiveUpdate.enabled = true"
|
|
btnLiveUpdate.enabled = true
|
|
)
|
|
btnSpawnCharacter.enabled = true
|
|
InitialiseRAGWidgets()
|
|
)
|
|
else
|
|
(
|
|
messagebox "No connection to the game could be established"
|
|
btnSpawnCharacter.enabled = false
|
|
btnConnectToGame.state = off
|
|
etLinkStatus.text = "Not connected to the game"
|
|
btnLiveUpdate.state = off
|
|
btnLiveUpdate.enabled = false
|
|
liveClothEditingService = undefined
|
|
connectionTimer.Stop()
|
|
)
|
|
)
|
|
else
|
|
(
|
|
etLinkStatus.text = "Not connected to the game"
|
|
btnSpawnCharacter.enabled = false
|
|
btnLiveUpdate.state = off
|
|
btnLiveUpdate.enabled = false
|
|
liveClothEditingService = undefined
|
|
connectionTimer.Stop()
|
|
)
|
|
)
|
|
|
|
-- Retrieves distance down the tree this particular object is
|
|
fn GetNumLevels obj =
|
|
(
|
|
levels = 0
|
|
|
|
if (obj != undefined) then
|
|
(
|
|
while (obj.parent != undefined) do
|
|
(
|
|
levels = levels + 1
|
|
obj = obj.parent
|
|
)
|
|
)
|
|
|
|
return levels
|
|
)
|
|
|
|
-- Retrieves the root bone from a skin modifier
|
|
fn GetRootBone curObjSel theSkin =
|
|
(
|
|
-- Keep track of what was selected in the modifiers panel before changing it
|
|
local previousSelectedObject = ModPanel.GetCurrentObject()
|
|
SetCommandPanelTaskMode #modify
|
|
ModPanel.SetCurrentObject curObjSel.modifiers[#skin]
|
|
|
|
--print "Skin:"
|
|
--print theSkin
|
|
|
|
if (theSkin == undefined) then
|
|
(
|
|
return undefined
|
|
)
|
|
|
|
boneCount = skinops.getnumberbones theSkin
|
|
if (boneCount == 0) then
|
|
(
|
|
return undefined
|
|
)
|
|
--print boneCount
|
|
|
|
local boneObjName = skinops.GetBoneName theSkin 1 0
|
|
local rootBoneNode = getNodeByName boneObjName
|
|
|
|
for boneIdx = 2 to boneCount do
|
|
(
|
|
boneObjName = skinops.GetBoneName theSkin boneIdx 0
|
|
boneNode = getNodeByName boneObjName
|
|
|
|
if (boneNode != undefined) then
|
|
(
|
|
boneNodeLevels = GetNumLevels boneNode
|
|
rootBoneNodeLevels = GetNumLevels rootBoneNode
|
|
|
|
if (boneNodeLevels < rootBoneNodeLevels) then
|
|
(
|
|
rootBoneNode = boneNode
|
|
)
|
|
)
|
|
)
|
|
|
|
--print "Root Bone:"
|
|
--print rootBoneNode
|
|
--ModPanel.SetCurrentObject previousSelectedObject
|
|
|
|
return rootBoneNode
|
|
)
|
|
|
|
-- Does the same thing that the exported does so that we can attempt to match up in-game verts
|
|
-- exposed via REST to those in max.
|
|
fn GetExportedVertsForSelection =
|
|
(
|
|
local exportedVertices = #()
|
|
curObjSel = ::GetValidObjectSelection()
|
|
|
|
if (curObjSel != undefined) then
|
|
(
|
|
-- Get the root bone node for calculating the vert transform
|
|
rootBoneNode = GetRootBone curObjSel curObjSel.modifiers[#skin]
|
|
|
|
if (rootBoneNode != undefined) then
|
|
(
|
|
parentNode = rootBoneNode.parent
|
|
--print "Parent:"
|
|
--print parentNode
|
|
|
|
parentTransform = parentNode.transform
|
|
invParentTransform = inverse parentTransform
|
|
|
|
--print invParentTransform
|
|
|
|
objTransform = curObjSel.transform
|
|
orthogonalize objTransform
|
|
objTranslation = objTransform.translation
|
|
modifiedObjTransform = transMatrix objTranslation
|
|
|
|
SetCommandPanelTaskMode #modify
|
|
ModPanel.SetCurrentObject curObjSel
|
|
|
|
for i=1 to (polyop.getNumVerts curObjSel) do
|
|
(
|
|
currentVert = polyop.getVert curObjSel i node:$.parent
|
|
transformedVert = currentVert * modifiedObjTransform * invParentTransform
|
|
|
|
-- Convert the exported vert to a RSG.Base.Math.Vector3f's
|
|
rsgBaseVert = dotNetObject "RSG.Base.Math.Vector3f" transformedVert.x transformedVert.y transformedVert.z
|
|
|
|
append exportedVertices rsgBaseVert
|
|
)
|
|
)
|
|
)
|
|
|
|
return exportedVertices
|
|
)
|
|
|
|
fn OnLiveUpdateStateChanged enabled =
|
|
(
|
|
if (enabled) then
|
|
(
|
|
local restInterfaceWidgetName = GetRestInterfaceWidgetName()
|
|
local keepGoing = false
|
|
|
|
if (RemoteConnection.WidgetExists(restInterfaceWidgetName) == true) then
|
|
(
|
|
RemoteConnection.WriteBoolWidget restInterfaceWidgetName true
|
|
keepGoing = true
|
|
)
|
|
else
|
|
(
|
|
keepGoing = queryBox "Unable to determine if the character is present in game!\n\nPlease ensure that the character is spawned and that the REST interface is enabled for the character you are editing:\nCloth Management -> <character_name> <vertex_count> <address> -> Rest Interface\n\nClick 'yes' once this has been set up, or 'no' to cancel." title:"Warning: RAG Connection Issue"
|
|
)
|
|
|
|
if (keepGoing) then
|
|
(
|
|
-- Get the vertices as they would appear in the independent export data
|
|
exportedVertices = GetExportedVertsForSelection()
|
|
--print "Exported Verts:"
|
|
--print exportedVertices
|
|
|
|
local pinRadii = undefined
|
|
|
|
try
|
|
(
|
|
liveClothEditingService.UpdateVertexMapping(exportedVertices)
|
|
|
|
-- Check that the number of verts in max match those in game
|
|
pinRadii = liveClothEditingService.GetAllPinRadii(0)
|
|
)
|
|
catch
|
|
(
|
|
messagebox liveClothEditingService.LastExceptionMessage title:liveClothEditingService.LastExceptionType
|
|
)
|
|
|
|
--print ("Pin Radii Count: " + (pinRadii.count as string))
|
|
--print ("Selection Count: " + ($.numverts as string))
|
|
|
|
-- Check that we managed to match the pin radii
|
|
if (pinRadii == undefined) then
|
|
(
|
|
etRestStatus.text = "Not registered. Max->Game mapping issue."
|
|
btnLiveUpdate.state = off
|
|
liveClothEditingService.ClearVertexMapping()
|
|
)
|
|
-- Ensure that the game version of the piece of cloth has the same number of verts as the max version
|
|
else if (pinRadii.count != $.numverts) then
|
|
(
|
|
local msg = "Unable to live edit the cloth due to a mismatch in the number of vertices.\n" +
|
|
"The max version of the cloth has " + ($.numverts as string) +
|
|
" verts whereas the game version has " + (pinRadii.count as string) + " verts."
|
|
|
|
messagebox msg title:"Vertex Count Mismatch"
|
|
|
|
etRestStatus.text = "Not registered. Vertex count mismatch."
|
|
btnLiveUpdate.state = off
|
|
liveClothEditingService.ClearVertexMapping()
|
|
)
|
|
else
|
|
(
|
|
-- Last thing to check is that the ingame values match the max values
|
|
if (CheckInGameValuesMatchMax() == false) then
|
|
(
|
|
etRestStatus.text = "Not registered. Aborted due to data mismatch."
|
|
btnLiveUpdate.state = off
|
|
liveClothEditingService.ClearVertexMapping()
|
|
)
|
|
else
|
|
(
|
|
local tokens = filterString maxFileName "."
|
|
etRestStatus.text = ("Registered to " + tokens[1])
|
|
restTimer.Start()
|
|
)
|
|
)
|
|
)
|
|
else
|
|
(
|
|
etRestStatus.text = "Not registered. Aborted by user."
|
|
btnLiveUpdate.state = off
|
|
liveClothEditingService.ClearVertexMapping()
|
|
restTimer.Stop()
|
|
)
|
|
)
|
|
else
|
|
(
|
|
etRestStatus.text = "Not registered"
|
|
liveClothEditingService.ClearVertexMapping()
|
|
restTimer.Stop()
|
|
)
|
|
|
|
liveLinkInfo.visible = (btnLiveUpdate.state == on)
|
|
::RageClothChannelTuning.UpdateInterface()
|
|
)
|
|
|
|
-- Monitors the state of the game connection
|
|
fn OnConnectionTick s e =
|
|
(
|
|
if (btnConnectToGame.state == on and RemoteConnection.IsConnected() == false) then
|
|
(
|
|
--If connection has faltered, then notify the UI.
|
|
print "Lost connection to the game!"
|
|
btnConnectToGame.state = off
|
|
OnConnectionStateChanged false
|
|
)
|
|
)
|
|
|
|
-- Monitors the state of the rest connection
|
|
fn OnRestConnectionTick s e =
|
|
(
|
|
if (btnLiveUpdate.state == on) then
|
|
(
|
|
local restInterfaceWidgetName = GetRestInterfaceWidgetName()
|
|
|
|
-- Check that the cloth is still on screen and that the REST connection is still
|
|
-- registered to it.
|
|
if (RemoteConnection.WidgetExists(restInterfaceWidgetName) == false or
|
|
RemoteConnection.ReadBoolWidget(restInterfaceWidgetName) == false) then
|
|
(
|
|
btnLiveUpdate.state = off
|
|
OnLiveUpdateStateChanged false
|
|
etRestStatus.text = "Lost connection"
|
|
)
|
|
)
|
|
)
|
|
|
|
fn OnOpen =
|
|
(
|
|
--Create a periodic timer to ping the connection status in the assembly.
|
|
connectionTimer = dotNetObject "System.Windows.Forms.Timer"
|
|
dotnet.addEventHandler connectionTimer "tick" OnConnectionTick
|
|
connectionTimer.Interval = connectionTimerInterval
|
|
|
|
restTimer = dotNetObject "System.Windows.Forms.Timer"
|
|
dotnet.addEventHandler restTimer "tick" OnRestConnectionTick
|
|
restTimer.Interval = restTimerInterval
|
|
|
|
-- Update the locally stored selection
|
|
curObjSel = ::GetValidObjectSelection()
|
|
)
|
|
|
|
fn OnClose =
|
|
(
|
|
-- Switch off the live connection
|
|
btnConnectToGame.state = off
|
|
|
|
dotnet.removeEventHandler connectionTimer "tick" OnConnectionTick
|
|
connectionTimer = undefined
|
|
|
|
dotnet.removeEventHandler restTimer "tick" OnRestConnectionTick
|
|
restTimer = undefined
|
|
)
|
|
|
|
fn OnSpawnCharacter =
|
|
(
|
|
local tokens = filterString maxFileName "."
|
|
|
|
RemoteConnection.WriteStringWidget RentaCrowdPed1WidgetName tokens[1]
|
|
RemoteConnection.WriteIntWidget RentaCrowdPed2WidgetName 0
|
|
RemoteConnection.WriteIntWidget RentaCrowdPed3WidgetName 0
|
|
RemoteConnection.WriteIntWidget RentaCrowdSizeWidgetName 1
|
|
RemoteConnection.SendCommand("widget \"" + RentaCrowdSpawnWidgetName + "\"");
|
|
RemoteConnection.SendSyncCommand()
|
|
)
|
|
|
|
fn UpdatePinRadiiForSelection selectedVerts newValue =
|
|
(
|
|
if (btnLiveUpdate.state == on and btnLiveUpdate.state == on) then
|
|
(
|
|
try
|
|
(
|
|
-- For now transfer the values one by one
|
|
nSelVerts = selectedVerts.count
|
|
for i in 1 to nSelVerts do
|
|
(
|
|
liveClothEditingService.UpdatePinRadius (selectedVerts[i].index - 1) 0 newValue
|
|
)
|
|
)
|
|
catch
|
|
(
|
|
messagebox liveClothEditingService.LastExceptionMessage title:liveClothEditingService.LastExceptionType
|
|
)
|
|
)
|
|
)
|
|
|
|
-------------------------------------------------------------------------
|
|
-- Events
|
|
-------------------------------------------------------------------------
|
|
|
|
-- rollout is opening
|
|
on RageClothLiveLink open do
|
|
(
|
|
OnOpen()
|
|
)
|
|
|
|
-- rollout is closing
|
|
on RageClothLiveLink close do
|
|
(
|
|
OnClose()
|
|
)
|
|
|
|
-- connect to game button changes state
|
|
on btnConnectToGame changed state do
|
|
(
|
|
OnConnectionStateChanged (state == on)
|
|
)
|
|
|
|
-- user requests that the character is spawned
|
|
on btnSpawnCharacter pressed do
|
|
(
|
|
OnSpawnCharacter()
|
|
)
|
|
|
|
-- enable live updates button changes state
|
|
on btnLiveUpdate changed state do
|
|
(
|
|
OnLiveUpdateStateChanged (state == on)
|
|
)
|
|
) |