Add files via upload
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<Bindings>
|
||||
<Binding name="Toggle guild tradeskills" header="GUILDRECIPESOCTO">GuildRecipesOcto.tsgui.toggle()</Binding>
|
||||
</Bindings>
|
||||
+803
@@ -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,
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
]]
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user