---------------------------------------------------- -- SamUI v0.3.3 -- Eine vollständige UI-Alternative -- WoW Classic Anniversary TBC -- Interface 20506 -- Build 69110 ---------------------------------------------------- local _, ns = ... local module = ns:new_module("profiles") local profile_providers = {} local profile_setup_required = false local default_profile_data local welcome_messages = { "Willkommen und möge Elune mit Euch sein.", "Willkommen, Held Azeroths – möge das Licht Euren Weg erhellen.", "Willkommen zurück, möge die Weisheit der Ahnen Euch begleiten.", "Willkommen, möge der Wind Euch auf Eurem Pfad tragen.", "Willkommen, Champion – möge Euch Ruhm und Ehre erwarten.", "Willkommen, tapferer Streiter – möge jeder Kampf in Sieg enden.", "Willkommen zurück in Azeroth, möge Weisheit Euren Weg säumen.", "Willkommen, möge die Kraft der Titanen mit Euch sein.", "Willkommen, Reisender – möge das Schicksal Euch wohlgesonnen sein.", "Willkommen zurück, möge ein warmes Lagerfeuer auf Euch warten.", "Willkommen, möge das Mondlicht Euren Pfad erleuchten.", "Willkommen, Held – möge Eure Kräfte niemals versiegen.", "Willkommen zurück, möge Ysera Euch ruhige Träume schenken.", "Willkommen, möge Euer Erz stets rein und Eure Rüstung stark sein.", "Willkommen, tapferer Abenteurer – möge Shattrath Euch Rast gewähren.", "Willkommen zurück, möge der Geist der Ahnen Euch leiten.", "Willkommen, möge die weite Wildnis Azeroths Euch Frieden schenken.", "Willkommen, Held Azeroths – möge Eure Klinge niemals stumpf werden.", "Willkommen zurück – möge das Glück auf Eurer Seite sein.", "Willkommen, Reisender – möge jeder Kampf Euch stärker machen.", "Willkommen zurück, möge Euer Mut niemals wanken.", "Willkommen, möge jeder Schritt Euch näher zum Ziel bringen.", "Willkommen zurück, möge das Glück Euer treuer Begleiter sein.", "Willkommen, Abenteurer – möge Azeroth Euch mit offenen Armen empfangen.", "Willkommen zurück, möge die Sterne Euch den Weg weisen.", } function ns:get_random_welcome_message() return welcome_messages[math.random(#welcome_messages)] end local hardcoded_default_profile_data = {["aktionsleisten"]={["allgemein"]={["cooldown_numbers_enabled"]=true,["lock_modifier"]="SHIFT",["tooltip_enabled"]=true,},["leiste1"]={["button_count"]=12,["buttons_per_row"]=6,["offset_x"]=0,["offset_y"]=20,["range_check_enabled"]=true,["scale"]=90,["spacing"]=10,},["leiste10"]={["button_count"]=12,["buttons_per_row"]=12,["enabled"]=false,["offset_x"]=0,["offset_y"]=398,["range_check_enabled"]=true,["scale"]=100,["spacing"]=4,},["leiste2"]={["button_count"]=12,["buttons_per_row"]=6,["enabled"]=true,["offset_x"]=-260,["offset_y"]=20,["range_check_enabled"]=true,["scale"]=90,["spacing"]=10,},["leiste3"]={["button_count"]=12,["buttons_per_row"]=6,["enabled"]=true,["offset_x"]=260,["offset_y"]=20,["range_check_enabled"]=true,["scale"]=90,["spacing"]=10,},["leiste4"]={["button_count"]=12,["buttons_per_row"]=12,["enabled"]=false,["offset_x"]=0,["offset_y"]=146,["range_check_enabled"]=true,["scale"]=100,["spacing"]=4,},["leiste5"]={["button_count"]=12,["buttons_per_row"]=12,["enabled"]=false,["offset_x"]=0,["offset_y"]=188,["range_check_enabled"]=true,["scale"]=100,["spacing"]=4,},["leiste6"]={["button_count"]=12,["buttons_per_row"]=12,["enabled"]=false,["offset_x"]=0,["offset_y"]=230,["range_check_enabled"]=true,["scale"]=100,["spacing"]=4,},["leiste7"]={["button_count"]=12,["buttons_per_row"]=12,["enabled"]=false,["offset_x"]=0,["offset_y"]=272,["range_check_enabled"]=true,["scale"]=100,["spacing"]=4,},["leiste8"]={["button_count"]=12,["buttons_per_row"]=12,["enabled"]=false,["offset_x"]=0,["offset_y"]=314,["range_check_enabled"]=true,["scale"]=100,["spacing"]=4,},["leiste9"]={["button_count"]=12,["buttons_per_row"]=12,["enabled"]=false,["offset_x"]=0,["offset_y"]=356,["range_check_enabled"]=true,["scale"]=100,["spacing"]=4,},["pet"]={["buttons_per_row"]=12,["enabled"]=true,["offset_x"]=260,["offset_y"]=110,["scale"]=70,["spacing"]=5,},["stance"]={["buttons_per_row"]=12,["enabled"]=true,["offset_x"]=-110,["offset_y"]=110,["scale"]=70,["spacing"]=5,},},["blizzard"]={["actionbars_shown"]=false,["arenaframes_shown"]=true,["groupframes_shown"]=true,["raidframes_shown"]=true,["unitframes_shown"]=false,},["minimap"]={["calendar_icon"]=false,["clock"]=true,["clock_24h"]=true,["group_finder_icon"]=true,["mail_icon"]=true,["zone_text"]=false,["zoom_buttons"]=false,},["tooltip"]={["guild_name_shown"]=true,["player_name_colored"]=true,},["unitframes"]={["pet"]={["aggro_glow_enabled"]=true,["health_smoothing_enabled"]=true,["enabled"]=true,["height"]=30,["offset_x"]=20,["offset_y"]=0,["width"]=100,},["player"]={["aggro_glow_enabled"]=true,["health_smoothing_enabled"]=true,["enabled"]=true,["height"]=30,["offset_x"]=-260,["offset_y"]=200,["width"]=240,},["target"]={["aggro_glow_enabled"]=true,["health_smoothing_enabled"]=true,["enabled"]=true,["height"]=30,["offset_x"]=520,["offset_y"]=0,["width"]=240,},["targettarget"]={["aggro_glow_enabled"]=true,["health_smoothing_enabled"]=true,["enabled"]=true,["height"]=30,["offset_x"]=-20,["offset_y"]=0,["width"]=100,},},} local start_marker = "<--SAMUI-START-->" local end_marker = "<--SAMUI-ENDE-->" local export_prefix = "SAMUI1:" local export_alphabet = "/+9876543210zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA" local export_decode_map = {} for index = 1, #export_alphabet do export_decode_map[export_alphabet:sub(index, index)] = index - 1 end local function encode_export_string(value) local output = {} for index = 1, #value, 3 do local first = value:byte(index) or 0 local second = value:byte(index + 1) or 0 local third = value:byte(index + 2) or 0 local remaining = #value - index + 1 local combined = first * 65536 + second * 256 + third local first_index = math.floor(combined / 262144) % 64 local second_index = math.floor(combined / 4096) % 64 local third_index = math.floor(combined / 64) % 64 local fourth_index = combined % 64 output[#output + 1] = export_alphabet:sub(first_index + 1, first_index + 1) output[#output + 1] = export_alphabet:sub(second_index + 1, second_index + 1) output[#output + 1] = remaining > 1 and export_alphabet:sub(third_index + 1, third_index + 1) or "=" output[#output + 1] = remaining > 2 and export_alphabet:sub(fourth_index + 1, fourth_index + 1) or "=" end return export_prefix .. table.concat(output) end local function decode_export_string(value) if type(value) ~= "string" then return nil, "ungültiges Format" end local compact = value:gsub("%s+", "") if compact:sub(1, #export_prefix) ~= export_prefix then return nil, "ungültiges Format" end local encoded = compact:sub(#export_prefix + 1) if encoded == "" or #encoded % 4 ~= 0 then return nil, "ungültiges Format" end local output = {} for index = 1, #encoded, 4 do local first_char = encoded:sub(index, index) local second_char = encoded:sub(index + 1, index + 1) local third_char = encoded:sub(index + 2, index + 2) local fourth_char = encoded:sub(index + 3, index + 3) local is_last_group = index + 3 == #encoded if first_char == "=" or second_char == "=" then return nil, "ungültiges Format" end if not is_last_group and (third_char == "=" or fourth_char == "=") then return nil, "ungültiges Format" end if third_char == "=" and fourth_char ~= "=" then return nil, "ungültiges Format" end local first_value = export_decode_map[first_char] local second_value = export_decode_map[second_char] local third_value = third_char == "=" and 0 or export_decode_map[third_char] local fourth_value = fourth_char == "=" and 0 or export_decode_map[fourth_char] if first_value == nil or second_value == nil or third_value == nil or fourth_value == nil then return nil, "ungültiges Format" end local combined = first_value * 262144 + second_value * 4096 + third_value * 64 + fourth_value output[#output + 1] = string.char(math.floor(combined / 65536) % 256) if third_char ~= "=" then output[#output + 1] = string.char(math.floor(combined / 256) % 256) end if fourth_char ~= "=" then output[#output + 1] = string.char(combined % 256) end end return table.concat(output) end local function deep_copy(value, seen) if type(value) ~= "table" then return value end seen = seen or {} if seen[value] then return seen[value] end local copy = {} seen[value] = copy for key, entry in pairs(value) do copy[deep_copy(key, seen)] = deep_copy(entry, seen) end return copy end local function merge_missing_values(destination, defaults) if type(destination) ~= "table" or type(defaults) ~= "table" then return end for key, default_value in pairs(defaults) do local current_value = destination[key] if current_value == nil then destination[key] = deep_copy(default_value) elseif type(current_value) == "table" and type(default_value) == "table" then merge_missing_values(current_value, default_value) end end end local function remove_obsolete_profile_values(data) if type(data) ~= "table" then return end local actionbar_data = data.aktionsleisten local stance_data = type(actionbar_data) == "table" and actionbar_data.stance or nil if type(stance_data) == "table" then stance_data.button_count = nil end end local function get_sorted_keys(data) local keys = {} for key in pairs(data) do keys[#keys + 1] = key end table.sort(keys, function(left, right) local left_type = type(left) local right_type = type(right) if left_type ~= right_type then return left_type < right_type end if left_type == "number" then return left < right end return tostring(left) < tostring(right) end) return keys end local function is_finite_number(value) return type(value) == "number" and value == value and value < math.huge and value > -math.huge end local function serialize_value(value, seen) local value_type = type(value) if value_type == "nil" then return "nil" elseif value_type == "boolean" then return tostring(value) elseif value_type == "number" then if not is_finite_number(value) then return nil, "ungültiger Zahlenwert" end return tostring(value) elseif value_type == "string" then return string.format("%q", value) elseif value_type ~= "table" then return nil, "nicht unterstützter Datentyp" end if seen[value] then return nil, "zyklische Tabellen werden nicht unterstützt" end seen[value] = true local parts = { "{" } local keys = get_sorted_keys(value) for index = 1, #keys do local key = keys[index] local key_type = type(key) if key_type ~= "string" and key_type ~= "number" and key_type ~= "boolean" then seen[value] = nil return nil, "nicht unterstützter Tabellen-Key" end local serialized_key, key_error = serialize_value(key, seen) if not serialized_key then seen[value] = nil return nil, key_error end local serialized_entry, entry_error = serialize_value(value[key], seen) if not serialized_entry then seen[value] = nil return nil, entry_error end parts[#parts + 1] = "[" .. serialized_key .. "]=" .. serialized_entry .. "," end parts[#parts + 1] = "}" seen[value] = nil return table.concat(parts) end local function validate_imported_value(value, seen) local value_type = type(value) if value_type == "nil" or value_type == "boolean" or value_type == "string" then return true elseif value_type == "number" then return is_finite_number(value) end if value_type ~= "table" then return false end seen = seen or {} if seen[value] then return false end seen[value] = true for key, entry in pairs(value) do local key_type = type(key) if key_type ~= "string" and key_type ~= "number" and key_type ~= "boolean" then return false end if key_type == "number" and not is_finite_number(key) then return false end if not validate_imported_value(entry, seen) then return false end end seen[value] = nil return true end function ns:initialize_database() samui_db = samui_db or {} samui_char_db = samui_char_db or {} profile_setup_required = type(samui_db.profiles) ~= "table" if profile_setup_required then samui_db.profiles = {} end ns.db = samui_db ns.char_db = samui_char_db end function ns:register_profile_provider(key, collect_fn, apply_fn) if type(key) ~= "string" or key == "" then return false end if type(collect_fn) ~= "function" or type(apply_fn) ~= "function" then return false end profile_providers[key] = { collect_fn = collect_fn, apply_fn = apply_fn, } return true end function ns:collect_profile_data() local data = {} for key, provider in pairs(profile_providers) do local provider_data = provider.collect_fn() if type(provider_data) == "table" then data[key] = provider_data end end return data end function ns:apply_profile_data(data) if type(data) ~= "table" then return false, "Profil-Daten fehlen" end for key, provider in pairs(profile_providers) do if data[key] ~= nil then provider.apply_fn(data[key]) end end return true end function ns:serialize_profile(data) if type(data) ~= "table" then return nil, "Profil-Daten fehlen" end local serialized, error_message = serialize_value(data, {}) if not serialized then return nil, error_message end return start_marker .. "return " .. serialized .. end_marker end function ns:deserialize_profile(str) if type(str) ~= "string" or str:sub(1, #start_marker) ~= start_marker or str:sub(-#end_marker) ~= end_marker then return nil, "ungültiges Format" end local source = str:sub(#start_marker + 1, #str - #end_marker) local loader = loadstring or load if type(loader) ~= "function" or type(setfenv) ~= "function" then return nil, "Import ist in diesem Client nicht verfügbar" end local chunk, load_error = loader(source, "SamUI-Profilimport") if not chunk then return nil, load_error or "ungültiges Format" end setfenv(chunk, {}) local success, data = pcall(chunk) if not success then return nil, tostring(data) end if type(data) ~= "table" or not validate_imported_value(data, {}) then return nil, "ungültige Profildaten" end return data end function ns:get_profile_names() local names = {} if type(samui_db) ~= "table" or type(samui_db.profiles) ~= "table" then return names end for name in pairs(samui_db.profiles) do if name ~= "SamUI-DD" and name ~= "SamUI-Heal" then names[#names + 1] = name end end table.sort(names, function(left, right) return string.lower(left) < string.lower(right) end) local sorted = {} if samui_db.profiles["SamUI-DD"] then sorted[#sorted + 1] = "SamUI-DD" end if samui_db.profiles["SamUI-Heal"] then sorted[#sorted + 1] = "SamUI-Heal" end for index = 1, #names do sorted[#sorted + 1] = names[index] end return sorted end function ns:is_locked_profile(name) return name == "SamUI-DD" or name == "SamUI-Heal" end function ns:validate_profile_name(name) if type(name) ~= "string" or name == "" or name:match("^%s*$") then return false, "Profilname '" .. tostring(name or "") .. "' darf nicht leer sein" end local length = #name if length < 2 or length > 32 then return false, "Profilname '" .. name .. "' muss 2 bis 32 Zeichen lang sein" end if not name:match("^[%w _%-]+$") then return false, "Profilname '" .. name .. "' enthält ungültige Zeichen" end local lower_name = string.lower(name) for existing_name in pairs(samui_db.profiles) do if string.lower(existing_name) == lower_name then return false, "Profilname '" .. name .. "' existiert bereits" end end return true end function ns:create_profile(name) local valid, error_message = self:validate_profile_name(name) if not valid then return false, error_message end samui_db.profiles[name] = self:collect_profile_data() ns:chat_message("Profil '" .. name .. "' erstellt") return true end function ns:copy_profile(source_name, new_name) local source = samui_db.profiles[source_name] if type(source) ~= "table" then return false, "Quellprofil '" .. source_name .. "' wurde nicht gefunden" end local valid, error_message = self:validate_profile_name(new_name) if not valid then return false, error_message end samui_db.profiles[new_name] = deep_copy(source) ns:chat_message("Profil '" .. source_name .. "' als '" .. new_name .. "' kopiert") return true end function ns:delete_profile(name) if self:is_locked_profile(name) then local error_message = "Basisprofil '" .. name .. "' kann nicht gelöscht werden" ns:chat_message(error_message, true) return false, error_message end if name == self:get_active_profile() then local error_message = "Aktives Profil '" .. name .. "' kann nicht gelöscht werden" ns:chat_message(error_message, true) return false, error_message end if not samui_db.profiles[name] then local error_message = "Profil '" .. name .. "' wurde nicht gefunden" ns:chat_message(error_message, true) return false, error_message end samui_db.profiles[name] = nil ns:chat_message("Profil '" .. name .. "' gelöscht") return true end function ns:load_profile(name) local data = samui_db.profiles[name] if type(data) ~= "table" then return false, "Profil '" .. name .. "' wurde nicht gefunden" end if type(default_profile_data) == "table" then merge_missing_values(data, default_profile_data) end remove_obsolete_profile_values(data) return self:apply_profile_data(data) end function ns:set_active_profile(name) if type(samui_db.profiles[name]) ~= "table" then return false, "Profil '" .. name .. "' wurde nicht gefunden" end samui_char_db.active_profile = name return self:load_profile(name) end function ns:get_active_profile() return samui_char_db and samui_char_db.active_profile or nil end function ns:get_active_profile_section(key) if type(key) ~= "string" or key == "" then return nil end local active_profile = self:get_active_profile() if type(active_profile) ~= "string" or type(samui_db) ~= "table" or type(samui_db.profiles) ~= "table" then return nil end local profile_data = samui_db.profiles[active_profile] if type(profile_data) ~= "table" then return nil end local section = profile_data[key] return type(section) == "table" and section or nil end function ns:export_profile(name) local data = samui_db.profiles[name] if type(data) ~= "table" then return nil, "Profil '" .. name .. "' wurde nicht gefunden" end local serialized, serialize_error = self:serialize_profile(data) if not serialized then return nil, serialize_error end return encode_export_string(serialized) end function ns:import_profile(str, new_name) local valid, error_message = self:validate_profile_name(new_name) if not valid then return false, error_message end local serialized = decode_export_string(str) if not serialized then return false, "Ungültiger Profil-String" end local data = self:deserialize_profile(serialized) if not data then return false, "Ungültiger Profil-String" end if type(default_profile_data) == "table" then merge_missing_values(data, default_profile_data) end remove_obsolete_profile_values(data) samui_db.profiles[new_name] = data ns:chat_message("Profil '" .. new_name .. "' importiert") return true end function ns:initialize_base_profiles() local provider_default_data = self:collect_profile_data() default_profile_data = deep_copy(hardcoded_default_profile_data) merge_missing_values(default_profile_data, provider_default_data) remove_obsolete_profile_values(default_profile_data) if profile_setup_required then samui_db.profiles["SamUI-DD"] = deep_copy(default_profile_data) samui_db.profiles["SamUI-Heal"] = deep_copy(default_profile_data) profile_setup_required = false end for _, profile_data in pairs(samui_db.profiles) do if type(profile_data) == "table" then merge_missing_values(profile_data, default_profile_data) remove_obsolete_profile_values(profile_data) end end return true end function ns:reset_base_profiles() if type(default_profile_data) ~= "table" then return false, "Lua-Defaults sind nicht verfügbar" end if type(samui_db) ~= "table" or type(samui_char_db) ~= "table" then return false, "Profildatenbank ist nicht verfügbar" end if type(samui_db.profiles) ~= "table" then samui_db.profiles = {} end samui_db.profiles["SamUI-DD"] = deep_copy(default_profile_data) samui_db.profiles["SamUI-Heal"] = deep_copy(default_profile_data) samui_char_db.active_profile = "SamUI-DD" return true end function module:initialize() end ns:register_initial_module("profiles")