1 #ifdef HAVE_CONFIG_H
2     #include "config.h"
3 #endif
4 
5 #include <Eris/Account.h>
6 #include <Eris/Connection.h>
7 #include <Eris/LogStream.h>
8 #include <Eris/Exceptions.h>
9 #include <Eris/Avatar.h>
10 #include <Eris/Router.h>
11 #include <Eris/Response.h>
12 #include <Eris/DeleteLater.h>
13 #include <Eris/Timeout.h>
14 #include "SpawnPoint.h"
15 #include "CharacterType.h"
16 
17 #include <Atlas/Objects/Entity.h>
18 #include <Atlas/Objects/Operation.h>
19 #include <Atlas/Objects/Anonymous.h>
20 
21 #include <algorithm>
22 #include <cassert>
23 #include <iostream>
24 
25 using Atlas::Objects::Root;
26 using Atlas::Message::Element;
27 using namespace Atlas::Objects::Operation;
28 using Atlas::Objects::Entity::RootEntity;
29 using Atlas::Objects::Entity::Anonymous;
30 typedef Atlas::Objects::Entity::Account AtlasAccount;
31 using Atlas::Objects::smart_dynamic_cast;
32 
33 namespace Eris {
34 
35 class AccountRouter : public Router
36 {
37 public:
AccountRouter(Account * pl)38     AccountRouter(Account* pl) :
39         m_account(pl)
40     {
41         m_account->getConnection()->setDefaultRouter(this);
42     }
43 
~AccountRouter()44     virtual ~AccountRouter()
45     {
46         m_account->getConnection()->clearDefaultRouter();
47     }
48 
handleOperation(const RootOperation & op)49     virtual RouterResult handleOperation(const RootOperation& op)
50     {
51         // logout
52         if (op->getClassNo() == LOGOUT_NO) {
53             debug() << "Account received forced logout from server";
54             m_account->internalLogout(false);
55             return HANDLED;
56         }
57 
58         if ((op->getClassNo() == SIGHT_NO) && (op->getTo() == m_account->getId()))
59         {
60             const std::vector<Root>& args = op->getArgs();
61             AtlasAccount acc = smart_dynamic_cast<AtlasAccount>(args.front());
62             m_account->updateFromObject(acc);
63 
64             // refresh character data if it changed
65             if (!acc->isDefaultCharacters()) m_account->refreshCharacterInfo();
66 
67             return HANDLED;
68         }
69 
70         return IGNORED;
71     }
72 
73 private:
74     Account* m_account;
75 };
76 
77 #pragma mark -
78 
Account(Connection * con)79 Account::Account(Connection *con) :
80     m_con(con),
81     m_status(DISCONNECTED),
82     m_doingCharacterRefresh(false)
83 {
84     if (!m_con) throw InvalidOperation("invalid Connection passed to Account");
85 
86     m_router = new AccountRouter(this);
87 
88     m_con->Connected.connect(sigc::mem_fun(this, &Account::netConnected));
89     m_con->Failure.connect(sigc::mem_fun(this, &Account::netFailure));
90 }
91 
~Account()92 Account::~Account()
93 {
94     ActiveCharacterMap::iterator it;
95     for (it = m_activeCharacters.begin(); it != m_activeCharacters.end(); )
96     {
97         ActiveCharacterMap::iterator cur = it++;
98         deactivateCharacter(cur->second); // send logout op
99         // cur gets invalidated by innerDeactivateCharacter
100         delete cur->second;
101     }
102 
103     if (isLoggedIn()) logout();
104     delete m_router;
105 }
106 
login(const std::string & uname,const std::string & password)107 Result Account::login(const std::string &uname, const std::string &password)
108 {
109     if (!m_con->isConnected()) {
110         error() << "called login on unconnected Connection";
111         return NOT_CONNECTED;
112     }
113 
114     if (m_status != DISCONNECTED) {
115         error() << "called login, but state is not currently disconnected";
116         return ALREADY_LOGGED_IN;
117     }
118 
119     return internalLogin(uname, password);
120 }
121 
createAccount(const std::string & uname,const std::string & fullName,const std::string & pwd)122 Result Account::createAccount(const std::string &uname,
123     const std::string &fullName,
124     const std::string &pwd)
125 {
126     // store for re-logins
127     m_username = uname;
128     m_pass = pwd;
129 
130 
131     AtlasAccount account;
132     account->setPassword(pwd);
133     account->setName(fullName);
134     account->setUsername(uname);
135 
136     return createAccount(account);
137 }
138 
createAccount(Atlas::Objects::Entity::Account accountOp)139 Result Account::createAccount(Atlas::Objects::Entity::Account accountOp)
140 {
141     if (!m_con->isConnected()) return NOT_CONNECTED;
142     if (m_status != DISCONNECTED) return ALREADY_LOGGED_IN;
143 
144     m_status = LOGGING_IN;
145 
146     Create c;
147     c->setSerialno(getNewSerialno());
148     c->setArgs1(accountOp);
149 
150     m_con->getResponder()->await(c->getSerialno(), this, &Account::loginResponse);
151     m_con->send(c);
152 
153     m_timeout.reset(new Timeout(5000));
154     m_timeout->Expired.connect(sigc::mem_fun(this, &Account::handleLoginTimeout));
155 
156     return NO_ERR;
157 }
158 
logout()159 Result Account::logout()
160 {
161     if (!m_con->isConnected()) {
162         error() << "called logout on bad connection ignoring";
163         return NOT_CONNECTED;
164     }
165 
166     if (m_status == LOGGING_OUT) return NO_ERR;
167 
168     if (m_status != LOGGED_IN) {
169         error() << "called logout on non-logged-in Account";
170         return NOT_LOGGED_IN;
171     }
172 
173     m_status = LOGGING_OUT;
174 
175     Logout l;
176     Anonymous arg;
177     arg->setId(m_accountId);
178     l->setArgs1(arg);
179     l->setSerialno(getNewSerialno());
180 
181     m_con->getResponder()->await(l->getSerialno(), this, &Account::logoutResponse);
182     m_con->send(l);
183 
184     m_timeout.reset(new Timeout(5000));
185     m_timeout->Expired.connect(sigc::mem_fun(this, &Account::handleLogoutTimeout));
186 
187     return NO_ERR;
188 }
189 
getCharacterTypes(void) const190 const std::vector< std::string > & Account::getCharacterTypes(void) const
191 {
192     return m_characterTypes;
193 }
194 
getCharacters()195 const CharacterMap& Account::getCharacters()
196 {
197     if (m_status != LOGGED_IN)
198         error() << "Not logged into an account : getCharacter returning empty dictionary";
199 
200     return _characters;
201 }
202 
refreshCharacterInfo()203 Result Account::refreshCharacterInfo()
204 {
205     if (!m_con->isConnected()) return NOT_CONNECTED;
206     if (m_status != LOGGED_IN) return NOT_LOGGED_IN;
207 
208     // silently ignore overlapping refreshes
209     if (m_doingCharacterRefresh) return NO_ERR;
210 
211     _characters.clear();
212 
213     if (m_characterIds.empty())
214     {
215         GotAllCharacters.emit(); // we must emit the done signal
216         return NO_ERR;
217     }
218 
219 // okay, now we know we have at least one character to lookup, set the flag
220     m_doingCharacterRefresh = true;
221 
222     Look lk;
223     Anonymous obj;
224     lk->setFrom(m_accountId);
225 
226     for (StringSet::iterator I=m_characterIds.begin(); I!=m_characterIds.end(); ++I)
227     {
228         obj->setId(*I);
229         lk->setArgs1(obj);
230         lk->setSerialno(getNewSerialno());
231         m_con->getResponder()->await(lk->getSerialno(), this, &Account::sightCharacter);
232         m_con->send(lk);
233     }
234 
235     return NO_ERR;
236 }
237 
createCharacter(const Atlas::Objects::Entity::RootEntity & ent)238 Result Account::createCharacter(const Atlas::Objects::Entity::RootEntity &ent)
239 {
240     if (!m_con->isConnected()) return NOT_CONNECTED;
241     if (m_status != LOGGED_IN) {
242         if ((m_status == CREATING_CHAR) || (m_status == TAKING_CHAR)) {
243             error() << "duplicate char creation / take";
244             return DUPLICATE_CHAR_ACTIVE;
245         } else {
246             error() << "called createCharacter on unconnected Account, ignoring";
247             return NOT_LOGGED_IN;
248         }
249     }
250 
251     Create c;
252     c->setArgs1(ent);
253     c->setFrom(m_accountId);
254     c->setSerialno(getNewSerialno());
255     m_con->send(c);
256 
257     m_con->getResponder()->await(c->getSerialno(), this, &Account::avatarResponse);
258     m_status = CREATING_CHAR;
259     return NO_ERR;
260 }
261 
262 /*
263 void Account::createCharacter()
264 {
265     if (!_lobby || _lobby->getAccountID().empty())
266         throw InvalidOperation("no account exists!");
267 
268     if (!_con->isConnected())
269         throw InvalidOperation("Not connected to server");
270 
271     throw InvalidOperation("No UserInterface handler defined");
272 
273     // FIXME look up the dialog, create the instance,
274     // hook in a slot to feed the serialno of any Create op
275     // the dialog passes back to createCharacterHandler()
276 }
277 
278 void Account::createCharacterHandler(long serialno)
279 {
280     if (serialno)
281         NewCharacter((new World(this, _con))->createAvatar(serialno));
282 }
283 */
284 
takeTransferredCharacter(const std::string & id,const std::string & key)285 Result Account::takeTransferredCharacter(const std::string &id, const std::string &key)
286 {
287     if (!m_con->isConnected()) return NOT_CONNECTED;
288     if (m_status != LOGGED_IN) {
289         if ((m_status == CREATING_CHAR) || (m_status == TAKING_CHAR)) {
290             error() << "duplicate char creation / take";
291             return DUPLICATE_CHAR_ACTIVE;
292         } else {
293             error() << "called createCharacter on unconnected Account, ignoring";
294             return NOT_LOGGED_IN;
295         }
296     }
297 
298     Anonymous what;
299     what->setId(id);
300     what->setAttr("possess_key", key);
301 
302     Look l;
303     l->setFrom(getId());
304     l->setArgs1(what);
305     l->setSerialno(getNewSerialno());
306     m_con->send(l);
307 
308     m_con->getResponder()->await(l->getSerialno(), this, &Account::avatarResponse);
309     m_status = TAKING_CHAR;
310     return NO_ERR;
311 }
312 
takeCharacter(const std::string & id)313 Result Account::takeCharacter(const std::string &id)
314 {
315     if (m_characterIds.count(id) == 0) {
316         error() << "Character '" << id << "' not owned by Account " << m_username;
317         return BAD_CHARACTER_ID;
318     }
319 
320     if (!m_con->isConnected()) return NOT_CONNECTED;
321     if (m_status != LOGGED_IN) {
322         if ((m_status == CREATING_CHAR) || (m_status == TAKING_CHAR)) {
323             error() << "duplicate char creation / take";
324             return DUPLICATE_CHAR_ACTIVE;
325         } else {
326             error() << "called createCharacter on unconnected Account, ignoring";
327             return NOT_LOGGED_IN;
328         }
329     }
330 
331     Anonymous what;
332     what->setId(id);
333 
334     Look l;
335     l->setFrom(id);  // should this be m_accountId?
336     l->setArgs1(what);
337     l->setSerialno(getNewSerialno());
338     m_con->send(l);
339 
340     m_con->getResponder()->await(l->getSerialno(), this, &Account::avatarResponse);
341     m_status = TAKING_CHAR;
342     return NO_ERR;
343 }
344 
deactivateCharacter(Avatar * av)345 Result Account::deactivateCharacter(Avatar* av)
346 {
347     av->deactivate();
348     return NO_ERR;
349 }
350 
isLoggedIn() const351 bool Account::isLoggedIn() const
352 {
353     return ((m_status == LOGGED_IN) ||
354         (m_status == TAKING_CHAR) || (m_status == CREATING_CHAR));
355 }
356 
357 #pragma mark -
358 
internalLogin(const std::string & uname,const std::string & pwd)359 Result Account::internalLogin(const std::string &uname, const std::string &pwd)
360 {
361     assert(m_status == DISCONNECTED);
362 
363     m_status = LOGGING_IN;
364     m_username = uname; // store for posterity
365 
366     AtlasAccount account;
367     account->setPassword(pwd);
368     account->setUsername(uname);
369 
370     Login l;
371     l->setArgs1(account);
372     l->setSerialno(getNewSerialno());
373     m_con->getResponder()->await(l->getSerialno(), this, &Account::loginResponse);
374     m_con->send(l);
375 
376     m_timeout.reset(new Timeout(5000));
377     m_timeout->Expired.connect(sigc::mem_fun(this, &Account::handleLoginTimeout));
378 
379     return NO_ERR;
380 }
381 
logoutResponse(const RootOperation & op)382 void Account::logoutResponse(const RootOperation& op)
383 {
384     if (!op->instanceOf(INFO_NO))
385         warning() << "received a logout response that is not an INFO";
386 
387     internalLogout(true);
388 }
389 
internalLogout(bool clean)390 void Account::internalLogout(bool clean)
391 {
392     if (clean) {
393         if (m_status != LOGGING_OUT)
394             error() << "got clean logout, but not logging out already";
395     } else {
396         if ((m_status != LOGGED_IN) && (m_status != TAKING_CHAR) && (m_status != CREATING_CHAR))
397             error() << "got forced logout, but not currently logged in";
398     }
399 
400     m_con->unregisterRouterForTo(m_router, m_accountId);
401     m_status = DISCONNECTED;
402     m_timeout.reset();
403 
404     if (m_con->getStatus() == BaseConnection::DISCONNECTING) {
405         m_con->unlock();
406     } else {
407         LogoutComplete.emit(clean);
408     }
409 }
410 
loginResponse(const RootOperation & op)411 void Account::loginResponse(const RootOperation& op)
412 {
413     if (op->instanceOf(ERROR_NO)) {
414         loginError(smart_dynamic_cast<Error>(op));
415     } else if (op->instanceOf(INFO_NO)) {
416         const std::vector<Root>& args = op->getArgs();
417         loginComplete(smart_dynamic_cast<AtlasAccount>(args.front()));
418     } else
419         warning() << "received malformed login response: " << op->getClassNo();
420 }
421 
loginComplete(const AtlasAccount & p)422 void Account::loginComplete(const AtlasAccount &p)
423 {
424     if (m_status != LOGGING_IN) {
425         error() << "got loginComplete, but not currently logging in!";
426     }
427 
428     if (!p.isValid()) {
429         error() << "no account in response.";
430         return;
431     }
432 
433     //The user name being different should not be a fatal thing.
434     if (p->getUsername() != m_username) {
435         warning() << "received username does not match existing";
436         m_username = p->getUsername();
437     }
438 
439     m_status = LOGGED_IN;
440     m_accountId = p->getId();
441 
442     m_con->registerRouterForTo(m_router, m_accountId);
443     updateFromObject(p);
444 
445     // notify an people watching us
446     LoginSuccess.emit();
447 
448     m_con->Disconnecting.connect(sigc::mem_fun(this, &Account::netDisconnecting));
449     m_timeout.reset();
450 }
451 
avatarLogoutRequested(Avatar * avatar)452 void Account::avatarLogoutRequested(Avatar* avatar)
453 {
454     AvatarDeactivated.emit(avatar);
455     delete avatar;
456 }
457 
updateFromObject(const AtlasAccount & p)458 void Account::updateFromObject(const AtlasAccount &p)
459 {
460     m_characterIds = StringSet(p->getCharacters().begin(), p->getCharacters().end());
461     m_parents = p->getParents();
462 
463     if(p->hasAttr("character_types") == true)
464     {
465         const Atlas::Message::Element& CharacterTypes(p->getAttr("character_types"));
466 
467         if(CharacterTypes.isList() == true)
468         {
469             const Atlas::Message::ListType & CharacterTypesList(CharacterTypes.asList());
470             Atlas::Message::ListType::const_iterator iCharacterType(CharacterTypesList.begin());
471             Atlas::Message::ListType::const_iterator iEnd(CharacterTypesList.end());
472 
473             m_characterTypes.reserve(CharacterTypesList.size());
474             while(iCharacterType != iEnd)
475             {
476                 if(iCharacterType->isString() == true)
477                 {
478                     m_characterTypes.push_back(iCharacterType->asString());
479                 }
480                 else
481                 {
482                     error() << "An element of the \"character_types\" list is not a String.";
483                 }
484                 ++iCharacterType;
485             }
486         }
487         else
488         {
489             error() << "Account has attribute \"character_types\" which is not of type List.";
490         }
491     }
492 
493     if (p->hasAttr("spawns")) {
494         const Atlas::Message::Element& spawns(p->getAttr("spawns"));
495 
496         if (spawns.isList()) {
497             m_spawnPoints.clear();
498             const Atlas::Message::ListType & spawnsList(spawns.asList());
499             for (Atlas::Message::ListType::const_iterator I = spawnsList.begin(); I != spawnsList.end(); ++I) {
500                 if (I->isMap()) {
501                     const Atlas::Message::MapType& spawnMap = I->asMap();
502                     Atlas::Message::MapType::const_iterator spawnNameI = spawnMap.find("name");
503                     if (spawnNameI != spawnMap.end()) {
504                         const Atlas::Message::Element& name(spawnNameI->second);
505                         if (name.isString()) {
506                             CharacterTypeStore characterTypes;
507                             Atlas::Message::MapType::const_iterator characterTypesI = spawnMap.find("character_types");
508 
509                             if (characterTypesI != spawnMap.end()) {
510                                 const Atlas::Message::Element& characterTypesElement(characterTypesI->second);
511                                 if (characterTypesElement.isList()) {
512                                     const Atlas::Message::ListType & characterTypesList(characterTypesElement.asList());
513                                     for (Atlas::Message::ListType::const_iterator J = characterTypesList.begin(); J != characterTypesList.end(); ++J)
514                                     {
515                                         if (J->isString()) {
516                                             characterTypes.push_back(CharacterType(J->asString(), ""));
517                                         } else {
518                                             error() << "Character type is not of type string.";
519                                         }
520                                     }
521                                 } else {
522                                     error() << "Character type element is not of type list.";
523                                 }
524                             }
525                             SpawnPoint spawnPoint(name.asString(), characterTypes, "");
526                             m_spawnPoints.insert(SpawnPointMap::value_type(spawnPoint.getName(), spawnPoint));
527                         } else {
528                             error() << "Spawn name is not a string.";
529                         }
530                     } else {
531                         error() << "Spawn point has no name defined.";
532                     }
533                 }
534             }
535         } else {
536             error() << "Account has attribute \"spawns\" which is not of type List.";
537         }
538     }
539 }
540 
getErrorMessage(const RootOperation & err)541 std::string getErrorMessage(const RootOperation & err)
542 {
543     std::string msg;
544     const std::vector<Root>& args = err->getArgs();
545     if (args.empty()) {
546         error() << "got Error error op from server without args";
547         msg = "Unknown error.";
548     } else {
549         const Root & arg = args.front();
550         Atlas::Message::Element message;
551         if (arg->copyAttr("message", message) != 0) {
552             error() << "got Error error op from server without message";
553             msg = "Unknown error.";
554         } else {
555             if (!message.isString()) {
556                 error() << "got Error error op from server with bad message";
557                 msg = "Unknown error.";
558             } else {
559                 msg = message.String();
560             }
561         }
562     }
563     return msg;
564 }
565 
loginError(const Error & err)566 void Account::loginError(const Error& err)
567 {
568     assert(err.isValid());
569     if (m_status != LOGGING_IN) {
570         error() << "got loginError while not logging in";
571     }
572 
573     std::string msg = getErrorMessage(err);
574 
575     // update state before emitting signal
576     m_status = DISCONNECTED;
577     m_timeout.reset();
578 
579     LoginFailure.emit(msg);
580 }
581 
handleLoginTimeout()582 void Account::handleLoginTimeout()
583 {
584     m_status = DISCONNECTED;
585     deleteLater(m_timeout.release());
586 
587     LoginFailure.emit("timed out waiting for server response");
588 }
589 
avatarResponse(const RootOperation & op)590 void Account::avatarResponse(const RootOperation& op)
591 {
592     if (op->instanceOf(ERROR_NO)) {
593         std::string msg = getErrorMessage(op);
594 
595         // creating or taking a character failed for some reason
596         AvatarFailure(msg);
597         m_status = Account::LOGGED_IN;
598     } else if (op->instanceOf(INFO_NO)) {
599         const std::vector<Root>& args = op->getArgs();
600         if (args.empty()) {
601             warning() << "no args character create/take response";
602             return;
603         }
604 
605         RootEntity ent = smart_dynamic_cast<RootEntity>(args.front());
606         if (!ent.isValid()) {
607             warning() << "malformed character create/take response";
608             return;
609         }
610 
611         Avatar* av = new Avatar(*this, ent->getId());
612         AvatarSuccess.emit(av);
613         m_status = Account::LOGGED_IN;
614 
615         assert(m_activeCharacters.count(av->getId()) == 0);
616         m_activeCharacters[av->getId()] = av;
617 
618         // expect another op with the same refno
619         m_con->getResponder()->ignore(op->getRefno());
620     } else
621         warning() << "received incorrect avatar create/take response";
622 }
623 
internalDeactivateCharacter(Avatar * av)624 void Account::internalDeactivateCharacter(Avatar* av)
625 {
626     assert(m_activeCharacters.count(av->getId()) == 1);
627     m_activeCharacters.erase(av->getId());
628 }
629 
sightCharacter(const RootOperation & op)630 void Account::sightCharacter(const RootOperation& op)
631 {
632     if (!m_doingCharacterRefresh) {
633         error() << "got sight of character outside a refresh, ignoring";
634         return;
635     }
636 
637     const std::vector<Root>& args = op->getArgs();
638     if (args.empty()) {
639         error() << "got sight of character with no args";
640         return;
641     }
642 
643     RootEntity ge = smart_dynamic_cast<RootEntity>(args.front());
644     if (!ge.isValid()) {
645         error() << "got sight of character with malformed args";
646         return;
647     }
648 
649     CharacterMap::iterator C = _characters.find(ge->getId());
650     if (C != _characters.end()) {
651         error() << "duplicate sight of character " << ge->getId();
652         return;
653     }
654 
655     // okay, we can now add it to our map
656     _characters.insert(C, CharacterMap::value_type(ge->getId(), ge));
657     GotCharacterInfo.emit(ge);
658 
659     // check if we're done
660     if (_characters.size() == m_characterIds.size()) {
661         m_doingCharacterRefresh = false;
662         GotAllCharacters.emit();
663     }
664 }
665 
666 /* this will only ever get encountered after the connection is initally up;
667 thus we use it to trigger a reconnection. Note that some actions by
668 Lobby and World are also required to get everything back into the correct
669 state */
670 
netConnected()671 void Account::netConnected()
672 {
673     // re-connection
674     if (!m_username.empty() && !m_pass.empty() && (m_status == DISCONNECTED)) {
675         debug() << "Account " << m_username << " got netConnected, doing reconnect";
676         internalLogin(m_username, m_pass);
677     }
678 }
679 
netDisconnecting()680 bool Account::netDisconnecting()
681 {
682     if (m_status == LOGGED_IN) {
683         m_con->lock();
684         logout();
685         return false;
686     } else
687         return true;
688 }
689 
netFailure(const std::string &)690 void Account::netFailure(const std::string& /*msg*/)
691 {
692 
693 }
694 
handleLogoutTimeout()695 void Account::handleLogoutTimeout()
696 {
697     error() << "LOGOUT timed out waiting for response";
698 
699     m_status = DISCONNECTED;
700     deleteLater(m_timeout.release());
701 
702     LogoutComplete.emit(false);
703 }
704 
avatarLogoutResponse(const RootOperation & op)705 void Account::avatarLogoutResponse(const RootOperation& op)
706 {
707     if (!op->instanceOf(INFO_NO)) {
708         warning() << "received an avatar logout response that is not an INFO";
709         return;
710     }
711 
712     const std::vector<Root>& args(op->getArgs());
713 
714     if (args.empty() || (args.front()->getClassNo() != LOGOUT_NO)) {
715         warning() << "argument of avatar logout INFO is not a logout op";
716         return;
717     }
718 
719     RootOperation logout = smart_dynamic_cast<RootOperation>(args.front());
720     const std::vector<Root>& args2(logout->getArgs());
721     if (args2.empty()) {
722         warning() << "argument of avatar logout INFO is logout without args";
723         return;
724     }
725 
726     std::string charId = args2.front()->getId();
727     debug() << "got logout for character " << charId;
728 
729     if (!m_characterIds.count(charId)) {
730         warning() << "character ID " << charId << " is unknown on account " << m_accountId;
731     }
732 
733     ActiveCharacterMap::iterator it = m_activeCharacters.find(charId);
734     if (it == m_activeCharacters.end()) {
735         warning() << "character ID " << charId << " does not correspond to an active avatar.";
736         return;
737     }
738 
739     AvatarDeactivated.emit(it->second);
740     delete it->second; // will call back into internalDeactivateCharacter
741 }
742 
743 } // of namespace Eris
744