Add files via upload

This commit is contained in:
totalllyswede
2026-07-15 23:00:59 -07:00
committed by GitHub
commit f3e0219cc6
27 changed files with 5532 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
<Bindings>
<Binding name="Toggle guild tradeskills" header="GUILDRECIPESOCTO">GuildRecipesOcto.tsgui.toggle()</Binding>
</Bindings>
+803
View File
@@ -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,
}
+332
View File
@@ -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
+171
View File
@@ -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
+281
View File
@@ -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()
+19
View File
@@ -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
+318
View File
@@ -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
+72
View File
@@ -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 <Name>` | 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.
+216
View File
@@ -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
+760
View File
@@ -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
+532
View File
@@ -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<string, boolean> -- 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
Binary file not shown.
Binary file not shown.
Binary file not shown.
+306
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="ChatThrottleLib.lua"/>
<Script file="AceComm-3.0.lua"/>
</Ui>
+502
View File
@@ -0,0 +1,502 @@
--
-- ChatThrottleLib by Mikk
--
-- Manages AddOn chat output to keep player from getting kicked off.
--
-- ChatThrottleLib.SendChatMessage/.SendAddonMessage functions that accept
-- a Priority ("BULK", "NORMAL", "ALERT") as well as prefix for SendChatMessage.
--
-- Priorities get an equal share of available bandwidth when fully loaded.
-- Communication channels are separated on extension+chattype+destination and
-- get round-robinned. (Destination only matters for whispers and channels,
-- obviously)
--
-- Will install hooks for SendChatMessage and SendAdd[Oo]nMessage to measure
-- bandwidth bypassing the library and use less bandwidth itself.
--
--
-- Fully embeddable library. Just copy this file into your addon directory,
-- add it to the .toc, and it's done.
--
-- Can run as a standalone addon also, but, really, just embed it! :-)
--
--
-- ChangeLog and notes for this version:
-- There is no historic CTL version 14 and this would supersede all other Vanilla era
-- versions (<=13) while also not stepping on private server TBC versions (15+)
--
-- Modifications for this version are simply to throttle raw chat lines per second and
-- have nothing to do with the bytes sent. Chat to CHANNEL, RAID, etc all count toward
-- this limit. Otherwise, this is the same as version 13 as it relates to throttling
-- raw bytes sent. To be clear, this update doesn't further throttle raw add-on messages.
--
local CTL_VERSION = 14
local MAX_CPS = 800 -- 2000 seems to be safe if NOTHING ELSE is happening. let's call it 800.
local MSG_OVERHEAD = 40 -- Guesstimate overhead for sending a message; source+dest+chattype+protocolstuff
local BURST = 4000 -- WoW's server buffer seems to be about 32KB. 8KB should be safe, but seen disconnects on _some_ servers. Using 4KB now.
local MIN_FPS = 20 -- Reduce output CPS to half (and don't burst) if FPS drops below this value
-- Turtle seems to allow > 6 lines per second in some situations; but for pure spam throughput, a value of 6 here seems to be the limit.
-- Due to timing issues, setting this to 5.75 seems to allow the most throughput without any accidental soft bans.
local TURTLE_MAX_CHAT_LINES_PER_SECOND = 5
if(ChatThrottleLib and ChatThrottleLib.version>=CTL_VERSION) then
-- There's already a newer (or same) version loaded. Buh-bye.
return;
end
if(not ChatThrottleLib) then
ChatThrottleLib = {}
end
local ChatThrottleLib = ChatThrottleLib
local strlen = strlen
local setmetatable = setmetatable
local getn = getn
local tremove = tremove
local tinsert = tinsert
local tostring = tostring
local GetTime = GetTime
local format = format
ChatThrottleLib.version=CTL_VERSION;
-----------------------------------------------------------------------
-- Double-linked ring implementation
local Ring = {}
local RingMeta = { __index=Ring }
function Ring:New()
local ret = {}
setmetatable(ret, RingMeta)
return ret;
end
function Ring:Add(obj) -- Append at the "far end" of the ring (aka just before the current position)
if(self.pos) then
obj.prev = self.pos.prev;
obj.prev.next = obj;
obj.next = self.pos;
obj.next.prev = obj;
else
obj.next = obj;
obj.prev = obj;
self.pos = obj;
end
end
function Ring:Remove(obj)
obj.next.prev = obj.prev;
obj.prev.next = obj.next;
if(self.pos == obj) then
self.pos = obj.next;
if(self.pos == obj) then
self.pos = nil;
end
end
end
-----------------------------------------------------------------------
-- Recycling bin for pipes (kept in a linked list because that's
-- how they're worked with in the rotating rings; just reusing members)
ChatThrottleLib.PipeBin = { count=0 }
function ChatThrottleLib.PipeBin:Put(pipe)
for i=getn(pipe),1,-1 do
tremove(pipe, i);
end
pipe.prev = nil;
pipe.next = self.list;
self.list = pipe;
self.count = self.count+1;
end
function ChatThrottleLib.PipeBin:Get()
if(self.list) then
local ret = self.list;
self.list = ret.next;
ret.next=nil;
self.count = self.count - 1;
return ret;
end
return {};
end
function ChatThrottleLib.PipeBin:Tidy()
if(self.count < 25) then
return;
end
if(self.count > 100) then
n=self.count-90;
else
n=10;
end
for i=2,n do
self.list = self.list.next;
end
local delme = self.list;
self.list = self.list.next;
delme.next = nil;
end
-----------------------------------------------------------------------
-- Recycling bin for messages
ChatThrottleLib.MsgBin = {}
function ChatThrottleLib.MsgBin:Put(msg)
msg.text = nil;
tinsert(self, msg);
end
function ChatThrottleLib.MsgBin:Get()
local ret = tremove(self, getn(self));
if(ret) then return ret; end
return {};
end
function ChatThrottleLib.MsgBin:Tidy()
if(getn(self)<50) then
return;
end
if(getn(self)>150) then -- "can't happen" but ...
for n=getn(self),120,-1 do
tremove(self,n);
end
else
for n=getn(self),getn(self)-20,-1 do
tremove(self,n);
end
end
end
-----------------------------------------------------------------------
-- ChatThrottleLib:Init
-- Initialize queues, set up frame for OnUpdate, etc
function ChatThrottleLib:Init()
-- Set up queues
if(not self.Prio) then
self.Prio = {}
self.Prio["ALERT"] = { ByName={}, Ring = Ring:New(), avail=0 };
self.Prio["NORMAL"] = { ByName={}, Ring = Ring:New(), avail=0 };
self.Prio["BULK"] = { ByName={}, Ring = Ring:New(), avail=0 };
end
-- v4: total send counters per priority
for _,Prio in pairs(self.Prio) do
Prio.nTotalSent = Prio.nTotalSent or 0;
end
self.avail = self.avail or 0; -- v5
self.nTotalSent = self.nTotalSent or 0; -- v5
-- Set up a frame to get OnUpdate events
if(not self.Frame) then
self.Frame = CreateFrame("Frame");
self.Frame:Hide();
end
self.Frame.Show = self.Frame.Show; -- cache for speed
self.Frame.Hide = self.Frame.Hide; -- cache for speed
self.Frame:SetScript("OnUpdate", self.OnUpdate);
self.Frame:SetScript("OnEvent", self.OnEvent); -- v11: Monitor P_E_W so we can throttle hard for a few seconds
self.Frame:RegisterEvent("PLAYER_ENTERING_WORLD");
self.OnUpdateDelay=0;
self.TurtleChatLinesAvailable=TURTLE_MAX_CHAT_LINES_PER_SECOND;
self.LastAvailUpdate=GetTime();
self.HardThrottlingBeginTime=GetTime(); -- v11: Throttle hard for a few seconds after startup
-- Hook SendChatMessage and SendAddonMessage so we can measure unpiped traffic and avoid overloads (v7)
if(not self.ORIG_SendChatMessage) then
--SendChatMessage
self.ORIG_SendChatMessage = SendChatMessage;
SendChatMessage = function(a1,a2,a3,a4) return ChatThrottleLib.Hook_SendChatMessage(a1,a2,a3,a4); end
--SendAdd[Oo]nMessage
if(SendAddonMessage or SendAddOnMessage) then -- v10: don't pretend like it doesn't exist if it doesn't!
self.ORIG_SendAddonMessage = SendAddonMessage or SendAddOnMessage;
SendAddonMessage = function(a1,a2,a3) return ChatThrottleLib.Hook_SendAddonMessage(a1,a2,a3); end
if(SendAddOnMessage) then -- in case Slouken changes his mind...
SendAddOnMessage = SendAddonMessage;
end
end
end
self.nBypass = 0;
end
-----------------------------------------------------------------------
-- ChatThrottleLib.Hook_SendChatMessage / .Hook_SendAddonMessage
function ChatThrottleLib.Hook_SendChatMessage(text, chattype, language, destination)
local self = ChatThrottleLib;
local size = strlen(tostring(text or "")) + strlen(tostring(chattype or "")) + strlen(tostring(destination or "")) + 40;
self.avail = self.avail - size;
self.nBypass = self.nBypass + size;
self.TurtleSendChat()
return self.ORIG_SendChatMessage(text, chattype, language, destination);
end
function ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype)
local self = ChatThrottleLib;
local size = strlen(tostring(text or "")) + strlen(tostring(chattype or "")) + strlen(tostring(prefix or "")) + 40;
self.avail = self.avail - size;
self.nBypass = self.nBypass + size;
return self.ORIG_SendAddonMessage(prefix, text, chattype);
end
-----------------------------------------------------------------------
-- ChatThrottleLib:UpdateAvail
-- Update self.avail with how much bandwidth is currently available
function ChatThrottleLib:UpdateAvail()
local now = GetTime();
local newavail = MAX_CPS * (now-self.LastAvailUpdate);
if(now - self.HardThrottlingBeginTime < 5) then
-- First 5 seconds after startup/zoning: VERY hard clamping to avoid irritating the server rate limiter, it seems very cranky then
self.avail = min(self.avail + (newavail*0.1), MAX_CPS*0.5);
elseif(GetFramerate()<MIN_FPS) then -- GetFrameRate call takes ~0.002 secs
newavail = newavail * 0.5;
self.avail = min(MAX_CPS, self.avail + newavail);
self.bChoking = true; -- just for stats
else
self.avail = min(BURST, self.avail + newavail);
self.bChoking = false;
end
self.avail = max(self.avail, 0-(MAX_CPS*2)); -- Can go negative when someone is eating bandwidth past the lib. but we refuse to stay silent for more than 2 seconds; if they can do it, we can.
self.LastAvailUpdate = now;
return self.avail;
end
-----------------------------------------------------------------------
-- Despooling logic
function ChatThrottleLib.TurtleSendChat()
self = ChatThrottleLib;
self.TurtleChatLinesAvailable = self.TurtleChatLinesAvailable - 1
-- Showing the frame will start to build back the available buffer and re-hide when max lines are available
self.Frame:Show();
end
function ChatThrottleLib.IsTurtleSendChatReady()
self = ChatThrottleLib;
if self.TurtleChatLinesAvailable > 1 then
return true
end
-- print("Chat Throttled")
return false
end
function ChatThrottleLib:Despool(Prio)
local ring = Prio.Ring;
while(ring.pos and Prio.avail>ring.pos[1].nSize and self.IsTurtleSendChatReady()) do
local msg = tremove(Prio.Ring.pos, 1);
if(not Prio.Ring.pos[1]) then
local pipe = Prio.Ring.pos;
Prio.Ring:Remove(pipe);
Prio.ByName[pipe.name] = nil;
self.PipeBin:Put(pipe);
else
Prio.Ring.pos = Prio.Ring.pos.next;
end
Prio.avail = Prio.avail - msg.nSize;
msg.f(msg[1], msg[2], msg[3], msg[4]);
if msg.type == "chat" then self.TurtleSendChat() end
Prio.nTotalSent = Prio.nTotalSent + msg.nSize;
self.MsgBin:Put(msg);
end
end
function ChatThrottleLib.OnEvent()
-- v11: We know that the rate limiter is touchy after login. Assume that it's touch after zoning, too.
self = ChatThrottleLib;
if(event == "PLAYER_ENTERING_WORLD") then
self.HardThrottlingBeginTime=GetTime(); -- Throttle hard for a few seconds after zoning
self.avail = 0;
end
end
function ChatThrottleLib.OnUpdate()
self = ChatThrottleLib;
self.OnUpdateDelay = self.OnUpdateDelay + arg1;
if(self.OnUpdateDelay < 0.08) then
return;
end
if self.TurtleChatLinesAvailable < TURTLE_MAX_CHAT_LINES_PER_SECOND then
self.TurtleChatLinesAvailable = math.min(self.TurtleChatLinesAvailable + self.OnUpdateDelay * TURTLE_MAX_CHAT_LINES_PER_SECOND, TURTLE_MAX_CHAT_LINES_PER_SECOND)
end
self.OnUpdateDelay = 0;
self:UpdateAvail();
if(self.avail<0) then
return; -- argh. some bastard is spewing stuff past the lib. just bail early to save cpu.
end
-- See how many of or priorities have queued messages
local n=0;
for prioname,Prio in pairs(self.Prio) do
if(Prio.Ring.pos or Prio.avail<0) then
n=n+1;
end
end
-- Anything queued still?
if(n<1 and self.TurtleChatLinesAvailable >= TURTLE_MAX_CHAT_LINES_PER_SECOND) then
-- Nope. Move spillover bandwidth to global availability gauge and clear self.bQueueing
for prioname,Prio in pairs(self.Prio) do
self.avail = self.avail + Prio.avail;
Prio.avail = 0;
end
self.bQueueing = false;
self.Frame:Hide();
return;
end
-- There's stuff queued. Hand out available bandwidth to priorities as needed and despool their queues
local avail= self.avail/n;
self.avail = 0;
for prioname,Prio in pairs(self.Prio) do
if(Prio.Ring.pos or Prio.avail<0) then
Prio.avail = Prio.avail + avail;
if(Prio.Ring.pos and Prio.avail>Prio.Ring.pos[1].nSize) then
self:Despool(Prio);
end
end
end
-- Expire recycled tables if needed
self.MsgBin:Tidy();
self.PipeBin:Tidy();
end
-----------------------------------------------------------------------
-- Spooling logic
function ChatThrottleLib:Enqueue(prioname, pipename, msg)
local Prio = self.Prio[prioname];
local pipe = Prio.ByName[pipename];
if(not pipe) then
self.Frame:Show();
pipe = self.PipeBin:Get();
pipe.name = pipename;
Prio.ByName[pipename] = pipe;
Prio.Ring:Add(pipe);
end
tinsert(pipe, msg);
self.bQueueing = true;
end
function ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, language, destination)
if(not (self and prio and text and self.Prio[prio] ) ) then
error('Usage: ChatThrottleLib:SendChatMessage("{BULK||NORMAL||ALERT}", "prefix" or nil, "text"[, "chattype"[, "language"[, "destination"]]]', 2);
end
prefix = prefix or tostring(this); -- each frame gets its own queue if prefix is not given
local nSize = strlen(text) + MSG_OVERHEAD;
-- Check if there's room in the global available bandwidth gauge to send directly
if(not self.bQueueing and nSize < self:UpdateAvail() and self.IsTurtleSendChatReady()) then
self.avail = self.avail - nSize;
self.ORIG_SendChatMessage(text, chattype, language, destination);
self.TurtleSendChat()
self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize;
return;
end
-- Message needs to be queued
msg=self.MsgBin:Get();
msg.f=self.ORIG_SendChatMessage
msg.type="chat";
msg[1]=text;
msg[2]=chattype or "SAY";
msg[3]=language;
msg[4]=destination;
msg.n = 4
msg.nSize = nSize;
self:Enqueue(prio, format("%s/%s/%s", prefix, chattype, destination or ""), msg);
end
function ChatThrottleLib:SendAddonMessage(prio, prefix, text, chattype)
if(not (self and prio and prefix and text and chattype and self.Prio[prio] ) ) then
error('Usage: ChatThrottleLib:SendAddonMessage("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype")', 0);
end
local nSize = strlen(prefix) + 1 + strlen(text) + MSG_OVERHEAD;
-- Check if there's room in the global available bandwidth gauge to send directly
if(not self.bQueueing and nSize < self:UpdateAvail()) then
self.avail = self.avail - nSize;
self.ORIG_SendAddonMessage(prefix, text, chattype);
self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize;
return;
end
-- Message needs to be queued
msg=self.MsgBin:Get();
msg.f=self.ORIG_SendAddonMessage;
msg.type="addon";
msg[1]=prefix;
msg[2]=text;
msg[3]=chattype;
msg.n = 3
msg.nSize = nSize;
self:Enqueue(prio, format("%s/%s", prefix, chattype), msg);
end
-----------------------------------------------------------------------
-- Get the ball rolling!
ChatThrottleLib:Init();
--[[ WoWBench debugging snippet
if(WOWB_VER) then
local function SayTimer()
print("SAY: "..GetTime().." "..arg1);
end
ChatThrottleLib.Frame:SetScript("OnEvent", SayTimer);
ChatThrottleLib.Frame:RegisterEvent("CHAT_MSG_SAY");
end
]]
+209
View File
@@ -0,0 +1,209 @@
local ACECORE_MAJOR, ACECORE_MINOR = "AceCore-3.0", 2
local AceCore, oldminor = LibStub:NewLibrary(ACECORE_MAJOR, ACECORE_MINOR)
if not AceCore then return end -- No upgrade needed
AceCore._G = AceCore._G or getfenv()
local _G = AceCore._G
local strsub, strgsub, strfind = string.sub, string.gsub, string.find
local tremove, tconcat = table.remove, table.concat
local tgetn, tsetn = table.getn, table.setn
local new, del
do
local list = setmetatable({}, {__mode = "k"})
function new()
local t = next(list)
if not t then
return {}
end
list[t] = nil
return t
end
function del(t)
setmetatable(t, nil)
for k in pairs(t) do
t[k] = nil
end
tsetn(t,0)
list[t] = true
end
print = print or function(text)
DEFAULT_CHAT_FRAME:AddMessage(text)
end
-- debug
function AceCore.listcount()
local count = 0
for k in list do
count = count + 1
end
return count
end
end -- AceCore.new, AceCore.del
AceCore.new, AceCore.del = new, del
local function errorhandler(err)
return geterrorhandler()(err)
end
AceCore.errorhandler = errorhandler
local function CreateSafeDispatcher(argCount)
local code = [[
local errorhandler = LibStub("AceCore-3.0").errorhandler
local method, UP_ARGS
local function call()
local func, ARGS = method, UP_ARGS
method, UP_ARGS = nil, NILS
return func(ARGS)
end
return function(func, ARGS)
method, UP_ARGS = func, ARGS
return xpcall(call, errorhandler)
end
]]
local c = 4*argCount-1
local s = "b01,b02,b03,b04,b05,b06,b07,b08,b09,b10,b11,b12,b13,b14,b15,b16,b17,b18,b19,b20"
code = strgsub(code, "UP_ARGS", string.sub(s,1,c))
s = "a01,a02,a03,a04,a05,a06,a07,a08,a09,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20"
code = strgsub(code, "ARGS", string.sub(s,1,c))
s = "nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil"
code = strgsub(code, "NILS", string.sub(s,1,c))
return assert(loadstring(code, "safecall SafeDispatcher["..tostring(argCount).."]"))()
end
local SafeDispatchers = setmetatable({}, {__index=function(self, argCount)
local dispatcher
if not tonumber(argCount) then dbg(debugstack()) end
if argCount > 0 then
dispatcher = CreateSafeDispatcher(argCount)
else
dispatcher = function(func) return xpcall(func,errorhandler) end
end
rawset(self, argCount, dispatcher)
return dispatcher
end})
local function safecall(func,argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
-- we check to see if the func is passed is actually a function here and don't error when it isn't
-- this safecall is used for optional functions like OnInitialize OnEnable etc. When they are not
-- present execution should continue without hinderance
if type(func) == "function" then
return SafeDispatchers[argc](func,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
end
end
AceCore.safecall = safecall
local function CreateDispatcher(argCount)
local code = [[
return function(func,ARGS)
return func(ARGS)
end
]]
local s = "a01,a02,a03,a04,a05,a06,a07,a08,a09,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20"
code = strgsub(code, "ARGS", string.sub(s,1,4*argCount-1))
return assert(loadstring(code, "call Dispatcher["..tostring(argCount).."]"))()
end
AceCore.Dispatchers = setmetatable({}, {__index=function(self, argCount)
local dispatcher
if argCount > 0 then
dispatcher = CreateDispatcher(argCount)
else
dispatcher = function(func) return func() end
end
rawset(self, argCount, dispatcher)
return dispatcher
end})
-- some string functions
-- vanilla available string operations:
-- sub, gfind, rep, gsub, char, dump, find, upper, len, format, byte, lower
-- we will just replace every string.match with string.find in the code
function AceCore.strtrim(s)
return strgsub(s, "^%s*(.-)%s*$", "%1")
end
local function strsplit(delim, s, n)
if n and n < 2 then return s end
beg = beg or 1
local i,j = string.find(s,delim,beg)
if not i then
return s, nil
end
return string.sub(s,1,j-1), strsplit(delim, string.sub(s,j+1), n and n-1 or nil)
end
AceCore.strsplit = strsplit
-- Ace3v: fonctions copied from AceHook-2.1
local protFuncs = {
CameraOrSelectOrMoveStart = true, CameraOrSelectOrMoveStop = true,
TurnOrActionStart = true, TurnOrActionStop = true,
PitchUpStart = true, PitchUpStop = true,
PitchDownStart = true, PitchDownStop = true,
MoveBackwardStart = true, MoveBackwardStop = true,
MoveForwardStart = true, MoveForwardStop = true,
Jump = true, StrafeLeftStart = true,
StrafeLeftStop = true, StrafeRightStart = true,
StrafeRightStop = true, ToggleMouseMove = true,
ToggleRun = true, TurnLeftStart = true,
TurnLeftStop = true, TurnRightStart = true,
TurnRightStop = true,
}
local function issecurevariable(x)
return protFuncs[x] and 1 or nil
end
AceCore.issecurevariable = issecurevariable
local function hooksecurefunc(arg1, arg2, arg3)
if type(arg1) == "string" then
arg1, arg2, arg3 = _G, arg1, arg2
end
local orig = arg1[arg2]
if type(orig) ~= "function" then
error("The function "..arg2.." does not exist", 2)
end
arg1[arg2] = function(...)
local tmp = {orig(unpack(arg))}
arg3(unpack(arg))
return unpack(tmp)
end
end
AceCore.hooksecurefunc = hooksecurefunc
-- pickfirstset() - picks the first non-nil value and returns it
local function pickfirstset(argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
if (argc <= 1) or (a1 ~= nil) then
return a1
else
return pickfirstset(argc-1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
end
end
AceCore.pickfirstset = pickfirstset
local function countargs(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
if (a1 == nil) then return 0 end
return 1 + countargs(a2,a3,a4,a5,a6,a7,a8,a9,a10)
end
AceCore.countargs = countargs
-- wipe preserves metatable
function AceCore.wipe(t)
for k,v in pairs(t) do t[k] = nil end
tsetn(t,0)
return t
end
function AceCore.truncate(t,e)
e = e or tgetn(t)
for i=1,e do
if t[i] == nil then
tsetn(t,i-1)
return
end
end
tsetn(t,e)
end
+4
View File
@@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="AceCore-3.0.lua"/>
</Ui>
@@ -0,0 +1,286 @@
--- **AceSerializer-3.0** can serialize any variable (except functions or userdata) into a string format,
-- that can be send over the addon comm channel. AceSerializer was designed to keep all data intact, especially
-- very large numbers or floating point numbers, and table structures. The only caveat currently is, that multiple
-- references to the same table will be send individually.
--
-- **AceSerializer-3.0** can be embeded into your addon, either explicitly by calling AceSerializer: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 AceSerializer itself.\\
-- It is recommended to embed AceSerializer, otherwise you'll have to specify a custom `self` on all calls you
-- make into AceSerializer.
-- @class file
-- @name AceSerializer-3.0
-- @release $Id: AceSerializer-3.0.lua 1135 2015-09-19 20:39:16Z nevcairiel $
local MAJOR,MINOR = "AceSerializer-3.0", 5
local AceSerializer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not AceSerializer then return end
-- Lua APIs
local strbyte, strchar, gsub, gfind, format = string.byte, string.char, string.gsub, string.gfind, string.format
local assert, error, pcall = assert, error, pcall
local type, tostring, tonumber = type, tostring, tonumber
local pairs, select, frexp = pairs, select, math.frexp
local tconcat, tgetn = table.concat, table.getn
-- quick copies of string representations of wonky numbers
local inf = 1/0
local serNaN -- can't do this in 4.3, see ace3 ticket 268
local serInf, serInfMac = "1.#INF", "inf"
local serNegInf, serNegInfMac = "-1.#INF", "-inf"
-- Serialization functions
local function SerializeStringHelper(ch) -- Used by SerializeValue for strings
-- We use \126 ("~") as an escape character for all nonprints plus a few more
local n = strbyte(ch)
if n==30 then -- v3 / ticket 115: catch a nonprint that ends up being "~^" when encoded... DOH
return "\126\122"
elseif n<=32 then -- nonprint + space
return "\126"..strchar(n+64)
elseif n==94 then -- value separator
return "\126\125"
elseif n==126 then -- our own escape character
return "\126\124"
elseif n==127 then -- nonprint (DEL)
return "\126\123"
else
assert(false) -- can't be reached if caller uses a sane regex
end
end
local function SerializeValue(v, res, nres)
-- We use "^" as a value separator, followed by one byte for type indicator
local t=type(v)
if t=="string" then -- ^S = string (escaped to remove nonprints, "^"s, etc)
res[nres+1] = "^S"
res[nres+2] = gsub(v,"[%c \94\126\127]", SerializeStringHelper)
nres=nres+2
elseif t=="number" then -- ^N = number (just tostring()ed) or ^F (float components)
local str = tostring(v)
if tonumber(str)==v --[[not in 4.3 or str==serNaN]] then
-- translates just fine, transmit as-is
res[nres+1] = "^N"
res[nres+2] = str
nres=nres+2
elseif v == inf or v == -inf then
res[nres+1] = "^N"
res[nres+2] = v == inf and serInf or serNegInf
nres=nres+2
else
local m,e = frexp(v)
res[nres+1] = "^F"
res[nres+2] = format("%.0f",m*2^53) -- force mantissa to become integer (it's originally 0.5--0.9999)
res[nres+3] = "^f"
res[nres+4] = tostring(e-53) -- adjust exponent to counteract mantissa manipulation
nres=nres+4
end
elseif t=="table" then -- ^T...^t = table (list of key,value pairs)
nres=nres+1
res[nres] = "^T"
for k,v in pairs(v) do
nres = SerializeValue(k, res, nres)
nres = SerializeValue(v, res, nres)
end
nres=nres+1
res[nres] = "^t"
elseif t=="boolean" then -- ^B = true, ^b = false
nres=nres+1
if v then
res[nres] = "^B" -- true
else
res[nres] = "^b" -- false
end
elseif t=="nil" then -- ^Z = nil (zero, "N" was taken :P)
nres=nres+1
res[nres] = "^Z"
else
error(MAJOR..": Cannot serialize a value of type '"..t.."'") -- can't produce error on right level, this is wildly recursive
end
return nres
end
local serializeTbl = { "^1" } -- "^1" = Hi, I'm data serialized by AceSerializer protocol rev 1
--- Serialize the data passed into the function.
-- Takes a list of values (strings, numbers, booleans, nils, tables)
-- and returns it in serialized form (a string).\\
-- May throw errors on invalid data types.
-- @param ... List of values to serialize
-- @return The data in its serialized form (string)
function AceSerializer:Serialize(...)
local nres = 1
for i = 1,tgetn(arg) do
local v = arg[i]
nres = SerializeValue(v, serializeTbl, nres)
end
serializeTbl[nres+1] = "^^" -- "^^" = End of serialized data
return tconcat(serializeTbl, "", 1, nres+1)
end
-- Deserialization functions
local function DeserializeStringHelper(escape)
if escape<"~\122" then
return strchar(strbyte(escape,2,2)-64)
elseif escape=="~\122" then -- v3 / ticket 115: special case encode since 30+64=94 ("^") - OOPS.
return "\030"
elseif escape=="~\123" then
return "\127"
elseif escape=="~\124" then
return "\126"
elseif escape=="~\125" then
return "\94"
end
error("DeserializeStringHelper got called for '"..escape.."'?!?") -- can't be reached unless regex is screwed up
end
local function DeserializeNumberHelper(number)
--[[ not in 4.3 if number == serNaN then
return 0/0
else]]if number == serNegInf or number == serNegInfMac then
return -inf
elseif number == serInf or number == serInfMac then
return inf
else
return tonumber(number)
end
end
-- DeserializeValue: worker function for :Deserialize()
-- It works in two modes:
-- Main (top-level) mode: Deserialize a list of values and return them all
-- Recursive (table) mode: Deserialize only a single value (_may_ of course be another table with lots of subvalues in it)
--
-- The function _always_ works recursively due to having to build a list of values to return
--
-- Callers are expected to pcall(DeserializeValue) to trap errors
local function DeserializeValue(iter,single,ctl,data)
if not single then
ctl,data = iter()
end
if not ctl then
error("Supplied data misses AceSerializer terminator ('^^')")
end
if ctl=="^^" then
-- ignore extraneous data
return
end
local res
if ctl=="^S" then
res = gsub(data, "~.", DeserializeStringHelper)
elseif ctl=="^N" then
res = DeserializeNumberHelper(data)
if not res then
error("Invalid serialized number: '"..tostring(data).."'")
end
elseif ctl=="^F" then -- ^F<mantissa>^f<exponent>
local ctl2,e = iter()
if ctl2~="^f" then
error("Invalid serialized floating-point number, expected '^f', not '"..tostring(ctl2).."'")
end
local m=tonumber(data)
e=tonumber(e)
if not (m and e) then
error("Invalid serialized floating-point number, expected mantissa and exponent, got '"..tostring(m).."' and '"..tostring(e).."'")
end
res = m*(2^e)
elseif ctl=="^B" then -- yeah yeah ignore data portion
res = true
elseif ctl=="^b" then -- yeah yeah ignore data portion
res = false
elseif ctl=="^Z" then -- yeah yeah ignore data portion
res = nil
elseif ctl=="^T" then
-- ignore ^T's data, future extensibility?
res = {}
local k,v
while true do
ctl,data = iter()
if ctl=="^t" then break end -- ignore ^t's data
k = DeserializeValue(iter,true,ctl,data)
if k==nil then
error("Invalid AceSerializer table format (no table end marker)")
end
ctl,data = iter()
v = DeserializeValue(iter,true,ctl,data)
if v==nil then
error("Invalid AceSerializer table format (no table end marker)")
end
res[k]=v
end
else
error("Invalid AceSerializer control code '"..ctl.."'")
end
if not single then
return res,DeserializeValue(iter)
else
return res
end
end
--- Deserializes the data into its original values.
-- Accepts serialized data, ignoring all control characters and whitespace.
-- @param str The serialized data (from :Serialize)
-- @return true followed by a list of values, OR false followed by an error message
function AceSerializer:Deserialize(str)
str = gsub(str, "[%c ]", "") -- ignore all control characters; nice for embedding in email and stuff
local iter = gfind(str, "(^.)([^^]*)") -- Any ^x followed by string of non-^
local ctl,data = iter()
if not ctl or ctl~="^1" then
-- we purposefully ignore the data portion of the start code, it can be used as an extension mechanism
return false, "Supplied data is not AceSerializer data (rev 1)"
end
return pcall(DeserializeValue, iter)
end
----------------------------------------
-- Base library stuff
----------------------------------------
AceSerializer.internals = { -- for test scripts
SerializeValue = SerializeValue,
SerializeStringHelper = SerializeStringHelper,
}
local mixins = {
"Serialize",
"Deserialize",
}
AceSerializer.embeds = AceSerializer.embeds or {}
function AceSerializer:Embed(target)
for k, v in pairs(mixins) do
target[v] = self[v]
end
self.embeds[target] = true
return target
end
-- Update embeds
for target, v in pairs(AceSerializer.embeds) do
AceSerializer:Embed(target)
end
@@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="AceSerializer-3.0.lua"/>
</Ui>
+379
View File
@@ -0,0 +1,379 @@
--- **AceTimer-3.0** provides a central facility for registering timers.
-- AceTimer supports one-shot timers and repeating timers. All timers are stored in an efficient
-- data structure that allows easy dispatching and fast rescheduling. Timers can be registered
-- or canceled at any time, even from within a running timer, without conflict or large overhead.\\
-- AceTimer is currently limited to firing timers at a frequency of 0.01s as this is what the WoW timer API
-- restricts us to.
--
-- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you
-- need to cancel the timer you just registered.
--
-- **AceTimer-3.0** can be embeded into your addon, either explicitly by calling AceTimer: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 AceTimer itself.\\
-- It is recommended to embed AceTimer, otherwise you'll have to specify a custom `self` on all calls you
-- make into AceTimer.
-- @class file
-- @name AceTimer-3.0
-- @release $Id: AceTimer-3.0.lua 1119 2014-10-14 17:23:29Z nevcairiel $
local MAJOR, MINOR = "AceTimer-3.0", 18 -- Bump minor on changes
local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not AceTimer then return end -- No upgrade needed
local AceCore = LibStub("AceCore-3.0")
local safecall = AceCore.safecall
AceTimer.counter = AceTimer.counter or {}
AceTimer.hash = AceTimer.hash or {} -- Array of [1..BUCKETS] = linked list of timers (using .next member)
AceTimer.activeTimers = AceTimer.activeTimers or {} -- Active timer list
AceTimer.frame = AceTimer.frame or CreateFrame("Frame", "AceTimer30Frame")
local counter = AceTimer.counter
local activeTimers = AceTimer.activeTimers -- Upvalue our private data
local timerFrame = AceTimer.frame
-- Lua APIs
local type, unpack, next, error = type, unpack, next, error
local floor, max, min, mod = math.floor, math.max, math.min, math.mod
local tostring = tostring
-- WoW APIs
local GetTime = GetTime
--[[
Timers will not be fired more often than HZ-1 times per second.
Keep at intended speed PLUS ONE or we get bitten by floating point rounding errors (n.5 + 0.1 can be n.599999)
If this is ever LOWERED, all existing timers need to be enforced to have a delay >= 1/HZ on lib upgrade.
If this number is ever changed, all entries need to be rehashed on lib upgrade.
]]
local HZ = 11
local minDelay = 1/(HZ-1)
--[[
Prime for good distribution
If this number is ever changed, all entries need to be rehashed on lib upgrade.
]]
local BUCKETS = 131
local hash = AceTimer.hash
for i=1,BUCKETS do
hash[i] = hash[i] or false -- make it an integer-indexed array; it's faster than hashes
end
local new, del
do
local list = setmetatable({}, {__mode = "k"})
function new(self, loop, func, delay, argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
local name = loop and "ScheduleRepeatingTimer" or "ScheduleTimer"
if self == AceTimer then
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): use your own 'self'", 3)
end
if not func or not delay then
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): 'callback' and 'delay' must have set values.", 3)
end
if argc and (type(argc) ~= "number" or floor(argc) ~= argc) then
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): 'argc' must be an integer.", 3)
end
if type(func) == "string" then
if type(self) ~= "table" then
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): 'self' - must be a table.", 3)
elseif type(self[func]) ~= "function" then
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): Tried to register '"..func.."' as the callback, but it is not a method.", 3)
end
elseif type(func) ~= "function" then
error(MAJOR..": " .. name .. "(callback, delay, argc, args...): Tried to register '"..tostring(func).."' as the callback, but it is not a function.", 3)
end
if delay < minDelay then
delay = minDelay
end
-- Create and stuff timer in the correct hash bucket
local now = GetTime()
local timer = next(list) or {}
list[timer] = nil
timer.object = self
timer.func = func
timer.delay = delay
timer.status = loop and "loop" or "once"
timer.ends = now + delay
timer.argsCount = argc or 0
timer[1] = a1
timer[2] = a2
timer[3] = a3
timer[4] = a4
timer[5] = a5
timer[6] = a6
timer[7] = a7
timer[8] = a8
timer[9] = a9
timer[10] = a10
local bucket = floor(mod((now+delay)*HZ,BUCKETS)) + 1
timer.next = hash[bucket]
hash[bucket] = timer
local id = tostring(timer) -- user has only access to the id but not the table itself
activeTimers[id] = timer
counter[self] = (counter[self] or 0) + 1
timerFrame:Show()
return id
end
function del(t)
local id = tostring(t)
activeTimers[id] = nil
if not next(activeTimers) then
timerFrame:Hide()
end
local self = t.object
for k in pairs(t) do t[k] = nil end
list[t] = true
if counter[self] then
counter[self] = counter[self] - 1
else
counter[self] = nil
end
end
end -- new, del
--- Schedule a new one-shot timer.
-- The timer will fire once in `delay` seconds, unless canceled before.
-- @param callback Callback function for the timer pulse (funcref or method name).
-- @param delay Delay for the timer, in seconds.
-- @param argc The numbers of arguments to be passed to the callback function
-- @param a1,...,a10 The arguments
-- @usage
-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
--
-- function MyAddOn:OnEnable()
-- self:ScheduleTimer("TimerFeedback", 5)
-- end
--
-- function MyAddOn:TimerFeedback()
-- print("5 seconds passed")
-- end
function AceTimer:ScheduleTimer(func, delay, argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
return new(self, nil, func, delay, argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
end
--- Schedule a repeating timer.
-- The timer will fire every `delay` seconds, until canceled.
-- @param callback Callback function for the timer pulse (funcref or method name).
-- @param delay Delay for the timer, in seconds.
-- @param argc The numbers of arguments to be passed to the callback function
-- @param a1,...,a10 The arguments
-- @usage
-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
--
-- function MyAddOn:OnEnable()
-- self.timerCount = 0
-- self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5)
-- end
--
-- function MyAddOn:TimerFeedback()
-- self.timerCount = self.timerCount + 1
-- print(("%d seconds passed"):format(5 * self.timerCount))
-- -- run 30 seconds in total
-- if self.timerCount == 6 then
-- self:CancelTimer(self.testTimer)
-- end
-- end
function AceTimer:ScheduleRepeatingTimer(func, delay, argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
return new(self, true, func, delay, argc,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
end
--- Cancels a timer with the given id, registered by the same addon object as used for `:ScheduleTimer`
-- Both one-shot and repeating timers can be canceled with this function, as long as the `id` is valid
-- and the timer has not fired yet or was canceled before.
-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
function AceTimer:CancelTimer(id)
local timer = activeTimers[id]
if not timer then
return false
else
-- Ace3v: the timer will always be collected in the next update but not here
-- this is necessary for AceBucket to determinate if the bucket has been unregistered
-- in the callback
timer.status = nil
activeTimers[id] = nil
return true
end
end
--- Cancels all timers registered to the current addon object ('self')
function AceTimer:CancelAllTimers()
if type(self) ~= "table" then
error(MAJOR..": CancelAllTimers(): 'self' - must be a table",2)
end
if self == AceTimer then
error(MAJOR..": CancelAllTimers(): supply a meaningful 'self'", 2)
end
for k,v in pairs(activeTimers) do
if v.object == self then
AceTimer.CancelTimer(self, k)
end
end
end
--- Returns the time left for a timer with the given id, registered by the current addon object ('self').
-- This function will return 0 when the id is invalid.
-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
-- @return The time left on the timer.
function AceTimer:TimeLeft(id)
local timer = activeTimers[id]
if not timer then
return 0
else
return timer.ends - GetTime()
end
end
function AceTimer:TimerStatus(id)
local timer = activeTimers[id]
if not timer then
return nil
else
return timer.status
end
end
-- ---------------------------------------------------------------------
-- Embed handling
AceTimer.embeds = AceTimer.embeds or {}
local mixins = {
"ScheduleTimer", "ScheduleRepeatingTimer",
"CancelTimer", "CancelAllTimers",
"TimeLeft", "TimerStatus"
}
function AceTimer:Embed(target)
AceTimer.embeds[target] = true
for _,v in pairs(mixins) do
target[v] = AceTimer[v]
end
return target
end
-- AceTimer:OnEmbedDisable(target)
-- target (object) - target object that AceTimer is embedded in.
--
-- cancel all timers registered for the object
function AceTimer:OnEmbedDisable(target)
target:CancelAllTimers()
end
for addon in pairs(AceTimer.embeds) do
AceTimer:Embed(addon)
end
-- --------------------------------------------------------------------
-- OnUpdate handler
--
-- traverse buckets, always chasing "now", and fire timers that have expired
local lastint = floor(GetTime() * HZ)
local function OnUpdate()
local now = GetTime()
local nowint = floor(now * HZ)
-- Have we passed into a new hash bucket?
if nowint == lastint then return end
local soon = now + 1 -- +1 is safe as long as 1 < HZ < BUCKETS/2
-- Pass through each bucket at most once
-- Happens on e.g. instance loads, but COULD happen on high local load situations also
for curint = (max(lastint, nowint-BUCKETS) + 1), nowint do -- loop until we catch up with "now", usually only 1 iteratio
local curbucket = mod(curint,BUCKETS) + 1 -- Ace3v: both int so no floor here
-- Yank the list of timers out of the bucket and empty it. This allows reinsertion in the currently-processed bucket from callbacks.
local nexttimer = hash[curbucket]
hash[curbucket] = false -- false rather than nil to prevent the array from becoming a hash
while nexttimer do
local timer = nexttimer
nexttimer = timer.next
local status = timer.status
if not status then
del(timer)
else
local ends = timer.ends
if (status == "loop" or status == "once") and ends < soon then
local object = timer.object
local callback = timer.func
if type(callback) == "string" then
callback = (type(object) == "table") and object[callback]
if type(callback) == "function" then
safecall(callback, timer.argsCount+1, object,
timer[1], timer[2], timer[3], timer[4], timer[5],
timer[6], timer[7], timer[8], timer[9], timer[10])
else
status = "once"
end
elseif type(callback) == "function" then
safecall(callback, timer.argsCount,
timer[1], timer[2], timer[3], timer[4], timer[5],
timer[6], timer[7], timer[8], timer[9], timer[10])
else
-- probably nilled out by CancelTimer
status = "once" -- don't reschedule it
end
if status == "once" then
del(timer)
else
local delay = timer.delay
local newends = ends + delay
if newends < now then -- Keep lag from making us firing a timer unnecessarily. (Note that this still won't catch too-short-delay timers though.)
newends = now + delay
end
timer.ends = newends
-- add next timer execution to the correct bucket
local bucket = floor(mod(newends*HZ,BUCKETS)) + 1
timer.next = hash[bucket]
hash[bucket] = timer
end
else
-- reinsert (yeah, somewhat expensive, but shouldn't be happening too often either due to hash distribution)
timer.next = hash[curbucket]
hash[curbucket] = timer
end
end
end
end
lastint = nowint
end
local lastchecked = nil
local function OnEvent()
if event ~= "PLAYER_REGEN_ENABLED" then return end
local addon = next(counter, lastchecked)
if not addon then
addon = next(counter)
end
lastchecked = addon
if not addon then -- should only happen if counter is empty
return
end
local n = counter[addon]
if n > BUCKETS then
DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: The addon/module '"..tostring(addon).."' has "..tostring(n).." live timers. Surely that's not intended?")
end
end
timerFrame:SetScript("OnUpdate", OnUpdate)
timerFrame:SetScript("OnEvent", OnEvent)
timerFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
timerFrame:Hide()
+4
View File
@@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="AceTimer-3.0.lua"/>
</Ui>
@@ -0,0 +1,280 @@
--[[ $Id: CallbackHandler-1.0.lua 1131 2015-06-04 07:29:24Z nevcairiel $ ]]
local MAJOR, MINOR = "CallbackHandler-1.0", 6
local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR)
if not CallbackHandler then return end -- No upgrade needed
-- Lua APIs
local tconcat, tinsert, tgetn, tsetn = table.concat, table.insert, table.getn, table.setn
local assert, error, loadstring = assert, error, loadstring
local setmetatable, rawset, rawget = setmetatable, rawset, rawget
local next, pairs, type, tostring = next, pairs, type, tostring
local strgsub = string.gsub
local new, del
do
local list = setmetatable({}, {__mode = "k"})
function new()
local t = next(list)
if not t then
return {}
end
list[t] = nil
return t
end
function del(t)
setmetatable(t, nil)
for k in pairs(t) do
t[k] = nil
end
tsetn(t,0)
list[t] = true
end
end
local meta = {__index = function(tbl, key) rawset(tbl, key, new()) return tbl[key] end}
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
-- List them here for Mikk's FindGlobals script
-- GLOBALS: geterrorhandler
local function errorhandler(err)
return geterrorhandler()(err)
end
CallbackHandler.errorhandler = errorhandler
local function CreateDispatcher(argCount)
local code = [[
local xpcall, errorhandler = xpcall, LibStub("CallbackHandler-1.0").errorhandler
local method, UP_ARGS
local function call()
local func, ARGS = method, UP_ARGS
method, UP_ARGS = nil, NILS
return func(ARGS)
end
return function(handlers, ARGS)
local index
index, method = next(handlers)
if not method then return end
repeat
UP_ARGS = ARGS
xpcall(call, errorhandler)
index, method = next(handlers, index)
until not method
end
]]
local c = 4*argCount-1
local s = "b01,b02,b03,b04,b05,b06,b07,b08,b09,b10"
code = strgsub(code, "UP_ARGS", string.sub(s,1,c))
s = "a01,a02,a03,a04,a05,a06,a07,a08,a09,a10"
code = strgsub(code, "ARGS", string.sub(s,1,c))
s = "nil,nil,nil,nil,nil,nil,nil,nil,nil,nil"
code = strgsub(code, "NILS", string.sub(s,1,c))
return assert(loadstring(code, "safecall Dispatcher["..tostring(argCount).."]"))()
end
local Dispatchers = setmetatable({}, {__index=function(self, argCount)
local dispatcher = CreateDispatcher(argCount)
rawset(self, argCount, dispatcher)
return dispatcher
end})
--------------------------------------------------------------------------
-- CallbackHandler:New
--
-- target - target object to embed public APIs in
-- RegisterName - name of the callback registration API, default "RegisterCallback"
-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback"
-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API.
function CallbackHandler:New(target, RegisterName, UnregisterName, UnregisterAllName)
RegisterName = RegisterName or "RegisterCallback"
UnregisterName = UnregisterName or "UnregisterCallback"
if UnregisterAllName==nil then -- false is used to indicate "don't want this method"
UnregisterAllName = "UnregisterAllCallbacks"
end
-- we declare all objects and exported APIs inside this closure to quickly gain access
-- to e.g. function names, the "target" parameter, etc
-- Create the registry object
local events = setmetatable({}, meta)
local registry = { recurse=0, events=events }
-- registry:Fire() - fires the given event/message into the registry
function registry:Fire(eventname, argc, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10)
if not rawget(events, eventname) or not next(events[eventname]) then return end
local oldrecurse = registry.recurse
registry.recurse = oldrecurse + 1
argc = argc or 0
Dispatchers[argc+1](events[eventname], eventname, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10)
registry.recurse = oldrecurse
if registry.insertQueue and oldrecurse==0 then
-- Something in one of our callbacks wanted to register more callbacks; they got queued
for eventname,callbacks in pairs(registry.insertQueue) do
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
for self,func in pairs(callbacks) do
events[eventname][self] = func
-- fire OnUsed callback?
if first and registry.OnUsed then
registry.OnUsed(registry, target, eventname)
first = nil
end
end
del(callbacks)
end
del(registry.insertQueue)
registry.insertQueue = nil
end
end
-- Registration of a callback, handles:
-- self["method"], leads to self["method"](self, ...)
-- self with function ref, leads to functionref(...)
-- "addonId" (instead of self) with function ref, leads to functionref(...)
-- all with an optional arg, which, if present, gets passed as first argument (after self if present)
target[RegisterName] = function(self, eventname, method, ...)
if type(eventname) ~= "string" then
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2)
end
method = method or eventname
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
if type(method) ~= "string" and type(method) ~= "function" then
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'method' - string or function expected.", 2)
end
local regfunc
local a1 = arg[1]
if type(method) == "string" then
-- self["method"] calling style
if type(self) ~= "table" then
error("Usage: "..RegisterName.."(eventname, method[, arg]): self was not a table?", 2)
elseif self==target then
error("Usage: "..RegisterName.."(eventname, method[, arg]): do not use Library:"..RegisterName.."(), use your own 'self'.", 2)
elseif type(self[method]) ~= "function" then
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'method' - method '"..tostring(method).."' not found on 'self'.", 2)
end
if tgetn(arg) >= 1 then
regfunc = function (...) return self[method](self,a1,unpack(arg)) end
else
regfunc = function (...) return self[method](self,unpack(arg)) end
end
else
-- function ref with self=object or self="addonId"
if type(self)~="table" and type(self)~="string" then
error("Usage: "..RegisterName.."(self or addonId, eventname, method[, arg]): 'self or addonId': table or string expected.", 2)
end
if tgetn(arg) >= 1 then
regfunc = function (...) return method(a1, unpack(arg)) end
else
regfunc = method
end
end
if events[eventname][self] or registry.recurse<1 then
-- if registry.recurse<1 then
-- we're overwriting an existing entry, or not currently recursing. just set it.
events[eventname][self] = regfunc
-- fire OnUsed callback?
if registry.OnUsed and first then
registry.OnUsed(registry, target, eventname)
end
else
-- we're currently processing a callback in this registry, so delay the registration of this new entry!
-- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency
registry.insertQueue = registry.insertQueue or setmetatable(new(),meta)
registry.insertQueue[eventname][self] = regfunc
end
end
-- Unregister a callback
target[UnregisterName] = function(self, eventname)
if not self or self==target then
error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2)
end
if type(eventname) ~= "string" then
error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2)
end
if rawget(events, eventname) and events[eventname][self] then
events[eventname][self] = nil
-- Fire OnUnused callback?
if registry.OnUnused and not next(events[eventname]) then
registry.OnUnused(registry, target, eventname)
end
if rawget(events, eventname) and not next(events[eventname]) then
del(events[eventname])
events[eventname] = nil
end
end
if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then
registry.insertQueue[eventname][self] = nil
end
end
-- OPTIONAL: Unregister all callbacks for given selfs/addonIds
if UnregisterAllName then
target[UnregisterAllName] = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
if not a1 then
error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or 'addonId' to unregister events for.", 2)
end
if a1 == target then
error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or 'addonId'", 2)
end
-- use our registry table as argument table
registry[1] = a1
registry[2] = a2
registry[3] = a3
registry[4] = a4
registry[5] = a5
registry[6] = a6
registry[7] = a7
registry[8] = a8
registry[9] = a9
registry[10] = a10
for i=1,10 do
local self = registry[i]
registry[i] = nil
if self then
if registry.insertQueue then
for eventname, callbacks in pairs(registry.insertQueue) do
if callbacks[self] then
callbacks[self] = nil
end
end
end
for eventname, callbacks in pairs(events) do
if callbacks[self] then
callbacks[self] = nil
-- Fire OnUnused callback?
if registry.OnUnused and not next(callbacks) then
registry.OnUnused(registry, target, eventname)
end
end
end
end
end
end
end
return registry
end
-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it
-- try to upgrade old implicit embeds since the system is selfcontained and
-- relies on closures to work.
@@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="CallbackHandler-1.0.lua"/>
</Ui>
+34
View File
@@ -0,0 +1,34 @@
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info
-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
local _G = getfenv()
local strfind, strfmt = string.find, string.format
local LibStub = _G[ LIBSTUB_MAJOR ]
if not LibStub or LibStub.minor < LIBSTUB_MINOR then
LibStub = LibStub or { libs = {}, minors = {} }
_G[ LIBSTUB_MAJOR ] = LibStub
LibStub.minor = LIBSTUB_MINOR
function LibStub:NewLibrary( major, minor )
assert( type( major ) == "string", "Bad argument #2 to `NewLibrary' (string expected)" )
local _, _, num = strfind( minor, "(%d+)" )
minor = assert( tonumber( num ), "Minor version must either be a number or contain a number." )
local oldminor = self.minors[ major ]
if oldminor and oldminor >= minor then return nil end
self.minors[ major ], self.libs[ major ] = minor, self.libs[ major ] or {}
return self.libs[ major ], oldminor
end
function LibStub:GetLibrary( major, silent )
if not self.libs[ major ] and not silent then
error( strfmt( "Cannot find a library instance of %q.", tostring( major ) ), 2 )
end
return self.libs[ major ], self.minors[ major ]
end
function LibStub:IterateLibraries() return pairs( self.libs ) end
setmetatable( LibStub, { __call = LibStub.GetLibrary } )
end
+8
View File
@@ -0,0 +1,8 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Include file="CallbackHandler-1.0\CallbackHandler-1.0.xml"/>
<Include file="AceCore-3.0\AceCore-3.0.xml"/>
<Include file="AceComm-3.0\AceComm-3.0.xml"/>
<Include file="AceTimer-3.0\AceTimer-3.0.xml"/>
<Include file="AceSerializer-3.0\AceSerializer-3.0.xml"/>
</Ui>