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