Files
External-Item-Descriptions/features/eid_api.lua
T

2564 lines
103 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 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
-- Handle Dice Room Floor
elseif descObj.ObjType == 1000 and descObj.ObjVariant == 76 then
return EID.InlineIcons["DiceFace" .. descObj.ObjSubType]
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