Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix infinite loop in unique item randomization #7370

Merged
merged 2 commits into from
Sep 4, 2024

Conversation

obligaron
Copy link
Contributor

I tried to create some debug items for testing (with dev.items.spawn) and sometimes I encountered an infinite loop in TryRandomUniqueItem.

After some debugging I found out that it is trying to generate one of the following unqiues

  • The Grandfather
  • Schaefer's Hammer
  • Amulet of Warding

The problem can be reproduced with debug lua commands dev.items.spawnUnique command and one of the above uniques.

The cause was that the uid offset was not always calculated correctly. There are two reasons why this can happen

  1. The valid uniques in CheckUnique and TryRandomUniqueItem can be different because the check against UIMinLvl is different (CheckUnique calculates it with GetItemBLevel and TryRandomUniqueItem has a custom implementation). So the calculated offset can be different or the desired unique can't be generated by CheckUnique.
  2. When calculating the offset the check against UIMinLvl is == and not >= as in CheckUnique.

All this can only happen for some specific uniques or item levels, so the problem is not so easy to get in normal gameplay.

This PR

  • fixes the offset calculation by
    • generating the item level the same way as SetupAllItems does
    • reuse the logic for getting valid uniques from CheckUnique
  • simplifies item generation (we can reuse the same seed and set the required offset)
  • doesn't affect existing items, because the logic in CheckUnique remains the same

@kphoenix137
Copy link
Collaborator

Hold that thought. I seem to have some uniques vanishing before I can reach them. Also it appears to be generating like vanilla. Let me test further.

@kphoenix137
Copy link
Collaborator

kphoenix137 commented Aug 25, 2024

Okay, so I was able to get some item drops, but the vanished from the ground before I could pick them up:

  • Bramble
  • The Celestial Bow
  • The Bleeder
  • Gonnagal's Dirk
  • Ring of Regha
  • Staff of Shadows
  • The Blackoak Bow
  • The Deflector

