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 proficiency books' effect on crafting #48650
Conversation
I am moving fixes 2 and 3 to a separate PR (#48658), since fixing 1 is going to take a while. Please review that first. |
Looking good. |
I found a simple solution that seems to work. I'm caching the calls to rec.batch_time() in craft_activity_actor::do_turn(). The cache is invalidated when crafting speed or assistants change, and at 5% progress increments when proficiencies may be gained. Playtesting confirms that the massive slowdown is eliminated by doing this. I used debug messages to verify that the cache is re-populated only at the 5% increment marks under normal conditions. PR updated. |
Standard practice would be to make the members involved in the caching |
Thanks, I'm ashamed to admit that I didn't know C++ had a "mutable" keyword. Sounds good, I'll give that a shot. |
I was stuck in "const hell" for a while because item_location code is really not designed to work with const item pointers. I bailed out because I think it would take a lot of work to change that. I also noticed that the crafting failure formula is making recipes X times more likely to succeed instead of X times less likely when you're missing proficiencies (and it's also not affected by books), so I need to investigate/fix that before marking the PR as ready. |
All the fixes are in, ready for review now. I would prefer to get #48658 and #48673 in first (although this PR contains the work from the others too). That would allow me to rebase and shrink this PR (actually not sure if rebase is needed or GitHub will recompute the diff against master's head automatically). |
f695f18
to
943da8d
Compare
943da8d
to
ffdbbcd
Compare
There is no bonus to mitigation, if you mitigate it can only to at best to 0x, which pushes it closes to the normal rates of 50% chance success or the value of 1. Any increment of the multiplier will increment the value of 1 a degree higher. It still takes more time and is less effective than having proficiency. It's just unclear to people because they don't know where the normal point exists between the calculations. It's tied to where you have a 50% success chance. Having proficiency already provides a bonus effect. You can even see the difference in time between the normal value and the proficiency mitigating closer to 0x. It's where all the multipliers are being scaled off of, which is why you get some weird numbers sometimes, but it should all just be functioning normally, they just don't increment normally in the way you think they should, especially when mitigated. The bonus tied to having a proficiency is considered a -.5x time multiplier with a 1x more likely to succeed than normal. You treat 1x just like you would treat 100%. This should be 100% (after being fixed) but it's currently 75% now. It's confusing to show for players for reason, the time multiplier isn't really time, it just from the normal recipe the scale the rest of the craft off. You can treat it as a bonus though if it makes it more clear. It does decrease the duration to craft though but is more related to how much progress to the degree of item completion you're awarded per tick. |
Before I add this to the pile, are there any string changes for translation in here? I see on the other pr you were conscious of the concerns there. |
I'm not seeing any. |
ffdbbcd
to
36c62f9
Compare
Correct, I avoided modifying any strings in order to make it possible to put this in 0.F. |
0f8ef84
to
d364d4d
Compare
The commit that made that #49048 necessary was reverted on master. |
Thanks for the clarification. If the reversion you're talking about is #49053 then I already picked it up when I rebased so I expect to not need another rebase. I see that the tests appear to be passing (though not all have finished). |
Also makes Character::crafting_inventory() const.
f6cc2c1
to
ec35ae9
Compare
I've updated the PR (rebased on master now that #48658 has been merged), reducing the number of diffs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the const
additions here are generally a step in the right direction, so thanks for that.
I haven't understood the details of exactly how your other changes affect the logic. I'd be happier if you could add a unit test that fails with the old code but passes with the new code. There's already some crafting tests that you could hopefully use for inspiration.
@@ -561,7 +554,7 @@ const inventory &Character::crafting_inventory( const tripoint &src_pos, int rad | |||
cached_crafting_inventory->form_from_map( inv_pos, radius, this, false, clear_path ); | |||
} | |||
|
|||
for( const item_location &it : all_items_loc() ) { | |||
for( const item_location &it : const_cast<Character *>( this )->all_items_loc() ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the morally correct thing here would be to introduce a new const_item_location
class which is like item_location
, except with const access to the underlying item, and then have a const
overload of all_items_loc
which uses that. It looks like cached_crafting_inventory.add_item
copies its argument, so I think that should work.
That's the sort of thing that could reasonably go in a follow-up PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still have a bit of PTSD from the last time I tried propagating const-ness to item_location. I would definitely prefer to leave it for another PR since even if I succeed with this new approach it will introduce a lot of extra diffs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment suggesting that this would be a good thing to add, perhaps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
I'll work on adding a unit test in crafting_test.cpp today. In order to avoid brittle expectations such as finishing the craft in a specific number of turns I'll probably just compare the time taken to craft with and without a book present. [Done] |
Is a further check vs how long with the actual proficiency (that that's less time still) desirable? |
Good idea. The time taken with the book should be strictly between the time taken with no prof and no mitigation and time taken with the prof. [Done] |
99f78f1
to
959440e
Compare
959440e
to
f46c907
Compare
Summary of key changes:
The caching in craft_activity_actor::do_turn (activity_actor.cpp:1769) was necessary to fix a performance issue resulting from the above changes. The newly added test fails on master because of the first check ( |
dbc8e65
to
6ffd7e5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I can tell this is good to go. I would not mind seeing a few more comments on it for future editors. I will clear with a more versed person before merging.
@@ -561,7 +554,7 @@ const inventory &Character::crafting_inventory( const tripoint &src_pos, int rad | |||
cached_crafting_inventory->form_from_map( inv_pos, radius, this, false, clear_path ); | |||
} | |||
|
|||
for( const item_location &it : all_items_loc() ) { | |||
for( const item_location &it : const_cast<Character *>( this )->all_items_loc() ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment suggesting that this would be a good thing to add, perhaps?
* Fix proficiency books' effect on crafting * Cache recipe batch time in craft_activity_actor::do_turn() * Make crafting time functions const and/or operate on const Character. Also makes Character::crafting_inventory() const. * add a unit test * also check that proficiency mitigation is not as good as having prof * use a struct to encapsulate the crafting inventory cache * clean up one hack * Add TODO for getting rid of const_cast
Summary
Bugfixes "Fix proficiency books' effect on crafting"
Purpose of change
Fixes #48465
PR #46347 added the ability for books to partially mitigate the crafting penalties incurred due to missing proficiencies, as described in FR #44680. However, it introduced some issues.
It changed the crafting UI but didn't actually change the crafting time. If you read recipe::time_to_craft_moves() it's pretty clear that book proficiencies are not used. The total time taken to craft an item when you're missing proficiencies is unaffected by books (verified via gameplay as well). Reported in Mitigated proficiency penalties do not apply correctly to crafting time #48465
It implemented the mitigation formula incorrectly, which would result in books letting you craft faster when missing a proficiency than when you had the proficiency (if they actually took effect). This caused player confusion, e.g. 0.1x as long as normal crafting from mitigated proficiency #46662
When a book provides a proficiency time_factor of 0.1, a malus of 1.5 due to lacking that proficiency should become 1.05 (1 + 0.5 * 0.1) not 0.15 (1.5 * 0.1). The book is supposed to reduce the penalty, not turn it into a bonus.
The formula for stacking mitigation effects from multiple books is also wrong and can result in negative proficiency malus.
With 2 books that provide time_factors of 0.1 and 0.5, the formula from Assisting with proficiencies through books #44680 (as implemented in the current code) yields a combined time_factor of 1 - sqrt((1 - 0.1)^2 + (1 - 0.5)^2) = -0.03, which is nonsense.
The book proficiency mitigation effect on the failure rate was also not getting applied
(in addition to the penalty acting as a bonus, see Fix the crafting success formula when crafter is missing proficiencies. #48673)Describe the solution
Unresolved issues:it makes the game slow to a crawl when crafting in the middle of a pile of books and ingredients[fixed]it removes const qualifiers from a bunch of APIs, which feels very wrong[fixed]The slowdown was due to calling Character::crafting_inventory() from the proficiency_time_maluses() added to recipe::time_to_craft_moves(), which is called many times while crafting from craft_activity_actor::do_turn(). Character::crafting_inventory() is an expensive member function of the player object that needs to scan all nearby items. It's non-const (hence the removal of const qualifiers) because it does caching. But the cache is only valid for one turn, which doesn't help in this situation.
The fix for this was to cache the results of the two rec.batch_time() calls in craft_activity_actor::do_turn(). The cache is invalidated when crafting speed or assistants change, and at 5% progress increments when proficiencies may be gained.
Describe alternatives you've considered
Rolling back #46347. But I like the idea and I really want this to work.
Not sure if I need to also check for a change in craft.charges as a condition that invalidates the cache.[no, it doesn't change] On the other hand, it may be possible to remove the check for changing speed and assistants (which may also slow down crafting due to looking around the map) and rely entirely on the 5% progress updates. We might be using the wrong crafting times sometimes, but not for longer than 5% of the crafting progress. But, 5% could be a long time for some recipes.Comparing floating point numbers in the cache invalidation check is probably not ideal. Maybe use epsilon equality.
Considered changing craft_activity_actor::start() to calculate the total number of moves required there instead of in craft_activity_actor::do_turn(). But the number of moves may change during crafting.
To avoid propagating non-const references, I have 2 ideas for changing Character::crafting_inventory() to be a const member function. One is to put the crafting inventory cache into a static global variable, the other is to use a const_cast inside that function to force-allow modification of the cache object. I can't say that I'm a big fan of either approach.
Testing
Build 2 game binaries, one without this change ("before") and one with ("after").
Using "before", start a new game in evac shelter. Debug all skills to 10 and INT to 20. Debug unlock all recipes. Debug the Life Support, HVAC mutations. Move next to a computer terminal for the light (but not next to any counter, which boosts crafting speed) or spawn and activate an atomic light. Debug kill all monsters. Debug spawn a plank and 2 long strings. Save.
Bring up the crafting menu and look at "short bow".
Reload (still using "before"). Spawn "The Bowyer's Buddy", and bring up the crafting UI again.
Switch to the "after" binary and reload. Verify that the crafting UI looks the same as in "before" without a book, and you can craft the bow in 20 hours (finish at 4am).
To measure performance regression, time the operation. On my computer it took 31 seconds, which agrees with the "before" timing within measurement error.
Reload. Spawn the book. The UI should now look like this:
Here's a more complicated example with a multiple-proficiency craft and a pile of books.
Before
After
Additional context
For my next PR I plan to implement another of Erk's ideas from #44680:
Once we have the ability to reduce proficiency penalties by a factor, the same infrastructure could and should be used to make proficiency penalties gradually fall off as you get close to the necessary xp to have the proficiency.