Spell stacking restrictions integrated using linked_timer_id and a new field, type_group_spell_id

Fix #267 - spell stacking restrictions in place

min_class_skill_req is now pulled from the database
sint32 type_group_spell_id added to spells, can use this to distinguish spells of different classes not competing (eg. wards of templar/inquisitor dont stack)
GetSpellData and SetSpellData support min_class_skill_req and type_group_spell_id
This commit is contained in:
Image 2021-02-24 12:15:10 -05:00
parent ba0fbaee7e
commit 4663fa36c9
12 changed files with 204 additions and 6 deletions

View file

@ -0,0 +1 @@
alter table spells add column type_group_spell_id int(10) signed not null default '0';

View file

@ -931,6 +931,61 @@ SpellEffects* Entity::GetSpellEffect(int32 id, Entity* caster) {
return ret;
}
SpellEffects* Entity::GetSpellEffectWithLinkedTimer(int32 id, int32 linked_timer, sint32 type_group_spell_id, Entity* caster) {
SpellEffects* ret = 0;
InfoStruct* info = GetInfoStruct();
MSpellEffects.readlock(__FUNCTION__, __LINE__);
for(int i = 0; i < 45; i++) {
if(info->spell_effects[i].spell_id != 0xFFFFFFFF)
{
if( (info->spell_effects[i].spell_id == id && linked_timer == 0 && type_group_spell_id == 0) ||
(linked_timer > 0 && info->spell_effects[i].spell->spell->GetSpellData()->linked_timer == linked_timer) ||
(type_group_spell_id > 0 && info->spell_effects[i].spell->spell->GetSpellData()->type_group_spell_id == type_group_spell_id))
{
if (type_group_spell_id >= -1 && (!caster || info->spell_effects[i].caster == caster)){
ret = &info->spell_effects[i];
break;
}
}
}
}
MSpellEffects.releasereadlock(__FUNCTION__, __LINE__);
return ret;
}
LuaSpell* Entity::HasLinkedTimerID(LuaSpell* spell, Spawn* target, bool stackWithOtherPlayers) {
if(!spell->spell->GetSpellData()->linked_timer)
return nullptr;
LuaSpell* ret = nullptr;
InfoStruct* info = GetInfoStruct();
MSpellEffects.readlock(__FUNCTION__, __LINE__);
//this for loop primarily handles self checks and 'friendly' checks
for(int i = 0; i < NUM_MAINTAINED_EFFECTS; i++) {
if(info->maintained_effects[i].spell_id != 0xFFFFFFFF)
{
if( ((info->maintained_effects[i].spell_id == spell->spell->GetSpellID() && spell->spell->GetSpellData()->type_group_spell_id >= 0) ||
(info->maintained_effects[i].spell->spell->GetSpellData()->linked_timer > 0 && info->maintained_effects[i].spell->spell->GetSpellData()->linked_timer == spell->spell->GetSpellData()->linked_timer) ||
(spell->spell->GetSpellData()->type_group_spell_id > 0 && spell->spell->GetSpellData()->type_group_spell_id == info->maintained_effects[i].spell->spell->GetSpellData()->type_group_spell_id)) &&
((spell->spell->GetSpellData()->friendly_spell) ||
(!spell->spell->GetSpellData()->friendly_spell && spell->spell->GetSpellData()->type_group_spell_id >= -1 && spell->caster == info->maintained_effects[i].spell->caster) ) &&
(target == nullptr || info->maintained_effects[i].spell->initial_target == target->GetID())) {
ret = info->maintained_effects[i].spell;
break;
}
}
}
MSpellEffects.releasereadlock(__FUNCTION__, __LINE__);
if(!ret && !stackWithOtherPlayers && target && target->IsEntity())
{
SpellEffects* effect = ((Entity*)target)->GetSpellEffectWithLinkedTimer(spell->spell->GetSpellID(), spell->spell->GetSpellData()->linked_timer, spell->spell->GetSpellData()->type_group_spell_id, nullptr);
if(effect)
ret = effect->spell;
}
return ret;
}
InfoStruct* Entity::GetInfoStruct(){
return &info_struct;
}

