From 3bc290dcb1e67ae6a28df693e16512dfff4bd7af Mon Sep 17 00:00:00 2001 From: Cinecom <42314202+Cinecom@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:24:16 +0100 Subject: [PATCH] Update 2.0 - Added a new feature which allows syncing between multiple accounts (in beta) - Added new consumes to the itemlist - Added materials or objectives needed to make the consumes in the tooltips - Fixed some wrong ID's in the itemlist - Increased Performance in item scanning - Added class color codes to the preset lists and ordered the items better --- ConsumesManager.toc | 11 +- Itemlist.lua | 206 ++-- Main.lua | 1651 ++++++++++++++++++++++++------ libs/LibCompress/LibCompress.lua | 798 +++++++++++++++ libs/LibStub/LibStub.lua | 33 + libs/LzwLib-1.0/LzwLib-1.0.lua | 435 ++++++++ 6 files changed, 2728 insertions(+), 406 deletions(-) create mode 100644 libs/LibCompress/LibCompress.lua create mode 100644 libs/LibStub/LibStub.lua create mode 100644 libs/LzwLib-1.0/LzwLib-1.0.lua diff --git a/ConsumesManager.toc b/ConsumesManager.toc index 786d9ba..568e3b3 100644 --- a/ConsumesManager.toc +++ b/ConsumesManager.toc @@ -1,11 +1,16 @@ ## Interface: 11200 ## Title: |cff00ff00Consumes Manager|r |cff808080[Horyoshi]|r -## Notes: Tracks and manages consumables across your inventory, bank and mail through multiple characters. Keeps track of how much consumes you have left. +## Notes: Easily track and manage your consumables, food buffs, and more across your inventory, bank, and mail, while supporting multiple characters and accounts. ## Author: Horyoshi -## Version: 1.8 +## Version: 2.0 ## SavedVariables: ConsumesManager_Options, ConsumesManager_SelectedItems, ConsumesManager_Data +libs\LibStub\LibStub.lua +libs\LibCompress\LibCompress.lua +libs\LibCompress\LibCompress.xml +libs\LzwLib-1.0\LzwLib-1.0.lua + Main.lua Itemlist.lua Classpresets.lua -minimap_icon.xml \ No newline at end of file +minimap_icon.xml diff --git a/Itemlist.lua b/Itemlist.lua index 07a9296..2797aa3 100644 --- a/Itemlist.lua +++ b/Itemlist.lua @@ -1,126 +1,126 @@ consumablesCategories = { ["Elixirs"] = { - { id = 13452, name = "Elixir of the Mongoose", texture = "Interface\\Icons\\INV_Potion_32", description = "Increases Agility by 25 and critical strike chance by 2% for 1 hour." }, - { id = 9187, name = "Elixir of Greater Agility", texture = "Interface\\Icons\\INV_Potion_94", description = "Increases Agility by 15 for 1 hour." }, - { id = 13453, name = "Elixir of Brute Force", texture = "Interface\\Icons\\INV_Potion_40", description = "Increases Strength and Stamina by 18 for 1 hour." }, - { id = 13447, name = "Elixir of the Sages", texture = "Interface\\Icons\\INV_Potion_29", description = "Increases Intellect and Spirit by 18 for 1 hour." }, - { id = 13454, name = "Greater Arcane Elixir", texture = "Interface\\Icons\\INV_Potion_25", description = "Increases spell damage by up to 35 for 1 hour." }, - { id = 13445, name = "Elixir of Superior Defense", texture = "Interface\\Icons\\INV_Potion_66", description = "Increases armor by 450 for 1 hour." }, - { id = 17708, name = "Elixir of Frost Power", texture = "Interface\\Icons\\INV_Potion_03", description = "Increases Frost spell damage by up to 15 for 1 hour." }, - { id = 22193, name = "Bloodkelp Elixir of Resistance", texture = "Interface\\Icons\\INV_Potion_21", description = "Increases all magical resistances by 15 for 30 minutes." }, - { id = 3825, name = "Elixir of Fortitude", texture = "Interface\\Icons\\INV_Potion_43", description = "Increases the player's maximum health by 120 for 1 hour." }, - { id = 9206, name = "Elixir of Giants", texture = "Interface\\Icons\\INV_Potion_61", description = "Increases your Strength by 25 for 1 hour." }, - { id = 9179, name = "Elixir of Greater Intellect", texture = "Interface\\Icons\\INV_Potion_10", description = "Increases Intellect by 25 for 1 hour." }, - { id = 50237, name = "Elixir of Greater Nature Power", texture = "Interface\\Icons\\INV_Potion_22", description = "Increases nature spell damage by up to 55 for 1 hour." }, - { id = 21546, name = "Elixir of Greater Firepower", texture = "Interface\\Icons\\INV_Potion_60", description = "Increases spell fire damage by up to 40 for 1 hour." }, - { id = 3386, name = "Elixir of Poison Resistance", texture = "Interface\\Icons\\INV_Potion_12", description = "Imbiber is cured of up to four poisons up to level 60." }, - { id = 9264, name = "Elixir of Shadow Power", texture = "Interface\\Icons\\INV_Potion_46", description = "Increases spell shadow damage by up to 40 for 30 minutes." }, - { id = 61224, name = "Dreamshard Elixir", texture = "Interface\\Icons\\INV_Potion_12", description = "Grants 2% spell critical and 15 spell power for 1 hour." } + { id = 13452, name = "Elixir of the Mongoose", mats = {"2x Mountain Silversage", "2x Plaguebloom", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_32", description = "Increases Agility by 25 and critical strike chance by 2% for 1 hour." }, + { id = 9187, name = "Elixir of Greater Agility", mats = {"1x Sungrass", "1x Goldthorn", "1x Crystal Vial" }, texture = "Interface\\Icons\\INV_Potion_94", description = "Increases Agility by 15 for 1 hour." }, + { id = 13453, name = "Elixir of Brute Force", mats = {"2x Gromsblood", "2x Plaguebloom", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_40", description = "Increases Strength and Stamina by 18 for 1 hour." }, + { id = 13447, name = "Elixir of the Sages", mats = {"1x Dreamfoil", "2x Plaguebloom", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_29", description = "Increases Intellect and Spirit by 18 for 1 hour." }, + { id = 13454, name = "Greater Arcane Elixir", mats = {"3x Dreamfoil", "1x Mountain Silversage", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_25", description = "Increases spell damage by up to 35 for 1 hour." }, + { id = 13445, name = "Elixir of Superior Defense", mats = {"2x Stonescale Oil", "1x Sungrass", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_66", description = "Increases armor by 450 for 1 hour." }, + { id = 17708, name = "Elixir of Frost Power", mats = {"2x Wintersbite", "1x Khadgar's Whisker", "1x Leaded Vial"}, texture = "Interface\\Icons\\INV_Potion_03", description = "Increases Frost spell damage by up to 15 for 1 hour." }, + { id = 22193, name = "Bloodkelp Elixir of Resistance", mats = {"Quest: More Components of Importance"}, texture = "Interface\\Icons\\INV_Potion_21", description = "Increases all magical resistances by 15 for 30 minutes." }, + { id = 3825, name = "Elixir of Fortitude", mats = {"1x Wild Steelbloom", "1x Goldthorn", "1x Leaded Vial"}, texture = "Interface\\Icons\\INV_Potion_43", description = "Increases the player's maximum health by 120 for 1 hour." }, + { id = 9206, name = "Elixir of Giants", mats = {"1x Sungrass", "1x Gromsblood", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_61", description = "Increases your Strength by 25 for 1 hour." }, + { id = 9179, name = "Elixir of Greater Intellect", mats = {"1x Blindweed", "1x Khadgar's Whisker", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_10", description = "Increases Intellect by 25 for 1 hour." }, + { id = 50237, name = "Elixir of Greater Nature Power", mats = {"3x Heart of the Wild", "1x Golden Sansam", "1x Sungrass", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_22", description = "Increases nature spell damage by up to 55 for 1 hour." }, + { id = 21546, name = "Elixir of Greater Firepower", mats = {"3x Fire Oil", "3x Firebloom", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_60", description = "Increases spell fire damage by up to 40 for 1 hour." }, + { id = 3386, name = "Elixir of Poison Resistance", mats = {"1x Large Venom Sac", "1x Bruiseweed", "1x Leaded Vial"}, texture = "Interface\\Icons\\INV_Potion_12", description = "Imbiber is cured of up to four poisons up to level 60." }, + { id = 9264, name = "Elixir of Shadow Power", mats = {"3x Ghost Mushroom", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_46", description = "Increases spell shadow damage by up to 40 for 30 minutes." }, + { id = 61224, name = "Dreamshard Elixir", mats = {"1x Dream Dust", "1x Small Dream Shard", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_12", description = "Grants 2% spell critical and 15 spell power for 1 hour." } }, ["Flasks"] = { - { id = 13513, name = "Flask of Chromatic Resistance", texture = "Interface\\Icons\\INV_Potion_48", description = "Increases your resistance to all schools of magic by 25 for 2 hours." }, - { id = 13511, name = "Flask of Distilled Wisdom", texture = "Interface\\Icons\\INV_Potion_97", description = "Increases the player's maximum mana by 2000 for 2 hours." }, - { id = 13512, name = "Flask of Supreme Power", texture = "Interface\\Icons\\INV_Potion_41", description = "Increases damage done by magical spells and effects by up to 150 for 2 hours." }, - { id = 13510, name = "Flask of the Titans", texture = "Interface\\Icons\\INV_Potion_62", description = "Increases the player's maximum health by 1200 for 2 hours." }, - { id = 13506, name = "Flask of Petrification", texture = "Interface\\Icons\\INV_Potion_26", description = "You turn to stone, protecting you from all physical attacks and spells for 60 sec." } + { id = 13513, name = "Flask of Chromatic Resistance", mats = {"30x Icecap", "10x Mountain Silversage", "1x Black Lotus", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_48", description = "Increases your resistance to all schools of magic by 25 for 2 hours." }, + { id = 13511, name = "Flask of Distilled Wisdom", mats = {"30x Dreamfoil", "10x Icecap", "1x Black Lotus", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_97", description = "Increases the player's maximum mana by 2000 for 2 hours." }, + { id = 13512, name = "Flask of Supreme Power", mats = {"30x Dreamfoil", "10x Mountain Silversage", "1x Black Lotus", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_41", description = "Increases damage done by magical spells and effects by up to 150 for 2 hours." }, + { id = 13510, name = "Flask of the Titans", mats = {"30x Gromsblood", "10x Stonescale Oil", "1x Black Lotus", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_62", description = "Increases the player's maximum health by 1200 for 2 hours." }, + { id = 13506, name = "Flask of Petrification", mats = {"30x Stonescale Oil", "10x Mountain Silversage", "1x Black Lotus", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_26", description = "You turn to stone, protecting you from all physical attacks and spells for 60 sec." } }, ["Protection Potions"] = { - { id = 13457, name = "Greater Fire Protection Potion", texture = "Interface\\Icons\\INV_Potion_24", description = "Absorbs 1950 to 3250 Fire damage for 1 hour." }, - { id = 13456, name = "Greater Frost Protection Potion", texture = "Interface\\Icons\\INV_Potion_20", description = "Absorbs 1950 to 3250 Frost damage for 1 hour." }, - { id = 13458, name = "Greater Nature Protection Potion", texture = "Interface\\Icons\\INV_Potion_22", description = "Absorbs 1950 to 3250 Nature damage for 1 hour." }, - { id = 13459, name = "Greater Shadow Protection Potion", texture = "Interface\\Icons\\INV_Potion_23", description = "Absorbs 1950 to 3250 Shadow damage for 1 hour." }, - { id = 13461, name = "Greater Arcane Protection Potion", texture = "Interface\\Icons\\INV_Potion_83", description = "Absorbs 1950 to 3250 Arcane damage for 1 hour." }, - { id = 13460, name = "Greater Holy Protection Potion", texture = "Interface\\Icons\\INV_Potion_09", description = "Absorbs 1950 to 3251 holy damage for 1 hour." } + { id = 13457, name = "Greater Fire Protection Potion", mats = {"1x Elemental Fire", "1x Firebloom", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_24", description = "Absorbs 1950 to 3250 Fire damage for 1 hour." }, + { id = 13456, name = "Greater Frost Protection Potion", mats = {"1x Elemental Water", "1x Icecap", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_20", description = "Absorbs 1950 to 3250 Frost damage for 1 hour." }, + { id = 13458, name = "Greater Nature Protection Potion", mats = {"1x Elemental Earth", "1x Dreamfoil", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_22", description = "Absorbs 1950 to 3250 Nature damage for 1 hour." }, + { id = 13459, name = "Greater Shadow Protection Potion", mats = {"1x Shadow Oil", "2x Arthas' Tears", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_23", description = "Absorbs 1950 to 3250 Shadow damage for 1 hour." }, + { id = 13461, name = "Greater Arcane Protection Potion", mats = {"1x Dream Dust", "1x Dreamfoil", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_83", description = "Absorbs 1950 to 3250 Arcane damage for 1 hour." }, + { id = 13460, name = "Greater Holy Protection Potion", mats = {"1x Elemental Air", "1x Golden Sansam", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_09", description = "Absorbs 1950 to 3251 holy damage for 1 hour." } }, ["Weapon Enhancements"] = { - { id = 12404, name = "Dense Sharpening Stone", texture = "Interface\\Icons\\INV_Stone_SharpeningStone_05", description = "Increases weapon damage by 8 for 30 minutes." }, - { id = 12645, name = "Thorium Shield Spike", texture = "Interface\\Icons\\INV_Misc_Armorkit_20", description = "Attaches a Thorium Spike to a shield, causing 20-30 damage when blocking." }, - { id = 20748, name = "Brilliant Mana Oil", texture = "Interface\\Icons\\INV_Potion_100", description = "Increases mana regeneration by 12 and spell damage by up to 25 for 30 minutes." }, - { id = 20749, name = "Brilliant Wizard Oil", texture = "Interface\\Icons\\INV_Potion_105", description = "Increases spell damage by up to 36 for 30 minutes." }, - { id = 23123, name = "Blessed Wizard Oil", texture = "Interface\\Icons\\INV_Potion_26", description = "Increases spell damage against undead by up to 60. Lasts for 1 hour." }, - { id = 3829, name = "Frost Oil", texture = "Interface\\Icons\\INV_Potion_20", description = "Adds a chance to inflict Frost damage on hit. Lasts 30 minutes." }, - { id = 23122, name = "Consecrated Sharpening Stone", texture = "Interface\\Icons\\inv_stone_sharpeningstone_02", description = "Increases melee attack power by 100 against undead for 30 minutes." }, - { id = 18262, name = "Elemental Sharpening Stone", texture = "Interface\\Icons\\INV_Stone_02", description = "Increases critical strike chance by 2% for 30 minutes." }, - { id = 8928, name = "Instant Poison VI", texture = "Interface\\Icons\\Ability_Poisons", description = "20% chance to inflict 112 to 149 Nature damage." }, - { id = 3776, name = "Crippling Poison II", texture = "Interface\\Icons\\INV_Potion_19", description = "30% chance to inflict the enemy, slowing their movement speed by 60% for 12 sec." }, - { id = 20844, name = "Deadly Poison V", texture = "Interface\\Icons\\Ability_Rogue_DualWeild", description = "30% chance of poisoning the enemy for 136 Nature damage over 12 sec. Stacks up to 5 times on a single target." }, - { id = 5763, name = "Mind-numbing Poison III", texture = "Interface\\Icons\\Spell_Nature_NullifyDisease", description = "20% chance of poisoning the enemy, increasing their casting time by 60% for 14 sec." }, - { id = 10922, name = "Wound Poison IV", texture = "Interface\\Icons\\Ability_PoisonSting", description = "30% chance of poisoning the enemy, reducing all healing effects used on them by 135 for 15 sec. Stacks up to 5 times on a single target." }, - { id = 12643, name = "Dense Weightstone", texture = "Interface\\Icons\\INV_stone_weightstone_05", description = "Increase the damage of a blunt weapon by 8 for 30 minutes." } + { id = 12404, name = "Dense Sharpening Stone", mats = {"1x Dense Stone"}, texture = "Interface\\Icons\\INV_Stone_SharpeningStone_05", description = "Increases weapon damage by 8 for 30 minutes." }, + { id = 12645, name = "Thorium Shield Spike", mats = {"4x Thorium Bar", "4x Dense Grinding Stone", "2x Essence of Earth"}, texture = "Interface\\Icons\\INV_Misc_Armorkit_20", description = "Attaches a Thorium Spike to a shield, causing 20-30 damage when blocking." }, + { id = 20748, name = "Brilliant Mana Oil", mats = {"2x Large Brilliant Shard", "3x Purple Lotus", "1x Imbued Vial"}, texture = "Interface\\Icons\\INV_Potion_100", description = "Increases mana regeneration by 12 and spell damage by up to 25 for 30 minutes." }, + { id = 20749, name = "Brilliant Wizard Oil", mats = {"2x Large Brilliant Shard", "3x Firebloom", "1x Imbued Vial"}, texture = "Interface\\Icons\\INV_Potion_105", description = "Increases spell damage by up to 36 for 30 minutes." }, + { id = 23123, name = "Blessed Wizard Oil", mats = {"Quest: 8x Necrotic Rune"}, texture = "Interface\\Icons\\INV_Potion_26", description = "Increases spell damage against undead by up to 60. Lasts for 1 hour." }, + { id = 3829, name = "Frost Oil", mats = {"4x Khadgar's Whisker", "2x Wintersbite", "1x Leaded Vial"}, texture = "Interface\\Icons\\INV_Potion_20", description = "Adds a chance to inflict Frost damage on hit. Lasts 30 minutes." }, + { id = 23122, name = "Consecrated Sharpening Stone", mats = {"Quest: 8x Necrotic Rune"}, texture = "Interface\\Icons\\inv_stone_sharpeningstone_02", description = "Increases melee attack power by 100 against undead for 30 minutes." }, + { id = 18262, name = "Elemental Sharpening Stone", mats = {"2x Elemental Earth", "3x Dense Stone"}, texture = "Interface\\Icons\\INV_Stone_02", description = "Increases critical strike chance by 2% for 30 minutes." }, + { id = 8928, name = "Instant Poison VI", mats = {"4x Dust of Deterioration", "1x Crystal Vial"}, texture = "Interface\\Icons\\Ability_Poisons", description = "20% chance to inflict 112 to 149 Nature damage." }, + { id = 3776, name = "Crippling Poison II", mats = {"3x Essence of Agony", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_19", description = "30% chance to inflict the enemy, slowing their movement speed by 60% for 12 sec." }, + { id = 20844, name = "Deadly Poison V", mats = {"7x Deathweed", "1x Crystal Vial"}, texture = "Interface\\Icons\\Ability_Rogue_DualWeild", description = "30% chance of poisoning the enemy for 136 Nature damage over 12 sec. Stacks up to 5 times on a single target." }, + { id = 9186, name = "Mind-numbing Poison III", mats = {"2x Dust of Deterioration", "2x Essence of Agony", "1x Crystal Vial"}, texture = "Interface\\Icons\\Spell_Nature_NullifyDisease", description = "20% chance of poisoning the enemy, increasing their casting time by 60% for 14 sec." }, + { id = 10922, name = "Wound Poison IV", mats = {"2x Essence of Agony", "2x Deathweed", "1x Crystal Vial"}, texture = "Interface\\Icons\\Ability_PoisonSting", description = "30% chance of poisoning the enemy, reducing all healing effects used on them by 135 for 15 sec. Stacks up to 5 times on a single target." }, + { id = 12643, name = "Dense Weightstone", mats = {"1x Dense Stone", "1x Runecloth"}, texture = "Interface\\Icons\\INV_stone_weightstone_05", description = "Increase the damage of a blunt weapon by 8 for 30 minutes." } }, ["Combat Potions"] = { - { id = 13455, name = "Greater Stoneshield Potion", texture = "Interface\\Icons\\INV_Potion_69", description = "Increases armor by 2000 for 2 minutes." }, - { id = 13442, name = "Mighty Rage Potion", texture = "Interface\\Icons\\INV_Potion_41", description = "Restores 45 to 75 rage and increases Strength by 60 for 20 seconds." }, - { id = 20007, name = "Mageblood Potion", texture = "Interface\\Icons\\INV_Potion_45", description = "Regenerates 12 mana every 5 seconds for 1 hour." }, - { id = 61423, name = "Dreamtonic", texture = "Interface\\Icons\\INV_Potion_10", description = "Increases spell damage by up to 35 for 20 minutes." } + { id = 13455, name = "Greater Stoneshield Potion", mats = {"3x Stonescale Oil", "1x Thorium Ore", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_69", description = "Increases armor by 2000 for 2 minutes." }, + { id = 13442, name = "Mighty Rage Potion", mats = {"3x Gromsblood", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_41", description = "Restores 45 to 75 rage and increases Strength by 60 for 20 seconds." }, + { id = 20007, name = "Mageblood Potion", mats = {"1x Dreamfoil", "2x Plaguebloom", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_45", description = "Regenerates 12 mana every 5 seconds for 1 hour." }, + { id = 61423, name = "Dreamtonic", mats = {"Quest: 1x Small Dream Shard"}, texture = "Interface\\Icons\\INV_Potion_10", description = "Increases spell damage by up to 35 for 20 minutes." }, + { id = 61181, name = "Potion of Quickness", mats = {"1x Gromsblood", "2x Mountain Silversage", "1x Swiftness Potion"}, texture = "Interface\\Icons\\inv_potion_08", description = "Increases haste by 5% for 30 sec." } }, ["Utility Items"] = { - { id = 20008, name = "Living Action Potion", texture = "Interface\\Icons\\INV_Potion_07", description = "Removes movement impairing effects and grants immunity to them for 5 seconds." }, - { id = 3387, name = "Limited Invulnerability Potion", texture = "Interface\\Icons\\INV_Potion_62", description = "Makes you immune to physical attacks for 6 seconds." }, - { id = 5634, name = "Free Action Potion", texture = "Interface\\Icons\\INV_Potion_04", description = "Grants immunity to stun and movement impairing effects for 30 seconds." }, - { id = 61225, name = "Lucidity Potion", texture = "Interface\\Icons\\INV_Potion_36", description = "Become immune to Sleep, Polymorph, and Charm effects for 30 sec." }, - { id = 9030, name = "Restorative Potion", texture = "Interface\\Icons\\INV_Potion_01", description = "Removes a magic, curse, poison, or disease effect every 5 seconds for 30 seconds." }, - { id = 4390, name = "Iron Grenade", texture = "Interface\\Icons\\INV_Misc_Bomb_08", description = "Inflicts 132 to 219 Fire damage and stuns enemies in a 3-yard radius for 3 seconds." }, - { id = 15993, name = "Thorium Grenade", texture = "Interface\\Icons\\INV_Misc_Bomb_08", description = "Inflicts 300 to 501 Fire damage and stuns enemies in a 3-yard radius for 3 seconds." }, - { id = 10646, name = "Goblin Sapper Charge", texture = "Interface\\Icons\\spell_fire_selfdestruct", description = "Deals 450-751 Fire damage to enemies within 10 yards, also damages you." }, - { id = 61675, name = "Nordanaar Herbal Tea", texture = "Interface\\Icons\\INV_Drink_Milk_05", description = "Restores 525 to 875 health and 810 to 1350 mana." }, - { id = 13462, name = "Purification Potion", texture = "Interface\\Icons\\INV_Potion_31", description = "Attempts to remove one Curse, one Disease and one Poison from the Imbiber." }, - { id = 13444, name = "Major Mana Potion", texture = "Interface\\Icons\\INV_Potion_76", description = "Restores 1350 to 2251 mana." }, - { id = 13446, name = "Major Healing Potion", texture = "Interface\\Icons\\INV_Potion_54", description = "Restores 1050 to 1751 health." }, - { id = 7676, name = "Thistle Tea", texture = "Interface\\Icons\\INV_Drink_Milk_05", description = "Instantly restores 100 energy." }, - { id = 20004, name = "Major Troll's Blood Potion", texture = "Interface\\Icons\\inv_potion_80", description = "Regenerate 20 health every 5 sec for 1 hour." }, - { id = 14530, name = "Heavy Runecloth Bandage", texture = "Interface\\Icons\\inv_misc_bandage_12", description = "Heals 2000 damage over 8 sec." }, - { id = 13180, name = "Stratholme Holy Water", texture = "Interface\\Icons\\inv_potion_75", description = "Inflicts between 438 and 562 damage to Undead in a 10 yard radius." }, + { id = 20008, name = "Living Action Potion", mats = {"2x Icecap", "2x Mountain Silversage", "2x Heart of the Wild", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_07", description = "Removes movement impairing effects and grants immunity to them for 5 seconds." }, + { id = 3387, name = "Limited Invulnerability Potion", mats = {"2x Blindweed", "1x Ghost Mushroom", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_62", description = "Makes you immune to physical attacks for 6 seconds." }, + { id = 5634, name = "Free Action Potion", mats = {"2x Blackmouth Oil", "1x Stranglekelp", "1x Leaded Vial"}, texture = "Interface\\Icons\\INV_Potion_04", description = "Grants immunity to stun and movement impairing effects for 30 seconds." }, + { id = 61225, name = "Lucidity Potion", mats = {"1x Murloc Eye", "1x Dreamfoil", "1x Purple Lotus", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_36", description = "Become immune to Sleep, Polymorph, and Charm effects for 30 sec." }, + { id = 9030, name = "Restorative Potion", mats = {"1x Elemental Earth", "1x Goldthorn", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_01", description = "Removes a magic, curse, poison, or disease effect every 5 seconds for 30 seconds." }, + { id = 4390, name = "Iron Grenade", mats = {"1x Iron Bar", "1x Heavy Blasting Powder", "1x Silk Cloth"}, texture = "Interface\\Icons\\INV_Misc_Bomb_08", description = "Inflicts 132 to 219 Fire damage and stuns enemies in a 3-yard radius for 3 seconds." }, + { id = 15993, name = "Thorium Grenade", mats = {"1x Thorium Widget", "3x Thorium Bar", "3x Dense Blasting Powder", "3x Runecloth"}, texture = "Interface\\Icons\\INV_Misc_Bomb_08", description = "Inflicts 300 to 501 Fire damage and stuns enemies in a 3-yard radius for 3 seconds." }, + { id = 10646, name = "Goblin Sapper Charge", mats = {"1x Mageweave Cloth", "3x Solid Blasting Powder", "1x Unstable Trigger"}, texture = "Interface\\Icons\\spell_fire_selfdestruct", description = "Deals 450-751 Fire damage to enemies within 10 yards, also damages you." }, + { id = 61675, name = "Nordanaar Herbal Tea", mats = {"Quest: 1x Small Dream Shard"}, texture = "Interface\\Icons\\INV_Drink_Milk_05", description = "Restores 525 to 875 health and 810 to 1350 mana." }, + { id = 13462, name = "Purification Potion", mats = {"2x Icecap", "2x Plaguebloom", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_31", description = "Attempts to remove one Curse, one Disease and one Poison from the Imbiber." }, + { id = 13444, name = "Major Mana Potion", mats = {"2x Dreamfoil", "2x Icecap", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_76", description = "Restores 1350 to 2251 mana." }, + { id = 13446, name = "Major Healing Potion", mats = {"2x Golden Sansam", "1x Mountain Silversage", "1x Crystal Vial"}, texture = "Interface\\Icons\\INV_Potion_54", description = "Restores 1050 to 1751 health."}, + { id = 7676, name = "Thistle Tea", mats = {"1x Swiftthistle", "1x Refreshing Spring Water"}, texture = "Interface\\Icons\\INV_Drink_Milk_05", description = "Instantly restores 100 energy." }, + { id = 20004, name = "Major Troll's Blood Potion", mats = {"1x Gromsblood", "2x Plaguebloom", "1x Crystal Vial"}, texture = "Interface\\Icons\\inv_potion_80", description = "Regenerate 20 health every 5 sec for 1 hour." }, + { id = 14530, name = "Heavy Runecloth Bandage", mats = {"2x Runecloth"}, texture = "Interface\\Icons\\inv_misc_bandage_12", description = "Heals 2000 damage over 8 sec." }, + { id = 13180, name = "Stratholme Holy Water", mats = {"Looted from Crates in Stratholme"}, texture = "Interface\\Icons\\inv_potion_75", description = "Inflicts between 438 and 562 damage to Undead in a 10 yard radius." } }, ["Food Buffs"] = { - { id = 21023, name = "Dirge's Kickin' Chimaerok Chops", texture = "Interface\\Icons\\INV_Misc_Food_65", description = "Increases Strength by 25 for 1 hour." }, - { id = 18254, name = "Runn Tum Tuber Surprise", texture = "Interface\\Icons\\INV_Misc_Food_63", description = "Increases Intellect by 10 for 1 hour." }, - { id = 13931, name = "Nightfin Soup", texture = "Interface\\Icons\\INV_Drink_17", description = "Restores 8 mana every 5 seconds for 1 hour." }, - { id = 20452, name = "Smoked Desert Dumplings", texture = "Interface\\Icons\\INV_Misc_Food_64", description = "Increases Strength by 20 for 1 hour." }, - { id = 51711, name = "Sweet Mountain Berry (Agility)", texture = "Interface\\Icons\\INV_Misc_Food_40", description = "Increases Agility by 10 for 1 hour." }, - { id = 51714, name = "Sweet Mountain Berry (Stamina)", texture = "Interface\\Icons\\INV_Misc_Food_40", description = "Increases Stamina by 10 for 1 hour." }, - { id = 51717, name = "Hardened Mushroom", texture = "Interface\\Icons\\INV_Mushroom_11", description = "Increases 25 Stamina for 15 minutes." }, - { id = 51720, name = "Power Mushroom", texture = "Interface\\Icons\\INV_Mushroom_11", description = "Increases 20 Strength for 15 minutes." }, - { id = 21151, name = "Rumsey Rum Black Label", texture = "Interface\\Icons\\INV_Drink_04", description = "Increases Stamina by 15 for 15 minutes." }, - { id = 12218, name = "Monster Omelet", texture = "Interface\\Icons\\INV_Misc_Food_06", description = "Increases 12 Stamina and 12 Spirit for 15 minutes." }, - { id = 60977, name = "Danonzo's Tel'Abim Delight", texture = "Interface\\Icons\\INV_Misc_Food_06", description = "Gain 22 spell damage for 15 minutes." }, - { id = 60978, name = "Danonzo's Tel'Abim Medley", texture = "Interface\\Icons\\INV_Misc_Food_07", description = "Increase haste by 2% for 15 minutes." }, - { id = 60976, name = "Danonzo's Tel'Abim Surprise", texture = "Interface\\Icons\\INV_Misc_Food_09", description = "Gain 45 Ranged Attack Power for 15 minutes." }, - { id = 84041, name = "Gilneas Hot Stew", texture = "Interface\\Icons\\inv_drink_19", description = "Gain 12 spell damage for 15 minutes." }, - { id = 21217, name = "Sagefish Delight", texture = "Interface\\Icons\\inv_misc_fish_21", description = "Gain 6 mana every 5 seconds for 15 minutes." } + { id = 21023, name = "Dirge's Kickin' Chimaerok Chops", mats = {"1x Hot Spices", "1x Goblin Rocket Fuel", "1x Deeprock Salt", "1x Chimaerok Tenderloin"}, texture = "Interface\\Icons\\INV_Misc_Food_65", description = "Increases Strength by 25 for 1 hour." }, + { id = 18254, name = "Runn Tum Tuber Surprise", mats = {"1x Runn Tum Tuber", "1 Soothing Spices"}, texture = "Interface\\Icons\\INV_Misc_Food_63", description = "Increases Intellect by 10 for 1 hour." }, + { id = 13931, name = "Nightfin Soup", mats = {"1x Raw Nightfin Snapper", "1x Refreshing Spring Water"}, texture = "Interface\\Icons\\INV_Drink_17", description = "Restores 8 mana every 5 seconds for 1 hour." }, + { id = 20452, name = "Smoked Desert Dumplings", mats = {"1x Sandworm Meat", "1x Soothing Spices"}, texture = "Interface\\Icons\\INV_Misc_Food_64", description = "Increases Strength by 20 for 1 hour." }, + { id = 51711, name = "Sweet Mountain Berry (Agility)", mats = {"Gardening: Mountain Berry Bush Seeds"}, texture = "Interface\\Icons\\INV_Misc_Food_40", description = "Increases Agility by 10 for 1 hour." }, + { id = 51714, name = "Sweet Mountain Berry (Stamina)", mats = {"Gardening: Mountain Berry Bush Seeds"}, texture = "Interface\\Icons\\INV_Misc_Food_40", description = "Increases Stamina by 10 for 1 hour." }, + { id = 51717, name = "Hardened Mushroom", mats = {"Gardening: Magic Mushroom Spores"}, texture = "Interface\\Icons\\INV_Mushroom_11", description = "Increases 25 Stamina for 15 minutes." }, + { id = 51720, name = "Power Mushroom", mats = {"Gardening: Magic Mushroom Spores"}, texture = "Interface\\Icons\\INV_Mushroom_11", description = "Increases 20 Strength for 15 minutes." }, + { id = 21151, name = "Rumsey Rum Black Label", mats = {"Diverse Attainable"}, texture = "Interface\\Icons\\INV_Drink_04", description = "Increases Stamina by 15 for 15 minutes." }, + { id = 12218, name = "Monster Omelet", mats = {"1x Giant Egg", "2x Soothing Spices"}, texture = "Interface\\Icons\\INV_Misc_Food_06", description = "Increases 12 Stamina and 12 Spirit for 15 minutes." }, + { id = 60977, name = "Danonzo's Tel'Abim Delight", mats = {"1x Gargantuan Tel'Abim Banana", "1x Soothing Spices", "1x Icecap"}, texture = "Interface\\Icons\\INV_Misc_Food_06", description = "Gain 22 spell damage for 15 minutes." }, + { id = 60978, name = "Danonzo's Tel'Abim Medley", mats = {"1x Gargantuan Tel'Abim Banana", "1x Soothing Spices", "2x Golden Sansam"}, texture = "Interface\\Icons\\INV_Misc_Food_07", description = "Increase haste by 2% for 15 minutes." }, + { id = 60976, name = "Danonzo's Tel'Abim Surprise", mats = {"1x Gargantuan Tel'Abim Banana", "1x Soothing Spices", "1x Heart of the Wild"}, texture = "Interface\\Icons\\INV_Misc_Food_09", description = "Gain 45 Ranged Attack Power for 15 minutes." }, + { id = 84041, name = "Gilneas Hot Stew", mats = {"1x Red Wolf Meat", "1x White Spider Meat", "1x Refreshing Spring Water"}, texture = "Interface\\Icons\\inv_drink_19", description = "Gain 12 spell damage for 15 minutes." }, + { id = 21217, name = "Sagefish Delight", mats = {"1x Raw Greater Sagefish", "1x Hot Spices"}, texture = "Interface\\Icons\\inv_misc_fish_21", description = "Gain 6 mana every 5 seconds for 15 minutes." } }, ["Special Buffs"] = { - { id = 12451, name = "Juju Power", texture = "Interface\\Icons\\INV_Misc_MonsterScales_11", description = "Increases Strength by 30 for 30 minutes." }, - { id = 12460, name = "Juju Might", texture = "Interface\\Icons\\INV_Misc_MonsterScales_07", description = "Increases Attack Power by 40 for 30 minutes." }, - { id = 12455, name = "Juju Ember", texture = "Interface\\Icons\\INV_Misc_MonsterScales_15", description = "Increases Fire Resistance by 15 for 30 minutes." }, - { id = 12457, name = "Juju Chill", texture = "Interface\\Icons\\INV_Misc_MonsterScales_09", description = "Increase Frost resistance by 15 for 10 minutes." }, - { id = 12820, name = "Winterfall Firewater", texture = "Interface\\Icons\\INV_Potion_92", description = "Increases Attack Power by 35 for 20 minutes." }, - { id = 20079, name = "Spirit of Zanza", texture = "Interface\\Icons\\INV_Potion_30", description = "Increases the player's Spirit by 50 and Stamina by 50 for 2 hours." }, - { id = 20081, name = "Swiftness of Zanza", texture = "Interface\\Icons\\inv_potion_31", description = "Increases the player's run speed by 20% for 2 hours." }, - { id = 8412, name = "Ground Scorpok Assay", texture = "Interface\\Icons\\inv_misc_dust_02", description = "Increases Agility by 25 for 1 hour." }, - { id = 8410, name = "R.O.I.D.S.", texture = "Interface\\Icons\\inv_stone_15", description = "Increases Strength by 25 for 1 hour." }, - { id = 8423, name = "Cerebral Cortex Compound", texture = "Interface\\Icons\\inv_potion_32", description = " Increases Intellect by 25 for 1 hour." } + { id = 12451, name = "Juju Power", mats = {"Quest: 3x Winterfall E'ko (Kill Furbolgs)"}, texture = "Interface\\Icons\\INV_Misc_MonsterScales_11", description = "Increases Strength by 30 for 30 minutes." }, + { id = 12460, name = "Juju Might", mats = {"Quest: 3x Frostmaul E'ko (Kill Frostmaul Giants)"}, texture = "Interface\\Icons\\INV_Misc_MonsterScales_07", description = "Increases Attack Power by 40 for 30 minutes." }, + { id = 12455, name = "Juju Ember", mats = {"Quest: 3x Shardtooth E'ko (Kill Bears)"}, texture = "Interface\\Icons\\INV_Misc_MonsterScales_15", description = "Increases Fire Resistance by 15 for 30 minutes." }, + { id = 12457, name = "Juju Chill", mats = {"Quest: 3x Chillwind E'ko (Kill Flying Chillwinds)"}, texture = "Interface\\Icons\\INV_Misc_MonsterScales_09", description = "Increase Frost resistance by 15 for 10 minutes." }, + { id = 12820, name = "Winterfall Firewater", mats = {"Drops from Furbolgs in Winterspring"}, texture = "Interface\\Icons\\INV_Potion_92", description = "Increases Attack Power by 35 for 20 minutes." }, + { id = 20079, name = "Spirit of Zanza", mats = {"Quest: 1x Zandalar Honor Token"}, texture = "Interface\\Icons\\INV_Potion_30", description = "Increases the player's Spirit by 50 and Stamina by 50 for 2 hours." }, + { id = 20081, name = "Swiftness of Zanza", mats = {"Quest: 1x Zandalar Honor Token"}, texture = "Interface\\Icons\\inv_potion_31", description = "Increases the player's run speed by 20% for 2 hours." }, + { id = 8412, name = "Ground Scorpok Assay", mats = {"Quest: 1x Blasted Boar Lung", "2x Vulture Gizzard", "3x Scorpok Pincer"}, texture = "Interface\\Icons\\inv_misc_dust_02", description = "Increases Agility by 25 for 1 hour." }, + { id = 8410, name = "R.O.I.D.S.", mats = {"Quest: 1x Scorpok Pincer", "2x Blasted Boar Lung", "3x Snickerfang Jowl"}, texture = "Interface\\Icons\\inv_stone_15", description = "Increases Strength by 25 for 1 hour." }, + { id = 8423, name = "Cerebral Cortex Compound", mats = {"Quest: 10x Basilisk Brain", "2x Vulture Gizzard"}, texture = "Interface\\Icons\\inv_potion_32", description = " Increases Intellect by 25 for 1 hour." } } } + -- Initialize tables + consumablesList = {} + consumablesNameToID = {} + consumablesTexture = {} + consumablesDescription = {} + consumablesMats = {} - --- Initialize tables -consumablesList = {} -consumablesNameToID = {} -consumablesTexture = {} -consumablesDescription = {} - --- Populate consumablesList and other lookup tables -for categoryName, consumables in pairs(consumablesCategories) do - for _, consumable in ipairs(consumables) do - consumablesList[consumable.id] = consumable.name - consumablesNameToID[consumable.name] = consumable.id - consumablesTexture[consumable.id] = consumable.texture - consumablesDescription[consumable.id] = consumable.description - end -end - + -- Populate consumablesList and other lookup tables + for categoryName, consumables in pairs(consumablesCategories) do + for _, consumable in ipairs(consumables) do + consumablesList[consumable.id] = consumable.name + consumablesNameToID[consumable.name] = consumable.id + consumablesTexture[consumable.id] = consumable.texture + consumablesDescription[consumable.id] = consumable.description + consumablesMats[consumable.id] = consumable.mats or {} + end + end \ No newline at end of file diff --git a/Main.lua b/Main.lua index b9a08a5..0f2d407 100644 --- a/Main.lua +++ b/Main.lua @@ -1,66 +1,47 @@ --- Metadata --------------------------------------------------------------------------------------------- -AddonName = "Consumes Manager" -Version = "1.8" -VState = "release" -WindowWidth = 350 - -- Onload & Click Functionality ------------------------------------------------------------------------- -function ConsumesManager_OnLoad(self) - self:RegisterForDrag("LeftButton") - self:SetScript("OnDragStart", function() ConsumesManager_OnDragStart(self) end) - self:SetScript("OnDragStop", function() ConsumesManager_OnDragStop(self) end) + WindowWidth = 350 - -- Create a frame for the delay - local delayFrame = CreateFrame("Frame") - local elapsed = 0 - local delay = 1 + function ConsumesManager_OnLoad(self) + self:RegisterForDrag("LeftButton") + self:SetScript("OnDragStart", function() ConsumesManager_OnDragStart(self) end) + self:SetScript("OnDragStop", function() ConsumesManager_OnDragStop(self) end) + end - delayFrame:SetScript("OnUpdate", function() - elapsed = elapsed + arg1 -- arg1 provides the time since the last frame - if elapsed >= delay then - DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00" .. AddonName .. "|r |cffaaaaaa(v" .. Version .. "-" .. VState .. ")|r |cffffffffLoaded!|r |cffaaaaaa- Made by Horyoshi|r") - delayFrame:SetScript("OnUpdate", nil) -- Stop the OnUpdate script - end - end) -end - -function ConsumesManager_HandleClick(self, button) - if not self then return end - if button == "LeftButton" and not IsShiftKeyDown() then - if ConsumesManager_MainFrame and ConsumesManager_MainFrame:IsShown() then - ConsumesManager_MainFrame:Hide() - else - ConsumesManager_ShowMainWindow() + function ConsumesManager_HandleClick(self, button) + if not self then return end + if button == "LeftButton" and not IsShiftKeyDown() then + if ConsumesManager_MainFrame and ConsumesManager_MainFrame:IsShown() then + ConsumesManager_MainFrame:Hide() + else + ConsumesManager_ShowMainWindow() + end end end -end - -function ConsumesManager_OnDragStart(self) - if IsShiftKeyDown() then - self:StartMoving() - self.isMoving = true + function ConsumesManager_OnDragStart(self) + if IsShiftKeyDown() then + self:StartMoving() + self.isMoving = true + end end -end -function ConsumesManager_OnDragStop(self) - if self and self.isMoving then - self:StopMovingOrSizing() - self.isMoving = false + function ConsumesManager_OnDragStop(self) + if self and self.isMoving then + self:StopMovingOrSizing() + self.isMoving = false + end end -end -if not ConsumesManager_Options then - ConsumesManager_Options = {} -end -if not ConsumesManager_SelectedItems then - ConsumesManager_SelectedItems = {} -end -if not ConsumesManager_Data then - ConsumesManager_Data = {} -end - + if not ConsumesManager_Options then + ConsumesManager_Options = {} + end + if not ConsumesManager_SelectedItems then + ConsumesManager_SelectedItems = {} + end + if not ConsumesManager_Data then + ConsumesManager_Data = {} + end -- Event frame for updating data ------------------------------------------------------------------------ local ConsumesManager_EventFrame = CreateFrame("Frame") @@ -75,6 +56,31 @@ end ConsumesManager_EventFrame:SetScript("OnEvent", function() if event == "PLAYER_LOGIN" then + if ConsumesManager_Options and ConsumesManager_Options.Channel and ConsumesManager_Options.Password then + local channelName = DecodeMessage(ConsumesManager_Options.Channel) + local channelPassword = DecodeMessage(ConsumesManager_Options.Password) + JoinChannelByName(channelName, channelPassword) + SetChannelPassword(channelName, channelPassword) + MultiAccountChannelAnnounce = "|cffffffff" .. GetAddOnMetadata("ConsumesManager", "Title") .. ":|r |cffffffffJoined|r |cffffc0c0[" .. channelName .. "]|r|cffffffff. Multi-account synchronization |cff00ff00enabled|r|cffffffff.|r" + ReadData("start") + else + MultiAccountChannelAnnounce = "|cffffffff" .. GetAddOnMetadata("ConsumesManager", "Title") .. ":|r Multi-account synchronization |cffff0000disabled|r|cffffffff. Check the addon options for setup.|r" + ReadData("stop") + end + + local delayFrame = CreateFrame("Frame") + local elapsed = 0 + local delay = 1 + + delayFrame:SetScript("OnUpdate", function() + elapsed = elapsed + arg1 -- arg1 provides the time since the last frame + if elapsed >= delay then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00" .. GetAddOnMetadata("ConsumesManager", "Title") .. "|r |cffaaaaaa(v" .. GetAddOnMetadata("ConsumesManager", "Version") .. ")|r |cffffffffLoaded!|r") + DEFAULT_CHAT_FRAME:AddMessage(MultiAccountChannelAnnounce) + delayFrame:SetScript("OnUpdate", nil) -- Stop the OnUpdate script + end + end) + ConsumesManager_ScanPlayerInventory() ConsumesManager_ScanPlayerBank() ConsumesManager_ScanPlayerMail() @@ -88,7 +94,7 @@ end end elseif event == "BANKFRAME_OPENED" then ConsumesManager_ScanPlayerBank() - elseif event == "PLAYERBANKSLOTS_CHANGED" then + elseif event == "PLAYERBANKSLOTS_CHANGED" then ConsumesManager_ScanPlayerBank() ConsumesManager_ScanPlayerInventory() elseif event == "ITEM_LOCK_CHANGED" then @@ -141,10 +147,44 @@ function ConsumesManager_CreateMainWindow() }) ConsumesManager_MainFrame:SetBackdropColor(0.1, 0.1, 0.1, 1) + -- Progress Bar for Syncing + ProgressBarFrame = CreateFrame("Frame", "ConsumesManager_ProgressBar", ConsumesManager_MainFrame, BackdropTemplateMixin and "BackdropTemplate") + ProgressBarFrame:SetWidth(20) + ProgressBarFrame:SetHeight(496) + ProgressBarFrame:SetPoint("TOPRIGHT", ConsumesManager_MainFrame, "TOPRIGHT", 10, -8) + ProgressBarFrame:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + edgeSize = 1, + }) + ProgressBarFrame:SetBackdropColor(0,0,0,1) + ProgressBarFrame:SetBackdropBorderColor(0.5,0.5,0.5,1) + ProgressBarFrame:SetFrameLevel(ConsumesManager_MainFrame:GetFrameLevel() - 1) + ProgressBarFrame:Hide() + + ProgressBarFrame_fill = CreateFrame("Frame", "ConsumesManager_ProgressBarFill", ProgressBarFrame, BackdropTemplateMixin and "BackdropTemplate") + ProgressBarFrame_fill:SetWidth(17) + ProgressBarFrame_fill:SetHeight(0) + ProgressBarFrame_fill:SetPoint("BOTTOMLEFT", ProgressBarFrame, "BOTTOMLEFT", 1, 2) + ProgressBarFrame_fill:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8" + }) + ProgressBarFrame_fill:SetBackdropColor(0,0.6,0,1) + ProgressBarFrame_fill:Hide() + ProgressBarFrame_fill:SetFrameLevel(ProgressBarFrame:GetFrameLevel() + 1) + + + ProgressBarFrame_Text = ProgressBarFrame_fill:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + ProgressBarFrame_Text:SetText("S\n\nY\n\nN\n\nC\n\nI\n\nN\n\nG") + ProgressBarFrame_Text:SetPoint("CENTER", ProgressBarFrame, "CENTER", 1, 0) + ProgressBarFrame_Text:SetTextColor(1,1,1) + ProgressBarFrame_Text:Hide() + + -- Title Text local titleText = ConsumesManager_MainFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") - titleText:SetText(AddonName) - titleText:SetPoint("TOP", ConsumesManager_MainFrame, "TOP", 0, 0) + titleText:SetText("Consumes Manager") + titleText:SetPoint("TOP", ConsumesManager_MainFrame, "TOP", 0, -2) -- Calculate the width of the title text and adjust the title background accordingly local titleWidth = titleText:GetStringWidth() + 200 @@ -237,7 +277,6 @@ function ConsumesManager_CreateMainWindow() return tab end - -- Manager Tab @@ -268,6 +307,25 @@ function ConsumesManager_CreateMainWindow() end tab4:SetScript("OnClick", tab4.originalOnClick) + + -- Send Data Button + sendDataButton = CreateTab("ConsumesManager_sendDataButton", "Interface\\Icons\\inv_misc_punchcards_prismatic", 280, "Push Data", 5) + function updateSenDataButtonState() + if ConsumesManager_Options.Channel == nil or ConsumesManager_Options.Channel == "" or ConsumesManager_Options.Password == nil or ConsumesManager_Options.Password == "" then + sendDataButton:Hide() + ReadData("stop") + else + sendDataButton:Show() + ReadData("start") + end + end + updateSenDataButtonState() + sendDataButton.originalOnClick = function() + PushData() + end + sendDataButton:SetScript("OnClick", sendDataButton.originalOnClick) + + -- Add Grey Line Under Tabs local tabsLine = ConsumesManager_MainFrame:CreateTexture(nil, "ARTWORK") tabsLine:SetHeight(1) @@ -302,12 +360,13 @@ function ConsumesManager_CreateMainWindow() tab4Content:SetPoint("TOPLEFT", ConsumesManager_MainFrame, "TOPLEFT", 30, -80) ConsumesManager_MainFrame.tabs[4] = tab4Content - -- Footer Text + -- Footer Button to Push Database local footerText = ConsumesManager_MainFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") - footerText:SetText("Made by Horyoshi (v" .. Version .. "-" .. VState .. ")") + footerText:SetText("Made by Horyoshi (v" .. GetAddOnMetadata("ConsumesManager", "Version") .. ")") footerText:SetTextColor(0.6, 0.6, 0.6) footerText:SetPoint("BOTTOM", ConsumesManager_MainFrame, "BOTTOM", 0, 15) + -- Add Grey Line Above Footer local footerLine = ConsumesManager_MainFrame:CreateTexture(nil, "ARTWORK") footerLine:SetHeight(1) @@ -348,6 +407,8 @@ function ConsumesManager_ShowMainWindow() ConsumesManager_UpdateManagerContent() -- Update the Presets content ConsumesManager_UpdatePresetsConsumables() + ConsumesManager_UpdateSettingsContent() + ConsumesManager_UpdateManagerContent() end @@ -900,7 +961,6 @@ function ConsumesManager_UpdateManagerContent() end end - function ConsumesManager_UpdateManagerScrollBar() local ManagerFrame = ConsumesManager_MainFrame.tabs[1] local scrollBar = ManagerFrame.scrollBar @@ -1235,14 +1295,68 @@ end -- Presets Window ----------------------------------------------------------------------------------- + + local classColors = { + ["Rogue"] = "fff569", + ["Mage"] = "69ccf0", + ["Warrior"] = "c79c6e", + ["Hunter"] = "abd473", + ["Druid"] = "ff7d0a", + ["Priest"] = "ffffff", + ["Warlock"] = "9482c9", + ["Shaman"] = "0070dd", + ["Paladin"] = "f58cba" + } + + -- Manually ordered raids (adjust as needed) + local orderedRaids = { + "Molten Core", + "Blackwing Lair", + "Emerald Sanctum", + "Temple of Ahn'Qiraj", + "Naxxramas", + "The Tower of Karazhan" + } + + local function GetLastWord(str) + local spacePos = 0 + while true do + local found = string.find(str, " ", spacePos + 1) + if found then + spacePos = found + else + break + end + end + if spacePos == 0 then + return str + else + return string.sub(str, spacePos + 1) + end + end + + local function SortClassesByLastWord(t) + local n = table.getn(t) + local i = 1 + while i < n do + local j = i + 1 + while j <= n do + local lwA = GetLastWord(t[i]) + local lwB = GetLastWord(t[j]) + if lwA > lwB then + t[i], t[j] = t[j], t[i] + end + j = j + 1 + end + i = i + 1 + end + end + function ConsumesManager_CreatePresetsContent(parentFrame) - -- Define line height for consumable entries local lineHeight = 18 - -- Create Dropdowns for Raids and Classes - -- Raid Dropdown local raidDropdown = CreateFrame("Frame", "ConsumesManager_PresetsRaidDropdown", parentFrame, "UIDropDownMenuTemplate") - raidDropdown:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", -20, 0) -- Adjusted for better positioning + raidDropdown:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", -20, 0) UIDropDownMenu_SetWidth(120, raidDropdown) UIDropDownMenu_SetText("Select |cffffff00Raid|r", raidDropdown) @@ -1251,9 +1365,8 @@ function ConsumesManager_CreatePresetsContent(parentFrame) raidDropdownText:SetJustifyH("LEFT") end - -- Class Dropdown local classDropdown = CreateFrame("Frame", "ConsumesManager_PresetsClassDropdown", parentFrame, "UIDropDownMenuTemplate") - classDropdown:SetPoint("LEFT", raidDropdown, "RIGHT", -20, 0) -- Positive offset to prevent overlap + classDropdown:SetPoint("LEFT", raidDropdown, "RIGHT", -20, 0) UIDropDownMenu_SetWidth(120, classDropdown) UIDropDownMenu_SetText("Select |cffffff00Class|r", classDropdown) @@ -1262,65 +1375,49 @@ function ConsumesManager_CreatePresetsContent(parentFrame) classDropdownText:SetJustifyH("LEFT") end - -- Initialize dropdown menus local classes = {} - - -- Populate Classes Dropdown for className, _ in pairs(classPresets) do table.insert(classes, className) end - -- Manual sort of classes (bubble sort) - for i = 1, table.getn(classes) - 1 do - for j = i + 1, table.getn(classes) do - if classes[i] > classes[j] then - classes[i], classes[j] = classes[j], classes[i] - end - end - end + SortClassesByLastWord(classes) UIDropDownMenu_Initialize(classDropdown, function() - local index = 1 - while classes[index] do - local className = classes[index] - local currentIndex = index -- Capture the current index - local currentClassName = className -- Capture the current class name + local idx = 1 + while classes[idx] do + local cName = classes[idx] + local cIndex = idx local info = {} - info.text = className + local lastWord = GetLastWord(cName) + local color = classColors[lastWord] or "ffffff" + info.text = "|cff" .. color .. cName .. "|r" info.func = function() - UIDropDownMenu_SetSelectedID(classDropdown, currentIndex) - ConsumesManager_SelectedClass = currentClassName + UIDropDownMenu_SetSelectedID(classDropdown, cIndex) + ConsumesManager_SelectedClass = cName ConsumesManager_UpdateRaidsDropdown() ConsumesManager_UpdatePresetsConsumables() end UIDropDownMenu_AddButton(info) - index = index + 1 + idx = idx + 1 end end) - -- Extract uniqueRaids from first class - local uniqueRaids = {} - local seenRaids = {} + -- Build initial raid list from the first class, but we won't sort them automatically + -- We rely on our manual "orderedRaids" list + local uniqueRaids, seen = {}, {} local count = 0 local firstClass = next(classPresets) if firstClass then - local classPresetsList = classPresets[firstClass] - for i = 1, table.getn(classPresetsList) do - local raidName = classPresetsList[i].raid - if not seenRaids[raidName] then + local classList = classPresets[firstClass] + local i = 1 + while classList[i] do + local rName = classList[i].raid + if not seen[rName] then count = count + 1 - uniqueRaids[count] = raidName - seenRaids[raidName] = true - end - end - end - - -- Manual sort uniqueRaids (bubble sort) - for i = 1, count - 1 do - for j = i + 1, count do - if uniqueRaids[i] > uniqueRaids[j] then - uniqueRaids[i], uniqueRaids[j] = uniqueRaids[j], uniqueRaids[i] + uniqueRaids[count] = rName + seen[rName] = true end + i = i + 1 end end @@ -1333,47 +1430,42 @@ function ConsumesManager_CreatePresetsContent(parentFrame) UIDropDownMenu_SetText("Select |cffffff00Raid|r", raidDropdown) return end - local i = 1 - while uniqueRaids[i] do - local raidName = uniqueRaids[i] - local currentIndex = i -- Capture the current index - local currentRaidName = raidName -- Capture the current raid name - local info = {} - info.text = raidName - info.func = function() - UIDropDownMenu_SetSelectedID(raidDropdown, currentIndex) - ConsumesManager_SelectedRaid = currentRaidName - ConsumesManager_UpdatePresetsConsumables() + while orderedRaids[i] do + local orName = orderedRaids[i] + if seen[orName] then + local cIndex = i + local cRaidName = orName + local info = {} + info.text = orName + info.func = function() + UIDropDownMenu_SetSelectedID(raidDropdown, cIndex) + ConsumesManager_SelectedRaid = cRaidName + ConsumesManager_UpdatePresetsConsumables() + end + UIDropDownMenu_AddButton(info) end - UIDropDownMenu_AddButton(info) i = i + 1 end - - -- Reset the dropdown text to default after populating UIDropDownMenu_SetText("Select |cffffff00Raid|r", raidDropdown) end) - -- Do not auto-select a raid UIDropDownMenu_SetSelectedID(raidDropdown, 0) - -- Scroll Frame local scrollFrame = CreateFrame("ScrollFrame", "ConsumesManager_PresetsScrollFrame", parentFrame) - scrollFrame:SetPoint("TOPLEFT", classDropdown, "BOTTOMLEFT", -135, -40) -- Positioned below the dropdowns - scrollFrame:SetPoint("BOTTOMRIGHT", parentFrame, "BOTTOMRIGHT", -25, -5) -- Adjusted to fit within parentFrame + scrollFrame:SetPoint("TOPLEFT", classDropdown, "BOTTOMLEFT", -135, -40) + scrollFrame:SetPoint("BOTTOMRIGHT", parentFrame, "BOTTOMRIGHT", -25, -5) scrollFrame:EnableMouseWheel(true) - -- Scroll Child Frame local scrollChild = CreateFrame("Frame", nil, scrollFrame) - scrollChild:SetWidth(parentFrame:GetWidth() - 40) -- Adjusted width - scrollChild:SetHeight(1) -- Initial height + scrollChild:SetWidth(parentFrame:GetWidth() - 40) + scrollChild:SetHeight(1) scrollFrame:SetScrollChild(scrollChild) parentFrame.scrollChild = scrollChild parentFrame.scrollFrame = scrollFrame - -- Scroll Bar local scrollBar = CreateFrame("Slider", "ConsumesManager_PresetsScrollBar", parentFrame) - scrollBar:SetPoint("TOPRIGHT", parentFrame, "TOPRIGHT", -2, -35) -- Adjusted to be below the buttons + scrollBar:SetPoint("TOPRIGHT", parentFrame, "TOPRIGHT", -2, -35) scrollBar:SetPoint("BOTTOMRIGHT", parentFrame, "BOTTOMRIGHT", -2, 16) scrollBar:SetWidth(16) scrollBar:SetOrientation('VERTICAL') @@ -1381,29 +1473,30 @@ function ConsumesManager_CreatePresetsContent(parentFrame) scrollBar:SetBackdrop({ bgFile = "Interface\\Buttons\\UI-SliderBar-Background", edgeFile = "Interface\\Buttons\\UI-SliderBar-Border", - tile = true, tileSize = 8, edgeSize = 8, + tile = 1, tileSize = 8, edgeSize = 8, insets = { left = 3, right = 3, top = 3, bottom = 3 } }) scrollBar:SetScript("OnValueChanged", function() - local value = this:GetValue() - parentFrame.scrollFrame:SetVerticalScroll(value) + local val = this:GetValue() + parentFrame.scrollFrame:SetVerticalScroll(val) end) parentFrame.scrollBar = scrollBar - - -- Initially hide the scrollbar scrollBar:Hide() - -- OnMouseWheel Script Correction scrollFrame:SetScript("OnMouseWheel", function() - local delta = arg1 - local current = this:GetVerticalScroll() - local maxScroll = this.range or 0 -- Changed from this.maxScroll to this.range - local newScroll = math.max(0, math.min(current - (delta * 20), maxScroll)) - this:SetVerticalScroll(newScroll) - parentFrame.scrollBar:SetValue(newScroll) + local d = arg1 + local cur = this:GetVerticalScroll() + local mx = this.range or 0 + local new = 0 + if d < 0 then + new = math.min(cur + 20, mx) + else + new = math.max(cur - 20, 0) + end + this:SetVerticalScroll(new) + parentFrame.scrollBar:SetValue(new) end) - -- Order Buttons local orderByNameButton = CreateFrame("Button", "ConsumesManager_PresetsOrderByNameButton", parentFrame, "UIPanelButtonTemplate") orderByNameButton:SetWidth(100) orderByNameButton:SetHeight(24) @@ -1412,7 +1505,6 @@ function ConsumesManager_CreatePresetsContent(parentFrame) orderByNameButton:SetPoint("TOPLEFT", ConsumesManager_MainFrame.tabs[3], "TOPLEFT", -4, -35) orderByNameButton:SetScript("OnClick", function() if ConsumesManager_Options.presetsSortOrder == "name" then - -- Toggle sort direction if ConsumesManager_Options.presetsSortDirection == "asc" then ConsumesManager_Options.presetsSortDirection = "desc" else @@ -1433,7 +1525,6 @@ function ConsumesManager_CreatePresetsContent(parentFrame) orderByAmountButton:SetPoint("LEFT", orderByNameButton, "RIGHT", 10, 0) orderByAmountButton:SetScript("OnClick", function() if ConsumesManager_Options.presetsSortOrder == "amount" then - -- Toggle sort direction if ConsumesManager_Options.presetsSortDirection == "desc" then ConsumesManager_Options.presetsSortDirection = "asc" else @@ -1448,15 +1539,10 @@ function ConsumesManager_CreatePresetsContent(parentFrame) parentFrame.orderByNameButton = orderByNameButton parentFrame.orderByAmountButton = orderByAmountButton - - -- Initially hide the order buttons orderByNameButton:Hide() orderByAmountButton:Hide() - -- Consumables List parentFrame.presetsConsumables = {} - - -- Message Label local messageLabel = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") messageLabel:SetText("|cffff0000Please select both a Raid and a Class.|r") messageLabel:SetPoint("CENTER", parentFrame, "CENTER", 0, 0) @@ -1469,75 +1555,58 @@ function ConsumesManager_UpdateRaidsDropdown() if not raidDropdown then return end - - -- Store the previously selected raid - local previousSelectedRaid = ConsumesManager_SelectedRaid - + local prevRaid = ConsumesManager_SelectedRaid UIDropDownMenu_ClearAll(raidDropdown) - local uniqueRaids = {} - local seenRaids = {} - local count = 0 - + local uniqueRaids, seen = {}, {} + local c = 0 if ConsumesManager_SelectedClass and classPresets[ConsumesManager_SelectedClass] then - local classPresetsList = classPresets[ConsumesManager_SelectedClass] - for i = 1, table.getn(classPresetsList) do - local raidName = classPresetsList[i].raid - if not seenRaids[raidName] then - count = count + 1 - uniqueRaids[count] = raidName - seenRaids[raidName] = true - end - end - end - - -- Manual sort uniqueRaids (bubble sort) - for i = 1, count - 1 do - for j = i + 1, count do - if uniqueRaids[i] > uniqueRaids[j] then - uniqueRaids[i], uniqueRaids[j] = uniqueRaids[j], uniqueRaids[i] + local clList = classPresets[ConsumesManager_SelectedClass] + local i = 1 + while clList[i] do + local rName = clList[i].raid + if not seen[rName] then + c = c + 1 + uniqueRaids[c] = rName + seen[rName] = true end + i = i + 1 end end UIDropDownMenu_Initialize(raidDropdown, function() - if count == 0 then + if c == 0 then local info = {} info.text = "No Raids Available" - info.disabled = true + info.disabled = 1 UIDropDownMenu_AddButton(info) UIDropDownMenu_SetText("Select |cffffff00Raid|r", raidDropdown) return end - local i = 1 - local selectedIndex = 0 -- To track if previous raid exists - while uniqueRaids[i] do - local raidName = uniqueRaids[i] - local currentIndex = i -- Capture the current index - local currentRaidName = raidName -- Capture the current raid name - local info = {} - info.text = raidName - info.func = function() - UIDropDownMenu_SetSelectedID(raidDropdown, currentIndex) - ConsumesManager_SelectedRaid = currentRaidName - ConsumesManager_UpdatePresetsConsumables() + local selectedIndex = 0 + while orderedRaids[i] do + local raidName = orderedRaids[i] + if seen[raidName] then + local currentIndex = i + local currentRaidName = raidName + local info = {} + info.text = raidName + info.func = function() + UIDropDownMenu_SetSelectedID(raidDropdown, currentIndex) + ConsumesManager_SelectedRaid = currentRaidName + ConsumesManager_UpdatePresetsConsumables() + end + UIDropDownMenu_AddButton(info) + if raidName == prevRaid then + selectedIndex = i + end end - UIDropDownMenu_AddButton(info) - - -- Check if this raid was previously selected - if raidName == previousSelectedRaid then - selectedIndex = i - end - i = i + 1 end - - -- Set the dropdown to the previous selection if it exists if selectedIndex > 0 then UIDropDownMenu_SetSelectedID(raidDropdown, selectedIndex) else - -- If the previous raid isn't available, reset selection UIDropDownMenu_SetSelectedID(raidDropdown, 0) UIDropDownMenu_SetText("Select |cffffff00Raid|r", raidDropdown) ConsumesManager_SelectedRaid = nil @@ -1545,6 +1614,7 @@ function ConsumesManager_UpdateRaidsDropdown() end) end + function ConsumesManager_UpdatePresetsScrollBar() local PresetsFrame = ConsumesManager_MainFrame.tabs[3] if not PresetsFrame then @@ -2089,10 +2159,9 @@ end - -- Settings Window ----------------------------------------------------------------------------------- function ConsumesManager_CreateSettingsContent(parentFrame) - -- Scroll Frame + -- Scroll Frame Setup local scrollFrame = CreateFrame("ScrollFrame", "ConsumesManager_SettingsScrollFrame", parentFrame) scrollFrame:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", 0, 0) scrollFrame:SetPoint("BOTTOMRIGHT", parentFrame, "BOTTOMRIGHT", -20, 0) @@ -2106,7 +2175,6 @@ function ConsumesManager_CreateSettingsContent(parentFrame) parentFrame.scrollBar:SetValue(newScroll) end) - -- Scroll Child Frame local scrollChild = CreateFrame("Frame", nil, scrollFrame) scrollChild:SetWidth(WindowWidth - 10) scrollChild:SetHeight(1) @@ -2114,33 +2182,25 @@ function ConsumesManager_CreateSettingsContent(parentFrame) parentFrame.scrollChild = scrollChild parentFrame.scrollFrame = scrollFrame - -- Initialize variables parentFrame.checkboxes = {} - local index = 0 -- Position index - local lineHeight = 20 -- Increased to accommodate spacing + local index = 0 + local lineHeight = 20 - -- Ensure settings table for characters exists ConsumesManager_Options["Characters"] = ConsumesManager_Options["Characters"] or {} + ConsumesManager_Options.enableCategories = (ConsumesManager_Options.enableCategories == nil) and true or ConsumesManager_Options.enableCategories + ConsumesManager_Options.showUseButton = (ConsumesManager_Options.showUseButton == nil) and true or ConsumesManager_Options.showUseButton - -- **ADDED**: Initialize enableCategories and showUseButton with default values if nil - ConsumesManager_Options.enableCategories = ConsumesManager_Options.enableCategories == nil and true or ConsumesManager_Options.enableCategories - ConsumesManager_Options.showUseButton = ConsumesManager_Options.showUseButton == nil and true or ConsumesManager_Options.showUseButton - - -- Get list of characters local realmName = GetRealmName() local faction = UnitFactionGroup("player") local playerName = UnitName("player") local characterList = {} - - -- Ensure data structure exists if ConsumesManager_Data[realmName] and ConsumesManager_Data[realmName][faction] then for characterName, _ in pairs(ConsumesManager_Data[realmName][faction]) do table.insert(characterList, characterName) end end - -- Ensure current character is included local playerInList = false for _, name in ipairs(characterList) do if name == playerName then @@ -2152,59 +2212,47 @@ function ConsumesManager_CreateSettingsContent(parentFrame) table.insert(characterList, playerName) end - -- Sort the character list table.sort(characterList) - -- Create FontString for title + -- Title local title = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") - title:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, 0) -- Offset from the top + title:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, 0) title:SetText("Select Characters To Track") title:SetTextColor(1, 1, 1) - -- Offset index to start below the title local startYOffset = -20 - -- For each character + -- Character Checkboxes for _, characterName in ipairs(characterList) do index = index + 1 - - -- Create a local copy of characterName for the closure local currentCharacterName = characterName - -- Create a frame that encompasses the checkbox and label local itemFrame = CreateFrame("Frame", "ConsumesManager_CharacterFrame" .. index, scrollChild) itemFrame:SetWidth(WindowWidth - 10) itemFrame:SetHeight(18) itemFrame:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, startYOffset - (index - 1) * lineHeight) - -- Create the checkbox inside the itemFrame local checkbox = CreateFrame("CheckButton", "ConsumesManager_CharacterCheckbox" .. index, itemFrame) checkbox:SetWidth(16) checkbox:SetHeight(16) checkbox:SetPoint("LEFT", itemFrame, "LEFT", 0, 0) - -- Create Textures for the checkbox checkbox:SetNormalTexture("Interface\\Buttons\\UI-CheckBox-Up") checkbox:SetPushedTexture("Interface\\Buttons\\UI-CheckBox-Down") checkbox:SetHighlightTexture("Interface\\Buttons\\UI-CheckBox-Highlight") checkbox:SetCheckedTexture("Interface\\Buttons\\UI-CheckBox-Check") - -- Create FontString for label local label = itemFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") label:SetPoint("LEFT", checkbox, "RIGHT", 4, 0) label:SetText(currentCharacterName) label:SetJustifyH("LEFT") - -- Set up the checkbox OnClick handler checkbox:SetScript("OnClick", function() ConsumesManager_Options["Characters"][currentCharacterName] = (checkbox:GetChecked() == 1) ConsumesManager_UpdateAllContent() end) - - -- Load saved setting if ConsumesManager_Options["Characters"][currentCharacterName] == nil then - -- Default to checked checkbox:SetChecked(true) ConsumesManager_Options["Characters"][currentCharacterName] = true else @@ -2212,27 +2260,23 @@ function ConsumesManager_CreateSettingsContent(parentFrame) end parentFrame.checkboxes[currentCharacterName] = checkbox - - -- Make the itemFrame clickable (so clicking on the label checks/unchecks the box) itemFrame:EnableMouse(true) itemFrame:SetScript("OnMouseDown", function() checkbox:Click() end) end - -- Add spacing of 20 below the character list index = index + 1 - -- Create 'General Settings' title with spacing of 20 + -- General Settings Title local generalSettingsTitle = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") generalSettingsTitle:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, startYOffset - index * lineHeight) generalSettingsTitle:SetText("General Settings") generalSettingsTitle:SetTextColor(1, 1, 1) - -- Move index down for the checkboxes index = index + 1 - -- Create 'Enable Categories' checkbox + -- Enable Categories Checkbox local enableCategoriesFrame = CreateFrame("Frame", "ConsumesManager_EnableCategoriesFrame", scrollChild) enableCategoriesFrame:SetWidth(WindowWidth - 10) enableCategoriesFrame:SetHeight(18) @@ -2243,21 +2287,14 @@ function ConsumesManager_CreateSettingsContent(parentFrame) enableCategoriesCheckbox:SetWidth(16) enableCategoriesCheckbox:SetHeight(16) enableCategoriesCheckbox:SetPoint("LEFT", enableCategoriesFrame, "LEFT", 0, 0) - enableCategoriesCheckbox:SetNormalTexture("Interface\\Buttons\\UI-CheckBox-Up") enableCategoriesCheckbox:SetPushedTexture("Interface\\Buttons\\UI-CheckBox-Down") enableCategoriesCheckbox:SetHighlightTexture("Interface\\Buttons\\UI-CheckBox-Highlight") enableCategoriesCheckbox:SetCheckedTexture("Interface\\Buttons\\UI-CheckBox-Check") - - -- **UPDATED**: Set the checkbox state based on ConsumesManager_Options.enableCategories enableCategoriesCheckbox:SetChecked(ConsumesManager_Options.enableCategories) enableCategoriesCheckbox:SetScript("OnClick", function() - if enableCategoriesCheckbox:GetChecked() then - ConsumesManager_Options.enableCategories = true - else - ConsumesManager_Options.enableCategories = false - end + ConsumesManager_Options.enableCategories = enableCategoriesCheckbox:GetChecked() ConsumesManager_UpdateAllContent() end) @@ -2265,15 +2302,13 @@ function ConsumesManager_CreateSettingsContent(parentFrame) enableCategoriesLabel:SetPoint("LEFT", enableCategoriesCheckbox, "RIGHT", 4, 0) enableCategoriesLabel:SetText("Enable Categories") enableCategoriesLabel:SetJustifyH("LEFT") - - -- Make the frame clickable (so clicking on the label checks/unchecks the box) enableCategoriesFrame:SetScript("OnMouseDown", function() enableCategoriesCheckbox:Click() end) - index = index + 1 -- Move index down for next checkbox + index = index + 1 - -- Create 'Show Use Button' checkbox + -- Show Use Button Checkbox local showUseButtonFrame = CreateFrame("Frame", "ConsumesManager_ShowUseButtonFrame", scrollChild) showUseButtonFrame:SetWidth(WindowWidth - 10) showUseButtonFrame:SetHeight(18) @@ -2284,59 +2319,263 @@ function ConsumesManager_CreateSettingsContent(parentFrame) showUseButtonCheckbox:SetWidth(16) showUseButtonCheckbox:SetHeight(16) showUseButtonCheckbox:SetPoint("LEFT", showUseButtonFrame, "LEFT", 0, 0) - showUseButtonCheckbox:SetNormalTexture("Interface\\Buttons\\UI-CheckBox-Up") showUseButtonCheckbox:SetPushedTexture("Interface\\Buttons\\UI-CheckBox-Down") showUseButtonCheckbox:SetHighlightTexture("Interface\\Buttons\\UI-CheckBox-Highlight") showUseButtonCheckbox:SetCheckedTexture("Interface\\Buttons\\UI-CheckBox-Check") - - -- **UPDATED**: Set the checkbox state based on ConsumesManager_Options.showUseButton showUseButtonCheckbox:SetChecked(ConsumesManager_Options.showUseButton) + showUseButtonCheckbox:SetScript("OnClick", function() - if showUseButtonCheckbox:GetChecked() then - ConsumesManager_Options.showUseButton = true - else - ConsumesManager_Options.showUseButton = false - end + ConsumesManager_Options.showUseButton = showUseButtonCheckbox:GetChecked() ConsumesManager_UpdateAllContent() end) - local showUseButtonLabel = showUseButtonFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") showUseButtonLabel:SetPoint("LEFT", showUseButtonCheckbox, "RIGHT", 4, 0) showUseButtonLabel:SetText("Show Use Button") showUseButtonLabel:SetJustifyH("LEFT") - - -- Make the frame clickable (so clicking on the label checks/unchecks the box) showUseButtonFrame:SetScript("OnMouseDown", function() showUseButtonCheckbox:Click() end) - index = index + 1 -- Move index down after adding General Settings + index = index + 1 - -- Add 'Reset Addon' Button - local resetButton = CreateFrame("Button", "ConsumesManager_ResetButton", scrollChild, "UIPanelButtonTemplate") + -- Multi-Account Setup + local multiAccountTitle = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + multiAccountTitle:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, startYOffset - index * lineHeight - 20) + multiAccountTitle:SetText("Multi-Account Setup |cffff0000(BETA!)|r") + multiAccountTitle:SetTextColor(1, 1, 1) + + index = index + 1 + + local multiAccountInfo = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal") + multiAccountInfo:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, startYOffset - index * lineHeight - 20) + multiAccountInfo:SetText("Set a unique channel name and password. \nRepeat this setup for each of your alt-accounts.") + multiAccountInfo:SetJustifyH("LEFT") + + index = index + 2 + + -- More Info Button + local popup = MultiAccountInfoPopup() + local MoreInfoBtn = CreateFrame("Button", "ConsumesManager_MoreInfoBtn", scrollChild, "UIPanelButtonTemplate") + MoreInfoBtn:SetWidth(70) + MoreInfoBtn:SetHeight(20) + MoreInfoBtn:SetText("More Info") + MoreInfoBtn:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, startYOffset - index * lineHeight - 10) + MoreInfoBtn:SetScript("OnClick", function() + if popup:IsShown() then + popup:Hide() + else + popup:Show() + end + end) + + index = index + 1 + + -- Channel Input + local channelLabel = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal") + channelLabel:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, startYOffset - index * lineHeight - 20) + channelLabel:SetText("Channel:") + channelLabel:SetWidth(60) + channelLabel:SetJustifyH("LEFT") + channelLabel:SetTextColor(1, 1, 1) + + -- Create a frame to hold the editbox so the sub-textures stay aligned + local channelFrame = CreateFrame("Frame", nil, scrollChild) + channelFrame:SetHeight(20) + channelFrame:SetWidth(140) + channelFrame:SetPoint("LEFT", channelLabel, "RIGHT", 10, 0) + + local channelEditBox = CreateFrame("EditBox", "ConsumesManager_ChannelEditBox", channelFrame, "InputBoxTemplate") + channelEditBox:SetAutoFocus(false) + channelEditBox:SetMaxLetters(50) + channelEditBox:SetAllPoints(channelFrame) -- Fill the entire holding frame + + local leftTex = getglobal(channelEditBox:GetName().."Left") + local midTex = getglobal(channelEditBox:GetName().."Middle") + local rightTex= getglobal(channelEditBox:GetName().."Right") + + -- Anchor them so they move with the EditBox + if leftTex then + leftTex:ClearAllPoints() + leftTex:SetPoint("LEFT", channelEditBox, "LEFT", -5, 0) + end + if midTex then + midTex:ClearAllPoints() + midTex:SetPoint("LEFT", leftTex, "RIGHT", 0, 0) + midTex:SetPoint("RIGHT", rightTex, "LEFT", 0, 0) + end + if rightTex then + rightTex:ClearAllPoints() + rightTex:SetPoint("RIGHT", channelEditBox, "RIGHT", 5, 0) + end + + -- Retrieve stored channel + local stored_channel = "" + if ConsumesManager_Options.Channel and ConsumesManager_Options.Channel ~= "" then + stored_channel = DecodeMessage(ConsumesManager_Options.Channel) + end + channelEditBox:SetText(stored_channel) + + index = index + 1 + + -- Password Input + local passwordLabel = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal") + passwordLabel:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, startYOffset - index * lineHeight - 20) + passwordLabel:SetText("Password:") + passwordLabel:SetWidth(60) + passwordLabel:SetJustifyH("LEFT") + passwordLabel:SetTextColor(1, 1, 1) + + -- Same holding frame approach + local passwordFrame = CreateFrame("Frame", nil, scrollChild) + passwordFrame:SetHeight(20) + passwordFrame:SetWidth(140) + passwordFrame:SetPoint("LEFT", passwordLabel, "RIGHT", 10, 0) + + local passwordEditBox = CreateFrame("EditBox", "ConsumesManager_PasswordEditBox", passwordFrame, "InputBoxTemplate") + passwordEditBox:SetAutoFocus(false) + passwordEditBox:SetMaxLetters(50) + passwordEditBox:SetAllPoints(passwordFrame) + + local pLeft = getglobal(passwordEditBox:GetName().."Left") + local pMid = getglobal(passwordEditBox:GetName().."Middle") + local pRight= getglobal(passwordEditBox:GetName().."Right") + + if pLeft then + pLeft:ClearAllPoints() + pLeft:SetPoint("LEFT", passwordEditBox, "LEFT", -5, 0) + end + if pMid then + pMid:ClearAllPoints() + pMid:SetPoint("LEFT", pLeft, "RIGHT", 0, 0) + pMid:SetPoint("RIGHT", pRight, "LEFT", 0, 0) + end + if pRight then + pRight:ClearAllPoints() + pRight:SetPoint("RIGHT", passwordEditBox, "RIGHT", 5, 0) + end + + local stored_password = "" + if ConsumesManager_Options.Password and ConsumesManager_Options.Password ~= "" then + stored_password = DecodeMessage(ConsumesManager_Options.Password) + end + passwordEditBox:SetText(stored_password) + + index = index + 1 + + -- Join and Leave Channel Buttons + joinChannelButton = CreateFrame("Button", "ConsumesManager_JoinChannelButton", scrollChild, "UIPanelButtonTemplate") + joinChannelButton:SetWidth(140) + joinChannelButton:SetHeight(24) + joinChannelButton:SetText("Save & Join Channel") + joinChannelButton:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, startYOffset - index * lineHeight - 25) + + LeaveChannelButton = CreateFrame("Button", "ConsumesManager_LeaveChannelButton", scrollChild, "UIPanelButtonTemplate") + LeaveChannelButton:SetWidth(140) + LeaveChannelButton:SetHeight(24) + LeaveChannelButton:SetText("Leave Channel") + LeaveChannelButton:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 140, startYOffset - index * lineHeight - 25) + + -- Function to Update Leave Button State + local function UpdateLeaveButtonState() + if ConsumesManager_Options.Channel == "" or ConsumesManager_Options.Channel == nil then + LeaveChannelButton:Disable() + LeaveChannelButton:SetAlpha(0.5) + channelEditBox:SetText("") + passwordEditBox:SetText("") + else + LeaveChannelButton:Enable() + LeaveChannelButton:SetAlpha(1) + end + end + + UpdateLeaveButtonState() + + -- Leave Channel Button Script + LeaveChannelButton:SetScript("OnClick", function() + if ConsumesManager_Options.Channel == "" or ConsumesManager_Options.Channel == nil then + UpdateLeaveButtonState() + updateSenDataButtonState() + else + local decoded_channel = DecodeMessage(ConsumesManager_Options.Channel) + DEFAULT_CHAT_FRAME:AddMessage("|cffffffff" .. GetAddOnMetadata("ConsumesManager", "Title") .. ":|r |cffffffffYou left|r |cffffc0c0[" .. decoded_channel .. "]|r|cffffffff. Multi-account sync |cffff0000disabled|r|cffffffff.|r") + LeaveChannelByName(decoded_channel) + ConsumesManager_Options.Channel = nil + ConsumesManager_Options.Password = nil + UpdateLeaveButtonState() + updateSenDataButtonState() + channelErrorMessage:Hide() + end + end) + + -- Channel Error Message + local channelErrorMessage = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal") + channelErrorMessage:SetPoint("TOPLEFT", joinChannelButton, "BOTTOMLEFT", 0, -5) + channelErrorMessage:SetTextColor(1, 0, 0) + channelErrorMessage:SetText("Failed to join channel. Read chat for more info.") + channelErrorMessage:Hide() + + -- Function to Update Join Button State + local function UpdateJoinButtonState() + local ctext = channelEditBox:GetText() + local ptext = passwordEditBox:GetText() + if ctext ~= "" and ptext ~= "" then + joinChannelButton:Enable() + joinChannelButton:SetAlpha(1) + else + joinChannelButton:Disable() + joinChannelButton:SetAlpha(0.5) + end + end + + channelEditBox:SetScript("OnTextChanged", UpdateJoinButtonState) + passwordEditBox:SetScript("OnTextChanged", UpdateJoinButtonState) + + UpdateJoinButtonState() + + -- Function to Handle Channel Join Failures + function ConsumesManager_ChannelJoinFailed(error_message) + ConsumesManager_Options.Channel = nil + ConsumesManager_Options.Password = nil + channelEditBox:SetText("") + passwordEditBox:SetText("") + UpdateJoinButtonState() + channelErrorMessage:Show() + channelErrorMessage:SetText(error_message) + end + + index = index + 3 + + -- Danger Zone Title + local DangerZonTitle = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + DangerZonTitle:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, startYOffset - index * lineHeight - 30) + DangerZonTitle:SetText("Danger Zone") + DangerZonTitle:SetTextColor(1, 1, 1) + + index = index + 2 + + -- Reset Addon Button + resetButton = CreateFrame("Button", "ConsumesManager_ResetButton", scrollChild, "UIPanelButtonTemplate") resetButton:SetWidth(120) resetButton:SetHeight(24) resetButton:SetText("Reset Addon") - resetButton:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, startYOffset - index * lineHeight - 20) - + resetButton:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, startYOffset - index * lineHeight - 10) resetButton:SetScript("OnClick", function() - -- Reset saved variables + if ConsumesManager_Options.Channel then + local decoded_channel = DecodeMessage(ConsumesManager_Options.Channel) + LeaveChannelByName(decoded_channel) + end ConsumesManager_Options = {} ConsumesManager_SelectedItems = {} ConsumesManager_Data = {} - -- Reload UI ReloadUI() end) - index = index + 1 -- Move index down after adding Reset Button - - -- Adjust the scroll child height - scrollChild.contentHeight = math.abs(startYOffset) + index * lineHeight + 40 + scrollChild.contentHeight = math.abs(startYOffset) + index * lineHeight + 100 scrollChild:SetHeight(scrollChild.contentHeight) - -- Scroll Bar + -- Scroll Bar Setup local scrollBar = CreateFrame("Slider", "ConsumesManager_SettingsScrollBar", parentFrame) scrollBar:SetPoint("TOPRIGHT", parentFrame, "TOPRIGHT", -2, -16) scrollBar:SetPoint("BOTTOMRIGHT", parentFrame, "BOTTOMRIGHT", -2, 16) @@ -2350,19 +2589,268 @@ function ConsumesManager_CreateSettingsContent(parentFrame) insets = { left = 3, right = 3, top = 3, bottom = 3 } }) scrollBar:SetScript("OnValueChanged", function() - local value = scrollBar:GetValue() -- **RESTORED** this line + local value = this:GetValue() scrollFrame:SetVerticalScroll(value) end) parentFrame.scrollBar = scrollBar - -- Update the scrollbar ConsumesManager_UpdateSettingsScrollBar() + + + if not ConsumesManager_ChannelFrame then + ConsumesManager_ChannelFrame = CreateFrame("Frame", "ConsumesManager_ChannelFrame") + end + + local channelmsg = "" + + joinChannelButton:SetScript("OnClick", function() + + ConsumesManager_ChannelFrame:RegisterEvent("CHAT_MSG_CHANNEL_NOTICE") + + channelErrorMessage:Hide() + local ctext = channelEditBox:GetText() + local ptext = passwordEditBox:GetText() + local final_result = nil + local try_again = false + + ConsumesManager_ChannelFrame:SetScript("OnEvent", function() + local noticeType = string.upper(arg1 or "") + local channelName = string.upper(arg9 or "") + local inputChannelName = string.upper(ctext or "") + + if noticeType == "WRONG_PASSWORD" and channelName == inputChannelName then + channelmsg = "WRONG_PASSWORD" + DEFAULT_CHAT_FRAME:AddMessage(noticeType) + elseif noticeType == "NOT_MODERATOR" and channelName == inputChannelName then + channelmsg = "NOT_MODERATOR" + DEFAULT_CHAT_FRAME:AddMessage(noticeType) + elseif noticeType == "YOU_JOINED" and channelName == inputChannelName then + channelmsg = "YOU_JOINED" + DEFAULT_CHAT_FRAME:AddMessage(noticeType) + end + + end) + + + -- Don't join same channel + if ConsumesManager_Options.Channel then + if DecodeMessage(ConsumesManager_Options.Channel) == ctext then + channelErrorMessage:SetText("Already in this channel") + channelErrorMessage:Show() + return + end + end + + -- Block attempts to join big system channels + local blocked = { "world", "general", "localdefense", "hardcore", "lft", "trade" } + local lowerChannel = string.lower(ctext) + for _, b in pairs(blocked) do + if lowerChannel == b then + ConsumesManager_ChannelJoinFailed("You cannot use a global channel") + return + end + end + + JoinChannelByName(ctext, ptext) + + joinChannelButton:SetText("Connecting... (4)") + joinChannelButton:Disable() + joinChannelButton:SetAlpha(0.5) + + + local delayFrame = CreateFrame("Frame") + delayFrame:Show() + local elapsed = 0 + local delay = 5 + local one_attempt = 0 + + delayFrame:SetScript("OnUpdate", function() + + + if elapsed > 1 and elapsed < 2 then + + joinChannelButton:SetText("Connecting... (3)") + + if one_attempt == 0 then + + DEFAULT_CHAT_FRAME:AddMessage("message: " .. channelmsg) + + if channelmsg == "WRONG_PASSWORD" then + final_result = "WRONG_PASSWORD" + elseif channelmsg == "YOU_JOINED" then + DEFAULT_CHAT_FRAME:AddMessage("setting password") + SetChannelPassword(ctext, ptext) + end + end + + one_attempt = 1 + + + elseif elapsed > 2 and elapsed < 3 then + joinChannelButton:SetText("Connecting... (2)") + + + if one_attempt == 1 then + + DEFAULT_CHAT_FRAME:AddMessage("message: " .. channelmsg) + + if channelmsg == "YOU_JOINED" then + final_result = "SUCCESS" + elseif channelmsg == "NOT_MODERATOR" then + LeaveChannelByName(ctext) + try_again = true + end + end + + one_attempt = 2 + + + + elseif elapsed > 3 and elapsed < 4 then + joinChannelButton:SetText("Connecting... (1)") + + if one_attempt == 2 then + + DEFAULT_CHAT_FRAME:AddMessage("message: " .. channelmsg) + + if try_again == true then + + JoinChannelByName(ctext, ptext) + + end + end + + one_attempt = 3 + + elseif elapsed > 4 and elapsed < 5 then + joinChannelButton:SetText("Connecting... (0)") + + if one_attempt == 3 then + + DEFAULT_CHAT_FRAME:AddMessage("message: " .. channelmsg) + + if try_again == true then + + if channelmsg == "YOU_JOINED" then + final_result = "SUCCESS" + elseif channelmsg == "NOT_MODERATOR" then + final_result = "NOT_MODERATOR" + end + end + end + + one_attempt = 4 + + end + + + + elapsed = elapsed + arg1 + + if elapsed >= delay then + + delayFrame:SetScript("OnUpdate", nil) + delayFrame:Hide() + + + -- ACTION AFTER 3 SECONDS + + + if final_result == "SUCCESS" then + + ConsumesManager_Options.Channel = EncodeMessage(ctext) + ConsumesManager_Options.Password = EncodeMessage(ptext) + + DEFAULT_CHAT_FRAME:AddMessage("|cffffffff" .. GetAddOnMetadata("ConsumesManager", "Title") .. + ":|r |cffffffffYou joined|r |cffffc0c0[" .. ctext .. + "]|r|cffffffff. Multi-account sync |cff00ff00enabled|r|cffffffff.|r") + + elseif final_result == "WRONG_PASSWORD" then + + ConsumesManager_ChannelJoinFailed("Wrong password. Try again.") + + DEFAULT_CHAT_FRAME:AddMessage("|cffffffff" .. GetAddOnMetadata("ConsumesManager", "Title") .. + ":|r |cffffffffWrong password for|r |cffffc0c0[" .. ctext .. + "]|r|cffffffff. Multi-account sync |cffff0000disabled|r|cffffffff.|r") + + elseif final_result == "NOT_MODERATOR" then + + ConsumesManager_ChannelJoinFailed("This is not your channel.") + DEFAULT_CHAT_FRAME:AddMessage("|cffffffff" .. GetAddOnMetadata("ConsumesManager", "Title") .. + ":|r |cffffffffYou don't own|r |cffffc0c0[" .. ctext .. + "]|r|cffffffff. Multi-account sync |cffff0000disabled|r|cffffffff.|r") + + else + LeaveChannelByName(ctext) + ConsumesManager_ChannelJoinFailed("Unknown error") + DEFAULT_CHAT_FRAME:AddMessage("|cffffffff" .. GetAddOnMetadata("ConsumesManager", "Title") .. + ":|r |cffffffffFailed to join|r |cffffc0c0[" .. ctext .. + "]|r|cffffffff. Multi-account sync |cffff0000disabled|r|cffffffff.|r") + end + + joinChannelButton:SetText("Save & Join Channel") + joinChannelButton:Enable() + joinChannelButton:SetAlpha(1) + updateSenDataButtonState() + UpdateLeaveButtonState() + + ConsumesManager_ChannelFrame:UnregisterEvent("CHAT_MSG_CHANNEL_NOTICE") + + end + delayFrame:Show() + end) + end) + + + + + + + end +function ConsumesManager_UpdateSettingsContent() + local parentFrame = ConsumesManager_MainFrame and ConsumesManager_MainFrame.tabs and ConsumesManager_MainFrame.tabs[4] + if not parentFrame or not parentFrame.scrollFrame then + return + end + + -- Remove existing child + local oldChild = parentFrame.scrollFrame:GetScrollChild() + if oldChild then + oldChild:Hide() + oldChild:SetParent(nil) + parentFrame.scrollFrame:SetScrollChild(nil) + end + + -- Remove old scrollbar + if parentFrame.scrollBar then + parentFrame.scrollBar:Hide() + parentFrame.scrollBar:SetParent(nil) + parentFrame.scrollBar = nil + end + + -- New scroll child + local newScrollChild = CreateFrame("Frame", nil, parentFrame.scrollFrame) + newScrollChild:SetWidth(WindowWidth - 10) + newScrollChild:SetHeight(1) + newScrollChild.contentHeight = 0 + parentFrame.scrollFrame:SetScrollChild(newScrollChild) + parentFrame.scrollChild = newScrollChild + + -- Build content + ConsumesManager_CreateSettingsContent(parentFrame) + + -- Reset scroll + if parentFrame.scrollBar then + parentFrame.scrollBar:SetValue(0) + end +end + + function ConsumesManager_UpdateSettingsScrollBar() - local OptionsFrame = ConsumesManager_MainFrame.tabs[4] -- Corrected indexing + local OptionsFrame = ConsumesManager_MainFrame and ConsumesManager_MainFrame.tabs and ConsumesManager_MainFrame.tabs[4] if not OptionsFrame then - print("Error: OptionsFrame (tabs[4]) is nil in UpdateSettingsScrollBar") return end local scrollBar = OptionsFrame.scrollBar @@ -2370,7 +2858,7 @@ function ConsumesManager_UpdateSettingsScrollBar() local scrollChild = OptionsFrame.scrollChild local totalHeight = scrollChild.contentHeight - local shownHeight = 320 -- Adjust based on your UI + local shownHeight = 420 local maxScroll = math.max(0, totalHeight - shownHeight) scrollFrame.maxScroll = maxScroll @@ -2390,6 +2878,7 @@ end + -- Global Functions ----------------------------------------------------------------------------- function ConsumesManager_UpdateUseButtons() if not ConsumesManager_MainFrame or not ConsumesManager_MainFrame.tabs or not ConsumesManager_MainFrame.tabs[1] then @@ -2492,9 +2981,9 @@ end function ConsumesManager_UpdateAllContent() ConsumesManager_UpdateManagerContent() ConsumesManager_UpdatePresetsConsumables() + ConsumesManager_UpdateSettingsContent() end - function ConsumesManager_DisableTab(tab) tab.isEnabled = false tab:EnableMouse(true) -- Keep mouse enabled for tooltip @@ -2568,8 +3057,68 @@ function ConsumesManager_UpdateTabStates() end end +function MultiAccountInfoPopup() + -- Create the frame + local popup = CreateFrame("Frame", "MyPopupFrame", UIParent) + popup:SetHeight(180) + popup:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + popup:SetBackdrop({ + bgFile = "Interface\\ChatFrame\\ChatFrameBackground", -- Solid black background + edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", + tile = false, + tileSize = 0, + edgeSize = 32, + insets = { left = 8, right = 8, top = 8, bottom = 8 } + }) + popup:SetBackdropColor(0, 0, 0, 1) -- Black background with full opacity + popup:SetBackdropBorderColor(1, 1, 1, 1) -- White border + popup:SetFrameStrata("DIALOG") + popup:SetFrameLevel(1000) + popup:SetMovable(true) + popup:EnableMouse(true) + popup:RegisterForDrag("LeftButton") + popup:SetScript("OnDragStart", function() + this:StartMoving() + end) + popup:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + end) + + -- Add a close button + local closeButton = CreateFrame("Button", nil, popup, "UIPanelCloseButton") + closeButton:SetPoint("TOPRIGHT", popup, "TOPRIGHT", -5, -5) + + -- Add a title + local infotitle = popup:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + infotitle:SetPoint("TOPLEFT", popup, "TOPLEFT", 20, -20) + infotitle:SetText("How does it work?") + infotitle:SetTextColor(1,1,1) + + -- Add the infotext + popup.infotext = popup:CreateFontString(nil, "OVERLAY", "GameFontNormal") + popup.infotext:SetPoint("TOPLEFT", popup, "TOPLEFT", 20, -35) + popup.infotext:SetJustifyH("LEFT") + popup.infotext:SetText( + "|cffff0000(Beta Feature! Might be glitchy!)|r\n\n" .. + "To sync accounts, follow these steps:\n\n" .. + "1. Join a private channel with a unique name and password.\n" .. + "2. The channel info is saved for all characters on your account.\n" .. + "3. Log into your alt account on a second client.\n" .. + "4. Join the same channel via this addon to link both accounts.\n" .. + "5. Keep one character from your main account online for syncing.\n" .. + "6. Click on 'Push Data' to send the database from one account to all the other that are online." + ) + -- Function to adjust width dynamically after text is set + popup:SetScript("OnShow", function() + local textWidth = popup.infotext:GetStringWidth() + 40 -- Add padding + popup:SetWidth(textWidth) + end) + + popup:Hide() + return popup +end @@ -2616,10 +3165,15 @@ function ConsumesManager_ShowConsumableTooltip(itemID) -- Content text local content = tooltipFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") content:SetPoint("TOPLEFT", icon, "BOTTOMLEFT", 0, -10) - content:SetJustifyH("LEFT") tooltipFrame.content = content + -- Mats text + local mats = tooltipFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + mats:SetPoint("TOPLEFT", content, "BOTTOMLEFT", 0, -10) + mats:SetJustifyH("LEFT") + tooltipFrame.mats = mats + ConsumesManager_CustomTooltip = tooltipFrame end @@ -2628,13 +3182,39 @@ function ConsumesManager_ShowConsumableTooltip(itemID) -- Get item info local itemName = consumablesList[itemID] or "Unknown Item" local itemTexture = consumablesTexture[itemID] or "Interface\\Icons\\INV_Misc_QuestionMark" + -- Set icon and title tooltipFrame.icon:SetTexture(itemTexture) tooltipFrame.title:SetText(itemName) + local mats = consumablesMats[itemID] or {} + local matsText = "" + + if type(mats) == "table" and next(mats) then + local index = 0 + local divider = "" + for _, mat in ipairs(mats) do + index = index + 1 + if index > 1 then + divider = " | " + else + divider = "" + end + + matsText = matsText .. divider .. "|cff4ac9ff" .. mat .. "|r" + end + else + matsText = "|cff696969No materials specified for this item.|r" + end + + tooltipFrame.mats:SetText(matsText) + + + -- Prepare content text local contentText = "" + local matsText = "" local realmName = GetRealmName() local faction = UnitFactionGroup("player") @@ -2696,7 +3276,9 @@ function ConsumesManager_ShowConsumableTooltip(itemID) if not hasItems then contentText = contentText .. "|cffff0000No items found for this consumable.|r" + lineHeightAdjust = 10 else + lineHeightAdjust = 0 -- Sort characters alphabetically table.sort(characterList, function(a, b) return a.name < b.name end) @@ -2728,11 +3310,11 @@ function ConsumesManager_ShowConsumableTooltip(itemID) -- Adjust tooltip height based on the number of lines local lineHeightTooltip = 12 - local totalHeight = 50 + (numLines * lineHeightTooltip) + local totalHeight = 60 + (numLines * lineHeightTooltip) + lineHeightAdjust tooltipFrame:SetHeight(totalHeight) -- Set the width based on content - local titleWidth = tooltipFrame.title:GetStringWidth() + 70 + local titleWidth = math.max(tooltipFrame.mats:GetStringWidth() + 20, tooltipFrame.title:GetStringWidth() + 60) local maxWidth = math.max(titleWidth, tooltipFrame.content:GetStringWidth() + 20) tooltipFrame:SetWidth(maxWidth) @@ -2782,6 +3364,12 @@ function ConsumesManager_ShowItemsTooltip(itemID) description:SetTextColor(1, 1, 1) tooltipFrame.description = description + -- Mats text + local mats = tooltipFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + mats:SetPoint("TOPLEFT", description, "BOTTOMLEFT", 0, -10) + mats:SetJustifyH("LEFT") + tooltipFrame.mats = mats + ConsumesManager_ItemsTooltip = tooltipFrame end @@ -2797,17 +3385,39 @@ function ConsumesManager_ShowItemsTooltip(itemID) tooltipFrame.title:SetText(itemName) tooltipFrame.description:SetText(itemDescription) + local mats = consumablesMats[itemID] or {} + local matsText = "" + + if type(mats) == "table" and next(mats) then + local index = 0 + local divider = "" + for _, mat in ipairs(mats) do + index = index + 1 + if index > 1 then + divider = " | " + else + divider = "" + end + + matsText = matsText .. divider .. "|cff4ac9ff" .. mat .. "|r" + end + else + matsText = "|cff696969No materials specified for this item.|r" + end + + tooltipFrame.mats:SetText(matsText) + -- Adjust the height of the description based on its content tooltipFrame.description:SetWidth(maxDescriptionWidth) tooltipFrame.description:SetText(itemDescription) local descriptionHeight = tooltipFrame.description:GetHeight() -- Adjust tooltip height based on content - local totalHeight = 70 + descriptionHeight + local totalHeight = 80 + descriptionHeight tooltipFrame:SetHeight(totalHeight) -- Set the width of the tooltip - local titleWidth = tooltipFrame.title:GetStringWidth() + 70 + local titleWidth = math.max(tooltipFrame.title:GetStringWidth() + 70, tooltipFrame.mats:GetStringWidth() + 20) local maxWidth = math.max(titleWidth, maxDescriptionWidth + 20) tooltipFrame:SetWidth(maxWidth) @@ -2831,21 +3441,18 @@ end - -- Scan Functions --------------------------------------------------------------------------------------- function ConsumesManager_ScanPlayerInventory() local playerName = UnitName("player") local realmName = GetRealmName() local faction = UnitFactionGroup("player") - -- Initialize data structures ConsumesManager_Data[realmName] = ConsumesManager_Data[realmName] or {} ConsumesManager_Data[realmName][faction] = ConsumesManager_Data[realmName][faction] or {} ConsumesManager_Data[realmName][faction][playerName] = ConsumesManager_Data[realmName][faction][playerName] or {} local data = ConsumesManager_Data[realmName][faction][playerName] data["inventory"] = {} - -- Scan all bags (0 to 4 represent backpack and additional bags) for bag = 0, 4 do local numSlots = GetContainerNumSlots(bag) if numSlots then @@ -2855,10 +3462,11 @@ function ConsumesManager_ScanPlayerInventory() local _, _, itemID = string.find(link, "item:(%d+)") if itemID then itemID = tonumber(itemID) - -- Scan all items, regardless of selection - local texture, itemCount = GetContainerItemInfo(bag, slot) - if itemCount and itemCount > 0 then - data["inventory"][itemID] = (data["inventory"][itemID] or 0) + itemCount + if consumablesList[itemID] then + local _, itemCount = GetContainerItemInfo(bag, slot) + if itemCount and itemCount > 0 then + data["inventory"][itemID] = (data["inventory"][itemID] or 0) + itemCount + end end end end @@ -2866,14 +3474,12 @@ function ConsumesManager_ScanPlayerInventory() end end - -- Update the Manager window and use buttons ConsumesManager_UpdateUseButtons() ConsumesManager_UpdateManagerContent() ConsumesManager_UpdateTabStates() end function ConsumesManager_ScanPlayerBank() - -- Ensure the BankFrame is open before scanning if not BankFrame or not BankFrame:IsShown() then return end @@ -2882,14 +3488,12 @@ function ConsumesManager_ScanPlayerBank() local realmName = GetRealmName() local faction = UnitFactionGroup("player") - -- Initialize data structures ConsumesManager_Data[realmName] = ConsumesManager_Data[realmName] or {} ConsumesManager_Data[realmName][faction] = ConsumesManager_Data[realmName][faction] or {} ConsumesManager_Data[realmName][faction][playerName] = ConsumesManager_Data[realmName][faction][playerName] or {} local data = ConsumesManager_Data[realmName][faction][playerName] data["bank"] = {} - -- In WoW 1.12, bank bags are -1 and 5 to 10 for bag = -1, 10 do if bag == -1 or (bag >= 5 and bag <= 10) then local numSlots = GetContainerNumSlots(bag) @@ -2900,10 +3504,11 @@ function ConsumesManager_ScanPlayerBank() local _, _, itemID = string.find(link, "item:(%d+)") if itemID then itemID = tonumber(itemID) - -- Scan all items, regardless of selection - local texture, itemCount = GetContainerItemInfo(bag, slot) - if itemCount and itemCount > 0 then - data["bank"][itemID] = (data["bank"][itemID] or 0) + itemCount + if consumablesList[itemID] then + local _, itemCount = GetContainerItemInfo(bag, slot) + if itemCount and itemCount > 0 then + data["bank"][itemID] = (data["bank"][itemID] or 0) + itemCount + end end end end @@ -2912,13 +3517,11 @@ function ConsumesManager_ScanPlayerBank() end end - -- Update the Manager window ConsumesManager_UpdateManagerContent() ConsumesManager_UpdateTabStates() end function ConsumesManager_ScanPlayerMail() - -- Ensure the MailFrame is open before scanning if not MailFrame or not MailFrame:IsShown() then return end @@ -2927,7 +3530,6 @@ function ConsumesManager_ScanPlayerMail() local realmName = GetRealmName() local faction = UnitFactionGroup("player") - -- Initialize data structures ConsumesManager_Data[realmName] = ConsumesManager_Data[realmName] or {} ConsumesManager_Data[realmName][faction] = ConsumesManager_Data[realmName][faction] or {} ConsumesManager_Data[realmName][faction][playerName] = ConsumesManager_Data[realmName][faction][playerName] or {} @@ -2937,19 +3539,468 @@ function ConsumesManager_ScanPlayerMail() local numInboxItems = GetInboxNumItems() if numInboxItems and numInboxItems > 0 then for mailIndex = 1, numInboxItems do - local itemName, itemTexture, itemCount, itemQuality = GetInboxItem(mailIndex) + local itemName, _, itemCount = GetInboxItem(mailIndex) if itemName and itemCount and itemCount > 0 then - -- Since GetInboxItemLink is not available in 1.12, use itemName to get itemID local itemID = consumablesNameToID[itemName] - if itemID then - -- Scan all items, regardless of selection + if itemID and consumablesList[itemID] then data["mail"][itemID] = (data["mail"][itemID] or 0) + itemCount end end end end - -- Update the Manager window ConsumesManager_UpdateManagerContent() ConsumesManager_UpdateTabStates() -end \ No newline at end of file +end + + + + +-- Multi-Account Data Handling -------------------------------------------------------------------------- + + + local Converter = LibStub("LibCompress", true) + + +-- SEND DATA + local countdownFrame = CreateFrame("Frame") + countdownFrame:Hide() + local countdownTimer = 0 + local Syncing = false + local ProgressBar = 0 + local total_time_stored = 0 + local BarHeight = 0 + + local function SyncInProgress(syncing, totalTime) + if syncing then + sendDataButton:Disable() + sendDataButton.icon:SetDesaturated(true) + LeaveChannelButton:Disable() + LeaveChannelButton:SetAlpha(0.5) + joinChannelButton:Disable() + joinChannelButton:SetAlpha(0.5) + resetButton:Disable() + resetButton:SetAlpha(0.5) + + ProgressBarFrame:Show() + ProgressBarFrame_fill:Show() + ProgressBarFrame_Text:Show() + + ProgressBar = totalTime + if total_time_stored == 0 then + total_time_stored = totalTime + end + countdownTimer = 0 + BarHeight = 0 + ProgressBarFrame_fill:SetHeight(0) + + countdownFrame:Show() + else + sendDataButton:Enable() + sendDataButton.icon:SetDesaturated(false) + LeaveChannelButton:Enable() + LeaveChannelButton:SetAlpha(1) + joinChannelButton:Enable() + joinChannelButton:SetAlpha(1) + resetButton:Enable() + resetButton:SetAlpha(1) + + ProgressBarFrame:Hide() + ProgressBarFrame_Text:Hide() + ProgressBarFrame_fill:SetHeight(0) + ProgressBar = 0 + total_time_stored = 0 + BarHeight = 0 + ProgressBarFrame_fill:Hide() + + countdownFrame:Hide() + end + Syncing = syncing + end + + countdownFrame:SetScript("OnUpdate", function() + if Syncing and ProgressBar > 0 then + countdownTimer = countdownTimer + arg1 + if countdownTimer >= 1 then + ProgressBar = ProgressBar - 1 + if BarHeight < 492 then + BarHeight = 492 - (492 / (total_time_stored - 1)) * (ProgressBar - 1) + if BarHeight > 492 then + BarHeight = 492 + end + end + ProgressBarFrame_fill:SetHeight(BarHeight) + countdownTimer = 0 + end + end + end) + + function PushData() + if not ConsumesManager_Options.Channel or ConsumesManager_Options.Channel == "" or + not ConsumesManager_Options.Password or ConsumesManager_Options.Password == "" then + return + end + + local realmName = GetRealmName() + local faction = UnitFactionGroup("player") + local dataTable = ConsumesManager_Data[realmName][faction] + local serialized = Converter:TableToString(dataTable) + local compressed = lzw:compress(serialized) + local data = EncodeMessage(compressed) + + local channelName = DecodeMessage(ConsumesManager_Options.Channel) + local channelNumber = GetChannelName(channelName) + local length = string.len(data) + local chunkSize = 100 + local pos = 1 + local queue = {} + while pos <= length do + local chunk = string.sub(data, pos, pos + chunkSize - 1) + pos = pos + chunkSize + table.insert(queue, chunk) + end + + local totalMessages = table.getn(queue) + DEFAULT_CHAT_FRAME:AddMessage("|cffffffff" .. GetAddOnMetadata("ConsumesManager", "Title") .. ":|r |cffffffffData pushing started in|r |cffffc0c0[" .. DecodeMessage(ConsumesManager_Options.Channel) .. "]|r|cffffffff. Please wait...|r") + SendChatMessage("CM_SYNC_STARTED", "CHANNEL", nil, channelNumber) + SyncInProgress(true, totalMessages) + + local receivedChunks = {} + local sendFrame = CreateFrame("Frame") + local eventFrame = CreateFrame("Frame") + eventFrame:RegisterEvent("CHAT_MSG_CHANNEL") + + local state = "IDLE" + local currentIndex = 0 + local sendTimeout = 2 + local lockoutDuration = 10 + local lockedOutUntil = 0 + local waitingForVerification = false + local verificationElapsed = 0 + local verificationWaitTime = 1 + local currentStartTime = 0 + + eventFrame:SetScript("OnEvent", function() + if event == "CHAT_MSG_CHANNEL" then + local msg = arg1 + local iStart, iEnd, msgIndex, chunk = string.find(msg, "^CM_(%d+):(.+)$") + if msgIndex and chunk then + msgIndex = tonumber(msgIndex) + receivedChunks[msgIndex] = chunk + if state == "WAITING" and msgIndex == currentIndex + 1 then + table.remove(queue, 1) + currentIndex = currentIndex + 1 + state = "IDLE" + end + end + end + end) + + local function verifyData() + for i=1, totalMessages do + if not receivedChunks[i] then + DEFAULT_CHAT_FRAME:AddMessage("|cffffffff" .. GetAddOnMetadata("ConsumesManager", "Title") .. ":|r Data validation |cffff0000failed|r|cffffffff!|r") + return + end + end + local reconstructed = table.concat(receivedChunks, "") + if reconstructed == data then + DEFAULT_CHAT_FRAME:AddMessage("|cffffffff" .. GetAddOnMetadata("ConsumesManager", "Title") .. ":|r Data validation |cff00ff00succeeded|r|cffffffff!|r") + else + DEFAULT_CHAT_FRAME:AddMessage("|cffffffff" .. GetAddOnMetadata("ConsumesManager", "Title") .. ":|r Data validation |cffff0000failed|r|cffffffff!|r") + end + end + + sendFrame:SetScript("OnUpdate", function() + local now = GetTime() + + if waitingForVerification then + verificationElapsed = verificationElapsed + arg1 + if verificationElapsed >= verificationWaitTime then + DEFAULT_CHAT_FRAME:AddMessage("|cffffffff" .. GetAddOnMetadata("ConsumesManager", "Title") .. ":|r Data pushing |cff00ff00finished|r|cffffffff!|r") + verifyData() + SendChatMessage("CM_SYNC_STOPPED", "CHANNEL", nil, channelNumber) + SyncInProgress(false, 0) + eventFrame:UnregisterEvent("CHAT_MSG_CHANNEL") + sendFrame:SetScript("OnUpdate", nil) + sendFrame:Hide() + end + return + end + + if table.getn(queue) == 0 then + waitingForVerification = true + verificationElapsed = 0 + return + end + + if state == "LOCKED" then + if now >= lockedOutUntil then + state = "IDLE" + else + return + end + end + + if state == "IDLE" then + local nextMsg = queue[1] + if nextMsg then + SendChatMessage("CM_" .. (currentIndex + 1) .. ":" .. nextMsg, "CHANNEL", nil, channelNumber) + currentStartTime = now + state = "WAITING" + end + elseif state == "WAITING" then + if now - currentStartTime > sendTimeout then + state = "LOCKED" + lockedOutUntil = now + lockoutDuration + end + end + end) + + sendFrame:Show() + end + +-- READ DATA + + + local collecting = false + local collectedChunks = {} + local collectingFrom = "" + local collectingCount = 0 + local dataComplete = false + local eventFrame = nil + local channelName = "" + + local function ResetCollection() + collecting = false + collectingFrom = "" + collectedChunks = {} + collectingCount = 0 + dataComplete = false + end + + -- Adjusted to accept 'sender' as a parameter + local function StopCollecting(sender) + collecting = false + + if sendDataButton then + sendDataButton:Enable() + sendDataButton.icon:SetDesaturated(false) + end + + local total = table.getn(collectedChunks) + if total > 0 then + local i = 1 + local finalString = "" + while i <= total do + if collectedChunks[i] then + finalString = finalString .. collectedChunks[i] + end + i = i + 1 + end + DEFAULT_CHAT_FRAME:AddMessage( + "|cffffffff" .. GetAddOnMetadata("ConsumesManager", "Title") .. ":|r |cffffffffData |cff00ff00successfully|r retrieved from|r |cff00ccff" + .. sender .. "|r|cffffffff (" + .. total .. " chunks & " .. string.len(finalString) .. " data length)|r" + ) + combineVariableTables(finalString) + else + DEFAULT_CHAT_FRAME:AddMessage( + "|cffffffff" .. GetAddOnMetadata("ConsumesManager", "Title") .. ":|r |cffffffffData transmission |cffff0000failed|r from|r |cff00ccff" + .. sender .. "|r" + ) + end + end + + local function StartCollecting(sender) + collecting = true + collectingFrom = sender + collectedChunks = {} + collectingCount = 0 + dataComplete = false + + if sendDataButton then + sendDataButton:Disable() + sendDataButton.icon:SetDesaturated(true) + end + + DEFAULT_CHAT_FRAME:AddMessage( + "|cffffffff" .. GetAddOnMetadata("ConsumesManager", "Title") + .. ":|r |cffffffffReceiving data from|r |cff00ccff" + .. sender .. "|r|cffffffff...|r" + ) + end + + local function OnChatMessage() + if event == "CHAT_MSG_CHANNEL" then + local msg = arg1 + local sender = arg2 + local chName = arg9 + + if chName == channelName and channelName ~= "" then + if msg == "CM_SYNC_STARTED" then + if sender ~= UnitName("player") and not collecting then + StartCollecting(sender) + end + elseif msg == "CM_SYNC_STOPPED" then + if collecting and sender == collectingFrom then + StopCollecting(sender) -- Pass 'sender' here + ResetCollection() + end + else + if collecting and sender == collectingFrom then + local iStart, iEnd, msgIndex, chunk = string.find(msg, "^CM_(%d+):(.+)$") + if msgIndex and chunk then + msgIndex = tonumber(msgIndex) + collectedChunks[msgIndex] = chunk + collectingCount = collectingCount + 1 + end + end + end + end + end + end + + function ReadData(mode) + if not ConsumesManager_Options.Channel or ConsumesManager_Options.Channel == "" then + return + end + + channelName = DecodeMessage(ConsumesManager_Options.Channel) + local channelNumber = GetChannelName(channelName) + + if not channelNumber or channelNumber == 0 then + return + end + + if mode == "start" then + if not eventFrame then + eventFrame = CreateFrame("Frame") + eventFrame:SetScript("OnEvent", OnChatMessage) + end + eventFrame:RegisterEvent("CHAT_MSG_CHANNEL") + elseif mode == "stop" then + if eventFrame then + eventFrame:UnregisterEvent("CHAT_MSG_CHANNEL") + if collecting then + StopCollecting(collectingFrom) -- Pass 'collectingFrom' + ResetCollection() + end + end + end + end + + -- Register for PLAYER_LOGIN (or VARIABLES_LOADED in 1.12) to start listening automatically + local startupFrame = CreateFrame("Frame") + startupFrame:RegisterEvent("PLAYER_LOGIN") + startupFrame:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + ReadData("start") + end + end) + + + function combineVariableTables(compressed_message) + + local receivedTable = {} + local realmName = GetRealmName() + local faction = UnitFactionGroup("player") + + local decoded = DecodeMessage(compressed_message) + local uncompressed = lzw:decompress(decoded) + receivedTable = Converter:StringToTable(uncompressed) + + local existingTable = ConsumesManager_Data[realmName][faction] + + + local function mergeTables(original, newData) + for key, value in newData do + if type(value) == "table" and type(original[key]) == "table" then + mergeTables(original[key], value) + else + original[key] = value + end + end + end + + mergeTables(existingTable, receivedTable) + ConsumesManager_UpdateAllContent() + + end + +-- BASE64 ENCODE AND DECODE -------------------------------------------------------------------------------------------------- + + local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + local d = {} + do + local i = 1 + while i <= string.len(b) do + local c = string.sub(b, i, i) + d[c] = i - 1 + i = i + 1 + end + end + + function EncodeMessage(data) + local encoded = {} + local length = string.len(data) + local i = 1 + while i <= length do + local c1 = string.byte(data, i, i) i = i + 1 + local c2 = (i <= length) and string.byte(data, i, i) or nil i = i + 1 + local c3 = (i <= length) and string.byte(data, i, i) or nil i = i + 1 + + local o1 = bit.rshift(c1, 2) + local o2 = bit.bor(bit.lshift(bit.band(c1, 3),4), (c2 and bit.rshift(c2, 4) or 0)) + local o3 = (c2 and bit.bor(bit.lshift(bit.band(c2,15),2), (c3 and bit.rshift(c3,6) or 0)) or 64) + local o4 = (c3 and bit.band(c3,63) or 64) + + table.insert(encoded, string.sub(b, o1+1, o1+1)) + table.insert(encoded, string.sub(b, o2+1, o2+1)) + if o3 ~= 64 then + table.insert(encoded, string.sub(b, o3+1, o3+1)) + else + table.insert(encoded, "=") + end + if o4 ~= 64 then + table.insert(encoded, string.sub(b, o4+1, o4+1)) + else + table.insert(encoded, "=") + end + end + return table.concat(encoded, "") + end + + function DecodeMessage(str) + local decoded = {} + local length = string.len(str) + local i = 1 + while i <= length do + local c1 = string.sub(str, i, i) i = i + 1 + local c2 = string.sub(str, i, i) i = i + 1 + if (not c2) or (c1 == '=') or (c2 == '=') then + break + end + local c3 = string.sub(str, i, i) i = i + 1 + local c4 = string.sub(str, i, i) i = i + 1 + + local dc1 = d[c1] + local dc2 = d[c2] + local dc3 = (c3 and c3 ~= '=' and d[c3]) or nil + local dc4 = (c4 and c4 ~= '=' and d[c4]) or nil + + local o1 = bit.bor(bit.lshift(dc1, 2), bit.rshift(dc2, 4)) + table.insert(decoded, string.char(o1)) + + if dc3 then + local o2 = bit.bor(bit.lshift(bit.band(dc2, 15),4), bit.rshift(dc3, 2)) + table.insert(decoded, string.char(o2)) + if dc4 then + local o3 = bit.bor(bit.lshift(bit.band(dc3, 3),6), dc4) + table.insert(decoded, string.char(o3)) + end + end + end + return table.concat(decoded, "") + end + diff --git a/libs/LibCompress/LibCompress.lua b/libs/LibCompress/LibCompress.lua new file mode 100644 index 0000000..bf3552b --- /dev/null +++ b/libs/LibCompress/LibCompress.lua @@ -0,0 +1,798 @@ +---------------------------------------------------------------------------------- +-- +-- LibCompress.lua +-- +-- Authors: jjsheets and Galmok of European Stormrage (Horde) +-- Email : sheets.jeff@gmail.com and galmok@gmail.com +-- Licence: GPL version 2 (General Public License) +---------------------------------------------------------------------------------- + + +local MAJOR, MINOR = "LibCompress", 3 + +local LibCompress,oldminor = LibStub:NewLibrary(MAJOR, MINOR) + +if not LibCompress then return end + +-- list of codecs in this file: +-- \000 - Never used +-- \001 - Uncompressed +-- \002 - LZW +-- \003 - Huffman + + +-- local is faster than global +local table_insert = table.insert +local table_remove = table.remove +local table_concat = table.concat +local string_char = string.char +local string_byte = string.byte +local string_len = string.len +local string_sub = string.sub +local unpack = unpack +local pairs = pairs +local math_modf = math.modf +local bit_band = bit.band +local bit_bor = bit.bor +local bit_lshift = bit.lshift +local bit_rshift = bit.rshift +local tostring = tostring +local next = next +local strlen = strlen +local strsplit = string.split +local strfind = string.find +local strgfind = string.gfind +local tonumber = tonumber + +-------------------------------------------------------------------------------- +-- Cleanup + +local tables = {} -- tables that may be cleaned have to be kept here +local tables_to_clean = {} -- list of tables by name (string) that may be reset to {} after a timeout +local timeout = -1 + +-- tables that may be erased +local function cleanup() + for k,v in pairs(tables_to_clean) do + tables[k]={} + tables_to_clean[k]=nil + end +end + +local function onUpdate(frame, elapsed) + timeout = timeout - arg1 + if timeout < 0 then + this:Hide() + cleanup() + end +end + +LibCompress.frame = LibCompress.frame or CreateFrame("frame", nil, UIParent) -- reuse the old frame +LibCompress.frame:SetScript("OnUpdate", onUpdate) +LibCompress.frame:Hide() + +local function setCleanupTables(s1, s2) + timeout = 15 -- empty tables after 15 seconds + if not LibCompress.frame:IsShown() then + LibCompress.frame:Show() + end + + if tables_to_clean[s1] then + tables_to_clean[s1] = true + end + if tables_to_clean[s2] then + tables_to_clean[s2] = true + end +end + + +-------------------------------------------------------------------------------- +-- LZW codec +-- implemented by sheets.jeff@gmail.com + +-- encode is used to uniquely encode a number into a sequence of bytes that can be decoded using decode() +-- the bytes returned by this do not contain "\000" +local bytes = {} +local function encode(x) + local math_modf = math.modf + for k = 1, getn(bytes) do bytes[k] = nil end + local xmod + x, xmod = math_modf(x/255.0) + xmod = xmod * 255 + bytes[getn(bytes) + 1] = xmod + while x > 0 do + x, xmod = math_modf(x/255.0) + xmod = xmod * 255.0 + bytes[getn(bytes) + 1] = xmod + end + if getn(bytes) == 1 and bytes[1] > 0 and bytes[1] < 250 then + return string_char(bytes[1]) + else + for i = 1, getn(bytes) do bytes[i] = bytes[i] + 1 end + return string_char(256 - getn(bytes), unpack(bytes)) + end +end + +--decode converts a unique character sequence into its equivalent number, from ss, beginning at the ith char. +-- returns the decoded number and the count of characters used in the decode process. +local function decode(ss,i) + i = i or 1 + local a = string_byte(ss,i,i) + if a > 249 then + local r = 0 + a = 256 - a + for n = i+a, i+1, -1 do + r = r * 255 + string_byte(ss,n,n) - 1 + end + return r, a + 1 + else + return a, 1 + end +end + +-- +-- Added by Shino +-- +function LibCompress:TableToString(t) + if t == nil then return nil end + if type(t)=="string" then return t end + local cache={} + local function sub_mod(t) + if (type(t)=="table") then + for pos,val in pairs(t) do + if (type(val)=="table") then + table_insert(cache, pos..">".."{") + sub_mod(val) + table_insert(cache, "}") + elseif (type(val)=="string") then + table_insert(cache, pos..'>"'..val..'"') + else + table_insert(cache, pos..">"..tostring(val)) + end + end + else + table_insert(cache, tostring(t)) + end + end + sub_mod(t) + return table_concat(cache, ",") -- ? maybe use a different delimiter +end + +-- Adding a costum string.split function cause this one has a problem with too big tables +-- Credits: https://gist.github.com/jaredallard/ddb152179831dd23b230 +function LibCompress:StringSplit(delimiter, str) + local result = { } + local from = 1 + local delim_from, delim_to = strfind( str, delimiter, from ) + while delim_from do + table_insert( result, strsub( str, from , delim_from-1 ) ) + from = delim_to + 1 + delim_from, delim_to = strfind( str, delimiter, from ) + end + table.insert( result, strsub( str, from ) ) + return result +end + +function LibCompress:StringToTable(t) + if t=="" or t==nil then return nil end + if type(t)=="table" then return t end + local table = self:StringSplit(",",t) + local function fillSubTable(start) + local subT = {} + local cont = true + local po = nil + for k, v in pairs(table) do + if k>start then + cont = true + po = nil + for pos in strgfind(v, "(.+)>{") do -- opening sub table + po = tonumber(pos) + if po then + subT[po], start = fillSubTable(k) + else + subT[pos], start = fillSubTable(k) + end + end + if k>start then -- cause it could have changed if a new table was opened + for pos, val in strgfind(v, '(.+)%>"(.+)"') do + po = tonumber(pos) + if po then + subT[po] = ""..val + else + subT[pos] = ""..val + end + cont = false + end + if cont then + for pos, val in strgfind(v, "(.+)%>(.+)") do -- Do floats work too? + po = tonumber(pos) + if val == "false" then + if po then + subT[po] = false + else + subT[pos] = false + end + elseif val == "true" then + if po then + subT[po] = true + else + subT[pos] = true + end + else + if po then + subT[po] = tonumber(val) + else + subT[pos] = tonumber(val) + end + end + end + if v=="}" then -- closing sub table + return subT,k -- Returning the end + end + end + end + end + end + return subT, 0 + end + return fillSubTable(0) +end + +-- Shino end -- + +-- Compresses the given uncompressed string. +-- Unless the uncompressed string starts with "\002", this is guaranteed to return a string equal to or smaller than +-- the passed string. +-- the returned string will only contain "\000" characters in rare circumstances, and will contain none if the +-- source string has none. +local dict = {} +function LibCompress:CompressLZW(uncompressed) + if type(uncompressed) == "string" then + local dict_size = 256 + for k, _ in pairs(dict) do + dict[k] = nil + end + local result = {"\002"} + local w = '' + local ressize = 1 + for i = 0, 255 do + dict[string_char(i)] = i + end + for i = 1, strlen(uncompressed) do + local c = string_sub(uncompressed, i,i) + local wc = w..c + if dict[wc] then + w = wc + else + dict[wc] = dict_size + dict_size = dict_size +1 + local r = encode(dict[w]) + ressize = ressize + strlen(r) + result[getn(result) + 1] = r + w = c + end + end + if w then + local r = encode(dict[w]) + ressize = ressize + strlen(r) + result[getn(result) + 1] = r + end + if (strlen(uncompressed)+1) > ressize then + return table_concat(result, "") + else + return "\002"..uncompressed + end + else + return nil, "Can only compress strings" + end +end + +-- if the passed string is a compressed string, this will decompress it and return the decompressed string. +-- Otherwise it return an error message +-- compressed strings are marked by beginning with "\002" +function LibCompress:DecompressLZW(compressed) + if type(compressed) == "string" then + if string_sub(compressed,1,1) ~= "\002" then + return nil, "Can only decompress LZW compressed data ("..tostring(string_sub(compressed,1,1))..")" + end + compressed = string_sub(compressed,2)-- ? + local dict_size = 256 + for k, _ in pairs(dict) do + dict[k] = nil + end + for i = 0, 255 do + dict[i] = string_char(i) + end + local result = {} + local t = 1 + local delta, k + k, delta = decode(compressed,t) + t = t + delta + result[getn(result)+1] = dict[k] + local w = dict[k] + local entry + while t <= strlen(compressed) do + k, delta = decode(compressed,t) + t = t + delta + entry = dict[k] or (w..string_sub(w,1,1)) + result[getn(result)+1] = entry + dict[dict_size] = w..string_sub(entry,1,1) + dict_size = dict_size + 1 + w = entry + end + return table_concat(result) + else + return nil, "Can only uncompress strings" + end +end + + +-------------------------------------------------------------------------------- +-- Huffman codec +-- implemented by Galmok of European Stormrage (Horde), galmok@gmail.com + +local function addCode(tree, bcode,len) + if tree then + tree.bcode = bcode; + tree.blength = len; + if tree.c1 then + addCode(tree.c1, bit_bor(bcode, bit_lshift(1,len)), len+1) + end + if tree.c2 then + addCode(tree.c2, bcode, len+1) + end + end +end + +local function escape_code(code, len) + local escaped_code = 0; + local b; + local l = 0; + for i = len-1, 0,- 1 do + b = bit_band( code, bit_lshift(1,i))==0 and 0 or 1 + escaped_code = bit_lshift(escaped_code,1+b) + b + l = l + b; + end + return escaped_code, len+l +end + +tables.Huffman_compressed = {} +tables.Huffman_large_compressed = {} + +local compressed_size = 0 +local remainder; +local remainder_length; +local function addBits(tbl, code, len) + remainder = remainder + bit_lshift(code, remainder_length) + remainder_length = len + remainder_length + if remainder_length > 32 then + return true -- Bits lost due to too long code-words. + end + while remainder_length>=8 do + compressed_size = compressed_size + 1 + tbl[compressed_size] = string_char(bit_band(remainder, 255)) + remainder = bit_rshift(remainder, 8) + remainder_length = remainder_length -8 + end +end + +-- word size for this huffman algorithm is 8 bits (1 byte). This means the best compression is representing 1 byte with 1 bit, i.e. compress to 0.125 of original size. +function LibCompress:CompressHuffman(uncompressed) + if not type(uncompressed)=="string" then + return nil, "Can only compress strings" + end + + -- make histogram + local hist = {} + local n = 0 + -- dont have to use all datat to make the histogram + local uncompressed_size = string_len(uncompressed) + local c; + for i = 1, uncompressed_size do + c = string_byte(uncompressed, i) + hist[c] = (hist[c] or 0) + 1 + end + + --Start with as many leaves as there are symbols. + local leafs = {} + local leaf; + local symbols = {} + for symbol, weight in pairs(hist) do + leaf = { symbol=string_char(symbol), weight=weight }; + symbols[symbol] = leaf; + table_insert(leafs, leaf) + end + --Enqueue all leaf nodes into the first queue (by probability in increasing order so that the least likely item is in the head of the queue). + sort(leafs, function(a,b) if a.weightb.weight then return false else return nil end end) + + local nLeafs = getn(leafs) + + -- create tree + local huff = {} + --While there is more than one node in the queues: + local l,h, li, hi, leaf1, leaf2 + local newNode; + while (getn(leafs)+getn(huff) > 1) do + -- Dequeue the two nodes with the lowest weight. + -- Dequeue first + if not next(huff) then + li, leaf1 = next(leafs) + table_remove(leafs, li) + elseif not next(leafs) then + hi, leaf1 = next(huff) + table_remove(huff, hi) + else + li, l = next(leafs); + hi, h = next(huff); + if l.weight<=h.weight then + leaf1 = l; + table_remove(leafs, li) + else + leaf1 = h; + table_remove(huff, hi) + end + end + -- Dequeue second + if not next(huff) then + li, leaf2 = next(leafs) + table_remove(leafs, li) + elseif not next(leafs) then + hi, leaf2 = next(huff) + table_remove(huff, hi) + else + li, l = next(leafs); + hi, h = next(huff); + if l.weight<=h.weight then + leaf2 = l; + table_remove(leafs, li) + else + leaf2 = h; + table_remove(huff, hi) + end + end + + --Create a new internal node, with the two just-removed nodes as children (either node can be either child) and the sum of their weights as the new weight. + newNode = { c1 = leaf1, c2 = leaf2, weight = leaf1.weight+leaf2.weight } + table_insert(huff,newNode) + end + if getn(leafs)>0 then + li, l = next(leafs) + table_insert(huff, l) + table_remove(leafs, li) + end + huff = huff[1]; + + -- assign codes to each symbol + -- c1 = "0", c2 = "1" + -- As a common convention, bit '0' represents following the left child and bit '1' represents following the right child. + -- c1 = left, c2 = right + + addCode(huff,0,0); + if huff then + huff.bcode = 0 + huff.blength = 1 + end + + -- READING + -- bitfield = 0 + -- bitfield_len = 0 + -- read byte1 + -- bitfield = bitfield + bit_lshift(byte1, bitfield_len) + -- bitfield_len = bitfield_len + 8 + -- read byte2 + -- bitfield = bitfield + bit_lshift(byte2, bitfield_len) + -- bitfield_len = bitfield_len + 8 + -- (use 5 bits) + -- word = bit_band( bitfield, bit_lshift(1,5)-1) + -- bitfield = bit_rshift( bitfield, 5) + -- bitfield_len = bitfield_len - 5 + -- read byte3 + -- bitfield = bitfield + bit_lshift(byte3, bitfield_len) + -- bitfield_len = bitfield_len + 8 + + -- WRITING + remainder = 0; + remainder_length = 0; + + local compressed = tables.Huffman_compressed + --compressed_size = 0 + + -- first byte is version info. 0 = uncompressed, 1 = 8-bit word huffman compressed + compressed[1] = "\003" + + -- Header: byte 0=#leafs, byte 1-3=size of uncompressed data + -- max 2^24 bytes + local l = string_len(uncompressed) + compressed[2] = string_char(bit_band(nLeafs-1, 255)) -- number of leafs + compressed[3] = string_char(bit_band(l, 255)) -- bit 0-7 + compressed[4] = string_char(bit_band(bit_rshift(l, 8), 255)) -- bit 8-15 + compressed[5] = string_char(bit_band(bit_rshift(l, 16), 255)) -- bit 16-23 + compressed_size = 5 + + -- create symbol/code map + for symbol, leaf in pairs(symbols) do + addBits(compressed, symbol, 8); + if addBits(compressed, escape_code(leaf.bcode, leaf.blength)) then + -- code word too long. Needs new revision to be able to handle more than 32 bits + return string_char(0)..uncompressed + end + addBits(compressed, 3, 2); + end + + -- create huffman code + local large_compressed = tables.Huffman_large_compressed + local large_compressed_size = 0 + local ulimit + for i = 1, l, 200 do + ulimit = l<(i+199) and l or (i+199) + for sub_i = i, ulimit do + c = string_byte(uncompressed, sub_i) + addBits(compressed, symbols[c].bcode, symbols[c].blength) + end + large_compressed_size = large_compressed_size + 1 + large_compressed[large_compressed_size] = table_concat(compressed, "", 1, compressed_size) + compressed_size = 0 + end + + -- add remainding bits (if any) + if remainder_length>0 then + large_compressed_size = large_compressed_size + 1 + large_compressed[large_compressed_size] = string_char(remainder) + end + local compressed_string = table_concat(large_compressed, "", 1, large_compressed_size) + + -- is compression worth it? If not, return uncompressed data. + if (strlen(uncompressed)+1) <= strlen(compressed_string) then + return "\001"..uncompressed + end + + setCleanupTables("Huffman_compressed", "Huffman_large_compressed") + return compressed_string +end + +-- lookuptable (cached between calls) +local lshiftMask = {} +setmetatable(lshiftMask, { + __index = function (t, k) + local v = bit_lshift(1, k) + rawset(t, k, v) + return v + end +}) + +-- lookuptable (cached between calls) +local lshiftMinusOneMask = {} +setmetatable(lshiftMinusOneMask, { + __index = function (t, k) + local v = bit_lshift(1, k)-1 + rawset(t, k, v) + return v + end +}) + +local function getCode(bitfield, field_len) + if field_len>=2 then + local b; + local p = 0; + for i = 0, field_len-1 do + b = bit_band(bitfield, lshiftMask[i]) + if not (p==0) and not (b == 0) then + -- found 2 bits set right after each other (stop bits) + return bit_band( bitfield, lshiftMinusOneMask[i-1]), i-1, + bit_rshift(bitfield, i+1), field_len-i-1 + end + p = b + end + end + return nil +end + +local function unescape_code(code, code_len) + local unescaped_code=0; + local b; + local l = 0; + local i = 0 + while i < code_len do + b = bit_band( code, lshiftMask[i]) + if not (b==0) then + unescaped_code = bit_bor(unescaped_code, lshiftMask[l]) + i = i + 1 + end + i = i + 1 + l = l + 1 + end + return unescaped_code, l +end + +tables.Huffman_uncompressed = {} +tables.Huffman_large_uncompressed = {} -- will always be as big as the larges string ever decompressed. Bad, but clearing i every timetakes precious time. + +function LibCompress:DecompressHuffman(compressed) + if type(compressed)~="string" or compressed == nil then + if type(compressed)=="table" then + return compressed + end + return nil + end + + local compressed_size = strlen(compressed) + --decode header + local info_byte = string_byte(compressed) + -- is data compressed + if info_byte==1 then + return string_sub(compressed,2) --return uncompressed data + end + if not (info_byte==3) then + return nil, "Can only decompress Huffman compressed data ("..tostring(info_byte)..")" + end + + local num_symbols = string_byte(string_sub(compressed, 2, 2)) + 1 + local c0 = string_byte(string_sub(compressed, 3, 3)) + local c1 = string_byte(string_sub(compressed, 4, 4)) + local c2 = string_byte(string_sub(compressed, 5, 5)) + local orig_size = c2*65536 + c1*256 + c0 + if orig_size==0 then + return ""; + end + + -- decode code->symbal map + local bitfield = 0; + local bitfield_len = 0; + local map = {} -- only table not reused in Huffman decode. + setmetatable(map, { + __index = function (t, k) + local v = {} + rawset(t, k, v) + return v + end + }) + + local i = 6; -- byte 1-5 are header bytes + local c, cl; + local minCodeLen = 1000; + local maxCodeLen = 0; + local symbol, code, code_len, _bitfield, _bitfield_len; + local n = 0; + local state = 0; -- 0 = get symbol (8 bits), 1 = get code (varying bits, ends with 2 bits set) + while ncompressed_size then + return nil, "Cannot decode map" + end + + c = string_byte(compressed, i) + bitfield = bit_bor(bitfield, bit_lshift(c, bitfield_len)) + bitfield_len = bitfield_len + 8 + + if state == 0 then + symbol = bit_band(bitfield, 255) + bitfield = bit_rshift(bitfield, 8) + bitfield_len = bitfield_len -8 + state = 1 -- search for code now + else + code, code_len, _bitfield, _bitfield_len = getCode(bitfield, bitfield_len) + if code then + bitfield, bitfield_len = _bitfield, _bitfield_len + c, cl = unescape_code(code, code_len) + map[cl][c]=string_char(symbol) + minCodeLen = clmaxCodeLen and cl or maxCodeLen + --print("symbol: "..string_char(symbol).." code: "..tobinary(c, cl)) + n = n + 1 + state = 0 -- search for next symbol (if any) + end + end + i=i+1 + end + + -- dont create new subtables for entries not in the map. Waste of space. + -- But do return an empty table to prevent runtime errors. (instead of returning nil) + local mt = {} + setmetatable(map, { + __index = function (t, k) + return mt + end + }) + + local uncompressed = tables.Huffman_uncompressed + local large_uncompressed = tables.Huffman_large_uncompressed + local uncompressed_size = 0 + local large_uncompressed_size = 0 + local test_code + local test_code_len = minCodeLen; + local symbol; + local dec_size = 0; + compressed_size = compressed_size + 1 + local temp_limit = 200; -- first limit of uncompressed data. large_uncompressed will hold strings of length 200 + while true do + if test_code_len<=bitfield_len then + test_code=bit_band( bitfield, lshiftMinusOneMask[test_code_len]) + symbol = map[test_code_len][test_code] + if symbol then + uncompressed_size = uncompressed_size + 1 + uncompressed[uncompressed_size]=symbol + dec_size = dec_size + 1 + + if dec_size>=orig_size then -- checked here for speed reasons + break; + end + -- process compressed bytes in smaller chunks + large_uncompressed_size = large_uncompressed_size + 1 + large_uncompressed[large_uncompressed_size] = table_concat(uncompressed, "", 1, uncompressed_size) + uncompressed_size = 0 + temp_limit = temp_limit + 200 -- repeated chunk size is 200 uncompressed bytes + temp_limit = temp_limit > orig_size and orig_size or temp_limit + + bitfield = bit_rshift(bitfield, test_code_len) + bitfield_len = bitfield_len - test_code_len + test_code_len = minCodeLen + else + test_code_len = test_code_len + 1 + if test_code_len>maxCodeLen then + return nil, "Decompression error at "..tostring(i).."/"..tostring(strlen(compressed)) + end + end + else + c = string_byte(compressed, i) + bitfield = bitfield + bit_lshift(c or 0, bitfield_len) + bitfield_len = bitfield_len + 8 + i = i + 1 + if i > temp_limit then + if i > compressed_size then + break; + end + end + end + end + + setCleanupTables("Huffman_uncompressed", "Huffman_large_uncompressed") + return table_concat(large_uncompressed, "", 1, large_uncompressed_size)..table_concat(uncompressed, "", 1, uncompressed_size) +end + +-------------------------------------------------------------------------------- +-- Generic codec interface + +function LibCompress:DecompressUncompressed(data) + if type(data)~="string" then + return nil, "Can only handle strings" + end + if string.byte(data) ~= 1 then + return nil, "Can only handle uncompressed data" + end + return string_sub(data, 2) +end + +local compression_methods = { + [2] = LibCompress.CompressLZW, + [3] = LibCompress.CompressHuffman +} + +local decompression_methods = { + [1] = LibCompress.DecompressUncompressed, + [2] = LibCompress.DecompressLZW, + [3] = LibCompress.DecompressHuffman +} + +-- try all compression codecs and return best result +function LibCompress:Compress(data) + local method = next(compression_methods) + local result = compression_methods[method](self, data); + local n; + method = next(compression_methods, method) + while method do + n = compression_methods[method](self, data) + if getn(n) < getn(result) then + result = n + end + method = next(compression_methods, method) + end + return result +end + +function LibCompress:Decompress(data) + local header_info = string.byte(data) + if decompression_methods[header_info] then + return decompression_methods[header_info](self, data) + else + return nil, "Unknown compression method ("..tostring(header_info)..")" + end +end \ No newline at end of file diff --git a/libs/LibStub/LibStub.lua b/libs/LibStub/LibStub.lua new file mode 100644 index 0000000..d25a55b --- /dev/null +++ b/libs/LibStub/LibStub.lua @@ -0,0 +1,33 @@ +-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info +-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke +local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS! +local _G = getfenv() +local strfind, strfmt = string.find, string.format +local LibStub = _G[LIBSTUB_MAJOR] + +if not LibStub or LibStub.minor < LIBSTUB_MINOR then + LibStub = LibStub or { libs = {}, minors = {} } + _G[LIBSTUB_MAJOR] = LibStub + LibStub.minor = LIBSTUB_MINOR + + function LibStub:NewLibrary(major, minor) + assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)") + local _,_,num = strfind(minor, "(%d+)") + minor = assert(tonumber(num), "Minor version must either be a number or contain a number.") + + local oldminor = self.minors[major] + if oldminor and oldminor >= minor then return nil end + self.minors[major], self.libs[major] = minor, self.libs[major] or {} + return self.libs[major], oldminor + end + + function LibStub:GetLibrary(major, silent) + if not self.libs[major] and not silent then + error(strfmt("Cannot find a library instance of %q.", tostring(major)), 2) + end + return self.libs[major], self.minors[major] + end + + function LibStub:IterateLibraries() return pairs(self.libs) end + setmetatable(LibStub, { __call = LibStub.GetLibrary }) +end \ No newline at end of file diff --git a/libs/LzwLib-1.0/LzwLib-1.0.lua b/libs/LzwLib-1.0/LzwLib-1.0.lua new file mode 100644 index 0000000..3af9f33 --- /dev/null +++ b/libs/LzwLib-1.0/LzwLib-1.0.lua @@ -0,0 +1,435 @@ +--[[ + +* Copyright (c) 2006, Lucas Alonso & Ben Golus +* All rights reserved. +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* * Redistributions in binary form must reproduce the above copyright +* notice, this list of conditions and the following disclaimer in the +* documentation and/or other materials provided with the distribution. +* * The names of the authors may not be used to endorse or promote products +* derived from this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY +* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY +* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +luke@secretlevel.com +ben@secretlevel.com + +]]-- + +--[[ + +A simple lua LZW compression library. + +---- + +Syntax: + +lzw:compress("string"[,chatsafe][,CompressTable,TableSize]); +returns compressed string + +lzw:decompress("string"[,chatsafe][,CompressTable,TableSize]); +returns decompressed string + +---- + +Typical usage: + +---- + +local compressed = lzw:compress("Some text or arbitrary data in a string"); +local decompressed = lzw:decompress(compressed); + +---- + +Optimal compression can be achieved by creating code tables for specific input sets. For example: + +---- + +local DecompressTable = {}; +local CompressTable = {}; +local TableSize; + +lzw:begin_analyze(); + +for all input strings... + lzw:analyze_string( Str ); +... + +TableSize = lzw:end_analyze(DecompressTable, CompressTable); + +local compressed = lzw:compress("Some data to compress", nil, CompressTable, TableSize); +local decompressed = lzw:decompress(compressed, nil, DecompressTable, TableSize); + +---- + +Note that the decompression table and table size must be stored to be able to decompress data at a later point in time. + +---- + +The resulting compressed string can optionally be output in a chat safe format. + +---- + +local compressed = lzw:compress( Str, true ); +local decompressed = lzw:decompress( Str, true ); + +---- + +Note that a string that has been compressed using the chat safe flag must be decompressed with the flag or the decompressed +string may become garbled. + +]]-- +local version = 1; + +local lib = { + version = version; + RB_TempStream = ""; + WB_TempStream = ""; + WB_Used = 0; + WB_Curr = 0; + WB_Max = 8; + RB_Curr = 0; + RB_Loaded = 0; + RB_StreamPos = 0; + RB_StreamLen = 0; + RB_ByteTable = {}; + AnalyzeCharacters = {}; +}; + +local function CopyTable(var) + if ( not var ) then return; end + + if ( type(var) == "table" ) then + local ret = {}; + for k, v in var do + ret[k] = CopyTable(v); + end + return ret; + else + return var; + end +end + +local function GetRequiredBitDepth( TableSize ) + if( TableSize >= 256 ) then + return 9; + elseif( TableSize >= 128 ) then + return 8; + elseif( TableSize >= 64 ) then + return 7; + elseif( TableSize >= 32 ) then + return 6; + elseif( TableSize >= 16 ) then + return 5; + elseif( TableSize >= 8 ) then + return 4; + elseif( TableSize >= 4 ) then + return 3; + elseif( TableSize >= 2 ) then + return 2; + elseif( TableSize >= 1 ) then + return 1; + end +end + +function lib:init() + if ( self.DefaultLZWTableSize ) then return; end + + local QMask = bit.lshift( 1, 30 )-1; + local MaxSize = bit.lshift( 1, 30 ); + local MaxMask = MaxSize-1; + + self.MaskTable = {}; + for i=0, 30 do + local TempMask = bit.band( bit.lshift( MaxSize-1, i ), QMask ); + self.MaskTable[i] = bit.band( bit.bnot(TempMask), MaxMask ); + end + + self.DefaultLZWTable = {}; + self.DefaultLZWReverseTable = {}; + self.DefaultLZWTableSize = 1; + -- Initialize the reverse code table (Code Index -> String array). + for i=1, 255 do + tinsert( self.DefaultLZWReverseTable, string.char(i)); + self.DefaultLZWTableSize = self.DefaultLZWTableSize + 1; + end + + -- Our initial code table is the ASCII character set, codes 1-255. + -- String -> Code Index + for i, char in ipairs(self.DefaultLZWReverseTable) do + self.DefaultLZWTable[char] = i; + end +end + +function lib:readbits( Bits, Stream ) + if( Stream ) then + self.RB_TempStream = Stream; + self.RB_StreamPos = 1; + self.RB_StreamLen = string.len(Stream); + self.RB_Curr = 0; + self.RB_Loaded = 0; + for i=1, self.RB_StreamLen do + self.RB_ByteTable[i] = string.byte( string.sub( self.RB_TempStream, i, i) ); + end + end + local Val = 0; + local BitsRead = 0; + + while( BitsRead < Bits ) do + if( self.RB_Loaded <= 0 ) then + if( self.RB_StreamPos == self.RB_StreamLen+1 ) then + return -1; + end + self.RB_Curr = self.RB_ByteTable[self.RB_StreamPos]; + self.RB_StreamPos = self.RB_StreamPos + 1; + self.RB_Loaded = self.WB_Max; + end + + local Avail = self.RB_Loaded; + local NumRead = Bits - BitsRead; + + if( NumRead > Avail ) then + NumRead = Avail; + end + + local CurrMask = bit.band( self.RB_Curr, self.MaskTable[NumRead] ); + if( BitsRead > 0 ) then + CurrMask = bit.lshift( CurrMask, BitsRead ); + end + Val = bit.bor( Val, CurrMask ); + + self.RB_Loaded = self.RB_Loaded - NumRead; + BitsRead = BitsRead + NumRead; + if( self.RB_Loaded > 0 ) then + self.RB_Curr = bit.rshift( self.RB_Curr, NumRead ); + end + end + return Val; +end + +function lib:writebits( Val, Bits ) + if( Val < 0 ) then + if( self.WB_Used > 0 ) then + self.WB_TempStream = self.WB_TempStream .. string.char( self.WB_Curr ); + end + self.WB_Used = 0; + self.WB_Curr = 0; + local StreamQ = self.WB_TempStream; + self.WB_TempStream = ""; + return( StreamQ ); + end + while( Bits > 0 ) do + local NumWrite = self.WB_Max - self.WB_Used; + if( NumWrite > Bits ) then + NumWrite = Bits; + end + local Over = Bits - NumWrite; + self.WB_Curr = bit.bor( self.WB_Curr, bit.lshift( bit.band( Val, self.MaskTable[NumWrite] ), self.WB_Used ) ); + self.WB_Used = self.WB_Used + NumWrite; + if( self.WB_Used == self.WB_Max ) then + self.WB_TempStream = self.WB_TempStream .. string.char( self.WB_Curr ); + self.WB_Curr = 0; + self.WB_Used = 0; + end + Bits = Over; + Val = bit.rshift( Val, NumWrite ); + end +end + +local tab = string.char(9); +local newline = string.char(10); +local percent = string.char(37); +local percentpercent = percent..percent; +local pipe = string.char(124); +local special = string.char(255); + +local specialtab = special..string.char(1); +local specialnewline = special..string.char(2); +local specialpercent = special..string.char(3); +local specialpipe = special..string.char(4); +local specials = special..string.char(5); +local specialS = special..string.char(6); +local specialspecial = special..special; + +function lib:makesafe( Input ) + Input = string.gsub(Input, "s", specials); + Input = string.gsub(Input, "S", specialS); + Input = string.gsub(Input, special, specialspecial); + Input = string.gsub(Input, tab, specialtab); + Input = string.gsub(Input, newline, specialnewline); + Input = string.gsub(Input, percentpercent, specialpercent); + Input = string.gsub(Input, pipe, specialpipe); + return Input; +end + +function lib:unsafe( Input ) + Input = string.gsub(Input, specials, "s"); + Input = string.gsub(Input, specialS, "S"); + Input = string.gsub(Input, specialspecial, special); + Input = string.gsub(Input, specialtab, tab); + Input = string.gsub(Input, specialnewline, newline); + Input = string.gsub(Input, specialpercent, percent); + Input = string.gsub(Input, specialpipe, pipe); + return Input; +end + +function lib:compress( Input, Safe, LZWTable, LZWTableSize ) + + if( not LZWTable ) then + LZWTable = self.DefaultLZWTable; + LZWTableSize = self.DefaultLZWTableSize; + end + + local TableSize = LZWTableSize; + local CodeTable = CopyTable( LZWTable ); + local StreamPos = 1; + local StreamLen = string.len(Input); + local CodeBits = GetRequiredBitDepth( LZWTableSize ); + local MaxCode = 2 ^ CodeBits; + + -- Grab the first character out of Input into Work. + local Work = string.sub( Input, StreamPos, StreamPos ); + StreamPos = StreamPos + 1; + + -- Compress the input string. + while( StreamPos <= StreamLen ) do + -- Chop next character off input. + local Char = string.sub( Input, StreamPos, StreamPos ); + StreamPos = StreamPos + 1; + -- Lookup string in the code table. + if( CodeTable[Work..Char] ) then + -- If it's in the code table, do nothing. + Work = Work .. Char; + else + if( TableSize == MaxCode ) then + CodeBits = CodeBits + 1; + MaxCode = MaxCode * 2; + end + -- If the string is NOT in the code table, output the previous + -- string, which WAS in the code table (see above). + self:writebits( CodeTable[Work], CodeBits ); + + -- Add this new string to the code table. + CodeTable[Work..Char] = TableSize; + TableSize = TableSize + 1; + + -- Reset and start again. + Work = Char; + end + end + + -- Ran out of new characters, so just output whatever we had at the end. + self:writebits( CodeTable[Work], CodeBits ); + + -- Finally get result stream. + if ( Safe ) then + -- Format safe for sending over chat + return self:makesafe(self:writebits( -1 )); + else + return self:writebits( -1 ); + end +end + +function lib:decompress( Input, Safe, LZWReverseTable, LZWTableSize ) + + if ( Safe ) then + -- Decode chat safe string + Input = self:unsafe(Input); + end + + if( not LZWReverseTable ) then + LZWReverseTable = self.DefaultLZWReverseTable; + LZWTableSize = self.DefaultLZWTableSize; + end + + local Table = CopyTable( LZWReverseTable ); + local TableSize = LZWTableSize; + local CodeBits = GetRequiredBitDepth( TableSize ); + local MaxCode = 2 ^ CodeBits; + + if( TableSize == MaxCode ) then + CodeBits = CodeBits + 1; + MaxCode = MaxCode * 2; + end + + -- Grab first code. + local LastCode = self:readbits( CodeBits, Input ); + + local Code = 0; + -- Result string, starts out with a simple ascii character from the first code. + local Result = Table[LastCode]; + local LastChar = Result; + + Code = self:readbits( CodeBits ); + + while( Code > 0 ) do + local CodeString; + if( Table[Code] ) then + CodeString = Table[Code]; + else + CodeString = Table[LastCode] .. LastChar; + end + + Result = Result .. CodeString; + LastChar = string.sub( CodeString, 1, 1 ); + + -- Create a new code based on the string from the last code, and the first character of this one. + Table[TableSize] = Table[LastCode] .. LastChar; + TableSize = TableSize + 1; + + if( TableSize+1 == MaxCode ) then + CodeBits = CodeBits + 1; + MaxCode = MaxCode * 2; + end + + LastCode = Code; + + Code = self:readbits( CodeBits ); + end + return Result; +end + +function lib:begin_analyze() + self.AnalyzeCharacters = {}; +end + +function lib:end_analyze( OutTable ,OutReverseTable ) + local OutTableSize = 1; + OutTable[0] = nil; + for k, v in self.AnalyzeCharacters do + OutReverseTable[k] = OutTableSize; + OutTableSize = OutTableSize + 1; + end + + for k, v in OutReverseTable do + OutTable[v] = k; + end + self.AnalyzeCharacters = {}; + return OutTableSize; +end + +function lib:analyze_string( String ) + local ClearLen = string.len( String ); + for i=1, ClearLen do + local c = string.sub( String, i, i ); + self.AnalyzeCharacters[c] = 1; + end +end + +if ( not lzw or lzw.version < lzw.version ) then + lzw = lib; + lib:init(); +end