(Note: All the other uniques I've encountered so far have not vanished)

This was played in Nightmare, so some of these could legally drop even in vanilla. What it looks like to me is that the uidOffset being utilized is causing the items to be in an invalid state for "impossible items", although there are items that don't require uidOffset at all to be functional, although those items are vanishing as well. So far it does appear that I'm getting mostly "vanilla" style drops (last valid uid), and the ones that aren't last valid uid vanish.

@obligaron
Copy link
Contributor Author

How do you generate the uniques?
I tried to generate Bramble and Bleeder with spawnUnique command and they work (single player hellfire; can pick them up from ground and place them there again).

@kphoenix137
Copy link
Collaborator

kphoenix137 commented Aug 25, 2024

How do you generate the uniques? I tried to generate Bramble and Bleeder with spawnUnique command and they work (single player hellfire; can pick them up from ground and place them there again).

I'm forcing all monsters to take on unique monster status, then spamming chain lightning around the dungeon. I personally don't trust the debug command

@kphoenix137
Copy link
Collaborator

kphoenix137 commented Aug 25, 2024

With this PR logic:

Desktop.2024.08.25.-.16.18.33.03.mp4

Current master logic:

Desktop.2024.08.25.-.16.21.18.04.mp4

Debug modifications:
items.cpp L3381:

} else if (true || monster.isUnique() || dropsSpecialTreasure) {

L3415

	int uper = 15;

L3237

	item._iIdentified = true;

@obligaron
Copy link
Contributor Author

obligaron commented Aug 25, 2024

Thanks @kphoenix137 🙂
I think I found the issue. The item position wasn't set correctly when the unique is (re)created.

This didn't matter in the debug commands, because the position was set after the TryRandomUniqueItem.

@kphoenix137
Copy link
Collaborator

Thanks @kphoenix137 🙂
I think I found the issue. The item position wasn't set correctly when the unique is (re)created.

Thats funny. I remember having the same exact problem when coding this in the first place. I just forgot what the problem was until you mentioned that

@StephenCWills
Copy link
Member

You left your debug code in.

@kphoenix137
Copy link
Collaborator

kphoenix137 commented Aug 26, 2024

Bad news, nearly all the uniques I picked up morph in vanilla (Specifically most of the ones I was picking up were previously on the "vanish" list, so those are the ones being recreated). Are you utilizing uidOffset for more than just the "impossible" uniques? If so, that will certainly break reverse compatibility. uidOffset was introduced as a workaround to allow the impossibles to exist in DevilutionX without morphing on regeneration, and allow them to be moved to vanilla where they would inevitably morph, but still change back into what they were found as when returning to DevilutionX.

The code for selecting a target level was introduced in order to create reverse compatible uniques, as we would first select the uid we want, and then brute force search using target item data to find that unique, that doesn't exactly match the source of the drop, but it successfully allows us to create stable uniques that won't morph in vanilla. The idea was to take advantage of the fact vanilla always selects the last applicable uid, so we'd select a target level that makes sure that the desired uid is the last in the list.

@kphoenix137
Copy link
Collaborator

kphoenix137 commented Aug 26, 2024

I just tested forcing all monsters as unique monsters and forced the War Hammer as the selected idx for every drop, and I was able to successfully get Schaefer's Hammer to drop in the latest master. Maybe there's something wrong with the debug command for generating these uniques?

Edit: Back again, I got an infinite loop in Single Player Hellfire. I'll see what happens in Single Player Diablo

Edit: No infinite loop in SP Diablo

Edit: Infinite loop in SP and MP Hellfire. Looking into it.

@kphoenix137
Copy link
Collaborator

kphoenix137 commented Aug 26, 2024

INFO: SetupAllItems: iblvl: 34
INFO: CheckUnique: (Schaefer's Hammer) Checking if lvl (34) is at least UIMinLvl (16)
INFO: CheckUnique: Added uid 52 to valid uniques vector
INFO: CheckUnique: Added uid 102 to valid uniques vector
INFO: CheckUnique: Selected uid: 102
INFO: TryRandomUniqueItem: Made it past return. Item is Unique, and item is a not a quest unique
INFO: TryRandomUniqueItem: Item._iUid (should be non-zero): 102
INFO: TryRandomUniqueItem: Item._iCreateInfo & CF_UNIQUE: 512
INFO: TryRandomUniqueItem: Item._iMiscId: 0
INFO: TryRandomUniqueItem: Item._iMagical: 2
INFO: TryRandomUniqueItem: We selected new uid: 102
INFO: TryRandomUniqueItem: The new uid matches the old one, returning.

Here, the vanilla algorithm picked Thunderclap, and then TryRandomUniqueItem() also picked Thunderclap, so it didn't need to regenerate.

INFO: SetupAllItems: iblvl: 31
INFO: CheckUnique: (Schaefer's Hammer) Checking if lvl (31) is at least UIMinLvl (16)
INFO: CheckUnique: Added uid 52 to valid uniques vector
INFO: CheckUnique: Added uid 102 to valid uniques vector
INFO: CheckUnique: Selected uid: 102

Vanilla algorithm decides again that it's a Thunderclap.

INFO: TryRandomUniqueItem: Made it past return. Item is Unique, and item is a not a quest unique
INFO: TryRandomUniqueItem: Item._iUid (should be non-zero): 102
INFO: TryRandomUniqueItem: Item._iCreateInfo & CF_UNIQUE: 512
INFO: TryRandomUniqueItem: Item._iMiscId: 0
INFO: TryRandomUniqueItem: Item._iMagical: 2
INFO: TryRandomUniqueItem: We selected new uid: 52
INFO: TryRandomUniqueItem: Potential uper adjustment. uper is: 15
INFO: TryRandomUniqueItem: uidOffset: 0

At this point, TryRandomUniqueItem() decides instead of Thunderclap, we're going to drop Schaefer's Hammer, with no uidOffset and keeping the item at uper15.

INFO: TryRandomUniqueItem: Forcing item generation until we find a match.
INFO: SetupAllItems: iblvl: 16
INFO: TryRandomUniqueItem: item._iUid 0 is not equal to desired uid 52, roll again
INFO: TryRandomUniqueItem: Forcing item generation until we find a match.
INFO: SetupAllItems: iblvl: 16
INFO: TryRandomUniqueItem: item._iUid 0 is not equal to desired uid 52, roll again
INFO: TryRandomUniqueItem: Forcing item generation until we find a match.

This repeats for a bit as it's selecting non-uniques.

INFO: SetupAllItems: iblvl: 16
INFO: CheckUnique: (Schaefer's Hammer) Checking if lvl (16) is at least UIMinLvl (16)
INFO: CheckUnique: Added uid 52 to valid uniques vector
INFO: CheckUnique: Added uid 102 to valid uniques vector
INFO: CheckUnique: Selected uid: 102

We finally roll a unique, but CheckUnique() selects Thunderclap. Both uniques are available. Since the lvl being passed is 16, naturally this should mean it would pick Schaefer's Hammer, however Hellfire added uniques that made the table no longer chronological, so instead of picking Schaefer's Hammer with a min level of 16, it picks Thunderclap with a min level of 13.

INFO: TryRandomUniqueItem: item._iUid 102 is not equal to desired uid 52, roll again
INFO: TryRandomUniqueItem: Forcing item generation until we find a match.
INFO: SetupAllItems: iblvl: 16
INFO: TryRandomUniqueItem: item._iUid 0 is not equal to desired uid 52, roll again
INFO: TryRandomUniqueItem: Forcing item generation until we find a match.
INFO: SetupAllItems: iblvl: 16
INFO: TryRandomUniqueItem: item._iUid 0 is not equal to desired uid 52, roll again
INFO: TryRandomUniqueItem: Forcing item generation until we find a match.
INFO: SetupAllItems: iblvl: 16
INFO: CheckUnique: (Schaefer's Hammer) Checking if lvl (16) is at least UIMinLvl (16)
INFO: CheckUnique: Added uid 52 to valid uniques vector
INFO: CheckUnique: Added uid 102 to valid uniques vector
INFO: CheckUnique: Selected uid: 102

As expected, it continues on and on like this. I truly didn't anticipate this happening as I was testing in Diablo. Maybe you already explained this, but the original post seems overly technical for my understanding. :(

Source/items.cpp Show resolved Hide resolved
Source/items.cpp Show resolved Hide resolved
Source/items.cpp Show resolved Hide resolved
Source/items.cpp Show resolved Hide resolved
Source/items.cpp Outdated Show resolved Hide resolved
Source/items.cpp Outdated Show resolved Hide resolved
Source/items.cpp Outdated Show resolved Hide resolved
@obligaron obligaron force-pushed the spawnunique branch 3 times, most recently from eee54c3 to 57974a8 Compare August 28, 2024 17:44
Copy link
Member

@StephenCWills StephenCWills left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found a new typo

test/items_test.cpp Outdated Show resolved Hide resolved
test/items_test.cpp Outdated Show resolved Hide resolved
test/items_test.cpp Outdated Show resolved Hide resolved
@StephenCWills StephenCWills merged commit 138f937 into diasurgical:master Sep 4, 2024
23 checks passed
@AJenbo
Copy link
Member

AJenbo commented Sep 4, 2024

Nice work, infinite loops are terrible

@obligaron obligaron deleted the spawnunique branch September 8, 2024 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants