-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathhidden_item_manager.lua
More file actions
798 lines (687 loc) · 25.8 KB
/
hidden_item_manager.lua
File metadata and controls
798 lines (687 loc) · 25.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
-- Hidden Item Manager, by Connor (aka Ghostbroster)
-- Version 2.3.1
--
-- Manages a system of hidden Lemegeton Item Wisps to simulate the effects of passive items without actually granting the player those items (so they can't be removed or rerolled!).
-- Good for giving the effect of an item temporarily, making an item effect "innate" to a character, and all sorts of other stuff, probably.
-- Please keep in mind that the game has a TOTAL FAMILIAR LIMIT of 64 at a time! Each item provided by this is a wisp familiar!
-- So given that, please be careful and considerate when using this.
--
-- GitHub Page: https://github.com/ConnorForan/HiddenItemManager
-- Please refer to the GitHub page or the README file for more information and a guide.
--
-- Thanks Cake, DeadInfinity, Erfly, Taiga, and anyone else who might have helped figure out these wisp tricks.
--
-- Let me know if you have any problems or would like to suggest additional features/functions.
-- Discord: Connor#2143
-- Steam: Ghostbroster Connor
-- Email: [email protected]
-- Twitter: @Ghostbroster
local HiddenItemManager = {}
local game = Game()
local kWispPos = Vector(-1000, -1000)
local kZeroVector = Vector.Zero
local kPersistentWispMarker = 617413666
local kEarlyCallbackPriority = -9999
local kLateCallbackPriority = 9999
local function LOG_ERROR(str)
local prefix = ""
if HiddenItemManager.Mod then
prefix = "" .. HiddenItemManager.Mod.Name .. "."
end
local fullStr = "[" .. prefix .. "HiddenItemManager] ERROR: " .. str
print(fullStr)
Isaac.DebugString(fullStr)
end
local function LOG(str)
local prefix = ""
if HiddenItemManager.Mod then
prefix = "" .. HiddenItemManager.Mod.Name .. "."
end
local fullStr = "[" .. prefix .. "HiddenItemManager]: " .. str
Isaac.DebugString(fullStr)
end
--------------------------------------------------
-- Initialization
local Callbacks = {}
local function AddCallback(callbackID, func, param, priority)
table.insert(Callbacks, {
Callback = callbackID,
Func = func,
Param = param,
Priority = priority or kEarlyCallbackPriority,
})
end
local function AddLateCallback(callbackID, func, param)
AddCallback(callbackID, func, param, kLateCallbackPriority)
end
function HiddenItemManager:Init(mod)
HiddenItemManager.Mod = mod
HiddenItemManager.WispTag = "HiddenItemManager:" .. mod.Name
if not mod.AddedHiddenItemManagerCallbacks then
for _, tab in ipairs(Callbacks) do
mod:AddPriorityCallback(tab.Callback, tab.Priority, tab.Func, tab.Param)
end
mod.AddedHiddenItemManagerCallbacks = true
else
LOG_ERROR("More than one instance initialized!")
end
return HiddenItemManager
end
--------------------------------------------------
-- Storage/Utility
local kDefaultGroup = "HIDDEN_ITEM_MANAGER_DEFAULT"
local function GetGroup(group)
if group then
return ""..group
else
return kDefaultGroup
end
end
-- Hidden item wisp data sorted into a nested table.
-- player.InitSeed -> groupName -> CollectibleType -> wisp.InitSeed -> dataTable
-- This table is good for API lookups like checking if an item effect is active, or counting them.
local DATA = {}
-- Info on ALL hidden item wisps, simply just mapped by their InitSeeds.
-- This table is good for wisps looking up their own data, as well as for SaveData.
local INDEX = {}
-- Cache for EntityPtrs to wisps.
local WISP_PTRS = {}
-- Groups that should not apply costumes from wisps.
local NO_COSTUME_GROUPS = {}
-- Removes all empty subtables from a given table.
local function CleanUp(tab)
for k, v in pairs(tab) do
if type(v) == "table" then
if next(v) then
CleanUp(v)
else
tab[k] = nil
end
end
end
end
AddCallback(ModCallbacks.MC_POST_NEW_ROOM, function()
CleanUp(DATA)
end)
-- Find the DATA table entry for the given playerKey+group+itemID.
-- If allowInit is true, will initialize any missing subtables to empty.
-- Otherwise, returns nil if a subtable is not found.
local function FindData(playerKey, group, itemID, allowInit)
group = GetGroup(group)
if not DATA[playerKey] then
if not allowInit then
return
end
DATA[playerKey] = {}
end
if not DATA[playerKey][group] then
if not allowInit then
return
end
DATA[playerKey][group] = {}
end
if not itemID then
return DATA[playerKey][group]
end
if not DATA[playerKey][group][itemID] then
if not allowInit then
return
end
DATA[playerKey][group][itemID] = {}
end
return DATA[playerKey][group][itemID]
end
-- Insert new data into the DATA table.
local function InsertData(playerKey, group, itemID, wispKey, data)
local tab = FindData(playerKey, group, itemID, true)
tab[wispKey] = data
end
-- Removes data from the DATA table, if it exists.
local function ClearData(playerKey, group, itemID, wispKey)
local tab = FindData(playerKey, group, itemID, false)
if tab then
tab[wispKey] = nil
end
end
local function GetWispKey(entity)
if not entity then return end
return ""..entity.InitSeed
end
local function GetPlayerKey(player)
if not player then return end
if player.Type ~= EntityType.ENTITY_PLAYER then
LOG_ERROR("Found invalid player reference in GetPlayerKey!")
return
end
-- Player InitSeeds are inconsistent with Tainted Lazarus & co-op.
-- However, using collectible RNG seeds seems to work, even if potentially breakable.
player = player:ToPlayer()
if player:GetPlayerType() == PlayerType.PLAYER_LAZARUS2_B then
return ""..player:GetCollectibleRNG(CollectibleType.COLLECTIBLE_INNER_EYE):GetSeed() -- flip sucks
end
return ""..player:GetCollectibleRNG(CollectibleType.COLLECTIBLE_SAD_ONION):GetSeed()
end
-- Given the data entry for a hidden item, gets the wisp.
local function GetWisp(tab)
if not tab then return end
local ptr = WISP_PTRS[tab.WispKey]
if ptr and ptr.Ref then
if ptr.Ref.Type == EntityType.ENTITY_FAMILIAR and ptr.Ref.Variant == FamiliarVariant.ITEM_WISP then
return ptr.Ref:ToFamiliar()
end
LOG_ERROR("Found invalid wisp reference in GetWisp!")
end
end
-- Given the data entry for a hidden item, gets the player.
local function GetPlayer(tab)
if not tab then return end
local wisp = GetWisp(tab)
if wisp and wisp.Player then
if wisp.Player.Type ~= EntityType.ENTITY_PLAYER then
LOG_ERROR("Found an invalid Player reference in GetPlayer!")
return
end
return wisp.Player
end
-- Player wasn't found on the wisp. Might be due to us temporarily nulling `wisp.Player` to avoid Sacrificial Altar. See if we can find the player.
for i=0, game:GetNumPlayers()-1 do
local player = game:GetPlayer(i)
if GetPlayerKey(player) == tab.PlayerKey then
return player
end
end
end
local function KillWisp(wisp)
if not wisp then return end
if wisp.Player and wisp.Player.Type == EntityType.ENTITY_PLAYER and wisp.Player:Exists() and wisp.SubType == CollectibleType.COLLECTIBLE_MARS then
wisp.Player:TryRemoveNullCostume(NullItemID.ID_MARS)
end
wisp:Remove()
if wisp.Player and wisp.Player.Type ~= EntityType.ENTITY_PLAYER then
LOG_ERROR("Found wisp with an invalid Player reference in KillWisp!")
elseif wisp.Type == EntityType.ENTITY_FAMILIAR and wisp.Variant == FamiliarVariant.ITEM_WISP then
-- Kill() after Remove() makes sure the effects of wisps are removed properly while still skipping the death animation/sounds.
wisp:Kill()
else
LOG_ERROR("Found invalid wisp reference in KillWisp!")
end
end
-- Removes the hidden item wisp from both data tables with the given key.
local function RemoveWisp(key)
local tab = INDEX[key]
if not tab then return end
local player = GetPlayer(tab)
local item = tab.Item
if player then
ClearData(GetPlayerKey(player), tab.Group, item, key)
else
-- Failed to find the player. Whatever, just make certain the data for this wisp gone.
for playerKey, playerData in pairs(DATA) do
ClearData(playerKey, tab.Group, item, key)
end
end
INDEX[key] = nil
end
-- Called continuously on item wisps to make sure they STAY hidden.
local function KeepWispHidden(wisp)
wisp.EntityCollisionClass = EntityCollisionClass.ENTCOLL_NONE
wisp.GridCollisionClass = EntityGridCollisionClass.GRIDCOLL_NONE
wisp.Visible = false
wisp.Position = kWispPos
wisp.Velocity = kZeroVector
end
local function TagWisp(wisp)
wisp:GetData().HIDDEN_ITEM_MANAGER_WISP = HiddenItemManager.WispTag
end
-- Returns true if the wisp is one owned by THIS instance of the HiddenItemManager library.
local function IsManagedWisp(wisp)
return wisp:GetData().HIDDEN_ITEM_MANAGER_WISP == HiddenItemManager.WispTag
end
-- Returns true if the wisp is one owned by ANY instance of the HiddenItemManager library.
local function IsAnyHiddenItemManagerWisp(wisp)
return wisp:GetData().HIDDEN_ITEM_MANAGER_WISP ~= nil
end
-- Leave behind a very specific value in the coins/keys/hearts fields of our wisps.
-- Only used as a fallback for wisp identification if all else fails, since these
-- fields are actually persistent across quit+continue.
-- Mainly used to delete wisps originally from this library that weren't "claimed" by any instance of it.
local function ApplyPersistentHiddenItemManagerMark(wisp)
wisp.Coins = kPersistentWispMarker
wisp.Hearts = kPersistentWispMarker
wisp.Keys = kPersistentWispMarker
end
local function WasHiddenItemManagerWisp(wisp)
return wisp.Coins == kPersistentWispMarker
or wisp.Hearts == kPersistentWispMarker
or wisp.Keys == kPersistentWispMarker
end
-- Initializes (or re-initializes) an item wisp to be one of our hidden ones.
local function InitializeWisp(wisp)
wisp:AddEntityFlags(EntityFlag.FLAG_NO_QUERY | EntityFlag.FLAG_NO_REWARD)
wisp:ClearEntityFlags(EntityFlag.FLAG_APPEAR)
KeepWispHidden(wisp)
wisp:RemoveFromOrbit()
TagWisp(wisp)
local wispKey = GetWispKey(wisp)
local tab = INDEX[wispKey]
tab.WispKey = wispKey
WISP_PTRS[wispKey] = EntityPtr(wisp)
tab.PlayerKey = GetPlayerKey(wisp.Player)
tab.Initialized = true
if NO_COSTUME_GROUPS[tab.Group] and not wisp.Player:HasCollectible(wisp.SubType, true) then
wisp.Player:RemoveCostume(Isaac.GetItemConfig():GetCollectible(wisp.SubType))
end
end
-- Removes item costumes from players if they should be hidden.
function HiddenItemManager:CheckCostumes()
for key, data in pairs(INDEX) do
local wisp = GetWisp(data)
local player = GetPlayer(data)
if wisp and player then
if NO_COSTUME_GROUPS[data.Group] and not player:HasCollectible(wisp.SubType, true) then
player:RemoveCostume(Isaac.GetItemConfig():GetCollectible(wisp.SubType))
end
end
end
end
-- Spawns a hidden item wisp.
local function SpawnWisp(player, itemID, duration, group, removeOnNewRoom, removeOnNewLevel)
if not HiddenItemManager.Mod then
LOG_ERROR("Not initialized! Did you forget to call `hiddenItemManager:Init(mod)`?")
end
group = GetGroup(group)
if not itemID or itemID < 1 then
LOG_ERROR("Attempted to add invalid CollectibleType `" .. (itemID or "NULL") .. "` to group: " .. group)
end
if duration and duration < 1 then
duration = nil
end
local wisp = player:AddItemWisp(itemID, kWispPos)
local wispKey = GetWispKey(wisp)
local tab = {
Item = itemID,
Group = group,
Duration = duration,
RemoveOnNewRoom = removeOnNewRoom,
RemoveOnNewLevel = removeOnNewLevel,
ErrorCount = 0,
AddTime = game:GetFrameCount(),
}
InsertData(GetPlayerKey(player), group, itemID, wispKey, tab)
INDEX[wispKey] = tab
InitializeWisp(wisp)
HiddenItemManager:ItemWispUpdate(wisp)
end
local function AddInternal(player, itemID, duration, group, removeOnNewRoom, removeOnNewLevel, numToAdd)
if numToAdd < 1 then return end
for i=1, numToAdd do
SpawnWisp(player, itemID, duration, group, removeOnNewRoom, removeOnNewLevel)
end
end
--------------------------------------------------
-- API Functions
-- Hide costumes for any wisps added in the specified group.
-- Will hide costumes for wisps in the default group if unspecified.
function HiddenItemManager:HideCostumes(group)
NO_COSTUME_GROUPS[GetGroup(group)] = true
end
-- Add a hidden item(s) that will persist through room and floor transitions.
function HiddenItemManager:Add(player, itemID, duration, numToAdd, group)
AddInternal(player, itemID, duration, group, false, false, numToAdd or 1)
end
-- Add a hidden item(s) that will automatically expire when changing rooms.
function HiddenItemManager:AddForRoom(player, itemID, duration, numToAdd, group)
AddInternal(player, itemID, duration, group, true, true, numToAdd or 1)
end
-- Add a hidden item(s) that will automatically expire when changing floors.
function HiddenItemManager:AddForFloor(player, itemID, duration, numToAdd, group)
AddInternal(player, itemID, duration, group, false, true, numToAdd or 1)
end
-- Adds or removes copies of a hidden item within the group so that the total number of stacks is equal to targetStack.
function HiddenItemManager:CheckStack(player, itemID, targetStack, group)
local currentStack = HiddenItemManager:CountStack(player, itemID, group)
local diff = math.abs(currentStack - targetStack)
if currentStack > targetStack then
for i=1, diff do
HiddenItemManager:Remove(player, itemID, group)
end
elseif currentStack < targetStack then
HiddenItemManager:Add(player, itemID, -1, diff, group)
end
end
-- Removes the oldest of a particular hidden item from the specified group.
function HiddenItemManager:Remove(player, itemID, group)
local tab = FindData(GetPlayerKey(player), group, itemID)
if tab then
local removalCandidate
for wispKey, data in pairs(tab) do
if not removalCandidate or data.AddTime < removalCandidate.AddTime then
removalCandidate = data
end
end
RemoveWisp(removalCandidate.WispKey)
end
end
-- Removes all copies of a particular item from the specified group.
function HiddenItemManager:RemoveStack(player, itemID, group)
local tab = FindData(GetPlayerKey(player), group, itemID)
if tab then
for wispKey, _ in pairs(tab) do
RemoveWisp(wispKey)
end
end
end
-- Removes all hidden items from the specified group.
function HiddenItemManager:RemoveAll(player, group)
local tab = FindData(GetPlayerKey(player), group)
if tab then
for itemID, wispList in pairs(tab) do
for wispKey, _ in pairs(wispList) do
RemoveWisp(wispKey)
end
end
end
end
-- Returns true if the player has the given item within the specified group.
function HiddenItemManager:Has(player, itemID, group)
local tab = FindData(GetPlayerKey(player), group, itemID)
return tab ~= nil and next(tab) ~= nil
end
-- Returns how many hidden copies of a given item the player has within the specified group.
function HiddenItemManager:CountStack(player, itemID, group)
local tab = FindData(GetPlayerKey(player), group, itemID)
if not tab then return 0 end
local count = 0
for key, data in pairs(tab) do
count = count + 1
end
return count
end
-- Returns a table representing all of the item effects a player currently has from a specified group.
function HiddenItemManager:GetStacks(player, group)
local tab = FindData(GetPlayerKey(player), group)
local output = {}
if tab then
for itemID, _ in pairs(tab) do
local count = HiddenItemManager:CountStack(player, itemID, group)
if count > 0 then
output[itemID] = count
end
end
end
return output
end
--------------------------------------------------
-- Save/Load
local function TableSize(tab)
local count = 0
for k, v in pairs(tab) do
count = count + 1
end
return count
end
-- Returns the table that should be included in your SaveData when you save the game.
-- Pass this table into HiddenItemManager:LoadData() when you load your SaveData.
function HiddenItemManager:GetSaveData()
LOG("Saving wisp index of size: " .. TableSize(INDEX))
return {
INDEX = INDEX,
}
end
-- Should be called whenever you load the SaveData for your mod to re-initialize any existing item wisps.
-- Give it the table returned by HiddenItemManager:GetSaveData().
function HiddenItemManager:LoadData(saveData)
if saveData then
INDEX = saveData.INDEX or {}
for _, data in pairs(INDEX) do
data.Initialized = false
end
else
INDEX = {}
end
DATA = {}
HiddenItemManager.INITIALIZING = false
-- Check & re-initialize all existing wisps, just in case.
local oldPtrs = WISP_PTRS
WISP_PTRS = {}
for _, ptr in pairs(oldPtrs) do
if ptr and ptr.Ref then
HiddenItemManager:ItemWispUpdate(ptr.Ref:ToFamiliar())
end
end
HiddenItemManager:CheckWisps()
end
--------------------------------------------------
-- Wisp Handling
function HiddenItemManager:ItemWispUpdate(wisp)
if HiddenItemManager.INITIALIZING then return end
local wispKey = GetWispKey(wisp)
local wispData = INDEX[wispKey]
if wispData then
KeepWispHidden(wisp)
ApplyPersistentHiddenItemManagerMark(wisp)
local player = wisp.Player
local playerKey = GetPlayerKey(player)
if not IsManagedWisp(wisp) or not WISP_PTRS[wispKey] then
-- This wisp isn't marked as one of our wisps, but we're supposed to have a wisp with this InitSeed.
-- OR: we don't have a pointer to this wisp cached, meaning we may have reloaded a save or something.
-- Check if there's already an active wisp for this effect.
local existingWisp = GetWisp(wispData)
if existingWisp or not player or not playerKey then
-- Another wisp with this InitSeed already exists.
-- This can happen with Bazarus - familiars seem to get recreated to some extent when he flips.
-- Remove this one regardless, we don't want two wisps with the same seed.
KillWisp(wisp)
return false
end
-- Most likely, we've quit and continued a run, reloaded a save, or luamodded, or something.
-- Re-initialize this wisp as a hidden one.
InsertData(playerKey, wispData.Group, wispData.Item, wispKey, wispData)
InitializeWisp(wisp)
end
-- Check if timed wisp has expired.
local timedOut = (wispData.Duration and wispData.AddTime + wispData.Duration < game:GetFrameCount())
-- Remove the wisp if the player disappears or seems to get replaced.
local playerGone = (not player or playerKey ~= wispData.PlayerKey or not EntityRef(player).Entity:Exists())
if timedOut or playerGone then
RemoveWisp(wispKey)
KillWisp(wisp)
return false
end
elseif IsManagedWisp(wisp) then
-- No data for this wisp, but it's marked as one of ours. Kill it.
KeepWispHidden(wisp)
KillWisp(wisp)
return false
end
if IsManagedWisp(wisp) then
return false
end
end
AddCallback(ModCallbacks.MC_FAMILIAR_INIT, HiddenItemManager.ItemWispUpdate, FamiliarVariant.ITEM_WISP)
AddCallback(ModCallbacks.MC_FAMILIAR_UPDATE, HiddenItemManager.ItemWispUpdate, FamiliarVariant.ITEM_WISP)
function HiddenItemManager:ItemWispLateUpdate(wisp)
if HiddenItemManager.INITIALIZING then return end
local wispKey = GetWispKey(wisp)
local wispData = INDEX[wispKey]
if not wispData and not IsAnyHiddenItemManagerWisp(wisp) and WasHiddenItemManagerWisp(wisp) then
-- This wisp was at one point a HiddenItemManager wisp, but no instance of HiddenItemManager has claimed it. Kill it.
KeepWispHidden(wisp)
KillWisp(wisp)
return false
end
end
AddLateCallback(ModCallbacks.MC_FAMILIAR_UPDATE, HiddenItemManager.ItemWispLateUpdate, FamiliarVariant.ITEM_WISP)
function HiddenItemManager:CheckWisps()
for _, wisp in pairs(Isaac.FindByType(EntityType.ENTITY_FAMILIAR, FamiliarVariant.ITEM_WISP)) do
HiddenItemManager:ItemWispUpdate(wisp:ToFamiliar())
end
end
function HiddenItemManager:PostGameStarted(continuing)
HiddenItemManager.INITIALIZING = false
HiddenItemManager:CheckWisps()
HiddenItemManager:PostNewRoom()
end
AddLateCallback(ModCallbacks.MC_POST_GAME_STARTED, HiddenItemManager.PostGameStarted)
function HiddenItemManager:PostPlayerInit()
local numPlayers = #Isaac.FindByType(EntityType.ENTITY_PLAYER, -1, -1, false, false)
if numPlayers == 0 then
-- New run or continued run.
DATA = {}
INDEX = {}
WISP_PTRS = {}
HiddenItemManager.INITIALIZING = true
end
end
AddCallback(ModCallbacks.MC_POST_PLAYER_INIT, HiddenItemManager.PostPlayerInit)
function HiddenItemManager:PlayerUpdate(player)
player:GetData().hiddenItemManagerLastUpdate = game:GetFrameCount()
end
AddCallback(ModCallbacks.MC_POST_PLAYER_UPDATE, HiddenItemManager.PlayerUpdate)
local function IsActivePlayer(player)
if not player then return false end
local lastUpdate = player:GetData().hiddenItemManagerLastUpdate
return lastUpdate and game:GetFrameCount() - lastUpdate <= 1
end
function HiddenItemManager:PostUpdate()
if HiddenItemManager.INITIALIZING then
LOG_ERROR("Initialization may not have finished correctly? Did someone return non-nil in MC_POST_GAME_STARTED?")
HiddenItemManager.INITIALIZING = false
end
if HiddenItemManager.DoingSacrificialAltarProtection then
LOG_ERROR("Sacrificial Altar protection didn't finish on MC_USE_ITEM - was the activation canceled?")
HiddenItemManager:FinishSacrificialAltarProtection()
end
local wispsToRespawn = {}
for key, data in pairs(INDEX) do
local wisp = GetWisp(data)
local player = GetPlayer(data)
-- Ignore missing wisps if the player isn't found (could be due to something like Bazarus).
if data.Initialized and not wisp and IsActivePlayer(player) then
if data.ErrorCount >= 10 then
-- We tried to respawn a wisp like 10 times in a row, just give up.
LOG_ERROR("Something is constantly removing the Item Wisps or preventing them from spawning! Giving up on item #" .. data.Item .. " from group: " .. data.Group)
RemoveWisp(key)
else
if data.ErrorCount == 0 then
LOG_ERROR("Wisp disappeared unexpectedly! Respawning wisp for item #" .. data.Item .. " from group: " .. data.Group)
end
wispsToRespawn[key] = data
data.ErrorCount = data.ErrorCount + 1
end
end
end
-- When wisps disappear unexpectedly, try to respawn them at least a few times.
-- We won't try forever, however, to avoid infinite fights with another mod.
for oldKey, data in pairs(wispsToRespawn) do
local player = GetPlayer(data)
RemoveWisp(oldKey)
local wisp = player:AddItemWisp(data.Item, kWispPos)
local newKey = GetWispKey(wisp)
InsertData(GetPlayerKey(player), data.Group, data.Item, newKey, data)
INDEX[newKey] = data
InitializeWisp(wisp)
end
end
AddCallback(ModCallbacks.MC_POST_UPDATE, HiddenItemManager.PostUpdate)
function HiddenItemManager:PostNewRoom()
for key, data in pairs(INDEX) do
if data.RemoveOnNewRoom and data.AddTime < game:GetFrameCount() then
RemoveWisp(key)
else
data.ErrorCount = 0
end
end
HiddenItemManager:CheckCostumes()
end
AddCallback(ModCallbacks.MC_POST_NEW_ROOM, HiddenItemManager.PostNewRoom)
function HiddenItemManager:PostNewLevel()
for key, data in pairs(INDEX) do
if data.RemoveOnNewLevel and data.AddTime < game:GetFrameCount() then
RemoveWisp(key)
end
end
end
AddCallback(ModCallbacks.MC_POST_NEW_LEVEL, HiddenItemManager.PostNewLevel)
-- Disables collisions for wisps.
function HiddenItemManager:ItemWispCollision(wisp)
if IsManagedWisp(wisp) then
return true
end
end
AddCallback(ModCallbacks.MC_PRE_FAMILIAR_COLLISION, HiddenItemManager.ItemWispCollision, FamiliarVariant.ITEM_WISP)
-- Prevents wisps from taking or dealing damage.
function HiddenItemManager:ItemWispDamage(entity, damage, damageFlags, damageSourceRef, damageCountdown)
if entity and entity.Type == EntityType.ENTITY_FAMILIAR and entity.Variant == FamiliarVariant.ITEM_WISP and IsManagedWisp(entity) then
return false
end
if damageSourceRef.Type == EntityType.ENTITY_FAMILIAR and damageSourceRef.Variant == FamiliarVariant.ITEM_WISP
and damageSourceRef.Entity and IsManagedWisp(damageSourceRef.Entity) then
return false
end
end
AddCallback(ModCallbacks.MC_ENTITY_TAKE_DMG, HiddenItemManager.ItemWispDamage)
-- Prevents wisps from firing tears with book of virtues.
function HiddenItemManager:ItemWispTears(tear)
if tear.SpawnerEntity and tear.SpawnerEntity.Type == EntityType.ENTITY_FAMILIAR
and tear.SpawnerEntity.Variant == FamiliarVariant.ITEM_WISP
and IsManagedWisp(tear.SpawnerEntity) then
tear:Remove()
return true
end
end
AddCallback(ModCallbacks.MC_POST_TEAR_INIT, HiddenItemManager.ItemWispTears)
-- Protect the wisp from Sacrificial Altar by breaking its connection to the player briefly.
-- Thanks DeadInfinity for coming up with this trick.
function HiddenItemManager:StartSacrificialAltarProtection()
LOG("Detected Sacrificial Altar activation. Temporarily nulling wisp.Player...")
for _, data in pairs(INDEX) do
local wisp = GetWisp(data)
if wisp then
wisp:GetData().hiddenItemManagerCachedPlayer = wisp.Player
-- Should already be removed from the orbit, but call RemoveFromOrbit again just to be sure.
-- Setting the player to nil for a wisp thats currently in orbit crashes the game!
wisp:RemoveFromOrbit()
wisp.Player = nil
end
end
HiddenItemManager.DoingSacrificialAltarProtection = true
LOG("Sacrificial Altar handling underway...")
end
AddCallback(ModCallbacks.MC_PRE_USE_ITEM, HiddenItemManager.StartSacrificialAltarProtection, CollectibleType.COLLECTIBLE_SACRIFICIAL_ALTAR)
-- Restore the wisp's player connection after Sacrificial Altar is done (thanks again, Dead).
function HiddenItemManager:FinishSacrificialAltarProtection()
LOG("Detected Sacrificial Altar resolution. Fixing wisp.Player...")
HiddenItemManager.DoingSacrificialAltarProtection = nil
for key, data in pairs(INDEX) do
local wisp = GetWisp(data)
if wisp then
local player = wisp:GetData().hiddenItemManagerCachedPlayer or GetPlayer(data)
if player then
wisp.Player = player
else
LOG_ERROR("Somehow lost track of player during Sacrificial Altar protection. Giving up on item #" .. data.Item .. " from group: " .. data.Group)
if wisp then
wisp.Player = Isaac.GetPlayer() -- De-nil `wisp.Player` to avoid crashing if this somehow happens.
end
RemoveWisp(key)
end
wisp:GetData().hiddenItemManagerCachedPlayer = nil
end
end
LOG("Sacrificial Altar handling completed.")
end
AddCallback(ModCallbacks.MC_USE_ITEM, HiddenItemManager.FinishSacrificialAltarProtection, CollectibleType.COLLECTIBLE_SACRIFICIAL_ALTAR)
AddCallback(ModCallbacks.MC_USE_ITEM, function()
LOG("Detected Genesis activation. Clearing all wisps.")
INDEX = {}
DATA = {}
for _, ptr in pairs(WISP_PTRS) do
if ptr and ptr.Ref then
HiddenItemManager:ItemWispUpdate(ptr.Ref:ToFamiliar())
end
end
WISP_PTRS = {}
LOG("Genesis handling completed.")
end, CollectibleType.COLLECTIBLE_GENESIS)
return HiddenItemManager