diff --git a/Source/Framework/Constants/ItemConst.cs b/Source/Framework/Constants/ItemConst.cs index 5f163b9e0..b21e848a6 100644 --- a/Source/Framework/Constants/ItemConst.cs +++ b/Source/Framework/Constants/ItemConst.cs @@ -105,6 +105,7 @@ namespace Framework.Constants public const byte End = 30; public const byte Start = Profession1Tool; + public const byte MaxCount = Profession2Tool - Profession1Tool; } public struct InventorySlots @@ -886,7 +887,7 @@ namespace Framework.Constants Max = 1 } - public enum ItemSubclassPorfession + public enum ItemSubclassProfession { Blacksmithing = 0, Leatherworking = 1, diff --git a/Source/Framework/Database/Databases/CharacterDatabase.cs b/Source/Framework/Database/Databases/CharacterDatabase.cs index aba8ee7d0..78fe23bf9 100644 --- a/Source/Framework/Database/Databases/CharacterDatabase.cs +++ b/Source/Framework/Database/Databases/CharacterDatabase.cs @@ -124,7 +124,7 @@ namespace Framework.Database PrepareStatement(CharStatements.SEL_CHARACTER_GLYPHS, "SELECT talentGroup, glyphId FROM character_glyphs WHERE guid = ?"); PrepareStatement(CharStatements.SEL_CHARACTER_TALENTS, "SELECT talentId, talentGroup FROM character_talent WHERE guid = ?"); PrepareStatement(CharStatements.SEL_CHARACTER_PVP_TALENTS, "SELECT talentId0, talentId1, talentId2, talentId3, talentGroup FROM character_pvp_talent WHERE guid = ?"); - PrepareStatement(CharStatements.SEL_CHARACTER_SKILLS, "SELECT skill, value, max FROM character_skills WHERE guid = ?"); + PrepareStatement(CharStatements.SEL_CHARACTER_SKILLS, "SELECT skill, value, max, professionSlot FROM character_skills WHERE guid = ?"); PrepareStatement(CharStatements.SEL_CHARACTER_RANDOMBG, "SELECT guid FROM character_battleground_random WHERE guid = ?"); PrepareStatement(CharStatements.SEL_CHARACTER_BANNED, "SELECT guid FROM character_banned WHERE guid = ? AND active = 1"); PrepareStatement(CharStatements.SEL_CHARACTER_QUESTSTATUSREW, "SELECT quest FROM character_queststatus_rewarded WHERE guid = ? AND active = 1"); @@ -611,8 +611,8 @@ namespace Framework.Database PrepareStatement(CharStatements.UPD_CHAR_QUESTSTATUS_REWARDED_ACTIVE_BY_QUEST, "UPDATE character_queststatus_rewarded SET active = 0 WHERE quest = ? AND guid = ?"); PrepareStatement(CharStatements.DEL_INVALID_QUEST_PROGRESS_CRITERIA, "DELETE FROM character_queststatus_objectives_criteria WHERE questObjectiveId = ?"); PrepareStatement(CharStatements.DEL_CHAR_SKILL_BY_SKILL, "DELETE FROM character_skills WHERE guid = ? AND skill = ?"); - PrepareStatement(CharStatements.INS_CHAR_SKILLS, "INSERT INTO character_skills (guid, skill, value, max) VALUES (?, ?, ?, ?)"); - PrepareStatement(CharStatements.UPD_CHAR_SKILLS, "UPDATE character_skills SET value = ?, max = ? WHERE guid = ? AND skill = ?"); + PrepareStatement(CharStatements.INS_CHAR_SKILLS, "INSERT INTO character_skills (guid, skill, value, max, professionSlot) VALUES (?, ?, ?, ?)"); + PrepareStatement(CharStatements.UPD_CHAR_SKILLS, "UPDATE character_skills SET value = ?, max = ?, professionSlot = ? WHERE guid = ? AND skill = ?"); PrepareStatement(CharStatements.INS_CHAR_SPELL, "INSERT INTO character_spell (guid, spell, active, disabled) VALUES (?, ?, ?, ?)"); PrepareStatement(CharStatements.DEL_CHAR_SPELL_FAVORITE, "DELETE FROM character_spell_favorite WHERE guid = ? AND spell = ?"); PrepareStatement(CharStatements.DEL_CHAR_SPELL_FAVORITE_BY_CHAR, "DELETE FROM character_spell_favorite WHERE guid = ?"); diff --git a/Source/Game/Entities/Item/ItemTemplate.cs b/Source/Game/Entities/Item/ItemTemplate.cs index 24c962426..ba2d39ed2 100644 --- a/Source/Game/Entities/Item/ItemTemplate.cs +++ b/Source/Game/Entities/Item/ItemTemplate.cs @@ -63,23 +63,29 @@ namespace Game.Entities return false; } + static SkillType[] item_weapon_skills = + { + SkillType.Axes, SkillType.TwoHandedAxes, SkillType.Bows, SkillType.Guns, SkillType.Maces, + SkillType.TwoHandedMaces, SkillType.Polearms, SkillType.Swords, SkillType.TwoHandedSwords, SkillType.Warglaives, + SkillType.Staves, 0, 0, SkillType.FistWeapons, 0, + SkillType.Daggers, 0, 0, SkillType.Crossbows, SkillType.Wands, + SkillType.Fishing + }; + + static SkillType[] item_armor_skills = + { + 0, SkillType.Cloth, SkillType.Leather, SkillType.Mail, SkillType.PlateMail, 0, SkillType.Shield, 0, 0, 0, 0, 0 + }; + + static SkillType[] itemProfessionSkills = + { + SkillType.Blacksmithing, SkillType.Leatherworking, SkillType.Alchemy, SkillType.Herbalism, SkillType.Cooking, + SkillType.Mining, SkillType.Tailoring, SkillType.Engineering, SkillType.Enchanting, SkillType.Fishing, + SkillType.Skinning, SkillType.Jewelcrafting, SkillType.Inscription, SkillType.Archaeology + }; + public SkillType GetSkill() { - SkillType[] item_weapon_skills = - { - SkillType.Axes, SkillType.TwoHandedAxes, SkillType.Bows, SkillType.Guns, SkillType.Maces, - SkillType.TwoHandedMaces, SkillType.Polearms, SkillType.Swords, SkillType.TwoHandedSwords, SkillType.Warglaives, - SkillType.Staves, 0, 0, SkillType.FistWeapons, 0, - SkillType.Daggers, 0, 0, SkillType.Crossbows, SkillType.Wands, - SkillType.Fishing - }; - - SkillType[] item_armor_skills = - { - 0, SkillType.Cloth, SkillType.Leather, SkillType.Mail, SkillType.PlateMail, 0, SkillType.Shield, 0, 0, 0, 0, 0 - }; - - switch (GetClass()) { case ItemClass.Weapon: @@ -87,13 +93,16 @@ namespace Game.Entities return 0; else return item_weapon_skills[GetSubClass()]; - case ItemClass.Armor: if (GetSubClass() >= (int)ItemSubClassArmor.Max) return 0; else return item_armor_skills[GetSubClass()]; - + case ItemClass.Profession: + if (GetSubClass() >= (int)ItemSubclassProfession.Max) + return 0; + else + return itemProfessionSkills[GetSubClass()]; default: return 0; } diff --git a/Source/Game/Entities/Player/Player.DB.cs b/Source/Game/Entities/Player/Player.DB.cs index 9b0c2a594..3cac76658 100644 --- a/Source/Game/Entities/Player/Player.DB.cs +++ b/Source/Game/Entities/Player/Player.DB.cs @@ -335,6 +335,7 @@ namespace Game.Entities Race race = GetRace(); uint count = 0; Dictionary loadedSkillValues = new(); + List loadedProfessionsWithoutSlot = new(); // fixup old characters if (!result.IsEmpty()) { do @@ -348,6 +349,7 @@ namespace Game.Entities var skill = result.Read(0); var value = result.Read(1); var max = result.Read(2); + var professionSlot = result.Read(3); SkillRaceClassInfoRecord rcEntry = Global.DB2Mgr.GetSkillRaceClassInfo(skill, race, GetClass()); if (rcEntry == null) @@ -391,14 +393,14 @@ namespace Game.Entities if (skillLine.ParentSkillLineID != 0 && skillLine.ParentTierIndex != 0) { - int professionSlot = FindProfessionSlotFor(skill); if (professionSlot != -1) - SetUpdateFieldValue(ref m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.ProfessionSkillLine, (int)professionSlot), skill); + SetUpdateFieldValue(ref m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.ProfessionSkillLine, professionSlot), skill); + else + loadedProfessionsWithoutSlot.Add(skill); } } } - SetSkillLineId(skillStatusData.Pos, skill); SetSkillStep(skillStatusData.Pos, step); SetSkillRank(skillStatusData.Pos, value); @@ -433,9 +435,20 @@ namespace Game.Entities } } + foreach (ushort skill in loadedProfessionsWithoutSlot) + { + int emptyProfessionSlot = FindEmptyProfessionSlotFor(skill); + if (emptyProfessionSlot != -1) + { + SetUpdateFieldValue(ref m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.ProfessionSkillLine, emptyProfessionSlot), skill); + mSkillStatus[skill].State = SkillState.Changed; + } + } + if (HasSkill(SkillType.FistWeapons)) SetSkill(SkillType.FistWeapons, 0, GetSkillValue(SkillType.Unarmed), GetMaxSkillValueForLevel()); } + void _LoadSpells(SQLResult result, SQLResult favoritesResult) { if (!result.IsEmpty()) @@ -1786,6 +1799,7 @@ namespace Game.Entities ushort value = skillInfoField.SkillRank[pair.Value.Pos]; ushort max = skillInfoField.SkillMaxRank[pair.Value.Pos]; + sbyte professionSlot = (sbyte)GetProfessionSlotFor(pair.Key); switch (pair.Value.State) { @@ -1795,14 +1809,16 @@ namespace Game.Entities stmt.AddValue(1, (ushort)pair.Key); stmt.AddValue(2, value); stmt.AddValue(3, max); + stmt.AddValue(4, professionSlot); trans.Append(stmt); break; case SkillState.Changed: stmt = CharacterDatabase.GetPreparedStatement(CharStatements.UPD_CHAR_SKILLS); stmt.AddValue(0, value); stmt.AddValue(1, max); - stmt.AddValue(2, GetGUID().GetCounter()); - stmt.AddValue(3, (ushort)pair.Key); + stmt.AddValue(2, professionSlot); + stmt.AddValue(3, GetGUID().GetCounter()); + stmt.AddValue(4, (ushort)pair.Key); trans.Append(stmt); break; case SkillState.Deleted: diff --git a/Source/Game/Entities/Player/Player.Items.cs b/Source/Game/Entities/Player/Player.Items.cs index ecc6113dc..1314d3119 100644 --- a/Source/Game/Entities/Player/Player.Items.cs +++ b/Source/Game/Entities/Player/Player.Items.cs @@ -4829,6 +4829,65 @@ namespace Game.Entities slots[2] = InventorySlots.BagStart + 2; slots[3] = InventorySlots.BagStart + 3; break; + case InventoryType.ProfessionTool: + case InventoryType.ProfessionGear: + { + bool isProfessionTool = item.GetTemplate().GetInventoryType() == InventoryType.ProfessionTool; + + // Validate item class + if (!(item.GetTemplate().GetClass() == ItemClass.Profession)) + return ItemConst.NullSlot; + + // Check if player has profession skill + uint itemSkill = (uint)item.GetTemplate().GetSkill(); + if (!HasSkill(itemSkill)) + return ItemConst.NullSlot; + + switch ((ItemSubclassProfession)item.GetTemplate().GetSubClass()) + { + case ItemSubclassProfession.Cooking: + slots[0] = isProfessionTool ? ProfessionSlots.CookingTool : ProfessionSlots.CookingGear1; + break; + case ItemSubclassProfession.Fishing: + { + // Fishing doesn't make use of gear slots (clientside) + if (!isProfessionTool) + return ItemConst.NullSlot; + + slots[0] = ProfessionSlots.FishingTool; + break; + } + case ItemSubclassProfession.Blacksmithing : + case ItemSubclassProfession.Leatherworking : + case ItemSubclassProfession.Alchemy : + case ItemSubclassProfession.Herbalism : + case ItemSubclassProfession.Mining : + case ItemSubclassProfession.Tailoring : + case ItemSubclassProfession.Engineering : + case ItemSubclassProfession.Enchanting : + case ItemSubclassProfession.Skinning : + case ItemSubclassProfession.Jewelcrafting : + case ItemSubclassProfession.Inscription : + { + int professionSlot = GetProfessionSlotFor(itemSkill); + if (professionSlot == -1) + return ItemConst.NullSlot; + + if (isProfessionTool) + slots[0] = (byte)(ProfessionSlots.Profession1Tool + professionSlot * ProfessionSlots.MaxCount); + else + { + slots[0] = (byte)(ProfessionSlots.Profession1Gear1 + professionSlot * ProfessionSlots.MaxCount); + slots[0] = (byte)(ProfessionSlots.Profession1Gear2 + professionSlot * ProfessionSlots.MaxCount); + } + + break; + } + default: + return ItemConst.NullSlot; + } + break; + } default: return ItemConst.NullSlot; } @@ -4970,8 +5029,50 @@ namespace Game.Entities if (!swap && GetItemByPos(InventorySlots.Bag0, eslot) != null) return InventoryResult.NoSlotAvailable; + // if we are swapping 2 equiped items, CanEquipUniqueItem check + // should ignore the item we are trying to swap, and not the + // destination item. CanEquipUniqueItem should ignore destination + // item only when we are swapping weapon from bag + byte ignore = ItemConst.NullSlot; + switch (eslot) + { + case EquipmentSlot.MainHand: + ignore = EquipmentSlot.OffHand; + break; + case EquipmentSlot.OffHand: + ignore = EquipmentSlot.MainHand; + break; + case EquipmentSlot.Finger1: + ignore = EquipmentSlot.Finger2; + break; + case EquipmentSlot.Finger2: + ignore = EquipmentSlot.Finger1; + break; + case EquipmentSlot.Trinket1: + ignore = EquipmentSlot.Trinket2; + break; + case EquipmentSlot.Trinket2: + ignore = EquipmentSlot.Trinket1; + break; + case ProfessionSlots.Profession1Gear1: + ignore = ProfessionSlots.Profession1Gear2; + break; + case ProfessionSlots.Profession1Gear2: + ignore = ProfessionSlots.Profession1Gear1; + break; + case ProfessionSlots.Profession2Gear1: + ignore = ProfessionSlots.Profession2Gear2; + break; + case ProfessionSlots.Profession2Gear2: + ignore = ProfessionSlots.Profession2Gear1; + break; + } + + if (ignore == ItemConst.NullSlot || pItem != GetItemByPos(InventorySlots.Bag0, ignore)) + ignore = eslot; + // if swap ignore item (equipped also) - InventoryResult res2 = CanEquipUniqueItem(pItem, swap ? eslot : ItemConst.NullSlot); + InventoryResult res2 = CanEquipUniqueItem(pItem, swap ? ignore : ItemConst.NullSlot); if (res2 != InventoryResult.Ok) return res2; diff --git a/Source/Game/Entities/Player/Player.Spells.cs b/Source/Game/Entities/Player/Player.Spells.cs index b9ca64408..47f71834a 100644 --- a/Source/Game/Entities/Player/Player.Spells.cs +++ b/Source/Game/Entities/Player/Player.Spells.cs @@ -828,6 +828,11 @@ namespace Game.Entities } public void ModifySkillBonus(SkillType skillid, int val, bool talent) + { + ModifySkillBonus((uint)skillid, val, talent); + } + + public void ModifySkillBonus(uint skillid, int val, bool talent) { SkillInfo skillInfoField = m_activePlayerData.Skill; @@ -839,6 +844,12 @@ namespace Game.Entities SetSkillPermBonus(skillStatusData.Pos, (ushort)(skillInfoField.SkillPermBonus[skillStatusData.Pos] + val)); else SetSkillTempBonus(skillStatusData.Pos, (ushort)(skillInfoField.SkillTempBonus[skillStatusData.Pos] + val)); + + // Apply/Remove bonus to child skill lines + var childSkillLines = Global.DB2Mgr.GetSkillLinesForParentSkill(skillid); + if (childSkillLines != null) + foreach (var childSkillLine in childSkillLines) + ModifySkillBonus(childSkillLine.Id, val, talent); } public void StopCastingBindSight() @@ -1029,13 +1040,34 @@ namespace Game.Entities } public void SetSkill(uint id, uint step, uint newVal, uint maxVal) { - if (id == 0) + SkillLineRecord skillEntry = CliDB.SkillLineStorage.LookupByKey(id); + if (skillEntry == null) + { + Log.outError(LogFilter.Misc, $"Player::SetSkill: Skill (SkillID: {id}) not found in SkillLineStore for player '{GetName()}' ({GetGUID()})"); return; + } ushort currVal; var skillStatusData = mSkillStatus.LookupByKey(id); SkillInfo skillInfoField = m_activePlayerData.Skill; + void refreshSkillBonusAuras() + { + // Temporary bonuses + foreach (AuraEffect effect in GetAuraEffectsByType(AuraType.ModSkill)) + if (effect.GetMiscValue() == id) + effect.HandleEffect(this, AuraEffectHandleModes.Skill, true); + + foreach (AuraEffect effect in GetAuraEffectsByType(AuraType.ModSkill2)) + if (effect.GetMiscValue() == id) + effect.HandleEffect(this, AuraEffectHandleModes.Skill, true); + + // Permanent bonuses + foreach (AuraEffect effect in GetAuraEffectsByType(AuraType.ModSkillTalent)) + if (effect.GetMiscValue() == id) + effect.HandleEffect(this, AuraEffectHandleModes.Skill, true); + } + // Handle already stored skills if (skillStatusData != null) { @@ -1067,16 +1099,56 @@ namespace Game.Entities UpdateCriteria(CriteriaType.AchieveSkillStep, id); // update skill state - if (skillStatusData.State == SkillState.Unchanged) + if (skillStatusData.State == SkillState.Unchanged || skillStatusData.State == SkillState.Deleted) { if (currVal == 0) // activated skill, mark as new to save into database + { skillStatusData.State = SkillState.New; + + // Set profession line + int freeProfessionSlot = FindEmptyProfessionSlotFor(id); + if (freeProfessionSlot != -1) + SetUpdateFieldValue(ref m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.ProfessionSkillLine, freeProfessionSlot), id); + + refreshSkillBonusAuras(); + } else // updated skill, mark as changed to save into database skillStatusData.State = SkillState.Changed; } } else if (currVal != 0 && newVal == 0) // Deactivate skill line { + // Try to store profession tools and accessories into the bag + // If we can't, we can't unlearn the profession + int professionSlot = GetProfessionSlotFor(id); + if (professionSlot != -1) + { + byte professionSlotStart = (byte)(ProfessionSlots.Profession1Tool + professionSlot * ProfessionSlots.MaxCount); + + // Get all profession items equipped + for (byte slotOffset = 0; slotOffset < ProfessionSlots.MaxCount; ++slotOffset) + { + Item professionItem = GetItemByPos(InventorySlots.Bag0, (byte)(professionSlotStart + slotOffset)); + if (professionItem != null) + { + // Store item in bag + List professionItemDest = new(); + + if (CanStoreItem(ItemConst.NullBag, ItemConst.NullSlot, professionItemDest, professionItem, false) != InventoryResult.Ok) + { + SendPacket(new DisplayGameError(GameError.InvFull)); + return; + } + + RemoveItem(InventorySlots.Bag0, professionItem.GetSlot(), true); + StoreItem(professionItemDest, professionItem, true); + } + } + + // Clear profession lines + SetUpdateFieldValue(ref m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.ProfessionSkillLine, professionSlot), 0u); + } + //remove enchantments needing this skill UpdateSkillEnchantments(id, currVal, 0); // clear skill fields @@ -1088,10 +1160,8 @@ namespace Game.Entities SetSkillPermBonus(skillStatusData.Pos, 0); // mark as deleted so the next save will delete the data from the database - if (skillStatusData.State != SkillState.New) - skillStatusData.State = SkillState.Deleted; - else - skillStatusData.State = SkillState.Unchanged; + skillStatusData.State = SkillState.Deleted; + // remove all spells that related to this skill List skillLineAbilities = Global.DB2Mgr.GetSkillLineAbilitiesBySkill(id); @@ -1107,12 +1177,6 @@ namespace Game.Entities SetSkill(childSkillLine.Id, 0, 0, 0); } } - - // Clear profession lines - if (m_activePlayerData.ProfessionSkillLine[0] == id) - SetUpdateFieldValue(ref m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.ProfessionSkillLine, 0), 0u); - else if (m_activePlayerData.ProfessionSkillLine[1] == id) - SetUpdateFieldValue(ref m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.ProfessionSkillLine, 1), 0u); } } else @@ -1136,13 +1200,6 @@ namespace Game.Entities return; } - SkillLineRecord skillEntry = CliDB.SkillLineStorage.LookupByKey(id); - if (skillEntry == null) - { - Log.outError(LogFilter.Misc, $"Player.SetSkill: Skill (SkillID: {id}) not found in SkillLineStore for player '{GetName()}' ({GetGUID()})"); - return; - } - if (skillEntry.ParentSkillLineID != 0) { if (skillEntry.ParentTierIndex > 0) @@ -1168,12 +1225,9 @@ namespace Game.Entities if (!HasSkill((SkillType)childSkillLine.Id)) SetSkill(childSkillLine.Id, 0, 0, 0); - if (skillEntry.CategoryID == SkillCategory.Profession) - { - int freeProfessionSlot = FindProfessionSlotFor(id); - if (freeProfessionSlot != -1) - SetUpdateFieldValue(ref m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.ProfessionSkillLine, freeProfessionSlot), id); - } + int freeProfessionSlot = FindEmptyProfessionSlotFor(id); + if (freeProfessionSlot != -1) + SetUpdateFieldValue(ref m_values.ModifyValue(m_activePlayerData).ModifyValue(m_activePlayerData.ProfessionSkillLine, freeProfessionSlot), id); } if (skillStatusData == null) @@ -1194,19 +1248,7 @@ namespace Game.Entities if (newVal != 0) { - // temporary bonuses - foreach (var auraEffect in GetAuraEffectsByType(AuraType.ModSkill)) - if (auraEffect.GetMiscValue() == id) - auraEffect.HandleEffect(this, AuraEffectHandleModes.Skill, true); - - foreach (var auraEffect in GetAuraEffectsByType(AuraType.ModSkill2)) - if (auraEffect.GetMiscValue() == id) - auraEffect.HandleEffect(this, AuraEffectHandleModes.Skill, true); - - // permanent bonuses - foreach (var auraEffect in GetAuraEffectsByType(AuraType.ModSkillTalent)) - if (auraEffect.GetMiscValue() == id) - auraEffect.HandleEffect(this, AuraEffectHandleModes.Skill, true); + refreshSkillBonusAuras(); // Learn all spells for skill LearnSkillRewardedSpells(id, newVal, GetRace()); @@ -1609,12 +1651,24 @@ namespace Game.Entities } } - int FindProfessionSlotFor(uint skillId) + int GetProfessionSlotFor(uint skillId) + { + for (var i = 0; i < m_activePlayerData.ProfessionSkillLine.GetSize(); ++i) + if (m_activePlayerData.ProfessionSkillLine[i] == skillId) + return i; + + return -1; + } + + int FindEmptyProfessionSlotFor(uint skillId) { SkillLineRecord skillEntry = CliDB.SkillLineStorage.LookupByKey(skillId); if (skillEntry == null) return -1; + if (skillEntry.ParentSkillLineID != 0 || skillEntry.CategoryID != SkillCategory.Profession) + return -1; + int index = 0; // if there is no same profession, find any free slot foreach (var b in m_activePlayerData.ProfessionSkillLine)