/*==LICENSE==* CyanWorlds.com Engine - MMOG client, server and tools Copyright (C) 2011 Cyan Worlds, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . You can contact Cyan Worlds, Inc. by email legal@cyan.com or by snail mail at: Cyan Worlds, Inc. 14617 N Newport Hwy Mead, WA 99021 *==LICENSE==*/ #include "HeadSpin.h" #include #include "plComponentProcBase.h" #include "resource.h" #include "plComponent.h" #include "plComponentReg.h" #include "../MaxConvert/hsConverterUtils.h" #include "../pnSceneObject/plSceneObject.h" #include "plgDispatch.h" #include "../plScene/plSceneNode.h" #include "../MaxConvert/hsConverterUtils.h" #include "../MaxConvert/hsControlConverter.h" #include "../MaxConvert/hsMaterialConverter.h" #include "../MaxConvert/plBitmapCreator.h" #include "hsStringTokenizer.h" #include "../MaxMain/plMaxNode.h" #include "../pnKeyedObject/plKey.h" #include "hsResMgr.h" #include "../pnMessage/plNodeRefMsg.h" #include "../pnMessage/plObjRefMsg.h" #include "../pnMessage/plIntRefMsg.h" #include "../plMessage/plMatRefMsg.h" #include "../plMessage/plLayRefMsg.h" #include "plMaxAnimUtils.h" #include "../plInterp/plController.h" #include "../plPhysical/plSimDefs.h" #include "plPhysicsGroups.h" #include "../plAudible/plWinAudible.h" #include "../pnSceneObject/plAudioInterface.h" #include "../pnSceneObject/plCoordinateInterface.h" #include "../plSurface/plLayerAnimation.h" #include "../plSurface/hsGMaterial.h" #include "../plAudio/plWin32StaticSound.h" #include "../plAudioCore/plSoundBuffer.h" #include "plAudioComponents.h" #include "../MaxMain/plPlasmaRefMsgs.h" #include "../plAvatar/plArmatureMod.h" #include "../plAvatar/plAvBrainHuman.h" #include "../plAvatar/plAvBrainCritter.h" #include "../plAvatar/plAvatarClothing.h" #include "../plAvatar/plArmatureEffects.h" #include "../plGImage/plMipmap.h" #include "../plGImage/plLODMipmap.h" // Auto generation of shadows here. #include "plShadowComponents.h" #include "../plGLight/plShadowCaster.h" #include "plAvatarComponent.h" #include "../../tools/MaxComponent/plPhysicalComponents.h" #include "../MaxMain/plPhysicalProps.h" //#include //#include #include "plPickNode.h" #include "plPickMaterialMap.h" #include "../MaxMain/plMtlCollector.h" //#define BOB_SORT_AVATAR_FACES // CONSTANTS float kStdRestitution = 0.5f; float kStdFriction = 0.1f; void DummyCodeIncludeAvatarFunc() {} // PROTOTYPES class plAvatarComponent; class plCritterComponent; class plArmatureComponent; class plCritterCommands; plArmatureMod* plArmatureComponent::IGenerateMyArmMod(plHKPhysical* myHKPhys, plMaxNode* node) { plArmatureMod *avMod = TRACKED_NEW plArmatureMod(); avMod->SetRootName(node->GetKey()->GetName()); return avMod; } void plArmatureComponent::ISetupAvatarRenderPropsRecurse(plMaxNode *node) { node->SetNoSpanSort(true); node->SetNoFaceSort(true); node->SetNoDeferDraw(true); int i; for (i = 0; i < node->NumberOfChildren(); i++) { plMaxNode *pChild = (plMaxNode *)node->GetChildNode(i); ISetupAvatarRenderPropsRecurse(pChild); } } //SETUPPROPERTIES // Tests if IPB2 pointers are healthy and sets up the MaxNode Data hsBool plArmatureComponent::SetupProperties(plMaxNode* node, plErrorMsg* pErrMsg) { // Global issues node->SetMovable(true); node->SetForceLocal(true); #ifndef BOB_SORT_AVATAR_FACES ISetupAvatarRenderPropsRecurse(node); #endif // float mass, friction, restitution; // if(ClassID() == AVATAR_CLASS_ID || ClassID() == LOD_AVATAR_CLASS_ID) // { // mass = 12; // *** good number from old physical player // restitution = 0.5f; // friction = 0.1f; // } plMaxNode *animRoot = nil; if(ClassID() == AVATAR_CLASS_ID) animRoot = (plMaxNode *)fCompPB->GetINode(plAvatarComponent::kRootNode); else if (ClassID() == LOD_AVATAR_CLASS_ID) animRoot = (plMaxNode *)fCompPB->GetINode(plLODAvatarComponent::kRootNodeAddBtn); if(animRoot) { const char *nodeName = animRoot->GetName(); animRoot->SetDrawable(false); // make sure our root bone is invisible } // Ignore all of the old physicals, so old scenes can export if (ClassID() == AVATAR_CLASS_ID || ClassID() == LOD_AVATAR_CLASS_ID) { bool isLOD = ((ClassID() == LOD_AVATAR_CLASS_ID) != 0); plMaxNode* ignoreNode = (plMaxNode*)fCompPB->GetINode(isLOD ? plLODAvatarComponent::kPhysicsProxyFeet_DEAD : plAvatarComponent::kPhysicsProxyFeet_DEAD); if (ignoreNode) ignoreNode->SetCanConvert(false); ignoreNode = (plMaxNode*)fCompPB->GetINode(isLOD ? plLODAvatarComponent::kPhysicsProxyTorso_DEAD : plAvatarComponent::kPhysicsProxyTorso_DEAD); if (ignoreNode) ignoreNode->SetCanConvert(false); ignoreNode = (plMaxNode*)fCompPB->GetINode(isLOD ? plLODAvatarComponent::kPhysicsProxyHead_DEAD : plAvatarComponent::kPhysicsProxyHead_DEAD); if (ignoreNode) ignoreNode->SetCanConvert(false); } return true; } //// ISETARMATURESORECURSE // Do some strange magic to make sure that we know when all the parts of the avatar are loaded. void plArmatureComponent::ISetArmatureSORecurse(plMaxNode *node, plSceneObject *so) { if (node->CanConvert()) node->SetAvatarSO(so); int i; for (i = 0; i < node->NumberOfChildren(); i++) { plMaxNode *pChild = (plMaxNode *)node->GetChildNode(i); ISetArmatureSORecurse(pChild, so); } } hsBool plArmatureComponent::PreConvert(plMaxNode* node, plErrorMsg* pErrMsg) { // add audio interface and record/playback component pl2WayWinAudible* pAudible = TRACKED_NEW pl2WayWinAudible; // Add a key for it plKey key = hsgResMgr::ResMgr()->NewKey(IGetUniqueName(node), pAudible, node->GetLocation() ); plAudioInterface* ai = TRACKED_NEW plAudioInterface; plKey pAiKey = hsgResMgr::ResMgr()->NewKey(IGetUniqueName(node), (hsKeyedObject*)ai,node->GetLocation()); hsgResMgr::ResMgr()->AddViaNotify(pAiKey, TRACKED_NEW plObjRefMsg(node->GetKey(), plRefMsg::kOnCreate, 0, plObjRefMsg::kInterface), plRefFlags::kActiveRef); plIntRefMsg* pMsg = TRACKED_NEW plIntRefMsg(node->GetKey(), plRefMsg::kOnCreate, 0, plIntRefMsg::kAudible); hsgResMgr::ResMgr()->AddViaNotify(pAudible->GetKey(), pMsg, plRefFlags::kActiveRef ); ISetArmatureSORecurse(node, node->GetSceneObject()); // Uncomment this line to enable a single bone pallete for the entire avatar. node->SetupBoneHierarchyPalette(); return true; } // this is a little gross...the armature component shouldn't know that the subclasses // actually exist....it's a hard-to-detect implementation detail that breaks new subclasses.... hsBool plArmatureComponent::Convert(plMaxNode* node, plErrorMsg *pErrMsg) { // plHKPhysical *physical = plHKPhysical::ConvertToPhysical(node->GetSceneObject()); // physical->SetProperty(plSimulationInterface::kUpright, true); IAttachModifiers(node, pErrMsg); ISetupClothes(node, fArmMod, pErrMsg); // ArmatureEffects plArmatureEffectsMgr *effects = TRACKED_NEW plArmatureEffectsMgr(); hsgResMgr::ResMgr()->NewKey(IGetUniqueName(node), effects, node->GetLocation()); plGenRefMsg *msg = TRACKED_NEW plGenRefMsg(fArmMod->GetKey(), plRefMsg::kOnCreate, -1, -1); hsgResMgr::ResMgr()->AddViaNotify(effects->GetKey(), msg, plRefFlags::kActiveRef); // Attach effects plSceneObject *obj = node->GetSceneObject(); node->MakeCharacterHierarchy(pErrMsg); const plAGModifier *temp = static_cast(FindModifierByClass(obj, plAGModifier::Index())); plAGModifier *agMod = const_cast(temp); hsAssert(agMod, "Armature root didn't get a agmod. I'll make one for you."); if( ! agMod) { // MakeCharacterHierarchy will attach agmodifiers to all the bones in the hierarchy; // have to manually add any for non-bone objects... agMod = TRACKED_NEW plAGModifier("Handle"); // the player root is known as the handle node->AddModifier(agMod, IGetUniqueName(node)); } agMod->SetChannelName("Handle"); // Get the position and radius of the head and torso physicals if (ClassID() == AVATAR_CLASS_ID || ClassID() == LOD_AVATAR_CLASS_ID) { bool isLOD = ((ClassID() == LOD_AVATAR_CLASS_ID) != 0); float height = fCompPB->GetFloat(isLOD ? plLODAvatarComponent::kPhysicsHeight : plAvatarComponent::kPhysicsHeight); float width = fCompPB->GetFloat(isLOD ? plLODAvatarComponent::kPhysicsWidth : plAvatarComponent::kPhysicsWidth); fArmMod->SetPhysicalDims(height, width); } // node->SetupBonesAliasesRecur(node->GetKey()->GetName()); return true; } hsBool plArmatureComponent::IVerifyUsedNode(INode* thisNode, plErrorMsg* pErrMsg, hsBool IsHull) { if(thisNode != NULL) { if(((plMaxNode*)thisNode)->CanConvert()) { if(IsHull) ((plMaxNode*)thisNode)->SetDrawable(false); }else { pErrMsg->Set(true, "Ignored Node Selection", "The object that Node Ptr %s refs was set to be Ignored. Avatar Component failure.", thisNode->GetName()); pErrMsg->Set(false); return false; } }else{ pErrMsg->Set(true, "Empty Node in Avatar Component", "It is imperative that all the node pickers have real values.\n Avatar Component failure.").Show(); pErrMsg->Set(false); return false; } return true; } void plArmatureComponent::IAttachShadowCastModifiersRecur(plMaxNode* node, plShadowCaster* caster) { if( !node || !caster ) return; // Add test here for whether we want this guy to cast a shadow. plShadowCastComponent::AddShadowCastModifier(node, caster); int i; for( i = 0; i < node->NumberOfChildren(); i++ ) IAttachShadowCastModifiersRecur((plMaxNode*)node->GetChildNode(i), caster); } //class AvatarStats //{ //public: // float fFriction; // float fMaxVel; // float fAccel; // float fTurnForce; // hsBool fUseNewMovement; // // AvatarStats(float a, float b, float c, float d, hsBool newMove) // : fFriction(a), fMaxVel(b), fAccel(c), fTurnForce(d), fUseNewMovement(newMove) {}; // AvatarStats() : fFriction(-1.0), fMaxVel(-1.0), fAccel(-1.0), fTurnForce(-1.0) {}; //}; enum { kArmMain, kArmBounce, kArmReport, }; CLASS_DESC(plAvatarComponent, gAvatarCompDesc, "Avatar", "Avatar", COMP_TYPE_AVATAR, AVATAR_CLASS_ID) //Max paramblock2 stuff below. ParamBlockDesc2 gPAvatarBk ( plComponent::kBlkComp, _T("Avatar"), 0, &gAvatarCompDesc, P_AUTO_CONSTRUCT + P_AUTO_UI + P_MULTIMAP, plComponent::kRefComp, //Roll out 1, kArmMain, IDD_COMP_AVATAR, IDS_COMP_AVATARS, 0, 0, &gAvatarCompDlgProc, // kArmBounce, IDD_COMP_PHYS_CORE_GROUP, IDS_COMP_PHYS_BOUNCE, 0, APPENDROLL_CLOSED, &gBounceGroupProc, // kArmReport, IDD_COMP_PHYS_CORE_GROUP, IDS_COMP_PHYS_REPORT, 0, APPENDROLL_CLOSED, &gReportGroupProc, // params plAvatarComponent::kPhysicsProxyFeet_DEAD, _T("ProxyFeet"), TYPE_INODE, 0, 0, // p_ui, kArmMain, TYPE_PICKNODEBUTTON, IDC_COMP_AVATAR_PROXY_PICKB, // p_sclassID, GEOMOBJECT_CLASS_ID, // p_prompt, IDS_COMP_AVATAR_PROXYS, end, plAvatarComponent::kFriction, _T("Friction"), TYPE_FLOAT, 0, 0, p_ui, kArmMain, TYPE_SPINNER, EDITTYPE_POS_FLOAT, IDC_COMP_AVATAR_FRICTION_EDIT, IDC_COMP_AVATAR_FRICTION_SPIN, 0.1f, p_default, 0.9f, end, plAvatarComponent::kRootNode, _T("RootNode"), TYPE_INODE, 0, 0, p_ui, kArmMain, TYPE_PICKNODEBUTTON, IDC_COMP_AVATAR_ROOT_PICKB, p_prompt, IDS_COMP_AVATAR_PROXYS, end, plAvatarComponent::kMeshNode, _T("MeshNode"), TYPE_INODE, 0, 0, p_ui, kArmMain, TYPE_PICKNODEBUTTON, IDC_COMP_AVATAR_MESH_PICKB, //p_sclassID, GEOMOBJECT_CLASS_ID, p_prompt, IDS_COMP_AVATAR_PROXYS, end, plAvatarComponent::kClothingGroup, _T("ClothingGroup"), TYPE_INT, 0, 0, p_default, plClothingMgr::kClothingBaseMale, end, plArmatureComponent::kBounceGroups, _T("bounceGroups"), TYPE_INT, 0,0, p_default, plPhysicsGroups_DEAD::kCreatures | plPhysicsGroups_DEAD::kStaticSimulated | plPhysicsGroups_DEAD::kDynamicSimulated | plPhysicsGroups_DEAD::kAnimated, end, plArmatureComponent::kReportGroups, _T("reportGroups"), TYPE_INT, 0,0, end, plAvatarComponent::kBrainType, _T("Brain"), TYPE_INT, 0, 0, p_default, plAvatarComponent::kBrainHuman, end, plAvatarComponent::kSkeleton, _T("Skeleton"), TYPE_INT, 0, 0, p_default, plArmatureMod::kBoneBaseMale, end, plAvatarComponent::kPhysicsProxyTorso_DEAD, _T("ProxyTorso"), TYPE_INODE, 0, 0, // p_ui, kArmMain, TYPE_PICKNODEBUTTON, IDC_COMP_AVATAR_PROXY_PICKT, // p_sclassID, GEOMOBJECT_CLASS_ID, // p_prompt, IDS_COMP_AVATAR_PROXYS, end, plAvatarComponent::kPhysicsProxyHead_DEAD, _T("ProxyHead"), TYPE_INODE, 0, 0, // p_ui, kArmMain, TYPE_PICKNODEBUTTON, IDC_COMP_AVATAR_PROXY_PICKH, // p_sclassID, GEOMOBJECT_CLASS_ID, // p_prompt, IDS_COMP_AVATAR_PROXYS, end, plAvatarComponent::kPhysicsHeight, _T("physHeight"), TYPE_FLOAT, 0, 0, p_range, 0.1f, 50.0f, p_default, 5.f, p_ui, kArmMain, TYPE_SPINNER, EDITTYPE_FLOAT, IDC_PHYS_HEIGHT_EDIT, IDC_PHYS_HEIGHT_SPIN, SPIN_AUTOSCALE, end, plAvatarComponent::kPhysicsWidth, _T("physWidth"), TYPE_FLOAT, 0, 0, p_range, 0.1f, 50.0f, p_default, 2.5f, p_ui, kArmMain, TYPE_SPINNER, EDITTYPE_FLOAT, IDC_PHYS_WIDTH_EDIT, IDC_PHYS_WIDTH_SPIN, SPIN_AUTOSCALE, end, plAvatarComponent::kBodyFootstepSoundPage, _T("bodyFootstepPage"), TYPE_STRING, 0, 0, p_default, "Audio", p_ui, kArmMain, TYPE_EDITBOX, IDC_BODYFOOTSTEPPAGE_EDIT, end, plAvatarComponent::kAnimationPrefix,_T("animationPrefix"), TYPE_STRING, 0, 0, p_default, "Male", p_ui, kArmMain, TYPE_EDITBOX, IDC_ANIMATIONPREFIX_EDIT, end, end ); plAvatarComponent::plAvatarComponent() { fClassDesc = &gAvatarCompDesc; fClassDesc->MakeAutoParamBlocks(this); } void plAvatarComponent::IAttachModifiers(plMaxNode *node, plErrorMsg *pErrMsg) { const char *name = node->GetKey()->GetName(); plMaxNode *meshNode = (plMaxNode *)fCompPB->GetINode(plAvatarComponent::kMeshNode); plKey meshKey = meshNode->GetSceneObject()->GetKey(); plMaxNode *animRootNode = (plMaxNode *)fCompPB->GetINode(plAvatarComponent::kRootNode); plKey animRootKey = animRootNode->GetSceneObject()->GetKey(); plSceneObject * bodySO = node->GetSceneObject(); plArmatureMod* avMod = TRACKED_NEW plArmatureMod(); avMod->SetRootName(name); avMod->AppendMeshKey(meshKey); int skeletonType = fCompPB->GetInt(ParamID(kSkeleton)); avMod->SetBodyType( skeletonType ); // only make a human brain if we're a human if (skeletonType == plArmatureMod::kBoneBaseCritter) avMod->PushBrain(TRACKED_NEW plAvBrainCritter()); else avMod->PushBrain(TRACKED_NEW plAvBrainHuman(skeletonType == plArmatureMod::kBoneBaseActor)); avMod->SetBodyAgeName(node->GetAgeName()); avMod->SetBodyFootstepSoundPage(fCompPB->GetStr(ParamID(kBodyFootstepSoundPage))); avMod->SetAnimationPrefix(fCompPB->GetStr(ParamID(kAnimationPrefix))); //AddLinkSound(node, node->GetSceneObject()->GetKey(), pErrMsg ); plKey avKey = hsgResMgr::ResMgr()->NewKey(IGetUniqueName(node), avMod, node->GetLocation()); plObjRefMsg *objRefMsg = TRACKED_NEW plObjRefMsg(bodySO->GetKey(), plRefMsg::kOnCreate,-1, plObjRefMsg::kModifier); hsgResMgr::ResMgr()->AddViaNotify(avKey, objRefMsg, plRefFlags::kActiveRef); fArmMod = avMod; } // Helper function for the Avatar and LOD Avatar components void AddClothingToMod(plMaxNode *node, plArmatureMod *mod, int group, hsGMaterial *mat, plErrorMsg *pErrMsg) { plGenRefMsg *msg; char keyName[256]; TSTR sdata; hsStringTokenizer toker; if (mod == nil) { hsAssert(false, "Adding clothes to a nil armatureMod."); return; } plClothingBase *base = TRACKED_NEW plClothingBase(); if (node->GetUserPropString("layout", sdata)) { toker.Reset(sdata, hsConverterUtils::fTagSeps); base->SetLayoutName(toker.next()); } else base->SetLayoutName("BasicHuman"); sprintf(keyName, "%s_ClothingBase", node->GetName()); hsgResMgr::ResMgr()->NewKey(keyName, base, node->GetLocation()); plClothingOutfit *outfit = TRACKED_NEW plClothingOutfit(); outfit->fGroup = group; sprintf(keyName, "%s_outfit", mod->GetKey()->GetName()); hsgResMgr::ResMgr()->NewKey(keyName, outfit, node->GetLocation()); msg = TRACKED_NEW plGenRefMsg(outfit->GetKey(), plRefMsg::kOnCreate, -1, -1); hsgResMgr::ResMgr()->AddViaNotify(base->GetKey(), msg, plRefFlags::kActiveRef); // Add clothing base to outfit msg = TRACKED_NEW plGenRefMsg(mod->GetKey(), plRefMsg::kOnCreate, -1, -1); hsgResMgr::ResMgr()->AddViaNotify(outfit->GetKey(), msg, plRefFlags::kActiveRef); // Attach outfit plMipmap *baseTex = nil; plLayerInterface *li = nil; if (mat != nil) { li = mat->GetLayer(0)->BottomOfStack(); baseTex = plMipmap::ConvertNoRef(li->GetTexture()); hsAssert(li->GetTexture() == baseTex, "Base texture mismatch on avatar construction?"); } if (mat != nil && baseTex != nil) { // Let's fix up these bitmap flags. Normally, they are set on convert based on // the contents of the bitmap. But here our contents are subject to change at runtime. // Fortunately, we've got a pretty good idea what to expect. // I'm making a subjective decision here on forcing 32bit even when we run in 16 bit, // because an A4R4G4B4 makes the avatar look like it has a rare skin disease. baseTex->SetFlags(plMipmap::kAlphaChannelFlag | plMipmap::kForce32Bit | plMipmap::kDontThrowAwayImage); // The tex is what the outfit and the material hold onto. It's what // gets rendered. The base will hang onto the original baseTex. msg = TRACKED_NEW plGenRefMsg(base->GetKey(), plRefMsg::kOnCreate, -1, -1); hsgResMgr::ResMgr()->SendRef(baseTex->GetKey(), msg, plRefFlags::kActiveRef); // Set base texture of avatar plLayRefMsg* layRef = TRACKED_NEW plLayRefMsg(li->GetKey(), plRefMsg::kOnRemove, 0, plLayRefMsg::kTexture); hsgResMgr::ResMgr()->SendRef(baseTex->GetKey(), layRef, plRefFlags::kActiveRef); // Remove it from the material msg = TRACKED_NEW plGenRefMsg(outfit->GetKey(), plRefMsg::kOnCreate, -1, -1); hsgResMgr::ResMgr()->AddViaNotify(li->GetKey(), msg, plRefFlags::kActiveRef); // Set outfit's target layer interface msg = TRACKED_NEW plGenRefMsg(outfit->GetKey(), plRefMsg::kOnCreate, -1, -1); hsgResMgr::ResMgr()->AddViaNotify(mat->GetKey(), msg, plRefFlags::kActiveRef); // Outfit needs the material } else { pErrMsg->Set(outfit->fGroup != plClothingMgr::kClothingBaseNoOptions, node->GetName(), "This avatar expects clothing options, but has no material on which to place them. " "It will not be visable at runtime.").CheckAskOrCancel(); outfit->fGroup = plClothingMgr::kClothingBaseNoOptions; } } void plAvatarComponent::ISetupClothes(plMaxNode *node, plArmatureMod *mod, plErrorMsg *pErrMsg) { AddClothingToMod(node, mod, fCompPB->GetInt(kClothingGroup), nil, pErrMsg); } hsBool plAvatarComponent::SetupProperties(plMaxNode* node, plErrorMsg* pErrMsg) { node->SetItinerant(true); // if(!IVerifyUsedNode(fCompPB->GetINode(plAvatarComponent::kPhysicsProxyFeet), pErrMsg, true)) // return false; // if(!IVerifyUsedNode(fCompPB->GetINode(plAvatarComponent::kPhysicsProxyTorso), pErrMsg, true)) // return false; // if(!IVerifyUsedNode(fCompPB->GetINode(plAvatarComponent::kPhysicsProxyHead), pErrMsg, true)) // return false; if(!IVerifyUsedNode(fCompPB->GetINode(plAvatarComponent::kMeshNode), pErrMsg, false)) return false; if(!IVerifyUsedNode(fCompPB->GetINode(plAvatarComponent::kRootNode), pErrMsg, false)) return false; plMaxNode *meshNode = (plMaxNode *)fCompPB->GetINode(plAvatarComponent::kMeshNode); if (meshNode) { if (meshNode->GetObjectRef()->ClassID() == Class_ID(DUMMY_CLASS_ID, 0)) { meshNode->SetSwappableGeomTarget(plArmatureMod::kSwapTargetShadow); } } return plArmatureComponent::SetupProperties(node, pErrMsg); } class AvatarCompDlgProc : public ParamMap2UserDlgProc { public: AvatarCompDlgProc() {} ~AvatarCompDlgProc() {} BOOL DlgProc(TimeValue t, IParamMap2 *map, HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { int id = LOWORD(wParam); int code = HIWORD(wParam); IParamBlock2 *pb = map->GetParamBlock(); HWND cbox = NULL; char* buffer = NULL; int selection; switch (msg) { case WM_INITDIALOG: int j; for (j = 0; j < plClothingMgr::kMaxGroup; j++) { cbox = GetDlgItem(hWnd, IDC_COMP_AVATAR_CLOTHING_GROUP); SendMessage(cbox, CB_ADDSTRING, 0, (LPARAM)plClothingMgr::GroupStrings[j]); } selection = pb->GetInt(ParamID(plAvatarComponent::kClothingGroup)); SendMessage(cbox, CB_SETCURSEL, selection, 0); for (j = 0; j < plArmatureMod::kMaxBoneBase; j++) { cbox = GetDlgItem(hWnd, IDC_COMP_AVATAR_SKELETON); SendMessage(cbox, CB_ADDSTRING, 0, (LPARAM)plArmatureMod::BoneStrings[j]); } selection = pb->GetInt(ParamID(plAvatarComponent::kSkeleton)); SendMessage(cbox, CB_SETCURSEL, selection, 0); return TRUE; case WM_COMMAND: if (id == IDC_COMP_AVATAR_CLOTHING_GROUP) { selection = SendMessage(GetDlgItem(hWnd, id), CB_GETCURSEL, 0, 0); pb->SetValue(plAvatarComponent::kClothingGroup, t, selection); return TRUE; } if (id == IDC_COMP_AVATAR_SKELETON) { selection = SendMessage(GetDlgItem(hWnd, id), CB_GETCURSEL, 0, 0); pb->SetValue(plAvatarComponent::kSkeleton, t, selection); return TRUE; } break; } return FALSE; } void DeleteThis() {} }; static AvatarCompDlgProc gAvatarCompDlgProc; /////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////// class plCompoundCtrlComponent : plComponent { public: plCompoundCtrlComponent(); hsBool SetupProperties(plMaxNode *node, plErrorMsg *pErrMsg); hsBool Convert(plMaxNode* node,plErrorMsg *pErrMsg); }; CLASS_DESC(plCompoundCtrlComponent, gCompoundCtrlCompDesc, "Compound Controller", "CompoundCtrl", COMP_TYPE_AVATAR, Class_ID(0x3f2a790f, 0x30354673)) plCompoundCtrlComponent::plCompoundCtrlComponent() { fClassDesc = &gCompoundCtrlCompDesc; fClassDesc->MakeAutoParamBlocks(this); } hsBool plCompoundCtrlComponent::SetupProperties(plMaxNode *node, plErrorMsg *pErrMsg) { node->SetMovable(true); node->SetForceLocal(true); node->SetItinerant(true); return true; } hsBool plCompoundCtrlComponent::Convert(plMaxNode* node, plErrorMsg *pErrMsg) { const char *name = node->GetKey()->GetName(); node->MakeCharacterHierarchy(pErrMsg); node->SetupBonesAliasesRecur(name); // create and register the player modifier plAGMasterMod *agMaster = TRACKED_NEW plAGMasterMod(); node->AddModifier(agMaster, IGetUniqueName(node)); return true; } ///////////////////////////////////////////////////////////////////////////// // // LOD Avatar Stuff below class plLODAvatarComponentProc : public plVSBaseComponentProc { protected: IParamBlock2 *fPB; plLODAvatarComponent* fComp; HWND fMstrDlg; public: plLODAvatarComponentProc() : fComp(nil), fPB(nil) {} void UpdateBoneDisplay(IParamMap2 *pm) { HWND hWnd = pm->GetHWnd(); HWND hList = GetDlgItem(hWnd, IDC_COMP_LOD_AVATAR_BONELIST); IParamBlock2 *pb = pm->GetParamBlock(); ListBox_ResetContent(hList); int group = fComp->GetCurGroupIdx(); int startIdx = fComp->GetStartIndex(group); int endIdx = fComp->GetEndIndex(group); while (startIdx < endIdx) { INode *curNode = pb->GetINode(ParamID(plLODAvatarComponent::kBoneList), 0, startIdx); if (curNode == nil) { fComp->RemoveBone(startIdx); endIdx--; continue; } ListBox_AddString(hList, curNode->GetName()); startIdx++; } } virtual void Update(TimeValue t, Interval& valid, IParamMap2* pmap) { UpdateBoneDisplay(pmap); } BOOL DlgProc(TimeValue t, IParamMap2 *map, HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { int selection; HWND cbox = NULL; HWND hList = GetDlgItem(hWnd, IDC_COMP_LOD_AVATAR_BONELIST); switch (msg) { case WM_INITDIALOG: { int LodBeginState = map->GetParamBlock()->GetInt(plLODAvatarComponent::kLODState); HWND LODCombo = GetDlgItem(hWnd, IDC_COMP_LOD_AVATAR_STATE); fMstrDlg = hWnd; fPB = map->GetParamBlock(); fComp = (plLODAvatarComponent*) fPB->GetOwner(); VCharArray Nilptr; ILoadComboBox(LODCombo, fComp->fLODLevels); SendMessage(LODCombo, CB_SETCURSEL, LodBeginState, 0); // select the right one int i; for (i = 0; i < plClothingMgr::kMaxGroup; i++) { cbox = GetDlgItem(hWnd, IDC_COMP_AVATAR_CLOTHING_GROUP); SendMessage(cbox, CB_ADDSTRING, 0, (LPARAM)plClothingMgr::GroupStrings[i]); } selection = fPB->GetInt(ParamID(plLODAvatarComponent::kClothingGroup)); SendMessage(cbox, CB_SETCURSEL, selection, 0); for (i = 0; i < plArmatureMod::kMaxBoneBase; i++) { cbox = GetDlgItem(hWnd, IDC_COMP_AVATAR_SKELETON); SendMessage(cbox, CB_ADDSTRING, 0, (LPARAM)plArmatureMod::BoneStrings[i]); } selection = fPB->GetInt(ParamID(plLODAvatarComponent::kSkeleton)); SendMessage(cbox, CB_SETCURSEL, selection, 0); Mtl *mat = fPB->GetMtl(plLODAvatarComponent::kMaterial); Button_SetText(GetDlgItem(hWnd, IDC_COMP_LOD_AVATAR_MTL), (mat ? mat->GetName() : "(none)")); UpdateBoneDisplay(map); return true; } case WM_COMMAND: if (LOWORD(wParam) == IDC_COMP_AVATAR_CLOTHING_GROUP) { selection = SendMessage(GetDlgItem(hWnd, IDC_COMP_AVATAR_CLOTHING_GROUP), CB_GETCURSEL, 0, 0); fPB->SetValue(ParamID(plLODAvatarComponent::kClothingGroup), t, selection); return TRUE; } else if (LOWORD(wParam) == IDC_COMP_AVATAR_SKELETON) { selection = SendMessage(GetDlgItem(hWnd, IDC_COMP_AVATAR_SKELETON), CB_GETCURSEL, 0, 0); fPB->SetValue(ParamID(plLODAvatarComponent::kSkeleton), t, selection); return TRUE; } else if (HIWORD(wParam) == BN_CLICKED) { if (LOWORD(wParam) == IDC_COMP_LOD_AVATAR_BONE_ADD) { std::vector cids; cids.push_back(Class_ID(TRIOBJ_CLASS_ID, 0)); cids.push_back(Class_ID(EDITTRIOBJ_CLASS_ID, 0)); if (plPick::NodeRefKludge(fPB, plLODAvatarComponent::kLastPick, &cids, true, false)) fComp->AddSelectedBone(); return TRUE; } // Remove the currently selected material else if (LOWORD(wParam) == IDC_COMP_LOD_AVATAR_BONE_REMOVE) { int curSel = SendMessage(hList, LB_GETCURSEL, 0, 0); if (curSel >= 0) fComp->RemoveBone(curSel); return TRUE; } else if (LOWORD(wParam) == IDC_COMP_LOD_AVATAR_MTL) { Mtl *pickedMtl = plPickMaterialMap::PickMaterial(plMtlCollector::kPlasmaOnly); fPB->SetValue(plLODAvatarComponent::kMaterial, 0, pickedMtl); Button_SetText(GetDlgItem(hWnd, IDC_COMP_LOD_AVATAR_MTL), (pickedMtl ? pickedMtl->GetName() : "(none)")); return TRUE; } } else { int LodBeginState = map->GetParamBlock()->GetInt(plLODAvatarComponent::kLODState); if(fPB->GetINode(plLODAvatarComponent::kMeshNodeAddBtn,t)) fPB->SetValue(plLODAvatarComponent::kMeshNodeTab, t, fPB->GetINode(plLODAvatarComponent::kMeshNodeAddBtn,t), LodBeginState); if(LOWORD(wParam) == IDC_COMP_LOD_AVATAR_STATE && HIWORD(wParam) == CBN_SELCHANGE) { int idx = SendMessage((HWND)lParam, CB_GETCURSEL, 0, 0); fPB->SetValue(plLODAvatarComponent::kLODState, 0, idx); if(fPB->GetINode(plLODAvatarComponent::kMeshNodeTab, t, idx)) fPB->SetValue(plLODAvatarComponent::kMeshNodeAddBtn, t, fPB->GetINode(plLODAvatarComponent::kMeshNodeTab,t, idx)); else fPB->Reset(plLODAvatarComponent::kMeshNodeAddBtn); return true; } } break; case WM_CLOSE: { int LodBeginState = map->GetParamBlock()->GetInt(plLODAvatarComponent::kLODState); if(fPB->GetINode(plLODAvatarComponent::kMeshNodeAddBtn,t)) fPB->SetValue(plLODAvatarComponent::kMeshNodeTab, t, fPB->GetINode(plLODAvatarComponent::kMeshNodeAddBtn,t), LodBeginState); return false; } } return false; } void DeleteThis() {} }; //! A static variable. /*! A static instance used in the ParamBlock2 processing of the physical Dialog UIs. \sa plPhysCoreComponentProc() */ static plLODAvatarComponentProc gLODAvComponentProc; class plLODAvAccessor : public PBAccessor { public: //! Public Accessor Class, used in ParamBlock2 processing. /*! Workhorse for this Accessor Class (derived from Max's PBAccessor). When one of our parameters that is a ref changes, send out the component ref changed message. Normally, messages from component refs are ignored since they pass along all the messages of the ref, which generates a lot of false converts. */ void Set(PB2Value& v, ReferenceMaker* owner, ParamID id, int tabIndex, TimeValue t) { if (id == plLODAvatarComponent::kMeshNodeAddBtn) { plLODAvatarComponent *comp = (plLODAvatarComponent *)owner; IParamBlock2 *pb = comp->GetParamBlockByID(plLODAvatarComponent::kBlkComp); int LodBeginState = pb->GetInt(plLODAvatarComponent::kLODState); INode *node = pb->GetINode(plLODAvatarComponent::kMeshNodeAddBtn, t); if (node) pb->SetValue(plLODAvatarComponent::kMeshNodeTab, t, node, LodBeginState); } } }; plLODAvAccessor gLODAvatarAccessor; ///////////////////////////////////////////////////////////////////////////////////////// /////// // PLLODAVATARCOMPONENT CLASS_DESC(plLODAvatarComponent, gLODAvatarCompDesc, "LODAvatar", "LODAvatar", COMP_TYPE_AVATAR, LOD_AVATAR_CLASS_ID) //Max paramblock2 stuff below. ParamBlockDesc2 gPLODAvatarBk ( plComponent::kBlkComp, _T("Avatar"), 0, &gLODAvatarCompDesc, P_AUTO_CONSTRUCT + P_AUTO_UI + P_MULTIMAP, plComponent::kRefComp, //Roll out 1, kArmMain, IDD_COMP_LOD_AVATAR, IDS_COMP_LOD_AVATARS, 0, 0, &gLODAvComponentProc, // kArmBounce, IDD_COMP_PHYS_CORE_GROUP, IDS_COMP_PHYS_BOUNCE, 0, APPENDROLL_CLOSED, &gBounceGroupProc, // kArmReport, IDD_COMP_PHYS_CORE_GROUP, IDS_COMP_PHYS_REPORT, 0, APPENDROLL_CLOSED, &gReportGroupProc, // params plLODAvatarComponent::kPhysicsProxyFeet_DEAD, _T("ProxyFeet"), TYPE_INODE, 0, 0, // p_ui, kArmMain, TYPE_PICKNODEBUTTON, IDC_COMP_AVATAR_PROXY_PICKB, // p_sclassID, GEOMOBJECT_CLASS_ID, // p_prompt, IDS_COMP_AVATAR_PROXYS, end, plLODAvatarComponent::kFriction, _T("Friction"), TYPE_FLOAT, 0, 0, p_ui, kArmMain, TYPE_SPINNER, EDITTYPE_POS_FLOAT, IDC_COMP_AVATAR_FRICTION_EDIT, IDC_COMP_AVATAR_FRICTION_SPIN, 0.1f, p_default, 0.9f, end, plLODAvatarComponent::kLODState, _T("LODState"), TYPE_INT, 0, 0, p_range, 1, plLODAvatarComponent::kMaxNumLODLevels, end, plLODAvatarComponent::kMeshNodeTab, _T("MeshObject"), TYPE_INODE_TAB, plLODAvatarComponent::kMaxNumLODLevels, 0, 0, p_accessor, &gLODAvatarAccessor, end, plLODAvatarComponent::kRootNodeAddBtn, _T("RtNodePicker"), TYPE_INODE, 0, 0, p_ui, kArmMain, TYPE_PICKNODEBUTTON, IDC_COMP_LOD_AVATAR_ROOT_PICKB, p_prompt, IDS_COMP_AVATAR_PROXYS, end, plLODAvatarComponent::kMeshNodeAddBtn, _T("MshNodePicker"), TYPE_INODE, 0, 0, p_ui, kArmMain, TYPE_PICKNODEBUTTON, IDC_COMP_LOD_AVATAR_MESH_PICKB, //p_sclassID, GEOMOBJECT_CLASS_ID, p_prompt, IDS_COMP_AVATAR_PROXYS, end, plLODAvatarComponent::kClothingGroup, _T("ClothingGroup"), TYPE_INT, 0, 0, p_default, plClothingMgr::kClothingBaseMale, end, plArmatureComponent::kBounceGroups, _T("bounceGroups"), TYPE_INT, 0,0, p_default, plPhysicsGroups_DEAD::kCreatures | plPhysicsGroups_DEAD::kStaticSimulated | plPhysicsGroups_DEAD::kDynamicSimulated | plPhysicsGroups_DEAD::kAnimated, end, plArmatureComponent::kReportGroups, _T("reportGroups"), TYPE_INT, 0,0, end, plLODAvatarComponent::kBrainType, _T("Brain"), TYPE_INT, 0, 0, p_default, plLODAvatarComponent::kBrainHuman, end, plLODAvatarComponent::kGroupIdx, _T("GroupIndex"), TYPE_INT, 0, 0, p_default, 0, p_range, 0, plLODAvatarComponent::kMaxNumLODLevels - 1, p_ui, kArmMain, TYPE_SPINNER, EDITTYPE_INT, IDC_COMP_LOD_AVATAR_GROUP, IDC_COMP_LOD_AVATAR_GROUP_SPIN, 1.f, end, plLODAvatarComponent::kBoneList, _T("Bones"), TYPE_INODE_TAB, 0, 0, 0, end, plLODAvatarComponent::kGroupTotals, _T("Totals"), TYPE_INT_TAB, plLODAvatarComponent::kMaxNumLODLevels, 0, 0, p_default, 0, end, plLODAvatarComponent::kLastPick, _T("LastPick"), TYPE_INODE, 0, 0, // Temp storage space for the bone picker end, plLODAvatarComponent::kSkeleton, _T("Skeleton"), TYPE_INT, 0, 0, p_default, plArmatureMod::kBoneBaseMale, end, plLODAvatarComponent::kMaterial, _T("Material"), TYPE_MTL, 0, 0, end, plLODAvatarComponent::kPhysicsProxyTorso_DEAD, _T("ProxyTorso"), TYPE_INODE, 0, 0, // p_ui, kArmMain, TYPE_PICKNODEBUTTON, IDC_COMP_AVATAR_PROXY_PICKT, // p_sclassID, GEOMOBJECT_CLASS_ID, // p_prompt, IDS_COMP_AVATAR_PROXYS, end, // plLODAvatarComponent::kPhysicsProxyHead_DEAD, _T("ProxyHead"), TYPE_INODE, 0, 0, // p_ui, kArmMain, TYPE_PICKNODEBUTTON, IDC_COMP_AVATAR_PROXY_PICKH, // p_sclassID, GEOMOBJECT_CLASS_ID, // p_prompt, IDS_COMP_AVATAR_PROXYS, end, plLODAvatarComponent::kPhysicsHeight, _T("physHeight"), TYPE_FLOAT, 0, 0, p_range, 0.1f, 50.0f, p_default, 5.f, p_ui, kArmMain, TYPE_SPINNER, EDITTYPE_FLOAT, IDC_PHYS_HEIGHT_EDIT, IDC_PHYS_HEIGHT_SPIN, SPIN_AUTOSCALE, end, plLODAvatarComponent::kPhysicsWidth, _T("physWidth"), TYPE_FLOAT, 0, 0, p_range, 0.1f, 50.0f, p_default, 2.5f, p_ui, kArmMain, TYPE_SPINNER, EDITTYPE_FLOAT, IDC_PHYS_WIDTH_EDIT, IDC_PHYS_WIDTH_SPIN, SPIN_AUTOSCALE, end, plLODAvatarComponent::kBodyFootstepSoundPage, _T("bodyFootstepPage"), TYPE_STRING, 0, 0, p_default, "Audio", p_ui, kArmMain, TYPE_EDITBOX, IDC_BODYFOOTSTEPPAGE_EDIT, end, plLODAvatarComponent::kAnimationPrefix,_T("animationPrefix"), TYPE_STRING, 0, 0, p_default, "Male", p_ui, kArmMain, TYPE_EDITBOX, IDC_ANIMATIONPREFIX_EDIT, end, end ); plLODAvatarComponent::plLODAvatarComponent() : fMaterial(nil) { fClassDesc = &gLODAvatarCompDesc; fClassDesc->MakeAutoParamBlocks(this); fLODLevels.push_back("High"); fLODLevels.push_back("Medium"); fLODLevels.push_back("Low"); } void plLODAvatarComponent::IAttachModifiers( plMaxNode *node, plErrorMsg *pErrMsg) { const char *avatarName = node->GetKey()->GetName(); plMaxNode *animRoot = (plMaxNode *)fCompPB->GetINode(plLODAvatarComponent::kRootNodeAddBtn); plKey animRootKey = animRoot->GetSceneObject()->GetKey(); plArmatureLODMod* avMod = TRACKED_NEW plArmatureLODMod(avatarName); int skeletonType = fCompPB->GetInt(ParamID(kSkeleton)); avMod->SetBodyType( skeletonType ); if (skeletonType == plArmatureLODMod::kBoneBaseCritter) avMod->PushBrain(TRACKED_NEW plAvBrainCritter()); else avMod->PushBrain(TRACKED_NEW plAvBrainHuman(skeletonType == plArmatureMod::kBoneBaseActor)); avMod->SetBodyAgeName(node->GetAgeName()); avMod->SetBodyFootstepSoundPage(fCompPB->GetStr(ParamID(kBodyFootstepSoundPage))); avMod->SetAnimationPrefix(fCompPB->GetStr(ParamID(kAnimationPrefix))); int iLODCount = fCompPB->Count(plLODAvatarComponent::kMeshNodeTab); for (int i = 0; i < iLODCount; i++) { plMaxNode *meshNode = (plMaxNode *)fCompPB->GetINode(plLODAvatarComponent::kMeshNodeTab, 0, i); plKey meshKey = meshNode->GetSceneObject()->GetKey(); avMod->AppendMeshKey(meshKey); } node->AddModifier(avMod, IGetUniqueName(node)); fArmMod = avMod; IAttachShadowCastToLODs(node); } hsBool plLODAvatarComponent::SetupProperties(plMaxNode* node, plErrorMsg* pErrMsg) { node->SetItinerant(true); // if(!IVerifyUsedNode(fCompPB->GetINode(plLODAvatarComponent::kPhysicsProxyFeet), pErrMsg, true)) // return false; // if(!IVerifyUsedNode(fCompPB->GetINode(plLODAvatarComponent::kPhysicsProxyTorso), pErrMsg, true)) // return false; // if(!IVerifyUsedNode(fCompPB->GetINode(plLODAvatarComponent::kPhysicsProxyHead), pErrMsg, true)) // return false; if(!IVerifyUsedNode(fCompPB->GetINode(plLODAvatarComponent::kRootNodeAddBtn), pErrMsg, false)) return false; for(int i = 0; i < plLODAvatarComponent::kMaxNumLODLevels; i++) { plMaxNode *meshNode = (plMaxNode *)fCompPB->GetINode(plLODAvatarComponent::kMeshNodeTab, 0, i); if (!IVerifyUsedNode(meshNode, pErrMsg, false)) { return false; } else { if (meshNode->GetObjectRef()->ClassID() == Class_ID(DUMMY_CLASS_ID, 0)) { meshNode->SetSwappableGeomTarget(plArmatureMod::kSwapTargetShadow); } } } return plArmatureComponent::SetupProperties(node, pErrMsg); } hsBool plLODAvatarComponent::PreConvert(plMaxNode* node, plErrorMsg* pErrMsg) { hsBool result = plArmatureComponent::PreConvert(node, pErrMsg); hsTArray mats; Mtl *mtl = fCompPB->GetMtl(kMaterial); if (mtl) { hsMaterialConverter::Instance().GetMaterialArray(mtl, node, mats); fMaterial = (mats.GetCount() > 0 ? mats[0] : nil); } return result; } hsBool plLODAvatarComponent::Convert(plMaxNode* node, plErrorMsg* pErrMsg) { plArmatureComponent::Convert(node, pErrMsg); // Bone LOD stuff int numSoFar = 0; int i; for (i = 0; i < kMaxNumLODLevels; i++) { int numBones = fCompPB->GetInt(ParamID(kGroupTotals), 0, i); plKeyVector *keyVec = TRACKED_NEW plKeyVector; int j; for (j = 0; j < numBones; j++) { plMaxNode *compNode = (plMaxNode*)fCompPB->GetINode(ParamID(kBoneList), 0, numSoFar + j); if (compNode) { plAGModifier *agMod = compNode->HasAGMod(); keyVec->push_back(agMod ? agMod->GetKey() : nil); } } plArmatureLODMod::ConvertNoRef(fArmMod)->AppendBoneVec(keyVec); numSoFar += numBones; } return true; } void plLODAvatarComponent::ISetupClothes(plMaxNode *node, plArmatureMod *mod, plErrorMsg* pErrMsg) { AddClothingToMod(node, mod, fCompPB->GetInt(kClothingGroup), fMaterial, pErrMsg); } void plLODAvatarComponent::IAttachShadowCastModifiersRecur(plMaxNode* node, plShadowCaster* caster) { if( !node || !caster ) return; if( !node->GetSwappableGeom() && node->GetObjectRef()->ClassID() != Class_ID(DUMMY_CLASS_ID, 0)) plShadowCastComponent::AddShadowCastModifier(node, caster); int i; for( i = 0; i < node->NumberOfChildren(); i++ ) IAttachShadowCastModifiersRecur((plMaxNode*)node->GetChildNode(i), caster); } void plLODAvatarComponent::IAttachShadowCastToLODs(plMaxNode* rootNode) { plShadowCaster* caster = TRACKED_NEW plShadowCaster; hsgResMgr::ResMgr()->NewKey(IGetUniqueName(rootNode), caster, rootNode->GetLocation()); caster->SetSelfShadow(true); int iLODCount = fCompPB->Count(plLODAvatarComponent::kMeshNodeTab); for (int i = 0; i < iLODCount; i++) { plMaxNode *meshNode = (plMaxNode *)fCompPB->GetINode(plLODAvatarComponent::kMeshNodeTab, 0, i); if( meshNode ) { plShadowCastComponent::AddShadowCastModifier(meshNode, caster); // The LOD roots are a special case. IAttachShadowCastModifiersRecur(meshNode, caster); } } } int plLODAvatarComponent::GetCurGroupIdx() { return fCompPB->GetInt(ParamID(kGroupIdx)); } int plLODAvatarComponent::GetStartIndex(int group) { int result = 0; int i; for (i = 0; i < group; i++) result += fCompPB->GetInt(ParamID(kGroupTotals), 0, i); return result; } int plLODAvatarComponent::GetEndIndex(int group) { return GetStartIndex(group) + fCompPB->GetInt(ParamID(kGroupTotals), 0, group); } void plLODAvatarComponent::AddSelectedBone() { int group = GetCurGroupIdx(); int boneIdx = GetEndIndex(group); INode *node = fCompPB->GetINode(ParamID(kLastPick)); fCompPB->Insert(ParamID(kBoneList), boneIdx, 1, &node); fCompPB->SetValue(ParamID(kGroupTotals), 0, fCompPB->GetInt(ParamID(kGroupTotals), 0, group) + 1, group); } void plLODAvatarComponent::RemoveBone(int index) { int group = GetCurGroupIdx(); int boneIdx = GetStartIndex(group) + index; fCompPB->Delete(ParamID(kBoneList), boneIdx, 1); fCompPB->SetValue(ParamID(kGroupTotals), 0, fCompPB->GetInt(ParamID(kGroupTotals), 0, group) - 1, group); }