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

Repeatable crash with Coal/Charcoal Piles #3330

Closed
Pursec opened this issue Dec 24, 2023 · 3 comments
Closed

Repeatable crash with Coal/Charcoal Piles #3330

Pursec opened this issue Dec 24, 2023 · 3 comments
Labels
department: code Issues apparently caused by code status: resolved Job's done! version: 1.19 Issues introduced in 1.19
Milestone

Comments

@Pursec
Copy link

Pursec commented Dec 24, 2023

Game Version

v1.19-rc1

Platform

Windows

Modded

Vanilla

SP/MP

Singleplayer

Description

Was working on my own Piles for a mod and verifying functionality for 1.19rc1 (Rot and Salt Piles) and came across an issue regarding GetSelectionBoxes and GetCollisionBoxes giving an Index out of bounds exception resulting in an immediate crash and consequent crashes when loading into the world for a variety initial call reasons (pathfinding, mouse-hovering, etc...) with the same above two methods giving Index out of bounds. After some extensive testing and digging, I found these crashes could also be repeated with Vanilla coal and charcoal piles due to a series of odd behavior and events due to TryPartialCollapse implementation.

How to reproduce

The issue occurs due to a BlockEntityPiles Layer property returning a negative value, such as -1 and then being used as an Index for the CollisionBoxesByFillLevel array.

The Layer property is calculated as such in BECoalPile:
public int Layers => inventory[0].StackSize == 1 ? 1 : inventory[0].StackSize / 2 ;

Due to how PartialCollapses are written a pile can sometimes "collapse" and be left with a stacksize < 1.

This is due to a collapse always entailing a transfer of 2.

if (Api.World.Rand.NextDouble() < collapsibleLayerCount / (float)maxSteepness)
{
    if (TryPartialCollapse(npos.UpCopy(), 2)) return;
}
private bool TryPartialCollapse(BlockPos pos, int quantity)
{
    ...
    ...
    ...
    if (entity == null)
    {
        int prevstacksize = inventory[0].StackSize;

        inventory[0].Itemstack.StackSize = quantity;
        EntityBlockFalling entityblock = new EntityBlockFalling(Block, this, pos, null, 1, true, 0.05f);
        entityblock.DoRemoveBlock = false; // We want to split the pile, not remove it 
        world.SpawnEntity(entityblock);
        entityblock.ServerPos.Y -= 0.25f;
        entityblock.Pos.Y -= 0.25f;

        inventory[0].Itemstack.StackSize = prevstacksize - quantity;
        return true;
    }
    ...
    ...
    ...
}

This can result in potential for a pile in the situation of:
[x] Represents a pileBlock, X being its stackSize.

[1]
[16][4]

1 gets added to the tall pile

[2]
[16][4]

This can potentially collapse resulting in

[0]
[16][6]

For 0 and -1 stack sizes the pile is rendered but unselectable directly, only through its belowPile, if the pile gets interacted with to remove an item from it it will get replaced by an air block upon reaching a stackSize of 0.

However in an instance of managing to get a stackSize of -1, a player could accidentally engineer a situation where adding 1 item to its stack (thus creating a temporary stackSize of 0) and then resulting in a collapse situation would turn that pile into a stackSize of -2, its Layer property becoming -1, and thus resulting in a crash upon trying to use the above methods.

[3]
[16][4]

Player removes 2 from tall stack

[1]
[16][4]

Collapse can occur

[-1]
[16][6]

If Player attempts to add 1 item to tall stack, another collapse could occur, increased likelihood by smaller stacks surrounding
For example purpose, suppose player has already removed 2 from smaller stack thus increasing chance of collapse

[0]
[16][4]

Collapse occurs

[-2]
[16][6]

Resultant Layers property becomes -1 (stackSize / 2) thus crashing the game when used to retrieve CollisionBoxesByFillLevel at that index

Screenshots

Repeatable setup (with some Rng hassle due to PartialCollapse rand) to generate the crash.

Initial setup
BasicSetupPart1

Attempt to create a 3 size pile on top of the 16, this may take a few goes due to early collapse
BasicSetUpPart2

Right click the base 16 pile to remove 2 from the 3 size pile. Resulting in a bugged out -1 size Pile after it collapses.
BasicSetupUpPart3

Once in this situation attempting to add 1 to the 16 pile and getting a collapse to occur will crash the game with a log around the lines of what is attached

Logs

Running on 64 bit Windows 10.0.22621.0 with 31895 MB RAM
Game Version: v1.19.0-rc.1 (Unstable)
12/23/2023 21:50:06: Critical error occurred
Loaded Mods: game@1.19.0-rc.1, creative@1.19.0-rc.1, survival@1.19.0-rc.1
System.IndexOutOfRangeException: Index was outside the bounds of the array.
at Vintagestory.GameContent.BlockCoalPile.GetSelectionBoxes(IBlockAccessor blockAccessor, BlockPos pos) in VSSurvivalMod\Block\BlockCoalPile.cs:line 124
at Vintagestory.Client.NoObf.ClientMain.GetBlockIntersectionBoxes(BlockPos pos) in VintagestoryLib\Client\ClientMain.cs:line 3045
at Vintagestory.API.MathTools.AABBIntersectionTest.RayIntersectsBlockSelectionBox(BlockPos pos, BlockFilter filter) in VintagestoryApi\Math\AABBIntersectionTest.cs:line 117
at Vintagestory.API.MathTools.AABBIntersectionTest.GetSelectedBlock(Single maxDistance, BlockFilter filter) in VintagestoryApi\Math\AABBIntersectionTest.cs:line 102
at Vintagestory.Common.GameMain.RayTraceForSelection(IWorldIntersectionSupplier supplier, Ray ray, BlockSelection& blockSelection, EntitySelection& entitySelection, BlockFilter bfilter, EntityFilter efilter) in VintagestoryLib\Common\GameMain.cs:line 190
at Vintagestory.Common.GameMain.RayTraceForSelection(IPlayer player, BlockSelection& blockSelection, EntitySelection& entitySelection, BlockFilter bfilter, EntityFilter efilter) in VintagestoryLib\Common\GameMain.cs:line 197
at Vintagestory.Client.NoObf.SystemMouseInWorldInteractions.UpdateCurrentSelection() in VintagestoryLib\Client\Systems\Player\MouseInWorldInteractions.cs:line 380
at Vintagestory.Client.NoObf.SystemMouseInWorldInteractions.UpdatePicking(Single dt) in VintagestoryLib\Client\Systems\Player\MouseInWorldInteractions.cs:line 232
at Vintagestory.Client.NoObf.SystemMouseInWorldInteractions.OnFinalizeFrame(Single dt) in VintagestoryLib\Client\Systems\Player\MouseInWorldInteractions.cs:line 96
at Vintagestory.Client.NoObf.ClientEventManager.TriggerRenderStage(EnumRenderStage stage, Single dt) in VintagestoryLib\Client\Util\ClientEventManager.cs:line 185
at Vintagestory.Client.NoObf.ClientMain.TriggerRenderStage(EnumRenderStage stage, Single dt) in VintagestoryLib\Client\ClientMain.cs:line 802
at Vintagestory.Client.NoObf.ClientMain.RenderToDefaultFramebuffer(Single dt) in VintagestoryLib\Client\ClientMain.cs:line 1006
at Vintagestory.Client.ScreenManager.Render(Single dt) in VintagestoryLib\Client\ScreenManager.cs:line 674
at Vintagestory.Client.ScreenManager.OnNewFrame(Single dt) in VintagestoryLib\Client\ScreenManager.cs:line 649
at Vintagestory.Client.NoObf.ClientPlatformWindows.window_RenderFrame(FrameEventArgs e) in VintagestoryLib\Client\ClientPlatform\GameWindow.cs:line 78
at OpenTK.Windowing.Desktop.GameWindow.Run()
at Vintagestory.Client.ClientProgram.Start(ClientProgramArgs args, String[] rawArgs) in VintagestoryLib\Client\ClientProgram.cs:line 317
at Vintagestory.Client.ClientProgram.<>c__DisplayClass9_0.<.ctor>b__1() in VintagestoryLib\Client\ClientProgram.cs:line 128
at Vintagestory.ClientNative.CrashReporter.Start(ThreadStart start) in VintagestoryLib\Client\ClientPlatform\ClientNative\CrashReporter.cs:line 93

Event Log entries for Vintagestory.exe, the latest 1

{ TimeGenerated = 12/23/2023 19:19:21, Site = , Source = Application Error, Message = Faulting application name: Vintagestory.exe, version: 1.19.0.0, time stamp: 0x65310000
Faulting module name: coreclr.dll, version: 7.0.1023.36312, time stamp: 0x64b06d6c
Exception code: 0xc0000005
Fault offset: 0x00000000001c98a6
Faulting process id: 0x0xf064
Faulting application start time: 0x0x1da35fed0439ca6
Faulting application path: C:\Vintage Story\Vintagestory.exe
Faulting module path: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.10\coreclr.dll
Report Id: 2c151dd1-9eed-493d-b91d-0529e18a2e54
Faulting package full name:
Faulting package-relative application ID: }

@Pursec Pursec added the status: new This issue is fresh! label Dec 24, 2023
@Pursec
Copy link
Author

Pursec commented Dec 24, 2023

I believe this is the same crash/issue that has been reported a few times such as in: #3261 (comment)
#1776 (comment)

@RedramVS RedramVS added the department: code Issues apparently caused by code label Dec 25, 2023
@Pursec
Copy link
Author

Pursec commented Dec 25, 2023

Found a few additional Pile related bugs and their causes (and some potential fixes). These are related to the behavior of piles breaking as they are being built and a discrepancy on the size of a pile between the server and client. However are a bit of doozy to explain.

When constructing a pile and ending up in any format of

[16] (full pile)

[16]+2 (player seeks to add 2 to base)

X (pile temp created then "collapses" immediately
[16][2]

This looks like intended behavior at first, however, that pile above does not collapse, it breaks. As such it leaves behind the a drop amount based on its fill, in essence duplicating some portion of its items.

What is actually happening is the base pile [16] attempts to be filled by player interaction, is full so creates a new pile object above itself and hands off the TryPutItem into it creating a temp situation of:

[2]
[16]

Then it runs a TriggerPileChanged() but on the base [16] pile this in turn can end up creating (on the server at least) a very temp pile of

[2]
[14][2]

as 2 is removed from the base pile, creates a EntityFallingBlock above one of the horizontalface positions, it leaves the original pile at 14 and creates a blockEntity neighboring the small pile ontop.

[2] [EntityFallingBlock]
[14]

The EntityFallingBlock then naturally falls, removes the block from the initialPosition and triggers OnNeighbourBlockChanged() on its neighbors, this including the small [2] size pile.
At the time of triggering, on the server, the [2] size pile sits upon a [14] size pile.

Having a gander at BlockCoalPile.OnNeighbourBlockChanged() we see:

public override void OnNeighbourBlockChange(IWorldAccessor world, BlockPos pos, BlockPos neibpos)
{
    Block belowBlock = world.BlockAccessor.GetBlock(pos.DownCopy());
    if (!belowBlock.CanAttachBlockAt(world.BlockAccessor, this, pos.DownCopy(), BlockFacing.UP))
    {
        world.BlockAccessor.BreakBlock(pos, null);
        return;
    }
    ...
}

The size [2] pile checks whether or not it can be attached to the block below itself, if it cannot, it breaks itself.

CanAttachBlockAt() for BlockCoalPile is as such:

public override bool CanAttachBlockAt(IBlockAccessor blockAccessor, Block block, BlockPos pos, BlockFacing blockFace, Cuboidi attachmentArea = null)
{
    BlockEntityCoalPile be = blockAccessor.GetBlockEntity(pos) as BlockEntityCoalPile;
    if (be != null)
    {
        return be.OwnStackSize == be.MaxStackSize;
    }

    return base.CanAttachBlockAt(blockAccessor, block, pos, blockFace, attachmentArea);
}

The belowBlock is size 14, its MaxStackSize is 16. 14 == 16 ->False
As such the 2 size pile breaks itself and creates appropriate drops.

The temp situation is now:

[14][2]

This is the final situation, with one caveat. This is the situation as seen by the server. The code for TriggerPileChanged, which consequently calls TryPartialCollapse() which reduces the pile size and creates the EntityFallingBlock, only executes its block when called by the server, the client gets just an empty return.

void TriggerPileChanged()
{
    if (Api.Side != EnumAppSide.Server) return;
    ...
   foreach (var face in BlockFacing.HORIZONTALS)
   {
        ...
        if (Api.World.Rand.NextDouble() < collapsibleLayerCount / (float)maxSteepness)
        {
            if (TryPartialCollapse(npos.UpCopy(), 2)) return;
        }
    }
}

This creates a discrepancy as on the clients end, the base pile never became [14]. As for the exact degree of why or when the discrepancy is resolved so that it becomes [16] again on the server im still wrapping my head around, presumably after OnPlayerInteract() on the BECoalPile resolves and outside the exact scope of the Piles themselves.

You can confirm this behaivor by checking via debug statements the size of a pile when OnPlayerInteract() is called, after the base.OnPlayerInteract() is called, and after TriggerPileChanged() is called. And checking the size of pile and the pile below it when OnNeighbourBlockChanged() is called and passes the if-statement. Below in an example of done with debug statements inserted at the start of OnPlayerInteract(), after base.OnPlayerInteract() (in my case it was with a supplemental version within the same BlockEntityPile class I created but an exact copy of BlockEntityItemPile's, this was probably not needed just a fugue state coding moment), and after TriggerPileChanged().

client-debug.txt
24.12.2023 21:01:15 [Debug] Size before Interact: 16 //Adding 2 to a fullPile with a 1 size pile above it
24.12.2023 21:01:15 [Debug] Size before Interact: 1 //First pile had pile above it so called OnPlayerInteract on that pile
24.12.2023 21:01:15 [Debug] Size after interact: 3 // TryPutItem() suceeded in add 2. Now is a 2 tall pile of [3]/[16]
24.12.2023 21:01:15 [Debug] Pilesize after Trigger | 3 //TriggerPileChanged() did nothing as this is the client.
24.12.2023 21:01:15 [Debug] Size after interact: 16 //second piles OnPlayerInteract concluded and so continues the first piles call
24.12.2023 21:01:15 [Debug] Pilesize after Trigger | 16 //TriggerPileChanged(), no result as is called by Client
24.12.2023 21:01:22 [Debug] Size before Interact: 16 //Add 2 to first pile again
24.12.2023 21:01:22 [Debug] Size before Interact: 3 //First pile had pile above itself, calls OnPlayerInteract on it
24.12.2023 21:01:22 [Debug] Size after interact: 5 //Added 2 to 2nd pile
24.12.2023 21:01:22 [Debug] Pilesize after Trigger | 5 //TriggerPileChanged() called on 2nd Pile, no change as run on Client
24.12.2023 21:01:22 [Debug] Size after interact: 16 //2nd Pile OnPlayerInteract concluded, continue with 1st
24.12.2023 21:01:22 [Debug] Pilesize after Trigger | 16 //TriggerPileChanged(), no result, called on Client


server-debug.txt
24.12.2023 21:01:15 [Debug] Size before Interact: 16 //Adding 2 to fullpile
24.12.2023 21:01:15 [Debug] Size before Interact: 1 //First pile had pile above
24.12.2023 21:01:15 [Debug] Size after interact: 3 //TryPutItem suceeded
24.12.2023 21:01:15 [Debug] Pilesize after Trigger | 3 //Although called on server, this just got lucky enough to not create a new sub pile, surrounding piles were present and around size 4 at this time, thus collapse was not absolute.
24.12.2023 21:01:15 [Debug] Size after interact: 16 //Second piles OnPlayerInteract concluded
24.12.2023 21:01:15 [Debug] Pilesize after Trigger | 16 //Collapse did not occur on first pile either
24.12.2023 21:01:22 [Debug] Size before Interact: 16 //Add 2 to first pile again
24.12.2023 21:01:22 [Debug] Size before Interact: 3 //First pile had pile above, call OnPlayerInteract() on that pile
24.12.2023 21:01:22 [Debug] Size after interact: 5 //Adds 2 to 2nd pile
24.12.2023 21:01:22 [Debug] Pilesize after Trigger | 3 // SECOND PILE COLLAPSES after running TriggerPileChanged(), reducing its size back down to 3. THIS IS NOT REFLECTED IN THE CLIENT
24.12.2023 21:01:22 [Debug] Size after interact: 16 //2nd Pile OnPlayerInteract concluded, continue with 1st
24.12.2023 21:01:22 [Debug] Pilesize after Trigger | 16 //TriggerPileChanged(), no collapse on 1st pile.

The blockbreaking behaivor can really easily be seen in game, just by watching closely when making piles in creative or by engineering a few different structural examples:

[16]
[16] --------x32 (32 misc items dropped)
[16] + 2 -> [16][2]

[n<16] (any size below 16)------- xN
[16] +2 -------------------------> [16][2].

In approaching the problem to "fix" it I think one of the easiest "solutions" is to not allow TriggerPileChanged() to occur on anything but the top most of any given "stack" of BlockPile entities, thus in the event of a server-client discrepancy or order of operations a pile cannot accidentally break all piles above it, as it is the topmost pile. This can be done in a few ways, recursively checking whether a pile attempting to call or would call TriggerPileChanged() as part of onPlayerInteract() has another pile above it, and executing it on that topmost pile, or as simple as not searching but just "if !aboveBlock is BlockEntityItemPile)" comes to mind and is something im still testing out personally.

This change also makes pile laying and the collapse events more seamless and intuitive as if a player creates a large perfect stack of piles say 1 wide 5 tall (for whatever reason), a collapse would make the entire pile "shrink" and splurge out, rather than snap at the point of interaction from the bug and (at least to my view of the implementation) is more true to how piles were intended to be/made to collapse, always checking its relation to not just its neighbors but whether it is apart of a larger macro-pile (see TriggerPileChanged checking for a below pile to help weight the collapse chance). I also dont see much reason to ever consider a collapse event occurring, so long as there is a pile above. If a new pile is created above from an interaction, a collapse should start from that new pile, perhaps for sake of aesthetics and to make piles keep a similar degree of tendency to"sploot", a collapsing pile can "underflow" to its below parent, taking out some of its stackSize as well if it does not have enough by itself to collapse meaningfully or without error. That however would denote a whole slew of design changes to onTriggerCollapse() to consider.

On a final note, I am unsure whether or not this is a bug or not or even just a personal circumstance, but I (think?) piles when falling are meant to have a rendered entity (showing the collapse in motion), appearing as a 2 Layer pile when rendered falling through the air to its final position, however I have not been able to visually see this in game, not with CoalPiles, my own custom piles, or even StonePiles which uses the same method for making the EntityFallingBlock and shares an idential OnTesselation() with VS and the one in my mod (it being a direct copy of VS's). Its a potential bug for another time and comment however.

Apologies as well for the verboseness of these, this ended up being a far deeper rabbit hole in trying to fix one crash in my mod than I thought it would be and has sorta consumed my last couple days as a hyper-fixation.

@Craluminum2413 Craluminum2413 added this to the 1.19.0 milestone Dec 28, 2023
@Craluminum2413 Craluminum2413 added the version: 1.19 Issues introduced in 1.19 label Dec 31, 2023
@tyronx
Copy link
Contributor

tyronx commented Jan 1, 2024

hi thanks a lot for the thorough report, but yes this is indeed a bit too verbose to me. I'll fix the stack sizing bug in your first post, but if there are more bugs, please open new issues for these

Will be in rc5

@tyronx tyronx closed this as completed Jan 1, 2024
@Craluminum2413 Craluminum2413 added status: resolved Job's done! and removed status: new This issue is fresh! labels Jan 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
department: code Issues apparently caused by code status: resolved Job's done! version: 1.19 Issues introduced in 1.19
Projects
None yet
Development

No branches or pull requests

4 participants