-- -- File:: RsClothLiveLink.ms -- Description:: RAGE Cloth Editor Live-Link Rollout -- -- Author:: Michael Taschler -- 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 ->
-> 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) ) )