-- Init legacy global variables for other mods to hand over Descriptions if not __eidItemDescriptions then __eidItemDescriptions = {} end setmetatable(__eidItemDescriptions, { __newindex = function(_, k, v) EID:addCollectible(k, v) end }) if not __eidTrinketDescriptions then __eidTrinketDescriptions = {} end setmetatable(__eidTrinketDescriptions, { __newindex = function(_, k, v) EID:addTrinket(k, v) end }) if not __eidCardDescriptions then __eidCardDescriptions = {} end setmetatable(__eidCardDescriptions, { __newindex = function(_, k, v) EID:addCard(k, v) end }) if not __eidPillDescriptions then __eidPillDescriptions = {} end setmetatable(__eidPillDescriptions, { __newindex = function(_, k, v) EID:addPill(k, v) end }) if not __eidItemTransformations then __eidItemTransformations = {} end setmetatable(__eidItemTransformations, { __newindex = function(_, k, v) EID:assignTransformation("collectible", k, v) end }) if not __eidEntityDescriptions then __eidEntityDescriptions = {} end setmetatable(__eidEntityDescriptions, { __newindex = function(_, k, v) local entityID = {} for id in string.gmatch(k, "([^.]+)") do table.insert(entityID, id) end EID:addEntity(entityID[1], entityID[2], entityID[3], v[1], v[2]) end }) --------------------------------------------------------------------------- -------------------------Handle API Functions ----------------------------- local nullVector = Vector(0,0) local game = Game() local maxCardID = Card.NUM_CARDS - 1 local maxPillID = PillColor.NUM_PILLS - 1 local dynamicSpriteCache = {} -- used to store sprite objects of collectible icons etc. -- Adds a description for a collectible. Optional parameters: itemName, language function EID:addCollectible(id, description, itemName, language) itemName = itemName or nil language = language or "en_us" local modName = EID._currentMod -- Glitched Items exception so they don't have a mod name if id > 4294960000 then modName = nil end EID:CreateDescriptionTableIfMissing("custom", language) EID.descriptions[language].custom["5.100." .. id] = {id, itemName, description, modName} end -- Adds a description for a trinket. Optional parameters: itemName, language function EID:addTrinket(id, description, itemName, language) itemName = itemName or nil language = language or "en_us" EID:CreateDescriptionTableIfMissing("custom", language) EID.descriptions[language].custom["5.350." .. id] = {id, itemName, description, EID._currentMod} end -- Adds character specific information, which can be viewed in the Item Reminder function EID:addCharacterInfo(characterId, description, playerName, language) playerName = playerName or "Modded Character" language = language or "en_us" EID:CreateDescriptionTableIfMissing("CharacterInfo", language) EID.descriptions[language].CharacterInfo[characterId] = {playerName, description} end -- Adds information about appending text and multiplying numbers in a modded trinket's Golden/Mom's Box description. All three variables are optional, set to ""/0 or nil to not include them -- appendText: Text to be appended onto the description. Can be one string, or a table of two strings; one for doubling and one for tripling -- numbersToMultiply: The number inside the text that should be multiplied. can be one number, or a table of numbers -- maxMultiplier: is what tripling (Golden+Mom's Box) should multiply the numbers by (normally 3) -- (If it's less than 2, it also applies to doubling) -- Example: My modded trinket gives +0.5 range and when tripled, adds homing instead of tripling the range boost -- EID:addGoldenTrinketMetadata(Isaac.GetTrinketIdByName("Cool Trinket"), {"", "Homing tears"}, 0.5, 2) function EID:addGoldenTrinketMetadata(id, appendText, numbersToMultiply, maxMultiplier, language) maxMultiplier = maxMultiplier or 3 language = language or "en_us" if appendText == "" then appendText = nil elseif type(appendText) == "string" then appendText = {appendText} end if numbersToMultiply == 0 then numbersToMultiply = nil elseif type(numbersToMultiply) == "number" then numbersToMultiply = {numbersToMultiply} end EID.GoldenTrinketData[id] = {t = numbersToMultiply, mult = maxMultiplier, append = appendText and true} if appendText then EID:CreateDescriptionTableIfMissing("goldenTrinketEffects", language) EID.descriptions[language].goldenTrinketEffects[id] = { appendText[1], appendText[1], appendText[2] or appendText[1] } end end -- Add a fully custom data table to the table of Golden Trinket effects -- Check the comments above EID.GoldenTrinketData in eid_data.lua for some info about what is possible -- You may also want to add text entries into EID.descriptions[languageCode].goldenTrinketEffects function EID:addGoldenTrinketTable(id, dataTable) EID.GoldenTrinketData[id] = dataTable end -- Adds a description for a card/rune. Optional parameters: itemName, language function EID:addCard(id, description, itemName, language) itemName = itemName or nil language = language or "en_us" EID:CreateDescriptionTableIfMissing("custom", language) EID.descriptions[language].custom["5.300." .. id] = {id, itemName, description, EID._currentMod} end -- Adds a metadata for a card. Used for Blank Card/Clear Rune. Optional parameters: isRune -- Avalilable values for mimicCharge : 1~12 function EID:addCardMetadata(id, mimicCharge, isRune) if isRune then EID.blankCardHidden[id] = true EID.runeIDs[id] = true end EID.cardMetadata[id] = { mimiccharge = type(mimicCharge) == "number" and mimicCharge or -1 } end -- Adds a description for a pilleffect id. Optional parameters: itemName, language function EID:addPill(id, description, itemName, language) itemName = itemName or nil language = language or "en_us" EID:CreateDescriptionTableIfMissing("pills", language) EID:CreateDescriptionTableIfMissing("horsepills", language) EID.descriptions[language].pills[id+1] = {id, itemName, description, EID._currentMod} -- Default the horsepill to the same description as the base pill, for mods that only define the base pill if EID.isRepentance and EID.descriptions[language].horsepills[id+1] == nil then EID.descriptions[language].horsepills[id+1] = {id, itemName, description, EID._currentMod} end end function EID:addHorsePill(id, description, itemName, language) if not EID.isRepentance then return end itemName = itemName or nil language = language or "en_us" EID:CreateDescriptionTableIfMissing("horsepills", language) EID.descriptions[language].horsepills[id+1] = {id, itemName, description, EID._currentMod} end -- Adds a metadata for a pilleffect. Used for Placebo/False PHD. Optional parameters: class -- Avalilable values for mimicCharge : 1~12 -- For class value, "3-" ~ "3+" are available, although False PHD only cares for negative values. -- "3-" : Major negative effect - Gives +0.6 Damage / "2-", "1-" : Minor negative effect - Spawns a Black Heart function EID:addPillMetadata(id, mimicCharge, class) EID.pillMetadata[id] = { mimiccharge = type(mimicCharge) == "number" and mimicCharge or -1, class = class or "0", } end -- Adds a character specific description for the item "Birthright". Optional parameters: playerName, language function EID:addBirthright(characterId, description, playerName, language) playerName = playerName or nil language = language or "en_us" EID:CreateDescriptionTableIfMissing("birthright", language) EID.descriptions[language].birthright[characterId + 1] = {playerName, "", description} end -- Creates a new transformation with a given unique name and a display name function EID:createTransformation(uniqueName, displayName, language) language = language or "en_us" if EID.CustomTransformations[uniqueName] == nil then EID.CustomTransformations[uniqueName] = {} end EID.CustomTransformations[uniqueName][language] = displayName end -- Assigns transformations to an entity (Adds to existing transformations) -- valid target types: [collectible, trinket, card, pill, entity] -- when type = entity, targetIdentifier must be in the format "ID.Variant.subtype". for any other type, it can just be the id -- EXAMPLE: EID:assignTransformation("collectible", 1, "My Transformation") function EID:assignTransformation(targetType, targetIdentifier, transformationString) local entryID = EID:getIDVariantString(targetType) if entryID ~= nil then entryID = entryID.."."..targetIdentifier else entryID = targetIdentifier end EID:removeEntryFromString(EID.CustomTransformAssignments, entryID, transformationString) if EID.CustomTransformAssignments[entryID] == nil then EID.CustomTransformAssignments[entryID] = transformationString else EID.CustomTransformAssignments[entryID] = EID.CustomTransformAssignments[entryID]..","..transformationString end EID:removeEntryFromString(EID.CustomTransformRemovals, entryID, transformationString) end -- Try to automatically assign vanilla transformations to the entity function EID:tryAutodetectTransformationsCollectible(collectibleID) if not EID.isRepentance then return end local config = EID.itemConfig:GetCollectible(collectibleID) local transformations = {} transformations[EID.TRANSFORMATION.ANGEL] = config:HasTags(ItemConfig.TAG_ANGEL) or nil transformations[EID.TRANSFORMATION.BOB] = config:HasTags(ItemConfig.TAG_BOB) or nil transformations[EID.TRANSFORMATION.BOOKWORM] = config:HasTags(ItemConfig.TAG_BOOK) or nil transformations[EID.TRANSFORMATION.CONJOINED] = config:HasTags(ItemConfig.TAG_BABY) or nil transformations[EID.TRANSFORMATION.GUPPY] = config:HasTags(ItemConfig.TAG_GUPPY) or nil transformations[EID.TRANSFORMATION.LEVIATHAN] = config:HasTags(ItemConfig.TAG_DEVIL) or nil transformations[EID.TRANSFORMATION.LORD_OF_THE_FLIES] = config:HasTags(ItemConfig.TAG_FLY) or nil transformations[EID.TRANSFORMATION.MOM] = config:HasTags(ItemConfig.TAG_MOM) or nil transformations[EID.TRANSFORMATION.MUSHROOM] = config:HasTags(ItemConfig.TAG_MUSHROOM) or nil transformations[EID.TRANSFORMATION.POOP] = config:HasTags(ItemConfig.TAG_POOP) or nil transformations[EID.TRANSFORMATION.SPIDERBABY] = config:HasTags(ItemConfig.TAG_SPIDER) or nil transformations[EID.TRANSFORMATION.SPUN] = config:HasTags(ItemConfig.TAG_SYRINGE) or nil -- these dont have a tag : ADULT, STOMPY, SUPERBUM for k, _ in pairs(transformations) do EID:assignTransformation("collectible", collectibleID, k) end end -- Removes a transformation of an entity -- valid target types: [collectible, trinket, card, pill, entity] -- when type = entity, targetIdentifier must be in the format "ID.Variant.subtype". for any other type, it can just be the id -- EXAMPLE: EID:removeTransformation("collectible", 1, "My Transformation") function EID:removeTransformation(targetType, targetIdentifier, transformationString) local entryID = EID:getIDVariantString(targetType) if entryID ~= nil then entryID = entryID.."."..targetIdentifier else entryID = targetIdentifier end EID:removeEntryFromString(EID.CustomTransformRemovals, entryID, transformationString) if EID.CustomTransformRemovals[entryID] == nil then EID.CustomTransformRemovals[entryID] = transformationString else EID.CustomTransformRemovals[entryID] = EID.CustomTransformRemovals[entryID]..","..transformationString end EID:removeEntryFromString(EID.CustomTransformAssignments, entryID, transformationString) end -- Removes a given value from the string inside a table. Example: "1,2,3", removing 2 will return "1,3" function EID:removeEntryFromString(sourceTable, entryKey, entryValue) if sourceTable[entryKey] == nil then return end local newEntry = "" for str in string.gmatch(sourceTable[entryKey], "([^,]+)") do local addToList = true for removeStr in string.gmatch(entryValue, "([^,]+)") do if removeStr == str then addToList = false end end if addToList then newEntry = newEntry..","..str end end newEntry = newEntry:sub(2) if newEntry == "" then newEntry = nil end sourceTable[entryKey] = newEntry end -- Adds a description for a an Entity. Optional parameters: language, transformations -- when subtype is -1 or empty, it will affect all subtypes of that entity function EID:addEntity(id, variant, subtype, entityName, description, language) subtype = subtype or nil language = language or "en_us" if id == EntityType.ENTITY_EFFECT then EID.effectList[variant] = true end EID:CreateDescriptionTableIfMissing("custom", language) EID.descriptions[language].custom[id .. "." .. variant .. "." .. subtype] = { subtype, entityName, description } end -- Adds a new icon object with the shortcut defined in the "shortcut" variable (e.g. "{{shortcut}}" = your icon) -- Shortcuts are case Sensitive! Shortcuts can be overriden with this function to allow for full control over everything -- Setting "animationFrame" to -1 will play the animation. The spriteObject needs to be of class Sprite() and have an .anm2 loaded -- default values: leftOffset= -1 , topOffset = 0 function EID:addIcon(shortcut, animationName, animationFrame, width, height, leftOffset, topOffset, spriteObject) leftOffset = leftOffset or -1 topOffset = topOffset or 0 EID.InlineIcons[shortcut] = {animationName, animationFrame, width, height, leftOffset, topOffset, spriteObject} end -- Adds a custom poop spell to T???'s poop descriptions. This spell should be added with the "Custom Poop API" library to actually appear in-game. -- Token is the name of the spell in the Custom Poop API code. Examples: "CORNY", "BURNING", "BOMB". -- Name is the actual name you want to display. Examples: "Corny Poop", "Burning Poop", "Bomb". -- Icon is the displayed poop icon. Should be a markup. -- Description is the actual description showed to the player. Examples: "Spawns blue flies while intact", "Deals contact damage while intact#Leaves a fire behind when destroyed", "Normal throwable bomb". -- Language is the language you want to add it to. Examples: "en_us", "spa", "ru". -- EXAMPLE: EID:addCustomPoopSpell("MYPOOP", "I Made This Poop", "{{PoopSpell1}}", "Can be throwed to deal damage", "en_us") function EID:addCustomPoopSpell(token, name, icon, description, language) EID.descriptions[language]["poopSpells"][token] = {icon, name, description, EID._currentMod} end -- Adds a new color object with the shortcut defined in the "shortcut" variable (e.g. "{{shortcut}}" = your color) -- Shortcuts are case Sensitive! Shortcuts can be overriden with this function to allow for full control over everything -- Define a callback to let it be called when interpreting the color-markup. define a kColor otherwise for a simple color change function EID:addColor(shortcut, kColor, callback) if callback ~= nil then EID.InlineColors[shortcut] = callback else EID.InlineColors[shortcut] = kColor end end -- Overrides all potentially displayed texts and permanently displays the given texts. Can be turned of again using the "EID:hidePermanentText()" function function EID:displayPermanentText(descriptionObject, permName1, permName2) descriptionObject.PermanentTextEnglish = EID:getDescriptionEntryEnglish(permName1, permName2) EID.permanentDisplayTextObj = descriptionObject EID.isDisplayingPermanent = true end -- Hides permanently displayed text objects if they exist. function EID:hidePermanentText() EID.permanentDisplayTextObj = nil EID.isDisplayingPermanent = false end -- function to turn entity type names into actual ingame ID.Variant pairs function EID:getIDVariantString(typeName) if typeName == "collectible" or typeName == "collectibles" then return "5.100" elseif typeName == "trinket" or typeName == "trinkets" then return "5.350" elseif typeName == "card" or typeName == "cards" then return "5.300" elseif typeName == "pill" or typeName == "pills" or typeName == "horsepills" or typeName == "horsepill" then return "5.70" elseif typeName == "sacrifice" then return "-999.-1" elseif typeName == "dice" then return "1000.76" end return nil end -- function to turn entity typ and variants into their EID table-name function EID:getTableName(Type, Variant, SubType) local idString = Type.."."..Variant if idString == "5.100" then return "collectibles" elseif idString == "5.350" then return "trinkets" elseif idString == "5.300" then return "cards" elseif idString == "5.70" then if SubType <2049 then return "pills" else return "horsepills" end elseif idString == "-999.-1" then return "sacrifice" elseif idString == "1000.76" then return "dice" elseif idString == "1.0" then return "players" else return "custom" end end -- Loads a given font from a given file path and use it to render text function EID:loadFont(fontFileName) EID.font:Load(fontFileName, "") -- GoG Version of game somehow wants a string as the second argument EID.font:SetMissingCharacter(2) if not EID.font:IsLoaded() then Isaac.DebugString("EID - ERROR: Could not load font from '" .. EID.modPath .. "resources/font/default.fnt" .. "'") return false end return true end -- Returns if EID is displaying text right now function EID:isDisplayingText() return EID.isDisplaying end -- Returns true, if curse of blind is active function EID:hasCurseBlind() return game:GetLevel():GetCurses() & LevelCurse.CURSE_OF_BLIND > 0 end -- returns the current text position function EID:getTextPosition() local posVector = Vector(EID.UsedPosition.X, EID.UsedPosition.Y) -- Only apply position modifiers when not in Local Mode if EID.CurrentScaleType == "Size" then for _, modifier in pairs(EID.PositionModifiers) do posVector = posVector + modifier end end return posVector end -- Adds a text position modifier Vector, which will be applied to the text position variable -- Useful to add small offsets. For example: for schoolbag HUD function EID:addTextPosModifier(identifier, modifierVector) EID.PositionModifiers[identifier] = modifierVector end -- Removes a text position modifier Vector -- Useful to remove small offsets. For example: for schoolbag HUD function EID:removeTextPosModifier(identifier) EID.PositionModifiers[identifier] = nil end -- Changes the initial position of all eid descriptions -- Useful to totally alter and override the current initial Overlay position function EID:alterTextPos(newPosVector) EID.UsedPosition = newPosVector end -- returns the entity that is currently described. returns last described entity if currently not displaying text function EID:getLastDescribedEntity() return EID.lastDescriptionEntity end -- Appends a given string to the description of a given Description object function EID:appendToDescription(descObj, appendString) descObj.Description = descObj.Description..appendString end -- returns the description object of a specific entity -- falls back to english if the objID isnt available function EID:getDescriptionObjByEntity(entity) return EID:getDescriptionObj(entity.Type, entity.Variant, entity.SubType, entity) end -- returns the description object of the specified entity -- falls back to english if the objID isnt available -- entity is optional function EID:getDescriptionObj(Type, Variant, SubType, entity, checkModifiers) local description = {} description.ObjType = Type description.ObjVariant = Variant description.ObjSubType = SubType description.fullItemString = Type.."."..Variant.."."..SubType description.Name = EID:getObjectName(Type, Variant, SubType) description.Entity = entity or nil local tableEntry = EID:getDescriptionData(Type, Variant, SubType) description.Description = tableEntry and tableEntry[3] or EID:getXMLDescription(Type, Variant, SubType) description.Transformation = EID:getTransformation(Type, Variant, SubType) description.ModName = tableEntry and tableEntry[4] description.Quality = EID:getObjectQuality(description) description.Icon = EID:getObjectIcon(description) EID:getObjectItemTypeAndCharge(description) EID.DifferentEffectPlayers = {} if checkModifiers ~= false then description = EID:applyConditionals(description) description = EID:applyDescriptionModifier(description, SubType) end return description end -- returns description Object from the legacy mod descriptions if they exist function EID:getLegacyModDescription(Type, Variant, SubType) local tableName = EID:getTableName(Type, Variant, SubType) local customDesc = __eidEntityDescriptions[Type.."."..Variant.."."..SubType] if tableName == "collectibles" and __eidItemDescriptions[SubType] then return {"","",__eidItemDescriptions[SubType]} elseif tableName == "trinkets" and __eidTrinketDescriptions[SubType] then return{"","", __eidTrinketDescriptions[SubType]} elseif tableName == "cards" and __eidCardDescriptions[SubType] then return {"","",__eidCardDescriptions[SubType]} elseif tableName == "pills" and __eidPillDescriptions[SubType-1] then return {"","",__eidPillDescriptions[SubType-1]} elseif customDesc~=nil then return {"", customDesc[1], customDesc[2]} end return nil end -- apply Description Modifier to a given description object function EID:applyDescriptionModifier(description, SubType) for _,modifier in ipairs(EID.DescModifiers) do local result = modifier.condition(description) if type(result) == "table" then for _,callback in ipairs(result) do -- If the modifier loads a different description obj (which also goes through the modifier checks), we should stop our checks so text doesn't get printed twice if description.ObjSubType ~= SubType then break end description = callback(description) end elseif result then description = modifier.callback(description) end end return description end -- Returns the icon and mod name of a given EID description object as a preformatted description string function EID:getModNameString(descObj) local modString = "" if EID.Config["ModIndicatorDisplay"] == "Both" or EID.Config["ModIndicatorDisplay"] == "Name only" then local modName = EID.ModIndicator[descObj.ModName] and EID.ModIndicator[descObj.ModName].Name or descObj.ModName modString = modString .. " {{"..EID.Config["ModIndicatorTextColor"].."}}" .. modName end local modIcon = EID.ModIndicator[descObj.ModName] and EID.ModIndicator[descObj.ModName].Icon if (EID.Config["ModIndicatorDisplay"] == "Both" or EID.Config["ModIndicatorDisplay"] == "Icon only") and modIcon then modString = modString .. "{{".. modIcon .."}}" end return modString end -- Attempts to merge two given Description objects into one function EID:mergeDescriptionObjects(oldDescObj, newDescObj) for k,v in pairs(oldDescObj) do if not newDescObj[k] then newDescObj[k] = v end end return newDescObj end function EID:getObjectItemTypeAndCharge(descObj) if not (descObj.ObjType == 5 and descObj.ObjVariant == 100 and descObj.ObjSubType ~= nil) then return end local itemConfig = EID.itemConfig:GetCollectible(descObj.ObjSubType) if not itemConfig then return end descObj.ItemType = itemConfig.Type or -1 if descObj.ItemType == ItemType.ITEM_ACTIVE then descObj.ChargeType = itemConfig.ChargeType or 0 descObj.Charges = itemConfig.MaxCharges or 0 -- Special handling for dynamic charge items if EID.isRepentance and (descObj.ObjSubType == CollectibleType.COLLECTIBLE_BLANK_CARD or descObj.ObjSubType == CollectibleType.COLLECTIBLE_PLACEBO or descObj.ObjSubType == CollectibleType.COLLECTIBLE_CLEAR_RUNE or descObj.ObjSubType == CollectibleType.COLLECTIBLE_D_INFINITY) then descObj.ChargeType = ItemConfig.CHARGE_SPECIAL end end end -- returns the specified object table in the current language. -- falls back to english if it doesnt exist, unless specified otherwise function EID:getDescriptionEntry(objTable, objID, noFallback) if not objID then if noFallback then return EID.descriptions[EID:getLanguage()][objTable] else return EID.descriptions[EID:getLanguage()][objTable] or EID.descriptions["en_us"][objTable] end else local translatedTable = EID.descriptions[EID:getLanguage()][objTable] if noFallback then return translatedTable and translatedTable[objID] else return (translatedTable and translatedTable[objID]) or (EID.descriptions["en_us"][objTable] and EID.descriptions["en_us"][objTable][objID]) end end end function EID:getDescriptionEntryEnglish(objTable, objID) if not objID then return EID.descriptions["en_us"][objTable] else return EID.descriptions["en_us"][objTable] and EID.descriptions["en_us"][objTable][objID] end end -- returns the description data table related to a given id, variant and subtype -- falls back to english if it doesnt exist function EID:getDescriptionData(Type, Variant, SubType) local fullString = Type.."."..Variant local adjustedID = EID:getAdjustedSubtype(Type, Variant, SubType) local moddedDesc = EID:getDescriptionEntry("custom", fullString.."."..adjustedID) local tableName = EID:getTableName(Type, Variant, SubType) local legacyModdedDescription = EID:getLegacyModDescription(Type, Variant, adjustedID) local defaultDesc = EID:getDescriptionEntry(tableName, adjustedID) return moddedDesc or legacyModdedDescription or defaultDesc end -- Returns an adjusted SubType id for special cases like Horse Pills and Golden Trinkets function EID:getAdjustedSubtype(Type, Variant, SubType) local tableName = EID:getTableName(Type, Variant, SubType) if tableName == "trinkets" then if EID.isRepentance then return (SubType & TrinketType.TRINKET_ID_MASK) end elseif tableName == "sacrifice" then return math.min(#EID.descriptions["en_us"].sacrifice, SubType) elseif tableName == "pills" or tableName == "horsepills" then -- The effect of a pill varies depending on what player is looking at it in co-op -- EID.pillPlayer is a way to recheck a pill for what different players will turn it into local player = EID.pillPlayer or EID.player if EID.isRepentance and SubType % PillColor.PILL_GIANT_FLAG == PillColor.PILL_GOLD then return 9999 end local pool = game:GetItemPool() if EID.isRepentance and player:GetPlayerType() == PlayerType.PLAYER_THESOUL_B then SubType = pool:GetPillEffect(SubType, player:GetOtherTwin() or player) + 1 else SubType = pool:GetPillEffect(SubType, player) + 1 end end return SubType end -- Get the transformation uniqueName / ID of a given entity -- Example: EID:getTransformation(5,100,34) will return "12" which is the id for Bookworm function EID:getTransformation(id, variant, subType) local adjustedSubtype = EID:getAdjustedSubtype(id, variant, subType) local entityString = id.."."..variant.."."..adjustedSubtype local listToTest = "" local default = EID.EntityTransformations[entityString] if default~= nil then listToTest = default end if id == 5 and variant == 100 then local customLegacy = __eidItemTransformations[adjustedSubtype] if customLegacy~= nil then listToTest = listToTest..","..customLegacy end end local custom = EID.CustomTransformAssignments[entityString] if custom~= nil then listToTest = listToTest..","..custom end local transformationList = "" local removedList = EID.CustomTransformRemovals[entityString] for transform in string.gmatch(listToTest, "([^,]+)") do local isRemoved = false if removedList ~= nil then for removedTransform in string.gmatch(removedList, "([^,]+)") do if transform == removedTransform then isRemoved = true end end end if not isRemoved then transformationList = transformationList..","..transform end end return transformationList end --Get the name of the given transformation by its uniqueName / ID function EID:getTransformationName(id) local str = "Custom" if tonumber(id) == nil then -- get translated custom name local customTransform = EID.CustomTransformations[id] if customTransform ~= nil then return customTransform[EID:getLanguage()] or customTransform["en_us"] or id end return id end return EID:getDescriptionEntry("transformations")[tonumber(id) + 1] or str end -- tries to get the ingame name of an item based on its ID function EID:getObjectName(Type, Variant, SubType) local tableName = EID:getTableName(Type, Variant, SubType) local tableEntry = EID:getDescriptionData(Type, Variant, SubType) local name = nil if tableEntry ~= nil then if tableEntry[2] ~= nil and tableEntry[2] ~= "" and tableEntry[2] ~= EID.descriptions["en_us"][tableName][SubType] then name = tableEntry[2] end end if tableName == "collectibles" then if EID.itemConfig:GetCollectible(SubType) == nil then return Type.."."..Variant.."."..SubType end local vanillaName = EID.itemConfig:GetCollectible(SubType).Name return name or (not string.find(vanillaName, "^#") and vanillaName) or (EID.descriptions["en_us"][tableName][SubType] and EID.descriptions["en_us"][tableName][SubType][2]) or vanillaName elseif tableName == "trinkets" then local adjustedSubtype = EID:getAdjustedSubtype(Type, Variant, SubType) local vanillaName = EID.itemConfig:GetTrinket(adjustedSubtype).Name return name or (not string.find(vanillaName, "^#") and vanillaName) or EID.descriptions["en_us"][tableName][adjustedSubtype][2] or vanillaName elseif tableName == "cards" then local vanillaName = EID.itemConfig:GetCard(SubType).Name return name or (not string.find(vanillaName, "^#") and vanillaName) or EID.descriptions["en_us"][tableName][SubType][2] or vanillaName elseif tableName == "pills" or tableName == "horsepills" then local adjustedSubtype = EID:getAdjustedSubtype(Type, Variant, SubType) return EID:getPillName(adjustedSubtype, tableName == "horsepills") elseif tableName == "sacrifice" then return EID:getDescriptionEntry("sacrificeHeader").." ("..SubType.."/"..#EID.descriptions["en_us"].sacrifice..")" elseif tableName == "dice" then return EID:getDescriptionEntry("diceHeader").." ("..SubType..")" elseif tableName == "players" then return EID:getPlayerName(SubType) elseif tableName == "custom" then local xmlName = EID:GetEntityXMLName(Type, Variant, SubType) return name or xmlName or Type.."."..Variant.."."..SubType end return Type.."."..Variant.."."..SubType end function EID:getPlayerName(id, altFallback) local playerInfo = EID:getDescriptionEntry("CharacterInfo")[id] local birthrightInfo = EID.isRepentance and EID:getDescriptionEntry("birthright")[id+1] return (playerInfo and playerInfo[1]) or (birthrightInfo and birthrightInfo[1]) or altFallback or EID:findPlayerName(id) or "???" end -- Get the name of a given player ID by checking for a matching EntityPlayer -- This is for modded characters, whose name is best found by doing EntityPlayer:GetName() function EID:findPlayerName(id) local found, entityPlayer = EID:PlayersHaveCharacter(id, false) if entityPlayer then return entityPlayer:GetName() end end -- returns the name of a pill based on the pilleffect id function EID:getPillName(pillID, isHorsepill) local moddedDesc = EID:getDescriptionEntry("custom", "5.70."..pillID) local legacyModdedDescription = EID:getLegacyModDescription(5, 70, pillID) local tableName = isHorsepill and "horsepills" or "pills" local defaultDesc = EID:getDescriptionEntry(tableName, pillID) local name = moddedDesc or legacyModdedDescription or defaultDesc local vanillaName = "" if pillID == 9999 then vanillaName = "Golden Pill" -- only used for languages that haven't defined a Golden Pill name else vanillaName = EID.itemConfig:GetPillEffect(pillID - 1).Name end name = name and name[2] or (not string.find(vanillaName, "^#") and vanillaName) or EID.descriptions["en_us"][tableName][pillID][2] or vanillaName return string.gsub(name,"I'm Excited!!!","I'm Excited!!") -- prevent markup trigger end -- tries to get the ingame description of an object, based on their description in the XML files function EID:getXMLDescription(Type, Variant, SubType) local tableName = EID:getTableName(Type, Variant, SubType) local desc= nil if SubType == 0 then return "(no description available)" end if tableName == "collectibles" then desc = EID.itemConfig:GetCollectible(SubType).Description elseif tableName == "trinkets" then desc = EID.itemConfig:GetTrinket(SubType).Description elseif tableName == "cards" then desc = EID.itemConfig:GetCard(SubType).Description end return desc or "(no description available)" end -- check if an entity is part of the describable entities ---@diagnostic disable-next-line: duplicate-set-field function EID:hasDescription(entity) if not EID:EntitySanityCheck(entity) then return false end if EID:IsGridEntity(entity) then if EID.GridEntityWhitelist[entity:GetType()] then for _, func in ipairs(EID.GridEntityWhitelist[entity:GetType()]) do if func(entity) then return true end end end return false end local isAllowed = false local entityString = entity.Type .. "." .. entity.Variant .. "." .. entity.SubType if EID.IgnoredEntities[entity.Type .. "." .. entity.Variant] or EID.IgnoredEntities[entityString] then return false end if EID.Config["EnableEntityDescriptions"] and EID:getTableName(entity.Type, entity.Variant, entity.SubType) == "custom" then isAllowed = __eidEntityDescriptions[entityString] ~= nil isAllowed = isAllowed or EID:getDescriptionData(entity.Type, entity.Variant, entity.SubType) ~= nil isAllowed = isAllowed or EID:getEntityData(entity, "EID_Description") ~= nil end if entity.Type == EntityType.ENTITY_PICKUP then if entity.SubType == 0 and EID:getDescriptionData(entity.Type, entity.Variant, entity.SubType) ~= nil then -- allow pickups with subtype 0 to have a description, if its explictly defined return true end isAllowed = isAllowed or (entity.Variant == PickupVariant.PICKUP_COLLECTIBLE and EID.Config["DisplayItemInfo"]) isAllowed = isAllowed or (entity.Variant == 110 and entity:GetSprite():IsPlaying("Idle") and EID.Config["DisplayItemInfo"]) -- Broken Shovel isAllowed = isAllowed or (entity.Variant == PickupVariant.PICKUP_TRINKET and EID.Config["DisplayTrinketInfo"]) isAllowed = isAllowed or (entity.Variant == PickupVariant.PICKUP_TAROTCARD and EID.Config["DisplayCardInfo"]) isAllowed = isAllowed or (entity.Variant == PickupVariant.PICKUP_PILL and EID.Config["DisplayPillInfo"]) return isAllowed and (entity.SubType > 0 or -- For Flip descriptions, allow 5.100.0 pedestals to have descriptions under VERY specific criteria! (EID.isRepentance and EID:getEntityData(entity, "EID_FlipItemID") and EID:PlayersHaveCollectible(CollectibleType.COLLECTIBLE_FLIP))) end if entity.Type == 6 and entity.Variant == 16 and EID.Config["DisplayCraneInfo"] and EID.isRepentance then isAllowed = not entity:GetSprite():IsPlaying("Broken") and not entity:GetSprite():IsPlaying("Prize") and not entity:GetSprite():IsPlaying("OutOfPrizes") and (EID.CraneItemType[entity.InitSeed.."Drop"..entity.DropSeed] or EID.CraneItemType[tostring(entity.InitSeed)]) end if entity.Type == 1000 then if (entity.Variant == 161 and entity.SubType <= 2) or (entity.Variant == EffectVariant.DICE_FLOOR and EID.Config["DisplayDiceInfo"]) then isAllowed = true end end return isAllowed end -- Replaces shorthand-representations of a character with the internal reference function EID:replaceShortMarkupStrings(text) for _, pair in ipairs(EID.TextReplacementPairs) do text = string.gsub(text, pair[1], pair[2]) end return text end local VariantToColorText = { [100] = "{{ColorYellow}}", [350] = "{{ColorYellow}}", [300] = "{{ColorCard}}", [70] = "{{ColorPill}}" } -- Replaces name markup objects with the actual name function EID:replaceNameMarkupStrings(text) for word in string.gmatch(text, "{{Name.-}}") do local strTrimmed = string.gsub(word, "{{Name(.-)}}", function(a) return a end) local showIcon = true -- If the markup is {{NameOnlyX###}}, do not print an icon before the name if string.find(text, "NameOnly") then strTrimmed = string.gsub(word, "{{NameOnly(.-)}}", function(a) return a end) showIcon = false end local indicator = string.sub(strTrimmed, 1, 1) local id = tonumber(string.sub(strTrimmed, 2, -1)) local name = "" if tonumber(indicator) then local entityID = {} for e in string.gmatch(strTrimmed, "([^.]*)") do table.insert(entityID, tonumber(e)) end local iconText = "" local colorText = "" if entityID[1] == 1 then if showIcon then iconText = EID:GetPlayerIcon(entityID[3]) .. " " end colorText = "{{ColorIsaac}}" else if showIcon then iconText = "{{" .. EID:GetIconNameByVariant(entityID[2]) .. entityID[3] .. "}} " end colorText = VariantToColorText[entityID[2]] or "" end if entityID[2] == 70 then name = iconText .. colorText .. EID:getPillName(entityID[3], false) .. "{{CR}}" else name = iconText .. colorText .. EID:getObjectName(entityID[1], entityID[2], entityID[3]) .. "{{CR}}" end elseif indicator == "C" then -- Collectible name = (showIcon and "{{Collectible"..id.."}} " or "") .. "{{ColorYellow}}" .. EID:getObjectName(5, 100, id) .. "{{CR}}" elseif indicator == "T" then -- Trinket name = (showIcon and "{{Trinket"..id.."}} " or "") .. "{{ColorYellow}}" .. EID:getObjectName(5, 350, id) .. "{{CR}}" elseif indicator == "P" then -- Pills name = (showIcon and "{{Pill"..id.."}} " or "") .. "{{ColorPill}}" .. EID:getPillName(id, false) .. "{{CR}}" elseif indicator == "K" then -- Card name = (showIcon and "{{Card"..id.."}} " or "") .. "{{ColorCard}}" .. EID:getObjectName(5, 300, id) .. "{{CR}}" elseif indicator == "I" then -- Player (I for Isaac) name = (showIcon and EID:GetPlayerIcon(id) .. " " or "") .. "{{ColorIsaac}}" .. EID:getPlayerName(id) .. "{{CR}}" end text = string.gsub(text, word, name, 1) end return text end -- Generates a string with the defined pixel-length using a custom 1px wide character -- This will only work for this specific custom font function EID:generatePlaceholderString(length) return string.rep("¤", length) end -- Returns the inlineIcon object of a given Iconstring -- can be used to validate an iconstring function EID:getIcon(str) if str == nil then return EID.InlineIcons["ERROR"] end local strTrimmed = string.gsub(str,"{{(.-)}}",function(a) return a end ) if #strTrimmed <= #str then local itemIconObj = EID:createItemIconObject(strTrimmed) if itemIconObj then return itemIconObj end if type(EID.InlineIcons[strTrimmed]) == "function" then return EID.InlineIcons[strTrimmed](str) or EID.InlineIcons["ERROR"] end return EID.InlineIcons[strTrimmed] or EID.InlineIcons["ERROR"] else return EID.InlineIcons["ERROR"] end end -- Tries to read special markup used to generate icons for all Collectibles/Trinkets and the default Cards/Pills -- Returns an inlineIcon Object or nil if no parsing was possible function EID:createItemIconObject(str) local item = nil local subTypeIdentifier = 0 -- Check for {{Item#.##.###}} markup; easiest way to handle it is to just call this function again local itemID,numReplaceGeneric = string.gsub(str, "Item", "") if numReplaceGeneric > 0 and itemID ~= "" then local Type, Var, Sub = EID:SplitTVS(itemID) if Type == 1 then return EID:getIcon("Player" .. Sub) elseif Type == 5 then if Var == 100 then return EID:createItemIconObject("Collectible" .. Sub) elseif Var == 350 then return EID:createItemIconObject("Trinket" .. Sub) elseif Var == 300 then return EID:createItemIconObject("Card" .. Sub) elseif Var == 70 then return EID:createItemIconObject("Pill" .. Sub) end end end local collID,numReplace = string.gsub(str, "Collectible", "") if numReplace > 0 and collID ~= "" and tonumber(collID) ~= nil then item = EID.itemConfig:GetCollectible(tonumber(collID)) subTypeIdentifier = tonumber(collID) end local trinketID,numReplace2 = string.gsub(str, "Trinket", "") if numReplace2 > 0 and trinketID ~= "" and tonumber(trinketID) ~= nil then item = EID.itemConfig:GetTrinket(tonumber(trinketID)) subTypeIdentifier = tonumber(trinketID) end local cardID,numReplace3 = string.gsub(str, "Card", "") if numReplace3 > 0 and cardID ~= "" and tonumber(cardID) ~= nil then if tonumber(cardID) > maxCardID then return EID.InlineIcons[str] or EID.InlineIcons["Card"] end return {"Cards", tonumber(cardID)-1, 8, 8, 0, 1, EID.CardPillSprite} end local pillID,numReplace4 = string.gsub(str, "Pill", "") if numReplace4 > 0 and pillID ~= "" and tonumber(pillID) ~= nil then if tonumber(pillID % 2048) > maxPillID then return EID.InlineIcons[str] or EID.InlineIcons["Pill"] end return {"Pills", tonumber(pillID % 2048)-1, 9, 8, 0, 1, EID.CardPillSprite} end if item == nil then return nil end if dynamicSpriteCache[str] then return dynamicSpriteCache[str] else local spriteDummy = Sprite() spriteDummy:Load("gfx/eid_inline_icons.anm2", true) spriteDummy:ReplaceSpritesheet(1, item.GfxFileName) spriteDummy:LoadGraphics() local newDynamicSprite = {"ItemIcon", subTypeIdentifier, 11, 8, -2, -2, spriteDummy} dynamicSpriteCache[str] = newDynamicSprite return newDynamicSprite end end -- Returns the icon for a given transformation name or ID function EID:getTransformationIcon(str) if str == nil then return EID.InlineIcons["ERROR"] end if tonumber(str) ~= nil then str = EID.descriptions["en_us"].transformations[tonumber(str + 1)] end local transformSprite = EID:getIcon(str:gsub(" ", "")) if transformSprite[1] == "ERROR" then transformSprite = EID:getIcon("CustomTransformation") end return transformSprite end -- Returns the width of a given string in Pixels function EID:getStrWidth(str) return EID.font:GetStringWidthUTF8(str) end -- Searches thru the given string and replaces Iconplaceholders with icons. -- Returns 2 values. the string without the placeholders but with an accurate space between lines. and a table of all Inline Sprites function EID:filterIconMarkup(text, renderBulletPointIcon) local spriteTable = {} for word in string.gmatch(text, "{{.-}}") do local textposition = string.find(text, word) or 1 local callback = EID._NextIconModifier EID._NextIconModifier = nil local lookup = EID:getIcon(word) local preceedingTextWidth = EID:getStrWidth(string.sub(text, 0, textposition - 1)) * EID.Scale -- Center the small bullet point icons by adding an extra left offset to them. -- If the icon has a positive left offset, that means the offset is already included in the icon itself and we should not add it again if renderBulletPointIcon and lookup[3] < 11 and (#lookup < 5 or lookup[5] <= 0) then preceedingTextWidth = preceedingTextWidth + math.floor((12 - lookup[3]) / 2) * EID.Scale end table.insert(spriteTable, {lookup, preceedingTextWidth, callback}) text = string.gsub(text, word, EID:generatePlaceholderString(lookup[3]), 1) end return text, spriteTable end --renders a list of given inline sprite objects returned by the "EID:filterIconMarkup()" function -- Table entry format: {EID.InlineIcons Object, Width of text preceeding the icon} function EID:renderInlineIcons(spriteTable, posX, posY) for _, sprite in ipairs(spriteTable) do local Xoffset = sprite[1][5] or -1 local Yoffset = sprite[1][6] or 0 local spriteObj = (type(sprite[1][7]) == "function" and sprite[1][7]()) or sprite[1][7] or EID.InlineIconSprite if sprite[1][2] >= 0 then spriteObj:SetFrame(sprite[1][1], sprite[1][2]) elseif not spriteObj:IsPlaying(sprite[1][1]) or spriteObj:IsFinished(sprite[1][1]) then spriteObj:Play(sprite[1][1],true) else spriteObj:Update() end EID:renderIcon(spriteObj, posX + sprite[2] + Xoffset * EID.Scale, posY + Yoffset * EID.Scale, sprite[3], sprite[1][1], sprite[1][2]) end end -- helper function to render Icons in specific EID settins function EID:renderIcon(spriteObj, posX, posY, callback, animName, animFrame) spriteObj.Scale = Vector(EID.Scale, EID.Scale) spriteObj.Color = Color(1, 1, 1, EID.Config["Transparency"], 0, 0, 0) if callback then callback(spriteObj) end if EID.CachingDescription then table.insert(EID.CachedIcons[#EID.CachedIcons], {spriteObj, posX, posY, callback, animName, animFrame}) end spriteObj:Render(Vector(posX, posY), nullVector, nullVector) end -- Returns the icon used for the bulletpoint. It will look at the first word in the given string. -- Also returns the first word if it was rejected (so it can be removed from the line) function EID:handleBulletpointIcon(text) local firstWord = EID:removeColorMarkup(string.match(text, "([^%s]+)")) if EID:getIcon(firstWord) ~= EID.InlineIcons["ERROR"] and string.find(firstWord, "{{.-}}")~=nil then if not EID.Config["StatAndPickupBulletpoints"] and EID.StatPickupBulletpointBlacklist[firstWord] then return "\007", firstWord end return firstWord end return "\007" end -- Gets a KColor from a Markup-string (example Input: "{{ColorText}}") -- Returns the KColor object and a boolean value indicating if the given string was a color markup or not local colorFunc = nil function EID:getColor(str, baseKColor) local color = baseKColor local isColorMarkup = false if str ~= nil then local strTrimmed = string.gsub(str,"{{(.-)}}",function(a) return a end, 1) if #strTrimmed <= #str then if type(EID.InlineColors[strTrimmed]) == "function" then colorFunc = EID.InlineColors[strTrimmed] color = EID.InlineColors[strTrimmed](color) else if EID.InlineColors[strTrimmed] then colorFunc = nil end color = EID.InlineColors[strTrimmed] or color if EID.Config["ColorblindMode"] > 0 then color = EID.ColorBlindColors[EID.Config["ColorblindMode"]][strTrimmed] or color end end isColorMarkup = type(EID.InlineColors[strTrimmed]) ~= type(nil) end end color = EID:copyKColor(color) color.Alpha = math.min(color.Alpha, EID.Config["Transparency"]) return color, isColorMarkup end -- Filters a given string and looks for Colormarkup. Splits the text into subsections limited by them. -- Returns: Table of subsections of the text, their respective KColor, and the width of the subsection function EID:filterColorMarkup(text, baseKColor) local textPartsTable = {} local lastColor = baseKColor local lastFunc = colorFunc local lastPosition = 0 for word in string.gmatch(text, "{{.-}}") do local textposition = string.find(text, word) local lookup, isColor = EID:getColor(word, lastColor) if isColor then local preceedingText = string.sub(text, lastPosition, textposition - 1) local preceedingTextWidth = EID:getStrWidth(preceedingText) * EID.Scale lastPosition = textposition table.insert(textPartsTable, {preceedingText, lastColor, preceedingTextWidth, lastFunc}) lastColor = lookup lastFunc = colorFunc text = string.gsub(text, word, "", 1) end end table.insert(textPartsTable, {string.sub(text, lastPosition), lastColor, 0, lastFunc}) return textPartsTable end -- A simple function to remove color markup, to preserve bulletpoint icons after start-of-line color markup function EID:removeColorMarkup(text) for word in string.gmatch(text, "{{Color.-}}") do text = string.gsub(text, word, "", 1) end return text end -- A simple function to replace all markup {{ }} with placeholder strings, to use in fitTextToWidth function EID:replaceAllMarkupWithSpaces(text, checkBulletpoint) if checkBulletpoint then -- Check for the text to just be a bulletpoint icon, which should be considered as zero width -- (fixTextToWidth uses this function one word at a time) if EID:getIcon(text:gsub(" ", "")) ~= EID.InlineIcons["ERROR"] and string.find(text, "{{.-}}")~=nil then return "" end end -- iconsFound is used to make the next space after a markup icon be immune to line breaks, but only if it's just one icon with no other text local iconsFound = 0; if text:gsub(" ", ""):gsub("{{.-}}","") ~= "" then iconsFound = -999 end for word in string.gmatch(text, "{{.-}}") do local lookup = EID:getIcon(word) if lookup[1] ~= "ERROR" then text = string.gsub(text, word, EID:generatePlaceholderString(lookup[3]), 1) iconsFound = iconsFound + 1 else text = string.gsub(text, word, "", 1) end end return text, iconsFound end -- Fits a given string to a specific width -- returns the string as a table of lines function EID:fitTextToWidth(str, textboxWidth, breakUtf8Chars) local formattedLines = {} -- Lines with a {{NoLineBreak}} tag should be left in one continuous line if str:find("{{NoLineBreak}}") or str:find("{{NoLB}}") then str = str:gsub("{{NoLineBreak}}",""):gsub("{{NoLB}}","") table.insert(formattedLines,str) return formattedLines end local curLength = 0 local text = {} -- the first word we run into might actually be a bulletpoint icon, which should be zero width local isBulletpoint = true -- if we find an icon by itself, we should skip the following space local previousWordWasIcon = false local cursor = 1 local word_begin_index = 1 local byte = string.byte -- for speed up local sub = string.sub local byte_space = string.byte(' ') while cursor <= #str do -- ascii word: 0x0xxxxxxx -- utf8 word (sequence): 0x11xxxxxx 0x10xxxxxx 0x10xxxxxx ... 0x10xxxxxx -- see https://en.wikipedia.org/wiki/UTF-8 -- we can only break after space, or before 0x11xxxxxx local cur, next = byte(str,cursor), byte(str,cursor+1) if -- cond#1: we can break at the end of string cursor == #str or -- cond#2: we can break after space cur == byte_space or -- handle utf8 characters (breakUtf8Chars and( -- cond#3: we can break if the next character is 0x11xxxxxx ((next & 0xC0) == 0xC0) or -- cond#4: we can also break if the current is 0x10xxxxxx while the next is ascii but not space (((cur & 0xC0) == 0x80) and (next~= byte_space and (next & 0x80) == 0x00)) )) then --[[ -------------------ascii only--------------- word will be separated by spaces. wordA |wordB |wordC |^%@&Q%#^&#@!! |aksldj ↑ cond#2 we may break after every space. spaces | | | | | |woops ↑ cond#2 -------------------UTF8 only---------------- word is 0x11xxxxxx followed by several 0x10xxxxxx until the next 0x11xxxxxx (not included) 你|好|,|世|界|!|↑|↑|↑|↑ ↑ cond#3 byte data of a word: [0x11xxxxxx,0x10xxxxxx,0x10xxxxxx, (next is 0x11xxxxxx, cond#3)] --------------------ascii->utf8------------ utf8 word must start with 0x11xxxxxx, so cond#3 split before it. word|世|界 | ↑ ↑ cond#3 cond#3 world |means |世|界 ↑ cond#2 -------------------utf8->ascii-------------- 世|界|是|world | ↑ ↑ cond#4 cond#3 世|界|是 |world | ↑ ↑ cond#2 cond#3 ]] -- we can break after str[cursor] local word = sub(str, word_begin_index, cursor) local wordFiltered, iconsFound = EID:replaceAllMarkupWithSpaces(word, isBulletpoint) isBulletpoint = false local wordLength = EID:getStrWidth(wordFiltered) if curLength + wordLength <= textboxWidth or curLength < 17 or previousWordWasIcon then table.insert(text, word) curLength = curLength + wordLength else table.insert(formattedLines, table.concat(text)) text = { word } curLength = wordLength end previousWordWasIcon = (iconsFound == 1) -- next word starts here word_begin_index = cursor + 1 end cursor = cursor + 1 end -- fix this function applying crafting icon green tint to the start of the printed line EID._NextIconModifier = nil table.insert(formattedLines, table.concat(text)) return formattedLines end -- Renders a given string using the EID Custom font. This will also apply any markup and render icons -- needs to be called in a render Callback -- args: string, Vector(int, int), Vector(float,float), KColor obj, bool -- Returns the last used KColor function EID:renderString(str, position, scale, kcolor, renderBulletPointIcon) str = EID:replaceShortMarkupStrings(str) local textPartsTable = EID:filterColorMarkup(str, kcolor) local offsetX = 0 for _, textPart in ipairs(textPartsTable) do local strFiltered, spriteTable = EID:filterIconMarkup(textPart[1], renderBulletPointIcon) EID:renderInlineIcons(spriteTable, position.X + offsetX, position.Y) if strFiltered then -- prevent possible crash when strFiltered is nil EID.font:DrawStringScaledUTF8(strFiltered, position.X + offsetX, position.Y, scale.X, scale.Y, textPart[2], 0, false) if EID.CachingDescription then table.insert(EID.CachedStrings[#EID.CachedStrings], {strFiltered, position.X + offsetX, position.Y, textPart[2], textPart[4], EID.Config["Transparency"]}) end offsetX = offsetX + EID:getStrWidth(strFiltered) * scale.X end end return textPartsTable[#textPartsTable][2] end -- Adds Description object modifiers. -- Used for altering descriptions. Example: Spindown dice, Tarot Cloth, ... function EID:addDescriptionModifier(modifierName, condition, callback, position) position = position or #EID.DescModifiers + 1 for _,v in ipairs(EID.DescModifiers) do if v["name"] == modifierName then v["condition"] = condition v["callback"] = callback return end end table.insert(EID.DescModifiers, position, { name = modifierName, condition = condition, callback = callback }) end -- Removes a Description object modifier -- Used for altering descriptions. Example: Spindown dice, Tarot Cloth, ... function EID:removeDescriptionModifier(modifierName) for i,v in ipairs(EID.DescModifiers) do if v["name"] == modifierName then table.remove(EID.DescModifiers,i) return end end end -- Interpolates between 2 KColors with a given fraction. function EID:interpolateColors(kColor1, kColor2, fraction) local t = KColor( (kColor2.Red - kColor1.Red) * fraction + kColor1.Red, (kColor2.Green - kColor1.Green) * fraction + kColor1.Green, (kColor2.Blue - kColor1.Blue) * fraction + kColor1.Blue, (kColor2.Alpha - kColor1.Alpha) * fraction + kColor1.Alpha ) return t end function EID:updateDescriptionsViaTable(changeTable, tableToUpdate) for k,v in pairs(changeTable) do if v == "" then tableToUpdate[k] = nil else tableToUpdate[k] = v end end end -- Converts e.g. "5.100.69" format strings into 5, 100, and 69; returns 0 for any not included function EID:SplitTVS(tvsString) local Type, Var, Sub = 0, 0, 0 local tvsTable = {} for i in string.gmatch(tvsString, "[^.]+") do table.insert(tvsTable, tonumber(i)) end Type = tvsTable[1] or Type; Var = tvsTable[2] or Var; Sub = tvsTable[3] or Sub; return Type, Var, Sub end -- Checks if any player has a given item ID (or anyone is a given player ID) function EID:PlayersHaveItem(Type, Var, Sub) -- convert "5.100.69" format strings into type, var, sub if type(Type) == "string" then Type, Var, Sub = EID:SplitTVS(Type) -- assume collectible if only Type was given elseif Var == nil then Sub = Type; Type = 5; Var = 100 end if Type == 1 then return EID:PlayersHaveCharacter(Sub) elseif Type == 5 then if Var == 100 then return EID:PlayersHaveCollectible(Sub) elseif Var == 350 then return EID:PlayersHaveTrinket(Sub) elseif Var == 300 then return EID:PlayersHaveCard(Sub) elseif Var == 70 then return EID:PlayersHavePill(Sub) end end return false end -- Checks if the given player has the given item ID (or is the given player ID) function EID:PlayerHasItem(player, Type, Var, Sub) -- convert "5.100.69" format strings into type, var, sub if type(Type) == "string" then Type, Var, Sub = EID:SplitTVS(Type) -- assume collectible if only Type was given elseif Var == nil then Sub = Type; Type = 5; Var = 100 end if Type == 1 then return player:GetPlayerType() == Sub elseif Type == 5 then if Var == 100 then return player:HasCollectible(Sub) elseif Var == 350 then return player:HasTrinket(Sub) elseif Var == 300 then return EID:PlayerHasCard(player, Sub) elseif Var == 70 then return EID:PlayerHasPill(player, Sub) end end return false end -- Checks if any player has a given collectible ID, for modifiers function EID:PlayersHaveCollectible(collectibleID) for i = 0, game:GetNumPlayers() - 1 do local player = Isaac.GetPlayer(i) if player:HasCollectible(collectibleID) then return true, player, i end end return false end function EID:PlayersVoidedCollectible(collectibleID) for i = 0, game:GetNumPlayers() - 1 do local player = Isaac.GetPlayer(i) local playerNum = EID:getPlayerID(player, true) if player:HasCollectible(477) and EID.absorbedItems[tostring(playerNum)] and EID.absorbedItems[tostring(playerNum)][tostring(collectibleID)] then return true, player, i end end return false end function EID:PlayerVoidedCollectible(player, collectibleID) local i = EID:getPlayerID(player, true) if player:HasCollectible(477) and EID.absorbedItems[tostring(i)] and EID.absorbedItems[tostring(i)][tostring(collectibleID)] then return true end end -- Checks if any player has a given trinket ID, for modifiers function EID:PlayersHaveTrinket(trinketID) for i = 0, game:GetNumPlayers() - 1 do local player = Isaac.GetPlayer(i) if player:HasTrinket(trinketID) then return true, player, i end end return false end function EID:PlayersHaveCard(cardID) for i = 0, game:GetNumPlayers() - 1 do local player = Isaac.GetPlayer(i) for j = 0, (EID.isRepentance and 3 or 1) do if player:GetCard(j) == cardID then return true, player, i end end end return false end function EID:PlayerHasCard(player, cardID) for j = 0, (EID.isRepentance and 3 or 1) do if player:GetCard(j) == cardID then return true end end end function EID:PlayersHavePill(pillID) for i = 0, game:GetNumPlayers() - 1 do local player = Isaac.GetPlayer(i) for j = 0, (EID.isRepentance and 3 or 1) do if player:GetPill(j) == pillID then return true, player, i end end end return false end function EID:PlayerHasPill(player, pillID) for j = 0, (EID.isRepentance and 3 or 1) do if player:GetPill(j) == pillID then return true end end end -- Checks if someone is playing as a certain character, for modifiers -- includeTainted means we don't care if the player is Tainted or not (for things that, say, apply to Lost/Tainted Lost) function EID:PlayersHaveCharacter(playerType, includeTainted) for i = 0, game:GetNumPlayers() - 1 do local player = Isaac.GetPlayer(i) if player:GetPlayerType() == playerType then return true, player, i end if includeTainted and EID.isRepentance and EID.TaintedToRegularID[player:GetPlayerType()] == playerType then return true, player, i end end return false end -- Converts a given CollectibleID into the respective Spindown dice result function EID:getSpindownResult(collectibleID) if collectibleID <= 0 or collectibleID > 4294960000 then return 0 end local newID = collectibleID local attempts = 0 repeat newID = newID - 1 attempts = attempts + 1 --note: the order of the SkipLocked check statement is important so that the item is checked for being in a pool either way (to display a ? if it isn't) until (EID.itemConfig:GetCollectible(newID) and (EID:isCollectibleUnlocked(newID) or not EID.Config["SpindownDiceSkipLocked"]) and not EID.itemConfig:GetCollectible(newID).Hidden and EID:isCollectibleAllowed(newID)) or newID == CollectibleType.COLLECTIBLE_NULL or attempts > 10 return newID end -- Returns the maximum collectible id, including modded items local maxCollectibleID = nil -- cache after first use. this number will not change mid game function EID:GetMaxCollectibleID() if maxCollectibleID then return maxCollectibleID end local id = CollectibleType.NUM_COLLECTIBLES-1 local step = 16 while step > 0 do if EID.itemConfig:GetCollectible(id+step) ~= nil then id = id + step else step = step // 2 end end maxCollectibleID = id return id end local maxTrinketID = nil function EID:GetMaxTrinketID() if maxTrinketID then return maxTrinketID end local id = TrinketType.NUM_TRINKETS-1 local step = 16 while step > 0 do if EID.itemConfig:GetTrinket(id+step) ~= nil then id = id + step else step = step // 2 end end maxTrinketID = id return id end function EID:DetectModdedItems() if EID:GetMaxCollectibleID() > EID.XMLMaxItemID then return true end if EID.itemConfig:GetCollectible(EID.XMLMaxItemID) == nil then return true end return false end -- REPENTANCE ONLY! Return whether the collectible is considered available -- Bag of Crafting rerolls unavailable items; this function is kept brief to help BoC speed function EID:isCollectibleAvailable(collectibleID) if EID.itemAvailableStates[collectibleID] == nil then EID.itemAvailableStates[collectibleID] = EID.itemConfig:GetCollectible(collectibleID):IsAvailable() end return EID.itemAvailableStates[collectibleID] end -- REPENTANCE ONLY! Return our best guess on whether an achievement-locked collectible is unlocked -- (Things like Tainted Lost and Sacred Orb give false negatives) -- Spindown Dice skips over locked items, but not unavailable items function EID:isCollectibleUnlocked(collectibleID) local item = EID.itemConfig:GetCollectible(collectibleID) if item == nil then return false end if EID.itemUnlockStates[collectibleID] == nil then --whitelist all quest items and items with no associated achievement if item.AchievementID == -1 or (item.Tags and item.Tags & ItemConfig.TAG_QUEST == ItemConfig.TAG_QUEST) then EID.itemUnlockStates[collectibleID] = true return true end --blacklist all hidden items if item.Hidden then EID.itemUnlockStates[collectibleID] = false return false end --check the item's IsAvailable status if the function exists if item.IsAvailable then EID.itemUnlockStates[collectibleID] = item:IsAvailable() else EID.itemUnlockStates[collectibleID] = true end return EID.itemUnlockStates[collectibleID] else return EID.itemUnlockStates[collectibleID] end end EID.isCollectibleUnlockedAnyPool = EID.isCollectibleUnlocked -- old name before ItemConfig:IsAvailable was added -- REPENTANCE ONLY! Return whether the collectible is completely disallowed by the current game mode -- Bag of Crafting and Spindown Dice skip over disallowed items entirely function EID:isCollectibleAllowed(collectibleID) local item = EID.itemConfig:GetCollectible(collectibleID) if item == nil then return false elseif item.Tags == nil then return true else if game:IsGreedMode() and item:HasTags(ItemConfig.TAG_NO_GREED) then return false elseif game.Challenge > 0 and item:HasTags(ItemConfig.TAG_NO_CHALLENGE) then return false else return true end -- no need to check TAG_NO_DAILY because mods are disabled for Dailies end end -- Achievements Locked Check (do we have Cube of Meat or Book of Revelations unlocked?) ---@diagnostic disable-next-line: duplicate-set-field function EID:AreAchievementsAllowed() -- Tainted characters have definitely beaten Mom! -- (Fixes Tainted Lost's item pools, and potentially modded character's mechanics, ruining this check) if EID.player:GetPlayerType() < 21 then -- Challenge runs and TMTrainer might break the pool, so ignore them. if not game:GetSeeds():IsCustomRun() and not EID:PlayersHaveCollectible(CollectibleType.COLLECTIBLE_TMTRAINER) then local hasBookOfRevelationsUnlocked = EID:isCollectibleUnlocked(CollectibleType.COLLECTIBLE_BOOK_OF_REVELATIONS or CollectibleType.COLLECTIBLE_BOOK_REVELATIONS) if not hasBookOfRevelationsUnlocked then local hasCubeOfMeatUnlocked = EID:isCollectibleUnlocked(CollectibleType.COLLECTIBLE_CUBE_OF_MEAT) if not hasCubeOfMeatUnlocked then return false end end end end return true end -- Returns the dimension ID the player is currently in. -- 0: Normal Dimension -- 1: Secondary dimension, used by Downpour mirror dimension and Mines escape sequence -- 2: Death Certificate dimension function EID:GetDimension(level) local roomIndex = level:GetCurrentRoomIndex() for i = 0, 2 do if GetPtrHash(level:GetRoomByIdx(roomIndex, i)) == GetPtrHash(level:GetRoomByIdx(roomIndex, -1)) then return i end end return nil end -- Converts a given table into a string containing the crafting icons of the table -- Example input: {1,2,3,4,5,6,7,8} -- Result: "{{Crafting1}}{{Crafting2}}{{Crafting3}}{{Crafting4}}{{Crafting5}}{{Crafting6}}{{Crafting7}}{{Crafting8}}" local emptyPickupTable = {} for i=1, 29 do emptyPickupTable[i] = 0 end function EID:tableToCraftingIconsFull(craftTable, indicateCompleteContent) local sortedList = {table.unpack(craftTable)} table.sort(sortedList, function(a, b) return a < b end) local visitedItemCount = {table.unpack(emptyPickupTable)} local iconString = "" for _, nr in ipairs(sortedList) do visitedItemCount[nr] = visitedItemCount[nr] + 1 local containsItemResult = EID:bagContainsItem(nr, visitedItemCount[nr]) local completedColoring = indicateCompleteContent and containsItemResult and containsItemResult >= 1 and "{{IconGreenTint}}" or "" iconString = iconString..completedColoring.."{{Crafting"..nr.."}}" end return iconString end -- Converts a given table into a string containing the crafting icons of the table, which are also grouped to reduce render lag -- Example input: {1,1,1,2,2,3,3,3} -- Result: "3{{Crafting1}}2{{Crafting2}}3{{Crafting3}}" function EID:tableToCraftingIconsMerged(craftTable, indicateCompleteContent) local sortedList = {table.unpack(craftTable)} local filteredList = {table.unpack(emptyPickupTable)} for _, nr in ipairs(sortedList) do filteredList[nr] = filteredList[nr] + 1 end local iconString = "" for nr, count in ipairs(filteredList) do if (count > 0) then local coloring = "" if indicateCompleteContent then local bagContainsItem = EID:bagContainsItem(nr, count) if bagContainsItem == 1 then coloring = "{{ColorBagComplete}}" elseif bagContainsItem == 2 then coloring = "{{ColorBagOverfill}}" end end iconString = iconString..coloring..count.."{{Crafting"..nr.."}}{{CR}}" end end return iconString end -- Checks how many of an item there are in the bag -- Returns false if the item is not in the bag -- Returns 0 if there are fewer than the target amount -- Returns 1 if there are exactly the target amount -- Returns 2 if there are more than the target amount function EID:bagContainsItem(itemID, itemCount) local foundCount = 0 local bagItems = EID.BoC.BagItemsOverride or EID.BoC.BagItems for _, bagItem in ipairs(bagItems) do if bagItem == itemID then foundCount = foundCount + 1 end end if foundCount == 0 then return false elseif foundCount < itemCount then return 0 elseif foundCount == itemCount then return 1 else return 2 end end -- Get the number of pickups in the given recipe table that are inside our bag -- (For checking if a recipe is possible to create if you need to use every item in your bag) function EID:bagContainsCount(craftTable) local count = 0 local ingredCount = {table.unpack(emptyPickupTable)} for _, id in ipairs(craftTable) do ingredCount[id] = ingredCount[id] + 1 end local bagItems = EID.BoC.BagItemsOverride or EID.BoC.BagItems for _, bagItem in ipairs(bagItems) do if ingredCount[bagItem] > 0 then count = count + 1 ingredCount[bagItem] = ingredCount[bagItem] - 1 end end return count end function EID:handleHUDElement(hudElement) local alteredHudElement = {} for k,v in pairs(hudElement) do alteredHudElement[k] = v end local screenSize = EID:getScreenSize() local hudOffset = EID.Config["HUDOffset"] if EID.isRepentance then hudOffset = (Options.HUDOffset * 10) end for _,v in ipairs(hudElement.anchors) do if v == "TOP" then alteredHudElement.y = hudElement.y + hudOffset * 2 elseif v == "LEFT" then alteredHudElement.x = hudElement.x + hudOffset * 2 elseif v == "BOTTOM" then alteredHudElement.y = screenSize.Y*2 - alteredHudElement.y - alteredHudElement.height alteredHudElement.y = alteredHudElement.y - hudOffset * 2 elseif v == "RIGHT" then alteredHudElement.x = screenSize.X*2 - alteredHudElement.x - alteredHudElement.width alteredHudElement.x = alteredHudElement.x - hudOffset * 2 end end return alteredHudElement end function EID:getScreenSize() local room = game:GetRoom() local pos = room:WorldToScreenPosition(Vector(0,0)) - room:GetRenderScrollOffset() - game.ScreenShakeOffset local rx = pos.X + 60 * 26 / 40 local ry = pos.Y + 140 * (26 / 40) return Vector(rx*2 + 13*26, ry*2 + 7*26) end function EID:getEntityData(entity, str) if EID:EntitySanityCheck(entity) and not EID:IsGridEntity(entity) and entity:GetData() ~= nil then return entity:GetData()[str] end return nil end function EID:setEntityData(entity, str, value) if EID:EntitySanityCheck(entity) and not EID:IsGridEntity(entity) and entity:GetData() ~= nil then entity:GetData()[str] = value end end -- Function to fix font compatibility. Resets config font to a value compatible with your current language function EID:fixDefinedFont(forceRefresh) local curLang = EID:getLanguage() local curFont = EID.Config["FontType"] -- If the textbox width is set to the default, make it match the selected language's default -- Fixes language selection not respecting the language's textbox width, although it also prevents 130 from being used as the width with those languages if EID.Config["TextboxWidth"] == EID.DefaultConfig["TextboxWidth"] then EID.Config["TextboxWidth"] = EID.descriptions[curLang].fonts[1].textboxWidth or EID.DefaultConfig["TextboxWidth"] end -- If our currently loaded font is still valid, we don't need to reset values for _, v in ipairs(EID.descriptions[curLang].fonts) do if curFont == v.name then -- Load the new language's line height / textbox width if (forceRefresh) then EID.Config["LineHeight"] = EID.descriptions[curLang].fonts[1].lineHeight or EID.DefaultConfig["LineHeight"] EID.Config["TextboxWidth"] = EID.descriptions[curLang].fonts[1].textboxWidth or EID.DefaultConfig["TextboxWidth"] end return false end end EID.Config["FontType"] = EID.descriptions[curLang].fonts[1].name -- On new font load, reset line height / textbox width to the default values if there's no defined height/width EID.Config["LineHeight"] = EID.descriptions[curLang].fonts[1].lineHeight or EID.DefaultConfig["LineHeight"] EID.Config["TextboxWidth"] = EID.descriptions[curLang].fonts[1].textboxWidth or EID.DefaultConfig["TextboxWidth"] return true end -- Check if a given font name is valid for the currently selected language function EID:canUseFontType(fontType) local curLang = EID:getLanguage() for _, v in ipairs(EID.descriptions[curLang].fonts) do if fontType == v.name then return true end end return false end -- Creates a copy of a KColor object. This prevents overwriting existing function EID:copyKColor(colorObj) return KColor(colorObj.Red, colorObj.Green, colorObj.Blue, colorObj.Alpha) end -- Compares two KColors. Returns true if they are equal function EID:areColorsEqual(c1, c2) return c1.Red == c2.Red and c1.Green == c2.Green and c1.Blue == c2.Blue and c1.Alpha == c2.Alpha end -- Get KColor object of "Entity Name" texts function EID:getNameColor() return EID:getColor(EID.Config["ItemNameColor"], EID.InlineColors["ColorEIDObjName"]) end -- Get KColor object of "Description" texts function EID:getTextColor() return EID:getColor(EID.Config["TextColor"], EID.InlineColors["ColorEIDText"]) end -- Get KColor object of "Transformation" texts function EID:getTransformationColor() return EID:getColor(EID.Config["TransformationColor"], EID.InlineColors["ColorEIDTransform"]) end -- Get KColor object of "Error" texts function EID:getErrorColor() return EID:getColor(EID.Config["ErrorColor"], EID.InlineColors["ColorEIDError"]) end -- Specify the name of the mod which will be displayed next to the item name -- By default EID takes the mod name function EID:setModIndicatorName(newName) EID.ModIndicator[EID._currentMod].Name = newName end -- Set an icon for the mod which will be displayed next to the item name function EID:setModIndicatorIcon(iconMarkup, override) if override == nil then override = true end -- overide previous value if not specified if EID.ModIndicator[EID._currentMod].Icon ~= nil and override == false then return end EID.ModIndicator[EID._currentMod].Icon = iconMarkup end EID.Coroutines = {} -- Add a coroutine to be ran 60 times a second function EID:addCoroutine(name, func, overwrite) if overwrite or EID.Coroutines[name] == nil then EID.Coroutines[name] = coroutine.create(func) end end function EID:removeCoroutine(name) EID.Coroutines[name] = nil end -- ran 60 times a second in main game render function EID:resumeCoroutines() for k,v in pairs(EID.Coroutines) do if coroutine.resume(v) == false then EID:removeCoroutine(k) end end end -- Returns true if an item needs to be collected for the collection page ---@diagnostic disable-next-line: duplicate-set-field function EID:requiredForCollectionPage(itemID) if not EID.SaveGame or EID.Config["SaveGameNumber"] == 0 or itemID >= CollectibleType.NUM_COLLECTIBLES or game:GetVictoryLap() > 0 or game:GetSeeds():IsCustomRun() then return false end return EID.SaveGame[EID.Config["SaveGameNumber"]].ItemNeedsPickup[itemID] end -- Updates the item collection state of the players, based on the QueuedItem value. -- TODO: also check for D100 / MissingNo Item collections ---@diagnostic disable-next-line: duplicate-set-field function EID:checkPlayersForMissingItems() if not EID.SaveGame or EID.Config["SaveGameNumber"] == 0 or game:GetVictoryLap() > 0 or game:GetSeeds():IsCustomRun() then return end if EID.GameUpdateCount % 5 ~= 0 then return end for i = 0, game:GetNumPlayers() - 1 do local player = Isaac.GetPlayer(i) if player.QueuedItem.Item and EID.SaveGame[EID.Config["SaveGameNumber"]].ItemNeedsPickup[player.QueuedItem.Item.ID] then table.insert(EID.CollectedItems, player.QueuedItem.Item.ID) EID.SaveGame[EID.Config["SaveGameNumber"]].ItemNeedsPickup[player.QueuedItem.Item.ID] = nil end end end function EID:getPlayerID(entityPlayer, lazarusAdjust) if not entityPlayer then return 0 end for i = 0, game:GetNumPlayers() - 1 do local player = Isaac.GetPlayer(i) if GetPtrHash(player) == GetPtrHash(entityPlayer) then -- Dead Tainted Lazarus exceptions if lazarusAdjust and EID.isRepentance then if player:GetPlayerType() == 38 then -- Check for Birthright to make the ghostly form use the same base ID as the main form if player:GetMainTwin() then if GetPtrHash(player:GetMainTwin()) ~= GetPtrHash(player) then i = EID:getPlayerID(player:GetMainTwin()) end end -- Increase the ID by 666 if Dead Tainted Lazarus i = i + 666 end end return i end end return 0 end -- Get the current Language. Defaults to english if none is set. function EID:getLanguage() local lang = EID.Config["Language"] -- Reset invalid languages (like en_us_detailed) if EID.descriptions[lang] == nil then EID.Config["Language"] = "auto" lang = "auto" end if lang == "auto" then return Options and EID.LanguageMap[Options.Language] or "en_us" end return lang end function EID:AddToCollectiblesToCheckList(itemID) EID.collectiblesToCheck[itemID] = true end -- Add a specific entity to be ignored by EID. Set entitySubType to -1 in order to ignore all entities with this type+variant combi function EID:addIgnoredEntity(entityType, entityVariant, entitySubType) if entitySubType == -1 then EID.IgnoredEntities[entityType.."."..entityVariant] = true else EID.IgnoredEntities[entityType.."."..entityVariant.."."..entitySubType] = true end end -- Remove a specific entity from the ignored List of EID. function EID:removeIgnoredEntity(entityType, entityVariant, entitySubType) if entitySubType == -1 then EID.IgnoredEntities[entityType.."."..entityVariant] = nil else EID.IgnoredEntities[entityType.."."..entityVariant.."."..entitySubType] = nil end end -- Returns if this is a frame we should refresh our descriptions function EID:RefreshThisFrame() if EID.GameRenderCount % (60 / EID.Config["RefreshRate"]) == 0 then return true end return false end -- Returns true if any player is pressing the given button (you can also specify any of the input functions) function EID:PlayersActionPressed(button, inputFunc) inputFunc = inputFunc or Input.IsActionPressed for k,v in pairs(EID.controllerIndexes) do if inputFunc(button, k) then return true, v end end return false end function EID:replaceMarkupSize(description) if EID.Config["StatChangeIcons"] == false then description = string.gsub(description, "{{ArrowUp}} ({{[.-}}]+)", "{{ArrowUp}} ") description = string.gsub(description, "{{ArrowDown}} ({{[.-}}]+)", "{{ArrowDown}} ") -- Remove double spaces after markup removal description = string.gsub(description, " ", " ") description = string.gsub(description, "# ", "#") end if EID.Config["MarkupSize"] == "small" then for normal, small in pairs(EID.MarkupSizeMap) do description = string.gsub(description, normal, small) end elseif EID.Config["MarkupSize"] == "big" then for normal, small in pairs(EID.MarkupSizeMap) do description = string.gsub(description, small, normal) end end return description end -- Creates a table that contains all objects a transformation is associated with. EID.TransformationLookup = {} function EID:buildTransformationTables() EID.TransformationLookup = {} for entityString, transformationData in pairs(EID.EntityTransformations) do EID:alterTransformationLookup(entityString, transformationData, true) end -- legacy for subType, transformationData in pairs(__eidItemTransformations) do EID:alterTransformationLookup("5.100."..subType, transformationData, true) end -- add custom for entityString, transformationData in pairs(EID.CustomTransformAssignments) do EID:alterTransformationLookup(entityString, transformationData, true) end -- custom remove for entityString, transformationData in pairs(EID.CustomTransformRemovals) do EID:alterTransformationLookup(entityString, transformationData, nil) end end function EID:alterTransformationLookup(entityString, transformString, addToList) for transformation in string.gmatch(transformString, '([^,]+)') do if EID.TransformationLookup[transformation] == nil then EID.TransformationLookup[transformation] = {} end EID.TransformationLookup[transformation][entityString] = addToList end end -- Given a transformation identifier, itterate over every player and count the number of items they have which count towards that transformation EID.TransformationProgress = {} function EID:evaluateTransformationProgress(transformation) for i = 0, game:GetNumPlayers() - 1 do local player = Isaac.GetPlayer(i) local id = EID:getPlayerID(player, true) -- Dead Tainted Lazarus exception EID.TransformationProgress[id] = {} EID.TransformationProgress[id][transformation] = 0 local transformData = EID.TransformationData[transformation] if not EID.TransformationLookup[transformation] then return end if REPENTOGON and transformData and transformData.VanillaForm then -- REPENTOGON lets us ignore everything else for vanilla transformation progress EID.TransformationProgress[id][transformation] = player:GetPlayerFormCounter(transformData.VanillaForm) elseif transformData and transformData.VanillaForm and player:HasPlayerForm(transformData.VanillaForm) then EID.TransformationProgress[id][transformation] = transformData.NumNeeded or 3 else local pickupHistory = EID.PlayerItemInteractions[id].pickupHistory local pillsTable = {} if pickupHistory then for j = 1, #pickupHistory do if pickupHistory[j][1] == "pill" then local pillSubType = tostring(pickupHistory[j][3]) if not pillsTable[pillSubType] then pillsTable[pillSubType] = 0 end pillsTable[pillSubType] = pillsTable[pillSubType] + 1 -- collect pill SubType end end end local activesTable = EID.PlayerItemInteractions[id].actives for entityString, _ in pairs(EID.TransformationLookup[transformation]) do local eType, eVariant, eSubType = entityString:match("([^.]+).([^.]+).([^.]+)") if tonumber(eType) == EntityType.ENTITY_PICKUP then if tonumber(eVariant) == PickupVariant.PICKUP_COLLECTIBLE then local currentCount = EID.TransformationProgress[id][transformation] if activesTable[tostring(eSubType)] then EID.TransformationProgress[id][transformation] = EID.TransformationProgress[id][transformation] + activesTable[tostring(eSubType)] else local collCount = player:GetCollectibleNum(eSubType, true) if EID.PlayerItemInteractions[id].rerollItems then collCount = collCount - (EID.PlayerItemInteractions[id].rerollItems[tostring(eSubType)] or 0) end EID.TransformationProgress[id][transformation] = EID.TransformationProgress[id][transformation] + collCount -- Undo the Book of Virtues active item getting counted here if EID.isRepentance and tonumber(eSubType) == 584 and player:GetActiveItem() == 584 then EID.TransformationProgress[id][transformation] = EID.TransformationProgress[id][transformation] - 1 end end -- In AB+, only one copy of a given collectible is counted for trans if not EID.isRepentance and EID.TransformationProgress[id][transformation] > currentCount + 1 then EID.TransformationProgress[id][transformation] = currentCount + 1 end elseif tonumber(eVariant) == PickupVariant.PICKUP_TRINKET and player:HasTrinket(eSubType) then EID.TransformationProgress[id][transformation] = EID.TransformationProgress[id][transformation] + player:GetTrinketMultiplier(eSubType) elseif tonumber(eVariant) == PickupVariant.PICKUP_PILL then if pillsTable[tostring(eSubType)] then EID.TransformationProgress[id][transformation] = EID.TransformationProgress[id][transformation] + pillsTable[tostring(eSubType)] end end end end end end end -- Create a list of all grid entities in the room that have an EID description function EID:CheckCurrentRoomGridEntities() EID.CurrentRoomGridEntities = {} local room = game:GetRoom() for i = 1, room:GetGridSize(), 1 do local gridEntity = room:GetGridEntity(i) if gridEntity and EID:hasDescription(gridEntity) then EID.CurrentRoomGridEntities[i] = gridEntity end end end -- Workaround function to get the currently held pill of the players. Used to map Pill ID to pill color and vise versa EID.PlayerHeldPill = {} function EID:evaluateHeldPill() EID.PlayerHeldPill = {} for i = 0, game:GetNumPlayers() - 1 do local player = Isaac.GetPlayer(i) EID.PlayerHeldPill[i] = player:GetPill(0) end end -- Watch for a player's queued item (holding an item over their head) to track active item touches -- Used for Transformation Progress and for tracking Recently Touched Items EID.PlayerItemInteractions = {} EID.RecentlyTouchedItems = {} local hadQueuedItem = {} function EID:evaluateQueuedItems() for i = 0, game:GetNumPlayers() - 1 do local player = Isaac.GetPlayer(i) local id = EID:getPlayerID(player, true) EID:InitItemInteractionIfAbsent(id) if player.QueuedItem then -- Refresh our descriptions and grid entity list upon a queued passive item being added to a player if not player.QueuedItem.Item and hadQueuedItem[id] then EID.ForceRefreshCache = true EID:CheckCurrentRoomGridEntities() end hadQueuedItem[id] = player.QueuedItem.Item ~= nil if EID.PlayerItemInteractions[id].LastTouch + 45 >= game:GetFrameCount() and player.QueuedItem.Item then return else EID.PlayerItemInteractions[id].LastTouch = 0 end if not player.QueuedItem.Touched and player.QueuedItem.Item then EID.PlayerItemInteractions[id].LastTouch = game:GetFrameCount() local itemIDStr = tostring(player.QueuedItem.Item.ID) -- Add touched active items to our transformation progress table if player.QueuedItem.Item.Type == ItemType.ITEM_ACTIVE then -- A new active item was touched; initiate its touch count to 0 for all players -- (Fixes co-op bugs, compared to only initiating it for the toucher) EID:InitActiveItemInteraction(itemIDStr) local activesTable = EID.PlayerItemInteractions[id].actives EID.ForceRefreshCache = true activesTable[itemIDStr] = activesTable[itemIDStr] + 1 elseif player.QueuedItem.Item.Type ~= ItemType.ITEM_TRINKET then -- In AB+, Halo of Flies is counted as an active item due to inaccuracy with GetCollectibleNum if (not EID.isRepentance and itemIDStr == "10") then EID.PlayerItemInteractions[id].actives[itemIDStr] = EID.PlayerItemInteractions[id].actives[itemIDStr] + 1 end -- Put non-active item pickups into the recent item list, for printing in the Item Reminder local playerID = i if EID.isRepentance and player:GetPlayerType() == 38 then playerID = playerID + 666 end table.insert(EID.RecentlyTouchedItems[playerID], player.QueuedItem.Item.ID) EID:ResetItemReminderSelectedItems("Passives") end end end end end -- if the player ItemInteraction table doesnt exist, create it with its init values function EID:InitItemInteractionIfAbsent(playerID) playerID = playerID % 666 -- dead tainted lazarus exception if not EID.PlayerItemInteractions[playerID] then EID.PlayerItemInteractions[playerID] = { LastTouch = 0, actives = {}, pickupHistory = {}, rerollItems = {} } -- in AB+, initiate Halo of Flies as an active item (collectible ID 10 is used for ALL Pretty Flies) if not EID.isRepentance then EID.PlayerItemInteractions[playerID].actives["10"] = 0 end end if not EID.PlayerItemInteractions[playerID+666] then EID.PlayerItemInteractions[playerID+666] = { LastTouch = 0, actives = {}, pickupHistory = {}, rerollItems = {} } end EID.RecentlyTouchedItems[playerID] = EID.RecentlyTouchedItems[playerID] or {} EID.RecentlyTouchedItems[playerID+666] = EID.RecentlyTouchedItems[playerID+666] or {} end -- initialize a touched active item to 0 count for all players -- (Fixes co-op bugs, compared to only initiating it for the toucher) function EID:InitActiveItemInteraction(itemIDStr) for playerID = 0, game:GetNumPlayers() - 1 do EID:InitItemInteractionIfAbsent(playerID) EID.PlayerItemInteractions[playerID].actives[itemIDStr] = EID.PlayerItemInteractions[playerID].actives[itemIDStr] or 0 EID.PlayerItemInteractions[playerID+666].actives[itemIDStr] = EID.PlayerItemInteractions[playerID+666].actives[itemIDStr] or 0 end end -- Returns the quality of the described entity function EID:getObjectQuality(descObj) if EID.isRepentance and descObj.ObjType == 5 and descObj.ObjVariant == 100 and EID.itemConfig:GetCollectible(tonumber(descObj.ObjSubType)) then return tonumber(EID.itemConfig:GetCollectible(tonumber(descObj.ObjSubType)).Quality) end end -- Returns the Inline Icon definition object for a given object. EID.ObjectIcon = {} function EID:getObjectIcon(descObj) -- custom object icon if EID.ObjectIcon[descObj.ObjType.."."..descObj.ObjVariant.."."..descObj.ObjSubType] then return EID.ObjectIcon[descObj.ObjType.."."..descObj.ObjVariant.."."..descObj.ObjSubType] end if descObj.ObjType == 5 then if descObj.ObjVariant == 100 then return EID:createItemIconObject("Collectible" .. descObj.ObjSubType) elseif descObj.ObjVariant == 350 then return EID:createItemIconObject("Trinket" .. descObj.ObjSubType) elseif descObj.ObjVariant == 300 then return EID:createItemIconObject("Card" .. descObj.ObjSubType) elseif descObj.ObjVariant == 70 then if descObj.ObjSubType >= 2049 then return EID:createItemIconObject("Pill" .. (descObj.ObjSubType - 2048)) end return EID:createItemIconObject("Pill" .. descObj.ObjSubType) end end end -- Adds an EID Icon to an Object function EID:AddIconToObject(eType, eVariant, eSubType, iconName) EID.ObjectIcon[eType.."."..eVariant.."."..eSubType] = EID.InlineIcons[iconName] end -- Set a pilleffect to be permanently unidentifyable by EID function EID:SetPillEffectUnidentifyable(pillEffectID, isUnidentifyable) EID.UnidentifyablePillEffects[pillEffectID + 1] = isUnidentifyable or nil end -- Add pickup usage to history of pickups used by the player function EID:AddPickupToHistory(pickupType, effectID, player, useFlags, pillColorID) -- don't add mimiced or noannouncer cards/pills to Echo Chamber history local allowEchoChamber = true if EID.isRepentance and (useFlags & UseFlag.USE_MIMIC == UseFlag.USE_MIMIC or useFlags & UseFlag.USE_NOANNOUNCER == UseFlag.USE_NOANNOUNCER) then allowEchoChamber = false end local playerID = EID:getPlayerID(player, true) EID:InitItemInteractionIfAbsent(playerID) local historyTable = EID.PlayerItemInteractions[playerID].pickupHistory -- pickupType = ["pill","card"], pillColorID, effectID, hadEchoChamberWhenUsed table.insert(historyTable, 1, {pickupType, pillColorID, effectID, EID.isRepentance and player:HasCollectible(700) and allowEchoChamber}) end EID.WildCardEffects = {} EID.WildCardPillColor = {} EID.TemporaryWildCardEffects = {} -- resets every frame function EID:TrackWildCardEffects(effectID, player, pillColor) local playerID = EID:getPlayerID(player, true) -- ? Card exception; both it and the active it used are ignored by Wild Card if effectID == "5.300.48" then -- Blank Card using ? Card exception; Blank Card being used is still seen if EID.TemporaryWildCardEffects[playerID] ~= "5.100.286" then EID.TemporaryWildCardEffects[playerID] = nil end else EID.TemporaryWildCardEffects[playerID] = effectID EID.WildCardPillColor[playerID] = pillColor -- doesn't matter that much so just set it here end end function EID:UpdateWildCardEffects() for k,v in pairs(EID.TemporaryWildCardEffects) do EID.WildCardEffects[k] = v end EID.TemporaryWildCardEffects = {} end -- Render a sprite of an entity function EID:RenderEntity(entity, sprite, position) if entity.Type == 5 and entity.Variant == 100 then sprite:RenderLayer(1, position, nullVector, nullVector) elseif entity.Type == 6 and entity.Variant == 16 then -- Crane Game sprite:RenderLayer(2, position, nullVector, nullVector) else sprite:Render(position, nullVector, nullVector) end end -- Tries to get the Vanilla transformations of modded items based on Tags function EID:GetTransformationsOfModdedItems() if not EID.isRepentance then return end local numCollectibles = EID:GetMaxCollectibleID() for i = 733, numCollectibles, 1 do EID:tryAutodetectTransformationsCollectible(i) end end -- Collects items that the player got after using D4 item function EID:CollectRerolledItemsOfPlayer(player) if maxCollectibleID == nil then maxCollectibleID = EID:GetMaxCollectibleID() end local playerID = EID:getPlayerID(player, true) EID.PlayerItemInteractions[playerID].rerollItems = {} for i = 1, maxCollectibleID do local count = player:GetCollectibleNum(i, true) if count > 0 then EID.PlayerItemInteractions[playerID].rerollItems[tostring(i)] = count end end end -- Returns true if a given entity is a grid entity function EID:IsGridEntity(entity) return entity.Type == nil end -- Returns true, if the given entity is a valid game object and actually is something useful to process. Sometimes the game sends weird shit so this function is needed to catch bullshit function EID:EntitySanityCheck(entity) return entity ~= nil and type(entity) == "userdata" end -- returns true if the given pill color was used at least once in this game function EID:WasPillUsed(pillColor) return EID.UsedPillColors[tostring(pillColor)] ~= nil end -- returns the name of the given entity ---@diagnostic disable-next-line: duplicate-set-field function EID:GetEntityXMLName(Type, Variant, SubType) return EID.XMLEntityNames[Type.."."..Variant] or EID.XMLEntityNames[Type.."."..Variant.."."..SubType] end ---@diagnostic disable-next-line: duplicate-set-field function EID:GetEntityXMLNameByString(tvsString) local Type, Var, Sub = EID:SplitTVS(tvsString) return EID:GetEntityXMLName(Type, Var, Sub) end -- Get an item's RNG seed. We have no use for the RNG object itself because every other function it can do will advance the item's RNG, altering the game state function EID:GetItemSeed(player, id, variant) if player == nil then return game:GetSeeds():GetStartSeed() elseif variant == nil or variant == 100 then return player:GetCollectibleRNG(id):GetSeed() elseif variant == 350 then return player:GetTrinketRNG(id):GetSeed() elseif variant == 300 then return player:GetCardRNG(id):GetSeed() elseif variant == 70 then return player:GetPillRNG(id):GetSeed() end end local variantToName = { [70] = "Pill", [100] = "Collectible", [300] = "Card", [350] = "Trinket" } function EID:GetIconNameByVariant(variant) return variantToName[variant] end function EID:GetIconStringByDescriptionObject(descObj) if descObj and descObj.Icon then if type(descObj.Icon) == "table" then local iconName = EID:GetIconNameByVariant(descObj.ObjVariant) if iconName == "Card" or iconName == "Pill" then return "{{" .. iconName .. (descObj.Icon[2] + 1) .. "}}" end return "{{" .. iconName .. descObj.Icon[2] .. "}}" elseif type(descObj.Icon) == "string" then return descObj.Icon end end return "{{Blank}}" end -- returns the markup for a given player ID or type. If no icon was found, returns the customTransformation icon markup function EID:GetPlayerIcon(playerID, altFallback) local fallback = altFallback or "{{CustomTransformation}}" return EID:getIcon("Player" .. playerID) ~= EID.InlineIcons["ERROR"] and "{{Player" .. playerID .. "}}" or fallback end -- returns a list of all passive item ids local passiveItems = nil -- cache of all passive item ids function EID:GetAllPassiveItems() if passiveItems then return passiveItems end passiveItems = {} for i = 1, EID:GetMaxCollectibleID() do local config = EID.itemConfig:GetCollectible(i) if config ~= nil and (config.Type == ItemType.ITEM_PASSIVE or config.Type == ItemType.ITEM_FAMILIAR) then table.insert(passiveItems, i) end end end local activeItems = nil -- cache of all active item ids function EID:GetAllActiveItems() if activeItems then return activeItems end activeItems = {} for i = 1, EID:GetMaxCollectibleID() do local config = EID.itemConfig:GetCollectible(i) if config ~= nil and (config.Type == ItemType.ITEM_ACTIVE) then table.insert(activeItems, i) end end end -- Updates the EID.RecentlyTouchedItems table to include the players currently held passive items function EID:UpdateAllPlayerPassiveItems() local passives = EID:GetAllPassiveItems() local listUpdatedForPlayers = {} -- check if id is smaller max id, because numbers bigger a certain value can crash the game when calling HasCollectible() local maxCollID = EID:GetMaxCollectibleID() for i = 1, #EID.coopAllPlayers do local player = EID.coopAllPlayers[i] if player == nil then return listUpdatedForPlayers -- dont evaluate when bad data is present end local playerNum = EID:getPlayerID(player, true) -- remove items the player no longer has. reverse iteration to make deletion easier for index = #EID.RecentlyTouchedItems[playerNum], 1, -1 do local itemID = EID.RecentlyTouchedItems[playerNum][index] if itemID > maxCollID or not player:HasCollectible(itemID, true) then table.remove(EID.RecentlyTouchedItems[playerNum], index) listUpdatedForPlayers[i] = true -- If an item earlier than our oldest item is removed (e.g. Eve sacrificial altaring her Dead Bird), reduce it if index < EID.OldestItemIndex[playerNum] then EID.OldestItemIndex[playerNum] = EID.OldestItemIndex[playerNum] - 1 end end end -- add items the player did get with non-standard methods (Bag of Crafting, console command, item effects, etc...) for _, itemID in ipairs(passives) do if itemID <= maxCollID and player:HasCollectible(itemID, true) then local alreadyInList = false for _, heldItemID in ipairs(EID.RecentlyTouchedItems[playerNum]) do if itemID == heldItemID then alreadyInList = true break end end if not alreadyInList then table.insert(EID.RecentlyTouchedItems[playerNum], itemID) listUpdatedForPlayers[i] = true end end end -- super hacky Tainted Cain active items check, mostly for transformation progress -- Only adds up to one copy of any active, I can't think of a good way to recognize a craft of the same active twice. better than nothing if EID.isRepentance and player:GetPlayerType() == 23 then for j = 0, 1 do local itemID = tostring(player:GetActiveItem(j)) if itemID ~= "0" then EID:InitActiveItemInteraction(itemID) if EID.PlayerItemInteractions[playerNum].actives[itemID] == 0 then EID.PlayerItemInteractions[playerNum].actives[itemID] = 1 end end end end end return listUpdatedForPlayers end EID.OldestItemIndex = {} function EID:SetOldestItemIndex() for i = 0, game:GetNumPlayers() - 1 do EID.RecentlyTouchedItems[i] = EID.RecentlyTouchedItems[i] or {} if EID.OldestItemIndex[i] == nil then EID.OldestItemIndex[i] = #EID.RecentlyTouchedItems[i] + 1 end -- set up Dead Tainted Lazarus to be oldest slot 1 if EID.OldestItemIndex[i+666] == nil then EID.OldestItemIndex[i+666] = 1 end end end EID.GulpedTrinkets = {} -- Check for gulped trinkets that have been removed (e.g. perfection, walnut) function EID:UpdateAllPlayerTrinkets() for i = 1, #EID.coopAllPlayers do local player = EID.coopAllPlayers[i] if player == nil then return end local playerNum = EID:getPlayerID(player, true) if EID.GulpedTrinkets[playerNum] == nil then return end -- remove items the player no longer has. reverse iteration to make deletion easier for index = #EID.GulpedTrinkets[playerNum], 1, -1 do if not player:HasTrinket(EID.GulpedTrinkets[playerNum][index]) then table.remove(EID.GulpedTrinkets[playerNum], index) end end end end EID.WispsPerPlayer = {} -- This is automatically called shortly after main.lua sees a Lemegeton get used, and when the Item Reminder is opened function EID:UpdateAllPlayerLemegetonWisps() EID.WispsPerPlayer = {} for _, wisp in ipairs(Isaac.FindByType(3, 237, -1, true, false)) do local wispPlayer = wisp:ToFamiliar() and wisp:ToFamiliar().Player if wispPlayer then local playerNum = EID:getPlayerID(wispPlayer, true) EID.WispsPerPlayer[playerNum] = EID.WispsPerPlayer[playerNum] or {} table.insert(EID.WispsPerPlayer[playerNum], wisp) end end -- Sort wisps by age (newest first), and leave just the IDs for playerNum,wisps in pairs(EID.WispsPerPlayer) do table.sort(wisps, function(a, b) return a.FrameCount < b.FrameCount end) for i,v in ipairs(wisps) do wisps[i] = v.SubType end end end -- This table holds, for each pedestal in the room, a table of item IDs that have been on that pedestal, and timestamps of when they were first and last seen -- The Glitched Crown callback when describing the pedestal will sort by first timestamp, and delete entries with too old of a last timestamp (like after a reroll) EID.GlitchedCrownCheck = {} -- Watch pedestals for being a Glitched Crown style pedestal that flips between items too quickly to display descriptions for function EID:WatchForGlitchedCrown() if REPENTOGON then -- In REPENTOGON, always check even without Glitched Crown, allowing to check 5+ Soul of Isaac usage, or Everything Jar local curRoomIndex = game:GetLevel():GetCurrentRoomDesc().ListIndex EID.GlitchedCrownCheck[curRoomIndex] = EID.GlitchedCrownCheck[curRoomIndex] or {} for _, entity in ipairs(Isaac.FindByType(5, 100, -1, true, false)) do -- Use InitSeed and Index to prevent any Diplopia weirdness EID.GlitchedCrownCheck[curRoomIndex][entity.InitSeed..entity.Index] = EID.GlitchedCrownCheck[curRoomIndex][entity.InitSeed..entity.Index] or {} local pickup = entity:ToPickup() local cycle = pickup:GetCollectibleCycle() if #cycle > 1 then for i, subType in ipairs(cycle) do EID.GlitchedCrownCheck[curRoomIndex][entity.InitSeed..entity.Index][subType] = EID.GlitchedCrownCheck[curRoomIndex][entity.InitSeed..entity.Index][subType] or {EID.GameUpdateCount + i, EID.GameUpdateCount + i} -- update the last frame seen for the pedestal's current collectible ID EID.GlitchedCrownCheck[curRoomIndex][entity.InitSeed..entity.Index][subType][2] = EID.GameUpdateCount + i end end end else if not EID.collectiblesOwned[689] then return end local curRoomIndex = game:GetLevel():GetCurrentRoomDesc().ListIndex EID.GlitchedCrownCheck[curRoomIndex] = EID.GlitchedCrownCheck[curRoomIndex] or {} for _, entity in ipairs(Isaac.FindByType(5, 100, -1, true, false)) do -- Use InitSeed and Index to prevent any Diplopia weirdness EID.GlitchedCrownCheck[curRoomIndex][entity.InitSeed..entity.Index] = EID.GlitchedCrownCheck[curRoomIndex][entity.InitSeed..entity.Index] or {} -- Initialize the data about this pedestal showing its current item ID, if necessary -- in order to sort the items displayed, while also trashing items that haven't shown up in a while, keep both "initial frame seen" and "last frame seen" EID.GlitchedCrownCheck[curRoomIndex][entity.InitSeed..entity.Index][entity.SubType] = EID.GlitchedCrownCheck[curRoomIndex][entity.InitSeed..entity.Index][entity.SubType] or {EID.GameUpdateCount, EID.GameUpdateCount} -- update the last frame seen for the pedestal's current collectible ID EID.GlitchedCrownCheck[curRoomIndex][entity.InitSeed..entity.Index][entity.SubType][2] = EID.GameUpdateCount end end end -- Replaces Variable placeholders in string with a given value -- Example: "My {1} message" --> "My test message" -- varID can be omitted to replace {1} (or pass in a string table, to replace {1}, {2}, etc.) function EID:ReplaceVariableStr(str, varID, newString) if newString == nil then newString = varID varID = 1 end if type(str) ~= "string" or newString == nil then return str end if type(newString) == "table" then for i = 1, #newString do str = str:gsub("{"..i.."}", newString[i]) end return str else return str:gsub("{"..varID.."}", newString) end end -- deep table copy, copied from http://lua-users.org/wiki/CopyTable function EID:CopyTable(orig) local orig_type = type(orig) local copy if orig_type == 'table' then copy = {} for orig_key, orig_value in next, orig, nil do copy[EID:CopyTable(orig_key)] = EID:CopyTable(orig_value) end setmetatable(copy, EID:CopyTable(getmetatable(orig))) else -- number, string, boolean, etc copy = orig end return copy end -- super simple table concatenation: https://www.tutorialspoint.com/concatenation-of-tables-in-lua-programming function EID:ConcatTables(t1, t2) for i = 1, #t2 do t1[#t1 + 1] = t2[i] end return t1 end -- thing to fix find/replace pairs with hyphens (like "1-2") or pluses (like +1 Health) breaking because of special characters -- https://stackoverflow.com/questions/29072601/lua-string-gsub-with-a-hyphen function EID:SimpleReplace(str, what, with, count) what = string.gsub(what, "[%(%)%.%+%-%*%?%[%]%^%$%%]", "%%%1") -- escape pattern with = string.gsub(with, "[%%]", "%%%%") -- escape replacement return string.gsub(str, what, with, count) end -- simple decimal rounding, instead of just floor or ceil function EID:SimpleRound(num, dp) dp = dp or 2 local mult = 10^dp return math.floor(num * mult + 0.5)/mult end function EID:ArrayContains(t, value) for _,v in ipairs(t) do if v == value then return true end end return false end -- Find the closest player to the given entity function EID:ClosestPlayerTo(entity) local closestDist = 9999999 local closestPlayer = EID.player local numPlayers = game:GetNumPlayers() if EID.InsideItemReminder then return EID.ItemReminderPlayerEntity end if entity == nil or numPlayers == 1 then return closestPlayer end for i = 1, #EID.coopAllPlayers do local player = EID.coopAllPlayers[i] local dist = player.Position:Distance(entity.Position) if dist < closestDist then closestDist = dist closestPlayer = player end end return closestPlayer end -- Creates a description table for a given language to prevent outdated languages from breaking EID API functions for mods function EID:CreateDescriptionTableIfMissing(tableName, language) if language and not EID.descriptions[language] then EID.descriptions[language] = {} end if tableName and not EID.descriptions[language][tableName] then EID.descriptions[language][tableName] = {} end end -- returns true if the given pedestal-entity is hidden (questionmark sprite) function EID:IsItemHidden(entity) if EID:getEntityData(entity, "EID_DontHide") == true then return false end -- Repentogon check (does not account for curse of blind), -- with expection for alt path item descriptions when config option is enabled if REPENTOGON and entity:ToPickup():IsBlind() and not (not EID.Config["DisableOnAltPath"] and EID:IsAltChoice(entity)) then return true end if (EID.Config["DisableOnCurse"] and EID:hasCurseBlind() and not entity:ToPickup().Touched and not EID.isDeathCertRoom) or (EID.Config["HideUncollectedItemDescriptions"] and EID:requiredForCollectionPage(entity.SubType)) or (EID.Config["DisableOnAltPath"] and not entity:ToPickup().Touched and EID:IsAltChoice(entity)) or (EID.Config["DisableOnAprilFoolsChallenge"] and game.Challenge == Challenge.CHALLENGE_APRILS_FOOL) then return true end return false end