During the merge operation, it is quite common to be dealing with a node that has no child nodes. When there are no such nodes, we can return early from some functions to avoid allocating new collections that will not be used.
In the MergePartial operation, reuse a dictionary as scratch space when checking for conflicts. We introduce a IntoDictionaryWithConflictLog helper to allow this. This avoids allocating a new dictionary for the conflict log that gets thrown away at each check.
As the expected size is quite small here, providing an explicit buffer size helps as otherwise default buffers for 1024 characters are allocated by the StreamReader which must be cleaned up by the GC afterwards. These smaller buffers still need cleanup but waste less memory.
This allows actor.Info.HasTraitInfo to be used when checking if an actor needs to be added to the index, which is a cheaper call than actor.TraitsImplementing.
The rectangle defined by a frames's Offset+Size usually fits within the rectangle given by FrameSize. However it can sometimes extend outside this region due to padding added by the engine. This causes an array-out-of-bounds crash as we only allocate space to cover the FrameSize region.
Now, we teach the copying process to only copy the portion of the data that lies within the FrameSize region. If any data extends outside the region, it won't be copied.
Provide an explicit size of 512, smaller than the default 2048. The cursors for all mods still pack onto this single sheet, so we avoid wasting memory on larger sheets.
Sorting the cursors also helps pack them onto the sheets more efficiently.
When a move is made where the source and target locations are the same and no actual moving is required, a path of length 0 is returned. When a move cannot be made as there is no valid route, a path of length 0 is also returned. This means Move is unable to tell the difference between no movement required, and no path is possible. Currently it will hit the `hadNoPath` case and report CompleteDestinationBlocked.
To fix the scenario where the source and target location match, track a alreadyAtDestination field. When this scenario triggers, report CompleteDestinationReached instead.
This fixes activities that were using this result to inform their next actions. e.g. MoveOntoAndTurn would previously cancel the Turn portion of the activity, believing that the destination could not be reached. Now, it knows the destination was reached (since we are already there!) and will perform the turn.
Sheets can be buffered - where a copy of their data is kept in RAM. This allows for modifications to the sheet to be batched in the RAM buffer, before later being committed to VRAM in a single operation. The SheetBuilder class allows for sprites to be allocated onto one or more sheets. Each time a sheet is filled, it will allocate a new sheet. Sheets allocated by the sheet builder will typically use the buffering mechanism.
Previously each time the builder allocated a new sheet, the buffer would get thrown away, and the next sheet would allocate a fresh buffer. These buffers can be large and may accumulate before the GC cleans them up. So although only one buffer will be live at a time, they can cause a spike in memory used by the process during loading.
We can avoid this issue by allowing the buffer from the previous sheet to be reused by the next sheet. This is possible because the sheet builder only has one live sheet for modifications at a time, and they are all the same type and size. By allocating only one buffer per builder instead of one per sheet, we reduce the peak memory required during loading.
- In CursorManager, we can release the buffer on the final sheet after loading cursors. We don't need to release the buffer on every sheet in the builder, as the builder will handle that.
- In SpriteCache, we don't need to call CreateBuffer explicitly, as the builder will do that for us.
- In RadarWidget, we don't need to call CreateBuffer explicitly, as GetData will do that for us.
When the process is running, we use a finally block to call Log.Dispose and flush any outstanding logs to disk before the process exits. This works when we handle any exception in a matching catch block.
When the exception is unhandled, then the finally block will not run and instead the process will just exit. To fix this, flush the logs inside a catch block instead before rethrowing the error. This ensures we get logs even when crashing.