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