diff --git a/common/dynamic_zone_base.h b/common/dynamic_zone_base.h index 075c4e606..e321ce704 100644 --- a/common/dynamic_zone_base.h +++ b/common/dynamic_zone_base.h @@ -25,6 +25,8 @@ struct DynamicZoneMember DynamicZoneMember(uint32_t id, std::string name_, DynamicZoneMemberStatus status_) : id(id), name{std::move(name_)}, status(status_) {} + bool IsOnline() const { return status == DynamicZoneMemberStatus::Online || + status == DynamicZoneMemberStatus::InDynamicZone; } bool IsValid() const { return id != 0 && !name.empty(); } }; diff --git a/common/expedition_base.cpp b/common/expedition_base.cpp index 732a759e7..cb1882dca 100644 --- a/common/expedition_base.cpp +++ b/common/expedition_base.cpp @@ -1,5 +1,6 @@ #include "expedition_base.h" #include "repositories/expeditions_repository.h" +#include "rulesys.h" ExpeditionBase::ExpeditionBase(uint32_t id, const std::string& uuid, const std::string& expedition_name, const DynamicZoneMember& leader, @@ -91,3 +92,27 @@ DynamicZoneMember ExpeditionBase::GetMemberData(const std::string& character_nam } return member_data; } + +bool ExpeditionBase::SetInternalMemberStatus(uint32_t character_id, DynamicZoneMemberStatus status) +{ + if (status == DynamicZoneMemberStatus::InDynamicZone && !RuleB(Expedition, EnableInDynamicZoneStatus)) + { + status = DynamicZoneMemberStatus::Online; + } + + if (character_id == m_leader.id) + { + m_leader.status = status; + } + + auto it = std::find_if(m_members.begin(), m_members.end(), + [&](const DynamicZoneMember& member) { return member.id == character_id; }); + + if (it != m_members.end() && it->status != status) + { + it->status = status; + return true; + } + + return false; +} diff --git a/common/expedition_base.h b/common/expedition_base.h index 25c5cfd33..cdd2382f7 100644 --- a/common/expedition_base.h +++ b/common/expedition_base.h @@ -33,6 +33,7 @@ public: bool HasMember(uint32_t character_id); bool IsEmpty() const { return m_members.empty(); } void RemoveInternalMember(uint32_t character_id); + bool SetInternalMemberStatus(uint32_t character_id, DynamicZoneMemberStatus status); void LoadRepositoryResult(ExpeditionsRepository::ExpeditionWithLeader&& entry); void AddMemberFromRepositoryResult(ExpeditionMembersRepository::MemberWithName&& entry); diff --git a/common/servertalk.h b/common/servertalk.h index 7fbac5f6e..b7c59e123 100644 --- a/common/servertalk.h +++ b/common/servertalk.h @@ -148,7 +148,7 @@ #define ServerOP_ExpeditionMemberChange 0x0404 #define ServerOP_ExpeditionMemberSwap 0x0405 #define ServerOP_ExpeditionMemberStatus 0x0406 -#define ServerOP_ExpeditionGetOnlineMembers 0x0407 +#define ServerOP_ExpeditionGetMemberStatuses 0x0407 #define ServerOP_ExpeditionDzAddPlayer 0x0408 #define ServerOP_ExpeditionDzMakeLeader 0x0409 #define ServerOP_ExpeditionCharacterLockout 0x040d @@ -159,7 +159,6 @@ #define ServerOP_ExpeditionMembersRemoved 0x0412 #define ServerOP_ExpeditionLockoutDuration 0x0414 #define ServerOP_ExpeditionExpireWarning 0x0416 -#define ServerOP_ExpeditionChooseNewLeader 0x0417 #define ServerOP_DzAddRemoveCharacter 0x0450 #define ServerOP_DzRemoveAllCharacters 0x0451 @@ -2035,19 +2034,15 @@ struct ServerExpeditionMemberStatus_Struct { uint32 character_id; }; -struct ServerExpeditionCharacterEntry_Struct { - uint32 expedition_id; +struct ServerExpeditionMemberStatusEntry_Struct { uint32 character_id; - uint32 character_zone_id; - uint16 character_instance_id; - uint8 character_online; // 0: offline 1: online + uint8 online_status; // 0: unknown 1: Online 2: Offline 3: In Dynamic Zone 4: Link Dead }; -struct ServerExpeditionCharacters_Struct { - uint32 sender_zone_id; - uint16 sender_instance_id; +struct ServerExpeditionMemberStatuses_Struct { + uint32 expedition_id; uint32 count; - ServerExpeditionCharacterEntry_Struct entries[0]; + ServerExpeditionMemberStatusEntry_Struct entries[0]; }; struct ServerExpeditionLockout_Struct { diff --git a/world/expedition.cpp b/world/expedition.cpp index fceb6c7dc..a68ddd093 100644 --- a/world/expedition.cpp +++ b/world/expedition.cpp @@ -62,10 +62,8 @@ void Expedition::ChooseNewLeader() return; } - // we don't track expedition member status in world so may choose a linkdead member - // this is fine since it will trigger another change when that member goes offline auto it = std::find_if(m_members.begin(), m_members.end(), [&](const DynamicZoneMember& member) { - if (member.id != m_leader.id) { + if (member.id != m_leader.id && member.IsOnline()) { auto member_cle = client_list.FindCLEByCharacterID(member.id); return (member_cle && member_cle->GetOnline() == CLE_Status::InZone); } @@ -171,3 +169,62 @@ bool Expedition::Process() return false; } + +void Expedition::UpdateMemberStatus(uint32_t character_id, DynamicZoneMemberStatus status) +{ + SetInternalMemberStatus(character_id, status); + + // any member status update will trigger a leader fix if leader was offline + if (m_leader.status == DynamicZoneMemberStatus::Offline) + { + ChooseNewLeader(); + } +} + +void Expedition::SendZoneMemberStatuses(uint16_t zone_id, uint16_t instance_id) +{ + const auto& members = GetMembers(); + + uint32_t members_count = static_cast(members.size()); + uint32_t entries_size = sizeof(ServerExpeditionMemberStatusEntry_Struct) * members_count; + uint32_t pack_size = sizeof(ServerExpeditionMemberStatuses_Struct) + entries_size; + auto pack = std::make_unique(ServerOP_ExpeditionGetMemberStatuses, pack_size); + auto buf = reinterpret_cast(pack->pBuffer); + buf->expedition_id = GetID(); + buf->count = members_count; + + for (int i = 0; i < members.size(); ++i) + { + buf->entries[i].character_id = members[i].id; + buf->entries[i].online_status = static_cast(members[i].status); + } + + zoneserver_list.SendPacket(zone_id, instance_id, pack.get()); +} + +void Expedition::CacheMemberStatuses() +{ + // called when a new expedition is cached to fill member statuses + std::string zone_name{}; + std::vector all_clients; + all_clients.reserve(client_list.GetClientCount()); + client_list.GetClients(zone_name.c_str(), all_clients); + + for (const auto& member : m_members) + { + auto it = std::find_if(all_clients.begin(), all_clients.end(), + [&](const ClientListEntry* cle) { return (cle && cle->CharID() == member.id); }); + + auto status = DynamicZoneMemberStatus::Offline; + if (it != all_clients.end()) + { + status = DynamicZoneMemberStatus::Online; + if (GetDynamicZone().IsSameDz((*it)->zone(), (*it)->instance())) + { + status = DynamicZoneMemberStatus::InDynamicZone; + } + } + + SetInternalMemberStatus(member.id, status); + } +} diff --git a/world/expedition.h b/world/expedition.h index db0647093..119f78685 100644 --- a/world/expedition.h +++ b/world/expedition.h @@ -32,16 +32,18 @@ public: Expedition(); void RemoveMember(uint32_t character_id); + void CacheMemberStatuses(); void CheckExpireWarning(); void CheckLeader(); void ChooseNewLeader(); DynamicZone& GetDynamicZone() { return m_dynamic_zone; } bool Process(); - + void SendZoneMemberStatuses(uint16_t zone_id, uint16_t instance_id); void SendZonesExpeditionDeleted(); void SendZonesExpireWarning(uint32_t minutes_remaining); void SetDynamicZone(DynamicZone&& dz); bool SetNewLeader(const DynamicZoneMember& member); + void UpdateMemberStatus(uint32_t character_id, DynamicZoneMemberStatus status); private: void SendZonesLeaderChanged(); diff --git a/world/expedition_message.cpp b/world/expedition_message.cpp index 999dfd44a..36eee5ceb 100644 --- a/world/expedition_message.cpp +++ b/world/expedition_message.cpp @@ -35,11 +35,6 @@ void ExpeditionMessage::HandleZoneMessage(ServerPacket* pack) { switch (pack->opcode) { - case ServerOP_ExpeditionChooseNewLeader: - { - ExpeditionMessage::ChooseNewLeader(pack); - break; - } case ServerOP_ExpeditionCreate: { auto buf = reinterpret_cast(pack->pBuffer); @@ -69,9 +64,21 @@ void ExpeditionMessage::HandleZoneMessage(ServerPacket* pack) zoneserver_list.SendPacket(pack); break; } - case ServerOP_ExpeditionGetOnlineMembers: + case ServerOP_ExpeditionMemberStatus: { - ExpeditionMessage::GetOnlineMembers(pack); + auto buf = reinterpret_cast(pack->pBuffer); + auto expedition = expedition_state.GetExpedition(buf->expedition_id); + if (expedition) + { + auto status = static_cast(buf->status); + expedition->UpdateMemberStatus(buf->character_id, status); + } + zoneserver_list.SendPacket(pack); + break; + } + case ServerOP_ExpeditionGetMemberStatuses: + { + ExpeditionMessage::GetMemberStatuses(pack); break; } case ServerOP_ExpeditionDzAddPlayer: @@ -157,31 +164,14 @@ void ExpeditionMessage::MakeLeader(ServerPacket* pack) } } -void ExpeditionMessage::GetOnlineMembers(ServerPacket* pack) +void ExpeditionMessage::GetMemberStatuses(ServerPacket* pack) { - auto buf = reinterpret_cast(pack->pBuffer); - - // not efficient but only requested during caching - char zone_name[64] = {0}; - std::vector all_clients; - all_clients.reserve(client_list.GetClientCount()); - client_list.GetClients(zone_name, all_clients); - - for (uint32_t i = 0; i < buf->count; ++i) + auto buf = reinterpret_cast(pack->pBuffer); + auto expedition = expedition_state.GetExpedition(buf->expedition_id); + if (expedition) { - auto it = std::find_if(all_clients.begin(), all_clients.end(), [&](const ClientListEntry* cle) { - return (cle && cle->CharID() == buf->entries[i].character_id); - }); - - if (it != all_clients.end()) - { - buf->entries[i].character_zone_id = (*it)->zone(); - buf->entries[i].character_instance_id = (*it)->instance(); - buf->entries[i].character_online = true; - } + expedition->SendZoneMemberStatuses(buf->sender_zone_id, buf->sender_instance_id); } - - zoneserver_list.SendPacket(buf->sender_zone_id, buf->sender_instance_id, pack); } void ExpeditionMessage::SaveInvite(ServerPacket* pack) @@ -211,13 +201,3 @@ void ExpeditionMessage::RequestInvite(ServerPacket* pack) } } } - -void ExpeditionMessage::ChooseNewLeader(ServerPacket* pack) -{ - auto buf = reinterpret_cast(pack->pBuffer); - auto expedition = expedition_state.GetExpedition(buf->expedition_id); - if (expedition) - { - expedition->ChooseNewLeader(); - } -} diff --git a/world/expedition_message.h b/world/expedition_message.h index 8789577da..135ef0cba 100644 --- a/world/expedition_message.h +++ b/world/expedition_message.h @@ -26,8 +26,7 @@ class ServerPacket; namespace ExpeditionMessage { void AddPlayer(ServerPacket* pack); - void ChooseNewLeader(ServerPacket* pack); - void GetOnlineMembers(ServerPacket* pack); + void GetMemberStatuses(ServerPacket* pack); void HandleZoneMessage(ServerPacket* pack); void MakeLeader(ServerPacket* pack); void RequestInvite(ServerPacket* pack); diff --git a/world/expedition_state.cpp b/world/expedition_state.cpp index 33a92d61c..c1d520c69 100644 --- a/world/expedition_state.cpp +++ b/world/expedition_state.cpp @@ -110,6 +110,8 @@ void ExpeditionState::CacheExpeditions( } } + expedition->CacheMemberStatuses(); + m_expeditions.emplace_back(std::move(expedition)); } } diff --git a/world/zoneserver.cpp b/world/zoneserver.cpp index 9a319774a..4e37b1d7e 100644 --- a/world/zoneserver.cpp +++ b/world/zoneserver.cpp @@ -1366,17 +1366,16 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { case ServerOP_ExpeditionLockout: case ServerOP_ExpeditionLockoutDuration: case ServerOP_ExpeditionLockState: - case ServerOP_ExpeditionMemberStatus: case ServerOP_ExpeditionReplayOnJoin: case ServerOP_ExpeditionExpireWarning: { zoneserver_list.SendPacket(pack); break; } - case ServerOP_ExpeditionChooseNewLeader: case ServerOP_ExpeditionCreate: - case ServerOP_ExpeditionGetOnlineMembers: + case ServerOP_ExpeditionGetMemberStatuses: case ServerOP_ExpeditionMemberChange: + case ServerOP_ExpeditionMemberStatus: case ServerOP_ExpeditionMemberSwap: case ServerOP_ExpeditionMembersRemoved: case ServerOP_ExpeditionDzAddPlayer: diff --git a/zone/expedition.cpp b/zone/expedition.cpp index c3ef81e6a..7d404ff03 100644 --- a/zone/expedition.cpp +++ b/zone/expedition.cpp @@ -167,8 +167,6 @@ void Expedition::CacheExpeditions( auto expedition_members = ExpeditionMembersRepository::GetWithNames(database, expedition_ids); auto expedition_lockouts = ExpeditionLockoutsRepository::GetWithTimestamp(database, expedition_ids); - std::vector> expedition_character_ids; // for online status request - for (auto& entry : expedition_entries) { auto expedition = std::make_unique(); @@ -189,7 +187,6 @@ void Expedition::CacheExpeditions( if (member.expedition_id == expedition->GetID()) { expedition->AddMemberFromRepositoryResult(std::move(member)); - expedition_character_ids.emplace_back(expedition->GetID(), member.character_id); } } @@ -210,12 +207,11 @@ void Expedition::CacheExpeditions( } } + expedition->SendWorldExpeditionUpdate(ServerOP_ExpeditionGetMemberStatuses); + auto inserted = zone->expedition_cache.emplace(entry.id, std::move(expedition)); inserted.first->second->SendUpdatesToZoneMembers(); } - - // ask world for online members from all cached expeditions at once - Expedition::SendWorldGetOnlineMembers(expedition_character_ids); } void Expedition::CacheFromDatabase(uint32_t expedition_id) @@ -521,19 +517,12 @@ void Expedition::SetMemberStatus(Client* client, DynamicZoneMemberStatus status) { if (client) { - UpdateMemberStatus(client->CharacterID(), status); + SendMemberStatusToZoneMembers(client->CharacterID(), status); SendWorldMemberStatus(client->CharacterID(), status); - - // world could detect this itself but it'd have to process member status updates - // a member coming online will trigger a leader change if all members were offline - if (m_leader.status == DynamicZoneMemberStatus::Offline) - { - SendWorldExpeditionUpdate(ServerOP_ExpeditionChooseNewLeader); - } } } -void Expedition::UpdateMemberStatus(uint32_t update_member_id, DynamicZoneMemberStatus status) +void Expedition::SendMemberStatusToZoneMembers(uint32_t update_member_id, DynamicZoneMemberStatus status) { auto member_data = GetMemberData(update_member_id); if (!member_data.IsValid()) @@ -541,35 +530,19 @@ void Expedition::UpdateMemberStatus(uint32_t update_member_id, DynamicZoneMember return; } - if (status == DynamicZoneMemberStatus::InDynamicZone && !RuleB(Expedition, EnableInDynamicZoneStatus)) - { - status = DynamicZoneMemberStatus::Online; - } - - if (update_member_id == m_leader.id) - { - m_leader.status = status; - } - // if zone already had this member status cached avoid packet update to clients - if (member_data.status == status) + bool changed = SetInternalMemberStatus(update_member_id, status); + if (changed) { - return; - } - - auto outapp_member_status = CreateMemberListStatusPacket(member_data.name, status); - - for (auto& member : m_members) - { - if (member.id == update_member_id) + member_data = GetMemberData(update_member_id); // rules may override status + auto outapp_member_status = CreateMemberListStatusPacket(member_data.name, member_data.status); + for (auto& member : m_members) { - member.status = status; - } - - Client* member_client = entity_list.GetClientByCharID(member.id); - if (member_client) - { - member_client->QueuePacket(outapp_member_status.get()); + Client* member_client = entity_list.GetClientByCharID(member.id); + if (member_client) + { + member_client->QueuePacket(outapp_member_status.get()); + } } } } @@ -1095,6 +1068,8 @@ void Expedition::ProcessMakeLeader(Client* old_leader_client, Client* new_leader void Expedition::ProcessMemberAdded(const std::string& char_name, uint32_t added_char_id) { + AddInternalMember({ added_char_id, char_name, DynamicZoneMemberStatus::Online }); + // adds the member to this expedition and notifies both leader and new member Client* leader_client = entity_list.GetClientByCharID(m_leader.id); if (leader_client) @@ -1102,18 +1077,16 @@ void Expedition::ProcessMemberAdded(const std::string& char_name, uint32_t added leader_client->MessageString(Chat::Yellow, EXPEDITION_MEMBER_ADDED, char_name.c_str(), m_expedition_name.c_str()); } - AddInternalMember({ added_char_id, char_name, DynamicZoneMemberStatus::Online }); - Client* member_client = entity_list.GetClientByCharID(added_char_id); if (member_client) { member_client->SetExpeditionID(GetID()); member_client->SendDzCompassUpdate(); - SendClientExpeditionInfo(member_client); + member_client->QueuePacket(CreateInfoPacket().get()); member_client->MessageString(Chat::Yellow, EXPEDITION_MEMBER_ADDED, char_name.c_str(), m_expedition_name.c_str()); } - SendNewMemberAddedToZoneMembers(char_name); + SendMemberListToZoneMembers(); } void Expedition::ProcessMemberRemoved(const std::string& removed_char_name, uint32_t removed_char_id) @@ -1271,23 +1244,16 @@ void Expedition::AddLockoutClients( } } -void Expedition::SendNewMemberAddedToZoneMembers(const std::string& added_name) +void Expedition::SendMemberListToZoneMembers() { - // live only sends MemberListName when members are added from a swap, otherwise - // it sends expedition info (unnecessary) and the full member list - // we send a full member list update for both cases since MemberListName adds as - // "unknown" status (either due to unknown packet fields or future client change) auto outapp_members = CreateMemberListPacket(false); for (const auto& member : m_members) { - if (member.name != added_name) // new member already updated + Client* member_client = entity_list.GetClientByCharID(member.id); + if (member_client) { - Client* member_client = entity_list.GetClientByCharID(member.id); - if (member_client) - { - member_client->QueuePacket(outapp_members.get()); - } + member_client->QueuePacket(outapp_members.get()); } } } @@ -1562,29 +1528,6 @@ void Expedition::SendWorldSettingChanged(uint16_t server_opcode, bool setting_va worldserver.SendPacket(pack.get()); } -void Expedition::SendWorldGetOnlineMembers( - const std::vector>& expedition_character_ids) -{ - // request online status of characters - uint32_t count = static_cast(expedition_character_ids.size()); - uint32_t entries_size = sizeof(ServerExpeditionCharacterEntry_Struct) * count; - uint32_t pack_size = sizeof(ServerExpeditionCharacters_Struct) + entries_size; - auto pack = std::make_unique(ServerOP_ExpeditionGetOnlineMembers, pack_size); - auto buf = reinterpret_cast(pack->pBuffer); - buf->sender_zone_id = zone ? zone->GetZoneID() : 0; - buf->sender_instance_id = zone ? zone->GetInstanceID() : 0; - buf->count = count; - for (uint32_t i = 0; i < buf->count; ++i) - { - buf->entries[i].expedition_id = expedition_character_ids[i].first; - buf->entries[i].character_id = expedition_character_ids[i].second; - buf->entries[i].character_zone_id = 0; - buf->entries[i].character_instance_id = 0; - buf->entries[i].character_online = false; - } - worldserver.SendPacket(pack.get()); -} - void Expedition::SendWorldCharacterLockout( uint32_t character_id, const ExpeditionLockoutTimer& lockout, bool remove) { @@ -1794,7 +1737,8 @@ void Expedition::HandleWorldMessage(ServerPacket* pack) auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); if (expedition) { - expedition->UpdateMemberStatus(buf->character_id, static_cast(buf->status)); + auto status = static_cast(buf->status); + expedition->SendMemberStatusToZoneMembers(buf->character_id, status); } } break; @@ -1825,24 +1769,19 @@ void Expedition::HandleWorldMessage(ServerPacket* pack) } break; } - case ServerOP_ExpeditionGetOnlineMembers: + case ServerOP_ExpeditionGetMemberStatuses: { - // reply from world for online member statuses request (for multiple expeditions) - auto buf = reinterpret_cast(pack->pBuffer); - for (uint32_t i = 0; i < buf->count; ++i) + // reply from world for online member statuses request + auto buf = reinterpret_cast(pack->pBuffer); + auto expedition = Expedition::FindCachedExpeditionByID(buf->expedition_id); + if (expedition) { - auto member = reinterpret_cast(&buf->entries[i]); - auto expedition = Expedition::FindCachedExpeditionByID(member->expedition_id); - if (expedition) + for (uint32_t i = 0; i < buf->count; ++i) { - auto is_online = member->character_online; - auto status = is_online ? DynamicZoneMemberStatus::Online : DynamicZoneMemberStatus::Offline; - if (is_online && expedition->GetDynamicZone().IsInstanceID(member->character_instance_id)) - { - status = DynamicZoneMemberStatus::InDynamicZone; - } - expedition->UpdateMemberStatus(member->character_id, status); + auto status = static_cast(buf->entries[i].online_status); + expedition->SetInternalMemberStatus(buf->entries[i].character_id, status); } + expedition->SendMemberListToZoneMembers(); } break; } diff --git a/zone/expedition.h b/zone/expedition.h index adce73fcd..f5e7834bd 100644 --- a/zone/expedition.h +++ b/zone/expedition.h @@ -128,7 +128,6 @@ public: private: static void CacheExpeditions(std::vector&& expeditions); - static void SendWorldGetOnlineMembers(const std::vector>& expedition_character_ids); static void SendWorldCharacterLockout(uint32_t character_id, const ExpeditionLockoutTimer& lockout, bool remove); void AddLockout(const ExpeditionLockoutTimer& lockout, bool members_only = false); @@ -148,8 +147,9 @@ private: Client* client, const std::string& inviter_name, const std::string& swap_remove_name); void SendLeaderMessage(Client* leader_client, uint16_t chat_type, uint32_t string_id, const std::initializer_list& args = {}); + void SendMemberListToZoneMembers(); + void SendMemberStatusToZoneMembers(uint32_t update_character_id, DynamicZoneMemberStatus status); void SendMembersExpireWarning(uint32_t minutes); - void SendNewMemberAddedToZoneMembers(const std::string& added_name); void SendUpdatesToZoneMembers(bool clear = false, bool message_on_clear = true); void SendCompassUpdateToZoneMembers(); void SendWorldExpeditionUpdate(uint16_t server_opcode); @@ -167,7 +167,6 @@ private: void SetDynamicZone(DynamicZone&& dz); void TryAddClient(Client* add_client, const std::string& inviter_name, const std::string& swap_remove_name, Client* leader_client = nullptr); - void UpdateMemberStatus(uint32_t update_character_id, DynamicZoneMemberStatus status); std::unique_ptr CreateExpireWarningPacket(uint32_t minutes_remaining); std::unique_ptr CreateInfoPacket(bool clear = false); diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index 65f43676b..545625d0d 100644 --- a/zone/worldserver.cpp +++ b/zone/worldserver.cpp @@ -2905,7 +2905,7 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) case ServerOP_ExpeditionMemberStatus: case ServerOP_ExpeditionMembersRemoved: case ServerOP_ExpeditionReplayOnJoin: - case ServerOP_ExpeditionGetOnlineMembers: + case ServerOP_ExpeditionGetMemberStatuses: case ServerOP_ExpeditionDzAddPlayer: case ServerOP_ExpeditionDzMakeLeader: case ServerOP_ExpeditionCharacterLockout: