Crafting jobs don't resume after loading a saved world
Mithi83 opened this issue ยท 6 comments
Describe the bug
A crafting job that is still running when leaving the world will not resume when loading that world again.
How to reproduce the bug
- Create autocrafting setup
- Start a long running job (in my case 400 Iron Blocks)
- Click "Save and Quit to Title" while the job is still running
- Load that world back up
- Inspect the crafting status and notice no progress
Expected behavior
Crafting resumes after loading the world.
Additional details
No response
Which minecraft version are you using?
1.21
On which mod loaders does it happen?
NeoForge
Crash log
Selfcompiled main branch
I added a second crafting storage to the setup and it gets even stranger. In the broken state after rejoining a world with a crafting job in progress that will now hang forever, starting another crafting job of the same pattern works without any problems.
With the debugger I could narrow down the point where it fails to this:
- In CraftingCpuLogic.executeCrafting() we try to loop over all craftingService.getProviders(details). This comes back empty.
- Within craftingService.getProviders we call craftingProviders.getMediums(key). This comes back empty.
- Within craftingProviders.getMediums() we call this.craftingMethods.get(key) on the hashmap. This comes back empty.
- And within the HashMap we retrieve the AEItemKey.hashCode() and this differs between the working and the broken calls.
This can be traced back all the way to CraftingCpuLogic.executeCrafting() where we get details = task.getKey(). This basically means that it is another instance of AECraftingPattern that was loaded when rejoining the world, even though after a superficial look it seems to have the same values inside. The key point is that the hash is different and it therefore cannot find a provider for that non-existing AECraftingPattern.
Next step would be to trace it back to the place where the tasks are loaded from the file, but that will probably be a debugging session for another day.
Some new observations: hashCode is not constant. Restarting the game multiple times (not just saving/rejoining, but rather quitting the java process entirely) yields different hashCode values for the task that was running across a rejoin as well as the available pattern in the pattern provider between restarts. Just reloading yields different hashCode values for the task saved in the NBT of the crafting cpu, however the hashCode provided by the pattern in the pattern provider is constant.
There are two different code paths involved in getting these hashCodes. One is CraftingCpuLogic.readFromNBT which uses AEItemKey.fromTag(), the other is PatternProviderLogic.updatePatterns which ultimately uses AEItemKey.of(). The former is not constant across rejoins, the latter is constant.
A bit of debug-printing in a minimal setup (1 CPU, 1 Pattern Provider, 1 Molecular Assembler) revealed the following: The CraftingCpuLogic tries to call executeCrafting() and gets different results depending on whether we are in a working scenario before the save&quit or in a non-working scenario after a rejoin. In a working scenario the for loop over craftingService.getProviders(details) finds the provider, after a rejoin the for loop does not print anything, leading me to the conclusion that craftingService.getProviders(details) returns an empty container. I'll investigate further at a later point.
ItemStack.hashItemAndComponents(((ItemStack)((EncodedCraftingPattern)((Optional)((it.unimi.dsi.fastutil.objects.AbstractReference2ObjectMap.BasicEntry)((it.unimi.dsi.fastutil.objects.Reference2ObjectArrayMap)((PatchedDataComponentMap)((ItemStack)((AEItemKey)((AECraftingPattern)((HashMap.Node)((HashMap)craftingProviders.craftingMethods).entrySet().toArray()[0]).getKey()).definition).stack).components).patch).entrySet().toArray()[0]).getValue()).value).result))
result=-1276180869
ItemStack.hashItemAndComponents((ItemStack)((EncodedCraftingPattern)((Optional)((it.unimi.dsi.fastutil.objects.AbstractReference2ObjectMap.BasicEntry)((it.unimi.dsi.fastutil.objects.Reference2ObjectArrayMap)((PatchedDataComponentMap)((ItemStack)((AEItemKey)((AECraftingPattern)key).definition).stack).components).patch).entrySet().toArray()[0]).getValue()).value).result)
result=-1276180869
is equal but not
((ItemStack)((EncodedCraftingPattern)((Optional)((it.unimi.dsi.fastutil.objects.AbstractReference2ObjectMap.BasicEntry)((it.unimi.dsi.fastutil.objects.Reference2ObjectArrayMap)((PatchedDataComponentMap)((ItemStack)((AEItemKey)((AECraftingPattern)((HashMap.Node)((HashMap)craftingProviders.craftingMethods).entrySet().toArray()[0]).getKey()).definition).stack).components).patch).entrySet().toArray()[0]).getValue()).value).result).hashCode()
result=536009613
((ItemStack)((EncodedCraftingPattern)((Optional)((it.unimi.dsi.fastutil.objects.AbstractReference2ObjectMap.BasicEntry)((it.unimi.dsi.fastutil.objects.Reference2ObjectArrayMap)((PatchedDataComponentMap)((ItemStack)((AEItemKey)((AECraftingPattern)key).definition).stack).components).patch).entrySet().toArray()[0]).getValue()).value).result).hashCode()
result=1302252191
In the debugger I can't see a difference between the two results (marked in the picture with green handdrawn dots). Calling .hashCode(), as would a .hashCode() on the higher layers yields different results. Calling .hashCode() on result.getItem() yields identical results, as does calling .hashCode() on result.components. The other 3 values of result (count =1, popTime = 0, entityRepresentation = null) are the same and should not lead to different .hashCode() values of result, which makes me wonder what then leads to this behavior and how the actual implementation of .hashCode() in that class would be implemented.
What is somewhat strange with respect to other classes is that there is no explicit hashCode() implementation, just the ItemStack.hashItemAndComponents() that disregards some parts of the data. I'm not qualified enough to decide if that is a problem or not. This https://stackoverflow.com/questions/2237720/what-is-an-objects-hash-code-if-hashcode-is-not-overridden lets me think it is a problem indeed. Another idea I had is that maybe ItemStack must not be used in classes that should be hashed to prevent this. Either way this is way too low-level for me to debug further with my current skill set and I'd appreciate some help or deeper insight.
When in doubt just play around with the code... I tested my hypothesis that ItemStack should not be used in EncodedCraftingPattern and replaced it with GenericStack instead, as it is used in EncodedProcessingPattern. This works a little bit. My test case for crafting iron blocks would resume after a world load. There have been two drawbacks at least:
- It breaks existing saves as the format of EncodedCraftingPattern is changed.
- It does not work for patterns with empty slots right now, at least in my experimental implementation. Encoding a pattern for a glowstone block for example crashed right away.
I'd assume that a breaking change in the file format will not be an acceptable solution anyway, so my quest continues... Next try will be overriding equals() and hashCode() for EncodedCraftingPattern.