commit f3e0219cc6e4cd062a31daf9335b2b5574b9fb0c Author: totalllyswede Date: Wed Jul 15 23:00:59 2026 -0700 Add files via upload diff --git a/Bindings.xml b/Bindings.xml new file mode 100644 index 0000000..4b4ff1b --- /dev/null +++ b/Bindings.xml @@ -0,0 +1,3 @@ + + GuildRecipesOcto.tsgui.toggle() + diff --git a/Enchants.lua b/Enchants.lua new file mode 100644 index 0000000..935031d --- /dev/null +++ b/Enchants.lua @@ -0,0 +1,803 @@ +GuildRecipesOcto = GuildRecipesOcto or {} + +GuildRecipesOcto.Enchants = { + [ 7418 ] = { + [ "name" ] = "Enchant Bracer - Minor Health", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7420 ] = { + [ "name" ] = "Enchant Chest - Minor Health", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7421 ] = { + [ "name" ] = "Runed Copper Rod", + [ "icon" ] = "Interface\\Icons\\INV_Staff_Goldfeathered_01", + [ "item" ] = 6218, + }, + [ 7426 ] = { + [ "name" ] = "Enchant Chest - Minor Absorption", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7428 ] = { + [ "name" ] = "Enchant Bracer - Minor Deflection", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7443 ] = { + [ "name" ] = "Enchant Chest - Minor Mana", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7454 ] = { + [ "name" ] = "Enchant Cloak - Minor Resistance", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7457 ] = { + [ "name" ] = "Enchant Bracer - Minor Stamina", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7745 ] = { + [ "name" ] = "Enchant 2H Weapon - Minor Impact", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7748 ] = { + [ "name" ] = "Enchant Chest - Lesser Health", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7766 ] = { + [ "name" ] = "Enchant Bracer - Minor Spirit", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7771 ] = { + [ "name" ] = "Enchant Cloak - Minor Protection", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7776 ] = { + [ "name" ] = "Enchant Chest - Lesser Mana", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7779 ] = { + [ "name" ] = "Enchant Bracer - Minor Agility", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7782 ] = { + [ "name" ] = "Enchant Bracer - Minor Strength", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7786 ] = { + [ "name" ] = "Enchant Weapon - Minor Beastslayer", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7788 ] = { + [ "name" ] = "Enchant Weapon - Minor Striking", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7793 ] = { + [ "name" ] = "Enchant 2H Weapon - Lesser Intellect", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7795 ] = { + [ "name" ] = "Runed Silver Rod", + [ "icon" ] = "Interface\\Icons\\INV_Staff_01", + [ "item" ] = 6339, + }, + [ 7857 ] = { + [ "name" ] = "Enchant Chest - Health", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7859 ] = { + [ "name" ] = "Enchant Bracer - Lesser Spirit", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7861 ] = { + [ "name" ] = "Enchant Cloak - Lesser Fire Resistance", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7863 ] = { + [ "name" ] = "Enchant Boots - Minor Stamina", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 7867 ] = { + [ "name" ] = "Enchant Boots - Minor Agility", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13378 ] = { + [ "name" ] = "Enchant Shield - Minor Stamina", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13380 ] = { + [ "name" ] = "Enchant 2H Weapon - Lesser Spirit", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13419 ] = { + [ "name" ] = "Enchant Cloak - Minor Agility", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13421 ] = { + [ "name" ] = "Enchant Cloak - Lesser Protection", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13464 ] = { + [ "name" ] = "Enchant Shield - Lesser Protection", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13485 ] = { + [ "name" ] = "Enchant Shield - Lesser Spirit", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13501 ] = { + [ "name" ] = "Enchant Bracer - Lesser Stamina", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13503 ] = { + [ "name" ] = "Enchant Weapon - Lesser Striking", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13522 ] = { + [ "name" ] = "Enchant Cloak - Lesser Shadow Resistance", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13529 ] = { + [ "name" ] = "Enchant 2H Weapon - Lesser Impact", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13536 ] = { + [ "name" ] = "Enchant Bracer - Lesser Strength", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13538 ] = { + [ "name" ] = "Enchant Chest - Lesser Absorption", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13607 ] = { + [ "name" ] = "Enchant Chest - Mana", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13612 ] = { + [ "name" ] = "Enchant Gloves - Mining", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13617 ] = { + [ "name" ] = "Enchant Gloves - Herbalism", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13620 ] = { + [ "name" ] = "Enchant Gloves - Fishing", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13622 ] = { + [ "name" ] = "Enchant Bracer - Lesser Intellect", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13626 ] = { + [ "name" ] = "Enchant Chest - Minor Stats", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13628 ] = { + [ "name" ] = "Runed Golden Rod", + [ "icon" ] = "Interface\\Icons\\INV_Staff_10", + [ "item" ] = 11130, + }, + [ 13631 ] = { + [ "name" ] = "Enchant Shield - Lesser Stamina", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13635 ] = { + [ "name" ] = "Enchant Cloak - Defense", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13637 ] = { + [ "name" ] = "Enchant Boots - Lesser Agility", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13640 ] = { + [ "name" ] = "Enchant Chest - Greater Health", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13642 ] = { + [ "name" ] = "Enchant Bracer - Spirit", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13644 ] = { + [ "name" ] = "Enchant Boots - Lesser Stamina", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13646 ] = { + [ "name" ] = "Enchant Bracer - Lesser Deflection", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13648 ] = { + [ "name" ] = "Enchant Bracer - Stamina", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13653 ] = { + [ "name" ] = "Enchant Weapon - Lesser Beastslayer", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13655 ] = { + [ "name" ] = "Enchant Weapon - Lesser Elemental Slayer", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13657 ] = { + [ "name" ] = "Enchant Cloak - Fire Resistance", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13659 ] = { + [ "name" ] = "Enchant Shield - Spirit", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13661 ] = { + [ "name" ] = "Enchant Bracer - Strength", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13663 ] = { + [ "name" ] = "Enchant Chest - Greater Mana", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13687 ] = { + [ "name" ] = "Enchant Boots - Lesser Spirit", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13689 ] = { + [ "name" ] = "Enchant Shield - Lesser Block", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13693 ] = { + [ "name" ] = "Enchant Weapon - Striking", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13695 ] = { + [ "name" ] = "Enchant 2H Weapon - Impact", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13698 ] = { + [ "name" ] = "Enchant Gloves - Skinning", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13700 ] = { + [ "name" ] = "Enchant Chest - Lesser Stats", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13702 ] = { + [ "name" ] = "Runed Truesilver Rod", + [ "icon" ] = "Interface\\Icons\\INV_Staff_11", + [ "item" ] = 11145, + }, + [ 13746 ] = { + [ "name" ] = "Enchant Cloak - Greater Defense", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13794 ] = { + [ "name" ] = "Enchant Cloak - Resistance", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13815 ] = { + [ "name" ] = "Enchant Gloves - Agility", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13817 ] = { + [ "name" ] = "Enchant Shield - Stamina", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13822 ] = { + [ "name" ] = "Enchant Bracer - Intellect", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13836 ] = { + [ "name" ] = "Enchant Boots - Stamina", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13841 ] = { + [ "name" ] = "Enchant Gloves - Advanced Mining", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13846 ] = { + [ "name" ] = "Enchant Bracer - Greater Spirit", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13858 ] = { + [ "name" ] = "Enchant Chest - Superior Health", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13868 ] = { + [ "name" ] = "Enchant Gloves - Advanced Herbalism", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13882 ] = { + [ "name" ] = "Enchant Cloak - Lesser Agility", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13887 ] = { + [ "name" ] = "Enchant Gloves - Strength", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13890 ] = { + [ "name" ] = "Enchant Boots - Minor Speed", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13898 ] = { + [ "name" ] = "Enchant Weapon - Fiery Weapon", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13905 ] = { + [ "name" ] = "Enchant Shield - Greater Spirit", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13915 ] = { + [ "name" ] = "Enchant Weapon - Demonslaying", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13917 ] = { + [ "name" ] = "Enchant Chest - Superior Mana", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13931 ] = { + [ "name" ] = "Enchant Bracer - Deflection", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13933 ] = { + [ "name" ] = "Enchant Shield - Frost Resistance", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13935 ] = { + [ "name" ] = "Enchant Boots - Agility", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13937 ] = { + [ "name" ] = "Enchant 2H Weapon - Greater Impact", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13939 ] = { + [ "name" ] = "Enchant Bracer - Greater Strength", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13941 ] = { + [ "name" ] = "Enchant Chest - Stats", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13943 ] = { + [ "name" ] = "Enchant Weapon - Greater Striking", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13945 ] = { + [ "name" ] = "Enchant Bracer - Greater Stamina", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13947 ] = { + [ "name" ] = "Enchant Gloves - Riding Skill", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 13948 ] = { + [ "name" ] = "Enchant Gloves - Minor Haste", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 14293 ] = { + [ "name" ] = "Lesser Magic Wand", + [ "icon" ] = "Interface\\Icons\\INV_Staff_02", + [ "item" ] = 11287, + }, + [ 14807 ] = { + [ "name" ] = "Greater Magic Wand", + [ "icon" ] = "Interface\\Icons\\INV_Staff_07", + [ "item" ] = 11288, + }, + [ 14809 ] = { + [ "name" ] = "Lesser Mystic Wand", + [ "icon" ] = "Interface\\Icons\\INV_Staff_02", + [ "item" ] = 11289, + }, + [ 14810 ] = { + [ "name" ] = "Greater Mystic Wand", + [ "icon" ] = "Interface\\Icons\\INV_Wand_07", + [ "item" ] = 11290, + }, + [ 15596 ] = { + [ "name" ] = "Smoking Heart of the Mountain", + [ "icon" ] = "Interface\\Icons\\INV_Misc_Gem_Bloodstone_01", + [ "item" ] = 11811, + }, + [ 17180 ] = { + [ "name" ] = "Enchanted Thorium", + [ "icon" ] = "Interface\\Icons\\INV_Ingot_Eternium", + [ "item" ] = 12655, + }, + [ 17181 ] = { + [ "name" ] = "Enchanted Leather", + [ "icon" ] = "Interface\\Icons\\INV_Misc_Rune_05", + [ "item" ] = 12810, + }, + [ 20008 ] = { + [ "name" ] = "Enchant Bracer - Greater Intellect", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20009 ] = { + [ "name" ] = "Enchant Bracer - Superior Spirit", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20010 ] = { + [ "name" ] = "Enchant Bracer - Superior Strength", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20011 ] = { + [ "name" ] = "Enchant Bracer - Superior Stamina", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20012 ] = { + [ "name" ] = "Enchant Gloves - Greater Agility", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20013 ] = { + [ "name" ] = "Enchant Gloves - Greater Strength", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20014 ] = { + [ "name" ] = "Enchant Cloak - Greater Resistance", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20015 ] = { + [ "name" ] = "Enchant Cloak - Superior Defense", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20016 ] = { + [ "name" ] = "Enchant Shield - Superior Spirit", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20017 ] = { + [ "name" ] = "Enchant Shield - Greater Stamina", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20020 ] = { + [ "name" ] = "Enchant Boots - Greater Stamina", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20023 ] = { + [ "name" ] = "Enchant Boots - Greater Agility", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20024 ] = { + [ "name" ] = "Enchant Boots - Spirit", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20025 ] = { + [ "name" ] = "Enchant Chest - Greater Stats", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20026 ] = { + [ "name" ] = "Enchant Chest - Major Health", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20028 ] = { + [ "name" ] = "Enchant Chest - Major Mana", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20029 ] = { + [ "name" ] = "Enchant Weapon - Icy Chill", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20030 ] = { + [ "name" ] = "Enchant 2H Weapon - Superior Impact", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20031 ] = { + [ "name" ] = "Enchant Weapon - Superior Striking", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20032 ] = { + [ "name" ] = "Enchant Weapon - Lifestealing", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20033 ] = { + [ "name" ] = "Enchant Weapon - Unholy Weapon", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20034 ] = { + [ "name" ] = "Enchant Weapon - Crusader", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20035 ] = { + [ "name" ] = "Enchant 2H Weapon - Major Spirit", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20036 ] = { + [ "name" ] = "Enchant 2H Weapon - Major Intellect", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 20051 ] = { + [ "name" ] = "Runed Arcanite Rod", + [ "icon" ] = "Interface\\Icons\\INV_Wand_09", + [ "item" ] = 16207, + }, + [ 21931 ] = { + [ "name" ] = "Enchant Weapon - Winter's Might", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 22749 ] = { + [ "name" ] = "Enchant Weapon - Spell Power", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 22750 ] = { + [ "name" ] = "Enchant Weapon - Healing Power", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 23799 ] = { + [ "name" ] = "Enchant Weapon - Strength", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 23800 ] = { + [ "name" ] = "Enchant Weapon - Agility", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 23801 ] = { + [ "name" ] = "Enchant Bracer - Mana Regeneration", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 23802 ] = { + [ "name" ] = "Enchant Bracer - Healing Power", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 23803 ] = { + [ "name" ] = "Enchant Weapon - Mighty Spirit", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 23804 ] = { + [ "name" ] = "Enchant Weapon - Mighty Intellect", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 25072 ] = { + [ "name" ] = "Enchant Gloves - Threat", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 25073 ] = { + [ "name" ] = "Enchant Gloves - Shadow Power", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 25074 ] = { + [ "name" ] = "Enchant Gloves - Frost Power", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 25078 ] = { + [ "name" ] = "Enchant Gloves - Fire Power", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 25079 ] = { + [ "name" ] = "Enchant Gloves - Healing Power", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 25080 ] = { + [ "name" ] = "Enchant Gloves - Superior Agility", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 25081 ] = { + [ "name" ] = "Enchant Cloak - Greater Fire Resistance", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 25082 ] = { + [ "name" ] = "Enchant Cloak - Greater Nature Resistance", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 25083 ] = { + [ "name" ] = "Enchant Cloak - Stealth", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 25084 ] = { + [ "name" ] = "Enchant Cloak - Subtlety", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 25086 ] = { + [ "name" ] = "Enchant Cloak - Dodge", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 25124 ] = { + [ "name" ] = "Minor Wizard Oil", + [ "icon" ] = "Interface\\Icons\\INV_Poison_MindNumbing", + [ "item" ] = 20744, + [ "extra" ] = "|cffffffff5 Charges", + }, + [ 25125 ] = { + [ "name" ] = "Minor Mana Oil", + [ "icon" ] = "Interface\\Icons\\INV_Potion_98", + [ "item" ] = 20745, + [ "extra" ] = "|cffffffff5 Charges", + }, + [ 25126 ] = { + [ "name" ] = "Lesser Wizard Oil", + [ "icon" ] = "Interface\\Icons\\INV_Potion_103", + [ "item" ] = 20746, + [ "extra" ] = "|cffffffff5 Charges", + }, + [ 25127 ] = { + [ "name" ] = "Lesser Mana Oil", + [ "icon" ] = "Interface\\Icons\\INV_Potion_99", + [ "item" ] = 20747, + [ "extra" ] = "|cffffffff5 Charges", + }, + [ 25128 ] = { + [ "name" ] = "Wizard Oil", + [ "icon" ] = "Interface\\Icons\\INV_Potion_104", + [ "item" ] = 20750, + [ "extra" ] = "|cffffffff5 Charges", + }, + [ 25129 ] = { + [ "name" ] = "Brilliant Wizard Oil", + [ "icon" ] = "Interface\\Icons\\INV_Potion_105", + [ "item" ] = 20749, + [ "extra" ] = "|cffffffff5 Charges", + }, + [ 25130 ] = { + [ "name" ] = "Brilliant Mana Oil", + [ "icon" ] = "Interface\\Icons\\INV_Potion_100", + [ "item" ] = 20748, + [ "extra" ] = "|cffffffff5 Charges", + }, + [ 27837 ] = { + [ "name" ] = "Enchant 2H Weapon - Agility", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 57030 ] = { + [ "name" ] = "Enchant Bracer - Greater Agility", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 57028 ] = { + [ "name" ] = "Enchant Bracer - Spell Power", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 57142 ] = { + [ "name" ] = "Enchant Boots - Greater Spirit", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 57518 ] = { + [ "name" ] = "Eternal Dreamstone Shard", + [ "icon" ] = "Interface\\Icons\\inv_misc_gem_pearl_04", + }, + [ 57127 ] = { + [ "name" ] = "Enchant Boots - Superior Stamina", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 57148 ] = { + [ "name" ] = "Enchant Boots - Vampirism", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 57144 ] = { + [ "name" ] = "Enchant Bracer - Greater Deflection", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 57119 ] = { + [ "name" ] = "Enchant Chest - Mighty Mana", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 57117 ] = { + [ "name" ] = "Enchant Cloak - Greater Arcane Resistance", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 46601 ] = { + [ "name" ] = "Enchant Gloves - Arcane Power", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 46602 ] = { + [ "name" ] = "Enchant Gloves - Nature Power", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 46086 ] = { + [ "name" ] = "Enchant Gloves - Major Strength", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 57146 ] = { + [ "name" ] = "Enchant Bracer - Vampirism", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, + [ 41758 ] = { + [ "name" ] = "Enchanted Gemstone Oil", + [ "icon" ] = "Interface\\Icons\\INV_Potion_14", + }, + [ 45071 ] = { + [ "name" ] = "Enchant 2H Weapon - Minor Intellect", + [ "icon" ] = "Interface\\Icons\\Spell_Holy_GreaterHeal", + }, +} + +GuildRecipesOcto.Reagents = { + -- Dusts + [ "Strange Dust" ] = 10940, + [ "Soul Dust" ] = 11083, + [ "Vision Dust" ] = 11137, + [ "Dream Dust" ] = 11176, + [ "Illusion Dust" ] = 16204, + + -- Essences + [ "Lesser Magic Essence" ] = 10938, + [ "Greater Magic Essence" ] = 10939, + [ "Lesser Astral Essence" ] = 10998, + [ "Greater Astral Essence" ] = 11082, + [ "Lesser Mystic Essence" ] = 11134, + [ "Greater Mystic Essence" ] = 11135, + [ "Lesser Nether Essence" ] = 11174, + [ "Greater Nether Essence" ] = 11175, + [ "Lesser Eternal Essence" ] = 16202, + [ "Greater Eternal Essence" ] = 16203, + [ "Living Essence" ] = 12803, + [ "Essence of Undeath" ] = 12808, + [ "Essence of Earth" ] = 7076, + [ "Essence of Water" ] = 7080, + [ "Essence of Air" ] = 7082, + [ "Arcane Essence" ] = 61673, + + -- Shards + [ "Small Glimmering Shard" ] = 10978, + [ "Large Glimmering Shard" ] = 11084, + [ "Small Glowing Shard" ] = 11138, + [ "Large Glowing Shard" ] = 11139, + [ "Small Radiant Shard" ] = 11177, + [ "Large Radiant Shard" ] = 11178, + [ "Small Brilliant Shard" ] = 14343, + [ "Large Brilliant Shard" ] = 14344, + [ "Nexus Crystal" ] = 20725, + + -- Rods + [ "Copper Rod" ] = 6217, + [ "Silver Rod" ] = 6338, + [ "Golden Rod" ] = 11128, + [ "Truesilver Rod" ] = 11144, + [ "Arcanite Rod" ] = 16206, + + [ "Runed Copper Rod" ] = 6218, + [ "Runed Silver Rod" ] = 6339, + [ "Runed Golden Rod" ] = 11130, + [ "Runed Truesilver Rod" ] = 11145, + [ "Runed Arcanite Rod" ] = 16207, + + -- Vials + [ "Empty Vial" ] = 3371, + [ "Imbued Vial" ] = 18256, + [ "Crystal Vial" ] = 3371, + [ "Leaded Vial" ] = 3372, + + -- Gemstones + [ "Aquamarine" ] = 7909, + [ "Star Ruby" ] = 7910, + [ "Large Opal" ] = 12799, + [ "Azerothian Diamond" ] = 12800, + [ "Blue Sapphire" ] = 12361, + [ "Righteous Orb" ] = 12811, + [ "Arcane Crystal" ] = 12363, + [ "Shadowgem" ] = 1210, + + -- Flowers + [ "Purple Lotus" ] = 8831, + [ "Wildvine" ] = 8153, + [ "Firebloom" ] = 4625, + [ "Wintersbite" ] = 3819, + [ "Icecap" ] = 13467, + [ "Sungrass" ] = 8838, + + [ "Stranglethorn Seed" ] = 44614, + [ "Maple Seed" ] = 17034, + + -- Misc + [ "Elixir of Demonslaying" ] = 9224, + [ "Green Whelp Scale" ] = 7392, + [ "Rugged Leather" ] = 8170, + [ "Frost Oil" ] = 3829, + [ "Blackmouth Oil" ] = 6370, + [ "Fire Oil" ] = 6371, + [ "Blood of the Mountain" ] = 11382, + [ "Elemental Earth" ] = 7067, + [ "Elemental Fire" ] = 7068, + [ "Core of Earth" ] = 7075, + [ "Heart of Fire" ] = 7077, + [ "Essence of Fire" ] = 7078, + [ "Globe of Water" ] = 7079, + [ "Breath of Wind" ] = 7081, + [ "Ichor of Undeath" ] = 7972, + [ "Large Fang" ] = 5637, + [ "Thorium Bar" ] = 12359, + [ "Truesilver Bar" ] = 6037, + [ "Star Wood" ] = 11291, + [ "Simple Wood" ] = 4470, + [ "Iron Ore" ] = 2772, + [ "Shadow Protection Potion" ] = 13459, + [ "Guardian Stone" ] = 12809, + + -- Pearls + [ "Black Pearl" ] = 7971, + [ "Golden Pearl" ] = 13926, + [ "Iridescent Pearl" ] = 5500, + [ "Small Lustrous Pearl" ] = 5498, +} diff --git a/FrameBuilder.lua b/FrameBuilder.lua new file mode 100644 index 0000000..f82bcf6 --- /dev/null +++ b/FrameBuilder.lua @@ -0,0 +1,332 @@ +GuildRecipesOcto = GuildRecipesOcto or {} + +---@class GuildRecipesOcto +local m = GuildRecipesOcto + +if m.FrameBuilder then return end + +local M = {} + +---@alias FrameStyle +---| "TOOLTIP" +---| "NONE" + +---@class FrameBuilder +---@field name fun( self: FrameBuilder, name: string ): FrameBuilder +---@field type fun( self: FrameBuilder, type: FrameType ): FrameBuilder +---@field title fun( self: FrameBuilder, title: string ): FrameBuilder +---@field parent fun( self: FrameBuilder, parent: Frame ): FrameBuilder +---@field point fun( self: FrameBuilder, point: FramePoint, relative_region: string|Region|nil, relative_point: FramePoint, offset_x: number?, offset_y: number?) +---@field width fun( self: FrameBuilder, width: number ): FrameBuilder +---@field height fun( self: FrameBuilder, height: number ): FrameBuilder +---@field frame_level fun( self: FrameBuilder, frame_level: number ): FrameBuilder +---@field frame_style fun( self: FrameBuilder, frame_style: FrameStyle ): FrameBuilder +---@field strata fun( self: FrameBuilder, strata: FrameStrata ): FrameBuilder +---@field movable fun( self: FrameBuilder ): FrameBuilder +---@field backdrop fun( self: FrameBuilder, backdrop: Backdrop ): FrameBuilder +---@field backdrop_color fun( self: FrameBuilder, r: number, g: number, b: number, a: number ): FrameBuilder +---@field border_color fun( self: FrameBuilder, r: number, g: number, b: number, a: number ): FrameBuilder +---@field esc fun( self: FrameBuilder ): FrameBuilder +---@field close_button fun( self: FrameBuilder ): FrameBuilder +---@field on_drag_stop fun( self: FrameBuilder, callback: function ): FrameBuilder +---@field hidden fun( self: FrameBuilder ): FrameBuilder +---@field build fun( self: FrameBuilder ): Frame + +---@class FrameBuilderFactory +---@field new fun(): FrameBuilder + +---@return FrameBuilder +function M.new() + local options = { + backdrop = {} + } + local is_dragging + + local function create_frame() + ---@param parent Frame + ---@param title string + ---@return Frame + local function create_titlebar( parent, title ) + local frame = CreateFrame( "Frame", nil, parent ) + frame:SetPoint( "TopLeft", parent, "TopLeft", 5, -5 ) + frame:SetPoint( "BottomRight", parent, "TopRight", -5, -24 ) + frame:SetBackdrop( { bgFile = "Interface/Buttons/WHITE8x8" } ) + frame:SetBackdropColor( 0, 0, 0, 1 ) + + if options.frame_level then + frame:SetFrameLevel( options.frame_level ) + end + + local bottom_border = frame:CreateTexture( nil, "ARTWORK" ) + bottom_border:SetTexture( .6, .6, .6, 1 ) + bottom_border:SetPoint( "TopLeft", frame, "BottomLeft", -1, 1 ) + bottom_border:SetPoint( "BottomRight", frame, "BottomRight", 1, 0 ) + + + if options.close_button then + local btn_close = m.GuiElements.tiny_button( parent, "X", "Close Window" ) + btn_close:SetPoint( "TopRight", parent, "TopRight", -4, -4 ) + btn_close:SetScript( "OnClick", function() parent:Hide() end ) + + if options.frame_level then + btn_close:SetFrameLevel( options.frame_level + 1 ) + end + end + + local title_label = frame:CreateFontString( nil, "ARTWORK", "GRFontNormal" ) + title_label:SetPoint( "CENTER", frame, "CENTER", 0, 0 ) + title_label:SetTextColor( 1, 1, 1 ) + title_label:SetJustifyH( "CENTER" ) + title_label:SetText( title ) + + parent.title_label = title_label + + return frame + end + + local function create_main_frame() + local type = options.type or "Frame" + local parent = options.parent or UIParent + + local frame = CreateFrame( type, options.name, parent ) + + frame:SetWidth( options.width or 280 ) + frame:SetHeight( options.height or 100 ) + frame:EnableMouse( true ) + + if options.frame_style == "TOOLTIP" then + frame:SetBackdrop( { + bgFile = options.backdrop.bgFile or "Interface/Tooltips/UI-Tooltip-Background", + edgeFile = options.backdrop.edgeFile or "Interface/Tooltips/UI-Tooltip-Border", + tile = true, + tileSize = 16, + edgeSize = options.backdrop.edgeSize or 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 } + } ) + elseif options.frame_style == "NONE" then + if options.backdrop then + frame:SetBackdrop( { + bgFile = options.backdrop.bgFile, + edgeFile = options.backdrop.edgeFile, + tile = options.backdrop.tile or false, + tileSize = options.backdrop.tileSize, + edgeSize = options.backdrop.edgeSize, + insets = options.backdrop.insets + } ) + end + else + frame:SetBackdrop( { + bgFile = options.backdrop.bgFile or "Interface/Buttons/WHITE8x8", + edgeFile = options.backdrop.edgeFile or "Interface/Buttons/WHITE8x8", + tile = options.backdrop.tile or true, + tileSize = options.backdrop.tileSize or 0, + edgeSize = options.backdrop.edgeSize or 0.8, + insets = options.backdrop.insets or { left = 0, right = 0, top = 0, bottom = 0 } + } ) + end + + if options.points then + for _, p in pairs( options.points ) do + frame:SetPoint( p.point, p.relative_region or UIParent, p.relative_point, p.x, p.y ) + end + else + frame:SetPoint( "TopLeft", UIParent, "Center", -(options.width or 200) / 2, (options.height or 200) / 2 ) + end + + if options.backdrop_color then + local c = options.backdrop_color + frame:SetBackdropColor( c.r, c.g, c.b, c.a or 1 ) + else + frame:SetBackdropColor( 0, 0, 0, 0.7 ) + end + + if options.border_color then + local c = options.border_color + frame:SetBackdropBorderColor( c.r, c.g, c.b, options.frame_style == "Classic" and 1 or c.a ) + end + + if options.title then + create_titlebar( frame, options.title ) + end + + if options.hidden then + frame:Hide() + end + + if options.frame_level then + frame:SetFrameLevel( options.frame_level ) + end + + if options.strata then + frame:SetFrameStrata( options.strata ) + else + frame:SetFrameStrata( "DIALOG" ) + end + + if options.movable then + frame:SetMovable( true ) + frame:RegisterForDrag( "LeftButton" ) + + frame:SetScript( "OnDragStart", function() + if not frame:IsMovable() then return end + is_dragging = true + this:StartMoving() + end ) + + frame:SetScript( "OnDragStop", function() + if not frame:IsMovable() then return end + is_dragging = false + frame:StopMovingOrSizing() + + if options.on_drag_stop then + options.on_drag_stop( frame ) + end + end ) + else + frame:SetMovable( false ) + end + + if options.esc then + table.insert( UISpecialFrames, frame:GetName() ) + end + + if options.scale then + frame:SetScale( options.scale ) + end + + return frame + end + + local frame = create_main_frame() + return frame + end + + + local function name( self, v ) + options.name = v + return self + end + + local function type( self, v ) + options.type = v + return self + end + + local function title( self, v ) + options.title = v + return self + end + + local function parent( self, v ) + options.parent = v + return self + end + + local function point( self, _point, relative_region, relative_point, offset_x, offset_y ) + options.points = options.points or {} + table.insert( options.points, { + point = _point, + relative_region = relative_region, + relative_point = relative_point, + x = offset_x, + y = offset_y + } ) + return self + end + + local function width( self, v ) + options.width = v + return self + end + + local function height( self, v ) + options.height = v + return self + end + + local function frame_style( self, v ) + options.frame_style = v + return self + end + + local function frame_level( self, v ) + options.frame_level = v + return self + end + + local function strata( self, v ) + options.strata = v + return self + end + + local function movable( self ) + options.movable = true + return self + end + + local function backdrop( self, _backdrop ) + options.backdrop = _backdrop + return self + end + + local function backdrop_color( self, r, g, b, a ) + options.backdrop_color = { r = r, g = g, b = b, a = a } + return self + end + + local function border_color( self, r, g, b, a ) + options.border_color = { r = r, g = g, b = b, a = a } + return self + end + + local function esc( self ) + options.esc = true + return self + end + + local function close_button( self ) + options.close_button = true + return self + end + + local function on_drag_stop( self, callback ) + options.on_drag_stop = callback + return self + end + + local function hidden( self ) + options.hidden = true + return self + end + + local function build() + return create_frame() + end + + ---@type FrameBuilder + return { + name = name, + type = type, + title = title, + parent = parent, + point = point, + width = width, + height = height, + frame_level = frame_level, + frame_style = frame_style, + strata = strata, + movable = movable, + backdrop = backdrop, + backdrop_color = backdrop_color, + border_color = border_color, + esc = esc, + close_button = close_button, + on_drag_stop = on_drag_stop, + hidden = hidden, + build = build + } +end + +m.FrameBuilder = M + +return M diff --git a/GuiElements.lua b/GuiElements.lua new file mode 100644 index 0000000..1fabba7 --- /dev/null +++ b/GuiElements.lua @@ -0,0 +1,171 @@ +GuildRecipesOcto = GuildRecipesOcto or {} +---@class GuildRecipesOcto +local m = GuildRecipesOcto + +if m.GuiElements then return end + +---@class GuiElements +---@field create_text_in_container fun( parent: Frame, text: string?, type: FrameType?, font_type: string?): any +---@field tiny_button fun( parent: Frame, text: string?, tooltip: string?, color: table|string?, font-size: number? ): TinyButton +---@field create_button fun( parent: Frame, title: string, width: integer?, onclick: function, on_receive_drag: function? ): MyButton +local M = {} + + +M.font_normal = CreateFont( "GRFontNormal" ) +M.font_normal:SetFont( "Interface\\AddOns\\GuildRecipes_Octo\\assets\\Myriad-Pro.ttf", 12, "" ) + +M.font_highlight = CreateFont( "GRFontHighlight" ) +M.font_highlight:SetFont( "Interface\\AddOns\\GuildRecipes_Octo\\assets\\Myriad-Pro.ttf", 12, "" ) +M.font_highlight:SetTextColor( HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b ) + +M.font_normal_small = CreateFont( "GRFontNormalSmall" ) +M.font_normal_small:SetFont( "Interface\\AddOns\\GuildRecipes_Octo\\assets\\Myriad-Pro.ttf", 11, "" ) + +M.font_highlight_small = CreateFont( "GRFontHighlightSmall" ) +M.font_highlight_small:SetFont( "Interface\\AddOns\\GuildRecipes_Octo\\assets\\Myriad-Pro.ttf", 11, "" ) +M.font_highlight_small:SetTextColor( HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b ) + +function M.create_text_in_container( parent, text, type, font_type ) + ---@class TextFrame: Frame + local frame = CreateFrame( type or "Frame", nil, parent ) + + local label = frame:CreateFontString( nil, "ARTWORK", font_type or "GRFontNormal" ) + label:SetPoint( "Left", frame, "Left", 0, 0 ) + label:SetJustifyH( "Left" ) + + if text then label:SetText( text ) end + + frame.label = label + + frame.SetText = function( self, str ) + label:SetText( str ) + self:SetWidth( label:GetStringWidth() ) + end + + return frame +end + +---@param parent Frame +---@param text string? +---@param tooltip string? +---@param color string|table? +---@param font_size number? +---@return TinyButton +function M.tiny_button( parent, text, tooltip, color, font_size ) + ---@class TinyButton: Button + local button = CreateFrame( "Button", nil, parent ) + button.active = false + if not text then text = 'X' end + + if type( color ) == "string" and color ~= "" then + local str_color = color + color = {} + color.r, color.g, color.b, color.a = m.hex_to_rgba( str_color ) + end + + + if not color then color = { r = .9, g = .8, b = .25 } end + button:SetWidth( 18 ) + button:SetHeight( 18 ) + + button:SetHighlightTexture( "Interface\\Buttons\\UI-Panel-MinimizeButton-Highlight" ) + if text == 'X' then + button:SetNormalTexture( "Interface\\Buttons\\UI-Panel-MinimizeButton-Up" ) + button:SetPushedTexture( "Interface\\Buttons\\UI-Panel-MinimizeButton-Down" ) + else + button:SetNormalTexture( "Interface\\AddOns\\GuildRecipes_Octo\\assets\\tiny-button-up.tga" ) + button:SetPushedTexture( "Interface\\AddOns\\GuildRecipes_Octo\\assets\\tiny-button-down.tga" ) + end + button:GetHighlightTexture():SetTexCoord( .1875, .78125, .21875, .78125 ) + button:GetNormalTexture():SetTexCoord( .1875, .78125, .21875, .78125 ) + button:GetPushedTexture():SetTexCoord( .1875, .78125, .21875, .78125 ) + + if text ~= 'X' then + local font_x, font_y + + button:SetText( text ) + button:SetPushedTextOffset( -1.5, -1.5 ) + + if string.upper( text ) == text then + font_x, font_y = 0, 0 + font_size = font_size or 13 + else + font_x, font_y = -1, 2 + font_size = font_size or 15 + end + + button:GetFontString():SetFont( "FONTS\\FRIZQT__.TTF", font_size, "" ) + button:GetFontString():SetTextColor( color.r, color.g, color.b, color.a or 1 ) + button:GetFontString():SetPoint( "Center", button, "Center", font_x, font_y ) + end + + button:SetScript( "OnEnter", function() + local self = button + self:SetBackdropBorderColor( color.r, color.g, color.b, color.a or 1 ) + if tooltip then + GameTooltip:SetOwner( button, "ANCHOR_RIGHT" ) + GameTooltip:SetText( tooltip ) + GameTooltip:SetScale( 0.8 ) + GameTooltip:Show() + end + end ) + + button:SetScript( "OnLeave", function() + if tooltip and GameTooltip:IsVisible() then + GameTooltip:SetScale( 1 ) + GameTooltip:Hide() + end + end ) + + return button +end + +---@param parent Frame +---@param title string +---@param width integer? +---@param on_click function +---@param on_receive_drag function? +---@return MyButton +function M.create_button( parent, title, width, on_click, on_receive_drag ) + ---@class MyButton: Button + local btn = CreateFrame( "Button", nil, parent, title == "Cancel" and nil or "UIPanelButtonTemplate" ) + btn:SetScript( "OnClick", on_click ) + btn:SetScript( "OnReceiveDrag", on_receive_drag ) + btn:SetTextFontObject( GRFontNormalSmall ) + btn:SetHighlightFontObject( GRFontHighlightSmall ) + + if title == "Cancel" then + btn:SetNormalTexture( "Interface\\Buttons\\CancelButton-Up" ) + btn:GetNormalTexture():SetTexCoord( 0, 1, 0, 1 ) + btn:SetPushedTexture( "Interface\\Buttons\\CancelButton-Down" ) + btn:GetPushedTexture():SetTexCoord( 0, 1, 0, 1 ) + btn:SetHighlightTexture( "Interface\\Buttons\\CancelButton-Highlight" ) + btn:GetHighlightTexture():SetTexCoord( 0, 1, 0, 1 ) + btn:GetHighlightTexture():SetBlendMode( "ADD" ) + btn:SetHitRectInsets( 9, 7, 7, 10 ) + btn:SetWidth( 34 ) + btn:SetHeight( 34 ) + else + btn:SetWidth( width and width or 100 ) + btn:SetHeight( 24 ) + btn:SetText( title ) + end + + btn.Disable = function() + btn:EnableMouse( false ) + btn:GetFontString():SetTextColor( 0.5, 0.41, 0 ) + btn:GetNormalTexture():SetVertexColor( 0.5, 0.5, 0.5 ) + end + + btn.Enable = function() + btn:EnableMouse( true ) + btn:GetFontString():SetTextColor( 1, 0.82, 0 ) + btn:GetNormalTexture():SetVertexColor( 1, 1, 1 ) + end + + return btn +end + +m.GuiElements = M + +return M diff --git a/GuildRecipesOcto.lua b/GuildRecipesOcto.lua new file mode 100644 index 0000000..008b270 --- /dev/null +++ b/GuildRecipesOcto.lua @@ -0,0 +1,281 @@ +---@class GuildRecipesOcto +GuildRecipesOcto = GuildRecipesOcto or {} + +---@class GuildRecipesOcto +local m = GuildRecipesOcto + +---@diagnostic disable-next-line: undefined-global +local lib_stub = LibStub + +GuildRecipesOcto.name = "GuildRecipes_Octo" +GuildRecipesOcto.prefix = "GROCTO" +GuildRecipesOcto.tagcolor = "FFDD7744" +GuildRecipesOcto.events = {} +GuildRecipesOcto.debug_enabled = false +GuildRecipesOcto.legacy_warning_enabled = false + +BINDING_HEADER_GUILDRECIPESOCTO = "GuildRecipes Octo" + +---@class Item +---@field id integer +---@field name string +---@field quality integer? +---@field icon string? + +---@alias NotAceTimer any +---@alias TimerId number + +---@class AceTimer +---@field ScheduleTimer fun( self: NotAceTimer, callback: function, delay: number, ... ): TimerId +---@field ScheduleRepeatingTimer fun( self: NotAceTimer, callback: function, delay: number, arg: any ): TimerId +---@field CancelTimer fun( self: NotAceTimer, timer_id: number ) +---@field TimeLeft fun( self: NotAceTimer, timer_id: number ) + +---@class AceSerializer +---@field Serialize fun( self: any, ... ): string +---@field Deserialize fun( self: any, str: string ): any + +---@class AceComm +---@field RegisterComm fun( self: any, prefix: string, method: function? ) +---@field SendCommMessage fun( self: any, prefix: string, text: string, distribution: string, target: string?, prio: "BULK"|"NORMAL"|"ALERT"?, callbackFn: function?, callbackArg: any? ) + +function GuildRecipesOcto:init() + self.frame = CreateFrame( "Frame" ) + self.frame:SetScript( "OnEvent", function() + if m.events[ event ] then + m.events[ event ]() + end + end ) + + for k, _ in pairs( m.events ) do + m.frame:RegisterEvent( k ) + end +end + +function GuildRecipesOcto.events.ADDON_LOADED() + if arg1 == m.name then + ---@type AceTimer + m.ace_timer = lib_stub( "AceTimer-3.0" ) + + ---@type AceSerializer + m.ace_serializer = lib_stub( "AceSerializer-3.0" ) + + ---@type AceComm + m.ace_comm = lib_stub( "AceComm-3.0" ) + + ---@type MessageHandler + m.msg = m.MessageHandler.new( m.ace_timer, m.ace_serializer, m.ace_comm ) + + ---@type TradeskillGui + m.tsgui = m.Tradeskills.new() + + ---@type SlashCommand + m.slash_command = m.SlashCommand.new( m.name, { "gro", "GuildRecipesOcto" } ) + + m.version = GetAddOnMetadata( m.name, "Version" ) + m.info( string.format( "(v%s) Loaded", m.version ) ) + end +end + +function GuildRecipesOcto.events.PLAYER_LOGIN() + -- Initialize DB + GuildRecipesOctoDB = GuildRecipesOctoDB or {} + m.db = GuildRecipesOctoDB + m.db.players = m.db.players or {} + m.db.tradeskills = m.db.tradeskills or {} + m.db.tradeskills_last_update = m.db.tradeskills_last_update or {} + m.db.frame_tradeskills = m.db.frame_tradeskills or {} + + m.player = UnitName( "player" ) + m.player_class = UnitClass( "player" ) + if m.slash_command then + m.slash_command.init() + end + + m.tooltip = CreateFrame( "GameTooltip", "GuildRecipesOctoTooltip", nil, "GameTooltipTemplate" ) + m.tooltip:SetOwner( WorldFrame, "ANCHOR_NONE" ) + + if m.msg then + m.update_data() + end +end + +function GuildRecipesOcto.events.TRADE_SKILL_SHOW() + local reverse = m.build_reverse_trade_map( GetLocale() ) + local tradeskill = reverse[ GetTradeSkillLine() ] + + if m.TRADE_SKILL_LOCALIZATION[ tradeskill ] then + local num = GetNumTradeSkills() + + for i = 1, GetNumTradeSkills() do + local _, type = GetTradeSkillInfo( i ) + if type == "header" then + num = num - 1 + end + end + + m.db.tradeskills[ tradeskill ] = m.db.tradeskills[ tradeskill ] or {} + + if m.count_recipes( m.db.tradeskills[ tradeskill ], m.player ) ~= num then + for i = 1, GetNumTradeSkills() do + local _, type = GetTradeSkillInfo( i ) + if type ~= "header" then + local id, name, quality = m.parse_item_link( GetTradeSkillItemLink( i ) ) + local item = { + id = id, + name = name, + quality = quality + } + m.update_tradeskill_item( tradeskill, item, { m.player } ) + end + end + + m.db.tradeskills_last_update[ tradeskill ] = m.get_server_timestamp() + m.msg.send_tradeskill( tradeskill ) + end + end +end + +function GuildRecipesOcto.events.CRAFT_SHOW() + local reverse = m.build_reverse_trade_map( GetLocale() ) + local tradeskill = reverse[ GetCraftName() ] + + if tradeskill == "Enchanting" then + local num = GetNumCrafts() + + m.db.tradeskills[ tradeskill ] = m.db.tradeskills[ tradeskill ] or {} + + if m.count_recipes( m.db.tradeskills[ tradeskill ], m.player ) ~= num then + for i = 1, num do + local id, name, quality = m.parse_item_link( GetCraftItemLink( i ) ) + local item = { + id = id, + name = name, + quality = quality + } + m.update_tradeskill_item( tradeskill, item, { m.player } ) + end + + m.db.tradeskills_last_update[ tradeskill ] = m.get_server_timestamp() + m.msg.send_tradeskill( tradeskill ) + end + end +end + +function GuildRecipesOcto.events.UNIT_INVENTORY_CHANGED() + -- This can fire many times in quick succession (e.g. once per item when + -- looting a stack), and each call to tsgui.update() re-scans the guild + -- roster and redraws the visible list. Debounce so a burst of these only + -- triggers one actual update, instead of one per event. + if arg1 == "player" and m.tsgui.is_visible() and m.ace_timer then + if m.inventory_update_timer then + m.ace_timer.CancelTimer( m, m.inventory_update_timer ) + end + m.inventory_update_timer = m.ace_timer.ScheduleTimer( m, function() + m.inventory_update_timer = nil + if m.tsgui.is_visible() then + m.tsgui.update() + end + end, 0.5 ) + end +end + +---@param tradeskill string +---@param item Item +---@param players string[] +function GuildRecipesOcto.update_tradeskill_item( tradeskill, item, players ) + if item.id then + local player_ids = {} + for _, p in pairs( players ) do + if not m.find( p, m.db.players ) then + table.insert( m.db.players, p ) + end + + local _, player_id = m.find( p, m.db.players ) + if player_id then + table.insert( player_ids, player_id ) + end + end + + if m.db.tradeskills[ tradeskill ][ item.id ] then + m.debug( string.format( "Updating %s: %s", tradeskill, item.name ) ) + if not players then + m.debug( "ERROR, no players for: " .. tostring( item.name ) ) + return + end + + local recipe_players = m.comma_separated_to_table( m.db.tradeskills[ tradeskill ][ item.id ].p ) + for _, p in pairs( players ) do + local _, player_id = m.find( p, m.db.players ) + + if player_id and not m.find( tostring(player_id), recipe_players ) then + table.insert( recipe_players, player_id ) + end + end + m.db.tradeskills[ tradeskill ][ item.id ].p = m.table_to_comma_separated( recipe_players ) + else + m.debug( string.format( "Adding %s: %s", tradeskill, item.name ) ) + + m.db.tradeskills[ tradeskill ][ item.id ] = { + n = item.name, + q = item.quality, + p = m.table_to_comma_separated( player_ids ) + } + end + end +end + +function GuildRecipesOcto.update_data() + -- If we haven't updated in the last 48 hours, request an update + if next(m.db.tradeskills_last_update) == nil then + m.msg.request_tradeskills() + else + local now = m.get_server_timestamp() + for tradeskill, last_update in pairs( m.db.tradeskills_last_update ) do + if now >= last_update + 172800 then + m.msg.request_tradeskill( tradeskill ) + break + end + end + end +end + +---@param bag_start integer +---@param bag_end integer +---@param name string +---@return integer +function GuildRecipesOcto.find_item_count_bag( bag_start, bag_end, name ) + local count = 0 + for bag = bag_start, bag_end do + local slots = GetContainerNumSlots( bag ) + for slot = 1, slots do + local _, item_count = GetContainerItemInfo( bag, slot ) + if item_count and item_count > 0 then + local _, item_name = m.parse_item_link( GetContainerItemLink( bag, slot ) ) + if item_name == name then + count = count + item_count + end + end + end + end + return count +end + +function GuildRecipesOcto.fix() + for tradeskill, recipes in pairs( m.db.tradeskills ) do + for id, recipe in pairs( recipes ) do + if recipe.p and type( recipe.p ) == "table" then + local players = "" + for _, p in pairs( recipe.p ) do + if players ~= "" then + players = players .. "," + end + players = players .. p + end + recipe.p = players + end + end + end +end + +GuildRecipesOcto:init() diff --git a/GuildRecipes_Octo.toc b/GuildRecipes_Octo.toc new file mode 100644 index 0000000..1c525f7 --- /dev/null +++ b/GuildRecipes_Octo.toc @@ -0,0 +1,19 @@ +## Interface: 11200 +## Title: GuildRecipes Octo +## Author: Olzon +## Version: 2.0 +## Notes: Share and view tradeskill recipes with your guild +## SavedVariablesPerCharacter: GuildRecipesOctoDB + +libs\LibStub\LibStub.lua +libs\Libs.xml + +SlashCommand.lua +MessageHandler.lua +Utils.lua +FrameBuilder.lua +GuiElements.lua +TradeskillRecipesGui.lua +Enchants.lua +GuildRecipesOcto.lua +Bindings.xml diff --git a/MessageHandler.lua b/MessageHandler.lua new file mode 100644 index 0000000..442156e --- /dev/null +++ b/MessageHandler.lua @@ -0,0 +1,318 @@ +GuildRecipesOcto = GuildRecipesOcto or {} + +---@class GuildRecipesOcto +local m = GuildRecipesOcto + +if m.MessageHandler then return end + +---@type MessageCommand +local MessageCommand = { + RequestTradeskill = "RTS", + Tradeskill = "TS", + Ping = "PING", + Pong = "PONG", + VersionCheck = "VERSIONCHECK", + Version = "VERSION", +} + +---@alias MessageCommand +---| "RTS" +---| "TS" +---| "PING" +---| "PONG" +---| "VERSIONCHECK" +---| "VERSION" + +---@class MessageHandler +---@field send_tradeskill fun( tradeskill: string ) +---@field request_tradeskill fun( tradeskill: string ) +---@field request_tradeskills fun() +---@field version_check fun() + +local M = {} + +---@param ace_timer AceTimer +---@param ace_serializer AceSerializer +---@param ace_comm AceComm +function M.new( ace_timer, ace_serializer, ace_comm ) + local pinging = false + local best_ping = {} + local var_names = { + n = "name", + q = "quality", + c = "count", + p = "players", + d = "data", + i = "items", + ts = "tradeskill", + h = "hashes", + lu = "last_update", + --tlu = "tradeskills_last_update", + Alc = "Alchemy", + Bla = "Blacksmithing", + Eng = "Engineering", + Enc = "Enchanting", + Lea = "Leatherworking", + Tai = "Tailoring", + Jew = "Jewelcrafting", + + } + setmetatable( var_names, { __index = function( _, key ) return key end } ); + + ---@param t table + local function decode( t ) + local l = {} + for key, value in pairs( t ) do + if type( value ) == "table" then + value = decode( value ) + end + + l[ var_names[ key ] ] = value + end + return l + end + + ---@param command MessageCommand + ---@param data table? + local function broadcast( command, data ) + m.debug( string.format( "Broadcasting %s", command ) ) + + ace_comm:SendCommMessage( m.prefix, command .. "::" .. ace_serializer.Serialize( M, data ), "GUILD", nil, "NORMAL" ) + end + + local function send_tradeskill( tradeskill ) + m.debug( string.format( "Sending %s", tradeskill ) ) + + if not m.db.tradeskills[ tradeskill ] then + m.debug( string.format( "No data for tradeskill: %s", tradeskill ) ) + return + end + + local data = { + tradeskill = tradeskill, + recipes = {} + } + + for id, item in pairs( m.db.tradeskills[ tradeskill ] ) do + local players = {} + for _, p in pairs( m.comma_separated_to_table( item.p ) ) do + table.insert( players, m.db.players[ tonumber( p ) ] ) + end + + table.insert( data.recipes, { + id = id, + p = players + } ) + end + + broadcast( MessageCommand.Tradeskill, data ) + end + + local function send_tradeskills() + for tradeskill in pairs( m.db.tradeskills ) do + send_tradeskill( tradeskill ) + end + end + + local function request_tradeskill( tradeskill ) + pinging = true + best_ping = {} + + broadcast( MessageCommand.Ping, { + [string.sub( tradeskill, 1, 3 )] = m.tradeskill_hash( tradeskill ) + } ) + end + + local function request_tradeskills() + pinging = true + best_ping = {} + local hashes = {} + + for tradeskill in pairs(m.TRADE_SKILL_LOCALIZATION) do + hashes[ string.sub( tradeskill, 1, 3 ) ] = m.tradeskill_hash( tradeskill ) + end + + broadcast( MessageCommand.Ping, hashes ) + end + + local function version_check() + broadcast( MessageCommand.VersionCheck ) + end + + ---@param command string + ---@param data table + ---@param sender string + local function on_command( command, data, sender ) + if command == MessageCommand.Tradeskill then + -- + -- Receive tradeskill + -- + m.debug( string.format( "Receiving %s from %s.", data.tradeskill, sender ) ) + local tradeskill = data.tradeskill + m.db.tradeskills[ tradeskill ] = m.db.tradeskills[ tradeskill ] or {} + + for _, v in pairs( data.recipes ) do + if v.id then + ---@type Item + local item = nil + if m.db.tradeskills[ tradeskill ][ v.id ] then + item = { + id = v.id, + name = m.db.tradeskills[ tradeskill ][ v.id ].n, + quality = m.db.tradeskills[ tradeskill ][ v.id ].q + } + else + if tradeskill == "Enchanting" then + if v and v.id then + local name = m.Enchants[ v.id ] and m.Enchants[ v.id ].name + if not name then + m.error( string.format( "Unknown enchantment received (%d)", v.id ) ) + else + item = { + id = v.id, + name = name, + } + end + else + m.debug( "empty enchant data??" ) + end + else + m.debug( "Fetching item info for " .. tostring( v.id ) ) + m.get_item_info( v.id, function( item_info, players ) + if item_info then + local i = { + id = item_info.id, + name = item_info.name, + quality = item_info.quality + } + + m.update_tradeskill_item( tradeskill, i, players ) + else + m.debug( "No item_info for " .. tostring( v.id ) ) + end + end, v.players ) + end + end + + if item then + --m.debug( "got item" ) + m.update_tradeskill_item( tradeskill, item, v.players ) + end + end + end + + m.db.tradeskills_last_update[ tradeskill ] = m.get_server_timestamp() + elseif command == MessageCommand.RequestTradeskill and data.player == m.player then + -- + -- Request for tradeskill + -- + m.debug(m.dump(data)) + send_tradeskill( data.tradeskill ) + elseif command == MessageCommand.Ping and sender ~= m.player then + -- + -- Recive ping + -- + for tradeskill, hash in pairs( data or {} ) do + local local_hash = m.tradeskill_hash( tradeskill ) + if local_hash == hash then + data[ tradeskill ] = nil + else + data[ tradeskill ] = m.db.tradeskills_last_update[ tradeskill ] or 0 + end + end + + m.debug( m.dump( data ) ) + broadcast( MessageCommand.Pong, data ) + elseif command == MessageCommand.Pong and pinging then + -- + -- Receive pong + -- + m.debug( m.dump( data ) ) + for tradeskill, last_update in pairs( data or {} ) do + if not best_ping[ tradeskill ] or (last_update > best_ping[ tradeskill ].last_update) then + best_ping[ tradeskill ] = { + player = sender, + last_update = last_update + } + end + end + + if ace_timer:TimeLeft( M[ "ping_timer" ] ) == 0 then + M[ "ping_timer" ] = ace_timer.ScheduleTimer( M, function() + if pinging then + m.debug( "Ping timeout, requesting tradeskills from best pings." ) + pinging = false + for tradeskill, ping_info in pairs( best_ping ) do + m.debug( string.format( "Requesting %s from %s (last update: %d)", tradeskill, ping_info.player, ping_info.last_update ) ) + broadcast( MessageCommand.RequestTradeskill, { + player = ping_info.player, + ts = tradeskill + } ) + end + end + end, 2 ) + end + elseif command == MessageCommand.VersionCheck then + -- + -- Receive version request + -- + broadcast( MessageCommand.Version, { requester = sender, version = m.version, class = m.player_class } ) + elseif command == MessageCommand.Version then + -- + -- Receive version + -- + if data.requester == m.player then + m.info( string.format( "%s [v%s]", m.colorize_player_by_class( sender, data.class ), data.version ), true ) + end + end + end + + local function on_comm_received( prefix, data_str, _, sender ) + if prefix ~= m.prefix or sender == m.player then return end + + local command = string.match( data_str, "^(.-)::" ) + data_str = string.gsub( data_str, "^.-::", "" ) + + m.debug( "Received " .. command ) + + local success, data = ace_serializer.Deserialize( M, data_str ) + if success then + if data then + data = decode( data ) + end + + on_command( command, data, sender ) + else + m.error( "Corrupt data in addon message!" ) + end + end + + ace_comm.RegisterComm( M, m.prefix, on_comm_received ) + + -- Also listen on the original GuildRecipes prefix. If we hear another + -- player broadcasting on it, they're running the outdated addon -- send + -- them a one-time whisper per session letting them know it's been replaced. + local warned_players = {} + local function on_legacy_comm_received( prefix, data_str, _, sender ) + if sender == m.player then return end + if not m.legacy_warning_enabled then return end + if warned_players[ sender ] then return end + warned_players[ sender ] = true + SendChatMessage( + "GuildRecipes is out of date and will not work correctly with OctoWoW, please check the Discord for replacement version.", + "WHISPER", nil, sender + ) + end + ace_comm.RegisterComm( M, "GRECIPES", on_legacy_comm_received ) + + ---@type MessageHandler + return { + send_tradeskill = send_tradeskill, + request_tradeskill = request_tradeskill, + request_tradeskills = request_tradeskills, + version_check = version_check + } +end + +m.MessageHandler = M +return M diff --git a/README.md b/README.md new file mode 100644 index 0000000..016f615 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# GuildRecipes Octo + +**Version:** 2.0 +**Author:** Olzon +**Game Version:** OctoWoW (1.12.1) + +A standalone replacement for GuildRecipes, built specifically for OctoWoW. Lets guild members share and browse each other's tradeskill recipes in-game, with automatic syncing whenever players come online. + +Uses its own saved variables and communication channel — will **not** sync with players running the original GuildRecipes addon. Players still on the old addon will receive a whisper letting them know (if you have that option enabled). + +--- + +## Installation + +### Option A — GitAddonsManager (recommended) + +GitAddonsManager is a free tool that installs and updates addons directly from GitHub with one click. It's the easiest way to stay up to date. + +1. Download GitAddonsManager: + - **Windows:** https://gitlab.com/woblight/GitAddonsManager/-/jobs/artifacts/master/download?job=Win64 + - **Linux:** https://woblight.gitlab.io/overview/gitaddonsmanager/ +2. Open GitAddonsManager and point it at your OctoWoW `Interface\AddOns\` folder if it doesn't detect it automatically. +3. Click **Add addon** and paste in the repository URL: + ``` + https://github.com/totalllyswede/GuildRecipes_Octo + ``` +4. Click **Download/Update**. GitAddonsManager will clone the addon into your AddOns folder automatically. +5. To update in the future, just open GitAddonsManager and click **Update all**. + +### Option B — Manual install + +1. Download the latest release as a ZIP from the repository. +2. Extract the `GuildRecipes_Octo` folder into your `Interface\AddOns\` directory, so the path looks like: + ``` + Interface\AddOns\GuildRecipes_Octo\GuildRecipes_Octo.toc + ``` +3. Launch OctoWoW (or `/reload` if already in-game). + +> **Note:** The `assets` folder (fonts and button textures) must be present inside `GuildRecipes_Octo\assets\`. It is included in the repository and will be present if you use GitAddonsManager or download the full ZIP. + +--- + +## Getting started + +The addon syncs automatically. When you log in, it requests the latest recipe data from online guild members. When you open a tradeskill or enchanting window, your recipes are shared with the guild. + +Open the recipe browser with `/gro toggle` or bind a key under **Key Bindings → GuildRecipes Octo**. + +--- + +## Slash Commands + +| Command | Description | +|---|---| +| `/gro` | Show help | +| `/gro toggle` | Toggle the recipe window open/closed | +| `/gro show` / `/gro hide` | Show or hide the recipe window | +| `/gro refresh` | Request updated tradeskills from online guild members | +| `/gro players` | List all players with synced tradeskill data | +| `/gro remove_player ` | Manually remove a player's data | +| `/gro clear` | Clear all synced tradeskill data | +| `/gro versioncheck` | See which version of the addon guild members are running | +| `/gro legacywarning` | Toggle whisper warnings to players on the old addon (default: off) | +| `/gro debug` | Toggle debug output | + +--- + +## Notes + +- **Data is per-character.** Each character maintains its own synced database. +- **Syncing is passive.** No manual action is needed — the addon handles it automatically when you log in and open tradeskill windows. +- **Legacy warning** (`/gro legacywarning`) is off by default. When enabled, anyone in the guild still running the original GuildRecipes addon will receive a one-time whisper per session letting them know it has been replaced. diff --git a/SlashCommand.lua b/SlashCommand.lua new file mode 100644 index 0000000..bd8c7aa --- /dev/null +++ b/SlashCommand.lua @@ -0,0 +1,216 @@ +GuildRecipesOcto = GuildRecipesOcto or {} + +---@class GuildRecipesOcto +local m = GuildRecipesOcto + +if m.SlashCommand then return end + +---@class SlashCommand +---@field register fun( command: string|string[], func: fun( args: string[] ) ) +---@field init fun() +local M = {} + +---@param name string +---@param slash_commands string|string[] +function M.new( name, slash_commands ) + local _G = getfenv() + local commands = {} + + ---@param command string + ---@return boolean + local function has_command( command ) + for k, _ in pairs( commands ) do + if k == command then return true end + end + return false + end + + ---@param command string + ---@return fun(args: string[]) + local function get_command( command ) + local cmd = commands[ command ] + if cmd then return cmd else return commands[ "__DEFAULT__" ] end + end + + ---@param command string + ---@param args string[] + local function handle_command( command, args ) + local cmd = get_command( command ) + if cmd then + cmd( args ) + else + m.info( string.format( "%q is not a valid command.", command ) ) + end + end + + if type( slash_commands ) == "string" then + slash_commands = { slash_commands } + end + + for i, v in ipairs( slash_commands ) do + _G[ "SLASH_" .. string.upper( name ) .. i ] = "/" .. v + end + + SlashCmdList[ string.upper( name ) ] = function( msg ) + local args = {} + local t = {} + + msg = string.gsub( msg, "^%s*(.-)%s*$", "%1" ) + for part in string.gmatch( msg, "%S+" ) do + table.insert( args, part ) + end + + local command = args[ 1 ] + if getn( args ) > 1 then + for i = 2, getn( args ) do + table.insert( t, args[ i ] ) + end + end + + handle_command( command, t ) + end + + ---@param command string|string[] + ---@param func fun(args: string[]) + local function register( command, func ) + if type( command ) == "string" then + command = { command } + end + for _, v in pairs( command ) do + if not has_command( v ) then + if v ~= "__DEFAULT__" then v = string.lower( v ) end + commands[ v ] = func + end + end + end + + local function init() + register( "__DEFAULT__", function() + DEFAULT_CHAT_FRAME:AddMessage( string.format( "|c%s%s Help|r", m.tagcolor, m.name ) ) + DEFAULT_CHAT_FRAME:AddMessage( "|c" .. + m.tagcolor .. "/gro toggle|r|||c" .. m.tagcolor .. "show|r|||c" .. m.tagcolor .. "hide|r Toggle/show/hide guild recipes" ) + DEFAULT_CHAT_FRAME:AddMessage( "|c" .. m.tagcolor .. "/gro remove_player|r <|cffaaaaaaPlayer|r> Remove player" ) + DEFAULT_CHAT_FRAME:AddMessage( "|c" .. m.tagcolor .. "/gro clear|r Clears all tradeskills" ) + DEFAULT_CHAT_FRAME:AddMessage( "|c" .. m.tagcolor .. "/gro refresh|r Request updated tradeskills from other players" ) + DEFAULT_CHAT_FRAME:AddMessage( "|c" .. m.tagcolor .. "/gro players|r List players with synced tradeskill data" ) + DEFAULT_CHAT_FRAME:AddMessage( "|c" .. m.tagcolor .. "/gro legacywarning|r Toggle whisper warnings to players using the old addon (default: off)" ) + end ) + + register( { "toggle", "t" }, function() + m.tsgui.toggle() + end ) + + register( { "show", "s" }, function() + m.tsgui.show() + end ) + + register( { "hide", "s" }, function() + m.tsgui.hide() + end ) + + register( { "refresh", "r" }, function() + m.msg.request_tradeskills() + end ) + + register( { "players", "p" }, function() + if not m.db.players or not next( m.db.players ) then + m.info( "No players synced yet.", true ) + return + end + + m.info( "Players with synced tradeskill data:", true ) + local online_roster = m.build_online_roster() + for _, player in pairs( m.db.players ) do + local tradeskills = {} + for ts, recipes in pairs( m.db.tradeskills ) do + if m.count_recipes( recipes, player ) > 0 then + table.insert( tradeskills, ts ) + end + end + local online = online_roster[ player ] and "|cff00ff00online|r" or "|cffaaaaaaoffline|r" + local ts_list = getn( tradeskills ) > 0 and table.concat( tradeskills, ", " ) or "none" + m.info( string.format( "%s [%s] - %s", player, online, ts_list ), true ) + end + end ) + + register( { "clear", "c" }, function() + m.db.tradeskills = {} + m.db.tradeskills_last_update = {} + m.db.players = {} + m.info( "Cleared all tradeskill data.", true ) + end ) + + register( { "remove_player", "rp" }, function( args ) + if args and args[ 1 ] then + local player = args[ 1 ] + local _, player_id = m.find( player, m.db.players ) + local found = false + + if player_id then + for ts, tradeskill in pairs( m.db.tradeskills ) do + for id, recipe in pairs( tradeskill ) do + local players = m.comma_separated_to_table( recipe.p ) + local _, idx = m.find( tostring( player_id ), players ) + if idx then + found = true + table.remove( players, idx ) + recipe.p = m.table_to_comma_separated( players ) + if getn( players ) == 0 then + -- No players left for this recipe, remove it + tradeskill[ id ] = nil + end + end + end + if found then + -- If we removed any recipes, send updated tradeskill + m.msg.send_tradeskill( ts ) + end + if not next( tradeskill ) then + -- No recipes left for this tradeskill, remove it + m.db.tradeskills[ ts ] = nil + end + end + -- Remove player from player list + m.db.players[ player_id ] = nil + end + if found then + m.info( string.format( "Removed all recipes for %q", player ), true ) + else + m.info( string.format( "No recipes found for %q", player ), true ) + end + else + m.info( "You must provide a player name.", true ) + end + end ) + + register( { "versioncheck", "vc" }, function() + m.msg.version_check() + end ) + + register( "debug", function() + m.debug_enabled = not m.debug_enabled + if m.debug_enabled then + m.info( "Debug is enabled", true ) + else + m.info( "Debug is disabled", true ) + end + end ) + + register( { "legacywarning", "lw" }, function() + m.legacy_warning_enabled = not m.legacy_warning_enabled + if m.legacy_warning_enabled then + m.info( "Legacy warning whispers enabled. Players using the old GuildRecipes addon will be notified.", true ) + else + m.info( "Legacy warning whispers disabled.", true ) + end + end ) + end + + return { + register = register, + init = init + } +end + +m.SlashCommand = M +return M diff --git a/TradeskillRecipesGui.lua b/TradeskillRecipesGui.lua new file mode 100644 index 0000000..3349243 --- /dev/null +++ b/TradeskillRecipesGui.lua @@ -0,0 +1,760 @@ +GuildRecipesOcto = GuildRecipesOcto or {} + +---@class GuildRecipesOcto +local m = GuildRecipesOcto + +if m.Tradeskills then return end + +---@class TradeskillGui +---@field show fun( tab: string? ) +---@field hide fun() +---@field toggle fun() +---@field is_visible fun(): boolean +---@field update fun() + +local M = {} + +local _G = getfenv() + +---@return TradeskillGui +---@nodiscard +function M.new() + local popup + local selected_tradeskill = "All tradeskills" + local selected + local offset = 0 + local frame_items = {} + local search_result = {} + + local function save_position( self ) + local point, _, relative_point, x, y = self:GetPoint() + + m.db.frame_tradeskills.position = { + point = point, + relative_point = relative_point, + x = x, + y = y + } + end + + local function refresh() + for i = 1, 12 do + if search_result[ i + offset ] then + frame_items[ i ].set_item( search_result[ i + offset ] ) + frame_items[ i ].set_selected( selected == i + offset ) + else + frame_items[ i ]:Hide() + end + end + + local max = math.max( 0, getn( search_result ) - 12 ) + local value = math.min( max, popup.scroll_bar:GetValue() ) + + if value == 0 then + _G[ "GuildTradeskillsOctoScrollBarScrollUpButton" ]:Disable() + else + _G[ "GuildTradeskillsOctoScrollBarScrollUpButton" ]:Enable() + end + + if value == max then + _G[ "GuildTradeskillsOctoScrollBarScrollDownButton" ]:Disable() + else + _G[ "GuildTradeskillsOctoScrollBarScrollDownButton" ]:Enable() + end + end + + local function initialize_dropdown_skill() + local info = {} + + info.text = "All tradeskills" + info.value = info.text + info.arg1 = info.text + info.notCheckable = true + info.func = function( value ) + UIDropDownMenu_SetText( value, popup.dropdown_skill ) + selected_tradeskill = value + end + UIDropDownMenu_AddButton( info ) + + for key, opt in pairs( m.TRADE_SKILL_LOCALIZATION ) do + info.text = opt.enUS + info.value = key + info.arg1 = key + UIDropDownMenu_AddButton( info ) + end + end + + local function do_search( search_str ) + search_result = {} + + local function search( skill ) + if m.db.tradeskills[ skill ] then + for id, item in pairs( m.db.tradeskills[ skill ] ) do + if item.n and string.find( string.upper( item.n ), string.upper( search_str ), nil, true ) then + table.insert( search_result, { + id = id, + name = item.n, + players = item.p, + quality = item.q, + skill = skill + } ) + end + end + end + end + + if not selected_tradeskill or selected_tradeskill == "All tradeskills" then + for key in pairs( m.db.tradeskills ) do + search( key ) + end + else + search( selected_tradeskill ) + end + + table.sort( search_result, function( a, b ) + return a.name < b.name + end ) + + local max = math.max( 0, getn( search_result ) - 12 ) + popup.scroll_bar:SetMinMaxValues( 0, max ) + popup.scroll_bar:SetValue( 0 ) + + refresh() + end + + local function show_recipe( item, index ) + if selected == index then + popup.crafters.clear() + popup.info.clear() + selected = nil + return + else + popup.crafters.set( item ) + selected = index + end + + if item.skill == "Enchanting" then + item.link = m.make_enchant_link( item.id, item.name ) + popup.info.set( item ) + refresh() + return + end + + if GetSpellInfoAtlasLootDB then + local recipe = m.find( item.id, GetSpellInfoAtlasLootDB[ "craftspells" ], "craftItem" ) + + if recipe then + recipe.link = m.make_item_link( item.id, item.name, item.quality ) + popup.info.set( recipe ) + else + popup.info.clear( item.name .. " was not found in AtlasLoot database." ) + end + else + popup.info.clear( "AtlasLoot is required to view recipes." ) + end + + refresh() + end + + local function create_item( parent, index ) + local frame = m.FrameBuilder.new() + :type( "Button" ) + :parent( parent ) + :width( 382 ) + :height( 16 ) + :frame_style( "NONE" ) + :build() + + frame.slot_index = index + frame:Hide() + frame:SetHighlightTexture( "Interface\\QuestFrame\\UI-QuestTitleHighlight" ) + frame:SetScript( "OnMouseUp", function() + show_recipe( frame.item, frame.slot_index + offset ) + end ) + + local selected_tex = frame:CreateTexture( nil, "BACKGROUND" ) + selected_tex:SetTexture( "Interface\\QuestFrame\\UI-QuestLogTitleHighlight" ) + selected_tex:SetAllPoints( frame ) + selected_tex:SetVertexColor( 0.3, 0.3, 1, 1 ) + selected_tex:Hide() + + local text_item = m.GuiElements.create_text_in_container( frame, nil, "Button" ) + text_item:SetPoint( "Left", frame, "Left", 5, 0 ) + text_item:SetHeight( 16 ) + text_item:EnableMouse( true ) + + text_item:SetScript( "OnEnter", function() + GameTooltip:SetOwner( this, "ANCHOR_RIGHT" ) + GameTooltip:SetHyperlink( string.format( "%s:%d", frame.item.skill == "Enchanting" and "enchant" or "item", frame.item.id ) ) + frame:LockHighlight() + end ) + + text_item:SetScript( "OnLeave", function() + GameTooltip:Hide() + frame:UnlockHighlight() + end ) + + text_item:SetScript( "OnClick", function() + if IsShiftKeyDown() and frame.item then + if ChatFrameEditBox:IsVisible() then + if frame.item.skill == "Enchanting" then + ChatFrameEditBox:Insert( m.make_enchant_link( frame.item.id, frame.item.name ) ) + else + ChatFrameEditBox:Insert( m.make_item_link( frame.item.id, frame.item.name, frame.item.quality ) ) + end + return + end + end + show_recipe( frame.item, frame.slot_index + offset ) + end ) + + local text_tradeskill = frame:CreateFontString( nil, "ARTWORK", "GRFontNormal" ) + text_tradeskill:SetPoint( "Right", frame, "Right", -5, 0 ) + text_tradeskill:SetHeight( 16 ) + text_tradeskill:SetJustifyH( "Right" ) + + frame.set_selected = function( select ) + if select then + selected_tex:Show() + else + selected_tex:Hide() + end + end + + frame.set_item = function( item ) + frame.item = item + if item.skill == "Enchanting" then + text_item:SetText( string.format( "|cFF%s%s|r", "80B0FF", item.name ) ) + else + text_item:SetText( m.get_item_name_colorized( item ) ) + end + + if not selected_tradeskill or selected_tradeskill == "All tradeskills" then + text_tradeskill:SetText( item.skill ) + else + text_tradeskill:SetText( "" ) + end + + frame:Show() + end + + return frame + end + + local function create_reagent( parent, index ) + local frame = CreateFrame( "Button", "GuildTradeskillsOctoReagent" .. index, parent, "QuestItemTemplate" ) + local x = mod( index, 3 ) == 0 and 320 or (mod( index, 3 ) - 1) * 160 + local y = -5 - ((math.ceil( index / 3 ) - 1) * 52) + + frame:SetScale( 0.8 ) + frame:SetPoint( "TopLeft", parent.label_reagents, "BottomLeft", x, y ) + frame:Hide() + frame.reagent_id = nil + + frame:SetScript( "OnEnter", function() + if frame.reagent_id then + GameTooltip:SetOwner( this, "ANCHOR_LEFT" ) + GameTooltip:SetHyperlink( string.format( "item:%d", frame.reagent_id ) ) + end + end ) + frame:SetScript( "OnLeave", function() + GameTooltip:Hide() + end ) + + return frame + end + + local function create_crafters_frame( parent ) + local frame = m.FrameBuilder.new() + :parent( parent ) + :point( "TopLeft", parent.border_results, "BottomLeft", 0, -5 ) + :point( "Right", parent.btn_search, "Right", 0, 0 ) + :height( 70 ) + :frame_style( "TOOLTIP" ) + :backdrop( { bgFile = "Interface/Buttons/WHITE8x8" } ) + :backdrop_color( 0, 0, 0, 1 ) + :build() + + local label_crafters = frame:CreateFontString( nil, "ARTWORK", "GRFontHighlight" ) + label_crafters:SetPoint( "TopLeft", frame, "TopLeft", 10, -10 ) + + local text_crafters = frame:CreateFontString( nil, "ARTWORK", "GRFontHighlight" ) + text_crafters:SetPoint( "TopLeft", frame, "TopLeft", 10, -25 ) + text_crafters:SetWidth( 300 ) + text_crafters:SetJustifyH( "Left" ) + + local btn_reagents = m.GuiElements.create_button( frame, "Show reagents", 80, function() + if this:GetText() == "Show reagents" then + m.db.frame_tradeskills.show_reagents = true + this:SetText( "Hide reagents" ) + parent.info:Show() + parent:SetHeight( 584 ) + else + m.db.frame_tradeskills.show_reagents = false + this:SetText( "Show reagents" ) + parent.info:Hide() + parent:SetHeight( 354 ) + end + end ) + btn_reagents:SetPoint( "BottomRight", frame, "BottomRight", -8, 10 ) + btn_reagents:Hide() + frame.btn_reagents = btn_reagents + + frame.clear = function() + label_crafters:SetText( "" ) + text_crafters:SetText( "" ) + btn_reagents:Hide() + end + + frame.set = function( item ) + local have_alts = GuildAlts and GuildAlts.version and true or false + local online_roster = m.build_online_roster() + local players = "" + + for player_id in string.gmatch(item.players, "([^,]+)") do + local player = m.db.players[ tonumber(player_id) ] + local color = online_roster[ player ] and "FFFFFF" or "AAAAAA" + local main + if have_alts then + main = GuildAlts.get_main( player ) + if main then + local main_color = online_roster[ main ] and "FFFFFF" or "AAAAAA" + main = string.format( "|cFF%s(%s)|r", main_color, main ) + end + end + players = players .. string.format( "|cFF%s%s|r%s, ", color, player, main or "" ) + end + + label_crafters:SetText( string.format( "%s is craftable by:", m.get_item_name_colorized( item ) ) ) + text_crafters:SetText( string.match( players, "(.-), $" ) ) + btn_reagents:Show() + end + + return frame + end + + local function create_info_frame( parent ) + local frame = m.FrameBuilder.new() + :parent( parent ) + :point( "TopLeft", parent.crafters, "BottomLeft", 0, -5 ) + :point( "Right", parent.btn_search, "Right", 0, 0 ) + :height( 224 ) + :frame_style( "TOOLTIP" ) + :backdrop( { bgFile = "Interface/Buttons/WHITE8x8" } ) + :backdrop_color( 0, 0, 0, 1 ) + :build() + + local info = CreateFrame( "Frame", nil, frame ) + info:SetWidth( 380 ) + info:SetHeight( 1 ) + frame.info = info + + local scroll_frame = CreateFrame( "ScrollFrame", "GuildTradeskillsOctoInfoScrollFrame", frame, "UIPanelScrollFrameTemplate" ) + scroll_frame:SetPoint( "TopLeft", frame, "TopLeft", 5, -5 ) + scroll_frame:SetPoint( "BottomRight", frame, "BottomRight", -20, 5 ) + + _G[ "GuildTradeskillsOctoInfoScrollFrameScrollBar" ]:ClearAllPoints() + _G[ "GuildTradeskillsOctoInfoScrollFrameScrollBar" ]:SetPoint( "TopLeft", scroll_frame, "TopRight", 0, -16 ) + _G[ "GuildTradeskillsOctoInfoScrollFrameScrollBar" ]:SetPoint( "Bottom", scroll_frame, "Bottom", 0, 15 ) + + scroll_frame:SetScrollChild( info ) + + local frame_icon = CreateFrame( "Button", nil, info ) + frame_icon:SetPoint( "TopLeft", info, "TopLeft", 5, -5 ) + frame_icon:SetWidth( 32 ) + frame_icon:SetHeight( 32 ) + frame_icon:SetScript( "OnClick", function() + if IsShiftKeyDown() then + if ChatFrameEditBox:IsVisible() then + local chat_type = ChatFrameEditBox.chatType + local recipe = frame.recipe + + SendChatMessage( string.format( "Crafting of %s requires the following reagents:", recipe.link ), chat_type ) + for _, reagent in pairs( recipe.reagent_data ) do + SendChatMessage( string.format( "%s (%d)", reagent.link, reagent.count ), chat_type ) + end + return + end + end + end ) + + local icon = info:CreateTexture( nil, "ARTWORK" ) + icon:SetAllPoints( frame_icon ) + + local text_name = info:CreateFontString( nil, "ARTWORK", "GRFontNormal" ) + text_name:SetPoint( "TopLeft", info, "TopLeft", 45, -5 ) + text_name:SetJustifyH( "Left" ) + + local text_stats = info:CreateFontString( nil, "ARTWORK", "GRFontHighlightSmall" ) + text_stats:SetPoint( "TopLeft", text_name, "BottomLeft", 0, 0 ) + text_stats:SetJustifyH( "Left" ) + + local text_info = info:CreateFontString( nil, "ARTWORK", "GRFontHighlightSmall" ) + text_info:SetPoint( "TopLeft", text_stats, "BottomLeft", 0, 0 ) + text_info:SetWidth( 330 ) + text_info:SetJustifyH( "Left" ) + text_info:SetTextColor( 0, 1, 0, 1 ) + + local label_reagents = frame:CreateFontString( nil, "ARTWORK", "GRFontHighlightSmall" ) + label_reagents:SetPoint( "Top", text_info, "Bottom", 0, -10 ) + label_reagents:SetPoint( "Left", info, "Left", 5, 0 ) + label_reagents:SetText( "Reagents:" ) + info.label_reagents = label_reagents + + for i = 1, 8 do + create_reagent( info, i ) + end + + frame.clear = function( text ) + frame.recipe = nil + icon:SetTexture( nil ) + text_name:SetText( "" ) + text_stats:SetText( "" ) + text_info:SetText( "" ) + label_reagents:SetText( text or "" ) + + for i = 1, 8 do + local reagent = getglobal( "GuildTradeskillsOctoReagent" .. i ) + reagent:Hide() + end + + scroll_frame:UpdateScrollChildRect() + + if ((_G[ "GuildTradeskillsOctoInfoScrollFrameScrollBarScrollUpButton" ]:IsEnabled() == 0) and (_G[ "GuildTradeskillsOctoInfoScrollFrameScrollBarScrollDownButton" ]:IsEnabled() == 0)) then + _G[ "GuildTradeskillsOctoInfoScrollFrameScrollBar" ]:Hide() + else + _G[ "GuildTradeskillsOctoInfoScrollFrameScrollBar" ]:Show() + end + end + + local function set_reagent( slot, reagent_id, reagent_name, reagent_texture, reagent_count ) + local reagent = getglobal( "GuildTradeskillsOctoReagent" .. slot ) + local f_name = getglobal( "GuildTradeskillsOctoReagent" .. slot .. "Name" ) + local f_count = getglobal( "GuildTradeskillsOctoReagent" .. slot .. "Count" ) + local player_reagent_count = m.find_item_count_bag( 0, 4, reagent_name ) + + reagent.reagent_id = reagent_id + + if not reagent_texture or not reagent_name then + m.get_item_info( reagent_id, function( item_info ) + f_name:SetText( item_info.name ) + SetItemButtonTexture( reagent, item_info.texture ) + end ) + else + f_name:SetText( reagent_name ) + SetItemButtonTexture( reagent, reagent_texture ) + end + + if (player_reagent_count < reagent_count) then + SetItemButtonTextureVertexColor( reagent, GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b ); + f_name:SetTextColor( GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b ); + else + SetItemButtonTextureVertexColor( reagent, 1.0, 1.0, 1.0 ); + f_name:SetTextColor( HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b ); + end + + f_count:SetText( player_reagent_count .. " /" .. reagent_count ) + reagent:Show() + end + + frame.refresh = function() + if frame.recipe then + frame.set( frame.recipe ) + end + end + + frame.set = function( recipe ) + if not recipe then return end + recipe.reagent_data = {} + frame.recipe = recipe + + for i = 1, 8 do + local reagent = getglobal( "GuildTradeskillsOctoReagent" .. i ) + reagent:Hide() + end + + if recipe.skill == "Enchanting" then + icon:SetTexture( m.Enchants[ recipe.id ].icon ) + + m.scan_tooltip( string.format( "enchant:%d", recipe.id ), function( lines ) + local stats = "" + local desc = "" + local reagents = {} + + if lines then + for i, line in ipairs( lines ) do + if i > 1 then + if string.find( line, "Reagents:" ) then + line = string.gsub( line, "Reagents: ", "" ) + for reagent in string.gmatch( line, "([^,]+)" ) do + local name, count = string.match( reagent, "^%s?(.-)%s*%((%d+)%)" ) + local texture, link, quality + name = m.clean_string( name and name or reagent ) + if m.Reagents[ name ] then + _, link, quality, _, _, _, _, _, texture = GetItemInfo( m.Reagents[ name ] ) + else + texture = "Interface\\Icons\\INV_Misc_QuestionMark" + end + + table.insert( reagents, { + id = m.Reagents[ name ], + name = name, + link = m.Reagents[ name ] and m.make_item_link( m.Reagents[ name ], name, quality ) or name, + count = tonumber( count ) or 1, + icon = texture + } ) + end + else + if getn( reagents ) > 0 then + desc = desc .. line .. "\n" + else + stats = stats .. line .. "\n" + end + end + end + end + end + + text_name:SetText( recipe.name ) + text_stats:SetText( stats ~= "" and stats or "\n\n" ) + text_info:SetText( desc ) + label_reagents:SetText( "Reagents:" ) + + for i, reagent_data in pairs( reagents ) do + if not frame.recipe or not frame.recipe.reagent_data then return end + set_reagent( i, reagent_data.id, reagent_data.name, reagent_data.icon, reagent_data.count ) + frame.recipe.reagent_data[ i ] = reagent_data + end + end ) + else + m.get_item_info( recipe.craftItem, function( item_info ) + local item = { + name = item_info.name, + id = recipe.craftItem, + quality = item_info.quality, + icon = item_info.texture, + data = {} + } + + m.tooltip:ClearLines() + m.tooltip:SetHyperlink( "item:" .. item.id ) + m.tooltip:Show() + + local num_lines = m.tooltip:NumLines() + local stats = "" + local desc = "" + for i = 1, num_lines do + local line = _G[ "GuildRecipesOctoTooltipTextLeft" .. i ]:GetText() + if string.find( line, "^%s*$" ) then + break + end + if string.find( line, "Use:" ) or string.find( line, "Equip:" ) then + desc = desc .. line .. "\n" + elseif i > 1 and desc == "" then + stats = stats .. line .. "\n" + end + end + + icon:SetTexture( item.icon ) + text_name:SetText( m.get_item_name_colorized( item ) ) + text_stats:SetText( stats ~= "" and stats or "\n\n" ) + text_info:SetText( desc ) + label_reagents:SetText( "Reagents:" ) + + for i, reagent_data in pairs( recipe.reagents ) do + local reagent_count = reagent_data[ 2 ] or 1 + m.get_item_info( reagent_data[ 1 ], function( reagent_info, data ) + if not frame.recipe or not frame.recipe.reagent_data then return end + set_reagent( data.i, reagent_info.id, reagent_info.name, reagent_info.texture, reagent_count ) + frame.recipe.reagent_data[ data.i ] = { + id = reagent_info.id, + name = reagent_info.name, + link = m.make_item_link( reagent_info.id, reagent_info.name, reagent_info.quality ), + count = reagent_count + } + end, { i = i } ) + end + end ) + end + + scroll_frame:UpdateScrollChildRect() + + if ((_G[ "GuildTradeskillsOctoInfoScrollFrameScrollBarScrollUpButton" ]:IsEnabled() == 0) and (_G[ "GuildTradeskillsOctoInfoScrollFrameScrollBarScrollDownButton" ]:IsEnabled() == 0)) then + _G[ "GuildTradeskillsOctoInfoScrollFrameScrollBar" ]:Hide() + else + _G[ "GuildTradeskillsOctoInfoScrollFrameScrollBar" ]:Show() + end + end + + frame.clear() + return frame + end + + local function create_frame() + local frame = m.FrameBuilder.new() + :name( "GuildRecipesOctoFrame" ) + :title( string.format( "Guild Recipes Octo v%s", m.version ) ) + :frame_style( "TOOLTIP" ) + :frame_level( 100 ) + :backdrop( { bgFile = "Interface/Buttons/WHITE8x8" } ) + :backdrop_color( 0, 0, 0, 0.9 ) + :close_button() + :width( 427 ) + :height( 354 ) + :movable() + :esc() + :hidden() + :on_drag_stop( save_position ) + :build() + + if m.db.frame_tradeskills.position then + local p = m.db.frame_tradeskills.position + frame:ClearAllPoints() + frame:SetPoint( p.point, UIParent, p.relative_point, p.x, p.y ) + end + + local label_search = frame:CreateFontString( nil, "ARTWORK", "GRFontNormal" ) + label_search:SetPoint( "TopLeft", frame, "TopLeft", 12, -35 ) + label_search:SetTextColor( 1, 1, 1 ) + label_search:SetJustifyH( "Left" ) + label_search:SetText( "Search" ) + + local input_search = CreateFrame( "EditBox", "GuildTradeskillsOctoInputSearch", frame, "InputBoxTemplate" ) + frame.search = input_search + input_search:SetPoint( "TopLeft", frame, "TopLeft", 60, -29 ) + input_search:SetWidth( 180 ) + input_search:SetHeight( 22 ) + input_search:SetAutoFocus( false ) + input_search:SetScript( "OnEscapePressed", function() + input_search:ClearFocus() + end ) + input_search:SetScript( "OnEnterPressed", function() + do_search( input_search:GetText() ) + end ) + + local dropdown_skill = CreateFrame( "Frame", "GuildTradeskillsOctoSkillDropdown", frame, "UIDropDownMenuTemplate" ) + frame.dropdown_skill = dropdown_skill + dropdown_skill:SetPoint( "TopLeft", input_search, "TopRight", -5, 1 ) + dropdown_skill:SetScale( 0.9 ) + + local btn_search = m.GuiElements.create_button( frame, "Search", 60, function() + do_search( input_search:GetText() ) + end ) + btn_search:SetPoint( "TopLeft", dropdown_skill, "TopRight", -5, 0 ) + btn_search:SetHeight( 25 ) + frame.btn_search = btn_search + + UIDropDownMenu_Initialize( dropdown_skill, initialize_dropdown_skill ) + UIDropDownMenu_SetWidth( 90, dropdown_skill ) + UIDropDownMenu_SetText( "All tradeskills", dropdown_skill ) + + local border_results = m.FrameBuilder.new() + :parent( frame ) + :point( "TopLeft", label_search, "BottomLeft", -2, -10 ) + :point( "Right", btn_search, "Right", 0, 0 ) + :height( 211 ) + :frame_style( "TOOLTIP" ) + :backdrop( { bgFile = "Interface/Buttons/WHITE8x8" } ) + :backdrop_color( 0, 0, 0, 1 ) + :build() + + border_results:EnableMouseWheel( true ) + border_results:SetScript( "OnMouseWheel", function() + local value = frame.scroll_bar:GetValue() - arg1 + frame.scroll_bar:SetValue( value ) + end ) + frame.border_results = border_results + + local scroll_bar = CreateFrame( "Slider", "GuildTradeskillsOctoScrollBar", border_results, "UIPanelScrollBarTemplate" ) + scroll_bar:SetPoint( "TopRight", border_results, "TopRight", -5, -20 ) + scroll_bar:SetPoint( "Bottom", border_results, "Bottom", 0, 20 ) + scroll_bar:SetMinMaxValues( 0, 0 ) + scroll_bar:SetValueStep( 1 ) + scroll_bar:SetScript( "OnValueChanged", function() + offset = arg1 + refresh() + end ) + frame.scroll_bar = scroll_bar + + for i = 1, 12 do -- Set to 12 + local item = create_item( border_results, i ) + item:SetPoint( "TopLeft", border_results, "TopLeft", 4, ((i - 1) * -17) - 4 ) + table.insert( frame_items, item ) + end + + frame.crafters = create_crafters_frame( frame ) + frame.info = create_info_frame( frame ) + if not m.db.frame_tradeskills.show_reagents then + frame.info:Hide() + else + frame:SetHeight( 584 ) + frame.crafters.btn_reagents:SetText( "Hide reagents" ) + end + + return frame + end + + local function update_title() + if not popup or not popup.title_label then return end + + local synced = m.db.players and m.count( m.db.players ) or 0 + local online = 0 + if m.db.players then + local online_roster = m.build_online_roster() + for _, player in pairs( m.db.players ) do + if online_roster[ player ] then + online = online + 1 + end + end + end + + popup.title_label:SetText( string.format( + "Guild Recipes Octo v%s | |cFFFFFF00Synced Players: %d|r | |cFF00FF00Online: %d|r", + m.version, synced, online + ) ) + end + + local function show() + if not popup then + popup = create_frame() + end + + update_title() + popup:Show() + popup.info.refresh() + end + + local function hide() + if popup then + popup:Hide() + end + end + + local function toggle() + if popup and popup:IsVisible() then + popup:Hide() + else + show() + end + end + + local function is_visible() + return popup and popup:IsVisible() or false + end + + local function update() + update_title() + popup.info.refresh() + end + + return { + show = show, + hide = hide, + toggle = toggle, + is_visible = is_visible, + update = update, + } +end + +m.Tradeskills = M +return M \ No newline at end of file diff --git a/Utils.lua b/Utils.lua new file mode 100644 index 0000000..1545138 --- /dev/null +++ b/Utils.lua @@ -0,0 +1,532 @@ +GuildRecipesOcto = GuildRecipesOcto or {} + +---@class GuildRecipesOcto +local M = GuildRecipesOcto + +local QUALITY_COLORS = { + [ "ff9d9d9d" ] = 0, -- poor (gray) + [ "ffffffff" ] = 1, -- common (white) + [ "ff1eff00" ] = 2, -- uncommon (green) + [ "ff0070dd" ] = 3, -- rare (blue) + [ "ffa335ee" ] = 4, -- epic (purple) + [ "ffff8000" ] = 5, -- legendary (orange) +} + +M.TRADE_SKILL_LOCALIZATION = { + Alchemy = { + enUS = "Alchemy", + deDE = "Alchimie", + frFR = "Alchimie", + esES = "Alquimia", + koKR = "연금술", + zhCN = "炼金术", + zhTW = "鍊金術", + }, + Blacksmithing = { + enUS = "Blacksmithing", + deDE = "Schmiedekunst", + frFR = "Forge", + esES = "Herrería", + koKR = "대장기술", + zhCN = "锻造", + zhTW = "鍛造", + }, + Engineering = { + enUS = "Engineering", + deDE = "Ingenieurskunst", + frFR = "Ingénierie", + esES = "Ingeniería", + koKR = "기계공학", + zhCN = "工程学", + zhTW = "工程學", + }, + Leatherworking = { + enUS = "Leatherworking", + deDE = "Lederverarbeitung", + frFR = "Travail du cuir", + esES = "Peletería", + koKR = "가죽세공", + zhCN = "制皮", + zhTW = "製皮", + }, + Tailoring = { + enUS = "Tailoring", + deDE = "Schneiderei", + frFR = "Couture", + esES = "Sastrería", + koKR = "재봉술", + zhCN = "裁缝", + zhTW = "裁縫", + }, + Enchanting = { + enUS = "Enchanting", + deDE = "Verzauberkunst", + frFR = "Enchantement", + esES = "Encantamiento", + koKR = "마법부여", + zhCN = "附魔", + zhTW = "附魔", + }, + Jewelcrafting = { + enUS = "Jewelcrafting", + }, +} + +function M.build_reverse_trade_map( locale ) + local map = {} + for internal, locs in pairs( M.TRADE_SKILL_LOCALIZATION ) do + local localized = locs[ locale ] + if localized then + map[ localized ] = internal + end + end + return map +end + +---@param item_link string +---@return integer? id +---@return string? name +---@return integer? quality +function M.parse_item_link( item_link ) + if type( item_link ) ~= "string" then return nil, nil, nil end + + local item_id = tonumber( string.match( item_link, ":(%d+)" ) ) + local name = string.match( item_link, "|h%[(.-)%]|h" ) + local quality + + local color = string.match( item_link, "|c(%x%x%x%x%x%x%x%x)" ) + if color then + quality = QUALITY_COLORS[ string.lower( color ) ] + else + local _, _, q = GetItemInfo( item_id ) + quality = q or 1 + end + + return item_id, name, quality +end + +---@param item Item +---@return string +function M.get_item_name_colorized( item ) + local color = ITEM_QUALITY_COLORS[ item.quality or 1 ] + local hex = string.format( "%02x%02x%02x", color.r * 255, color.g * 255, color.b * 255 ) + + return string.format( "|cFF%s%s|r", hex, item.name ) +end + +---@param id integer +---@param name string +---@param quality integer +---@return string +function M.make_item_link( id, name, quality ) + local color = ITEM_QUALITY_COLORS[ quality or 1 ] + local hex = string.format( "%02x%02x%02x", color.r * 255, color.g * 255, color.b * 255 ) + + return string.format( "|cFF%s|Hitem:%d:0:0:0|h[%s]|h|r", hex, id, name ) +end + +function M.make_enchant_link( id, name ) + return string.format( "|cFF%s|Henchant:%d|h[%s]|h|r", "71d5ff", id, name ) +end + +function M.colorize_player_by_class( name, class ) + if not class then return name end + local color = RAID_CLASS_COLORS[ string.upper( class ) ] + if not color.colorStr then + color.colorStr = string.format( "ff%02x%02x%02x", color.r * 255, color.g * 255, color.b * 255 ) + end + return "|c" .. color.colorStr .. name .. "|r" +end + +function M.clean_string( input ) + input = string.gsub( input, "^%s+", "" ) + input = string.gsub( input, "%s+$", "" ) + input = string.gsub( input, "|c%x%x%x%x%x%x%x%x", "" ) + input = string.gsub( input, "|r", "" ) + + return input +end + +---@param player_name string +---@return boolean +---@nodiscard +function M.guild_member_online( player_name ) + for i = 1, GetNumGuildMembers() do + local name, _, _, _, _, _, _, _, online = GetGuildRosterInfo( i ) + if player_name == name and online == 1 then + return true + end + end + + return false +end + +---@return table -- name -> online, built from a single pass over the guild roster +---@nodiscard +-- Use this instead of calling guild_member_online() in a loop (e.g. once per +-- synced player, or once per recipe row) -- each guild_member_online() call +-- re-scans the entire roster, so doing that repeatedly is O(n*m) and can +-- visibly stutter the game when triggered by a frequent event. +function M.build_online_roster() + local online = {} + for i = 1, GetNumGuildMembers() do + local name, _, _, _, _, _, _, _, is_online = GetGuildRosterInfo( i ) + if name and is_online == 1 then + online[ name ] = true + end + end + return online +end + +function M.get_server_timestamp() + local server_hour, server_min = GetGameTime() + local local_time = date( "*t" ) + + local t = { + year = local_time.year, + month = local_time.month, + day = local_time.day, + hour = server_hour, + min = server_min, + sec = 0, + } + + local hour_diff = server_hour - local_time.hour + if hour_diff <= -20 then + t.day = t.day + 1 + elseif hour_diff >= 20 then + t.day = t.day - 1 + end + + return time( t ) +end + +---@param message string +---@param short boolean? +function M.info( message, short ) + local tag = string.format( "|c%s%s|r", M.tagcolor, short and "GRO" or "GuildRecipesOcto" ) + DEFAULT_CHAT_FRAME:AddMessage( string.format( "%s: %s", tag, message ) ) +end + +---@param message string +function M.error( message ) + local tag = string.format( "|c%s%s|r|cffff0000%s|r", M.tagcolor, "GR", "ERROR" ) + DEFAULT_CHAT_FRAME:AddMessage( string.format( "%s: %s", tag, message ) ) +end + +function M.debug( message ) + if M.debug_enabled then + M.info( message, true ) + end +end + +---@param t string[] +---@return string +---@nodiscard +function M.table_to_comma_separated( t ) + local s = "" + for _, v in pairs( t ) do + if s ~= "" then s = s .. "," end + s = s .. tostring( v ) + end + return s +end + +---@param s string +---@return string[] +---@nodiscard +function M.comma_separated_to_table( s ) + local t = {} + for v in string.gmatch( s, "([^,]+)" ) do + table.insert( t, v ) + end + return t +end + +---@param value string|number +---@param t table +---@param extract_field string? +---@nodiscard +function M.find( value, t, extract_field ) + if type( t ) ~= "table" or M.count( t ) == 0 then return nil end + + for i, v in pairs( t ) do + local val = extract_field and v[ extract_field ] or v + if val == value then return v, i end + end + + return nil +end + +---@param t table +---@param field string? +---@return number +function M.count( t, field ) + local count = 0 + for _, e in pairs( t ) do + if field and e[ field ] and e[ field ] > 0 or not field then + count = count + 1 + end + end + + return count +end + +---@param recipes table +---@param player string +---@return number +function M.count_recipes( recipes, player ) + local _, player_id = M.find( player, M.db.players ) + local count = 0 + + if player_id and recipes then + for _, recipe in pairs( recipes ) do + if M.find( tostring( player_id ), M.comma_separated_to_table( recipe.p ) ) then + count = count + 1 + end + end + end + + return count +end + +---@param id integer +---@param ufunc function +---@param data table? +---@return nil +function M.get_item_info( id, ufunc, data ) + local function callback( item_id, cbfunc, _data ) + local info = { GetItemInfo( item_id ) } + cbfunc( { + id = item_id, + name = info[ 1 ], + link = info[ 2 ], + quality = info[ 3 ], + texture = info[ 9 ], + }, _data ) + end + + if GetItemInfo( id ) then + callback( id, ufunc, data ) + return + end + + if not M.item_info_frame then + M.item_info_frame = CreateFrame( "Frame" ) + end + + if not M.get_item_info_items then + M.get_item_info_items = {} + end + + table.insert( M.get_item_info_items, { + id = id, + ufunc = ufunc, + data = data + } ) + + if not M.item_info_frame:GetScript( "OnUpdate" ) then + local elapsed = 0 + local poll_interval = 0.1 + + M.item_info_frame:SetScript( "OnUpdate", function() + local item_data = M.get_item_info_items[ 1 ] + if not item_data then + M.item_info_frame:SetScript( "OnUpdate", nil ) + return + end + + elapsed = elapsed + arg1 + if elapsed < poll_interval then return end + elapsed = 0 + + M.tooltip:SetOwner( WorldFrame, "ANCHOR_NONE" ) + M.tooltip:SetHyperlink( "item:" .. item_data.id ) + + if GetItemInfo( item_data.id ) then + callback( item_data.id, item_data.ufunc, item_data.data ) + table.remove( M.get_item_info_items, 1 ) + end + end ) + end +end + +---@param link string -- item link or spell link +---@param callback function -- function(lines: string[]|nil) end +---@param max_attempts number|nil -- retry limit (default 5) +---@param delay number|nil +function M.scan_tooltip( link, callback, max_attempts, delay ) + ---@class TooltipScan + ---@field link string + ---@field callback fun(lines: string[]|nil) + ---@field max_attempts number + ---@field delay number + ---@field attempt number + ---@field elapsed number + ---@field initialized boolean + + ---@type TooltipScan | nil + local current_scan = nil + local scan_queue = {} + local start_next_scan + + local function process_scan() + if not current_scan then + M.tooltip_scan_frame:SetScript( "OnUpdate", nil ) + return + end + + current_scan.elapsed = current_scan.elapsed + arg1 + if current_scan.elapsed < current_scan.delay then return end + + current_scan.elapsed = 0 + current_scan.attempt = current_scan.attempt + 1 + + M.tooltip:SetOwner( WorldFrame, "ANCHOR_NONE" ) + M.tooltip:ClearLines() + M.tooltip:SetHyperlink( current_scan.link ) + + local num_lines = M.tooltip:NumLines() + if num_lines and num_lines > 0 then + local lines = {} + for i = 1, num_lines do + local line = getglobal( "GuildRecipesOctoTooltipTextLeft" .. i ) + if line then + table.insert( lines, line:GetText() or "" ) + end + end + + local scan_callback = current_scan.callback + current_scan = nil + M.tooltip_scan_frame:SetScript( "OnUpdate", nil ) + scan_callback( lines ) + start_next_scan() + return + end + + if current_scan.attempt >= current_scan.max_attempts then + M.tooltip_scan_frame:SetScript( "OnUpdate", nil ) + M.debug( "Failed to scan tooltip for " .. current_scan.link ) + if current_scan.callback then + current_scan.callback( nil ) + end + + current_scan = nil + start_next_scan() + end + end + + function start_next_scan() + current_scan = table.remove( scan_queue, 1 ) + + if not current_scan then + M.tooltip_scan_frame:SetScript( "OnUpdate", nil ) + return + end + + current_scan.attempt = 0 + current_scan.elapsed = 0 + M.tooltip_scan_frame:SetScript( "OnUpdate", process_scan ) + end + + if not M.tooltip_scan_frame then + M.tooltip_scan_frame = CreateFrame( "Frame" ) + end + + table.insert( scan_queue, { + link = link, + callback = callback, + max_attempts = max_attempts or 5, + delay = delay or 0.1, + } ) + + if not current_scan then + start_next_scan() + end +end + +---@param tradeskill string +---@return integer +---@nodiscard +function M.tradeskill_hash( tradeskill ) + local keys = {} + local s = "" + local ts = M.db.tradeskills[ tradeskill ] + + if ts then + for k in pairs( ts ) do + table.insert( keys, k ) + end + + table.sort( keys ) + + for i = 1, getn( keys ) do + local _, c = string.gsub( ts[ keys[ i ] ].p, ",", "" ) + s = s .. keys[ i ] .. "=" .. c .. ";" + end + end + + return M.hash_string( s ) +end + +---@param str string +---@return integer +function M.hash_string( str ) + local hash = 5381 + for i = 1, string.len( str ) do + local c = string.byte( str, i ) + hash = mod( hash * 33 + c, 4294967296 ) + end + + return hash +end + +--- @param hex string +--- @return number r +--- @return number g +--- @return number b +--- @return number a +function M.hex_to_rgba( hex ) + local r, g, b, a = string.match( hex, "^#?(%x%x)(%x%x)(%x%x)(%x?%x?)$" ) + + r, g, b = tonumber( r, 16 ) / 255, tonumber( g, 16 ) / 255, tonumber( b, 16 ) / 255 + a = a ~= "" and tonumber( a, 16 ) / 255 or 1 + return r, g, b, a +end + +---@param o any +---@return string +function M.dump( o ) + if not o then return "nil" end + if type( o ) ~= 'table' then return tostring( o ) end + + local entries = 0 + local s = "{" + + for k, v in pairs( o ) do + if (entries == 0) then s = s .. " " end + + local key = type( k ) ~= "number" and '"' .. k .. '"' or k + + if (entries > 0) then s = s .. ", " end + + s = s .. "[" .. key .. "] = " .. M.dump( v ) + entries = entries + 1 + end + + if (entries > 0) then s = s .. " " end + return s .. "}" +end + +---@diagnostic disable-next-line: undefined-field +if not string.gmatch then string.gmatch = string.gfind end + +---@diagnostic disable-next-line: duplicate-set-field +string.match = function( str, pattern ) + if not str then return nil end + + local _, _, r1, r2, r3, r4, r5, r6, r7, r8, r9 = string.find( str, pattern ) + return r1, r2, r3, r4, r5, r6, r7, r8, r9 +end + +return M diff --git a/assets/Myriad-Pro.ttf b/assets/Myriad-Pro.ttf new file mode 100644 index 0000000..42237fc Binary files /dev/null and b/assets/Myriad-Pro.ttf differ diff --git a/assets/tiny-button-down.tga b/assets/tiny-button-down.tga new file mode 100644 index 0000000..4e64dce Binary files /dev/null and b/assets/tiny-button-down.tga differ diff --git a/assets/tiny-button-up.tga b/assets/tiny-button-up.tga new file mode 100644 index 0000000..11601d3 Binary files /dev/null and b/assets/tiny-button-up.tga differ diff --git a/libs/AceComm-3.0/AceComm-3.0.lua b/libs/AceComm-3.0/AceComm-3.0.lua new file mode 100644 index 0000000..4afb34d --- /dev/null +++ b/libs/AceComm-3.0/AceComm-3.0.lua @@ -0,0 +1,306 @@ +--- **AceComm-3.0** allows you to send messages of unlimited length over the addon comm channels. +-- It'll automatically split the messages into multiple parts and rebuild them on the receiving end.\\ +-- **ChatThrottleLib** is of course being used to avoid being disconnected by the server. +-- +-- **AceComm-3.0** can be embeded into your addon, either explicitly by calling AceComm:Embed(MyAddon) or by +-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object +-- and can be accessed directly, without having to explicitly call AceComm itself.\\ +-- It is recommended to embed AceComm, otherwise you'll have to specify a custom `self` on all calls you +-- make into AceComm. +-- @class file +-- @name AceComm-3.0 +-- @release $Id: AceComm-3.0.lua 1107 2014-02-19 16:40:32Z nevcairiel $ + +--[[ AceComm-3.0 + +TODO: Time out old data rotting around from dead senders? Not a HUGE deal since the number of possible sender names is somewhat limited. + +]] + +local MAJOR, MINOR = "AceComm-3.0", 9 + +local AceComm,oldminor = LibStub:NewLibrary(MAJOR, MINOR) + +if not AceComm then return end + +local CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0") +local CTL = assert(ChatThrottleLib, "AceComm-3.0 requires ChatThrottleLib") + +-- Lua APIs +local type, next, pairs, tostring = type, next, pairs, tostring +local strlen, strsub, strfind = string.len, string.sub, string.find +local tinsert, tconcat, tgetn, tremove = table.insert, table.concat, table.getn, table.remove +local error, assert = error, assert + + +-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded +-- List them here for Mikk's FindGlobals script +-- GLOBALS: LibStub, DEFAULT_CHAT_FRAME, geterrorhandler, RegisterAddonMessagePrefix + +AceComm.embeds = AceComm.embeds or {} + +-- for my sanity and yours, let's give the message type bytes some names +local MSG_MULTI_FIRST = "\001" +local MSG_MULTI_NEXT = "\002" +local MSG_MULTI_LAST = "\003" +local MSG_ESCAPE = "\004" + +-- remove old structures (pre WoW 4.0) +AceComm.multipart_origprefixes = nil +AceComm.multipart_reassemblers = nil + +-- the multipart message spool: indexed by a combination of sender+distribution+ +AceComm.multipart_spool = AceComm.multipart_spool or {} + +--- Register for Addon Traffic on a specified prefix +-- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent), max 16 characters +-- @param method Callback to call on message reception: Function reference, or method name (string) to call on self. Defaults to "OnCommReceived" +function AceComm:RegisterComm(prefix, method) + if method == nil then + method = "OnCommReceived" + end + + if strlen(prefix) > 16 then -- TODO: 15? + error("AceComm:RegisterComm(prefix,method): prefix length is limited to 16 characters") + end + + return AceComm._RegisterComm(self, prefix, method) -- created by CallbackHandler +end + +local warnedPrefix=false + +--- Send a message over the Addon Channel +-- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent) +-- @param text Data to send, nils (\000) not allowed. Any length. +-- @param distribution Addon channel, e.g. "RAID", "GUILD", etc; see SendAddonMessage API +-- @param target Destination for some distributions; see SendAddonMessage API +-- @param prio OPTIONAL: ChatThrottleLib priority, "BULK", "NORMAL" or "ALERT". Defaults to "NORMAL". +-- @param callbackFn OPTIONAL: callback function to be called as each chunk is sent. receives 3 args: the user supplied arg (see next), the number of bytes sent so far, and the number of bytes total to send. +-- @param callbackArg: OPTIONAL: first arg to the callback function. nil will be passed if not specified. +function AceComm:SendCommMessage(prefix, text, distribution, target, prio, callbackFn, callbackArg) + prio = prio or "NORMAL" -- pasta's reference implementation had different prio for singlepart and multipart, but that's a very bad idea since that can easily lead to out-of-sequence delivery! + if not( type(prefix)=="string" and + type(text)=="string" and + type(distribution)=="string" and + (target==nil or type(target)=="string") and + (prio=="BULK" or prio=="NORMAL" or prio=="ALERT") + ) then + error('Usage: SendCommMessage(addon, "prefix", "text", "distribution"[, "target"[, "prio"[, callbackFn, callbackarg]]])', 2) + end + + local textlen = strlen(text) + -- Yes, the max is 255 even if the dev post said 256. I tested. Char 256+ get silently truncated. /Mikk, 20110327 + -- Ace3v: substract the prefix length + local maxtextlen = 254 - strlen(prefix) + local queueName = prefix..distribution..(target or "") + + local ctlCallback = nil + if callbackFn then + ctlCallback = function(sent) + return callbackFn(callbackArg, sent, textlen) + end + end + + local forceMultipart + if strfind(text, "^[\001-\009]") then -- 4.1+: see if the first character is a control character + -- we need to escape the first character with a \004 + if textlen+1 > maxtextlen then -- would we go over the size limit? + forceMultipart = true -- just make it multipart, no escape problems then + else + text = "\004" .. text + end + end + + if not forceMultipart and textlen <= maxtextlen then + -- fits all in one message + CTL:SendAddonMessage(prio, prefix, text, distribution, target, queueName, ctlCallback, textlen) + else + maxtextlen = maxtextlen - 1 -- 1 extra byte for part indicator in prefix(4.0)/start of message(4.1) + + -- first part + local chunk = strsub(text, 1, maxtextlen) + CTL:SendAddonMessage(prio, prefix, MSG_MULTI_FIRST..chunk, distribution, target, queueName, ctlCallback, maxtextlen) + + -- continuation + local pos = 1+maxtextlen + + while pos+maxtextlen <= textlen do + chunk = strsub(text, pos, pos+maxtextlen-1) + CTL:SendAddonMessage(prio, prefix, MSG_MULTI_NEXT..chunk, distribution, target, queueName, ctlCallback, pos+maxtextlen-1) + pos = pos + maxtextlen + end + + -- final part + chunk = strsub(text, pos) + CTL:SendAddonMessage(prio, prefix, MSG_MULTI_LAST..chunk, distribution, target, queueName, ctlCallback, textlen) + end +end + + +---------------------------------------- +-- Message receiving +---------------------------------------- + +do + local compost = setmetatable({}, {__mode = "k"}) + local function new() + local t = next(compost) + if t then + compost[t]=nil + for i=tgetn(t),3,-1 do -- faster than pairs loop. don't even nil out 1/2 since they'll be overwritten + tremove(t) -- Ace3v: t[i] = nil wont affect the tgetn return value + end + return t + end + + return {} + end + + local function lostdatawarning(prefix,sender,where) + DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: lost network data regarding '"..tostring(prefix).."' from '"..tostring(sender).."' (in "..where..")") + end + + function AceComm:OnReceiveMultipartFirst(prefix, message, distribution, sender) + local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender + local spool = AceComm.multipart_spool + + --[[ + if spool[key] then + lostdatawarning(prefix,sender,"First") + -- continue and overwrite + end + --]] + + spool[key] = message -- plain string for now + end + + function AceComm:OnReceiveMultipartNext(prefix, message, distribution, sender) + local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender + local spool = AceComm.multipart_spool + local olddata = spool[key] + + if not olddata then + --lostdatawarning(prefix,sender,"Next") + return + end + + if type(olddata)~="table" then + -- ... but what we have is not a table. So make it one. (Pull a composted one if available) + local t = new() + t[1] = olddata -- add old data as first string + t[2] = message -- and new message as second string + spool[key] = t -- and put the table in the spool instead of the old string + else + tinsert(olddata, message) + end + end + + function AceComm:OnReceiveMultipartLast(prefix, message, distribution, sender) + local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender + local spool = AceComm.multipart_spool + local olddata = spool[key] + + if not olddata then + --lostdatawarning(prefix,sender,"End") + return + end + + spool[key] = nil + + if type(olddata) == "table" then + -- if we've received a "next", the spooled data will be a table for rapid & garbage-free tconcat + tinsert(olddata, message) + AceComm.callbacks:Fire(prefix, 3, tconcat(olddata, ""), distribution, sender) + compost[olddata] = true + else + -- if we've only received a "first", the spooled data will still only be a string + AceComm.callbacks:Fire(prefix, 3, olddata..message, distribution, sender) + end + end +end + + + + + + +---------------------------------------- +-- Embed CallbackHandler +---------------------------------------- + +if not AceComm.callbacks then + AceComm.callbacks = CallbackHandler:New(AceComm, + "_RegisterComm", + "UnregisterComm", + "UnregisterAllComm") +end + +AceComm.callbacks.OnUsed = nil +AceComm.callbacks.OnUnused = nil + +-- Ace3v: in vanilla, global vars: +-- event -> event type +-- arg1 -> prefix +-- arg2 -> message +-- arg3 -> channel +-- arg4 -> sender +local function OnEvent() + local prefix, message, distribution, sender = arg1, arg2, arg3, arg4 + if event == "CHAT_MSG_ADDON" then + local _, _, control, rest = strfind(message, "^([\001-\009])(.*)") + if control then + if control==MSG_MULTI_FIRST then + AceComm:OnReceiveMultipartFirst(prefix, rest, distribution, sender) + elseif control==MSG_MULTI_NEXT then + AceComm:OnReceiveMultipartNext(prefix, rest, distribution, sender) + elseif control==MSG_MULTI_LAST then + AceComm:OnReceiveMultipartLast(prefix, rest, distribution, sender) + elseif control==MSG_ESCAPE then + AceComm.callbacks:Fire(prefix, 3, rest, distribution, sender) + else + -- unknown control character, ignore SILENTLY (dont warn unnecessarily about future extensions!) + end + else + -- single part: fire it off immediately and let CallbackHandler decide if it's registered or not + AceComm.callbacks:Fire(prefix, 3, message, distribution, sender) + end + else + assert(false, "Received "..tostring(event).." event?!") + end +end + +AceComm.frame = AceComm.frame or CreateFrame("Frame", "AceComm30Frame") +AceComm.frame:SetScript("OnEvent", OnEvent) +AceComm.frame:UnregisterAllEvents() +AceComm.frame:RegisterEvent("CHAT_MSG_ADDON") + + +---------------------------------------- +-- Base library stuff +---------------------------------------- + +local mixins = { + "RegisterComm", + "UnregisterComm", + "UnregisterAllComm", + "SendCommMessage", +} + +-- Embeds AceComm-3.0 into the target object making the functions from the mixins list available on target:.. +-- @param target target object to embed AceComm-3.0 in +function AceComm:Embed(target) + for k, v in pairs(mixins) do + target[v] = self[v] + end + self.embeds[target] = true + return target +end + +function AceComm:OnEmbedDisable(target) + target:UnregisterAllComm() +end + +-- Update embeds +for target, v in pairs(AceComm.embeds) do + AceComm:Embed(target) +end diff --git a/libs/AceComm-3.0/AceComm-3.0.xml b/libs/AceComm-3.0/AceComm-3.0.xml new file mode 100644 index 0000000..c2e1bdf --- /dev/null +++ b/libs/AceComm-3.0/AceComm-3.0.xml @@ -0,0 +1,5 @@ + +