View file

@ -1157,6 +1157,8 @@ public:
MaintainedEffects* GetFreeMaintainedSpellSlot();
SpellEffects* GetFreeSpellEffectSlot();
SpellEffects* GetSpellEffect(int32 id, Entity* caster = 0);
SpellEffects* GetSpellEffectWithLinkedTimer(int32 id, int32 linked_timer = 0, sint32 type_group_spell_id = 0, Entity* caster = 0);
LuaSpell* HasLinkedTimerID(LuaSpell* spell, Spawn* target = nullptr, bool stackWithOtherPlayers = true);
//flags
int32 GetFlags() { return info_struct.get_flags(); }

View file

@ -2282,6 +2282,7 @@ void Player::AddSpellBookEntry(int32 spell_id, int8 tier, sint32 slot, int32 typ
spell->recast_available = 0;
spell->player = this;
spell->visible = true;
spell->in_use = false;
MSpellsBook.lock();
spells.push_back(spell);
MSpellsBook.unlock();
@ -2600,7 +2601,10 @@ void Player::UnlockAllSpells(bool modify_recast, Spell* exception) {
exception_spell_id = exception->GetSpellID();
MSpellsBook.writelock(__FUNCTION__, __LINE__);
for (itr = spells.begin(); itr != spells.end(); itr++) {
if ((*itr)->spell_id != exception_spell_id && (*itr)->type != SPELL_BOOK_TYPE_TRADESKILL)
if ((*itr)->in_use == false &&
(((*itr)->spell_id != exception_spell_id ||
(*itr)->timer > 0 && (*itr)->timer != exception->GetSpellData()->linked_timer)
&& (*itr)->type != SPELL_BOOK_TYPE_TRADESKILL))
AddSpellStatus((*itr), SPELL_STATUS_LOCK, modify_recast);
}
@ -2613,8 +2617,13 @@ void Player::LockSpell(Spell* spell, int16 recast) {
MSpellsBook.writelock(__FUNCTION__, __LINE__);
for (itr = spells.begin(); itr != spells.end(); itr++) {
spell2 = *itr;
if (spell2->spell_id == spell->GetSpellID() /*|| (spell->GetSpellData()->linked_timer > 0 && spell->GetSpellData()->linked_timer == spell2->timer)*/)
if (spell2->spell_id == spell->GetSpellID() || (spell->GetSpellData()->linked_timer > 0 && spell->GetSpellData()->linked_timer == spell2->timer))
{
spell2->in_use = true;
RemoveSpellStatus(spell2, SPELL_STATUS_LOCK, true, recast);
}
else if(spell2->in_use)
RemoveSpellStatus(spell2, SPELL_STATUS_LOCK, false, 0);
}
MSpellsBook.releasewritelock(__FUNCTION__, __LINE__);
}
@ -2627,8 +2636,11 @@ void Player::UnlockSpell(Spell* spell) {
MSpellsBook.writelock(__FUNCTION__, __LINE__);
for (itr = spells.begin(); itr != spells.end(); itr++) {
spell2 = *itr;
if (spell2->spell_id == spell->GetSpellID() /*|| (spell->GetSpellData()->linked_timer > 0 && spell->GetSpellData()->linked_timer == spell2->timer)*/)
if (spell2->spell_id == spell->GetSpellID() || (spell->GetSpellData()->linked_timer > 0 && spell->GetSpellData()->linked_timer == spell2->timer))
{
spell2->in_use = false;
AddSpellStatus(spell2, SPELL_STATUS_LOCK);
}
}
MSpellsBook.releasewritelock(__FUNCTION__, __LINE__);
}

View file

@ -161,6 +161,7 @@ struct SpellBookEntry{
int16 recast;
int32 timer;
bool save_needed;
bool in_use;
Player* player;
bool visible;
};

View file

@ -336,6 +336,11 @@ void SpellProcess::CheckInterrupt(InterruptStruct* interrupt){
entity->GetZone()->SendInterruptPacket(entity, interrupt->spell);
if(interrupt->error_code > 0)
entity->GetZone()->SendSpellFailedPacket(client, interrupt->error_code);
if(entity->IsPlayer())
{
((Player*)entity)->UnlockSpell(interrupt->spell->spell);
SendSpellBookUpdate(((Player*)entity)->GetClient());
}
}
bool SpellProcess::DeleteCasterSpell(Spawn* caster, Spell* spell, string reason){
@ -377,11 +382,13 @@ bool SpellProcess::DeleteCasterSpell(LuaSpell* spell, string reason){
spell->caster->GetZone()->TriggerCharSheetTimer();
}
}
CheckRecast(spell->spell, spell->caster);
if (spell->caster && spell->caster->IsPlayer())
SendSpellBookUpdate(spell->caster->GetZone()->GetClientBySpawn(spell->caster));
}
if(spell->caster->IsPlayer())
((Player*)spell->caster)->UnlockSpell(spell->spell);
spell->caster->RemoveProc(0, spell);
spell->caster->RemoveMaintainedSpell(spell);
CheckRemoveTargetFromSpell(spell, false);
@ -561,10 +568,15 @@ void SpellProcess::SendFinishedCast(LuaSpell* spell, Client* client){
UnlockAllSpells(client, spell->spell);
else
UnlockAllSpells(client);
if(spell->resisted && spell->spell->GetSpellData()->recast > 0)
CheckRecast(spell->spell, client->GetPlayer(), 0.5); // half sec recast on resisted spells
else if (!spell->interrupted && spell->spell->GetSpellData()->cast_type != SPELL_CAST_TYPE_TOGGLE)
CheckRecast(spell->spell, client->GetPlayer());
else if(spell->caster && spell->caster->IsPlayer())
{
((Player*)spell->caster)->LockSpell(spell->spell, (int16)(spell->spell->GetSpellData()->recast * 10));
}
PacketStruct* packet = configReader.getStruct("WS_FinishCastSpell", client->GetVersion());
if(packet){
packet->setMediumStringByName("spell_name", spell->spell->GetSpellData()->name.data.c_str());
@ -994,6 +1006,41 @@ void SpellProcess::ProcessSpell(ZoneServer* zone, Spell* spell, Entity* caster,
DeleteSpell(lua_spell);
return;
}
int8 spell_type = lua_spell->spell->GetSpellData()->spell_type;
LuaSpell* conflictSpell = caster->HasLinkedTimerID(lua_spell, target, (spell_type == SPELL_TYPE_DD || spell_type == SPELL_TYPE_DOT));
if(conflictSpell)
{
if(conflictSpell->spell->GetSpellData()->min_class_skill_req > 0 && lua_spell->spell->GetSpellData()->min_class_skill_req > 0)
{
if(conflictSpell->spell->GetSpellData()->min_class_skill_req <= lua_spell->spell->GetSpellData()->min_class_skill_req)
{
if(spell->GetSpellData()->friendly_spell)
{
ZoneServer* zone = caster->GetZone();
Spawn* tmpTarget = zone->GetSpawnByID(conflictSpell->initial_target);
if(tmpTarget && tmpTarget->IsEntity())
{
zone->RemoveTargetFromSpell(conflictSpell, tmpTarget);
((Entity*)tmpTarget)->RemoveSpellEffect(conflictSpell);
if(client)
UnlockSpell(client, conflictSpell->spell);
}
return;
}
else if(lua_spell->spell->GetSpellData()->spell_type == SPELL_TYPE_DEBUFF)
{
SpellCannotStack(zone, client, lua_spell->caster, lua_spell, conflictSpell);
return;
}
}
else
{
SpellCannotStack(zone, client, lua_spell->caster, lua_spell, conflictSpell);
return;
}
}
}
if ((caster->IsMezzed() && !spell->CastWhileMezzed()) || (caster->IsStunned() && !spell->CastWhileStunned()))
{
@ -1407,10 +1454,29 @@ bool SpellProcess::CastProcessedSpell(LuaSpell* spell, bool passive, bool in_her
if (spell->spell->GetSpellData()->max_aoe_targets > 0 && spell->targets.size() == 0) {
GetSpellTargetsTrueAOE(spell);
if (spell->targets.size() == 0) {
if(client)
{
client->GetPlayer()->UnlockAllSpells(true);
SendSpellBookUpdate(client);
}
spell->caster->GetZone()->SendSpellFailedPacket(client, SPELL_ERROR_NO_TARGETS_IN_RANGE);
return false;
}
}
if(!spell->spell->GetSpellData()->friendly_spell)
{
ZoneServer* zone = client->GetCurrentZone();
Spawn* tmpTarget = zone->GetSpawnByID(spell->initial_target);
int8 spell_type = spell->spell->GetSpellData()->spell_type;
LuaSpell* conflictSpell = spell->caster->HasLinkedTimerID(spell, tmpTarget, (spell_type == SPELL_TYPE_DD || spell_type == SPELL_TYPE_DOT));
if(conflictSpell && tmpTarget && tmpTarget->IsEntity())
{
((Entity*)tmpTarget)->RemoveSpellEffect(conflictSpell);
zone->RemoveTargetFromSpell(conflictSpell, tmpTarget);
}
}
MutexList<LuaSpell*>::iterator itr = active_spells.begin();
bool processedSpell = false;
LuaSpell* replace_spell = 0;
@ -2552,3 +2618,11 @@ void SpellProcess::DeleteSpell(LuaSpell* spell)
safe_delete(spell);
}
void SpellProcess::SpellCannotStack(ZoneServer* zone, Client* client, Entity* caster, LuaSpell* lua_spell, LuaSpell* conflictSpell)
{
LogWrite(SPELL__DEBUG, 1, "Spell", "%s cannot stack spell %s, conflicts with %s.", caster->GetName(), lua_spell->spell->GetName(), conflictSpell->spell->GetName());
zone->SendSpellFailedPacket(client, SPELL_ERROR_TAKE_EFFECT_MOREPOWERFUL);
lua_spell->caster->GetZone()->GetSpellProcess()->RemoveSpellScriptTimerBySpell(lua_spell);
DeleteSpell(lua_spell);
}

View file

@ -381,6 +381,8 @@ public:
void AddSpellCancel(LuaSpell* spell);
void DeleteSpell(LuaSpell* spell);
void SpellCannotStack(ZoneServer* zone, Client* client, Entity* caster, LuaSpell* lua_spell, LuaSpell* conflictSpell);
private:
/// <summary>Sends the spell data to the lua script</summary>
/// <param name='spell'>LuaSpell to call the lua script for</param>

View file

@ -69,6 +69,7 @@ Spell::Spell(Spell* host_spell)
spell->cast_type = host_spell->GetSpellData()->cast_type;
spell->cast_while_moving = host_spell->GetSpellData()->cast_while_moving;
spell->class_skill = host_spell->GetSpellData()->class_skill;
spell->min_class_skill_req = host_spell->GetSpellData()->min_class_skill_req;
spell->control_effect_type = host_spell->GetSpellData()->control_effect_type;
spell->description = EQ2_16BitString(host_spell->GetSpellData()->description);
spell->det_type = host_spell->GetSpellData()->det_type;
@ -139,6 +140,7 @@ Spell::Spell(Spell* host_spell)
spell->tier = host_spell->GetSpellData()->tier;
spell->ts_loc_index = host_spell->GetSpellData()->ts_loc_index;
spell->type = host_spell->GetSpellData()->type;
spell->type_group_spell_id = host_spell->GetSpellData()->type_group_spell_id;
}
heal_spell = host_spell->IsHealSpell();
@ -726,6 +728,7 @@ void Spell::SetPacketInformation(PacketStruct* packet, Client* client, bool disp
packet->setSubstructDataByName("spell_info", "type", spell->type);
packet->setSubstructDataByName("spell_info", "unknown_MJ1d", 1); //63119 test
packet->setSubstructDataByName("spell_info", "class_skill", spell->class_skill);
packet->setSubstructDataByName("spell_info", "min_class_skill_req", spell->min_class_skill_req);
packet->setSubstructDataByName("spell_info", "mastery_skill", spell->mastery_skill);
packet->setSubstructDataByName("spell_info", "duration_flag", spell->duration_until_cancel);
if (client && spell->type != 2) {
@ -1331,6 +1334,11 @@ bool Spell::GetSpellData(lua_State* state, std::string field)
lua_interface->SetInt32Value(state, GetSpellData()->class_skill);
valSet = true;
}
else if (field == "min_class_skill_req")
{
lua_interface->SetInt32Value(state, GetSpellData()->min_class_skill_req);
valSet = true;
}
else if (field == "mastery_skill")
{
lua_interface->SetInt32Value(state, GetSpellData()->mastery_skill);
@ -1636,6 +1644,11 @@ bool Spell::GetSpellData(lua_State* state, std::string field)
lua_interface->SetSInt32Value(state, GetSpellData()->spell_name_crc);
valSet = true;
}
else if (field == "type_group_spell_id")
{
lua_interface->SetSInt32Value(state, GetSpellData()->type_group_spell_id);
valSet = true;
}
return valSet;
}
@ -1683,6 +1696,12 @@ bool Spell::SetSpellData(lua_State* state, std::string field, int8 fieldArg)
GetSpellData()->class_skill = class_skill;
valSet = true;
}
else if (field == "min_class_skill_req")
{
int16 min_class_skill_req = lua_interface->GetInt16Value(state, fieldArg);
GetSpellData()->min_class_skill_req = min_class_skill_req;
valSet = true;
}
else if (field == "mastery_skill")
{
int32 mastery_skill = lua_interface->GetInt32Value(state, fieldArg);
@ -2031,6 +2050,12 @@ bool Spell::SetSpellData(lua_State* state, std::string field, int8 fieldArg)
GetSpellData()->spell_type = spell_type;
valSet = true;
}
else if (field == "type_group_spell_id")
{
sint32 type_group_spell_id = lua_interface->GetSInt32Value(state, fieldArg);
GetSpellData()->type_group_spell_id = type_group_spell_id;
valSet = true;
}
return valSet;
}

View file

@ -224,6 +224,7 @@ struct SpellData{
int16 icon_backdrop;
int16 type;
int32 class_skill;
int16 min_class_skill_req;
int32 mastery_skill;
int8 ts_loc_index;
int8 num_levels;
@ -286,6 +287,7 @@ struct SpellData{
int32 soe_spell_crc;
int8 spell_type;
int32 spell_name_crc;
sint32 type_group_spell_id;
};
class Spell{
public:

View file

@ -4555,7 +4555,7 @@ void WorldDatabase::LoadSpells()
int32 total = 0;
map<int32, vector<LevelArray*> >* level_data = LoadSpellClasses();
if( !database_new.Select(&result, "SELECT s.`id`, ts.spell_id, ts.index, `name`, `description`, `type`, `class_skill`, `mastery_skill`, `tier`, `is_aa`,`hp_req`, `power_req`,`power_by_level`, `cast_time`, `recast`, `radius`, `max_aoe_targets`, `req_concentration`, `range`, `duration1`, `duration2`, `resistibility`, `hp_upkeep`, `power_upkeep`, `duration_until_cancel`, `target_type`, `recovery`, `power_req_percent`, `hp_req_percent`, `icon`, `icon_heroic_op`, `icon_backdrop`, `success_message`, `fade_message`, `fade_message_others`, `cast_type`, `lua_script`, `call_frequency`, `interruptable`, `spell_visual`, `effect_message`, `min_range`, `can_effect_raid`, `affect_only_group_members`, `hit_bonus`, `display_spell_tier`, `friendly_spell`, `group_spell`, `spell_book_type`, spell_type+0, s.is_active, savagery_req, savagery_req_percent, savagery_upkeep, dissonance_req, dissonance_req_percent, dissonance_upkeep, linked_timer_id, det_type, incurable, control_effect_type, cast_while_moving, casting_flags, persist_through_death, not_maintained, savage_bar, savage_bar_slot, soe_spell_crc, 0xffffffff-CRC32(s.`name`) as 'spell_name_crc' "
if( !database_new.Select(&result, "SELECT s.`id`, ts.spell_id, ts.index, `name`, `description`, `type`, `class_skill`, `min_class_skill_req`, `mastery_skill`, `tier`, `is_aa`,`hp_req`, `power_req`,`power_by_level`, `cast_time`, `recast`, `radius`, `max_aoe_targets`, `req_concentration`, `range`, `duration1`, `duration2`, `resistibility`, `hp_upkeep`, `power_upkeep`, `duration_until_cancel`, `target_type`, `recovery`, `power_req_percent`, `hp_req_percent`, `icon`, `icon_heroic_op`, `icon_backdrop`, `success_message`, `fade_message`, `fade_message_others`, `cast_type`, `lua_script`, `call_frequency`, `interruptable`, `spell_visual`, `effect_message`, `min_range`, `can_effect_raid`, `affect_only_group_members`, `hit_bonus`, `display_spell_tier`, `friendly_spell`, `group_spell`, `spell_book_type`, spell_type+0, s.is_active, savagery_req, savagery_req_percent, savagery_upkeep, dissonance_req, dissonance_req_percent, dissonance_upkeep, linked_timer_id, det_type, incurable, control_effect_type, cast_while_moving, casting_flags, persist_through_death, not_maintained, savage_bar, savage_bar_slot, soe_spell_crc, 0xffffffff-CRC32(s.`name`) as 'spell_name_crc', type_group_spell_id "
"FROM (spells s, spell_tiers st) "
"LEFT JOIN spell_ts_ability_index ts "
"ON s.`id` = ts.spell_id "
@ -4614,8 +4614,8 @@ void WorldDatabase::LoadSpells()
/* Skill Requirements */
data->class_skill = result.GetInt32Str("class_skill");
data->min_class_skill_req = result.GetInt16Str("min_class_skill_req");
data->mastery_skill = result.GetInt32Str("mastery_skill");
// no min_class_skill_req?
/* Cost */
data->req_concentration = result.GetInt16Str("req_concentration");
@ -4653,6 +4653,7 @@ void WorldDatabase::LoadSpells()
data->resistibility = result.GetFloatStr("resistibility");
data->linked_timer = result.GetInt32Str("linked_timer_id");
data->spell_name_crc = result.GetInt32Str("spell_name_crc");
data->type_group_spell_id = result.GetSInt32Str("type_group_spell_id");
/* Cast Messaging */
string message = result.GetStringStr("success_message");

View file

@ -7814,4 +7814,25 @@ void ZoneServer::ProcessSpawnRemovals()
MSpawnList.releasewritelock(__FUNCTION__, __LINE__);
}
MPendingSpawnRemoval.releasewritelock(__FUNCTION__, __LINE__);
}
void ZoneServer::AddSpawnToGroup(Spawn* spawn, int32 group_id)
{
if( spawn->GetSpawnGroupID() > 0 )
spawn->RemoveSpawnFromGroup();
MutexList<int32>* groupList = &spawn_group_map.Get(group_id);
MutexList<int32>::iterator itr2 = groupList->begin();
while(itr2.Next())
{
Spawn* groupSpawn = GetSpawnByID(itr2.value);
if(groupSpawn)
{
// found existing group member to add it in
spawn->AddSpawnToGroup(groupSpawn);
break;
}
}
groupList->Add(spawn->GetID());
spawn->SetSpawnGroupID(group_id);
}

View file

@ -666,6 +666,8 @@ public:
void ProcessSpawnRemovals();
bool SendRemoveSpawn(Client* client, Spawn* spawn, PacketStruct* packet = 0, bool delete_spawn = false);
void AddSpawnToGroup(Spawn* spawn, int32 group_id);
private:
#ifndef WIN32
pthread_t ZoneThread;