Conversation
Pixnop
left a comment
There was a problem hiding this comment.
The fix works and #282 is closed by it: the branch builds, the suite is green at 22/22 here, both patches apply to a pristine baseline, hunk counters match and every hunk carries a marker. Three things need to change first, and they are all the same question: what happens to the EnumHandling a behavior wrote by ref just before it threw.
1. OnBlockBrokenWith now drops a behavior's veto (patch lines 19-27, generated Collectible.cs:730-736). The if (handled != PassThrough) block and the if (handled == PreventSubsequent) return result; line moved inside the try. The behavior writes handled before it throws, so the throw skips both checks: preventDefault stays false, the loop carries on, and control reaches the vanilla default at the bottom. A protection behavior that sets PreventSubsequent to veto a break and then throws now lets the block break, where before the player was kicked and the block stayed. Move the two checks below the catch. Your second hunk already does it that way, which is what makes the asymmetry stand out.
2. WalkBehaviors keeps the handling the throwing behavior wrote (patch lines 46-58, generated Collectible.cs:3569-3580). The catch swallows the exception, then if (handling == PreventDefault) executeDefault = false; runs on that stale value. For DamageItem the default action is the entire durability update, so a behavior that sets PreventDefault and then throws makes the tool take no damage at all, silently, on every break from then on. That is the exact shape of #282: Toolsmith's OnDamageItem has to set PreventDefault, otherwise vanilla durability would stack on top of its own tinkering. The description says the catch lets "item damage reduction and slot clearing on zero durability complete cleanly", and for the case the issue is about it does the opposite. Reset handling = EnumHandling.PassThrough in the catch, or decide the other way and say so in the marker.
3. The server fallback re-breaks from inside the catch (ServerSystemBlockSimulation patch lines 443-451, generated :883-891). Two separate problems. The retry has no guard of its own, so when the original exception is deterministic and came from that same path, an empty hand on a block whose BlockBehavior.OnBlockBroken throws for instance, and that loop is not one this PR wraps, the second call throws the same exception, it escapes TryModifyBlockInWorld and the player is kicked anyway, this time after the drops already spawned. And the guard GetBlock(pos).BlockId != 0 reads the Default layer, which returns the fluid layer when the solid layer is empty. Break sand under still water with a collectible path that throws after SpawnDropsAndRemoveBlock has run: the solid layer is air, Default hands back the water, the fallback calls OnBlockBroken a second time and the drops are duplicated.
Not blocking, but worth the same pass:
- Hunk 3 has no coverage at all. Revert only that try/catch, rebuild, run the two new scenarios: still 2/2 green, because the two Collectible catches swallow everything before it gets there. Reaching it needs a throwing
BlockBehavioror a throwingGetDrops. - Neither faulty behavior touches
bhHandlingbefore throwing, so both scenarios exercise the one case where the default still runs, and neither reads the pickaxe afterwards. A scenario that setsbhHandling = PreventDefaultbefore the throw and assertsGetRemainingDurabilityactually dropped would have caught point 2. - The behaviors are appended to
pickaxe.CollectibleBehaviorson the shared registry object and never removed. Both scenarios share one server, so whichever runs second carries the first one's faulty behavior too. Restore the array in a finally. isDedicatedField?.SetValue(server, true)quietly does nothing the day that compiler-generated field name changes, and both scenarios then pass with all three safeguards reverted.Assert.NotNullon the FieldInfo is one line.- Logging is unbounded now. The player is no longer kicked, so a deterministic mod exception repeats on every break, two
Logger.Errorcalls with a full stack trace each time. Someone mining at five blocks a second writes ten traces a second for as long as they keep going. Log once per behavior type and collectible, or rate limit. api?.Logger?.Error(...)is a silent swallow wheneverapiis null, and it is only set byOnLoadedNative.OnBlockBrokenWithhasworldin scope and can log throughworld.Logger.
One design question, asked as a question rather than a condition. ServerMain.DispatchClientPacket_mainthread is already ours and is already the single place where a packet-handling exception decides whether to kick. A policy there would be one hunk in code we own, rather than three inside vanilla method bodies, two of them in the vsapi fork that every version bump has to carry forward. What pushed you towards the three?
1a6de23 to
f85a9c5
Compare
|
Pixnop, I addressed your requested changes in Verification on the rebased branch: |
|
Independent verification pass on I re-derived the generated code rather than reading the patch file, since that is what actually runs. Applied both patches to a clean Blocker 1 is closed. Blocker 2 is closed. Blocker 3 is closed, both halves. The non-blocking items are in as well: Gate on my side, at On the design question, which deserves a real answer rather than a gesture at the alternative. A policy in
The other cost is portability. Two of the three live in the vsapi fork, and a vsapi hunk has to survive every version bump with the counter arithmetic intact, while a
Re-requesting review. |
|
Linux verification, since the target platform is a server and I had only tested on Windows. Bootstrapped Stratum from scratch in WSL 26.04 on ext4: decompiled I re-read the generated code on Linux rather than trusting the Windows bootstrap, since the two come from separate decompiles. The full suite is 29 of 29 on Linux. On Windows I got 28 of 29, with Re-requesting review. The two scenarios I would still like a second pair of eyes on are unchanged: |
|
The fixes from the review are on the current head, f85a9c5. The recorded verification for this head includes a clean Release build, 29 passing Atlas scenarios, and a passing smoke run with 21 console probes. The eight focused item-break scenarios cover the veto, durability, fallback, and fluid-layer cases; the shared behavior fixtures are restored, and logging is rate-limited. I have refreshed Pixnop's review request and would appreciate a re-review of this head. |
f85a9c5 to
81db480
Compare
|
Pixnop's requested blocking fixes and follow-ups are on Blocking fixes
Non-blocking follow-ups
Design question Verification on this head
Please review the new head, |
ae05c88 to
13a6864
Compare
|
Pixnop, I rebased On this head, bootstrap applied every patch. Both Release builds succeeded with zero errors. The focused item-break scenarios passed 8/8, the full Atlas suite passed 57/57, and Please re-review head |
Pixnop
left a comment
There was a problem hiding this comment.
All nine items from the last round are handled, and I checked them by reverting each fix: the handling checks sit below the catch, a failed handling resets to PassThrough, the fallback has its own catch and reads the solid layer, the scenarios set bhHandling, assert durability and restore the behavior arrays, and the reflection lookup is asserted. 57/57 here, the eight new scenarios included. Three things remain, all in the fallback and the log cap.
1. A break that fails twice still returns true. When the first attempt throws and the fallback throws too, the inner catch logs and execution falls through to BlocksRemoved++, DirtyForSaving and return true. HandleBlockPlaceOrBreak only reverts on false; on true it runs TriggerNeighbourBlocksUpdate and DidBreakBlock. The client had already removed the block locally before sending the packet, so the player sees air where the server and everyone else still have a block, until a relog, and DidBreakBlock fires for a block that is still there. Your own ContainRepeatedBlockBehaviorException shows it: the block is still granite at the end, and a counter on DidBreakBlock reads 2. Returning false after a failed fallback lets the existing revert path resync the client.
2. The retry guard is "solid layer not empty", not "this block is still here". Several vanilla blocks end a successful break by placing another solid block at the same position: tall grass cut with a knife, reeds, layered slow-dig blocks, TransformBreak. If something throws after the block's own OnBlockBroken returned, the guard sees a solid block and runs block4.OnBlockBroken again with the stale block4, so the drops spawn twice. Comparing the id at the position with block4.Id closes it.
3. The log-once keys leave out what tells failures apart, and never expire. Server side the key is the held item code (the block code with an empty hand), the exception type and the fallback flag. With a tool in hand the block is in neither the key nor the message, so a NullReferenceException in one mod's block hides the same exception type in another mod's block for the rest of the run. Collectible side the key is the behavior type alone, shared by OnBlockBrokenWith and the 22 WalkBehaviors callers, and the one line written says "during WalkBehaviors" whatever the callback was. Add the block code and the callback name to the keys, or use a time window per key with a suppressed count.
Tests, in the same pass if you can:
- All eight scenarios run with a Creative player, and Creative skips
GetDropsand the drop spawn, so duplicated drops are only covered through invocation counts. One Survival scenario counting spawned item entities would pin point 2. - The Collectible-side cap has no coverage: with its
TryAddremoved, all eight still pass.
Two things for the description. WalkBehaviors backs 22 callbacks, getters included, so all of them now swallow and fall through to the default, not only item damage. And #282's root cause, the Toolsmith exception that does not happen on a vanilla server, is contained here, not explained. I would keep that part open in an issue of its own.
13a6864 to
296458e
Compare
|
Pixnop's latest findings are addressed in commit 296458e.
Validation on the rebased implementation: pinned ILSpy bootstrap applied every patch; embedded and unembedded Release builds passed; ItemBreakResilienceScenarios passed 11/11. The smoke test reached WorldReady before final test-only edits. The only diff-check findings are blank context lines required by the nested unified patch files. Please review the new head. |
296458e to
c9a9fd9
Compare
|
Rebased and pushed current head c9a9fd9 onto indev c5c3aee after #347 merged. The latest run applied all patches, passed both Release builds, passed ItemBreakResilienceScenarios 11/11, and reached WorldReady in the smoke test. GitHub reports no checks for this branch. Please review the current head. |
Pixnop
left a comment
There was a problem hiding this comment.
All three points from the last round are fixed, and each fix has a scenario that goes red when I revert it: a double failure now returns false and DidBreakBlock stays silent, the retry guard compares block ids, and the log keys carry the block and the callback. The veto return in ae9add5 and the restored two-parameter WalkBehaviors are good catches too. 61/61 here. Three things before merge, then a few notes.
1. BlockBreak_Should_NotRetryOldBlockOrDuplicateDropsAfterReplacement is flaky. It failed in 6 of 33 runs of the class here, always at line 361 ("must not spawn its drops twice"). In every failure the item count went down, no new entity id appeared and the behavior ran once. The Survival player stands 1.58 blocks from the block centre, the pickup radius is 1.5, and the random spawn velocity sometimes carries a stone into range during the 5 ticks. So the scenario reports a duplicate when a stone was picked up, and a pickup could just as well hide a real duplicate. Moving the player a few blocks away, or counting spawns with OnEntitySpawn instead of live entities, fixes both.
2. The retry guard reads the solid layer, but block4 can come from the fluid layer. Lake ice and glacier ice (BlockLakeIce: fluid layer, solid sides) are taken from the fluid layer at the top of TryModifyBlockInWorld, so the guard at patch line 456 is always false for them. If the first attempt throws before the ice is removed, there is no fallback, nothing is left to throw, and the method returns true: the point 1 symptom again, a desynced client and a DidBreakBlock for a block that is still there. Reading the layer block4 was taken from closes it.
3. Before OnLoadedNative, a behavior exception now disappears without a trace. WalkBehaviorsCore logs through api?.Logger, and api is only set in OnLoadedNative, which the server reaches after the mod handler has run. A mod that calls GetMaxDurability, GetMiningSpeed and the like from AssetsLoaded, AssetsFinalize or StartServerSide while a behavior throws gets the default value and no log line anywhere. Vanilla let that exception reach the mod loader. Rethrowing when there is no logger keeps the old behavior for that window.
Notes, not blocking:
- The retry replays
Block.OnBlockBrokenfrom the top, so behaviors ahead of the thrower run twice per packet. A behavior that spawns the drops and then throws, with the same block id still in place, passes the guard and gets its drops a second time: a throwaway scenario showed it in 3 of 3 runs. That is the price of retrying, and a sentence in the description would help mod authors. - With a tool in hand, when the block side throws once and the fallback succeeds, the block breaks and the tool takes no damage.
- Since ae9add5 the server acts on the return value of
OnBlockBrokenWith, which vanilla discards. That is right for a real veto, but a mod that removes the block itself and then returns false now skipsBlocksRemoved, the neighbour update and DidBreakBlock. Checking that the block is still there before returning false would cover it. - No scenario pins the block code or the item code in the server-side log key: removing either leaves all of them green.
- On the Collectible side, every call from a mod subclass is logged as "during WalkBehaviors", and
GetTemperatureandAfterGetTemperatureshare one key.
ae9add5 to
818af9b
Compare
|
Thanks for the review. I updated the recovery path and added regressions for the reported cases:
Validation on rebased |
Pixnop
left a comment
There was a problem hiding this comment.
All three points from the last round are fixed, and each fix has a scenario that goes red when I revert it. The Survival scenario now breaks a block three blocks away: I ran the class 35 times on 818af9b and 34 runs were clean. The one bad run died on the chunkdbthread AllOnlinePlayers crash that #352 addresses, not on this scenario. Putting the offset back to +1 brings the flake back in 11 of 30 runs, with the same "must not spawn its drops twice" assertion. StratumGetSelectedBreakBlock gives the initial pick, the veto check and the retry guard one selection rule, and BlockBreak_Should_FallbackWhenSelectedBlockIsInFluidLayer goes red with the old solid-layer guard. WalkBehaviorsCore rethrows when api?.Logger is null, pinned by BlockBreak_Should_RethrowCollectibleBehaviorFailureWhenApiLoggerIsUnavailable. The notes are in too: fallback tool wear (exactly one point), the false return ignored once the block is gone, item and block code pinned in the server log key, and separate temperature keys. Removing any of them turns its scenario red. 66/66 on the full suite, 0 errors, and the same 171 and 4 warnings as indev.
Approving. Two things in the fallback are left, both for a follow-up or a last push here, your call:
-
The fallback catch returns false even when the fallback already broke the block (patch line 478, generated
ServerSystemBlockSimulation.cs:907-911). Since 818af9b the fallback try also runsGetDamagedByandDamageItem(patch 466-471), afterblock4.OnBlockBrokenhas removed the block and spawned the drops. If the wear step throws, the method returns false.HandleBlockPlaceOrBreakthen resyncs the client to air but skipsTriggerNeighbourBlocksUpdate(sand or gravel above stays floating, attached blocks are not re-checked) andDidBreakBlock, andBlocksRemovedand the hotbar broadcast are skipped as well. Behavior exceptions cannot get there becauseWalkBehaviorsCoreswallows them, but aDamageItemorGetDamagedByoverride, a Harmony patch or the default action can. One fault is enough: a modded tool whoseOnBlockBrokenWithoverride calls a throwingDamageItembeforebasethrows before removal, the fallback breaks the block, and the sameDamageItemthrows again. The same throw on a break that needs no fallback returns true with full bookkeeping. Indev kicked the player for any throw here, so this is not worse than before, only inconsistent with the rest of the recovery. It is the veto note from last round again: return false in that catch only whileStratumGetSelectedBreakBlock(blockPos).BlockId == block4.BlockId, as at patch line 445, or give the wear step its own try that only logs. -
A block that fails twice stays, and can be mined again for two sets of drops each time (same catch). Take a block whose break path spawns items, leaves its id in place and then throws every time. Vanilla already has the first half:
SpawnDropsAndRemoveBlockcalls the block entity'sOnBlockBrokenbeforeGetDrops, andBEToolrack(the anvil, forge and helve hammer too) spawns its held stacks without clearing them. Add a modBlockBehavior.GetDropson that block that always throws. Each break packet spawns the tools, throws, passes the id guard, spawns them again, throws, and returns false. The client is reverted, the block and its inventory stay, and the player mines it again at normal speed, with no tool wear and two log lines per item, block and exception type for the whole run. On indev the first throw kicked the player on a dedicated server, so it was one set per reconnect. Returning false here was my request in round 2, so this follows from that choice. It still wants either a brake, for example remembering (player, position, block id) after a failed fallback and skipping the retry or rethrowing to the kick the next time the same key fails twice, or a sentence in the description stating it as an accepted risk.
Smaller:
- The description says "A fallback can invoke collectible callbacks again from the start". The fallback calls
block4.OnBlockBrokendirectly and does not callOnBlockBrokenWithagain, so what repeats is the block side: block behaviors ahead of the thrower, the block entity'sOnBlockBroken,GetDrops. Mod authors will read that sentence, so it should name those. Collectible.cs.patchis not whatscripts/extract-patches.shproduces. TheGetTemperatureandAfterGetTemperaturechange sits in two hand-split hunks (@@ -3324,7and@@ -3332,6, patch lines 326 and 336) wheregit diff -U5emits a single@@ -3324,18 +3352,20.git applytakes it and the generated code is right, but the next extract run on that file will rewrite those hunks in someone else's diff. Re-running extract-patches fixes it.
… exceptions When an external mod behavior throws an unhandled exception during item damage (such as Toolsmith's NRE when an untracked tinkered tool breaks), the exception previously escaped ServerSystemBlockSimulation into DispatchClientPacket_mainthread, causing the dedicated server to forcibly kick the player with an unhandled exception notice while leaving block and inventory state unfinalized. Wrap Collectible.WalkBehaviors and Collectible.OnBlockBrokenWith in try-catch blocks so faulty mod behaviors log their error to the server logger while allowing vanilla default actions (item damage and break handling) to proceed. In addition, wrap TryModifyBlockInWorld item and block break callbacks with a fallback block removal if unhandled exceptions occur, ensuring dedicated servers do not disconnect players when third-party mod item breaking logic fails. Add regression Atlas scenarios covering both failure modes. Fixes #282
818af9b to
4dffb22
Compare
|
Rebased onto indev at a3966e2 to resolve the conflict with PR #353 in ServerSystemBlockSimulation.cs.patch. Validation on commit 4dffb22:
|
Summary
Contains exceptions from mod collectible behavior callbacks during item damage and block breaking so the server can finish or revert the operation safely. This references #282; it does not diagnose or fix Toolsmith's underlying exception.
Collectible.WalkBehaviors covers 22 callback and getter call sites. Each failure is logged with its callback name. When the API logger is unavailable, the exception is rethrown. The protected two-argument WalkBehaviors method remains available for binary compatibility.
Block breaking preserves veto and resync behavior. The fallback checks the same block using the original solid/fluid layer selection, including lake ice in the fluid layer. It retries only while the original selected block remains. If the callback removes or replaces that block before returning false, normal removal bookkeeping and events continue. A successful fallback applies block-breaking tool wear once. If recovery fails, the existing client-revert path can resync state.
A fallback can invoke collectible callbacks again from the start. Mod callbacks that perform side effects before throwing may therefore repeat those side effects during recovery.
Failure logs are rate-limited by item, block, exception type, callback, and fallback path. Regression scenarios cover throwing behaviors, retry and veto guards, fluid-layer ice, fallback tool wear, survival drops after block replacement, callback-specific logs, and log caps.
Type
Validation
Related issues
Refs #282