diff --git a/zone/client.cpp b/zone/client.cpp index 15a7e870d..ed29a3981 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -63,6 +63,10 @@ extern volatile bool RunLoops; #include "../common/expedition_lockout_timer.h" #include "cheat_manager.h" +#include "../common/repositories/character_spells_repository.h" +#include "../common/repositories/character_disciplines_repository.h" + + extern QueryServ* QServ; extern EntityList entity_list; extern Zone* zone; @@ -5587,7 +5591,7 @@ void Client::UpdateLDoNWinLoss(uint32 theme_id, bool win, bool remove) { break; default: return; - } + } database.UpdateAdventureStatsEntry(CharacterID(), theme_id, win, remove); } @@ -10550,7 +10554,7 @@ void Client::ReadBookByName(std::string book_name, uint8 book_type) out->window = 0xFF; out->type = book_type; out->invslot = 0; - + memcpy(out->booktext, book_text.c_str(), length); if (book_language > 0 && book_language < MAX_PP_LANGUAGE) { @@ -10673,3 +10677,45 @@ void Client::SummonBaggedItems(uint32 bag_item_id, const std::vector character_spells = {}; + + for (int index = 0; index < EQ::spells::SPELLBOOK_SIZE; index++) { + if (IsValidSpell(m_pp.spell_book[index])) { + auto spell = CharacterSpellsRepository::NewEntity(); + spell.id = CharacterID(); + spell.slot_id = index; + spell.spell_id = m_pp.spell_book[index]; + character_spells.emplace_back(spell); + } + } + + CharacterSpellsRepository::DeleteWhere(database, fmt::format("id = {}", CharacterID())); + + if (!character_spells.empty()) { + CharacterSpellsRepository::InsertMany(database, character_spells); + } +} + +void Client::SaveDisciplines() +{ + std::vector character_discs = {}; + + for (int index = 0; index < MAX_PP_DISCIPLINES; index++) { + if (IsValidSpell(m_pp.disciplines.values[index])) { + auto discipline = CharacterDisciplinesRepository::NewEntity(); + discipline.id = CharacterID(); + discipline.slot_id = index; + discipline.disc_id = m_pp.disciplines.values[index]; + character_discs.emplace_back(discipline); + } + } + + CharacterDisciplinesRepository::DeleteWhere(database, fmt::format("id = {}", CharacterID())); + + if (!character_discs.empty()) { + CharacterDisciplinesRepository::InsertMany(database, character_discs); + } +} diff --git a/zone/client.h b/zone/client.h index 550be9620..41fc994ee 100644 --- a/zone/client.h +++ b/zone/client.h @@ -799,10 +799,15 @@ public: std::vector GetMemmedSpells(); std::vector GetScribeableSpells(uint8 min_level = 1, uint8 max_level = 0); std::vector GetScribedSpells(); - void ScribeSpell(uint16 spell_id, int slot, bool update_client = true); - void UnscribeSpell(int slot, bool update_client = true); + // defer save used when bulk saving + void ScribeSpell(uint16 spell_id, int slot, bool update_client = true, bool defer_save = false); + void SaveSpells(); + void SaveDisciplines(); + + // defer save used when bulk saving + void UnscribeSpell(int slot, bool update_client = true, bool defer_save = false); void UnscribeSpellAll(bool update_client = true); - void UntrainDisc(int slot, bool update_client = true); + void UntrainDisc(int slot, bool update_client = true, bool defer_save = false); void UntrainDiscAll(bool update_client = true); void UntrainDiscBySpellID(uint16 spell_id, bool update_client = true); bool SpellGlobalCheck(uint16 spell_id, uint32 char_id); diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index f10e23a73..d04db5849 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -2048,7 +2048,7 @@ void Client::Handle_OP_AdventureMerchantPurchase(const EQApplicationPacket *app) } } - + } } else if (aps->Type == DiscordMerchant) { if (GetPVPPoints() < item_cost) { @@ -14791,8 +14791,8 @@ void Client::Handle_OP_Translocate(const EQApplicationPacket *app) zone->GetZoneID() == PendingTranslocateData.zone_id && zone->GetInstanceID() == PendingTranslocateData.instance_id ); - - if (parse->EventSpell(EVENT_SPELL_EFFECT_TRANSLOCATE_COMPLETE, nullptr, this, spell_id, "", 0) == 0) { + + if (parse->EventSpell(EVENT_SPELL_EFFECT_TRANSLOCATE_COMPLETE, nullptr, this, spell_id, "", 0) == 0) { // If the spell has a translocate to bind effect, AND we are already in the zone the client // is bound in, use the GoToBind method. If we send OP_Translocate in this case, the client moves itself // to the bind coords it has from the PlayerProfile, but with the X and Y reversed. I suspect they are diff --git a/zone/command.cpp b/zone/command.cpp index 723e96333..c48bbd685 100755 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -7928,7 +7928,7 @@ void command_scribespells(Client *c, const Seperator *sep) } if (!IsDiscipline(spell_id_) && !t->HasSpellScribed(spell_id)) { // isn't a discipline & we don't already have it scribed - t->ScribeSpell(spell_id_, book_slot); + t->ScribeSpell(spell_id_, book_slot, true, true); ++count; } @@ -7939,14 +7939,18 @@ void command_scribespells(Client *c, const Seperator *sep) } if (count > 0) { - t->Message(Chat::White, "Successfully scribed %i spells.", count); - if (t != c) - c->Message(Chat::White, "Successfully scribed %i spells for %s.", count, t->GetName()); + t->Message(Chat::White, "Successfully scribed %i spells.", count); + if (t != c) { + c->Message(Chat::White, "Successfully scribed %i spells for %s.", count, t->GetName()); + } + + t->SaveSpells(); } else { t->Message(Chat::White, "No spells scribed."); - if (t != c) - c->Message(Chat::White, "No spells scribed for %s.", t->GetName()); + if (t != c) { + c->Message(Chat::White, "No spells scribed for %s.", t->GetName()); + } } } @@ -8058,6 +8062,7 @@ void command_untraindiscs(Client *c, const Seperator *sep) { t = c->GetTarget()->CastToClient(); t->UntrainDiscAll(); + t->Message(Chat::Yellow, "All disciplines removed."); } void command_wpinfo(Client *c, const Seperator *sep) @@ -9275,7 +9280,7 @@ void command_npcedit(Client *c, const Seperator *sep) } if (strcasecmp(sep->arg[1], "gender") == 0) { - auto gender_id = atoi(sep->arg[2]); + auto gender_id = atoi(sep->arg[2]); c->Message(Chat::Yellow, fmt::format("NPC ID {} is now a {} ({}).", npc_id, gender_id, GetGenderName(gender_id)).c_str()); std::string query = fmt::format("UPDATE npc_types SET gender = {} WHERE id = {}", gender_id, npc_id); content_db.QueryDatabase(query); @@ -9461,7 +9466,7 @@ void command_npcedit(Client *c, const Seperator *sep) std::string query = fmt::format("UPDATE npc_types SET ammo_idfile = {} WHERE id = {}", atoi(sep->arg[2]), npc_id); content_db.QueryDatabase(query); return; - } + } if (strcasecmp(sep->arg[1], "weapon") == 0) { c->Message(Chat::Yellow, fmt::format("NPC ID {} will have Model {} set to their Primary and Model {} set to their Secondary on repop.", npc_id, atoi(sep->arg[2]), atoi(sep->arg[3])).c_str()); @@ -9679,7 +9684,7 @@ void command_npcedit(Client *c, const Seperator *sep) content_db.QueryDatabase(query); return; } - + if (strcasecmp(sep->arg[1], "accuracy") == 0) { c->Message(Chat::Yellow, fmt::format("NPC ID {} now has {} Accuracy.", npc_id, atoi(sep->arg[2])).c_str()); std::string query = fmt::format("UPDATE npc_types SET accuracy = {} WHERE id = {}", atoi(sep->arg[2]), npc_id); @@ -9749,7 +9754,7 @@ void command_npcedit(Client *c, const Seperator *sep) content_db.QueryDatabase(query); return; } - + if (strcasecmp(sep->arg[1], "armtexture") == 0) { c->Message(Chat::Yellow, fmt::format("NPC ID {} is now using Arm Texture {}.", npc_id, atoi(sep->arg[2])).c_str()); std::string query = fmt::format("UPDATE npc_types SET armtexture = {} WHERE id = {}", atoi(sep->arg[2]), npc_id); @@ -9934,7 +9939,7 @@ void command_npcedit(Client *c, const Seperator *sep) animation = 1; animation_name = "Sitting"; } else if(strcasecmp(sep->arg[2], "crouch") == 0 || atoi(sep->arg[2]) == 2) { // Crouch - animation = 2; + animation = 2; animation_name = "Crouching"; } else if(strcasecmp(sep->arg[2], "dead") == 0 || atoi(sep->arg[2]) == 3) { // Dead animation = 3; @@ -10999,7 +11004,6 @@ void command_traindisc(Client *c, const Seperator *sep) } else if (t->GetPP().disciplines.values[r] == 0) { t->GetPP().disciplines.values[r] = spell_id_; - database.SaveCharacterDisc(t->CharacterID(), r, spell_id_); change = true; t->Message(Chat::White, "You have learned a new discipline!"); ++count; // success counter @@ -11011,17 +11015,22 @@ void command_traindisc(Client *c, const Seperator *sep) } } - if (change) + if (change) { t->SendDisciplineUpdate(); + t->SaveDisciplines(); + } if (count > 0) { - t->Message(Chat::White, "Successfully trained %u disciplines.", count); - if (t != c) - c->Message(Chat::White, "Successfully trained %u disciplines for %s.", count, t->GetName()); - } else { + t->Message(Chat::White, "Successfully trained %u disciplines.", count); + if (t != c) { + c->Message(Chat::White, "Successfully trained %u disciplines for %s.", count, t->GetName()); + } + } + else { t->Message(Chat::White, "No disciplines trained."); - if (t != c) - c->Message(Chat::White, "No disciplines trained for %s.", t->GetName()); + if (t != c) { + c->Message(Chat::White, "No disciplines trained for %s.", t->GetName()); + } } } @@ -14880,7 +14889,7 @@ void command_dye(Client *c, const Seperator *sep) c->Message(Chat::White, "Command Syntax: #dye help | #dye [slot] [red] [green] [blue] [use_tint]"); return; } - + uint8 slot = 0; uint8 red = 255; uint8 green = 255; @@ -14902,7 +14911,7 @@ void command_dye(Client *c, const Seperator *sep) std::vector slot_messages; c->Message(Chat::White, "Command Syntax: #dye help | #dye [slot] [red] [green] [blue] [use_tint]"); c->Message(Chat::White, "Red, Green, and Blue go from 0 to 255."); - + for (const auto& slot : dye_slots) { slot_messages.push_back(fmt::format("({}) {}", slot_id, slot)); slot_id++; diff --git a/zone/effects.cpp b/zone/effects.cpp index 601018148..67efa4fe1 100644 --- a/zone/effects.cpp +++ b/zone/effects.cpp @@ -180,7 +180,7 @@ int32 Mob::GetActReflectedSpellDamage(int32 spell_id, int32 value, int effective value = int(static_cast(value) * CastToNPC()->GetSpellScale() / 100.0f); } } - + int32 base_spell_dmg = value; value = value * effectiveness / 100; @@ -343,7 +343,7 @@ int32 Mob::GetActSpellHealing(uint16 spell_id, int32 value, Mob* target) { } value += GetFocusEffect(focusFcHealAmtCrit, spell_id); //SPA 396 Add before critical - + if (!spells[spell_id].no_heal_damage_item_mod && itembonuses.HealAmt && spells[spell_id].classes[(GetClass() % 17) - 1] >= GetLevel() - 5) { value += GetExtraSpellAmt(spell_id, itembonuses.HealAmt, base_value); //Item Heal Amt Add before critical } @@ -351,7 +351,7 @@ int32 Mob::GetActSpellHealing(uint16 spell_id, int32 value, Mob* target) { if (target) { value += value * target->GetHealRate() / 100; //SPA 120 modifies value after Focus Applied but before critical } - + /* Apply critical hit modifier */ @@ -363,7 +363,7 @@ int32 Mob::GetActSpellHealing(uint16 spell_id, int32 value, Mob* target) { if (target) { value += target->GetFocusEffect(focusFcHealAmtIncoming, spell_id); //SPA 394 Add after critical } - + if (IsNPC() && CastToNPC()->GetHealScale()) { value = int(static_cast(value) * CastToNPC()->GetHealScale() / 100.0f); } @@ -619,13 +619,14 @@ bool Client::MemorizeSpellFromItem(uint32 item_id) { return false; } - for(int index = 0; index < EQ::spells::SPELLBOOK_SIZE; index++) { + for (int index = 0; index < EQ::spells::SPELLBOOK_SIZE; index++) { if (!HasSpellScribed(spell_id)) { auto next_slot = GetNextAvailableSpellBookSlot(); if (next_slot != -1) { ScribeSpell(spell_id, next_slot); return true; - } else { + } + else { Message( Chat::Red, "Unable to scribe spell %s (%i) to spellbook: no more spell book slots available.", @@ -635,12 +636,14 @@ bool Client::MemorizeSpellFromItem(uint32 item_id) { SummonItem(item_id); return false; } - } else { + } + else { Message(Chat::Red, "You already know this spell."); SummonItem(item_id); return false; } } + Message(Chat::Red, "You have learned too many spells and can learn no more."); return false; } diff --git a/zone/questmgr.cpp b/zone/questmgr.cpp index 89b47aea8..54a2524ab 100644 --- a/zone/questmgr.cpp +++ b/zone/questmgr.cpp @@ -1130,7 +1130,8 @@ uint16 QuestManager::scribespells(uint8 max_level, uint8 min_level) { if (initiator->HasSpellScribed(spell_id)) continue; - initiator->ScribeSpell(spell_id, book_slot); + // defer saving per spell and bulk save at the end + initiator->ScribeSpell(spell_id, book_slot, true, true); book_slot = initiator->GetNextAvailableSpellBookSlot(book_slot); spells_learned++; } @@ -1139,6 +1140,9 @@ uint16 QuestManager::scribespells(uint8 max_level, uint8 min_level) { if (spells_learned > 0) { std::string spell_message = (spells_learned == 1 ? "a new spell" : fmt::format("{} new spells", spells_learned)); initiator->Message(Chat::White, fmt::format("You have learned {}!", spell_message).c_str()); + + // bulk insert spells + initiator->SaveSpells(); } return spells_learned; } diff --git a/zone/spells.cpp b/zone/spells.cpp index 2ca332100..9efb8736b 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -445,7 +445,7 @@ bool Mob::DoCastSpell(uint16 spell_id, uint16 target_id, CastingSlot slot, ) { cast_failed = false; } - } else if (spell_target->IsRaidGrouped()) { + } else if (spell_target->IsRaidGrouped()) { Raid *target_raid = spell_target->GetRaid(); Raid *my_raid = GetRaid(); if ( @@ -3874,15 +3874,15 @@ bool Mob::SpellOnTarget(uint16 spell_id, Mob *spelltar, int reflect_effectivenes } /* Reflect - base= % Chance to Reflect - Limit= Resist Modifier (+Value for decrease chance to resist) + base= % Chance to Reflect + Limit= Resist Modifier (+Value for decrease chance to resist) Max= % of base spell damage (this is the base before any formula or focus is applied) On live any type of detrimental spell can be reflected as long as the Reflectable spell field is set, this includes AOE. The 'caster' of the reflected spell is owner of the reflect effect. Caster's focus effects are NOT applied to reflected spell. - + reflect_effectiveness is applied to damage spells, a value of 100 is no change to base damage. Other values change by percent. (50=50% of damage) we this variable to both check if a spell being applied is from a reflection and for the damage modifier. - + There are a few spells in database that are not detrimental that have Reflectable field set, however from testing, they do not actually reflect. */ if(spells[spell_id].reflectable && !reflect_effectiveness && spelltar && this != spelltar && IsDetrimentalSpell(spell_id) && @@ -4059,7 +4059,7 @@ bool Mob::SpellOnTarget(uint16 spell_id, Mob *spelltar, int reflect_effectivenes } } } - + entity_list.AddHealAggro( spelltar, this, CheckHealAggroAmount(spell_id, spelltar, (spelltar->GetMaxHP() - spelltar->GetHP()))); @@ -5314,42 +5314,49 @@ int Client::MemmedCount() { } -void Client::ScribeSpell(uint16 spell_id, int slot, bool update_client) +void Client::ScribeSpell(uint16 spell_id, int slot, bool update_client, bool defer_save) { - if(slot >= EQ::spells::SPELLBOOK_SIZE || slot < 0) + if (slot >= EQ::spells::SPELLBOOK_SIZE || slot < 0) { return; + } - if(update_client) - { - if(m_pp.spell_book[slot] != 0xFFFFFFFF) - UnscribeSpell(slot, update_client); + if (update_client) { + if (m_pp.spell_book[slot] != 0xFFFFFFFF) { + UnscribeSpell(slot, update_client, defer_save); + } } m_pp.spell_book[slot] = spell_id; - database.SaveCharacterSpell(this->CharacterID(), spell_id, slot); + + // defer save if we're bulk saving elsewhere + if (!defer_save) { + database.SaveCharacterSpell(this->CharacterID(), spell_id, slot); + } LogSpells("Spell [{}] scribed into spell book slot [{}]", spell_id, slot); - if(update_client) - { + if (update_client) { MemorizeSpell(slot, spell_id, memSpellScribing); } } -void Client::UnscribeSpell(int slot, bool update_client) +void Client::UnscribeSpell(int slot, bool update_client, bool defer_save) { - if(slot >= EQ::spells::SPELLBOOK_SIZE || slot < 0) + if (slot >= EQ::spells::SPELLBOOK_SIZE || slot < 0) { return; + } LogSpells("Spell [{}] erased from spell book slot [{}]", m_pp.spell_book[slot], slot); m_pp.spell_book[slot] = 0xFFFFFFFF; - database.DeleteCharacterSpell(this->CharacterID(), m_pp.spell_book[slot], slot); - if(update_client && slot < EQ::spells::DynamicLookup(ClientVersion(), GetGM())->SpellbookSize) - { - auto outapp = new EQApplicationPacket(OP_DeleteSpell, sizeof(DeleteSpell_Struct)); - DeleteSpell_Struct* del = (DeleteSpell_Struct*)outapp->pBuffer; + if (!defer_save) { + database.DeleteCharacterSpell(this->CharacterID(), m_pp.spell_book[slot], slot); + } + + if (update_client && slot < EQ::spells::DynamicLookup(ClientVersion(), GetGM())->SpellbookSize) { + auto outapp = new EQApplicationPacket(OP_DeleteSpell, sizeof(DeleteSpell_Struct)); + DeleteSpell_Struct *del = (DeleteSpell_Struct *) outapp->pBuffer; del->spell_slot = slot; - del->success = 1; + del->success = 1; QueuePacket(outapp); safe_delete(outapp); } @@ -5357,37 +5364,44 @@ void Client::UnscribeSpell(int slot, bool update_client) void Client::UnscribeSpellAll(bool update_client) { - for(int i = 0; i < EQ::spells::SPELLBOOK_SIZE; i++) - { - if(m_pp.spell_book[i] != 0xFFFFFFFF) - UnscribeSpell(i, update_client); + for (int i = 0; i < EQ::spells::SPELLBOOK_SIZE; i++) { + if (m_pp.spell_book[i] != 0xFFFFFFFF) { + UnscribeSpell(i, update_client, true); + } } + + // bulk save at end (this will only delete) + SaveSpells(); } -void Client::UntrainDisc(int slot, bool update_client) +void Client::UntrainDisc(int slot, bool update_client, bool defer_save) { - if(slot >= MAX_PP_DISCIPLINES || slot < 0) + if (slot >= MAX_PP_DISCIPLINES || slot < 0) { return; + } LogSpells("Discipline [{}] untrained from slot [{}]", m_pp.disciplines.values[slot], slot); m_pp.disciplines.values[slot] = 0; - database.DeleteCharacterDisc(this->CharacterID(), slot); - if(update_client) - { + if (!defer_save) { + database.DeleteCharacterDisc(this->CharacterID(), slot); + } + + if (update_client) { SendDisciplineUpdate(); } } void Client::UntrainDiscAll(bool update_client) { - int i; - - for(i = 0; i < MAX_PP_DISCIPLINES; i++) - { - if(m_pp.disciplines.values[i] != 0) - UntrainDisc(i, update_client); + for (int i = 0; i < MAX_PP_DISCIPLINES; i++) { + if (m_pp.disciplines.values[i] != 0) { + UntrainDisc(i, update_client, true); + } } + + // bulk delete / save + SaveDisciplines(); } void Client::UntrainDiscBySpellID(uint16 spell_id, bool update_client) @@ -6244,3 +6258,4 @@ bool Client::IsLinkedSpellReuseTimerReady(uint32 timer_id) } +