Closed NickStefan closed 4 months ago
Thanks for the PR.
RE: the seed string, it might be better to make it part of CHANCE rather than an entirely new section. Something like ChanceXXXX(100)? Also any reason you're using a string and not an integer directly?
Thanks for looking at this.
I'll try and fold the seed into the CHANCE section. Given that, I agree that providing a seed integer makes more sense since the chance section deals with numbers. There wasn't a great reason for making a string.
I've updated the code so that the seed is now an optional argument when setting a chanceR value, e.g. chanceR(80, 1234)
.
Passing the seed to chanceR should also work for swap types that predate this PR (Barrel01|Barrel02|NONE|ChanceR(80,12345)
). This provides an alternative to using the refHash; it can be randomly changed by changing the seed, but it's also going to be stable across save games and game loads.
For the feature I originally proposed--multiple base forms with random assignment--it allows doing something like this:
DoomstoneWarrior,DoomstoneMage,DoomstoneThief|DoomstoneWarrior,DoomstoneMage,DoomstoneThief|NONE|chanceR(100,350)
This would randomly swap these, using 350 as the RNG seed when assigning pairs.
LMK what you think and thanks for the earlier feedback.
Some manual testing that I did:
[Forms]
; seed is NONE, defaults to RefHash randomization
;DoomstoneWarrior,DoomstoneMage,DoomstoneThief,DoomstoneLady,DoomstoneApprentice,DoomstoneLover,DoomstoneSteed,DoomstoneAtronach,DoomstoneShadow,DoomstoneLord,DoomstoneRitual,DoomstoneTower,DoomstoneSerpent|DoomstoneWarrior,DoomstoneMage,DoomstoneThief,DoomstoneLady,DoomstoneApprentice,DoomstoneLover,DoomstoneSteed,DoomstoneAtronach,DoomstoneShadow,DoomstoneLord,DoomstoneRitual,DoomstoneTower,DoomstoneSerpent|NONE|NONE
; swap set isnt a set, just one formid, logs handled error
;DoomstoneWarrior,DoomstoneMage,DoomstoneThief,DoomstoneLady,DoomstoneApprentice,DoomstoneLover,DoomstoneSteed,DoomstoneAtronach,DoomstoneShadow,DoomstoneLord,DoomstoneRitual,DoomstoneTower,DoomstoneSerpent|DoomstoneWarrior|NONE|NONE
; swap set is shorter than base set, logs handled error
;DoomstoneWarrior,DoomstoneMage,DoomstoneThief,DoomstoneLady,DoomstoneApprentice,DoomstoneLover,DoomstoneSteed,DoomstoneAtronach,DoomstoneShadow,DoomstoneLord,DoomstoneRitual,DoomstoneTower,DoomstoneSerpent|DoomstoneWarrior,DoomstoneMage|NONE|NONE
; duplicates, swap set twice as long, should work same as first example
;DoomstoneWarrior,DoomstoneMage,DoomstoneThief,DoomstoneLady,DoomstoneApprentice,DoomstoneLover,DoomstoneSteed,DoomstoneAtronach,DoomstoneShadow,DoomstoneLord,DoomstoneRitual,DoomstoneTower,DoomstoneSerpent|DoomstoneWarrior,DoomstoneMage,DoomstoneThief,DoomstoneLady,DoomstoneApprentice,DoomstoneLover,DoomstoneSteed,DoomstoneAtronach,DoomstoneShadow,DoomstoneLord,DoomstoneRitual,DoomstoneTower,DoomstoneSerpent,DoomstoneWarrior,DoomstoneMage,DoomstoneThief,DoomstoneLady,DoomstoneApprentice,DoomstoneLover,DoomstoneSteed,DoomstoneAtronach,DoomstoneShadow,DoomstoneLord,DoomstoneRitual,DoomstoneTower,DoomstoneSerpent|NONE|NONE
; CHARACTER 1, specific seed
DoomstoneWarrior,DoomstoneMage,DoomstoneThief,DoomstoneLady,DoomstoneApprentice,DoomstoneLover,DoomstoneSteed,DoomstoneAtronach,DoomstoneShadow,DoomstoneLord,DoomstoneRitual,DoomstoneTower,DoomstoneSerpent|DoomstoneWarrior,DoomstoneMage,DoomstoneThief,DoomstoneLady,DoomstoneApprentice,DoomstoneLover,DoomstoneSteed,DoomstoneAtronach,DoomstoneShadow,DoomstoneLord,DoomstoneRitual,DoomstoneTower,DoomstoneSerpent|NONE|chanceR(100,350)
; CHARACTER 2, specific seed
;DoomstoneWarrior,DoomstoneMage,DoomstoneThief,DoomstoneLady,DoomstoneApprentice,DoomstoneLover,DoomstoneSteed,DoomstoneAtronach,DoomstoneShadow,DoomstoneLord,DoomstoneRitual,DoomstoneTower,DoomstoneSerpent|DoomstoneWarrior,DoomstoneMage,DoomstoneThief,DoomstoneLady,DoomstoneApprentice,DoomstoneLover,DoomstoneSteed,DoomstoneAtronach,DoomstoneShadow,DoomstoneLord,DoomstoneRitual,DoomstoneTower,DoomstoneSerpent|NONE|chanceR(100,850)
; Random every launch of skyrim executable
;DoomstoneWarrior,DoomstoneMage,DoomstoneThief,DoomstoneLady,DoomstoneApprentice,DoomstoneLover,DoomstoneSteed,DoomstoneAtronach,DoomstoneShadow,DoomstoneLord,DoomstoneRitual,DoomstoneTower,DoomstoneSerpent|DoomstoneWarrior,DoomstoneMage,DoomstoneThief,DoomstoneLady,DoomstoneApprentice,DoomstoneLover,DoomstoneSteed,DoomstoneAtronach,DoomstoneShadow,DoomstoneLord,DoomstoneRitual,DoomstoneTower,DoomstoneSerpent|NONE|chanceR(100)
;[References]
;0x000E7BDD~Skyrim.esm,0x000E7BD6~Skyrim.esm,0x000E7BDC~Skyrim.esm,0x000E7BDC~Skyrim.esm,0x000DC84A~Skyrim.esm,0x000E0DC2~Skyrim.esm,0x000E0DBE~Skyrim.esm,0x000E0E1F~Skyrim.esm,0x000E0F4A~Skyrim.esm,0x000D1E9F~Skyrim.esm,0x000E0F22~Skyrim.esm,0x000D9630~Skyrim.esm,0x000E0F6F~Skyrim.esm,0x000E0F55~Skyrim.esm|DoomstoneWarrior,DoomstoneMage,DoomstoneThief,DoomstoneLady,DoomstoneApprentice,DoomstoneLover,DoomstoneSteed,DoomstoneAtronach,DoomstoneShadow,DoomstoneLord,DoomstoneRitual,DoomstoneTower,DoomstoneSerpent|NONE|chanceR(100,235663)
Looks good to me.
Feature has been released. Can I have your Nexus username?
Thank you.
Looking at the Base Object Swapper mod page, I believe that the changelog/docs are slightly incorrect with what was merged.
I think the example given will actually log a handled error "SWAP formID set not found".
When we find a FormIDSet for the first ini argument, we expect another FormIDSet for the second argument. So the second argument actually gets turned into an empty set right now if it's only one FormID. This is arguably a bug that I can fix.
origBaseID,origBase2ID,origBase3ID|swapBaseID|propertyOverrides|chance
https://github.com/powerof3/BaseObjectSwapper/blob/master/src/SwapData.cpp#L96
Given an empty set, the code then steps over the if statement and on to log an error message.
if (auto swapFormIDs = util::GetFormIDOrderedSet(formPair[1]); !swapFormIDs.empty()) {
if (baseFormIDs.size() > swapFormIDs.size()) {
logger::error("\t\t\t\tfail : [{}] (SWAP formID set must be equal or larger than BASE formID set)", a_str);
return;
}
// ...
// ...
} else {
logger::error("\t\t\t\tfail : [{}] (SWAP formID set not found)", a_str);
}
So right now it works like: origBaseID,origBase2ID,origBase3ID|swapBaseID|propertyOverrides|chance -> error
origBaseID,origBase2ID,origBase3ID|swapBaseID,swapBaseID2,swapBaseID3|propertyOverrides|chance -> randomly assigns each base to one swap.
I think it would be pretty clean to handle "normal" swaps when the swapBaseID is just one formID. So then we'd have:
origBaseID,origBase2ID,origBase3ID|swapBaseID|propertyOverrides|chance -> swaps all of the origBaseIDs to swapBaseID
origBaseID,origBase2ID,origBase3ID|swapBaseID,swapBaseID2,swapBaseID3|propertyOverrides|chance -> randomly assigns each base to one swap
What
I'd like to modify BaseObjectSwapper to accept multiple base form IDs. When the library encounters multiple base form IDs, the library would then randomly assign each base form to one of the multiple swap ids.
For example, this INI could randomly assign
DoomstoneWarrior
toDoomstoneMage
, and thenDoomstoneMage
would be randomly assigned to one of[DoomstoneWarrior, DoomstoneThief]
, sinceDoomstoneMage
has already been assigned.Why
While it's true that the existing library supports enumerating each swap by hand, you'd have write out each permutation by hand if you wanted to see a different permutation on your next playthrough.
Basically, I think there's room for a swap type that randomizes swaps while also guaranteeing each swap only occurs once.
Details
Player Provided Seed String: I originally wanted to seed the RNG with the current save game's PlayerID, but that doesn't work since BaseObjectSwapper runs before loading specific characters. Then I tried using the randomHash RNG, but that doesn't work well since activators seem to remember whatever their script properties were on the very first game load. Finally, refHashing makes the swapping static across multiple characters, which also didn't feel right. So I settled on accepting a user provided RNG seed like so:
This means you can get a completely new set of swaps just by changing the seed string in one place.
OrderedSet: As I said earlier, activators go nuts if you swap them more than once on a play through. So, for that reason, I wanted the swaps to be deterministic given a specific, player-provided seed string. That means order matters. Therefore, I added an OrderedSet. If that's a deal breaker, I had a version that seemed to work using the unordered set library as well (I guess since the hashing is consistent?). It just felt kind of wrong to rely on read order from an unordered set library.
Testing
Thank you for providing a working CI pipeline. I was able to build and test various test cases on 1.6.640. I originally had more logs for testing, but removed most of them.
While I am a developer, I'm not as experienced with C++, so please feel free to optimize or change the code as you see fit. I wont be offended :).