Browse Source

Initial Commit

Roman Hergenreder 1 year ago
commit
3e730b6dce
6 changed files with 760 additions and 0 deletions
  1. 1 0
      .gitignore
  2. 57 0
      create_patch.py
  3. 24 0
      install_addon.sh
  4. 429 0
      patches/DBM-Core.patch
  5. 74 0
      patches/DBM-GUI.patch
  6. 175 0
      update_addons.py

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+/AddOns/

+ 57 - 0
create_patch.py

@@ -0,0 +1,57 @@
+#!/usr/bin/python3
+
+import os
+import re
+import sys
+import zipfile
+import datetime
+import subprocess
+import tempfile
+
+
+def get_original_zip(name):
+    latest_version = None
+    latest_file = None
+    for file in os.listdir("./AddOns"):
+        if re.match(f"{name}(-|_|\+)(v|r)?(.*).zip$", file, flags=re.IGNORECASE):
+            file = os.path.join("./AddOns", file)
+            modified = datetime.datetime.fromtimestamp(os.path.getmtime(file))
+            if latest_version is None or modified > latest_version:
+                latest_version = modified
+                latest_file = file
+
+    return latest_file
+
+def create_patch(orig_directory, curr_directory):
+    dir_name = os.path.basename(orig_directory)
+    orig_directory = os.path.realpath(orig_directory)
+    # curr_directory = os.path.realpath(curr_directory)
+    proc = subprocess.Popen(["diff", "-ruN", orig_directory, curr_directory], stdout=subprocess.PIPE)
+    output = proc.communicate()[0]
+    if output:
+        print("Creating Patch for:", dir_name)
+        if not os.path.isdir("./patches"):
+            os.makedirs("./patches")
+
+        with open(f"./patches/{dir_name}.patch", "wb") as f:
+            f.write(output)
+
+if __name__ == "__main__":
+    if len(sys.argv) < 1:
+        print("Usage:", sys.argv[0], "<addon>")
+    else:
+        addon = sys.argv[1]
+        original_file = get_original_zip(addon)
+        if not original_file:
+            print("Could not find original zip file. Are you executing this script from the correct path?")
+        else:
+            zip_file = zipfile.ZipFile(original_file, "r")
+            with tempfile.TemporaryDirectory() as temp_dir:
+                zip_file.extractall(temp_dir)
+                for directory in os.listdir(temp_dir):
+                    cur_dir = os.path.join("./AddOns", directory)
+                    orig_directory = os.path.join(temp_dir, directory)
+                    if os.path.isdir(orig_directory) and os.path.isdir(cur_dir):
+                        create_patch(orig_directory, cur_dir)
+
+                zip_file.close()

+ 24 - 0
install_addon.sh

@@ -0,0 +1,24 @@
+#!/bin/bash
+
+
+if [ $# -lt 1 ]; then
+  echo "Invalid usage: $0 <file>"
+  exit
+fi
+
+FILE=$1
+if [ ! -f "$FILE" ]; then
+    echo "File not found: $FILE"
+    exit
+fi
+
+FILE_NAME=$(basename $FILE)
+if [ ! $FILE -ef ./AddOns/$FILE_NAME ]; then
+    cp $FILE ./AddOns/
+fi
+
+for dir in $(zipinfo -1 $FILE | grep '/' | awk -F '/' '{print $1}' | sort -u); do
+  rm -rfd ./AddOns/$dir
+done
+
+unzip $FILE -d ./AddOns/

+ 429 - 0
patches/DBM-Core.patch

@@ -0,0 +1,429 @@
+diff -ruN /tmp/tmpko_jq3r7/DBM-Core/DBM-BossHealth.lua ./AddOns/DBM-Core/DBM-BossHealth.lua
+--- /tmp/tmpko_jq3r7/DBM-Core/DBM-BossHealth.lua	1970-01-01 01:00:00.000000000 +0100
++++ ./AddOns/DBM-Core/DBM-BossHealth.lua	2022-10-23 19:11:05.902083115 +0200
+@@ -0,0 +1,287 @@
++---------------
++--  Globals  --
++---------------
++DBM.BossHealth = {}
++
++
++-------------
++--  Locals --
++-------------
++local bossHealth = DBM.BossHealth
++local bars = {}
++local barCache = {}
++local updateFrame
++local getBarId
++local updateBar
++local anchor
++local header
++local dropdownFrame
++--local sortingEnabled
++
++do
++	local id = 0
++	function getBarId()
++		id = id + 1
++		return id
++	end
++end
++
++------------
++--  Menu  --
++------------
++local menu
++menu = {
++	{
++		text = DBM_CORE_RANGECHECK_LOCK,
++		checked = false, -- requires DBM.Options which is not available yet
++		func = function()
++			menu[1].checked = not menu[1].checked
++			DBM.Options.HealthFrameLocked = menu[1].checked
++		end
++	},
++	{
++		text = DBM_CORE_BOSSHEALTH_HIDE_FRAME,
++		notCheckable = true,
++		func = function() bossHealth:Hide() end
++	}
++}
++
++
++-----------------------
++--  Script Handlers  --
++-----------------------
++local function onMouseDown(self, button)
++	if button == "LeftButton" and not DBM.Options.HealthFrameLocked then
++		anchor.moving = true
++		anchor:StartMoving()
++	end
++end
++
++local function onMouseUp(self, button)
++	anchor.moving = nil
++	anchor:StopMovingOrSizing()
++	local point, _, _, x, y = anchor:GetPoint(1)
++	DBM.Options.HPFramePoint = point
++	DBM.Options.HPFrameX = x
++	DBM.Options.HPFrameY = y
++	if button == "RightButton" then
++		EasyMenu(menu, dropdownFrame, "cursor", nil, nil, "MENU")
++	end
++end
++
++local onHide = onMouseUp
++
++
++-----------------
++-- Apply Style --
++-----------------
++local function updateBarStyle(bar, id)
++	bar:ClearAllPoints()
++	if DBM.Options.HealthFrameGrowUp then
++		bar:SetPoint("BOTTOM", bars[id - 1] or anchor, "TOP", 0, 0)
++	else
++		bar:SetPoint("TOP", bars[id - 1] or anchor, "BOTTOM", 0, 0)
++	end
++	local barborder = _G[bar:GetName().."BarBorder"]
++	local barbar = _G[bar:GetName().."Bar"]
++	local width = DBM.Options.HealthFrameWidth
++	if width < 175 then -- these health frames really suck :(
++		barbar:ClearAllPoints()
++		barbar:SetPoint("CENTER", barbar:GetParent(), "CENTER", -6, 0)
++		bar:SetWidth(DBM.Options.HealthFrameWidth)
++		barborder:SetWidth(DBM.Options.HealthFrameWidth * 0.99)
++		barbar:SetWidth(DBM.Options.HealthFrameWidth * 0.95)
++	elseif width >= 225 then
++		barbar:ClearAllPoints()
++		barbar:SetPoint("CENTER", barbar:GetParent(), "CENTER", 5, 0)
++		bar:SetWidth(DBM.Options.HealthFrameWidth)
++		barborder:SetWidth(DBM.Options.HealthFrameWidth * 0.995)
++		barbar:SetWidth(DBM.Options.HealthFrameWidth * 0.965)
++	else
++		bar:SetWidth(DBM.Options.HealthFrameWidth)
++		barborder:SetWidth(DBM.Options.HealthFrameWidth * 0.99)
++		barbar:SetWidth(DBM.Options.HealthFrameWidth * 0.95)
++	end
++end
++
++-----------------------
++-- Create the Frame  --
++-----------------------
++local function createFrame(self)
++	anchor = CreateFrame("Frame", nil, UIParent)
++	anchor:SetWidth(60)
++	anchor:SetHeight(10)
++	anchor:SetMovable(1)
++	anchor:EnableMouse(1)
++	anchor:SetPoint(DBM.Options.HPFramePoint, UIParent, DBM.Options.HPFramePoint, DBM.Options.HPFrameX, DBM.Options.HPFrameY)
++	header = anchor:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
++	header:SetPoint("BOTTOM", anchor, "BOTTOM")
++	anchor:SetScript("OnUpdate", updateFrame)
++	anchor:SetScript("OnMouseDown", onMouseDown)
++	anchor:SetScript("OnMouseUp", onMouseUp)
++	anchor:SetScript("OnHide", onHide)
++	dropdownFrame = CreateFrame("Frame", "DBMBossHealthDropdown", anchor, "UIDropDownMenuTemplate")
++	menu[1].checked = DBM.Options.HealthFrameLocked
++end
++
++local function createBar(self, cId, name)
++	local bar = table.remove(barCache, #barCache) or CreateFrame("Frame", "DBM_BossHealth_Bar_"..getBarId(), anchor, "DBMBossHealthBarTemplate")
++	bar:Show()
++	local bartext = _G[bar:GetName().."BarName"]
++	local barborder = _G[bar:GetName().."BarBorder"]
++	local barbar = _G[bar:GetName().."Bar"]
++	barborder:SetScript("OnMouseDown", onMouseDown)
++	barborder:SetScript("OnMouseUp", onMouseUp)
++	barborder:SetScript("OnHide", onHide)
++	bar.id = cId
++	bar.hidden = false
++	bar:ClearAllPoints()
++	bartext:SetText(name)
++	updateBar(bar, 100)
++	return bar
++end
++
++
++
++------------------
++--  Bar Update  --
++------------------
++function updateBar(bar, percent, dontShowDead)
++	local bartimer = _G[bar:GetName().."BarTimer"]
++	local barbar = _G[bar:GetName().."Bar"]
++	bartimer:SetText((percent > 0 or dontShowDead) and math.floor(percent).."%" or DBM_CORE_DEAD)
++	barbar:SetValue(percent)
++	barbar:SetStatusBarColor((100 - percent) / 100, percent/100, 0)
++	bar.value = percent
++	local bossAlive = false
++	for i = 1, #bars do
++		if bars[i].value > 0 then
++			bossAlive = true
++			break
++		end
++	end
++	if not bossAlive and #bars > 0 then
++		bossHealth:Hide()
++	end
++end
++
++do
++	local t = 0
++	local targetCache = {}
++	
++	local function getCIDfromGUID(guid)
++		local guidType, _, playerdbID, _, _, cid, _ = strsplit("-", guid or "")
++		if guidType and (guidType == "Creature" or guidType == "Vehicle" or guidType == "Pet") then
++			return tonumber(cid)
++		elseif type and (guidType == "Player" or guidType == "Item") then
++			return tonumber(playerdbID)
++		end
++		return 0
++	end
++	
++--	local function compareBars(b1, b2)
++--		return b1.value > b2.value
++--	end
++	
++	function updateFrame(self, e)
++		t = t + e
++		if t >= 0.5 then
++			t = 0
++--			if #bars > DBM.Options.HPFrameMaxEntries then
++--				sortingEnabled = true
++--			end
++--			if sortingEnabled then
++--				table.sort(bars, compareBars)
++--			end
++			for i, v in ipairs(bars) do
++--				if i > DBM.Options.HPFrameMaxEntries then
++--					v:Hide()
++--				else
++--					v:Show()
++--				end
++				if type(v.id) == "number" then
++					local id = targetCache[v.id] -- ask the cache if we already know where the mob is
++					if getCIDfromGUID(UnitGUID(id or "")) ~= v.id then -- the cache doesn't know it, update the cache
++						targetCache[v.id] = nil
++						-- check focus target
++						if getCIDfromGUID(UnitGUID("focus")) == v.id then
++							targetCache[v.id] = "focus"
++						else
++							-- check target and raid/party targets
++							local uId = ((IsInRaid()) and "raid") or "party"
++							for i = 0, GetNumGroupMembers() do
++								id = (i == 0 and "target") or uId..i.."target"
++								if getCIDfromGUID(UnitGUID(id or "")) == v.id then
++									targetCache[v.id] = id
++									break
++								end
++							end
++						end
++					end
++					if getCIDfromGUID(UnitGUID(id or "")) == v.id then -- did we find the mob? if yes: update the health bar
++						updateBar(v, ((UnitHealth(id)) / (UnitHealthMax(id)) * 100 or 100))
++					end
++				elseif type(v.id) == "function" then -- generic bars
++					updateBar(v, v.id(), true)
++				end
++			end
++		end
++	end
++end
++
++-----------------------
++--  General Methods  --
++-----------------------
++function bossHealth:Show(name)
++	if not anchor then createFrame(bossHealth) end
++	header:SetText(name)
++	anchor:Show()
++	bossHealth:Clear()
++end
++
++function bossHealth:Clear()
++	if not anchor or not anchor:IsShown() then return end
++	for i = #bars, 1, -1 do
++		local bar = bars[i]
++		bar:Hide()
++		bar:ClearAllPoints()
++		barCache[#barCache + 1] = bar
++		bars[i] = nil
++	end
++--	sortingEnabled = false
++end
++
++function bossHealth:Hide()
++	if anchor then anchor:Hide() end
++end
++
++function bossHealth:AddBoss(cId, name)
++	if not anchor or not anchor:IsShown() then return end
++	table.insert(bars, createBar(self, cId, name))
++	updateBarStyle(bars[#bars], #bars)
++end
++
++function bossHealth:RemoveBoss(cId)
++	if not anchor or not anchor:IsShown() then return end
++	for i = #bars, 1, -1 do
++		local bar = bars[i]
++		if bar.id == cId then
++			if bars[i + 1] then
++				local next = bars[i + 1]
++				next:SetPoint("TOP", bars[i - 1] or anchor, "BOTTOM", 0, 0)
++			end
++			bar:Hide()
++			bar:ClearAllPoints()
++			barCache[#barCache + 1] = bar
++			table.remove(bars, i)
++		end
++	end
++end
++
++function bossHealth:UpdateSettings()
++	if not anchor then createFrame(bossHealth) end
++	anchor:SetPoint(DBM.Options.HPFramePoint, UIParent, DBM.Options.HPFramePoint, DBM.Options.HPFrameX, DBM.Options.HPFrameY)
++	for i, v in ipairs(bars) do
++		updateBarStyle(v, i)
++	end
++end
+diff -ruN /tmp/tmpko_jq3r7/DBM-Core/DBM-BossHealth.xml ./AddOns/DBM-Core/DBM-BossHealth.xml
+--- /tmp/tmpko_jq3r7/DBM-Core/DBM-BossHealth.xml	1970-01-01 01:00:00.000000000 +0100
++++ ./AddOns/DBM-Core/DBM-BossHealth.xml	2022-10-23 12:32:08.208657820 +0200
+@@ -0,0 +1,71 @@
++<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
++..\FrameXML\UI.xsd">
++	<Frame name="DBMBossHealthBarTemplate" enableMouse="true" virtual="true" frameStrata="MEDIUM" topLevel="true">
++		<Size>
++			<AbsDimension x="200" y="20"/>
++		</Size>
++		<Frames>
++			<StatusBar name="$parentBar">
++				<Size>
++					<AbsDimension x="190" y="12"/>
++				</Size>
++				<Anchors>
++					<Anchor point="CENTER" relativePoint="CENTER">
++						<Offset>
++							<AbsDimension x="2" y="0"/>
++						</Offset>
++					</Anchor>
++				</Anchors>
++				<Layers>
++					<Layer level="BACKGROUND">
++						<Texture name="$parentBackground">
++							<Color r="0" g="0" b="0" a="0.3"/>
++						</Texture>
++					</Layer>
++					<Layer level="OVERLAY">
++						<FontString name="$parentName" inherits="GameFontHighlightSmall" text="">
++							<Anchors>
++								<Anchor point="LEFT" relativePoint="LEFT">
++									<Offset>
++										<AbsDimension x="2" y="1"/>
++									</Offset>
++								</Anchor>
++							</Anchors>
++						</FontString>
++						<FontString name="$parentTimer" inherits="GameFontHighlightSmall" text="">
++							<Anchors>
++								<Anchor point="RIGHT" relativePoint="RIGHT">
++									<Offset>
++										<AbsDimension x="-2" y="1"/>
++									</Offset>
++								</Anchor>
++							</Anchors>
++						</FontString>
++					</Layer>
++				</Layers>
++				<Frames>
++					<Button name="$parentBorder">
++						<Size>
++							<AbsDimension x="198" y="32"/>
++						</Size>
++						<Anchors>
++							<Anchor point="LEFT">
++								<Offset>
++									<AbsDimension x="-4" y="0"/>
++								</Offset>
++							</Anchor>
++						</Anchors>
++						<NormalTexture name="$parentTextureNormal" file="Interface\PaperDollInfoFrame\UI-Character-Skills-BarBorder"/>
++					</Button>
++				</Frames>
++				<BarTexture name="$parentTextureBar" file="Interface\PaperDollInfoFrame\UI-Character-Skills-Bar"/>
++				<BarColor r="1.0" g="0.7" b="0.0"/>
++				<Scripts>
++					<OnLoad>
++						self:SetMinMaxValues(0, 100)
++					</OnLoad>
++				</Scripts>
++			</StatusBar>
++		</Frames>
++	</Frame>
++</Ui>
+diff -ruN /tmp/tmpko_jq3r7/DBM-Core/DBM-Core.lua ./AddOns/DBM-Core/DBM-Core.lua
+--- /tmp/tmpko_jq3r7/DBM-Core/DBM-Core.lua	2022-10-23 21:39:59.728572004 +0200
++++ ./AddOns/DBM-Core/DBM-Core.lua	2022-10-23 13:24:33.675244610 +0200
+@@ -370,6 +370,16 @@
+ 	ChatFrame = "DEFAULT_CHAT_FRAME",
+ 	CoreSavedRevision = 1,
+ 	SilentMode = false,
++	-- Custom
++	AlwaysShowHealthFrame = false,
++	HealthFrameGrowUp = false,
++	HealthFrameLocked = false,
++	HealthFrameWidth = 200,
++	HPFramePoint = "CENTER",
++	HPFrameX = -50,
++	HPFrameY = 50,
++	HPFrameMaxEntries = 5,
++
+ }
+ 
+ DBM.Mods = {}
+@@ -5020,6 +5030,19 @@
+ 						end
+ 					end
+ 				end
++
++				-- Custom
++				if (DBM.Options.AlwaysShowHealthFrame or mod.Options.HealthFrame) and mod.Options.Enabled then
++					DBM.BossHealth:Show(mod.localization.general.name)
++					if mod.bossHealthInfo then
++						for i = 1, #mod.bossHealthInfo, 2 do
++							DBM.BossHealth:AddBoss(mod.bossHealthInfo[i], mod.bossHealthInfo[i + 1])
++						end
++					else
++						DBM.BossHealth:AddBoss(mod.combatInfo.mob, mod.localization.general.name)
++					end
++				end
++
+ 				--call OnCombatStart
+ 				if mod.OnCombatStart then
+ 					local startEvent = syncedEvent or event
+@@ -5436,6 +5459,8 @@
+ 				self:CreatePizzaTimer(time, "", nil, nil, nil, true)--Auto Terminate infinite loop timers on combat end
+ 				self:TransitionToDungeonBGM(false, true)
+ 				self:Schedule(22, self.TransitionToDungeonBGM, self)
++				-- Custom
++				DBM.BossHealth:Hide()
+ 				--module cleanup
+ 				private:ClearModuleTasks()
+ 			end
+diff -ruN /tmp/tmpko_jq3r7/DBM-Core/DBM-Core.toc ./AddOns/DBM-Core/DBM-Core.toc
+--- /tmp/tmpko_jq3r7/DBM-Core/DBM-Core.toc	2022-10-23 21:39:59.741905320 +0200
++++ ./AddOns/DBM-Core/DBM-Core.toc	2022-10-23 12:32:46.308588636 +0200
+@@ -85,3 +85,10 @@
+ modules\UpdateReminder.lua
+ 
+ modules\objects\Localization.lua
++
++
++
++# Custom
++DBM-BossHealth.lua
++DBM-BossHealth.xml
++

+ 74 - 0
patches/DBM-GUI.patch

@@ -0,0 +1,74 @@
+diff -ruN /tmp/tmpko_jq3r7/DBM-GUI/DBM-GUI.toc ./AddOns/DBM-GUI/DBM-GUI.toc
+--- /tmp/tmpko_jq3r7/DBM-GUI/DBM-GUI.toc	2022-10-23 21:39:59.755238639 +0200
++++ ./AddOns/DBM-GUI/DBM-GUI.toc	2022-10-23 15:23:11.962084908 +0200
+@@ -74,3 +74,4 @@
+ modules\options\frames\InfoFrame.lua
+ modules\options\frames\Range.lua
+ modules\options\frames\Nameplate.lua
++modules\options\frames\BossHealth.lua
+\ Kein Zeilenumbruch am Dateiende.
+diff -ruN /tmp/tmpko_jq3r7/DBM-GUI/localization.de.lua ./AddOns/DBM-GUI/localization.de.lua
+--- /tmp/tmpko_jq3r7/DBM-GUI/localization.de.lua	2022-10-23 21:39:59.755238639 +0200
++++ ./AddOns/DBM-GUI/localization.de.lua	2022-10-23 15:23:11.962084908 +0200
+@@ -466,3 +466,10 @@
+ L.Area_Position="Position"
+ L.Area_Style="Stil"
+ L.Area_General="Allgemein"
++
++
++L.Panel_BossHealth="Lebensanzeige"
++L.Area_HPFrame                          = "Lebensanzeige-Optionen"
++L.HP_Enabled                            = "Lebensanzeige immer anzeigen (überschreibt boss-spezifische Option)"
++L.HP_GrowUpwards                        = "Erweitere Lebensanzeige nach oben"
++L.HP_ShowDemo                           = "Zeige Lebensanzeige"
+diff -ruN /tmp/tmpko_jq3r7/DBM-GUI/modules/options/frames/BossHealth.lua ./AddOns/DBM-GUI/modules/options/frames/BossHealth.lua
+--- /tmp/tmpko_jq3r7/DBM-GUI/modules/options/frames/BossHealth.lua	1970-01-01 01:00:00.000000000 +0100
++++ ./AddOns/DBM-GUI/modules/options/frames/BossHealth.lua	2022-10-23 15:23:11.962084908 +0200
+@@ -0,0 +1,46 @@
++local L = DBM_GUI_L
++local hpPanel = DBM_GUI.Cat_Frames:CreateNewPanel(L.Panel_BossHealth, "option")
++
++local hpArea = hpPanel:CreateArea(L.Area_HPFrame, nil, 150, true)
++hpArea:CreateCheckButton(L.HP_Enabled, true, nil, "AlwaysShowHealthFrame")
++local growbttn = hpArea:CreateCheckButton(L.HP_GrowUpwards, true)
++growbttn:SetScript("OnShow",  function(self) self:SetChecked(DBM.Options.HealthFrameGrowUp) end)
++growbttn:SetScript("OnClick", function(self) 
++        DBM.Options.HealthFrameGrowUp = not not self:GetChecked() 
++        DBM.BossHealth:UpdateSettings()
++end)
++
++
++local BarWidthSlider = hpArea:CreateSlider(L.BarWidth, 150, 275, 1)
++BarWidthSlider:SetPoint("TOPLEFT", hpArea.frame, "TOPLEFT", 20, -75)
++BarWidthSlider:SetScript("OnShow", function(self) self:SetValue(DBM.Options.HealthFrameWidth or 100) end)
++BarWidthSlider:HookScript("OnValueChanged", function(self) 
++        DBM.Options.HealthFrameWidth = self:GetValue()
++        DBM.BossHealth:UpdateSettings()
++end)
++
++local resetbutton = hpArea:CreateButton(L.Reset, 120, 16)
++resetbutton:SetPoint('BOTTOMRIGHT', hpArea.frame, "BOTTOMRIGHT", -5, 5)
++resetbutton:SetNormalFontObject(GameFontNormalSmall);
++resetbutton:SetHighlightFontObject(GameFontNormalSmall);		
++resetbutton:SetScript("OnClick", function()
++        DBM.Options.HPFramePoint = DBM.DefaultOptions.HPFramePoint
++        DBM.Options.HPFrameX = DBM.DefaultOptions.HPFrameX
++        DBM.Options.HPFrameY = DBM.DefaultOptions.HPFrameY
++        DBM.Options.HealthFrameGrowUp = DBM.DefaultOptions.HealthFrameGrowUp
++        DBM.Options.HealthFrameWidth = DBM.DefaultOptions.HealthFrameWidth
++        DBM.BossHealth:UpdateSettings()
++end)		
++
++local function createDummyFunc(i) return function() return i end end
++local showbutton = hpArea:CreateButton(L.HP_ShowDemo, 120, 16)
++showbutton:SetPoint('BOTTOM', resetbutton, "TOP", 0, 5)
++showbutton:SetNormalFontObject(GameFontNormalSmall);
++showbutton:SetHighlightFontObject(GameFontNormalSmall);		
++showbutton:SetScript("OnClick", function()
++        DBM.BossHealth:Show("Health Frame")
++        DBM.BossHealth:AddBoss(createDummyFunc(25), "TestBoss 1")
++        DBM.BossHealth:AddBoss(createDummyFunc(50), "TestBoss 2")
++        DBM.BossHealth:AddBoss(createDummyFunc(75), "TestBoss 3")
++        DBM.BossHealth:AddBoss(createDummyFunc(100), "TestBoss 4")			
++end)
+\ Kein Zeilenumbruch am Dateiende.

+ 175 - 0
update_addons.py

@@ -0,0 +1,175 @@
+#!/usr/bin/python3
+
+import time
+import re
+import os
+import shlex
+import requests
+import datetime
+import zipfile
+import shutil
+import urllib.parse
+from selenium import webdriver
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.chrome.options import Options
+from selenium.webdriver.common.keys import Keys
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.common.by import By
+from bs4 import BeautifulSoup
+
+USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0"
+BASE_URL = "https://www.curseforge.com"
+
+addons = {
+    "questie": "Questie",
+    # "omen-threat-meter": "Omen",
+    # "ora3": "oRA3",
+    "bagnon": "Bagnon",
+    "sexymap": "SexyMap",
+    "atlaslootclassic":"AtlasLootClassic",
+    "atlas-classicwow": "Atlas_ClassicWow",
+    "dbm-pvp": "DBM-PvP",
+    "deadly-boss-mods": "DBM",
+    "scrap": "Scrap",
+    "quartz": "Quartz",
+    "bartender4": "Bartender4",
+    "vuhdo": "VuhDo",
+    "power-auras-classic-v4": "PowerAuras",
+    "tacotip-gearscore-talents": "TacoTip",
+    "details": "Details",
+    # "weakauras-2": "WeakAuras",
+}
+
+
+def retrieve_last_update_ts(soup):
+
+    
+    div = soup.find("div", {"class": "cf-sidebar-inner"})
+    if not div:
+        return None
+    
+    elements = div.find_all(recursive=False)
+    for i, element in enumerate(elements):
+        if element.name == "h4" and element.text.strip() == "WoW Wrath of the Lich King Classic":
+            if i + 1 < len(elements) and elements[i+1].name == "ul":
+                try:
+                    abbr = elements[i+1].find("abbr")
+                    a = elements[i+1].find("a", {"data-tooltip": "Download file"})
+                    epoch = abbr["data-epoch"]
+                    if epoch:
+                        return datetime.datetime.fromtimestamp(int(epoch)), a["href"]
+                except Exception as ex:
+                    print(ex)
+                    pass
+    
+    print("Element not found")
+
+def get_current_version(name):
+    latest_version = None
+    for file in os.listdir("./AddOns"):
+        if re.match(f"{name}(-|_|\+)(v|r)?(.*).zip$", file, flags=re.IGNORECASE):
+            file = os.path.join("./AddOns", file)
+            modified = datetime.datetime.fromtimestamp(os.path.getmtime(file))
+            if latest_version is None or modified > latest_version:
+                latest_version = modified
+
+    return latest_version
+
+
+def wait_for_download(driver, name):
+    driver.execute_script("window.open('');")
+    time.sleep(1)
+    driver.switch_to.window(driver.window_handles[-1])
+    driver.get("chrome://downloads/")
+    downloaded_file = None
+    while not downloaded_file:
+        files = driver.execute_script("""
+            var downloadsManager = document.querySelector('downloads-manager');
+            if (downloadsManager) {
+                var items = downloadsManager.shadowRoot.getElementById('downloadsList').items;
+                if (items.every(e => e.state === "COMPLETE"))
+                    return items.map(e => e.fileUrl || e.file_url);
+            }""")
+        
+        if files:
+            for file in files:
+                parts = urllib.parse.urlparse(file)
+                if parts.scheme == "file":
+                    abs_path = urllib.parse.unquote(parts.path)
+                    if os.path.isfile(abs_path):
+                        if re.match(f"{name}(-|_|\+)v?(.*).zip$", os.path.basename(abs_path), flags=re.IGNORECASE):
+                            downloaded_file = abs_path
+                            break
+
+        time.sleep(1)
+    driver.execute_script("window.close();")
+    driver.switch_to.window(driver.window_handles[0])
+    return downloaded_file
+
+def extract_update(path):
+    zip_file = zipfile.ZipFile(path, "r")
+    directories = set()
+
+    for entry in zip_file.namelist():
+        if "/" in entry and not entry.startswith("/") and not ".." in entry:
+            directories.add(entry.split("/")[0])
+
+    for directory in directories:
+        if os.path.isdir(directory):
+            shutil.rmtree(directory)
+    
+    zip_file.extractall("./AddOns/")
+
+    for directory in directories:
+        patch_file = os.path.join("patches", f"{directory}.patch")
+        if os.path.isfile(patch_file):
+            patch_file_arg = shlex.quote(os.path.realpath(patch_file))
+            cwd = os.path.realpath(directory)
+            proc = subprocess.Popen(["sh", "-c", f"patch -p0 < {patch_file_arg}"], cwd=cwd)
+            proc.wait()
+
+    zip_file.close()
+
+def update_addon(driver, url, name):
+
+    current_version = get_current_version(name)
+    driver.get(f"{BASE_URL}/wow/addons/{url}")
+    WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.TAG_NAME, 'body')))
+
+    soup = BeautifulSoup(driver.page_source, "html.parser")
+    check = retrieve_last_update_ts(soup)
+    if not check:
+        print(f"[-] Unable to get last updated timestamp for: {addon}")
+        return False
+    
+    last_updated, download_url = check
+    ts = last_updated.strftime("%d.%m.%Y %H:%M")
+    current = "N/A" if current_version is None else current_version.strftime("%d.%m.%Y %H:%M")
+    print(f"[+] {addon} last updated @ {ts}, current @ {current}")
+    if current_version is None or last_updated > current_version:
+        print("[~] Downloading new file…")
+        driver.get(BASE_URL + download_url)
+        path = wait_for_download(driver, addon)
+        print("[+] Download completed:", os.path.basename(path))
+        extract_update(path)
+        print(f"[+] {addon} Updated successfully!")
+
+if __name__ == "__main__":
+
+    if not os.path.isdir("./AddOns/"):
+        print("AddOns directory not found. Are you executing this script from the correct path?")
+    else:
+
+        options = Options()
+        # options.headless = True
+        options.add_argument(f"user-agent={USER_AGENT}")
+        prefs = {
+            "download.default_directory": os.path.realpath("./AddOns"),
+            "profile.managed_default_content_settings.images": 2
+        }
+        options.add_experimental_option("prefs", prefs)
+
+        driver = webdriver.Chrome(options=options)
+        for url, addon in addons.items():
+            update_addon(driver, url, addon)
+        driver.close()