if EID and EID.Name then print("EID Error: Two instances of EID found! Please uninstall one of them!") return end -- If EID is already loaded, warn the user and dont load the second one. EID = RegisterMod("External Item Descriptions", 1) -- important variables EID.GameVersion = "ab+" EID.descriptions = {} -- Table that holds all translation strings EID.ItemNames = {} -- Table that holds all item names for each language EID.enableDebug = false local game = Game() EID.isRepentancePlus = REPENTANCE_PLUS or FontRenderSettings ~= nil -- Repentance+ adds FontRenderSettings() class. We use this to check if the DLC is enabled. V1.9.7.7 added REPENTANCE_PLUS variable EID.isRepentance = REPENTANCE or EID.isRepentancePlus -- REPENTANCE variable can be altered by any mod, so we save the value before anyone can alter it. V1.9.7.7 removed REPENTANCE variable, so we additionally check for Rep+ require("eid_config") EID.Config = EID.UserConfig EID.Config.Version = "3.2" -- note: changing this will reset everyone's settings to default! EID.ModVersion = 5.22 EID.ModVersionCommit = "21fc956" EID.DefaultConfig.Version = EID.Config.Version EID.isHidden = false EID.player = nil -- The primary Player Entity of Player 1 EID.players = {} -- Both Player Entities of Player 1 if applicable (includes Esau, T.Forgotten) EID.coopMainPlayers = {} -- The primary Player Entity for each controller being used EID.coopAllPlayers = {} -- Every Player Entity (includes Esau, T.Forgotten) EID.controllerIndexes = {} -- A table to map each controller index to their player number for coloring indicators EID.isMultiplayer = false -- Used to color P1's highlight/outline indicators (single player just uses white) EID.BoC = {} -- general variables EID.PositionModifiers = {} EID.DescModifiers = {} EID.UsedPosition = Vector(EID.Config["XPosition"], EID.Config["YPosition"]) EID.Scale = EID.Config["Size"] EID.isDisplaying = false EID.isDisplayingPermanent = false EID.permanentDisplayTextObj = nil EID.lastDescriptionEntity = nil EID.lineHeight = 11 EID.itemConfig = Isaac.GetItemConfig() EID.itemUnlockStates = {} EID.itemAvailableStates = {} EID.CraneItemType = {} EID.absorbedItems = {} EID.CollectedItems = {} EID.IgnoredEntities = {} EID.CurrentRoomGridEntities = {} EID.UnidentifyablePillEffects = {} -- List of pilleffects that are always unidentifyable EID.UsedPillColors = {} -- Colors of pills that have been eaten during this game local pathsChecked = {} local altPathItemChecked = {} local alwaysUseLocalMode = false -- set to true after drawing a non-local mode description this frame EID.ForceRefreshCache = false -- set to true to force-refresh descriptions, currently used for potential transformation text changes EID.holdTabPlayer = nil EID.holdTabCounter = 0 EID.DInfinityState = {} local forgottenDropTimer = 0 local preHourglassStatus = {} EID.GameUpdateCount = 0 EID.GameRenderCount = 0 -- Sprite inits EID.IconSprite = Sprite() EID.IconSprite:Load("gfx/eid_transform_icons.anm2", true) EID.InlineIconSprite = Sprite() EID.InlineIconSprite:Load("gfx/eid_inline_icons.anm2", true) EID.InlineIconSprite2 = Sprite() EID.InlineIconSprite2:Load("gfx/eid_inline_icons.anm2", true) EID.CardPillSprite = Sprite() EID.CardPillSprite:Load("gfx/eid_cardspills.anm2", true) EID.ItemSprite = Sprite() EID.ItemSprite:Load("gfx/005.100_collectible.anm2", true) EID.PlayerSprite = Sprite() EID.PlayerSprite:Load("gfx/eid_player_icons.anm2", true) local ArrowSprite = Sprite() ArrowSprite:Load("gfx/eid_transform_icons.anm2", true) ArrowSprite:Play("Arrow", false) EID.CursorSprite = Sprite() EID.CursorSprite:Load("gfx/eid_transform_icons.anm2", true) EID.CursorSprite:Play("Cursor") local hudBBSprite = Sprite() hudBBSprite:Load("gfx/eid_transform_icons.anm2", true) hudBBSprite:Play("boundingBox") local questionMarkSprite = Sprite() questionMarkSprite:Load("gfx/005.100_collectible.anm2",true) questionMarkSprite:ReplaceSpritesheet(1,"gfx/items/collectibles/questionmark.png") questionMarkSprite:LoadGraphics() EID.ModIndicator = { } -- Overwriting "RegisterMod" to track which mods are loading -- Useful to associate items to mods EID._currentMod = "" local OldRegisterMod = RegisterMod RegisterMod = function (modName, apiVersion, ...) EID._currentMod = modName EID.ModIndicator[modName] = { Name = modName, Icon = nil } return OldRegisterMod(modName, apiVersion, ...) end ------- Load all modules and other stuff ------ require("features.eid_api") require("features.eid_modular_descriptions") require("features.eid_modular_data_modifiers") require("features.eid_grid_descriptions") require("features.eid_language_manager") EID:InitializeLanguagePacks() pcall(require,"scripts.eid_savegames") require("features.eid_mcm") require("features.eid_data") if EID.isRepentancePlus then require("features.eid_xmldata_rep+") else require("features.eid_xmldata") end require("features.eid_conditionals") require("features.eid_modifiers") require("features.eid_holdmapdesc") require("features.eid_itemprediction") -- load Repentance features if EID.isRepentance then require("features.eid_bagofcrafting") require("features.eid_tmtrainer") end EID.LastRenderCallColor = EID:getTextColor() local nullVector = Vector(0,0) --------------------------------------------------------------------------- ------------------------------- Load Font --------------------------------- local modfolder ='external item descriptions_836319872' --release mod folder name --- Function to get the current mod path --- @return string The absolute path to the current mod function EID:GetCurrentModPath() if debug then return string.sub(debug.getinfo(EID.GetCurrentModPath).source,2) .. "/../" end --use some very hacky trickery to get the path to this mod local _, err = pcall(require, "") local _, basePathStart = string.find(err, "no file '", 1) local _, modPathStart = string.find(err, "no file '", basePathStart) local modPathEnd, _ = string.find(err, ".lua'", modPathStart) local modPath = string.sub(err, modPathStart+1, modPathEnd-1) modPath = string.gsub(modPath, "\\", "/") modPath = string.gsub(modPath, "//", "/") modPath = string.gsub(modPath, ":/", ":\\") return modPath end EID.modPath = EID:GetCurrentModPath() EID.font = Font() -- init font object EID:fixDefinedFont() local fontFile = EID.Config["FontType"] or "default" local success = EID:loadFont(EID.modPath .. "resources/font/eid_"..fontFile..".fnt") if not success then if EID.isRepentance then success = EID:loadFont("../mods/"..modfolder.."/resources/font/eid_"..fontFile..".fnt") if not success then EID:WriteErrorMsg("EID WAS NOT ABLE TO LOAD THE FONT!!!!!!!! Please contact the mod creator!\n") EID:WriteErrorMsg("File not found (absolute path): "..EID.modPath .. "resources/font/eid_"..fontFile..".fnt\n") EID:WriteErrorMsg("File not found (relative path): ../mods/"..modfolder.."/resources/font/eid_"..fontFile..".fnt") return else EID.modPath = "../mods/"..modfolder.."/" end else EID:WriteErrorMsg("EID WAS NOT ABLE TO LOAD THE FONT!!!!!!!! Please contact the mod creator!\n") EID:WriteErrorMsg("File does not exist: "..EID.modPath .. "resources/font/eid_"..fontFile..".fnt") return end end --- Function to reset all floor trackers when a new floor is entered function EID:onNewFloor() pathsChecked = {} if EID.isRepentance then EID.BoC.RoomQueries = {} EID.BoC.FloorQuery = {} EID.CraneItemType = {} EID.flipItemPositions = {} altPathItemChecked = {} end end EID:AddCallback(ModCallbacks.MC_POST_NEW_LEVEL, EID.onNewFloor) --- Function to check if a collectible pedestal is an alt path choice function EID:IsAltChoice(pickup) -- do not run this while Curse of the Blind is active, since this function is really just a "is collectible pedestal a red question mark" check if not EID.isRepentance or EID:hasCurseBlind() then return false end if altPathItemChecked[pickup.InitSeed] ~= nil then return altPathItemChecked[pickup.InitSeed] end if game:GetRoom():GetType() ~= RoomType.ROOM_TREASURE then altPathItemChecked[pickup.InitSeed] = false return false end local entitySprite = pickup:GetSprite() local name = entitySprite:GetAnimation() if name ~= "Idle" and name ~= "ShopIdle" then -- Collectible can be ignored. It's definitely not hidden altPathItemChecked[pickup.InitSeed] = false return false end questionMarkSprite:SetFrame(name,entitySprite:GetFrame()) -- Quickly check some points in entitySprite to not need to check the whole sprite -- We check the range from Y -40 to 10 in 3 pixel steps and also X -1 to 1. GetTexel() gets the color value of a sprite at a given location. the center of the sprite is here in the Pivot point of the sprite in the anm2 file. -- therefore we go negative 40 pixels up to read the sprite as it is on a pedestal. We also look 10 pixel down to make comparing shop items more accurate for i = -1,1,1 do for j = -40,10,3 do local qcolor = questionMarkSprite:GetTexel(Vector(i,j),nullVector,1,1) local ecolor = entitySprite:GetTexel(Vector(i,j),nullVector,1,1) if qcolor.Red ~= ecolor.Red or qcolor.Green ~= ecolor.Green or qcolor.Blue ~= ecolor.Blue then -- it is not same with question mark sprite altPathItemChecked[pickup.InitSeed] = false return false end end end altPathItemChecked[pickup.InitSeed] = true return true end --------------------------------------------------------------------------- -----------------Handle Crane Game & Flip Item Callbacks------------------- local initialItemNext = false local flipItemNext = false if EID.isRepentance then EID.flipItemPositions = {} EID.flipMaxIndex = -1 local lastGetItemResult = {nil, nil, nil, nil} -- itemID, Frame, gridIndex, InitSeed local lastFrameGridChecked = 0 --- Handle Flip Item positions and Crane Game item positions. Repentance Only function! ---@param selectedCollectible number ---@param itemPoolType number function EID:postGetCollectible(selectedCollectible, itemPoolType) -- Handle Crane Game if itemPoolType == ItemPoolType.POOL_CRANE_GAME then for _, crane in ipairs(Isaac.FindByType(6, 16, -1, true, false)) do if not crane:GetSprite():IsPlaying("Broken") then if not EID.CraneItemType[tostring(crane.InitSeed)] then EID.CraneItemType[tostring(crane.InitSeed)] = selectedCollectible break end end end end -- Handle Flip item -- PRE_ROOM_ENTITY_SPAWN sets us up to watch for the first POST_GET_COLLECTIBLE for this pedestal -- (Tainted Isaac and Glitched Crown cause additional calls that have to be ignored) -- POST_PICKUP_INIT occurs right before the Flip item is decided, so it sets us up to watch for the Flip item -- (it also watches for item rerolls to fill the new entity's GetData) -- POST_NEW_ROOM then handles putting the result in the entity's GetData local curFrame = Isaac.GetFrameCount() local curRoomIndex = game:GetLevel():GetCurrentRoomIndex() if curFrame == lastGetItemResult[2] then if initialItemNext then lastGetItemResult[1] = selectedCollectible elseif flipItemNext and lastGetItemResult[1] then EID.flipItemPositions[curRoomIndex] = EID.flipItemPositions[curRoomIndex] or {} EID.flipItemPositions[curRoomIndex][lastGetItemResult[4]] = {selectedCollectible, lastGetItemResult[3]} end end initialItemNext = false flipItemNext = false end EID:AddCallback(ModCallbacks.MC_POST_GET_COLLECTIBLE, EID.postGetCollectible) --- Pre Room Entity Spawn callback to handle Flip item and pedestal spawning. Repentance Only function! ---@param entityType any ---@param variant any ---@param subtype any ---@param gridIndex any function EID:preRoomEntitySpawn(entityType, variant, subtype, gridIndex) flipItemNext = false if entityType == 6 and variant == 14 then -- Inner Child pedestal lastGetItemResult = {688, Isaac.GetFrameCount(), gridIndex, nil} elseif entityType == 5 then lastGetItemResult = {nil, Isaac.GetFrameCount(), gridIndex, nil} -- Pedestals in need of a random item will call GET_COLLECTIBLE; fixed pedestals (Knife Piece 1) will not if subtype == 0 then initialItemNext = true else lastGetItemResult[1] = subtype end end end EID:AddCallback(ModCallbacks.MC_PRE_ROOM_ENTITY_SPAWN, EID.preRoomEntitySpawn) --- Post Pickup Init callback to handle Flip item initialization. Repentance Only function! ---@param entity any function EID:postPickupInitFlip(entity) initialItemNext = false flipItemNext = true lastGetItemResult[4] = entity.InitSeed local curRoomIndex = game:GetLevel():GetCurrentRoomIndex() local gridPos = game:GetRoom():GetGridIndex(entity.Position) -- Update a Flip item's init seed after D6 rerolls or using Flip (aka Grid Index didn't change, Init Seed did) if EID.flipItemPositions[curRoomIndex] and not EID.flipItemPositions[curRoomIndex][entity.InitSeed] then -- Check if any Flip pedestals have changed grid indexes (fixes bugs with Greed shops) if lastFrameGridChecked ~= Isaac.GetFrameCount() then EID:CheckFlipGridIndexes() end for k,v in pairs(EID.flipItemPositions[curRoomIndex]) do if v[2] == gridPos then EID.flipItemPositions[curRoomIndex][entity.InitSeed] = v EID.flipItemPositions[curRoomIndex][k] = nil break end end end -- Give this new entity its Flip Item data if possible local flipEntry = EID.flipItemPositions[curRoomIndex] and EID.flipItemPositions[curRoomIndex][entity.InitSeed] if flipEntry then EID:setEntityData(entity, "EID_FlipItemID", flipEntry[1]) end end EID:AddCallback(ModCallbacks.MC_POST_PICKUP_INIT, EID.postPickupInitFlip, PickupVariant.PICKUP_COLLECTIBLE) --- Check if a pedestal's index is valid for Flip items. Repentance Only function! ---@param entity any function EID:CheckPedestalIndex(entity) -- Only pedestals with indexes that were present at room load can be flip pedestals -- Fixes shop restock machines and Diplopia... mostly. At least while you're in the room. if EID:getEntityData(entity, "EID_FlipItemID") and entity.Index > EID.flipMaxIndex then local curRoomIndex = game:GetLevel():GetCurrentRoomIndex() local gridPos = game:GetRoom():GetGridIndex(entity.Position) local flipEntry = EID.flipItemPositions[curRoomIndex] and EID.flipItemPositions[curRoomIndex][entity.InitSeed] -- only wipe the data if the grid index matches (so Diplopia pedestals don't) if flipEntry and gridPos == flipEntry[2] then EID.flipItemPositions[curRoomIndex][entity.InitSeed] = nil end EID:setEntityData(entity, "EID_FlipItemID", nil) end end EID:AddCallback(ModCallbacks.MC_POST_PICKUP_UPDATE, EID.CheckPedestalIndex, PickupVariant.PICKUP_COLLECTIBLE) -- Before using Flip, swap all flippable pedestal's current item with the flip one (also, fix grid index if needed) -- Repentance only function! function EID:CheckFlipGridIndexes(collectibleType) -- also, reload our descriptions due to transformation progress changing upon Flip EID.ForceRefreshCache = true lastFrameGridChecked = Isaac.GetFrameCount() local curRoomIndex = game:GetLevel():GetCurrentRoomIndex() if EID.flipItemPositions[curRoomIndex] then local pedestals = Isaac.FindByType(5, 100, -1, true, false) for _, pedestal in ipairs(pedestals) do local flipEntry = EID.flipItemPositions[curRoomIndex][pedestal.InitSeed] if flipEntry then local gridPos = game:GetRoom():GetGridIndex(pedestal.Position) flipEntry[2] = gridPos if collectibleType == CollectibleType.COLLECTIBLE_FLIP then -- don't swap a flip shadow with an empty pedestal! if pedestal.SubType == 0 then EID.flipItemPositions[curRoomIndex][pedestal.InitSeed] = nil else flipEntry[1] = pedestal.SubType end end end end end end EID:AddCallback(ModCallbacks.MC_PRE_USE_ITEM, EID.CheckFlipGridIndexes, CollectibleType.COLLECTIBLE_FLIP) end -- MC_PRE_USE_ITEM callback to watch for Void absorbing active items function EID:CheckVoidAbsorbs(_, _, player) player = player or EID.player local playerID = EID:getPlayerID(player, true) EID.absorbedItems[tostring(playerID)] = EID.absorbedItems[tostring(playerID)] or {} -- Remove single use items from Void on use in Repentance if EID.isRepentance then for k,_ in pairs(EID.SingleUseCollectibles) do EID.absorbedItems[tostring(playerID)][tostring(k)] = nil end end for _,v in ipairs(EID:VoidRoomCheck()) do -- Don't include single use items in AB+ (they're immediately used and gone) if EID.isRepentance or not EID.SingleUseCollectibles[v] then EID.absorbedItems[tostring(playerID)][tostring(v)] = true end end EID.RecheckVoid = true end EID:AddCallback(ModCallbacks.MC_PRE_USE_ITEM, EID.CheckVoidAbsorbs, CollectibleType.COLLECTIBLE_VOID) function EID:HandleRenderingKeys() -- scale key must be handled before resetting to non-local mode EID:HandleScaleKey() if Input.IsButtonTriggered(EID.Config["HideKey"], 0) or (EID.player and Input.IsButtonTriggered(EID.Config["HideButton"], EID.player.ControllerIndex)) then EID.isHidden = not EID.isHidden end end --------------------------------------------------------------------------- --------------------------Handle Scale Shortcut---------------------------- local scaleMin = 0.1 local scaleMax = 2 local scaleSpeed = 0.01 -- scale size per frame local scaleToBigger = true EID.CurrentScaleType = "Size" -- Size or LocalModeSize; checked by EID:getTextPosition() to not apply modifiers in local mode local scaleHoldFrame = 0 function EID:HandleScaleKey() local scaleKey = EID.Config["SizeHotkey"] -- press and hold ScaleKey if Input.IsButtonPressed(scaleKey, 0) then if scaleHoldFrame > 60 then EID.MCM_OptionChanged = true local newScale if scaleToBigger then newScale = EID.Scale + scaleSpeed if newScale > scaleMax then scaleToBigger = false end else newScale = EID.Scale - scaleSpeed if newScale < scaleMin then scaleToBigger = true end end EID.Scale = newScale EID.Config[EID.CurrentScaleType] = newScale else scaleHoldFrame = scaleHoldFrame + 1 end end -- press ScaleKey if Input.IsButtonTriggered(scaleKey, 0) then EID.MCM_OptionChanged = true scaleHoldFrame = 0 local scale = EID.Scale -- switch between 1, 1.5 and 0.5 if math.abs(scale - 1) < 0.01 then scale = 1.5 elseif math.abs(scale - 1.5) < 0.01 then scale = 0.5 else scale = 1 end EID.Config[EID.CurrentScaleType] = scale EID.Scale = scale end end --------------------------------------------------------------------------- ---------------------------Printing Functions------------------------------ EID.previousDescs = {} EID.CachingDescription = false EID.CachedIcons = {} EID.CachedStrings = {} EID.CachedIndicators = {} EID.CachedRenderPoses = {} EID.descriptionsToPrint = {} EID.entitiesToPrint = {} --- Reset the description cache, used when options change or when printing descriptions function EID:ResetDescCache() EID.CachedIcons = {} EID.CachedStrings = {} EID.CachedRenderPoses = {} -- EID.CachedIndicators = {} EID.previousDescs = {} end --- Add a description to the print queue ---@param desc EID_DescObj | table -- The description object to add ---@param insertLoc integer | nil function EID:addDescriptionToPrint(desc, insertLoc) if desc.Entity and EID.entitiesToPrint[GetPtrHash(desc.Entity)] then return end if #EID.descriptionsToPrint == EID.Config["MaxDescriptionsToDisplay"] and not insertLoc then return end if insertLoc then table.insert(EID.descriptionsToPrint, insertLoc, desc) else table.insert(EID.descriptionsToPrint, desc) end -- if we've inserted a desc at the front of a full desc list, pop the last desc out of the table if #EID.descriptionsToPrint > EID.Config["MaxDescriptionsToDisplay"] then table.remove(EID.descriptionsToPrint) end if desc.Entity then EID.entitiesToPrint[GetPtrHash(desc.Entity)] = true end end --- Add a question mark description to the print queue ---@param entity Entity ---@param description string | nil function EID:addQuestionMarkDescription(entity, description) EID:addDescriptionToPrint({ Icon = EID.InlineIcons["QuestionMark"], Description = description or "", Entity = entity}) end local prevPrintFrame = 0 --- Print all descriptions that are currently in the description table --- @param useCached boolean | nil - If true, will use the cached descriptions instead of printing new ones function EID:printDescriptions(useCached) prevPrintFrame = EID.GameRenderCount EID.CachingDescription = false if not useCached then -- Test if we should print our cached descriptions or not -- Did an option change, or it's a different number of descriptions? if #EID.descriptionsToPrint ~= #EID.previousDescs or EID.OptionChanged or EID.MCM_OptionChanged then EID:printNewDescriptions() return end -- Do the description entities match, and the description texts match? for i,newDesc in ipairs(EID.descriptionsToPrint) do local oldDesc = EID.previousDescs[i] if newDesc.Description ~= oldDesc.Description or newDesc.Name ~= oldDesc.Name or (newDesc.Entity and oldDesc.Entity and GetPtrHash(newDesc.Entity) ~= GetPtrHash(oldDesc.Entity)) then EID:printNewDescriptions() return end end end if #EID.previousDescs > 0 then EID.isDisplaying = true end -- Print our cached descriptions for _,indicator in ipairs(EID.CachedIndicators) do EID:renderIndicator(indicator[1], indicator[2]) end for i,oldDesc in ipairs(EID.previousDescs) do EID:printDescription(oldDesc, i) end end --- Print all descriptions that are currently in the description table function EID:printNewDescriptions() EID.CachingDescription = true EID:ResetDescCache() for _,newDesc in ipairs(EID.descriptionsToPrint) do if newDesc.Description == "UnidentifiedPill" then if EID:renderUnidentifiedPill(newDesc.Entity) ~= false then table.insert(EID.previousDescs, newDesc) end elseif EID:printDescription(newDesc) ~= false then table.insert(EID.previousDescs, newDesc) end end EID.CachingDescription = false if #EID.previousDescs > 0 then EID.isDisplaying = true end end --- Prints a description to the screen ---@param desc EID_DescObj ---@param cachedID integer | nil -- If this is a cached description, pass the index of the cached description function EID:printDescription(desc, cachedID) EID:PositionLocalMode(desc.Entity) -- Do not print this description if it has to be drawn in the top-left and we've already drawn a top-left desc this frame if EID.CurrentScaleType == "Size" then if alwaysUseLocalMode then return else alwaysUseLocalMode = true end end EID.isDisplaying = true local renderPos = EID:getTextPosition() if cachedID then local localModeDiff = renderPos - EID.CachedRenderPoses[cachedID] -- Drawing our currently saved description for _,icon in ipairs(EID.CachedIcons[cachedID]) do -- set inline icon to specific frame if icon[6] >= 0 then icon[1]:SetFrame(icon[5], icon[6]) -- frame of -1 = it's an animation, let it play elseif not icon[1]:IsPlaying(icon[5]) or icon[1]:IsFinished(icon[5]) then icon[1]:Play(icon[5],true) else icon[1]:Update() end EID:renderIcon(icon[1], icon[2] + localModeDiff.X, icon[3] + localModeDiff.Y, icon[4]) end for _,str in ipairs(EID.CachedStrings[cachedID]) do -- check for func (Rainbow, Blink, Fade), reset the color's Alpha to EID.Config["Transparency"] if func if str[5] then str[4].Alpha = str[6] str[4] = str[5](str[4]) end EID.font:DrawStringScaledUTF8(str[1], str[2] + localModeDiff.X, str[3] + localModeDiff.Y, EID.Scale, EID.Scale, str[4], 0, false) end return else table.insert(EID.CachedIcons, {}) table.insert(EID.CachedStrings, {}) table.insert(EID.CachedRenderPoses, Vector(renderPos.X, renderPos.Y)) end local textScale = Vector(EID.Scale, EID.Scale) local offsetX = 0 EID.lineHeight = EID.Config["LineHeight"] if EID.Config["ShowItemIcon"] and desc.Icon then offsetX = offsetX + 14 EID:renderInlineIcons({{desc.Icon}}, renderPos.X - 3 * EID.Scale, renderPos.Y - 4 * EID.Scale) end --Display ItemType / Charge if EID.Config["ShowItemType"] and (desc.ItemType == ItemType.ITEM_ACTIVE or desc.ItemType == ItemType.ITEM_FAMILIAR) then local offsetY = 2 local itemTypeAnm2 = EID.ItemTypeAnm2Names[desc.ItemType] EID.IconSprite:Play(itemTypeAnm2) EID:renderIcon(EID.IconSprite, renderPos.X + offsetX * EID.Scale, renderPos.Y + offsetY * EID.Scale, nil, itemTypeAnm2 , 0) if desc.ItemType == ItemType.ITEM_ACTIVE then -- Display Charge offsetX = offsetX + 1 local anim = "numbers" local frameNum = desc.Charges or 0 if desc.ChargeType == (EID.isRepentance and ItemConfig.CHARGE_TIMED or 1) then anim = "Misc"; frameNum = 6 -- Timer Icon elseif desc.ChargeType == (EID.isRepentance and ItemConfig.CHARGE_SPECIAL or 2) then frameNum = 13 -- Question Mark Icon end EID.InlineIconSprite2:SetFrame(anim, frameNum) EID:renderIcon(EID.InlineIconSprite2, renderPos.X + offsetX * EID.Scale, renderPos.Y + offsetY * EID.Scale, nil, anim, frameNum) end offsetX = offsetX + 8 end --Display Itemname local curName = "" if EID.Config["ShowItemName"] and desc.Name then curName = desc.Name if EID.Config["TranslateItemName"] ~= 2 then local curLanguage = EID.Config["Language"] if EID:getLanguage() ~= EID.DefaultLanguageCode then EID.Config["Language"] = EID.DefaultLanguageCode local englishName = desc.PermanentTextEnglish or EID:getObjectName(desc.ObjType, desc.ObjVariant, desc.ObjSubType) EID.Config["Language"] = curLanguage if EID.Config["TranslateItemName"] == 1 then curName = englishName elseif EID.Config["TranslateItemName"] == 3 and curName ~= englishName then curName = curName.." ("..englishName..")" end end end end -- Display Entity ID if EID.Config["ShowObjectID"] and desc.ObjType and desc.ObjType > 0 then curName = curName.." {{ColorGray}}"..desc.ObjType.."."..desc.ObjVariant.."."..desc.ObjSubType end -- Display Quality if EID.Config["ShowQuality"] and desc.Quality then curName = curName.." - {{Quality"..desc.Quality.."}}" end -- Display Last Pool for Collectible for full reroll effects (icon) if desc.ItemPoolType and EID.Config["ShowItemPoolIcon"] then local itemConfig = EID.itemConfig:GetCollectible(desc.ObjSubType) if itemConfig:IsCollectible() and not itemConfig:HasTags(ItemConfig.TAG_QUEST) then if not EID.Config["ShowQuality"] then curName = curName.." - " end curName = curName..""..(EID.ItemPoolTypeToMarkup[desc.ItemPoolType] or "{{ItemPoolUnknown}}") end end -- Display the mod this item is from if desc.ModName then curName = curName .. EID:getModNameString(desc) end EID:renderString( curName, renderPos + (Vector(offsetX, -3) * EID.Scale), textScale, EID:getNameColor() ) renderPos.Y = renderPos.Y + EID.lineHeight * EID.Scale --Display Transformation if desc.Transformation and not (desc.Transformation == "0" or desc.Transformation == "") then for transform in string.gmatch(desc.Transformation, "([^,]+)") do --have a blank sprite info table if we aren't displaying it local transformSprite = EID.Config["TransformationIcons"] and EID:getTransformationIcon(transform) or {} local transformLineHeight = EID.lineHeight if EID.Config["TransformationIcons"] then transformLineHeight = math.max(EID.lineHeight, transformSprite[4]) EID:renderInlineIcons({{transformSprite}}, renderPos.X, renderPos.Y) end if EID.Config["TransformationText"] or EID.Config["TransformationProgress"] then local transformationName = "" if EID.Config["TransformationText"] then transformationName = EID:getTransformationName(transform) end if EID.Config["TransformationProgress"] then EID:evaluateTransformationProgress(transform) transformationName = transformationName .. " " for _, player in ipairs(EID.coopAllPlayers) do local playerType = player:GetPlayerType() if playerType ~= PlayerType.PLAYER_THESOUL_B and player:GetBabySkin() == -1 then if #EID.coopAllPlayers > 1 then transformationName = transformationName .. EID:GetPlayerIcon(playerType) end local numCollected = EID.TransformationProgress[EID:getPlayerID(player, true)] and EID.TransformationProgress[EID:getPlayerID(player, true)][transform] or 0 local numMax = EID.TransformationData[transform] and EID.TransformationData[transform].NumNeeded or 3 transformationName = transformationName.."("..numCollected.."/"..numMax..") " end end end local iconWidth = transformSprite[3] or -1 local iconHeight = transformSprite[4] or -1 local textOffsetY = math.min(0, (iconHeight - 9)) / 4 EID:renderString( transformationName, renderPos + (Vector(iconWidth + 4, textOffsetY)* EID.Scale), textScale, EID:getTransformationColor() ) end if (EID.Config["TransformationIcons"] or EID.Config["TransformationText"] or EID.Config["TransformationProgress"]) then renderPos.Y = renderPos.Y + transformLineHeight * EID.Scale end end end -- Display Possible Pool for Collectible if EID.isRepentance and EID.Config["ShowContainItemPool"] and (desc.ObjType == 5 and desc.ObjVariant == 100 and desc.ObjSubType ~= nil) then local itemConfig = EID.itemConfig:GetCollectible(desc.ObjSubType) if itemConfig:IsCollectible() then local pools = EID:GetPoolsForCollectible(desc.ObjSubType) if pools and #pools > 0 then local poolName = "{{ItemPool}} {{NoLB}}" for _, pool in ipairs(pools) do if EID.ItemPoolTypeToMarkup[pool] then poolName = poolName .. "" .. EID.ItemPoolTypeToMarkup[pool] end end renderPos = EID:printBulletPoints(poolName, renderPos, desc.IgnoreBulletPointIconConfig) end end end -- Display Last Pool for Collectible for full reroll effects (name) if desc.ItemPoolType and not EID.InsideItemReminder and EID.Config["ShowItemPoolText"] then local itemConfig = EID.itemConfig:GetCollectible(desc.ObjSubType) if itemConfig:IsCollectible() and not itemConfig:HasTags(ItemConfig.TAG_QUEST) then local lastPool = desc.ItemPoolType local poolName = "" local poolDescPrepend = EID:getDescriptionEntry("itemPoolFor") local poolDescTable = EID:getDescriptionEntry("itemPoolNames") local poolDescTableEng = EID:getDescriptionEntryEnglish("itemPoolNames") poolName = "{{RolledItemPool}} {{NoLB}}{{"..EID.Config["ItemPoolTextColor"].."}}"..poolDescPrepend..""..(EID.ItemPoolTypeToMarkup[lastPool] or "{{ItemPoolUnknown}}")..(poolDescTable[lastPool] or poolDescTableEng[lastPool] or "Modded item pool") .. "{{CR}}#" renderPos = EID:printBulletPoints(poolName, renderPos, desc.IgnoreBulletPointIconConfig) end end if EID.Config["ShowItemDescription"] then EID:printBulletPoints(desc.Description, renderPos, desc.IgnoreBulletPointIconConfig) end end --- Prints bullet points from a description string ---@param description string - The description string to print ---@param renderPos Vector - The position to render the bullet points at ---@param ignoreBPConfig boolean - If true, will ignore the bullet point icon configuration function EID:printBulletPoints(description, renderPos, ignoreBPConfig) local textboxWidth = tonumber(EID.Config["TextboxWidth"]) local textScale = Vector(EID.Scale, EID.Scale) local textOffset = Vector(12 * EID.Scale, 0) -- offset of the text to the bullet point local bulletpointOffset = Vector(-3 * EID.Scale, 0) -- offset of bulletpoint icons relative to the default bulletpoint description = EID:replaceNameMarkupStrings(description) description = EID:replaceShortMarkupStrings(description) description = EID:replaceMarkupSize(description) for line in string.gmatch(description, "([^#]+)") do local editedLine, numIndentations = EID:handleTextIndentation(line) local formatedLines = EID:fitTextToWidth(editedLine, textboxWidth, EID.BreakUtf8CharsLanguage[EID:getLanguage()]) local textColor = EID:getTextColor() local indentOffset = textOffset * numIndentations for i, lineToPrint in ipairs(formatedLines) do -- render bulletpoint if i == 1 then local bpIcon, rejectedIcon = EID:handleBulletpointIcon(lineToPrint, ignoreBPConfig) if EID:getIcon(bpIcon) ~= EID.InlineIcons["ERROR"] then lineToPrint = string.gsub(lineToPrint, bpIcon, "", 1) textColor = EID:renderString(bpIcon, renderPos + bulletpointOffset + indentOffset, textScale , textColor, true) else if rejectedIcon then lineToPrint = string.gsub(lineToPrint, rejectedIcon, "", 1) end textColor = EID:renderString(bpIcon, renderPos + indentOffset, textScale , textColor) end EID.LastRenderCallColor = EID:copyKColor(textColor) -- Save line start Color for eventual Color Reset call end -- Remove leading spaces lineToPrint = lineToPrint:match('^%s*(.*)') -- render text textColor = EID:renderString(lineToPrint, renderPos + textOffset + indentOffset, textScale, textColor) renderPos.Y = renderPos.Y + EID.lineHeight * EID.Scale end end return renderPos end --------------------------------------------------------------------------- ----------------------------Handle New Room-------------------------------- EID.isMirrorRoom = false EID.isDeathCertRoom = false if EID.isRepentance then --- Handle Flip Item in New Room. Repentance only function! function EID:AssignFlipItems() EID.flipMaxIndex = -1 local curRoomIndex = game:GetLevel():GetCurrentRoomIndex() if EID.flipItemPositions[curRoomIndex] then local pedestals = Isaac.FindByType(5, 100, -1, true, false) for _, pedestal in ipairs(pedestals) do local flipEntry = EID.flipItemPositions[curRoomIndex][pedestal.InitSeed] if flipEntry then if pedestal.Index > EID.flipMaxIndex then EID.flipMaxIndex = pedestal.Index end EID:setEntityData(pedestal, "EID_FlipItemID", flipEntry[1]) end end end end --- Handle MC_POST_NEW_ROOM for Repentance features function EID:onNewRoomRep() local level = game:GetLevel() EID.isMirrorRoom = level:GetCurrentRoom():IsMirrorWorld() EID.isDeathCertRoom = EID:GetDimension(level) == 2 EID:BOCHandleCurseOfMaze() -- Handle Flip Item initialItemNext = false flipItemNext = false EID:AssignFlipItems() end EID:AddCallback(ModCallbacks.MC_POST_NEW_ROOM, EID.onNewRoomRep) end --- MC_POST_NEW_ROOM callback to handle new rooms, mostly Glowing Hour Glass rewinding and Grid Entities function EID:onNewRoom() EID:CheckCurrentRoomGridEntities() -- Store values here to be rewound after using Glowing Hour Glass preHourglassStatus = {} preHourglassStatus.absorbedItems = EID:CopyTable(EID.absorbedItems) preHourglassStatus.PlayerItemInteractions = EID:CopyTable(EID.PlayerItemInteractions) if EID.isRepentance then preHourglassStatus.WildCardEffects = EID:CopyTable(EID.WildCardEffects) preHourglassStatus.WildCardPillColor = EID:CopyTable(EID.WildCardPillColor) preHourglassStatus.DInfinityState = EID:CopyTable(EID.DInfinityState) preHourglassStatus.BagItems = EID:CopyTable(EID.BoC.BagItems) preHourglassStatus.RoomQueries = EID:CopyTable(EID.BoC.RoomQueries) end end EID:AddCallback(ModCallbacks.MC_POST_NEW_ROOM, EID.onNewRoom) --------------------------------------------------------------------------- ---------------------------Handle Rendering-------------------------------- -- RGB colors for each player's highlights (Red, Blue, Yellow, Green) local playerRGB = { {1,0.6,0.6}, {0.5,0.75,1}, {0.9, 0.9, 0.5}, {0.5,1,0.75} } --- Render an unidentified pill description ---@param entity Entity function EID:renderUnidentifiedPill(entity) EID:PositionLocalMode(entity) if EID.CurrentScaleType == "Size" then if alwaysUseLocalMode then return else alwaysUseLocalMode = true end end EID.isDisplaying = true local pillColor = entity.SubType if pillColor >= 2049 then pillColor = pillColor - 2048 end local renderPos = EID:getTextPosition() local textScale = Vector(EID.Scale, EID.Scale) local offsetX = 0 if EID.CachingDescription then table.insert(EID.CachedStrings, {}) table.insert(EID.CachedIcons, {}) table.insert(EID.CachedRenderPoses, Vector(renderPos.X, renderPos.Y)) end local descriptionObj = EID:getDescriptionObj(entity.Type, entity.Variant, entity.SubType, entity, false) descriptionObj.Description = "" descriptionObj.ShowWhenUnidentified = false descriptionObj = EID:applyDescriptionModifier(descriptionObj, math.mininteger) if EID.Config["ShowItemIcon"] and descriptionObj.Icon then offsetX = offsetX + 14 EID:renderInlineIcons({{descriptionObj.Icon}}, renderPos.X - 3 * EID.Scale, renderPos.Y - 4 * EID.Scale) EID:renderInlineIcons({{EID.InlineIcons["SecretRoom"]}}, renderPos.X + 1 * EID.Scale, renderPos.Y - 4 * EID.Scale) end EID:renderString( EID:getDescriptionEntry("unidentifiedPill"), renderPos + (Vector(offsetX, -3) * EID.Scale), textScale, EID:getErrorColor() ) if EID.Config["ShowItemDescription"] and descriptionObj.ShowWhenUnidentified then renderPos.Y = renderPos.Y + EID.lineHeight * EID.Scale EID:printBulletPoints(descriptionObj.Description, renderPos, descriptionObj.IgnoreBulletPointIconConfig) end end --- Render an indicator for an entity, such as a highlight or arrow ---@param entity Entity ---@param playerNum integer - The player number to render the indicator for. Controls the arrow color function EID:renderIndicator(entity, playerNum) if EID.Config["Indicator"] == "none" then return end local repDiv = 1 local entityPos = Isaac.WorldToScreen(entity.PositionOffset and entity.Position + entity.PositionOffset or entity.Position) local ArrowOffset = Vector(0, -35) if entity.Variant == 100 and not entity:ToPickup():IsShopItem() then ArrowOffset = Vector(0, -62) end local arrowPos = Isaac.WorldToScreen(entity.Position + ArrowOffset) entityPos = entityPos + (EID:getEntityData(entity, "EID_RenderOffset") or Vector(0,0)) -- Move highlights a bit to fit onto the alt Item layout of Flip / Tainted Laz if EID.isRepentance and not EID:IsGridEntity(entity) then if entity.Variant == 100 and EID:PlayersHaveCollectible(CollectibleType.COLLECTIBLE_FLIP) and EID:getEntityData(entity, "EID_FlipItemID") then entityPos = entityPos + Vector(2.5,2.5) end end local sprite = nil if not EID:IsGridEntity(entity) then sprite = entity:GetSprite() end if EID.isRepentance then repDiv = 255 if EID.isMirrorRoom then local screenCenter = EID:getScreenSize()/2 entityPos.X = entityPos.X - (entityPos-screenCenter).X * 2 arrowPos.X = arrowPos.X - (arrowPos-screenCenter).X * 2 if sprite then sprite.FlipX = true end end end -- Don't apply sprite.Color changes to Effects (Dice Floors, Card Reading Portals), use Arrow instead if EID.Config["Indicator"] == "arrow" or entity.Type == 1000 or sprite == nil then ArrowSprite:RenderLayer(playerNum-1, arrowPos, nullVector, nullVector) else local colorMult = {1,1,1} local framecount = entity.FrameCount or game:GetFrameCount() if EID.isMultiplayer then colorMult = playerRGB[playerNum] end if EID.Config["Indicator"] == "blink" then local c = 255 - math.floor(255 * ((framecount % 40) / 40)) local r, g, b = math.floor(c*colorMult[1]), math.floor(c*colorMult[2]), math.floor(c*colorMult[3]) sprite.Color = Color(1, 1, 1, 1, r/repDiv, g/repDiv, b/repDiv) EID:RenderEntity(entity, sprite, entityPos) sprite.Color = Color(1, 1, 1, 1, 0, 0, 0) else if EID.Config["Indicator"] == "highlight" then local c = 255 - math.floor(255 * ((framecount % 40) / 40)) local r, g, b = math.floor(c*colorMult[1]), math.floor(c*colorMult[2]), math.floor(c*colorMult[3]) sprite.Color = Color(1, 1, 1, 1, r/repDiv, g/repDiv, b/repDiv) elseif EID.Config["Indicator"] == "border" then local c = 255 local r, g, b = math.floor(c*colorMult[1]), math.floor(c*colorMult[2]), math.floor(c*colorMult[3]) sprite.Color = Color(1, 1, 1, 1, r/repDiv, g/repDiv, b/repDiv) end EID:RenderEntity(entity, sprite, entityPos + Vector(0, 1)) EID:RenderEntity(entity, sprite, entityPos + Vector(0, -1)) EID:RenderEntity(entity, sprite, entityPos + Vector(1, 0)) EID:RenderEntity(entity, sprite, entityPos + Vector(-1, 0)) sprite.Color = Color(1, 1, 1, 1, 0, 0, 0) EID:RenderEntity(entity, sprite, entityPos) end end if EID.isMirrorRoom then if sprite then sprite.FlipX = false end end end --- Position the description box in Local Mode, next to the item or entity ---@param entity Entity ---@diagnostic disable-next-line: duplicate-set-field function EID:PositionLocalMode(entity) -- don't use Local Mode for descriptions without an entity (or dice floors) if (EID.Config["DisplayMode"] == "local" or alwaysUseLocalMode) and entity and entity.Variant ~= EffectVariant.DICE_FLOOR then EID.Scale = EID.Config["LocalModeSize"] EID.CurrentScaleType = "LocalModeSize" local textBoxWidth = EID.Config["LocalModeCentered"] and tonumber(EID.Config["TextboxWidth"])/2 * EID.Scale or -30 local textPosOffset = Vector(-textBoxWidth, 0) local additiveOffset = EID.LocalModePositionOffset.Default for _, offsetFunc in pairs(EID.LocalModePositionOffset) do if type(offsetFunc) == "function" then additiveOffset = offsetFunc(entity) or additiveOffset break end end textPosOffset = textPosOffset + additiveOffset EID:alterTextPos(Isaac.WorldToScreen(entity.Position + textPosOffset)) if EID.isMirrorRoom then EID:alterTextPos(Isaac.WorldToScreen(entity.Position + textPosOffset * Vector(-1,0))) local screenCenter = EID:getScreenSize()/2 EID.UsedPosition.X = EID.UsedPosition.X - (EID.UsedPosition-screenCenter).X * 2 end else EID.Scale = EID.Config["Size"] EID.CurrentScaleType = "Size" EID.UsedPosition = Vector(EID.Config["XPosition"], EID.Config["YPosition"]) end end --- Render HUD location indicators for debugging purposes function EID:renderHUDLocationIndicators() local mousePos = Isaac.WorldToRenderPosition(Input.GetMousePosition(true)) * 2 Isaac.RenderScaledText("Mouse pos X:"..mousePos.X.." Y:"..mousePos.Y, 100, 10, 0.5, 0.5, 1 ,1 ,1 ,1 ) Isaac.RenderScaledText("HUD Adjustment Preview!", 200, 10, 1, 1, 1 ,0.25 ,0.25 ,1 ) for k, v in pairs(EID.HUDElements) do local hudElement = EID:handleHUDElement(v) hudBBSprite.Scale = Vector(hudElement.width/2, hudElement.height/2) hudBBSprite.Color = Color(1, 1, 1, EID.Config["Transparency"], 0, 0, 0) hudBBSprite:Render(Vector(hudElement.x/2, hudElement.y/2), nullVector, nullVector) Isaac.RenderScaledText(k, hudElement.x/2, hudElement.y/2, 0.5, 0.5, 1, 1, 1 ,1) end end local lastMousePos = Vector(0,0) local lastMouseMove = 0 --- Handle the HUD hover functionality, checking if the mouse is over any HUD elements --- @return EID_DescObj | nil - Returns the description object if the mouse is over a HUD element, otherwise nil function EID:handleHoverHUD() local mousePos = Isaac.WorldToScreen(Input.GetMousePosition(true)) * 2 if mousePos:Distance(lastMousePos) > 2 then lastMousePos = mousePos lastMouseMove = game:GetFrameCount() end if game:GetFrameCount() - lastMouseMove > 60 * 3 then return nil end if EID.Config["ShowCursor"] then EID.CursorSprite:Render(Vector(mousePos.X / 2, mousePos.Y / 2), nullVector, nullVector) end for _, v in pairs(EID.HUDElements) do local hudElement = EID:handleHUDElement(v) if hudElement.x <= mousePos.X and (hudElement.x + hudElement.width) >= mousePos.X and hudElement.y <= mousePos.Y and (hudElement.y + hudElement.height) >= mousePos.Y then local result = hudElement.descriptionObj() if result then return result end end end return nil end --- Set the player and coop player data for EID function EID:setPlayer() local numPlayers = game:GetNumPlayers() -- Old simple setPlayer, to reduce runtime in single player if numPlayers == 1 or not EID.Config["CoopDescriptions"] then local p = Isaac.GetPlayer(0) if EID.isRepentance and p:GetPlayerType() == PlayerType.PLAYER_THEFORGOTTEN_B then EID.player = p:GetOtherTwin() or p else EID.player = p end EID.players = { EID.player } if EID.isRepentance then EID.players = { EID.player, EID.isRepentance and EID.player:GetOtherTwin() } end EID.coopMainPlayers = { EID.player } EID.coopAllPlayers = EID.players EID.controllerIndexes = {} EID.controllerIndexes[p.ControllerIndex] = 1 EID.isMultiplayer = false return end -- Get our primary player, our list of all players, and a list of the primary player for each controller EID.coopAllPlayers = {} -- all player characters in order EID.coopMainPlayers = {} -- the primary player character from each controller EID.controllerIndexes = {} -- simple table to map each controller index to P1/P2/P3/P4 (since index 0 could be P2) local currentPlayerNum = 1 for i = 0, numPlayers - 1 do local player = Isaac.GetPlayer(i) -- Don't include player entities with a parent (Strawman / Soul of Jacob and Esau) if player.Parent == nil then local newIndex = not EID.controllerIndexes[player.ControllerIndex] -- Tainted Soul is treated as the primary player for Tainted Forgotten, so swap them if EID.isRepentance and (player:GetPlayerType() == PlayerType.PLAYER_THEFORGOTTEN_B or player:GetPlayerType() == PlayerType.PLAYER_THESOUL_B) then player = player:GetOtherTwin() or player end table.insert(EID.coopAllPlayers, player) if newIndex then table.insert(EID.coopMainPlayers, player) EID.controllerIndexes[player.ControllerIndex] = currentPlayerNum currentPlayerNum = currentPlayerNum + 1 end end end EID.player = EID.coopAllPlayers[1] -- second entry will be nil if no twin or AB+ (REPENTANCE = nil) EID.players = { EID.player, EID.isRepentance and EID.player:GetOtherTwin() } EID.isMultiplayer = #EID.coopMainPlayers > 1 end --------------------------------------------------------------------------- ---------------------------On Update Function------------------------------ local collSpawned = false EID.RecheckVoid = false EID.ShouldCheckWisp = false --- MC_POST_UPDATE callback that runs 30 times a second; doesn't update while paused --- Handles updating the game state and checking for new items function EID:onGameUpdate() EID.GameUpdateCount = EID.GameUpdateCount + 1 EID:checkPlayersForMissingItems() EID:evaluateQueuedItems() EID:evaluateHeldPill() EID.TabHeldLastFrame = EID.TabHeldThisFrame EID.TabHeldThisFrame = EID:PlayersActionPressed(EID.Config["BagOfCraftingToggleKey"]) if collSpawned then collSpawned = false local curPositions = {} for _, entity in ipairs(Isaac.FindByType(5, 100, -1, true, false)) do -- Flag pedestals as being describable on frame 0 (for Tainted Isaac / Glitched Crown type pedestals to not be non-existent for a frame) EID:setEntityData(entity, "EID_DescribeOnFirstFrame", true) -- Fix Overlapping Pedestals if a collectible spawned this frame (needed for Mega Chest) local pos = entity.Position for _, otherPos in ipairs(curPositions) do if pos:Distance(otherPos[2]) == 0 then EID:setEntityData(entity, "EID_RenderOffset", Vector(10,0)) EID:setEntityData(otherPos[1], "EID_RenderOffset", Vector(-10,0)) end end table.insert(curPositions, {entity, entity.Position}) end EID.RecheckVoid = true end -- Check player items for starting items if EID.ShouldCheckStartingItems then EID:UpdateAllPlayerPassiveItems() EID:SetOldestItemIndex() EID.ShouldCheckStartingItems = false end if EID.isRepentance then EID:WatchForGlitchedCrown() EID:UpdateWildCardEffects() if EID.GameUpdateCount % 10 == 0 then -- Check wisp for adding reminder when using lemegeton if EID.ShouldCheckWisp then EID:UpdateAllPlayerLemegetonWisps() EID.ShouldCheckWisp = false end -- Remove Crane Game item data if it's giving the prize out for _, crane in ipairs(Isaac.FindByType(6, 16, -1, true, false)) do if EID.CraneItemType[tostring(crane.InitSeed)] then if crane:GetSprite():IsPlaying("Prize") then EID.CraneItemType[tostring(crane.InitSeed)] = nil -- Pair the Crane Game's new drop seed with the latest collectible ID it's gotten -- (fixes Glowing Hour Glass rewinds) elseif EID.CraneItemType[crane.InitSeed.."Drop"..crane.DropSeed] == nil then EID.CraneItemType[crane.InitSeed.."Drop"..crane.DropSeed] = EID.CraneItemType[tostring(crane.InitSeed)] end end end end end end EID:AddCallback(ModCallbacks.MC_POST_UPDATE, EID.onGameUpdate) -- Wait until all collectibles spawning this frame have spawned before checking what we need to check about them function EID:CollectibleSpawnedThisFrame(_) collSpawned = true end EID:AddCallback(ModCallbacks.MC_POST_PICKUP_INIT, EID.CollectibleSpawnedThisFrame, PickupVariant.PICKUP_COLLECTIBLE) -- Pathchecking local lastPathfindFrame = -1 local function attemptPathfind(entity) if (EID.Config["DisableObstructionOnFlight"] and EID.player.CanFly) then pathsChecked[entity.InitSeed] = true return true end -- Don't reattempt pathfinding more than 2 times a second, unless this is a new entity if pathsChecked[entity.InitSeed] == false and EID.GameUpdateCount - lastPathfindFrame < 15 then return false end local successful = EID:HasPathToPosition(EID.player.Position, entity.Position) pathsChecked[entity.InitSeed] = successful lastPathfindFrame = EID.GameUpdateCount return successful end local hasShownStartWarning = false --- Check for start of run warnings, such as old game version or modded items function EID:CheckStartOfRunWarnings() if EID.isRepentance and not EID.Config["DisableStartOfRunWarnings"] and game:GetFrameCount() < 10*30 then -- Old Repentance version check; update this to check for the existence of the newest mod API function EID uses -- 1.7.9b (Dec. 08, 2022): The IsAvailable function was added (checking for Isaac.RunCallback existing instead) if Isaac.RunCallback == nil then local demoDescObj = EID:getDescriptionObj(-999, -1, 1) demoDescObj.Name = EID:getDescriptionEntry("AchievementWarningTitle") or "" demoDescObj.Description = EID:getDescriptionEntry("OldGameVersionWarningText") or "" EID:displayPermanentText(demoDescObj, "AchievementWarningTitle") hasShownStartWarning = true -- Bag of Crafting modded items check elseif not REPENTOGON and EID:PlayersHaveCollectible(710) and EID:DetectModdedItems() and EID.Config.DisplayBagOfCrafting ~= "never" and (EID.Config.BagOfCraftingDisplayRecipesMode == "Recipe List" or EID.Config.BagOfCraftingDisplayRecipesMode == "Preview Only") then local demoDescObj = EID:getDescriptionObj(-999, -1, 1) demoDescObj.Name = EID:getDescriptionEntry("AchievementWarningTitle") or "" demoDescObj.Description = EID:getDescriptionEntry("ModdedRecipesWarningText") or "" EID:displayPermanentText(demoDescObj, "AchievementWarningTitle") hasShownStartWarning = true else if not EID:AreAchievementsAllowed() then local demoDescObj = EID:getDescriptionObj(-999, -1, 1) demoDescObj.Name = EID:getDescriptionEntry("AchievementWarningTitle") or "" demoDescObj.Description = ("{{AchievementLocked}} "..EID:getDescriptionEntry("AchievementWarningText")) or "" EID:displayPermanentText(demoDescObj, "AchievementWarningTitle") hasShownStartWarning = true end end elseif hasShownStartWarning then EID:hidePermanentText() hasShownStartWarning = false end end -- Check our position modifiers based on HUD offset and character choice function EID:CheckPosModifiers() -- HUD offset adjustment, done every frame so it looks nice while changing the option if Options then -- Read the HUD offset from the Options table if it exists. -- +3 Y to avoid overlapping with the shadow of the charge bar hud EID:addTextPosModifier("HudOffset", Vector(20, 12) * (Options.HUDOffset - 1) + Vector(0, 3)) else EID:addTextPosModifier("HudOffset", Vector((EID.Config["HUDOffset"] * 2) - 20, EID.Config["HUDOffset"] - 10)) end -- the other modifiers don't need to be ran as frequently if EID.GameRenderCount % 30 ~= 0 then return end -- Repentance+ pushes a lot of UI lower, so we have to push the description lower as well to avoid overlapping if EID.isRepentancePlus then EID:addTextPosModifier("Repentance+", Vector(0, 10)) else EID:removeTextPosModifier("Repentance+") end -- Greed Mode small right adjustment if game:IsGreedMode() then EID:addTextPosModifier("Greed Mode Horizontal", Vector(8, 0)) else EID:removeTextPosModifier("Greed Mode Horizontal") end if not EID.isRepentance then -- AB+ Schoolbag adjustment if EID.player:HasCollectible(CollectibleType.COLLECTIBLE_SCHOOLBAG) then EID:addTextPosModifier("Schoolbag", Vector(0, 30)) else EID:removeTextPosModifier("Schoolbag") end else -- Tainted Isaac and Tainted ??? unique HUD adjustment if EID.player:GetPlayerType() == 21 or EID.player:GetPlayerType() == 25 then EID:addTextPosModifier("Tainted HUD", Vector(0,26)) else EID:removeTextPosModifier("Tainted HUD") end -- Jacob and Esau card/pill adjustment if EID.player:GetPlayerType() == 19 then EID:addTextPosModifier("J&E HUD", Vector(0,16)) else EID:removeTextPosModifier("J&E HUD") end -- Magdalene Birthright, Keeper & Tainted Keeper third/fourth row of hearts adjustment if EID.player:GetEffectiveMaxHearts() + EID.player:GetSoulHearts() + (EID.player:GetBrokenHearts() * 2) >= 37 then EID:removeTextPosModifier("18 Heart HUD") EID:addTextPosModifier("24 Heart HUD", Vector(0,20)) elseif EID.player:GetEffectiveMaxHearts() + EID.player:GetSoulHearts() + (EID.player:GetBrokenHearts() * 2) >= 25 then EID:removeTextPosModifier("24 Heart HUD") EID:addTextPosModifier("18 Heart HUD", Vector(0,10)) else EID:removeTextPosModifier("18 Heart HUD") EID:removeTextPosModifier("24 Heart HUD") end end end --------------------------------------------------------------------------- ---------------------------On Render Function------------------------------ local searchPartitions = EntityPartition.FAMILIAR + EntityPartition.ENEMY + EntityPartition.PICKUP + EntityPartition.PLAYER EID.lastDescriptionEntity = nil EID.lastDist = 0 EID.OptionChanged = false EID.bagPlayer = nil --- Render function that runs every frame and handles the main EID rendering logic. function EID:OnRender() -- Increases by 60 per second, ignores pauses EID.GameRenderCount = EID.GameRenderCount + 1 EID.OptionChanged = EID.MCM_OptionChanged EID.MCM_OptionChanged = false EID:resumeCoroutines() ArrowSprite:Update() EID:setPlayer() EID.isDisplaying = false EID.descriptionsToPrint = {} EID.entitiesToPrint = {} alwaysUseLocalMode = false -- if frames were skipped (due to EID being hidden / in battle / in options), wipe the desc cache if EID.GameRenderCount > prevPrintFrame + 1 then EID:ResetDescCache() EID.CachedIndicators = {} end -- Do not check our hide or scale hotkeys while a tab that can modify them is open if EID.MCMCompat_isDisplayingEIDTab ~= "General" then EID:HandleRenderingKeys() end EID.TabPreviewID = 0 EID.TabDescThisFrame = false -- If MCM is open, don't show anything unless we are on a tab that explicitely writes its name in the MCMCompat_isDisplayingEIDTab variable if ModConfigMenu and ModConfigMenu.IsVisible and ModConfigMenu.Config["Mod Config Menu"].HideHudInMenu and EID.MCMCompat_isDisplayingEIDTab == "" then return end if EID.isRepentance then local hasBag, bagPlayer = EID:PlayersHaveCollectible(710) if hasBag then EID.bagPlayer = bagPlayer EID:handleBagOfCraftingUpdating() end end -- Handle descriptions that display regardless of player position EID:CheckStartOfRunWarnings() EID:CheckPosModifiers() EID:renderMCMDummyDescription() if EID.isHidden then return elseif EID.Config["HideInBattle"] then if Isaac.CountBosses() > 0 or Isaac.CountEnemies() > 0 then return end end if EID.isDisplayingPermanent and EID.permanentDisplayTextObj ~= nil then EID:printDescription(EID.permanentDisplayTextObj) EID.isDisplaying = true return end -- Check for Tab (or user setting) being held, for the Item Reminder local tabHeld, playerNumHoldingTab = EID:PlayersActionPressed(EID.Config["BagOfCraftingToggleKey"]) if tabHeld then EID.holdTabPlayer = EID.coopMainPlayers[playerNumHoldingTab] if EID.holdTabCounter == 0 then -- update item reminder player selection EID:ItemReminderHandleInitHoldTab() end EID.holdTabCounter = EID.holdTabCounter + 1 else EID.holdTabPlayer = nil EID.holdTabCounter = 0 end -- Check for Drop being pressed, for Repentance D Infinity tracking if EID.isRepentance then local dropTriggered, playerNumPressingDrop = EID:PlayersActionPressed(ButtonAction.ACTION_DROP, Input.IsActionTriggered) if dropTriggered and not game:IsPaused() then local playerPressingDrop = EID.coopMainPlayers[playerNumPressingDrop] local playerID = EID:getPlayerID(playerPressingDrop, true) local playerType = playerPressingDrop:GetPlayerType() -- Forgotten's drop requires a double tap to actually count for this if (playerType == 16 or playerType == 17) then -- technically we should track a forgotten drop timer for each player but forget it if EID.GameUpdateCount > forgottenDropTimer then -- the actual interval here seems to be 6.5. 7 has false positives, 6 has false negatives forgottenDropTimer = EID.GameUpdateCount + 7 dropTriggered = false else forgottenDropTimer = 0 end end if dropTriggered and playerPressingDrop:GetActiveItem(0) == 489 then EID.DInfinityState[playerID] = EID.DInfinityState[playerID] or 0 EID.DInfinityState[playerID] = (EID.DInfinityState[playerID] + 1) % 10 end end end EID:ItemReminderHandleInputs() if EID.ForceRefreshCache then EID:ResetDescCache() end -- This is not a frame we should check for new descriptions; just print our cached ones if not EID:RefreshThisFrame() and not EID.MCM_OptionChanged and not EID.ForceRefreshCache then EID:printDescriptions(true) return end EID.ForceRefreshCache = false -- We'll redraw the indicators in the process of determining what's in range, so wipe their cache EID.CachedIndicators = {} if EID.Config["EnableMouseControls"] then local hudDescription = EID:handleHoverHUD() if hudDescription then EID:addDescriptionToPrint(hudDescription) end end -- Decide how many player entities we're checking -- Primary P1 only, P1 + Esau, primary all controllers, or all controllers + Esaus local playerSearch = { EID.player } if EID.Config["CoopDescriptions"] then if EID.Config["PairedPlayerDescriptions"] then playerSearch = EID.coopAllPlayers else playerSearch = EID.coopMainPlayers end elseif EID.Config["PairedPlayerDescriptions"] then playerSearch = EID.players end local displayedDesc = {} -- Check for Bag of Crafting per player -- (Do this first because it can't be Local Mode, and should take precedence over other descriptions, even as Player 2+) if EID.isRepentance then for playerNum,player in ipairs(playerSearch) do if player:HasCollectible(710) then local craftingSuccess = EID:handleBagOfCraftingRendering() EID.BoC.IsDisplayingDescription = craftingSuccess if craftingSuccess then displayedDesc[playerNum] = true end end end end -- Check for descriptions to print per player for playerNum,player in ipairs(playerSearch) do if (not displayedDesc[playerNum] or EID.Config["DisplayAllNearby"]) and #EID.descriptionsToPrint < EID.Config["MaxDescriptionsToDisplay"] then -- Searching for the closest describable entity to this player EID.lastDescriptionEntity = nil EID.lastDist = 10000 local searchGroups = {} local sourcePos = player.Position local inRangeEntities = {} table.insert(searchGroups, Isaac.FindInRadius(sourcePos, tonumber(EID.Config["MaxDistance"])*40, searchPartitions)) for k,_ in pairs(EID.effectList) do table.insert(searchGroups, Isaac.FindByType(EntityType.ENTITY_EFFECT, tonumber(k), -1, true, false)) end for _, entitySearch in ipairs(searchGroups) do for _, entity in ipairs(entitySearch) do if EID:hasDescription(entity) and (entity.FrameCount > 0 or EID:getEntityData(entity, "EID_DescribeOnFirstFrame")) and not EID.entitiesToPrint[GetPtrHash(entity)] then table.insert(inRangeEntities, entity) local diff = entity.Position:__sub(sourcePos) -- break ties with the render offset (for Mega Chest double collectibles) local entityDataRenderOffset = EID:getEntityData(entity, "EID_RenderOffset") if diff:Length() == EID.lastDist and entityDataRenderOffset then diff = diff + entityDataRenderOffset end if diff:Length() < EID.lastDist then EID.lastDescriptionEntity = entity EID.lastDist = diff:Length() end end end end for _, gridEntity in pairs(EID.CurrentRoomGridEntities) do if EID:hasDescription(gridEntity) then table.insert(inRangeEntities, gridEntity) local diff = gridEntity.Position:__sub(sourcePos) if diff:Length() < EID.lastDist then EID.lastDescriptionEntity = gridEntity EID.lastDist = diff:Length() end end end if EID.Config["DisplayAllNearby"] then -- make the closest entity the first to get drawn table.insert(inRangeEntities, 1, EID.lastDescriptionEntity) else -- we only want to describe the closest entity inRangeEntities = { EID.lastDescriptionEntity } end -- Only display the indicator for the primary (closest / crafting) description if EID.lastDescriptionEntity then EID:renderIndicator(EID.lastDescriptionEntity, EID.controllerIndexes[player.ControllerIndex] or 1) table.insert(EID.CachedIndicators, {EID.lastDescriptionEntity, EID.controllerIndexes[player.ControllerIndex]}) end for _,closest in ipairs(inRangeEntities) do if not EID:IsGridEntity(closest) then --Handle GetData Entities (specific) if EID.Config["EnableEntityDescriptions"] and EID:getEntityData(closest, "EID_Description") then local desc = EID:getEntityData(closest, "EID_Description") local origDesc = EID:getDescriptionObjByEntity(closest) if desc ~= nil and type(desc) == "table" then origDesc = EID:mergeDescriptionObjects(origDesc, desc) else origDesc.Description = desc end EID:addDescriptionToPrint(origDesc) -- Handle Glitched Items elseif closest.Type == 5 and closest.Variant == 100 and closest.SubType > 4294960000 then if EID:IsItemHidden(closest) then EID:addQuestionMarkDescription(closest) else local glitchedObj = EID:getDescriptionObj(closest.Type, closest.Variant, closest.SubType, closest) local glitchedName = EID.itemConfig:GetCollectible(closest.SubType).Name local glitchedDesc = EID:getXMLDescription(closest.Type, closest.Variant, closest.SubType) -- force the default glitchy description if option is off if not EID.Config["DisplayGlitchedItemInfo"] then glitchedObj.Description = glitchedDesc -- grab the Item Config info if we haven't created a desc for it before, or its name has changed since we did (different game session) elseif glitchedObj.Description == glitchedDesc or glitchedObj.Name ~= glitchedName then EID:addCollectible(closest.SubType, EID:CheckGlitchedItemConfig(closest.SubType), glitchedName) glitchedObj = EID:getDescriptionObj(closest.Type, closest.Variant, closest.SubType, closest) end EID:addDescriptionToPrint(glitchedObj) end -- Handle Dice Room Floor elseif closest.Type == 1000 and closest.Variant == 76 then EID:addDescriptionToPrint(EID:getDescriptionObj(closest.Type, closest.Variant, closest.SubType+1, closest)) -- Handle Card Reading Portals elseif closest.Type == 1000 and closest.Variant == 161 then if closest.SubType <= 2 or (EID.isRepentancePlus and closest.SubType == 3) then local subtypeToCard = {18, 5, 19} if EID.isRepentancePlus then subtypeToCard = {18, 5, 19, 10} end -- Reuse the descriptions of The Emperor/Stars/Moon/Hermit, so no localization needed local descriptionObj = EID:getDescriptionObj(5, 300, subtypeToCard[closest.SubType+1], closest) -- Card Reading's name descriptionObj.Name = EID:getObjectName(5, 100, 660) -- Only keep the first line of the description (to avoid Tarot Cloth effects or Planetarium mention on The Stars) descriptionObj.Description = string.sub(descriptionObj.Description, 1, string.find(descriptionObj.Description, "#")) EID:addDescriptionToPrint(descriptionObj) end -- Handle Crane Game elseif closest.Type == 6 and closest.Variant == 16 then local collectibleID = EID.CraneItemType[closest.InitSeed.."Drop"..closest.DropSeed] or EID.CraneItemType[tostring(closest.InitSeed)] -- REPENTOGON lets us get the prize collectible directly if REPENTOGON then collectibleID = closest:ToSlot():GetPrizeCollectible() end if collectibleID then local wasHidden = false if EID:getEntityData(closest, "EID_DontHide") ~= true then local isHideUncollected = EID.Config["HideUncollectedItemDescriptions"] and EID:requiredForCollectionPage(collectibleID) if (EID.Config["DisableOnCurse"] and EID:hasCurseBlind()) or (isHideUncollected) or (EID.Config["DisableOnAprilFoolsChallenge"] and game.Challenge == Challenge.CHALLENGE_APRILS_FOOL) then local description = isHideUncollected and EID:getDescriptionEntry("CollectionPageInfo") or nil EID:addQuestionMarkDescription(closest, description) wasHidden = true; end end if not wasHidden then local descriptionObj = EID:getDescriptionObj(5, 100, collectibleID, closest) EID:addDescriptionToPrint(descriptionObj) end end --Handle Broken Shovel elseif closest.Type == 5 and closest.Variant == 110 then local descriptionObj = EID:getDescriptionObj(5, 100, 550, closest) EID:addDescriptionToPrint(descriptionObj) elseif closest.Variant == PickupVariant.PICKUP_TRINKET then --Handle Trinkets local descriptionObj = EID:getDescriptionObjByEntity(closest) EID:addDescriptionToPrint(descriptionObj) elseif closest.Variant == PickupVariant.PICKUP_COLLECTIBLE then --Handle Collectibles if EID:IsItemHidden(closest) then local isHideUncollected = EID.Config["HideUncollectedItemDescriptions"] and EID:requiredForCollectionPage(closest.SubType) local description = isHideUncollected and EID:getDescriptionEntry("CollectionPageInfo") or nil EID:addQuestionMarkDescription(closest, description) else local descriptionObj = EID:getDescriptionObjByEntity(closest) EID:addDescriptionToPrint(descriptionObj) end elseif closest.Variant == PickupVariant.PICKUP_TAROTCARD then local descriptionObj = EID:getDescriptionObjByEntity(closest) local wasHidden = false --Handle Cards & Runes if EID:getEntityData(closest, "EID_DontHide") ~= true then local isSoulstone = closest.SubType >= 81 and closest.SubType <= 97 local hideinShop = closest:ToPickup():IsShopItem() and ((not isSoulstone and not EID.Config["DisplayCardInfoShop"]) or (isSoulstone and not EID.Config["DisplaySoulstoneInfoShop"])) local isOptionsSpawn = EID.isRepentance and not EID.Config["DisplayCardInfoOptions?"] and closest:ToPickup().OptionsPickupIndex > 0 local obstructed = ((not isSoulstone and not EID.Config["DisplayObstructedCardInfo"]) or (not EID.Config["DisplayObstructedSoulstoneInfo"] and isSoulstone)) and (not pathsChecked[closest.InitSeed] and not attemptPathfind(closest)) if (isOptionsSpawn or hideinShop or obstructed) and not descriptionObj.ShowWhenUnidentified then -- Render Questionmark EID:addQuestionMarkDescription(closest) wasHidden = true end end local isCantrippedCard = game.Challenge == 43 and closest.SubType > 32768 if isCantrippedCard then -- Convert into item description for CanTripped Challenge descriptionObj = EID:getDescriptionObj(5, 100, closest.SubType - 32768, closest) EID:addDescriptionToPrint(descriptionObj) elseif not wasHidden then -- Show card / Rune description EID:addDescriptionToPrint(descriptionObj) end elseif closest.Variant == PickupVariant.PICKUP_PILL then --Handle Pills local wasHidden = false if EID:getEntityData(closest, "EID_DontHide") ~= true then local hideinShop = closest:ToPickup():IsShopItem() and not EID.Config["DisplayPillInfoShop"] local isOptionsSpawn = EID.isRepentance and not EID.Config["DisplayPillInfoOptions?"] and closest:ToPickup().OptionsPickupIndex > 0 local obstructed = not EID.Config["DisplayObstructedPillInfo"] and (not pathsChecked[closest.InitSeed] and not attemptPathfind(closest)) if isOptionsSpawn or hideinShop or obstructed then -- Render Questionmark EID:addQuestionMarkDescription(closest) wasHidden = true end end if not wasHidden then local pillColor = closest.SubType local pool = game:GetItemPool() local identified = pool:IsPillIdentified(pillColor) and not EID.Config["OnlyShowPillWhenUsedAtLeastOnce"] if EID.isRepentance and pillColor % PillColor.PILL_GIANT_FLAG == PillColor.PILL_GOLD then identified = true end local pillEffectID = EID:getAdjustedSubtype(closest.Type, closest.Variant, pillColor) local wasUsed = EID:WasPillUsed(pillColor) if (identified or wasUsed or EID.Config["ShowUnidentifiedPillDescriptions"]) and not EID.UnidentifyablePillEffects[pillEffectID] then -- show identified pill local descEntry = EID:getDescriptionObj(closest.Type, closest.Variant, pillColor, closest) EID:addDescriptionToPrint(descEntry) else -- show unidentified pill description EID:addDescriptionToPrint({ Description = "UnidentifiedPill", Entity = closest}) end end elseif EID.Config["EnableEntityDescriptions"] then --Handle Entities (omni) local descriptionEntry = EID:getDescriptionObjByEntity(closest) if descriptionEntry ~= nil then EID:addDescriptionToPrint(descriptionEntry) end end else -- Grid entities local desc = EID:getDescriptionObj(-999, closest:GetType(), closest:GetVariant(), closest) EID:addDescriptionToPrint(desc) end end end end -- handle showing the Hold Map Helper description if EID.Config["ItemReminderEnabled"] and EID.holdTabCounter >= 30 and EID.TabDescThisFrame == false and EID.holdTabPlayer ~= nil then EID:ItemReminderHandleInputs() local demoDescObj = EID:getDescriptionObj(-999, -1, 1) demoDescObj.PermanentTextEnglish = EID:getDescriptionEntryEnglish("ItemReminder", "Title") -- check for scrolling being enabled here and append scroll controls to the title? demoDescObj.Description = EID:ItemReminderGetDescription() demoDescObj.Name = EID:ItemReminderGetTitle() if (demoDescObj.Description ~= "") then EID:addDescriptionToPrint(demoDescObj, 1) end end EID:printDescriptions() end EID:AddCallback(ModCallbacks.MC_POST_RENDER, EID.OnRender) --- Add active item progress to the player's transformation progress. ---@param player EntityPlayer ---@param isD4 boolean local function AddActiveItemProgress(player, isD4) EID.ForceRefreshCache = true local playerID = EID:getPlayerID(player, true) EID:InitItemInteractionIfAbsent(playerID) local activesTable = EID.PlayerItemInteractions[playerID].actives -- don't check pocket items after D4, they don't reroll and would get counted twice local maxSlot = 3 if isD4 then maxSlot = 1 end for i = 0, maxSlot do local itemIDStr = tostring(player:GetActiveItem(i)) -- Book of Virtues does not count as an active item in Repentance if itemIDStr ~= "0" and not (EID.isRepentance and itemIDStr == "584") then EID:InitActiveItemInteraction(itemIDStr) activesTable[itemIDStr] = activesTable[itemIDStr] + 1 end end end -- Check the active items of every player for transformation progress (used at game start and after Genesis) local function CheckAllActiveItemProgress() for i = 0, game:GetNumPlayers() - 1 do AddActiveItemProgress(Isaac.GetPlayer(i), false) end end --- OnGameStartGeneral is called when the game starts, either from a save or a new run. --- It initializes various EID tables and resets item interactions. local function OnGameStartGeneral(_,isSave) EID:GetAllPassiveItems() EID:GetTransformationsOfModdedItems() EID:buildTransformationTables() EID.RecentlyTouchedItems = {} EID.GulpedTrinkets = {} EID.OldestItemIndex = {} if not isSave then EID.PlayerItemInteractions = {} EID.DInfinityState = {} end EID.ShouldCheckWisp = true EID.WildCardEffects = {} end EID:AddCallback(ModCallbacks.MC_POST_GAME_STARTED, OnGameStartGeneral) local function OnPostPlayerInit() EID:setPlayer() -- re-evaluate player list. Fixes crash when using "rewind" command in a MC_POST_RENDER callback EID.ShouldCheckStartingItems = true -- for The Stars? tracking end EID:AddCallback(ModCallbacks.MC_POST_PLAYER_INIT, OnPostPlayerInit) -- Add currently held active items after D4 was used. Used for Transformation Progress local function OnUseD4(_, _, _, player) -- in AB+, USE_ITEM doesn't provide a player AddActiveItemProgress(player or EID.player, true) if EID.isRepentance then local playerNum = EID:getPlayerID(player, true) EID.OldestItemIndex[playerNum] = -1 -- D4 makes it impossible for us to know what the oldest item is for The Stars? end end EID:AddCallback(ModCallbacks.MC_USE_ITEM, OnUseD4, CollectibleType.COLLECTIBLE_D4) -- MC_PRE_USE_ITEM callback to watch for smelting trinkets; includes Gulp and Marbles function EID:OnUseSmelter(_, _, player) player = player or EID.player local playerNum = EID:getPlayerID(player, true) EID.GulpedTrinkets[playerNum] = EID.GulpedTrinkets[playerNum] or {} for i=0,1 do local trinket = player:GetTrinket(i) if trinket > 0 then table.insert(EID.GulpedTrinkets[playerNum], trinket) end end end EID:AddCallback(ModCallbacks.MC_PRE_USE_ITEM, EID.OnUseSmelter, CollectibleType.COLLECTIBLE_SMELTER) -- MC_PRE_USE_ITEM callback to watch for Glowing Hourglass to revert certain variables local function OnUseGlowingHourglass(_, _, _, _) EID.absorbedItems = EID:CopyTable(preHourglassStatus.absorbedItems) EID.PlayerItemInteractions = EID:CopyTable(preHourglassStatus.PlayerItemInteractions) if EID.isRepentance then EID.WildCardEffects = EID:CopyTable(preHourglassStatus.WildCardEffects) EID.WildCardPillColor = EID:CopyTable(preHourglassStatus.WildCardPillColor) EID.DInfinityState = EID:CopyTable(preHourglassStatus.DInfinityState) EID.BoC.BagItems = EID:CopyTable(preHourglassStatus.BagItems) EID.BoC.RoomQueries = EID:CopyTable(preHourglassStatus.RoomQueries) end end EID:AddCallback(ModCallbacks.MC_PRE_USE_ITEM, OnUseGlowingHourglass, CollectibleType.COLLECTIBLE_GLOWING_HOUR_GLASS) -- Re-init transformation progress and item interactions after using Genesis if EID.isRepentance then local function OnUseGenesis(_, _, _, _) OnGameStartGeneral() CheckAllActiveItemProgress() end EID:AddCallback(ModCallbacks.MC_USE_ITEM, OnUseGenesis, CollectibleType.COLLECTIBLE_GENESIS) local function OnUseLemegeton(_, _, _, _, _, _) EID.ShouldCheckWisp = true end EID:AddCallback(ModCallbacks.MC_PRE_USE_ITEM, OnUseLemegeton, CollectibleType.COLLECTIBLE_LEMEGETON) -- Watch for any active item usage for Wild Card local function OnUseGeneral(_, collectibleType, _, player, useFlags, activeSlot) -- add the active to Wild Card tracking if it was a real active usage if useFlags & UseFlag.USE_OWNED > 0 and activeSlot >= 0 then EID:TrackWildCardEffects("5.100." .. collectibleType, player) end end EID:AddCallback(ModCallbacks.MC_PRE_USE_ITEM, OnUseGeneral) end --- OnUsePill is called when a pill is used, and handles pill history tracking and Wild Card effects. ---@param pillEffectID number The ID of the pill effect being used. ---@param player EntityPlayer The player using the pill. ---@param useFlags number Flags indicating how the pill was used (e.g., whether it was used with no animation). function EID:OnUsePill(pillEffectID, player, useFlags) player = player or EID.player --AB+ doesn't receive player in callback arguments! -- get the pill color by checking the player's pocket local pillColor = EID.PlayerHeldPill[EID:getPlayerID(player)] or 1 -- add the pill used to our history for Echo Chamber / Vurp! / transformation progress EID:AddPickupToHistory("pill", pillEffectID+1, player, useFlags, pillColor) -- add the pill to Wild Card tracking, unless it's a Temperance? pill if EID.isRepentance and useFlags ~= UseFlag.USE_NOANNOUNCER then EID:TrackWildCardEffects("5.70." .. (pillEffectID+1), player, pillColor) end EID.ForceRefreshCache = true -- for transformation progress update -- for tracking used pills, ignore gold pills and no animation pills, since those dont show what pull you used if EID.isRepentance and (pillColor % PillColor.PILL_GIANT_FLAG == PillColor.PILL_GOLD or useFlags & UseFlag.USE_NOANIM == UseFlag.USE_NOANIM) then return end -- in Repentance+, Amnesia horse pill unidentifies all pills if EID.isRepentancePlus and pillColor > PillColor.PILL_GIANT_FLAG and pillEffectID == PillEffect.PILLEFFECT_AMNESIA then EID.UsedPillColors = {} end EID.UsedPillColors[tostring(pillColor)] = true end EID:AddCallback(ModCallbacks.MC_USE_PILL, EID.OnUsePill) --- OnUseCard is called when a card is used, and handles card history tracking and Wild Card effects. ---@param cardID number The ID of the card being used. ---@param player EntityPlayer The player using the card. ---@param useFlags number Flags indicating how the card was used (e.g., whether it was used with no animation). function EID:OnUseCard(cardID, player, useFlags) player = player or EID.player --AB+ doesn't receive player in callback arguments! EID:AddPickupToHistory("card", cardID, player, useFlags) -- add the card to Wild Card tracking unless it's Wild Card if EID.isRepentance and cardID ~= 80 then EID:TrackWildCardEffects("5.300." .. cardID, player) end end EID:AddCallback(ModCallbacks.MC_USE_CARD, EID.OnUseCard) local json = require("json") local configIgnoreList = { ["BagContent"] = true, ["BagFloorContent"] = true, ["BagLearnedRecipes"] = true, ["CraneItemType"] = true, ["FlipItemPositions"] = true, ["AbsorbedItems"] = true, ["CollectedItems"] = true, ["PlayerItemInteractions"] = true, ["UsedPillColors"] = true, ["RecentlyTouchedItems"] = true, ["GulpedTrinkets"] = true, ["WildCardEffects"] = true, ["DInfinityState"] = true, ["OldestItemIndex"] = true, } -------------------------------- --------Handle Savedata--------- -------------------------------- --- OnGameStart is called when the game starts, either from a save or a new run. --- It initializes the config data if available. ---@param isSave boolean Indicates whether the game is starting from a save or a new run. function EID:OnGameStart(isSave) --Loading Moddata-- if EID:HasData() then local savedEIDConfig = json.decode(Isaac.LoadModData(EID)) -- JSON saves integer table keys as strings. we need to transform them back... used in OnGameStart local function ConvertSavedTable(tableName) for playerID, data in pairs(savedEIDConfig[tableName] or {}) do local convertedData = {} if type(data) == "table" then for key, value in pairs(data) do convertedData[tonumber(key) or key] = value end else convertedData = data end EID[tableName][tonumber(playerID)] = convertedData end end -- collection progress EID.CollectedItems = savedEIDConfig["CollectedItems"] or {} if EID.SaveGame and savedEIDConfig["SaveGameNumber"] > 0 then for _, id in ipairs(EID.CollectedItems) do EID.SaveGame[savedEIDConfig["SaveGameNumber"]].ItemNeedsPickup[id] = nil end end EID.PlayerItemInteractions = {} if isSave then -- JSON saves integer table keys as strings. we need to transform them back... ConvertSavedTable("PlayerItemInteractions") ConvertSavedTable("RecentlyTouchedItems") ConvertSavedTable("GulpedTrinkets") ConvertSavedTable("WildCardEffects") ConvertSavedTable("DInfinityState") ConvertSavedTable("OldestItemIndex") else -- check for the players' starting active items CheckAllActiveItemProgress() end EID.UsedPillColors = {} EID.absorbedItems = {} if isSave then EID.UsedPillColors = savedEIDConfig["UsedPillColors"] or {} EID.absorbedItems = savedEIDConfig["AbsorbedItems"] or {} end if EID.isRepentance then EID.BoC.BagItems = {} EID.BoC.LearnedRecipes = {} EID.CraneItemType = {} EID.flipItemPositions = {} if isSave then EID.BoC.BagItems = savedEIDConfig["BagContent"] or {} EID.BoC.LearnedRecipes = savedEIDConfig["BagLearnedRecipes"] or {} EID.BoC.RoomQueries = savedEIDConfig["BagFloorContent"] or {} EID.CraneItemType = savedEIDConfig["CraneItemType"] or {} -- turn list back into dict because json cant save dict indices. local flipItemTable = {} for _, v in ipairs(savedEIDConfig["FlipItemPositions"]) do local roomContent = {} for _, v1 in ipairs(v[2]) do roomContent[v1[1]] = v1[2] end flipItemTable[v[1]] = roomContent end EID.flipItemPositions = flipItemTable for k, _ in pairs(configIgnoreList) do savedEIDConfig[k] = nil end EID:AssignFlipItems() end end -- Check if eid_config.lua is in its default state (like if an EID update occurred) -- If it is, or MCM is loaded, load the config from savedata instead of eid_config.lua if savedEIDConfig.Version == EID.Config.Version then local isDefaultConfig = true for key, value in pairs(EID.Config) do if type(value) ~= type(EID.DefaultConfig[key]) and not configIgnoreList[key] then EID:WriteDebugMsg("EID Warning: Config value '"..key.."' has wrong data-type. Resetting it to default...") EID.Config[key] = EID.DefaultConfig[key] end if EID.DefaultConfig[key] ~= value then isDefaultConfig = false end end if isDefaultConfig or EID.MCMLoaded then for key, value in pairs(EID.Config) do if savedEIDConfig[key] ~= nil and type(value) == type(savedEIDConfig[key]) then EID.Config[key] = savedEIDConfig[key] end end end end EID.isHidden = EID.Config["InitiallyHidden"] EID.UsedPosition = Vector(EID.Config["XPosition"], EID.Config["YPosition"]) EID.Scale = EID.Config["Size"] EID.ItemReminderSelectedCategory = EID.Config["ItemReminderDisplayMode"] == "NoOverview" and 1 or 0 EID:AdjustLanguageConfigSetting() EID:fixDefinedFont() EID:loadFont(EID.modPath .. "resources/font/eid_"..EID.Config["FontType"]..".fnt") end end EID:AddCallback(ModCallbacks.MC_POST_GAME_STARTED, EID.OnGameStart) --- OnGameExit saves the current EID configuration. function EID:OnGameExit() if EID.isRepentance then EID.Config["BagContent"] = EID.BoC.BagItems or {} EID.Config["BagLearnedRecipes"] = EID.BoC.LearnedRecipes or {} EID.Config["BagFloorContent"] = EID.BoC.RoomQueries or {} EID.Config["CraneItemType"] = EID.CraneItemType or {} EID.Config["WildCardEffects"] = EID.WildCardEffects or {} EID.Config["DInfinityState"] = EID.DInfinityState or {} -- turn dictionary into list because json cant save dict indices. local flipItemTable = {} for k, v in pairs(EID.flipItemPositions) do local roomContent = {} for k1, v1 in pairs(v) do table.insert(roomContent, {k1,v1}) end table.insert(flipItemTable, {k,roomContent}) end EID.Config["FlipItemPositions"] = flipItemTable or {} end EID.Config["CollectedItems"] = EID.CollectedItems EID.Config["PlayerItemInteractions"] = EID.PlayerItemInteractions or {} EID.Config["RecentlyTouchedItems"] = EID.RecentlyTouchedItems or {} EID.Config["GulpedTrinkets"] = EID.GulpedTrinkets or {} EID.Config["UsedPillColors"] = EID.UsedPillColors EID.Config["AbsorbedItems"] = EID.absorbedItems or {} EID.Config["OldestItemIndex"] = EID.OldestItemIndex or {} EID.SaveData(EID, json.encode(EID.Config)) EID:hidePermanentText() EID.itemUnlockStates = {} EID.itemAvailableStates = {} end EID:AddCallback(ModCallbacks.MC_PRE_GAME_EXIT, EID.OnGameExit) if EID.enableDebug then require("features.eid_debugging") end -- load repentogon stuff last to allow overrides of functions require("features.eid_repentogon") Isaac.DebugString("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") Isaac.DebugString("External Item Descriptions v"..string.format("%.2f", EID.ModVersion).."_"..EID.ModVersionCommit.." loaded.") Isaac.DebugString("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") print("External Item Descriptions v"..string.format("%.2f", EID.ModVersion).."_"..EID.ModVersionCommit.." loaded.") -- Run EID compatibility Callback. Useful for mods that load before EID if EID.isRepentance then Isaac.RunCallback("EID_POST_LOAD") end