Files
OEVG 896f46f0d1
enUS Localization Update Announcement / localization-update-announcement (push) Canceled after 0s
Bagshui 1.6.4
2026-10-30 15:55:43 +00:00

683 lines
26 KiB
Lua

-- Bagshui Character Attribute Tracking
-- Exposes: BsCharacter (and Bagshui.components.Character)
-- Raises:
-- - BAGSHUI_CHARACTER_LEARNED_RECIPE
-- BAGSHUI_CHARACTER_UPDATE
-- BAGSHUI_EQUIPPED_UPDATE
-- BAGSHUI_EQUIPPED_HISTORY_UPDATE
-- BAGSHUI_MONEY_UPDATE
-- BAGSHUI_PROFESSION_ITEM_UPDATE
--
-- Builds lists of skills, spells, and profession crafts and reagents.
-- Profession limitations:
-- - Item lists can only be constructed when the profession window is opened -
-- that's why we register for TRADE_SKILL_SHOW and CRAFT_SHOW.
-- - Enchanting uses the "Craft" API instead of TradeSkill, which means that the
-- items returned from GetCraftItemLink are actually spells. It appears that
-- a list of Enchanting crafts can't be autogenerated and a database would need
-- to be constructed.
Bagshui:AddComponent(function()
-- Events that will trigger `Character:OnEvent()`.
local CHARACTER_EVENTS = {
BAGSHUI_INITIAL_CHARACTER_UPDATE = true, -- Delayed processing at startup.
CHAT_MSG_SKILL = true, -- New skills learned or leveled up.
CHAT_MSG_SYSTEM = true, -- Messages to parse for important events.
CRAFT_SHOW = true, -- Enchanting profession window is opened.
PLAYER_ENTERING_WORLD = true, -- Trigger initial processing at startup.
PLAYER_LEVEL_UP = true, -- Level up.
SKILL_LINES_CHANGED = true, -- Need to update skills (needed to catch some weird skills like Fist Weapons).
SPELLS_CHANGED = true, -- Need to update spells.
TRADE_SKILL_SHOW = true, -- Profession window is opened (other than Enchanting).
UNIT_INVENTORY_CHANGED = true, -- Equipped gear has changed -- requires arg1 == "player" check.
}
-- Events that should trigger a money update.
-- Making a table for these because there are a bunch.
-- Using key-value so it's easy to test in `Character:OnEvent()`.
local MONEY_EVENTS = {
PLAYER_MONEY = true,
PLAYER_TRADE_MONEY = true,
SEND_MAIL_MONEY_CHANGED = true,
SEND_MAIL_COD_CHANGED = true,
TRADE_MONEY_CHANGED = true, -- Might not need this one but might as well do it just to be safe.
}
-- The comments below refer to `currentCharacterInfo`, which is a pointer to
-- `Bagshui.currentCharacterData[BS_CONFIG_KEY.CHARACTER_INFO]`.
-- Array of property names for `currentCharacterInfo` whose values are tables.
local CHARACTER_INFO_TABLES = {
"skills",
"spellNamesToIds",
"spells",
"professionCrafts",
"professionReagents",
"equippedHistory",
"equipped",
}
-- Array of important property names and dummy values for `currentCharacterInfo`.
-- See comment above the initial call to `Character:UpdateInfo()` at the end of
-- this file for more information.
local CHARACTER_INFO_MINIMUM_PROPERTIES = {
class = "",
faction = "",
name = "",
realm = "",
level = 1,
localizedClass = "",
localizedFaction = "",
localizedRace = "",
money = 0,
race = "",
}
-- The game only gives skill categories as localized strings, so this is a way
-- of making internal references locale-independent.
local LOCALIZED_TO_EN_SKILL_ID = {
[L["Professions"]] = "professions",
[L["Secondary Skills"]] = "secondarySkills",
[L["Weapon Skills"]] = "weaponSkills",
[L["Armor Proficiencies"]] = "armorProficiencies",
}
-- We have no use for these skills, so don't store them.
local IGNORE_SKILL_CATEGORY = {
[L["Class Skills"]] = true,
[L["Languages"]] = true,
}
-- `Bagshui.currentCharacterData` table is guaranteed to be initialized by
-- `Bagshui:AddonLoaded()` so we don't need to check it.
local currentCharacterInfo = Bagshui.currentCharacterData[BS_CONFIG_KEY.CHARACTER_INFO]
-- Ensure key tables exist in `Bagshui.currentCharacterData[BS_CONFIG_KEY.CHARACTER_INFO]`.
for _, tableName in ipairs(CHARACTER_INFO_TABLES) do
if not currentCharacterInfo[tableName] then
currentCharacterInfo[tableName] = {}
end
end
-- Ensure skill tables exist.
-- 1.12 doesn't give any sort of IDs for skill types, only localized names, but
-- we're dealing with that via `LOCALIZED_TO_EN_SKILL_ID`.
for _, skillName in pairs(LOCALIZED_TO_EN_SKILL_ID) do
if not currentCharacterInfo.skills[skillName] then
currentCharacterInfo.skills[skillName] = {}
end
end
-- Build Character class.
local Character = {
-- `Bagshui.currentCharacterData[BS_CONFIG_KEY.CHARACTER_INFO]`.
info = currentCharacterInfo,
-- List of all inventory slots, including bags, and their localized names.
---@type table<number,string>
inventorySlots = {},
-- Bag slot IDs so `Character:UpdateGear()` can differentiate.
---@type table<number,true>
bagSlots = {},
-- When character info is nil, we'll retry a few times.
startupRetries = 0,
initialized = false,
}
-- Add skills, spells, etc.
for _, tableName in ipairs(CHARACTER_INFO_TABLES) do
Character[tableName] = currentCharacterInfo[tableName]
end
-- Add professions, secondarySkills, etc.
for _, skillName in pairs(LOCALIZED_TO_EN_SKILL_ID) do
Character[skillName] = currentCharacterInfo.skills[skillName]
end
-- Add inventory slots.
for slot, localized in pairs(BS_INVENTORY_SLOT_NAMES) do
Character.inventorySlots[slot] = localized
end
for inventoryType, config in pairs(Bagshui.config.Inventory) do
for bagNum, slotName in ipairs(config.inventorySlots) do
Character.inventorySlots[slotName] = inventoryType .. "Container" .. bagNum
Character.bagSlots[slotName] = true
end
end
Bagshui.environment.BsCharacter = Character
Bagshui.components.Character = Character
--- Event processing.
---@param event string WoW API event
---@param arg1 any First event argument.
function Character:OnEvent(event, arg1)
--Bagshui:PrintDebug("Character event " .. event .. " // " .. tostring(arg1))
-- Initial processing at startup. The delayed event is necessary to ensure
-- we can actually get the data we need, which isn't available instantly.
-- See comment above the initial call to Character:UpdateInfo() at the end of
-- this file for more information.
if event == "PLAYER_ENTERING_WORLD" then
if not self.initialized then
Bagshui:QueueEvent("BAGSHUI_INITIAL_CHARACTER_UPDATE", 1.5)
end
return
end
-- Set Character things up at login.
if event == "BAGSHUI_INITIAL_CHARACTER_UPDATE" then
self:UpdateSkillsAndSpells()
self:UpdateGear()
self:UpdateMoney()
-- Need to set this true before calling UpdateInfo() so that it will
-- correctly raise BAGSHUI_CHARACTER_UPDATE.
self.initialized = true
self:UpdateInfo()
return
end
-- Some important events are only conveyed through chat messages that need
-- string matching to determine whether they're what we want.
if event == "CHAT_MSG_SYSTEM" then
-- This seems to be the best way to know when a character learns a new recipe.
-- Handling it centrally so other classes just have to register for BAGSHUI_CHARACTER_LEARNED_RECIPE.
if string.find(arg1, L.ChatMsgIdentifier_LearnedRecipe) then
BsItemInfo:InvalidateUsableCache()
Bagshui:RaiseEvent("BAGSHUI_CHARACTER_LEARNED_RECIPE")
end
return
end
-- Nothing else can happen until the class is initialized.
if not self.initialized then
return
end
-- Equipped gear changed ("inventory" in this case is what's equipped, not what's in bags).
-- [This used to also fire on UPDATE_INVENTORY_ALERTS, but that is about equipped gear *durability*.]
if event == "UPDATE_INVENTORY_ALERTS" then
-- This fires for all characters in a party/raid, and we only care about the player.
if arg1 == "player" then
--self:UpdateGear()
Bagshui:QueueClassCallback(self, self.UpdateGear, 1)
end
return
end
-- Refresh spells/skills and remove any items from the profession tables that shouldn't be there anymore.
-- This is also where the initial update after PLAYER_ENTERING_WORLD occurs.
if event == "SPELLS_CHANGED" or event == "CHAT_MSG_SKILL" or event == "SKILL_LINES_CHANGED" then
Bagshui:QueueClassCallback(self, self.UpdateSkillsAndSpells, 1)
return
end
-- Player level changed.
if event == "PLAYER_LEVEL_UP" then
-- self:UpdateInfo(arg1)
Bagshui:QueueClassCallback(self, self.UpdateInfo, 1, false, arg1)
return
end
-- Money changed.
if MONEY_EVENTS[event] then
self:UpdateMoney()
return
end
-- Do a profession update.
-- This can only happen when the appropriate window is opened!
if event == "CRAFT_SHOW" or event == "TRADE_SKILL_SHOW" then
-- Wait a smidgen before attempting the update so that game functions will return values.
Bagshui:QueueClassCallback(self, self.UpdateProfessionItems, 1, false, event)
return
end
end
-- Determine whether there are actual changes in `Character:UpdateInfo()`.
local _updateInfo_oldValues = {}
--- Refresh basic character information.
---@param newLevel number? `arg1` from the `PLAYER_LEVEL_UP` event, needed because `UnitLevel()` can be wrong just after gaining a level.
function Character:UpdateInfo(newLevel)
-- Record old values.
for key, value in pairs(self.info) do
_updateInfo_oldValues[key] = value
end
self.info.name = _G.UnitName("player")
self.info.realm = BsUtil.Trim(_G.GetCVar("realmName"))
self.info.localizedClass, self.info.class = _G.UnitClass("player")
self.info.faction, self.info.localizedFaction = _G.UnitFactionGroup("player")
self.info.localizedRace, self.info.race = _G.UnitRace("player")
-- Level should never be 0. UnitLevel() returns 0 for a short time after login,
-- and we want to ignore that. The processing of CHARACTER_INFO_MINIMUM_PROPERTIES
-- at the end of this file will fill it with 1 for a brief time at the very first login.
-- After that, it'll be cached in SavedVariables.
newLevel = newLevel or _G.UnitLevel("player")
if newLevel ~= 0 then
self.info.level = newLevel
end
-- Ensure the standard Class entry is uppercase, just to be safe. That's how the game
-- typically returns it, so there are other parts of Bagshui that assume it'll be in all caps.
self.info.class = string.upper(self.info.class)
-- Retry at startup (when no `newLevel` parameter is passed) if needed due to empty values.
if not newLevel and not self.faction and self.infoRetries < 5 then
self.infoRetries = self.infoRetries + 1
Bagshui:QueueEvent("BAGSHUI_INITIAL_CHARACTER_UPDATE", 1)
return
end
-- Raise an event if there were changes.
if self.initialized then
for key, value in pairs(self.info) do
if self.info[key] ~= _updateInfo_oldValues[key] then
Bagshui:RaiseEvent("BAGSHUI_CHARACTER_UPDATE")
BsItemInfo:InvalidateUsableCache()
break
end
end
end
end
--- Keep money current.
function Character:UpdateMoney()
local newMoney = _G.GetMoney()
if not newMoney then
return
end
if self.info.money ~= newMoney then
Bagshui:RaiseEvent("BAGSHUI_MONEY_UPDATE")
end
self.info.money = newMoney
end
-- Reusable tables for `UpdateSkillsAndSpells()`.
local update_tempSpells = {}
local update_tempSkills = {}
--- Refresh the list of known skills and spells.
function Character:UpdateSkillsAndSpells()
-- Backup copy to compare against so we know whether changes actually happened.
BsUtil.TableCopy(self.spells, update_tempSpells)
BsUtil.TableCopy(self.skills, update_tempSkills)
-- Rebuild the lists fresh each time.
BsUtil.TableClear(self.spells)
BsUtil.TableClear(self.spellNamesToIds)
for _, skillTypeList in pairs(self.skills) do
BsUtil.TableClear(skillTypeList)
end
-- To get the full list of spells, we have to just keep asking GetSpellName
-- for spells, starting at 1, until it stops answering.
local spellNum = 1
while true do
local spellName, spellRank = _G.GetSpellName(spellNum, _G.BOOKTYPE_SPELL)
if not spellName then
break
end
self.spells[spellName] = spellRank
self.spellNamesToIds[spellName] = spellNum
spellNum = spellNum + 1
end
-- Iterate skills and place them in the appropriate category.
local skillCategory
local skip -- Will be either nil or true depending on whether the skill is in `IGNORE_SKILL_CATEGORY`.
for skillIndex = 1, _G.GetNumSkillLines() do
local skillName, isHeader, _, skillRank = _G.GetSkillLineInfo(skillIndex)
if isHeader then
skip = IGNORE_SKILL_CATEGORY[skillName]
if not skip then
skillCategory = LOCALIZED_TO_EN_SKILL_ID[skillName] or skillName
if not self.skills[skillCategory] then
self.skills[skillCategory] = {}
end
end
elseif not skip and skillCategory and skillName then
self.skills[skillCategory][skillName] = skillRank
end
end
-- Cleanup.
self:PruneProfessionItems()
-- Is anything different?
if
self.initialized
and (
not BsUtil.ObjectsEqual(self.spells, update_tempSpells)
or not BsUtil.ObjectsEqual(self.skills, update_tempSkills)
)
then
Bagshui:RaiseEvent("BAGSHUI_CHARACTER_UPDATE")
BsItemInfo:InvalidateUsableCache()
end
-- Don't need to keep contents (but do keep the tables).
BsUtil.TableClear(update_tempSpells)
BsUtil.TableClear(update_tempSkills)
end
--- Refresh the list of equipped armor and weapons.
function Character:UpdateGear()
-- Bs:PrintDebug("Character:UpdateGear()")
local historyChanged = false
local equippedChanged = false
local itemString, prevItemString
for slot, localized in pairs(self.inventorySlots) do
-- Track whether there's been a change.
prevItemString = self.equipped[slot] and self.equipped[slot].itemString or ""
-- Get the current item in this slot.
itemString = BsItemInfo:ParseItemLink(_G.GetInventoryItemLink("player", _G.GetInventorySlotInfo(slot)))
-- Ensure the slot has an item storage table available.
if not self.equipped[slot] then
self.equipped[slot] = {}
end
if itemString then
-- Fill slot information.
if not BsItemInfo:Get(itemString, self.equipped[slot], true, true) then
Bagshui:PrintDebug("Character:UpdateGear() GetItemInfo failed")
end
-- Set `count` property to at least 1 so that the Catalog will know that this item should be added to the totals.
self.equipped[slot].count = math.max(self.equipped[slot].count, 1)
-- Add to history (skip bags).
if not self.equippedHistory[itemString] and not self.bagSlots[slot] then
self.equippedHistory[itemString] = localized
historyChanged = true
end
else
-- Wipe the slot.
BsItemInfo:InitializeItem(self.equipped[slot])
end
-- Check for changes.
if itemString ~= prevItemString then
equippedChanged = true
end
end
if equippedChanged then
Bagshui:RaiseEvent("BAGSHUI_EQUIPPED_UPDATE")
end
if historyChanged then
Bagshui:RaiseEvent("BAGSHUI_EQUIPPED_HISTORY_UPDATE")
end
end
--- Remove the given item from the list of worn gear.
---@param itemLink string Item to remove.
function Character:RemoveFromEquippedGear(itemLink)
if self.equippedHistory[itemLink] then
self.equippedHistory[itemLink] = nil
Bagshui:RaiseEvent("BAGSHUI_EQUIPPED_HISTORY_UPDATE")
end
end
-- Remove any profession crafts/reagents that are no longer known.
function Character:PruneProfessionItems()
if self:PruneItemList(self.professionCrafts) or self:PruneItemList(self.professionReagents) then
Bagshui:RaiseEvent("BAGSHUI_PROFESSION_ITEM_UPDATE")
BsItemInfo:InvalidateUsableCache()
end
end
-- Clean up the given list of crafts or reagents.
---@param list table Crafts or reagents table to process.
---@return boolean # true if any items were removed from the list.
function Character:PruneItemList(list)
local changesMade = false
for item, skills in pairs(list) do
-- Clear out any skills associated with this item that this character no longer has.
for skillName, _ in pairs(skills) do
if not self.professions[skillName] or self.secondarySkills[skillName] then
list[item][skillName] = nil
changesMade = true
end
end
-- If there are no skills left, remove the item.
if BsUtil.TrueTableSize(skills) == 0 then
list[item] = nil
changesMade = true
end
end
return changesMade
end
-- Perform a profession update.
function Character:UpdateProfessionItems(event)
local changesMade = false
-- Functions for professions other than Enchanting.
local getSkillLine = _G.GetTradeSkillLine
local getNumSkills = _G.GetNumTradeSkills
local getSkillItemLink = _G.GetTradeSkillItemLink
local getSkillNumReagents = _G.GetTradeSkillNumReagents
local getSkillReagentItemLink = _G.GetTradeSkillReagentItemLink
-- Enchanting has its own set of functions, and it's the only profession
-- referred to as a "Craft" instead of a "TradeSkill".
if event == "CRAFT_SHOW" then
getSkillLine = _G.GetCraftDisplaySkillLine
getNumSkills = _G.GetNumCrafts
getSkillItemLink = _G.GetCraftItemLink
getSkillNumReagents = _G.GetCraftNumReagents
getSkillReagentItemLink = _G.GetCraftReagentItemLink
end
local craftName, craftType, craftItemLink, reagentItemLink
-- Find out which profession window was opened.
local professionName = getSkillLine()
-- Hunter pet training window shows up as a craft with a nil professionName.
if professionName then
-- Process each available skill.
local numSkills = getNumSkills()
if numSkills > 0 then
for skillNum = 1, numSkills do
-- Get the name and type of item crafted by this skill (need type to make sure it's not a header).
craftName, craftType = self:GetSkillInfo(event, skillNum)
if craftName and craftType ~= "header" then
-- Get the information about what this skill creates and add to list of crafts.
craftItemLink = getSkillItemLink(skillNum)
if self:AddProfessionItemToList(professionName, craftItemLink, self.professionCrafts) then
changesMade = true
end
-- Process each reagent for this skill
local numReagents = getSkillNumReagents(skillNum)
if numReagents > 0 then
for reagentNum = 1, numReagents do
-- Add to list of reagents
reagentItemLink = getSkillReagentItemLink(skillNum, reagentNum)
if self:AddProfessionItemToList(professionName, reagentItemLink, self.professionReagents) then
changesMade = true
end
end
end
end
end
end
end
-- Notify of changes.
if changesMade then
Bagshui:RaiseEvent("BAGSHUI_PROFESSION_ITEM_UPDATE")
BsItemInfo:InvalidateUsableCache()
end
end
--- Add an item's ID to the given list.
---@param professionName string Name of the associated profession.
---@param itemLink string Link of item produced by the skill.
---@param list table<number, table> List to add the item to.
---@return boolean? # True if changes made to `list`, false if no changes made, or nil if the itemLink wasn't able to be processed.
function Character:AddProfessionItemToList(professionName, itemLink, list)
if type(itemLink) ~= "string" then
return
end
local _, itemId = BsItemInfo:ParseItemLink(itemLink)
-- itemLinks from skills aren't always actual items (for example, Enchants
-- return an "enchant:####" link instead of "item:####:####:####:####").
-- When ParseItemLink doesn't get a normal item: link, it will return nil.
if not itemId then
return
end
-- Initialize item table if needed and store relevant profession.
if not list[itemId] then
list[itemId] = {}
end
if list[itemId][professionName] == true then
-- Already linked to the profession.
return false
end
list[itemId][professionName] = true
-- A change was made.
return true
end
--- GetTradeSkillInfo and GetCraftInfo return slightly different values,
--- so we can't just assign their functions directly to variables like everything
--- else in UpdateProfessionItems(). This wrapper resolves the differences.
---@param event string WoW event that triggered this update.
---@param skillNum number Current skill line number.
---@return string craftName Name of the item.
---@return string craftType "header" or difficulty of crafting.
function Character:GetSkillInfo(event, skillNum)
local craftName, craftType
if event == "CRAFT_SHOW" then
craftName, _, craftType = _G.GetCraftInfo(skillNum)
else
craftName, craftType = _G.GetTradeSkillInfo(skillNum)
end
return craftName, craftType
end
--- Is the given item ID in the provided profession item list? Designed to be used from a rule function.
--- itemTable must be provided in order to allow testing against arbitrary characters, not just the current one
--- Example: BsCharacter:TestItem(rules.item.id, rules.character.professionCrafts, ruleArguments)
---@param itemId number Item ID to be checked.
---@param professionTable table Profession crafts or reagent table produced by this class.
---@param argumentList string[]? If present, array of one or more profession names that will be used to additionally filter the results.
---@return boolean # Whether the item was found.
function Character:TestItem(itemId, professionTable, argumentList)
if type(professionTable) ~= "table" then
return false
end
-- The item needs to be in the given item table.
if not professionTable[itemId] then
return false
end
-- No arguments -- we already know this item is in the table, so return true.
if not argumentList or type(argumentList) ~= "table" or (argumentList and table.getn(argumentList) == 0) then
return true
end
-- Arguments are a list of keys (professions) for the tables within professionTable -- does this item belong to one of them?
local allArgumentsEmptyStrings = true
for _, argument in ipairs(argumentList) do
argument = argument and BsUtil.Trim(tostring(argument)) or ""
if string.len(argument) > 0 then
allArgumentsEmptyStrings = false
if professionTable[itemId][argument] or professionTable[itemId][L[argument]] then
return true
end
end
end
-- We weren't actually given any keys to check, so this item matches.
if allArgumentsEmptyStrings then
return true
end
-- Default to false.
return false
end
--- Given a skill name, return the character's current level for that skill.
---@param skillName string Skill to search for.
---@param character table? Character to check.
---@return number skillLevel Skill level, or -1 if the character doesn't have the skill.
function Character:GetSkillLevel(skillName, character)
character = character or self.info
for _, skills in pairs(character.skills) do
for skill, level in pairs(skills) do
if skill == skillName then
return level
end
end
end
return -1
end
--- Build a `<name colored by class> [<colored faction indicator]` string.
---@param characterId string Unique character identifier.
---@param includeRealm boolean? Append the character's realm to the end.
---@return string?
function Character:FormatCharacterName(characterId, includeRealm)
local characterData = characterId and (Bagshui.characters[characterId] or Bagshui.sharedCharacters[characterId])
if not characterData then
return
end
local characterInfo = characterData.info
return
-- Class color.
(
type(characterInfo.class) == "string"
and (_G.RAID_CLASS_COLORS[string.upper(characterInfo.class)] and _G.RAID_CLASS_COLORS[string.upper(
characterInfo.class
)].colorStr and ("|c" .. _G.RAID_CLASS_COLORS[string.upper(characterInfo.class)].colorStr))
or NORMAL_FONT_COLOR_CODE
)
-- Name.
.. characterInfo.name
-- End class color.
.. FONT_COLOR_CODE_CLOSE
.. " "
-- Faction color.
.. (type(characterInfo.faction) == "string" and BS_FONT_COLOR["FACTION_" .. string.upper(characterInfo.faction)] or BS_FONT_COLOR.FACTION_UNKNOWN)
-- Faction letter.
.. "["
.. (string.len(characterInfo.faction or "") > 0 and string.sub(characterInfo.faction, 1, 1) or "?")
.. "]"
-- End faction color.
.. FONT_COLOR_CODE_CLOSE
-- Realm.
.. (includeRealm and GRAY_FONT_COLOR_CODE .. " • " .. characterInfo.realm .. FONT_COLOR_CODE_CLOSE or "")
end
-- Class event registration.
-- This is done at the end because RegisterEvent expects the class to have an OnEvent function.
for event, enabled in pairs(CHARACTER_EVENTS) do
Bagshui:RegisterEvent(event, Character)
end
-- Money money money.
for event, enabled in pairs(MONEY_EVENTS) do
if enabled then
Bagshui:RegisterEvent(event, Character)
end
end
-- Make a first pass at grabbing character information. Only some data is typically
-- available right away, with UnitFactionGroup() returning nil and UnitLevel()
-- returning 0 until shortly after PLAYER_ENTERING_WORLD, so we also make a fill-in
-- pass with dummy values. The second call to UpdateInfo that occurs shortly after
-- PLAYER_ENTERING_WORLD should rectify the missing properties.
-- One important reason to do this is to avoid errors during initial rule validation,
-- which gets upset if BsCharacter.info.localizedClass is nil.
Character:UpdateInfo()
for property, initialValue in pairs(CHARACTER_INFO_MINIMUM_PROPERTIES) do
if not currentCharacterInfo[property] then
currentCharacterInfo[property] = initialValue
end
end
end)