/** * @file llappearancemgr.cpp * @brief Manager for initiating appearance changes on the viewer * * $LicenseInfo:firstyear=2004&license=viewergpl$ * * Copyright (c) 2004-2009, Linden Research, Inc. * * Second Life Viewer Source Code * The source code in this file ("Source Code") is provided by Linden Lab * to you under the terms of the GNU General Public License, version 2.0 * ("GPL"), unless you have obtained a separate licensing agreement * ("Other License"), formally executed by you and Linden Lab. Terms of * the GPL can be found in doc/GPL-license.txt in this distribution, or * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2 * * There are special exceptions to the terms and conditions of the GPL as * it is applied to this Source Code. View the full text of the exception * in the file doc/FLOSS-exception.txt in this software distribution, or * online at * http://secondlifegrid.net/programs/open_source/licensing/flossexception * * By copying, modifying or distributing this software, you acknowledge * that you have read and understood your obligations described above, * and agree to abide by those obligations. * * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, * COMPLETENESS OR PERFORMANCE. * $/LicenseInfo$ */ #include "llviewerprecompiledheaders.h" #include "llagent.h" #include "llagentwearables.h" #include "llappearancemgr.h" #include "llcommandhandler.h" #include "llfloatercustomize.h" #include "llgesturemgr.h" #include "llinventorybridge.h" #include "llinventoryfunctions.h" #include "llinventoryobserver.h" #include "llnotificationsutil.h" #include "llsidepanelappearance.h" #include "llsidetray.h" #include "llvoavatar.h" #include "llvoavatarself.h" #include "llviewerregion.h" #include "llwearablelist.h" LLUUID findDescendentCategoryIDByName(const LLUUID& parent_id,const std::string& name) { LLInventoryModel::cat_array_t cat_array; LLInventoryModel::item_array_t item_array; LLNameCategoryCollector has_name(name); gInventory.collectDescendentsIf(parent_id, cat_array, item_array, LLInventoryModel::EXCLUDE_TRASH, has_name); if (0 == cat_array.count()) return LLUUID(); else { LLViewerInventoryCategory *cat = cat_array.get(0); if (cat) return cat->getUUID(); else { llwarns << "null cat" << llendl; return LLUUID(); } } } // support for secondlife:///app/appearance SLapps class LLAppearanceHandler : public LLCommandHandler { public: // requests will be throttled from a non-trusted browser LLAppearanceHandler() : LLCommandHandler("appearance", UNTRUSTED_THROTTLE) {} bool handle(const LLSD& params, const LLSD& query_map, LLMediaCtrl* web) { // support secondlife:///app/appearance/show, but for now we just // make all secondlife:///app/appearance SLapps behave this way LLSideTray::getInstance()->showPanel("sidepanel_appearance", LLSD()); return true; } }; LLAppearanceHandler gAppearanceHandler; class LLWearInventoryCategoryCallback : public LLInventoryCallback { public: LLWearInventoryCategoryCallback(const LLUUID& cat_id, bool append) { mCatID = cat_id; mAppend = append; } void fire(const LLUUID& item_id) { /* * Do nothing. We only care about the destructor * * The reason for this is that this callback is used in a hack where the * same callback is given to dozens of items, and the destructor is called * after the last item has fired the event and dereferenced it -- if all * the events actually fire! */ } protected: ~LLWearInventoryCategoryCallback() { llinfos << "done all inventory callbacks" << llendl; // Is the destructor called by ordinary dereference, or because the app's shutting down? // If the inventory callback manager goes away, we're shutting down, no longer want the callback. if( LLInventoryCallbackManager::is_instantiated() ) { LLAppearanceMgr::instance().wearInventoryCategoryOnAvatar(gInventory.getCategory(mCatID), mAppend); } else { llwarns << "Dropping unhandled LLWearInventoryCategoryCallback" << llendl; } } private: LLUUID mCatID; bool mAppend; }; class LLOutfitObserver : public LLInventoryFetchObserver { public: LLOutfitObserver(const LLUUID& cat_id, bool copy_items, bool append) : mCatID(cat_id), mCopyItems(copy_items), mAppend(append) {} ~LLOutfitObserver() {} virtual void done(); void doWearCategory(); protected: LLUUID mCatID; bool mCopyItems; bool mAppend; }; void LLOutfitObserver::done() { llinfos << "done 2nd stage fetch" << llendl; gInventory.removeObserver(this); doOnIdle(boost::bind(&LLOutfitObserver::doWearCategory,this)); } void LLOutfitObserver::doWearCategory() { llinfos << "starting" << llendl; // We now have an outfit ready to be copied to agent inventory. Do // it, and wear that outfit normally. if(mCopyItems) { LLInventoryCategory* cat = gInventory.getCategory(mCatID); std::string name; if(!cat) { // should never happen. name = "New Outfit"; } else { name = cat->getName(); } LLViewerInventoryItem* item = NULL; item_ref_t::iterator it = mComplete.begin(); item_ref_t::iterator end = mComplete.end(); LLUUID pid; for(; it < end; ++it) { item = (LLViewerInventoryItem*)gInventory.getItem(*it); if(item) { if(LLInventoryType::IT_GESTURE == item->getInventoryType()) { pid = gInventory.findCategoryUUIDForType(LLFolderType::FT_GESTURE); } else { pid = gInventory.findCategoryUUIDForType(LLFolderType::FT_CLOTHING); } break; } } if(pid.isNull()) { pid = gInventory.getRootFolderID(); } LLUUID cat_id = gInventory.createNewCategory( pid, LLFolderType::FT_NONE, name); mCatID = cat_id; LLPointer<LLInventoryCallback> cb = new LLWearInventoryCategoryCallback(mCatID, mAppend); it = mComplete.begin(); for(; it < end; ++it) { item = (LLViewerInventoryItem*)gInventory.getItem(*it); if(item) { copy_inventory_item( gAgent.getID(), item->getPermissions().getOwner(), item->getUUID(), cat_id, std::string(), cb); } } // BAP fixes a lag in display of created dir. gInventory.notifyObservers(); } else { // Wear the inventory category. LLAppearanceMgr::instance().wearInventoryCategoryOnAvatar(gInventory.getCategory(mCatID), mAppend); } delete this; } class LLOutfitFetch : public LLInventoryFetchDescendentsObserver { public: LLOutfitFetch(bool copy_items, bool append) : mCopyItems(copy_items), mAppend(append) {} ~LLOutfitFetch() {} virtual void done(); protected: bool mCopyItems; bool mAppend; }; void LLOutfitFetch::done() { // What we do here is get the complete information on the items in // the library, and set up an observer that will wait for that to // happen. llinfos << "done first stage fetch" << llendl; LLInventoryModel::cat_array_t cat_array; LLInventoryModel::item_array_t item_array; gInventory.collectDescendents(mCompleteFolders.front(), cat_array, item_array, LLInventoryModel::EXCLUDE_TRASH); S32 count = item_array.count(); if(!count) { llwarns << "Nothing fetched in category " << mCompleteFolders.front() << llendl; //dec_busy_count(); gInventory.removeObserver(this); delete this; return; } LLOutfitObserver* outfit_observer = new LLOutfitObserver(mCompleteFolders.front(), mCopyItems, mAppend); LLInventoryFetchObserver::item_ref_t ids; for(S32 i = 0; i < count; ++i) { ids.push_back(item_array.get(i)->getUUID()); } // clean up, and remove this as an observer since the call to the // outfit could notify observers and throw us into an infinite // loop. //dec_busy_count(); gInventory.removeObserver(this); // increment busy count and either tell the inventory to check & // call done, or add this object to the inventory for observation. //inc_busy_count(); // do the fetch outfit_observer->fetchItems(ids); if(outfit_observer->isEverythingComplete()) { // everything is already here - call done. outfit_observer->done(); } else { // it's all on it's way - add an observer, and the inventory // will call done for us when everything is here. gInventory.addObserver(outfit_observer); } delete this; } LLUpdateAppearanceOnDestroy::LLUpdateAppearanceOnDestroy(): mFireCount(0) { } LLUpdateAppearanceOnDestroy::~LLUpdateAppearanceOnDestroy() { llinfos << "done update appearance on destroy" << llendl; if (!LLApp::isExiting()) { LLAppearanceMgr::instance().updateAppearanceFromCOF(); } } void LLUpdateAppearanceOnDestroy::fire(const LLUUID& inv_item) { llinfos << "callback fired" << llendl; mFireCount++; } struct LLFoundData { LLFoundData() : mAssetType(LLAssetType::AT_NONE), mWearableType(WT_INVALID), mWearable(NULL) {} LLFoundData(const LLUUID& item_id, const LLUUID& asset_id, const std::string& name, const LLAssetType::EType& asset_type, const EWearableType& wearable_type ) : mItemID(item_id), mAssetID(asset_id), mName(name), mAssetType(asset_type), mWearableType(wearable_type), mWearable( NULL ) {} LLUUID mItemID; LLUUID mAssetID; std::string mName; LLAssetType::EType mAssetType; EWearableType mWearableType; LLWearable* mWearable; }; class LLWearableHoldingPattern { public: LLWearableHoldingPattern(); ~LLWearableHoldingPattern(); bool pollFetchCompletion(); void onFetchCompletion(); bool isFetchCompleted(); bool isTimedOut(); void checkMissingWearables(); bool pollMissingWearables(); bool isMissingCompleted(); void recoverMissingWearable(EWearableType type); void clearCOFLinksForMissingWearables(); void onWearableAssetFetch(LLWearable *wearable); void onAllComplete(); typedef std::list<LLFoundData> found_list_t; found_list_t mFoundList; LLInventoryModel::item_array_t mObjItems; LLInventoryModel::item_array_t mGestItems; typedef std::set<S32> type_set_t; type_set_t mTypesToRecover; type_set_t mTypesToLink; S32 mResolved; LLTimer mWaitTime; bool mFired; }; LLWearableHoldingPattern::LLWearableHoldingPattern(): mResolved(0), mFired(false) { } LLWearableHoldingPattern::~LLWearableHoldingPattern() { } bool LLWearableHoldingPattern::isFetchCompleted() { return (mResolved >= (S32)mFoundList.size()); // have everything we were waiting for? } bool LLWearableHoldingPattern::isTimedOut() { static F32 max_wait_time = 60.0; // give up if wearable fetches haven't completed in max_wait_time seconds. return mWaitTime.getElapsedTimeF32() > max_wait_time; } void LLWearableHoldingPattern::checkMissingWearables() { std::vector<S32> found_by_type(WT_COUNT,0); std::vector<S32> requested_by_type(WT_COUNT,0); for (found_list_t::iterator it = mFoundList.begin(); it != mFoundList.end(); ++it) { LLFoundData &data = *it; if (data.mWearableType < WT_COUNT) requested_by_type[data.mWearableType]++; if (data.mWearable) found_by_type[data.mWearableType]++; } for (S32 type = 0; type < WT_COUNT; ++type) { llinfos << "type " << type << " requested " << requested_by_type[type] << " found " << found_by_type[type] << llendl; if (found_by_type[type] > 0) continue; if ( // Need to recover if at least one wearable of that type // was requested but none was found (prevent missing // pants) (requested_by_type[type] > 0) || // or if type is a body part and no wearables were found. ((type == WT_SHAPE) || (type == WT_SKIN) || (type == WT_HAIR) || (type == WT_EYES))) { mTypesToRecover.insert(type); mTypesToLink.insert(type); recoverMissingWearable((EWearableType)type); llwarns << "need to replace " << type << llendl; } } mWaitTime.reset(); if (!pollMissingWearables()) { doOnIdleRepeating(boost::bind(&LLWearableHoldingPattern::pollMissingWearables,this)); } } void LLWearableHoldingPattern::onAllComplete() { // Activate all gestures in this folder if (mGestItems.count() > 0) { llinfos << "Activating " << mGestItems.count() << " gestures" << llendl; LLGestureMgr::instance().activateGestures(mGestItems); // Update the inventory item labels to reflect the fact // they are active. LLViewerInventoryCategory* catp = gInventory.getCategory(LLAppearanceMgr::instance().getCOF()); if (catp) { gInventory.updateCategory(catp); gInventory.notifyObservers(); } } // Update wearables. llinfos << "Updating agent wearables with " << mResolved << " wearable items " << llendl; LLAppearanceMgr::instance().updateAgentWearables(this, false); // Update attachments to match those requested. if (isAgentAvatarValid()) { llinfos << "Updating " << mObjItems.count() << " attachments" << llendl; LLAgentWearables::userUpdateAttachments(mObjItems); } if (isFetchCompleted() && isMissingCompleted()) { // Only safe to delete if all wearable callbacks and all missing wearables completed. delete this; } } void LLWearableHoldingPattern::onFetchCompletion() { checkMissingWearables(); } // Runs as an idle callback until all wearables are fetched (or we time out). bool LLWearableHoldingPattern::pollFetchCompletion() { bool completed = isFetchCompleted(); bool timed_out = isTimedOut(); bool done = completed || timed_out; if (done) { llinfos << "polling, done status: " << completed << " timed out " << timed_out << " elapsed " << mWaitTime.getElapsedTimeF32() << llendl; mFired = true; if (timed_out) { llwarns << "Exceeded max wait time for wearables, updating appearance based on what has arrived" << llendl; } onFetchCompletion(); } return done; } class RecoveredItemLinkCB: public LLInventoryCallback { public: RecoveredItemLinkCB(EWearableType type, LLWearable *wearable, LLWearableHoldingPattern* holder): mHolder(holder), mWearable(wearable), mType(type) { } void fire(const LLUUID& item_id) { llinfos << "Recovered item link for type " << mType << llendl; mHolder->mTypesToLink.erase(mType); // Add wearable to FoundData for actual wearing LLViewerInventoryItem *item = gInventory.getItem(item_id); LLViewerInventoryItem *linked_item = item ? item->getLinkedItem() : NULL; if (linked_item) { gInventory.addChangedMask(LLInventoryObserver::LABEL, linked_item->getUUID()); if (item) { LLFoundData found(linked_item->getUUID(), linked_item->getAssetUUID(), linked_item->getName(), linked_item->getType(), linked_item->isWearableType() ? linked_item->getWearableType() : WT_INVALID ); found.mWearable = mWearable; mHolder->mFoundList.push_front(found); } else { llwarns << "inventory item not found for recovered wearable" << llendl; } } else { llwarns << "inventory link not found for recovered wearable" << llendl; } } private: LLWearableHoldingPattern* mHolder; LLWearable *mWearable; EWearableType mType; }; class RecoveredItemCB: public LLInventoryCallback { public: RecoveredItemCB(EWearableType type, LLWearable *wearable, LLWearableHoldingPattern* holder): mHolder(holder), mWearable(wearable), mType(type) { } void fire(const LLUUID& item_id) { llinfos << "Recovered item for type " << mType << llendl; LLViewerInventoryItem *itemp = gInventory.getItem(item_id); mWearable->setItemID(item_id); LLPointer<LLInventoryCallback> cb = new RecoveredItemLinkCB(mType,mWearable,mHolder); mHolder->mTypesToRecover.erase(mType); llassert(itemp); if (itemp) { link_inventory_item( gAgent.getID(), item_id, LLAppearanceMgr::instance().getCOF(), itemp->getName(), LLAssetType::AT_LINK, cb); } } private: LLWearableHoldingPattern* mHolder; LLWearable *mWearable; EWearableType mType; }; void LLWearableHoldingPattern::recoverMissingWearable(EWearableType type) { // Try to recover by replacing missing wearable with a new one. LLNotificationsUtil::add("ReplacedMissingWearable"); lldebugs << "Wearable " << LLWearableDictionary::getTypeLabel(type) << " could not be downloaded. Replaced inventory item with default wearable." << llendl; LLWearable* wearable = LLWearableList::instance().createNewWearable(type); // Add a new one in the lost and found folder. const LLUUID lost_and_found_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_LOST_AND_FOUND); LLPointer<LLInventoryCallback> cb = new RecoveredItemCB(type,wearable,this); create_inventory_item(gAgent.getID(), gAgent.getSessionID(), lost_and_found_id, wearable->getTransactionID(), wearable->getName(), wearable->getDescription(), wearable->getAssetType(), LLInventoryType::IT_WEARABLE, wearable->getType(), wearable->getPermissions().getMaskNextOwner(), cb); } bool LLWearableHoldingPattern::isMissingCompleted() { return mTypesToLink.size()==0 && mTypesToRecover.size()==0; } void LLWearableHoldingPattern::clearCOFLinksForMissingWearables() { for (found_list_t::iterator it = mFoundList.begin(); it != mFoundList.end(); ++it) { LLFoundData &data = *it; if ((data.mWearableType < WT_COUNT) && (!data.mWearable)) { // Wearable link that was never resolved; remove links to it from COF llinfos << "removing link for unresolved item " << data.mItemID.asString() << llendl; LLAppearanceMgr::instance().removeCOFItemLinks(data.mItemID,false); } } } bool LLWearableHoldingPattern::pollMissingWearables() { bool timed_out = isTimedOut(); bool missing_completed = isMissingCompleted(); bool done = timed_out || missing_completed; llinfos << "polling missing wearables, waiting for items " << mTypesToRecover.size() << " links " << mTypesToLink.size() << " wearables, timed out " << timed_out << " elapsed " << mWaitTime.getElapsedTimeF32() << " done " << done << llendl; if (done) { clearCOFLinksForMissingWearables(); onAllComplete(); } return done; } void LLWearableHoldingPattern::onWearableAssetFetch(LLWearable *wearable) { mResolved += 1; // just counting callbacks, not successes. llinfos << "onWearableAssetFetch, resolved count " << mResolved << " of requested " << mFoundList.size() << llendl; if (wearable) { llinfos << "wearable found, type " << wearable->getType() << " asset " << wearable->getAssetID() << llendl; } else { llwarns << "no wearable found" << llendl; } if (mFired) { llwarns << "called after holder fired" << llendl; return; } if (!wearable) { return; } for (LLWearableHoldingPattern::found_list_t::iterator iter = mFoundList.begin(); iter != mFoundList.end(); ++iter) { LLFoundData& data = *iter; if(wearable->getAssetID() == data.mAssetID) { data.mWearable = wearable; // Failing this means inventory or asset server are corrupted in a way we don't handle. llassert((data.mWearableType < WT_COUNT) && (wearable->getType() == data.mWearableType)); break; } } } static void onWearableAssetFetch(LLWearable* wearable, void* data) { LLWearableHoldingPattern* holder = (LLWearableHoldingPattern*)data; holder->onWearableAssetFetch(wearable); } static void removeDuplicateItems(LLInventoryModel::item_array_t& items) { LLInventoryModel::item_array_t new_items; std::set<LLUUID> items_seen; std::deque<LLViewerInventoryItem*> tmp_list; // Traverse from the front and keep the first of each item // encountered, so we actually keep the *last* of each duplicate // item. This is needed to give the right priority when adding // duplicate items to an existing outfit. for (S32 i=items.count()-1; i>=0; i--) { LLViewerInventoryItem *item = items.get(i); LLUUID item_id = item->getLinkedUUID(); if (items_seen.find(item_id)!=items_seen.end()) continue; items_seen.insert(item_id); tmp_list.push_front(item); } for (std::deque<LLViewerInventoryItem*>::iterator it = tmp_list.begin(); it != tmp_list.end(); ++it) { new_items.put(*it); } items = new_items; } const LLUUID LLAppearanceMgr::getCOF() const { return gInventory.findCategoryUUIDForType(LLFolderType::FT_CURRENT_OUTFIT); } const LLViewerInventoryItem* LLAppearanceMgr::getBaseOutfitLink() { const LLUUID& current_outfit_cat = getCOF(); LLInventoryModel::cat_array_t cat_array; LLInventoryModel::item_array_t item_array; // Can't search on FT_OUTFIT since links to categories return FT_CATEGORY for type since they don't // return preferred type. LLIsType is_category( LLAssetType::AT_CATEGORY ); gInventory.collectDescendentsIf(current_outfit_cat, cat_array, item_array, false, is_category, false); for (LLInventoryModel::item_array_t::const_iterator iter = item_array.begin(); iter != item_array.end(); iter++) { const LLViewerInventoryItem *item = (*iter); const LLViewerInventoryCategory *cat = item->getLinkedCategory(); if (cat && cat->getPreferredType() == LLFolderType::FT_OUTFIT) { return item; } } return NULL; } bool LLAppearanceMgr::getBaseOutfitName(std::string& name) { const LLViewerInventoryItem* outfit_link = getBaseOutfitLink(); if(outfit_link) { const LLViewerInventoryCategory *cat = outfit_link->getLinkedCategory(); if (cat) { name = cat->getName(); return true; } } return false; } // Update appearance from outfit folder. void LLAppearanceMgr::changeOutfit(bool proceed, const LLUUID& category, bool append) { if (!proceed) return; LLAppearanceMgr::instance().updateCOF(category,append); } // Create a copy of src_id + contents as a subfolder of dst_id. void LLAppearanceMgr::shallowCopyCategory(const LLUUID& src_id, const LLUUID& dst_id, LLPointer<LLInventoryCallback> cb) { LLInventoryCategory *src_cat = gInventory.getCategory(src_id); if (!src_cat) { llwarns << "folder not found for src " << src_id.asString() << llendl; return; } LLUUID parent_id = dst_id; if(parent_id.isNull()) { parent_id = gInventory.getRootFolderID(); } LLUUID subfolder_id = gInventory.createNewCategory( parent_id, LLFolderType::FT_NONE, src_cat->getName()); shallowCopyCategoryContents(src_id, subfolder_id, cb); gInventory.notifyObservers(); } // Copy contents of src_id to dst_id. void LLAppearanceMgr::shallowCopyCategoryContents(const LLUUID& src_id, const LLUUID& dst_id, LLPointer<LLInventoryCallback> cb) { LLInventoryModel::cat_array_t* cats; LLInventoryModel::item_array_t* items; gInventory.getDirectDescendentsOf(src_id, cats, items); for (LLInventoryModel::item_array_t::const_iterator iter = items->begin(); iter != items->end(); ++iter) { const LLViewerInventoryItem* item = (*iter); switch (item->getActualType()) { case LLAssetType::AT_LINK: { link_inventory_item(gAgent.getID(), item->getLinkedUUID(), dst_id, item->getName(), LLAssetType::AT_LINK, cb); break; } case LLAssetType::AT_LINK_FOLDER: { LLViewerInventoryCategory *catp = item->getLinkedCategory(); // Skip copying outfit links. if (catp && catp->getPreferredType() != LLFolderType::FT_OUTFIT) { link_inventory_item(gAgent.getID(), item->getLinkedUUID(), dst_id, item->getName(), LLAssetType::AT_LINK_FOLDER, cb); } break; } case LLAssetType::AT_CLOTHING: case LLAssetType::AT_OBJECT: case LLAssetType::AT_BODYPART: case LLAssetType::AT_GESTURE: { copy_inventory_item(gAgent.getID(), item->getPermissions().getOwner(), item->getUUID(), dst_id, item->getName(), cb); break; } default: // Ignore non-outfit asset types break; } } } BOOL LLAppearanceMgr::getCanMakeFolderIntoOutfit(const LLUUID& folder_id) { // These are the wearable items that are required for considering this // folder as containing a complete outfit. U32 required_wearables = 0; required_wearables |= 1LL << WT_SHAPE; required_wearables |= 1LL << WT_SKIN; required_wearables |= 1LL << WT_HAIR; required_wearables |= 1LL << WT_EYES; // These are the wearables that the folder actually contains. U32 folder_wearables = 0; LLInventoryModel::cat_array_t* cats; LLInventoryModel::item_array_t* items; gInventory.getDirectDescendentsOf(folder_id, cats, items); for (LLInventoryModel::item_array_t::const_iterator iter = items->begin(); iter != items->end(); ++iter) { const LLViewerInventoryItem* item = (*iter); if (item->isWearableType()) { const EWearableType wearable_type = item->getWearableType(); folder_wearables |= 1LL << wearable_type; } } // If the folder contains the required wearables, return TRUE. return ((required_wearables & folder_wearables) == required_wearables); } void LLAppearanceMgr::purgeBaseOutfitLink(const LLUUID& category) { LLInventoryModel::cat_array_t cats; LLInventoryModel::item_array_t items; gInventory.collectDescendents(category, cats, items, LLInventoryModel::EXCLUDE_TRASH); for (S32 i = 0; i < items.count(); ++i) { LLViewerInventoryItem *item = items.get(i); if (item->getActualType() != LLAssetType::AT_LINK_FOLDER) continue; if (item->getIsLinkType()) { LLViewerInventoryCategory* catp = item->getLinkedCategory(); if(catp && catp->getPreferredType() == LLFolderType::FT_OUTFIT) { gInventory.purgeObject(item->getUUID()); } } } } void LLAppearanceMgr::purgeCategory(const LLUUID& category, bool keep_outfit_links) { LLInventoryModel::cat_array_t cats; LLInventoryModel::item_array_t items; gInventory.collectDescendents(category, cats, items, LLInventoryModel::EXCLUDE_TRASH); for (S32 i = 0; i < items.count(); ++i) { LLViewerInventoryItem *item = items.get(i); if (keep_outfit_links && (item->getActualType() == LLAssetType::AT_LINK_FOLDER)) continue; if (item->getIsLinkType()) { gInventory.purgeObject(item->getUUID()); } } } // Keep the last N wearables of each type. For viewer 2.0, N is 1 for // both body parts and clothing items. void LLAppearanceMgr::filterWearableItems( LLInventoryModel::item_array_t& items, S32 max_per_type) { // Divvy items into arrays by wearable type. std::vector<LLInventoryModel::item_array_t> items_by_type(WT_COUNT); for (S32 i=0; i<items.count(); i++) { LLViewerInventoryItem *item = items.get(i); // Ignore non-wearables. if (!item->isWearableType()) continue; EWearableType type = item->getWearableType(); if(type < 0 || type >= WT_COUNT) { LL_WARNS("Appearance") << "Invalid wearable type. Inventory type does not match wearable flag bitfield." << LL_ENDL; continue; } items_by_type[type].push_back(item); } // rebuild items list, retaining the last max_per_type of each array items.clear(); for (S32 i=0; i<WT_COUNT; i++) { S32 size = items_by_type[i].size(); if (size <= 0) continue; S32 start_index = llmax(0,size-max_per_type); for (S32 j = start_index; j<size; j++) { items.push_back(items_by_type[i][j]); } } } // Create links to all listed items. void LLAppearanceMgr::linkAll(const LLUUID& category, LLInventoryModel::item_array_t& items, LLPointer<LLInventoryCallback> cb) { for (S32 i=0; i<items.count(); i++) { const LLInventoryItem* item = items.get(i).get(); link_inventory_item(gAgent.getID(), item->getLinkedUUID(), category, item->getName(), LLAssetType::AT_LINK, cb); } } void LLAppearanceMgr::updateCOF(const LLUUID& category, bool append) { LLViewerInventoryCategory *pcat = gInventory.getCategory(category); llinfos << "starting, cat " << (pcat ? pcat->getName() : "[UNKNOWN]") << llendl; const LLUUID cof = getCOF(); // Deactivate currently active gestures in the COF, if replacing outfit if (!append) { LLInventoryModel::item_array_t gest_items; getDescendentsOfAssetType(cof, gest_items, LLAssetType::AT_GESTURE, false); for(S32 i = 0; i < gest_items.count(); ++i) { LLViewerInventoryItem *gest_item = gest_items.get(i); if ( LLGestureMgr::instance().isGestureActive( gest_item->getLinkedUUID()) ) { LLGestureMgr::instance().deactivateGesture( gest_item->getLinkedUUID() ); } } } // Collect and filter descendents to determine new COF contents. // - Body parts: always include COF contents as a fallback in case any // required parts are missing. LLInventoryModel::item_array_t body_items; getDescendentsOfAssetType(cof, body_items, LLAssetType::AT_BODYPART, false); getDescendentsOfAssetType(category, body_items, LLAssetType::AT_BODYPART, false); // Reduce body items to max of one per type. removeDuplicateItems(body_items); filterWearableItems(body_items, 1); // - Wearables: include COF contents only if appending. LLInventoryModel::item_array_t wear_items; if (append) getDescendentsOfAssetType(cof, wear_items, LLAssetType::AT_CLOTHING, false); getDescendentsOfAssetType(category, wear_items, LLAssetType::AT_CLOTHING, false); // Reduce wearables to max of one per type. removeDuplicateItems(wear_items); filterWearableItems(wear_items, 5); // - Attachments: include COF contents only if appending. LLInventoryModel::item_array_t obj_items; if (append) getDescendentsOfAssetType(cof, obj_items, LLAssetType::AT_OBJECT, false); getDescendentsOfAssetType(category, obj_items, LLAssetType::AT_OBJECT, false); removeDuplicateItems(obj_items); // - Gestures: include COF contents only if appending. LLInventoryModel::item_array_t gest_items; if (append) getDescendentsOfAssetType(cof, gest_items, LLAssetType::AT_GESTURE, false); getDescendentsOfAssetType(category, gest_items, LLAssetType::AT_GESTURE, false); removeDuplicateItems(gest_items); // Remove current COF contents. bool keep_outfit_links = append; purgeCategory(cof, keep_outfit_links); gInventory.notifyObservers(); // Create links to new COF contents. llinfos << "creating LLUpdateAppearanceOnDestroy" << llendl; LLPointer<LLInventoryCallback> link_waiter = new LLUpdateAppearanceOnDestroy; linkAll(cof, body_items, link_waiter); linkAll(cof, wear_items, link_waiter); linkAll(cof, obj_items, link_waiter); linkAll(cof, gest_items, link_waiter); // Add link to outfit if category is an outfit. if (!append) { createBaseOutfitLink(category, link_waiter); } llinfos << "waiting for LLUpdateAppearanceOnDestroy" << llendl; } void LLAppearanceMgr::updatePanelOutfitName(const std::string& name) { LLSidepanelAppearance* panel_appearance = dynamic_cast<LLSidepanelAppearance *>(LLSideTray::getInstance()->getPanel("sidepanel_appearance")); if (panel_appearance) { panel_appearance->refreshCurrentOutfitName(name); } } void LLAppearanceMgr::createBaseOutfitLink(const LLUUID& category, LLPointer<LLInventoryCallback> link_waiter) { const LLUUID cof = getCOF(); LLViewerInventoryCategory* catp = gInventory.getCategory(category); std::string new_outfit_name = ""; purgeBaseOutfitLink(cof); if (catp && catp->getPreferredType() == LLFolderType::FT_OUTFIT) { link_inventory_item(gAgent.getID(), category, cof, catp->getName(), LLAssetType::AT_LINK_FOLDER, link_waiter); new_outfit_name = catp->getName(); } updatePanelOutfitName(new_outfit_name); } void LLAppearanceMgr::updateAgentWearables(LLWearableHoldingPattern* holder, bool append) { lldebugs << "updateAgentWearables()" << llendl; LLInventoryItem::item_array_t items; LLDynamicArray< LLWearable* > wearables; // For each wearable type, find the first instance in the category // that we recursed through. for( S32 i = 0; i < WT_COUNT; i++ ) { for (LLWearableHoldingPattern::found_list_t::iterator iter = holder->mFoundList.begin(); iter != holder->mFoundList.end(); ++iter) { LLFoundData& data = *iter; LLWearable* wearable = data.mWearable; if( wearable && ((S32)wearable->getType() == i) ) { LLViewerInventoryItem* item; item = (LLViewerInventoryItem*)gInventory.getItem(data.mItemID); if( item && (item->getAssetUUID() == wearable->getAssetID()) ) { items.put(item); wearables.put(wearable); } } } } if(wearables.count() > 0) { gAgentWearables.setWearableOutfit(items, wearables, !append); } // dec_busy_count(); } static void remove_non_link_items(LLInventoryModel::item_array_t &items) { LLInventoryModel::item_array_t pruned_items; for (LLInventoryModel::item_array_t::const_iterator iter = items.begin(); iter != items.end(); ++iter) { const LLViewerInventoryItem *item = (*iter); if (item && item->getIsLinkType()) { pruned_items.push_back((*iter)); } } items = pruned_items; } void LLAppearanceMgr::updateAppearanceFromCOF() { // update dirty flag to see if the state of the COF matches // the saved outfit stored as a folder link llinfos << "starting" << llendl; updateIsDirty(); dumpCat(getCOF(),"COF, start"); bool follow_folder_links = true; LLUUID current_outfit_id = getCOF(); // Find all the wearables that are in the COF's subtree. lldebugs << "LLAppearanceMgr::updateFromCOF()" << llendl; LLInventoryModel::item_array_t wear_items; LLInventoryModel::item_array_t obj_items; LLInventoryModel::item_array_t gest_items; getUserDescendents(current_outfit_id, wear_items, obj_items, gest_items, follow_folder_links); // Get rid of non-links in case somehow the COF was corrupted. remove_non_link_items(wear_items); remove_non_link_items(obj_items); remove_non_link_items(gest_items); if(!wear_items.count()) { LLNotificationsUtil::add("CouldNotPutOnOutfit"); return; } LLWearableHoldingPattern* holder = new LLWearableHoldingPattern; holder->mObjItems = obj_items; holder->mGestItems = gest_items; // Note: can't do normal iteration, because if all the // wearables can be resolved immediately, then the // callback will be called (and this object deleted) // before the final getNextData(). for(S32 i = 0; i < wear_items.count(); ++i) { LLViewerInventoryItem *item = wear_items.get(i); LLViewerInventoryItem *linked_item = item ? item->getLinkedItem() : NULL; if (item && item->getIsLinkType() && linked_item) { LLFoundData found(linked_item->getUUID(), linked_item->getAssetUUID(), linked_item->getName(), linked_item->getType(), linked_item->isWearableType() ? linked_item->getWearableType() : WT_INVALID ); #if 0 // Fault injection: uncomment this block to test asset // fetch failures (should be replaced by new defaults in // lost&found). if (found.mWearableType == WT_SHAPE || found.mWearableType == WT_JACKET) { found.mAssetID.generate(); // Replace with new UUID, guaranteed not to exist in DB } #endif holder->mFoundList.push_front(found); } else { if (!item) { llwarns << "Attempt to wear a null item " << llendl; } else if (!linked_item) { llwarns << "Attempt to wear a broken link [ name:" << item->getName() << " ] " << llendl; } } } for (LLWearableHoldingPattern::found_list_t::iterator it = holder->mFoundList.begin(); it != holder->mFoundList.end(); ++it) { LLFoundData& found = *it; llinfos << "waiting for onWearableAssetFetch callback, asset " << found.mAssetID.asString() << llendl; // Fetch the wearables about to be worn. LLWearableList::instance().getAsset(found.mAssetID, found.mName, found.mAssetType, onWearableAssetFetch, (void*)holder); } if (!holder->pollFetchCompletion()) { doOnIdleRepeating(boost::bind(&LLWearableHoldingPattern::pollFetchCompletion,holder)); } } void LLAppearanceMgr::getDescendentsOfAssetType(const LLUUID& category, LLInventoryModel::item_array_t& items, LLAssetType::EType type, bool follow_folder_links) { LLInventoryModel::cat_array_t cats; LLIsType is_of_type(type); gInventory.collectDescendentsIf(category, cats, items, LLInventoryModel::EXCLUDE_TRASH, is_of_type, follow_folder_links); } void LLAppearanceMgr::getUserDescendents(const LLUUID& category, LLInventoryModel::item_array_t& wear_items, LLInventoryModel::item_array_t& obj_items, LLInventoryModel::item_array_t& gest_items, bool follow_folder_links) { LLInventoryModel::cat_array_t wear_cats; LLFindWearables is_wearable; gInventory.collectDescendentsIf(category, wear_cats, wear_items, LLInventoryModel::EXCLUDE_TRASH, is_wearable, follow_folder_links); LLInventoryModel::cat_array_t obj_cats; LLIsType is_object( LLAssetType::AT_OBJECT ); gInventory.collectDescendentsIf(category, obj_cats, obj_items, LLInventoryModel::EXCLUDE_TRASH, is_object, follow_folder_links); // Find all gestures in this folder LLInventoryModel::cat_array_t gest_cats; LLIsType is_gesture( LLAssetType::AT_GESTURE ); gInventory.collectDescendentsIf(category, gest_cats, gest_items, LLInventoryModel::EXCLUDE_TRASH, is_gesture, follow_folder_links); } void LLAppearanceMgr::wearInventoryCategory(LLInventoryCategory* category, bool copy, bool append) { if(!category) return; llinfos << "wearInventoryCategory( " << category->getName() << " )" << llendl; // What we do here is get the complete information on the items in // the inventory, and set up an observer that will wait for that to // happen. LLOutfitFetch* outfit_fetcher = new LLOutfitFetch(copy, append); uuid_vec_t folders; folders.push_back(category->getUUID()); outfit_fetcher->fetchDescendents(folders); //inc_busy_count(); if(outfit_fetcher->isEverythingComplete()) { // everything is already here - call done. outfit_fetcher->done(); } else { // it's all on it's way - add an observer, and the inventory // will call done for us when everything is here. gInventory.addObserver(outfit_fetcher); } } // *NOTE: hack to get from avatar inventory to avatar void LLAppearanceMgr::wearInventoryCategoryOnAvatar( LLInventoryCategory* category, bool append ) { // Avoid unintentionally overwriting old wearables. We have to do // this up front to avoid having to deal with the case of multiple // wearables being dirty. if(!category) return; llinfos << "wearInventoryCategoryOnAvatar( " << category->getName() << " )" << llendl; if( gFloaterCustomize ) { gFloaterCustomize->askToSaveIfDirty(boost::bind(&LLAppearanceMgr::changeOutfit, &LLAppearanceMgr::instance(), _1, category->getUUID(), append)); } else { LLAppearanceMgr::changeOutfit(TRUE, category->getUUID(), append); } } void LLAppearanceMgr::wearOutfitByName(const std::string& name) { llinfos << "Wearing category " << name << llendl; //inc_busy_count(); LLInventoryModel::cat_array_t cat_array; LLInventoryModel::item_array_t item_array; LLNameCategoryCollector has_name(name); gInventory.collectDescendentsIf(gInventory.getRootFolderID(), cat_array, item_array, LLInventoryModel::EXCLUDE_TRASH, has_name); bool copy_items = false; LLInventoryCategory* cat = NULL; if (cat_array.count() > 0) { // Just wear the first one that matches cat = cat_array.get(0); } else { gInventory.collectDescendentsIf(LLUUID::null, cat_array, item_array, LLInventoryModel::EXCLUDE_TRASH, has_name); if(cat_array.count() > 0) { cat = cat_array.get(0); copy_items = true; } } if(cat) { LLAppearanceMgr::wearInventoryCategory(cat, copy_items, false); } else { llwarns << "Couldn't find outfit " <<name<< " in wearOutfitByName()" << llendl; } //dec_busy_count(); } bool areMatchingWearables(const LLViewerInventoryItem *a, const LLViewerInventoryItem *b) { return (a->isWearableType() && b->isWearableType() && (a->getWearableType() == b->getWearableType())); } class LLDeferredCOFLinkObserver: public LLInventoryObserver { public: LLDeferredCOFLinkObserver(const LLUUID& item_id, bool do_update): mItemID(item_id), mDoUpdate(do_update) { } ~LLDeferredCOFLinkObserver() { } /* virtual */ void changed(U32 mask) { const LLInventoryItem *item = gInventory.getItem(mItemID); if (item) { gInventory.removeObserver(this); LLAppearanceMgr::instance().addCOFItemLink(item,mDoUpdate); delete this; } } private: const LLUUID mItemID; bool mDoUpdate; }; void LLAppearanceMgr::addCOFItemLink(const LLUUID &item_id, bool do_update ) { const LLInventoryItem *item = gInventory.getItem(item_id); if (!item) { LLDeferredCOFLinkObserver *observer = new LLDeferredCOFLinkObserver(item_id, do_update); gInventory.addObserver(observer); } else { addCOFItemLink(item, do_update); } } void LLAppearanceMgr::addCOFItemLink(const LLInventoryItem *item, bool do_update ) { const LLViewerInventoryItem *vitem = dynamic_cast<const LLViewerInventoryItem*>(item); if (!vitem) { llwarns << "not an llviewerinventoryitem, failed" << llendl; return; } gInventory.addChangedMask(LLInventoryObserver::LABEL, vitem->getLinkedUUID()); LLInventoryModel::cat_array_t cat_array; LLInventoryModel::item_array_t item_array; gInventory.collectDescendents(LLAppearanceMgr::getCOF(), cat_array, item_array, LLInventoryModel::EXCLUDE_TRASH); bool linked_already = false; for (S32 i=0; i<item_array.count(); i++) { // Are these links to the same object? const LLViewerInventoryItem* inv_item = item_array.get(i).get(); if (inv_item->getLinkedUUID() == vitem->getLinkedUUID()) { linked_already = true; } // Are these links to different items of the same wearable // type? If so, new item will replace old. // MULTI-WEARABLES: revisit if more than one per type is allowed. else if (areMatchingWearables(vitem,inv_item)) { if (inv_item->getIsLinkType()) { gInventory.purgeObject(inv_item->getUUID()); } } } if (linked_already) { if (do_update) { LLAppearanceMgr::updateAppearanceFromCOF(); } return; } else { LLPointer<LLInventoryCallback> cb = do_update ? new ModifiedCOFCallback : 0; link_inventory_item( gAgent.getID(), vitem->getLinkedUUID(), getCOF(), vitem->getName(), LLAssetType::AT_LINK, cb); } return; } // BAP remove ensemble code for 2.1? void LLAppearanceMgr::addEnsembleLink( LLInventoryCategory* cat, bool do_update ) { #if SUPPORT_ENSEMBLES // BAP add check for already in COF. LLPointer<LLInventoryCallback> cb = do_update ? new ModifiedCOFCallback : 0; link_inventory_item( gAgent.getID(), cat->getLinkedUUID(), getCOF(), cat->getName(), LLAssetType::AT_LINK_FOLDER, cb); #endif } void LLAppearanceMgr::removeCOFItemLinks(const LLUUID& item_id, bool do_update) { gInventory.addChangedMask(LLInventoryObserver::LABEL, item_id); LLInventoryModel::cat_array_t cat_array; LLInventoryModel::item_array_t item_array; gInventory.collectDescendents(LLAppearanceMgr::getCOF(), cat_array, item_array, LLInventoryModel::EXCLUDE_TRASH); for (S32 i=0; i<item_array.count(); i++) { const LLInventoryItem* item = item_array.get(i).get(); if (item->getIsLinkType() && item->getLinkedUUID() == item_id) { gInventory.purgeObject(item->getUUID()); } } if (do_update) { LLAppearanceMgr::updateAppearanceFromCOF(); } } void LLAppearanceMgr::updateIsDirty() { LLUUID cof = getCOF(); LLUUID base_outfit; // find base outfit link const LLViewerInventoryItem* base_outfit_item = getBaseOutfitLink(); LLViewerInventoryCategory* catp = NULL; if (base_outfit_item && base_outfit_item->getIsLinkType()) { catp = base_outfit_item->getLinkedCategory(); } if(catp && catp->getPreferredType() == LLFolderType::FT_OUTFIT) { base_outfit = catp->getUUID(); } if(base_outfit.isNull()) { // no outfit link found, display "unsaved outfit" mOutfitIsDirty = true; } else { LLInventoryModel::cat_array_t cof_cats; LLInventoryModel::item_array_t cof_items; gInventory.collectDescendents(cof, cof_cats, cof_items, LLInventoryModel::EXCLUDE_TRASH); LLInventoryModel::cat_array_t outfit_cats; LLInventoryModel::item_array_t outfit_items; gInventory.collectDescendents(base_outfit, outfit_cats, outfit_items, LLInventoryModel::EXCLUDE_TRASH); if(outfit_items.count() != cof_items.count() -1) { // Current outfit folder should have one more item than the outfit folder. // this one item is the link back to the outfit folder itself. mOutfitIsDirty = true; } else { typedef std::set<LLUUID> item_set_t; item_set_t cof_set; item_set_t outfit_set; // sort COF items by UUID for (S32 i = 0; i < cof_items.count(); ++i) { LLViewerInventoryItem *item = cof_items.get(i); // don't add the base outfit link to the list of objects we're comparing if(item != base_outfit_item) { cof_set.insert(item->getLinkedUUID()); } } // sort outfit folder by UUID for (S32 i = 0; i < outfit_items.count(); ++i) { LLViewerInventoryItem *item = outfit_items.get(i); outfit_set.insert(item->getLinkedUUID()); } mOutfitIsDirty = (outfit_set != cof_set); } } } void LLAppearanceMgr::onFirstFullyVisible() { // If this is the very first time the user has logged into viewer2+ (from a legacy viewer, or new account) // then auto-populate outfits from the library into the My Outfits folder. llinfos << "avatar fully visible" << llendl; static bool check_populate_my_outfits = true; if (check_populate_my_outfits && (LLInventoryModel::getIsFirstTimeInViewer2() || gSavedSettings.getBOOL("MyOutfitsAutofill"))) { gAgentWearables.populateMyOutfitsFolder(); } check_populate_my_outfits = false; } //#define DUMP_CAT_VERBOSE void LLAppearanceMgr::dumpCat(const LLUUID& cat_id, const std::string& msg) { LLInventoryModel::cat_array_t cats; LLInventoryModel::item_array_t items; gInventory.collectDescendents(cat_id, cats, items, LLInventoryModel::EXCLUDE_TRASH); #ifdef DUMP_CAT_VERBOSE llinfos << llendl; llinfos << str << llendl; S32 hitcount = 0; for(S32 i=0; i<items.count(); i++) { LLViewerInventoryItem *item = items.get(i); if (item) hitcount++; llinfos << i <<" "<< item->getName() <<llendl; } #endif llinfos << msg << " count " << items.count() << llendl; } void LLAppearanceMgr::dumpItemArray(const LLInventoryModel::item_array_t& items, const std::string& msg) { llinfos << msg << llendl; for (S32 i=0; i<items.count(); i++) { LLViewerInventoryItem *item = items.get(i); llinfos << i <<" " << item->getName() << llendl; } llinfos << llendl; } LLAppearanceMgr::LLAppearanceMgr(): mAttachmentInvLinkEnabled(false), mOutfitIsDirty(false) { } LLAppearanceMgr::~LLAppearanceMgr() { } void LLAppearanceMgr::setAttachmentInvLinkEnable(bool val) { llinfos << "setAttachmentInvLinkEnable => " << (int) val << llendl; mAttachmentInvLinkEnabled = val; } void dumpAttachmentSet(const std::set<LLUUID>& atts, const std::string& msg) { llinfos << msg << llendl; for (std::set<LLUUID>::const_iterator it = atts.begin(); it != atts.end(); ++it) { LLUUID item_id = *it; LLViewerInventoryItem *item = gInventory.getItem(item_id); if (item) llinfos << "atts " << item->getName() << llendl; else llinfos << "atts " << "UNKNOWN[" << item_id.asString() << "]" << llendl; } llinfos << llendl; } void LLAppearanceMgr::registerAttachment(const LLUUID& item_id) { mRegisteredAttachments.insert(item_id); gInventory.addChangedMask(LLInventoryObserver::LABEL, item_id); //dumpAttachmentSet(mRegisteredAttachments,"after register:"); if (mAttachmentInvLinkEnabled) { LLAppearanceMgr::addCOFItemLink(item_id, false); // Add COF link for item. } else { //llinfos << "no link changes, inv link not enabled" << llendl; } } void LLAppearanceMgr::unregisterAttachment(const LLUUID& item_id) { mRegisteredAttachments.erase(item_id); gInventory.addChangedMask(LLInventoryObserver::LABEL, item_id); //dumpAttachmentSet(mRegisteredAttachments,"after unregister:"); if (mAttachmentInvLinkEnabled) { //LLAppearanceMgr::dumpCat(LLAppearanceMgr::getCOF(),"Removing attachment link:"); LLAppearanceMgr::removeCOFItemLinks(item_id, false); } else { //llinfos << "no link changes, inv link not enabled" << llendl; } } void LLAppearanceMgr::linkRegisteredAttachments() { for (std::set<LLUUID>::iterator it = mRegisteredAttachments.begin(); it != mRegisteredAttachments.end(); ++it) { LLUUID item_id = *it; addCOFItemLink(item_id, false); } mRegisteredAttachments.clear(); } BOOL LLAppearanceMgr::getIsInCOF(const LLUUID& obj_id) const { return gInventory.isObjectDescendentOf(obj_id, getCOF()); } BOOL LLAppearanceMgr::getIsProtectedCOFItem(const LLUUID& obj_id) const { if (!getIsInCOF(obj_id)) return FALSE; // If a non-link somehow ended up in COF, allow deletion. const LLInventoryObject *obj = gInventory.getObject(obj_id); if (obj && !obj->getIsLinkType()) { return FALSE; } // For now, don't allow direct deletion from the COF. Instead, force users // to choose "Detach" or "Take Off". return TRUE; /* const LLInventoryObject *obj = gInventory.getObject(obj_id); if (!obj) return FALSE; // Can't delete bodyparts, since this would be equivalent to removing the item. if (obj->getType() == LLAssetType::AT_BODYPART) return TRUE; // Can't delete the folder link, since this is saved for bookkeeping. if (obj->getActualType() == LLAssetType::AT_LINK_FOLDER) return TRUE; return FALSE; */ }